背景:
上午我们组里讨论起线程的生命周期,请看第一张图:
我一看就状态流转有不对,于是我提出了两处明显错误:
1、调用wait方法,进入等待阻塞,当notify唤醒后,进入的是锁定阻塞,只有获得锁了,才会进入就绪状态。
2、sleep进入超时阻塞,不是通过notify唤醒的,它根本就没释放锁。
于是,我就在网上找了一张图,来表达我认为的状态转换关系,见下:
为了让大家更明白wait()/notify()的使用以及原理,我们有必要重新翻找资料,梳理出正确的同步机制和线程的生命周期。由于时间关系,部分示例和图形都是来自于网络,如有遗漏了注明转载,还请多多见谅。
一、wait()和notify()使用不当可能导致的死锁
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
/**
* wait()和notify()方法调用示例.
* 期望的同步顺序是先执行线程Producer,再执行线程Consumer。
*
* 2019-06-06T14:01:38.950----thread Producer is waiting to get lock
* 2019-06-06T14:01:38.950----thread Producer get lock
* 2019-06-06T14:01:38.951----thread Consumer is waiting to get lock
* 2019-06-06T14:01:40.951----thread Producer do wait method
* 2019-06-06T14:01:40.951----thread Consumer get lock
* 2019-06-06T14:01:45.951----thread Consumer do notify method
* 2019-06-06T14:01:45.951----thread Producer wait end
*
* 可是实际重复运行多次的结果,却出现顺序是:先执行线程Consumer,再执行线程Producer,从而导致死锁的发生。
*
* 2019-06-06T14:04:08.628----thread Consumer is waiting to get lock
* 2019-06-06T14:04:08.629----thread Consumer get lock
* 2019-06-06T14:04:08.630----thread Producer is waiting to get lock
* 2019-06-06T14:04:13.630----thread Consumer do notify method
* 2019-06-06T14:04:13.630----thread Producer get lock
* 2019-06-06T14:04:15.630----thread Producer do wait method
*
*
*/
public class WaitNotifyCase {
public static void main(String[] args) {
final Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now() + "----" + "thread Producer is waiting to get lock");
synchronized (lock) {
try {
System.out.println(LocalDateTime.now() + "----" + "thread Producer get lock");
TimeUnit.SECONDS.sleep(2);
System.out.println(LocalDateTime.now() + "----" + "thread Producer do wait method");
lock.wait();
System.out.println(LocalDateTime.now() + "----" + "thread Producer wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now() + "----" + "thread Consumer is waiting to get lock");
synchronized (lock) {
System.out.println(LocalDateTime.now() + "----" + "thread Consumer get lock");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
System.out.println(LocalDateTime.now() + "----" + "thread Consumer do notify method");
}
}
}).start();
}
}
二、notify()修改为notifyAll()解决死锁问题
1、生产者
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class Producer implements Runnable {
List<Integer> cache;
public Producer(List<Integer> cache) {
this.cache = cache;
}
@Override
public void run() {
System.out.println(LocalDateTime.now() + "----" + "thread Producer 正在等待生产商品");
synchronized (cache) {
try {
System.out.println(LocalDateTime.now() + "----" + "thread Producer 每秒生产一个商品");
while (cache.size() == 1) {
cache.wait();
}
TimeUnit.SECONDS.sleep(1);
Integer content = new Random().nextInt();
cache.add(content);
System.out.println(LocalDateTime.now() + "----" + "thread Producer 生产内容是:" + content + ",通知消费者可以开始消费");
cache.notify();
System.out.println(LocalDateTime.now() + "----" + "thread Producer 通知消费者完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、消费者
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Consumer implements Runnable {
List<Integer> cache;
public Consumer(List<Integer> cache) {
this.cache = cache;
}
@Override
public void run() {
System.out.println(LocalDateTime.now() + "----" + "thread Consumer 正在等待消费商品");
synchronized (cache) {
System.out.println(LocalDateTime.now() + "----" + "thread Consumer 每秒消费一个商品");
try {
while (cache.isEmpty()) {
cache.wait();
}
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LocalDateTime.now() + "----" + "thread Consumer 消费" + cache.get(0) + ",通知生产者可以开始生产");
cache.remove(0);
cache.notify();
System.out.println(LocalDateTime.now() + "----" + "thread Consumer 通知生产者完成");
}
}
}
3、单个生产者单个消费者
import java.util.ArrayList;
import java.util.List;
/**
* 生产者消费者模型
*/
public class ProducerConsumerCase {
public static void main(String[] args) {
List<Integer> cache = new ArrayList<>();
new Thread(new Producer(cache)).start();
new Thread(new Consumer(cache)).start();
}
}
运行结果:
2019-06-06T14:55:30.155----thread Consumer 正在等待消费商品
2019-06-06T14:55:30.155----thread Producer 正在等待生产商品
2019-06-06T14:55:30.156----thread Consumer 每秒消费一个商品
2019-06-06T14:55:30.156----thread Producer 每秒生产一个商品
2019-06-06T14:55:31.157----thread Producer 生产内容是:1613162517,通知消费者可以开始消费
2019-06-06T14:55:31.157----thread Producer 通知消费者完成
2019-06-06T14:55:32.157----thread Consumer 消费1613162517,通知生产者可以开始生产
2019-06-06T14:55:32.157----thread Consumer 通知生产者完成
4、多个生产者多个消费者
import java.util.ArrayList;
import java.util.List;
/**
* 生产者消费者模型
*/
public class ProducerConsumerCase {
public static void main(String[] args) {
List<Integer> cache = new ArrayList<>();
new Thread(new Producer(cache)).start();
new Thread(new Producer(cache)).start();
new Thread(new Producer(cache)).start();
new Thread(new Consumer(cache)).start();
new Thread(new Consumer(cache)).start();
new Thread(new Consumer(cache)).start();
}
}
为了方便看到死锁的异常情况,修改消费者代码,消费商品的速度由1秒调整为2秒。
运行多次结果:
总结,修改上述代码中的notify()-->notifyAll(),解决死锁问题。
三、synchronized的锁机制
同步关键词,首先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。
下面使用流程图,把synchronized和wait()、notify()/notifyAll()的先后顺序梳理下。
1、无论是WaitThread还是NotifyThread,第一步都必须尝试进入监视器Monitor。
结合上面所说的synchronized的锁机制,可以解释为什么使用 wait() ,notify() 和 notifyAll()时,需要先对调用对象加synchronized锁了。
2、其次、判断Monitor.Enter是否成功。
如果Enter失败,进入同步队列SynchronizedQueue,直至Monitor Exit后,从同步队列中执行出队操作,继续尝试进入监视器Monitor。
如果Enter成功,说明获取到对象Object的锁了。
3、如果此时调用wait()方法,会释放上一步得到的锁,将当前线程放置到对象的等待队列WaitQueue,被移动的线程由Running变为Waiting,直至notify的到来。
4、notify 方法将等待队列的随机一个(HotSpot的实现是第一个)等待线程从等待队列种移到同步队列中,而 notifyAll 方法则是将等待队列种所有的线程全部移到同步队列,被移动的线程状态由 Waiting 变为 Blocked。
5、关于notify/notifyAll有必要补充的是,第一步也是尝试进入监视器Monitor, 当Enter成功后,再执行第4步,最后退出Monitor Exit,NotifyThread 执行完毕,释放锁。
四、分析wait()和notify()的源码
引用自大神的博文,这里就不赘述了。
Java的wait()、notify()学习三部曲之一:JVM源码分析,
Java的wait()、notify()学习三部曲之二:修改JVM源码看参数,
Java的wait()、notify()学习三部曲之三:修改JVM源码控制抢锁顺序,
五、线程的生命周期
至此,可以把最上第一张图订正如下: