Java多线程:线程通信:生产者 和 消费者 || 顺序打印
每博一文案
靠谱,是最高级的聪明:师父说:人生一回,道义一场。你对人对事的态度,藏着你一生的福报。
千金难买好人缘,人活的就是好口碑,别人怎么对你,是你的因果,你怎么对别人,是你的修行。
和谁在一起,决定了你是谁,而你是谁,就拥有怎样的人生。一个人为人处世最大的资本,不是才华也不是金钱,而是
看他靠不靠谱,良心,是做事的底线,人品,是做人的底牌,这世上从不缺优秀的人,难得的是靠谱的人,聪明的人
只适合聊聊天,但靠谱的人值得常伴心间,聪明只能让你走得快,但靠谱却能决定你走得多远。
所以,一个人最好的底牌就是“靠谱” 二字。
—————— 一禅心灵庙语
文章目录
1. 线程通信
1.1 wait(), notify(), notifyAll()等方法介绍
在Object.java中,定义了wait(), notify()和notifyAll()等方法。
wait()
的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。notify()和notifyAll()
的作用,则是唤醒当前对象上的等待线程;notify()
是唤醒单个线程,而notifyAll()
是唤醒所有的线程。- 这 wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报错:
IllegalMonitorStateException
异常 - 这 三个wait(),notify(), notifyAll()方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException
异常 - 如下是图示说明
Object类中关于等待/唤醒的API详细信息如下:
- notify() – 唤醒在此对象监视器上等待的单个线程。
- notifyAll() – 唤醒在此对象监视器上等待的所有线程。
- wait() – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
- wait(long timeout) – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
- wait(long timeout, int nanos) – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。
1.2 为什么notify(), wait()等函数定义在Object中,而不是Thread中
简单的解释的说明: 因为这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized 的同步锁, 因此这三个方法只能在Object类中声明,因为所有的对象/任意对象就继承于 Object 类,才能对应上面的任意对象都可以作为 synchronized 的同步锁。
复杂逻辑解释:
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。
wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!
OK,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。
现在,请思考一个问题: notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。
负责唤醒等待线程的那个线程(我们称为 “唤醒线程” ),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。
总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。
2. 顺序打印
使用两个线程打印 1-100。线程1, 线程2交替打印 : 线程1 打印偶数,线程2打印奇数
package blogs.blog4;
/**
* 使用两个线程打印1-100。线程1, 线程2 交替打印 : 线程1 打印偶数,线程2打印奇数
*/
public class ThreadTest14 {
public static void main(String[] args) {
MyThread14 myThread14 = new MyThread14();
Thread t1 = new Thread(myThread14);
Thread t2 = new Thread(myThread14);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
/**
* 打印偶数
*/
class MyThread14 implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
this.notify(); // 唤醒被 wait()阻塞的线程,打印偶数
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + "--->" + num++);
// 奇数打印完了,该线程进入阻塞状态,让另外一个线程进入,打印偶数
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
- 注意对于这 三个wait(), notify(), notifyAll()方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException
异常
- 这 wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报错:
IllegalMonitorStateException
异常
3. yield() 与 wait()的比较
我们知道,wait() 的作用是让当前线程由 “运行状态” 进入到 “等待(阻塞)状态”的同时,也会释放同步锁 。而 yield() 的作用是让步,它也会让当前线程离开 “运行状态”。它们的区别是:
- wait() 是让线程由 “运行状态” 进入到 “等待(阻塞)状态”,而不是 yield() 是让线程由 “运行状态” 进入到 “就绪状态”
- wait() 是会释放 它所持有的对象的同步锁,而 yield() 方法是不会 释放锁 的。
- 两个方法的声明的位置不同,wait()必须在synchronized同步代码块/同步方法当中,而且调用的对象必须和同步监视器“锁”的对象一致,不然报,yield()任意
- wait 是 Object 类的成员本地方法,sleep 是Thread 类的静态方法。
下面通过示例演示 yield() 是不会释放锁的
package blogs.blog4;
/**
* yield() 与 wait()的比较
*/
public class ThreadTest15 {
public static void main(String[] args) {
MyThread15 myThread15 = new MyThread15();
Thread t1 = new Thread(myThread15);
Thread t2 = new Thread(myThread15);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
class MyThread15 implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
if (i == 5) {
Thread.yield(); // 让当前线程进入 “就绪状态”,但不会释放线程中的锁.
}
}
}
}
}
解析:
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(this)。在t1运行过程中,虽然它会调用Thread.yield() 让当前线程进入 阻塞状态,但是却不会释放手中的锁 ,导致 t2线程无法拿到 “锁” ,就无法进入 synchronized(this) {} 代码块中执行,这里只有等到 t1线程执行完后,释放手中的锁,t2线程才可以进入到 synchronized(this) {} 代码块中执行。所以就出现了上述的 一个线程一直在执行直到运行完。另外一个线程才可以执行。
下面对比演示:wait()是会释放锁的
package blogs.blog4;
/**
* yield() 与 wait()的比较
*/
public class ThreadTest15 {
public static void main(String[] args) {
MyThread15 myThread15 = new MyThread15();
Thread t1 = new Thread(myThread15);
Thread t2 = new Thread(myThread15);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
class MyThread15 implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
if (i == 5) {
try {
this.wait(); // 让当前线程进入 “等待/阻塞状态”,并释放线程中的锁.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify(); // 唤醒被 wait()阻塞的线程
}
}
}
}
4. sleep() 与 wait()的比较
我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。
但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。
- 相同点:一旦执行,都可以使得当前线程进入阻塞状态。以及都需要异常处理。
- 不同点:
- 两个方法的声明的位置不同,两个方法的声明的位置不同,wait()必须在synchronized同步代码块/同步方法当中,而且调用的对象必须和同步监视器“锁”的对象一致,不然报,sleep()任意
- Wait 是 Object 类的成员方法,sleep 是Thread 类的静态本地方法
- sleep()阻塞不会释放手中的同步监视器“锁”,wait()阻塞的同时会释放手中的同步监视器”锁”
下面通过示例演示sleep()是不会释放锁的
package blogs.blog4;
/**
* yield() 与 wait()的比较
*/
public class ThreadTest15 {
public static void main(String[] args) {
MyThread15 myThread15 = new MyThread15();
Thread t1 = new Thread(myThread15);
Thread t2 = new Thread(myThread15);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
class MyThread15 implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
if (i == 5) {
try {
Thread.sleep(1000 * 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
解析:
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(this)。在t1运行过程中,虽然它会调用Thread.sleep(1000 * 1) 让线程睡眠 1 s 进入阻塞状态,但是却不会释放手中的锁 ,导致 t2线程无法拿到 “锁” ,就无法进入 synchronized(this) {} 代码块中执行,这里只有等到 t1线程执行完后,释放手中的锁,t2线程才可以进入到 synchronized(this) {} 代码块中执行。所以就出现了上述的 一个线程一直在执行直到运行完。另外一个线程才可以执行。
下面对比演示:wait()是会释放锁的
package blogs.blog4;
/**
* yield() 与 wait()的比较
*/
public class ThreadTest15 {
public static void main(String[] args) {
MyThread15 myThread15 = new MyThread15();
Thread t1 = new Thread(myThread15);
Thread t2 = new Thread(myThread15);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
class MyThread15 implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
if (i == 5) {
try {
this.wait(); // 让当前线程进入 “等待/阻塞状态”,并释放线程中的锁.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify(); // 唤醒被 wait()阻塞的线程
}
}
}
}
5. 生产者/消费者模式
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产。
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
- 当消费者发现仓储没产品可消费时候会通知生产者生产。
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
- **对于生产方法produce()而言:**当仓库满时,生产者线程等待,需要等待消费者消费产品之后,生产线程才能生产;生产者线程生产完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“消费者线程”,即我们所说的“通知消费者进行消费”。
- **对于消费方法consume()而言:**当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费;消费者线程消费完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“生产者线程”,即我们所说的“通知生产者进行生产”。
这是一种特殊的业务需求,在这种特殊的情况下需要使用 wait()方法和notify()方法/notifyAll() 方法
如下图示说明:
举例:先定义仓库最多只能存放 1 个产品,生产 1 个 产品,就消费 1 个产品
package blogs.blog4;
/**
* 生产者和消费者模型
*/
public class ProductTest {
public static void main(String[] args) {
// 创建仓库的实例对象
Clark clark = new Clark();
// 创建两个线程:生产者/消费者
Thread t1 = new Thread(new Product(clark)); // 生产线程,生产和消费是同一个仓库
Thread t2 = new Thread(new Consumer(clark)); // 消费线程,生产和消费是同一个仓库
// 设置线程名
t1.setName("生产者");
t2.setName("消费者");
// 创建线程
t1.start();
t2.start();
}
}
/**
* 仓库
*/
class Clark {
private int productCount = 0;
public Clark() {
}
/**
* 生产产品
*/
public synchronized void produceProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
// 仓库最多存放 1 个产品,仓库没有满生产
if (productCount < 1) {
// 生产 ++
this.productCount++;
System.out.println(Thread.currentThread().getName() + "-->开始生产第" + this.productCount + "个产品");
// 生产完了,打开消费线程,消费
this.notifyAll(); // 释放被 wait()阻塞的线程,优先高的优先
} else {
// 仓库满了,停止生产,停止生产线程 wait() 进入阻塞状态,并释放手中的锁。
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 消费产品
*/
public synchronized void consumeProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
// 仓库中有产品,才可以消费
if (this.productCount > 0) {
// 消费
System.out.println(Thread.currentThread().getName() + "-->开始消费第" + this.productCount + "个产品");
this.productCount--;
// 消费完了,打开生产线程,生产
this.notifyAll();
} else {
// 仓库没有产品,停止消费,该消费线程进入 阻塞状态 wait(),并释放手中的锁
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
class Product implements Runnable {
// 定义仓库
private Clark clark = null;
public Product() {
super(); // 调用父类中的构造器
}
public Product(Clark clark) {
super(); // 调用父类中的构造器,必须首行(一次)
this.clark = clark;
}
/**
* 生产产品
*/
@Override
public void run() {
while (true) {
this.clark.produceProduct();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private Clark clark = null;
public Consumer() {
super(); // 调用父类的构造器,首行(一次)
}
public Consumer(Clark clark) {
super(); // 调用父类的构造器,首行(一行)
this.clark = clark;
}
/**
* 消费
*/
@Override
public void run() {
while (true) {
this.clark.consumeProduct();
}
}
}
举例:定义仓库最多只能存放 20 个产品,生产 20 个 产品,就消费 20 个产品
只需要将: 生产的仓库 if 判断修改一下就好了。
package blogs.blog4;
/**
* 生产者和消费者模型
*/
public class ProductTest {
public static void main(String[] args) {
// 创建仓库的实例对象
Clark clark = new Clark();
// 创建两个线程:生产者/消费者
Thread t1 = new Thread(new Product(clark)); // 生产线程,生产和消费是同一个仓库
Thread t2 = new Thread(new Consumer(clark)); // 消费线程,生产和消费是同一个仓库
// 设置线程名
t1.setName("生产者");
t2.setName("消费者");
// 创建线程
t1.start();
t2.start();
}
}
/**
* 仓库
*/
class Clark {
private int productCount = 0;
public Clark() {
}
/**
* 生产产品
*/
public synchronized void produceProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
// 仓库最多存放 1 个产品,仓库没有满生产
if (productCount < 10) {
// 生产 ++
this.productCount++;
System.out.println(Thread.currentThread().getName() + "-->开始生产第" + this.productCount + "个产品");
// 生产完了,打开消费线程,消费
this.notifyAll(); // 释放被 wait()阻塞的线程,优先高的优先
} else {
// 仓库满了,停止生产,停止生产线程 wait() 进入阻塞状态,并释放手中的锁。
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 消费产品
*/
public synchronized void consumeProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
// 仓库中有产品,才可以消费
if (this.productCount > 0) {
// 消费
System.out.println(Thread.currentThread().getName() + "-->开始消费第" + this.productCount + "个产品");
this.productCount--;
// 消费完了,打开生产线程,生产
this.notifyAll();
} else {
// 仓库没有产品,停止消费,该消费线程进入 阻塞状态 wait(),并释放手中的锁
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
class Product implements Runnable {
// 定义仓库
private Clark clark = null;
public Product() {
super(); // 调用父类中的构造器
}
public Product(Clark clark) {
super(); // 调用父类中的构造器,必须首行(一次)
this.clark = clark;
}
/**
* 生产产品
*/
@Override
public void run() {
while (true) {
this.clark.produceProduct();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private Clark clark = null;
public Consumer() {
super(); // 调用父类的构造器,首行(一次)
}
public Consumer(Clark clark) {
super(); // 调用父类的构造器,首行(一行)
this.clark = clark;
}
/**
* 消费
*/
@Override
public void run() {
while (true) {
this.clark.consumeProduct();
}
}
}
4. 总结:
- wait() ,notify() ,notifyAll() 方法是定义在 Object 类当中的。
- wait() 是让当前线程进入阻塞状态,并释放当前线程占用的同步锁,notify() 唤醒被 wait() 阻塞的线程,优先级高的优先唤醒,仅仅只会唤醒一个线程,notifyAll() 唤醒被 wait() 阻塞的所有线程。
- wait() ,notify() ,notifyAll() 方法必须在 synchronized 代码块/方法中使用不然报
java.lang.IllegalMonitorStateException
异常。 - wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报:
IllegalMonitorStateException
异常。 - 注意 yield() 与 wait() 的区别: yield () 让线程进入 ”就绪状态“,但是不会释放线程中的”锁“,wait() 让线程进入 ”阻塞状态“,并释放线程中的”锁“。
- 注意 sleep() 与 wait() 的区别:sleep() 让线程进入”等待/阻塞状态“,但是不会释放线程中的”锁“,wait() 让线程进入 ”阻塞状态“,并释放线程中的”锁“。
- 编写 生产者 / 消费者模型:达到 生产 和 消费的一个平衡。
5. 最后:
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见 !!!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/82986.html