前言
在 《Java安全编码标准》中,关于 Java 线程安全,有一小节提到在静态条件下,使用通知 notify ,要使用 notifyAll 通知所有的等待线程而不是通知某一线线程。
英文文档:https://wiki.sei.cmu.edu/confluence/display/java/2+Rules
代码示例:
public class NotifyTest implements Runnable {
private static int time = 0;
private static final Object lock = new Object();
private final int step;
private NotifyTest(int step) {
this.step = step;
}
@Override
public void run() {
try {
synchronized (lock) {
while (time != step) {
System.out.println(Thread.currentThread().getName() + " " + time);
lock.wait();
}
System.out.println(Thread.currentThread().getName() + " " + time);
time++;
lock.notify();
// lock.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupted();
}
}
/**
* 容易造成死锁
*
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
new Thread(new NotifyTest(i)).start();
}
}
}
如代码所示,当调用 lock.notify() 时,违反了活性属性,lock.notify() 每次只会唤醒一个线程,除非正好唤醒下一个步骤的线程,否则就会出现死锁。
看一下运行 lock.notify() 的运行结果:
Thread-0 0
Thread-3 1
Thread-2 1
Thread-1 1
Thread-3 2
出现死锁。
那么为什么会出现死锁的情况呢?
我们看一下 notify 的含义和 notifyAll 的含义
notify: 唤醒一个线程
- 当单个线程等待的时候调用它的时候那么之前的单个线程会被唤醒
- 如果是多个线程在等待这个对象锁那么就随机唤醒一个线程
Wakes up a single thread that is waiting on this object's monitor.
notifyAll:唤醒所有的线程
- notifyAll()只会唤醒那些等待抢占指定object’s monitor的线程,其他线程则不会被唤醒。
- notifyAll()只会一个一个的唤醒,而并非统一唤醒。因为在同一时间内,只有一个线程能够持有object’s monitor
- notifyAll()只是随机的唤醒线程,并非有序唤醒。
Wakes up all threads that are waiting on this object's monitor
wait:把自己放到了都是等待这个对象锁的等待队列中然后放弃对这个对象锁的所有权,为禁用对象休眠直到下面四事件
- notify
- notifyAll
- 线程中断
- 超时
* This method causes the current thread (call it <var>T</var>) to
* place itself in the wait set for this object and then to relinquish
* any and all synchronization claims on this object. Thread <var>T</var>
* becomes disabled for thread scheduling purposes and lies dormant
* until one of four things happens:
* <ul>
* <li>Some other thread invokes the {@code notify} method for this
* object and thread <var>T</var> happens to be arbitrarily chosen as
* the thread to be awakened.
* <li>Some other thread invokes the {@code notifyAll} method for this
* object.
* <li>Some other thread {@linkplain Thread#interrupt() interrupts}
* thread <var>T</var>.
* <li>The specified amount of real time has elapsed, more or less. If
* {@code timeout} is zero, however, then real time is not taken into
* consideration and the thread simply waits until notified.
* </ul>
划重点:对象锁的模型
JVM 会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set。
Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。
Wait Set:当调用 wait 方法时,会将当前线程放入 Wait Set。当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的会放入 Entry Set 中,状态变为 BLOCKED 状态;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为BLOCKED状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。当线程获得锁后,BLOCKED 状态变为 RUNNABLE 状态。
每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。
那么我们分析一下为什么上面的程序会发生死锁?
代码中中总共有四个线程:
Thread-0
Thread-1
Thread-2
Thread-3
执行顺序:
Thread-0 0
Thread-3 1
Thread-2 1
Thread-1 1
Thread-3 2
- 执行前 Entry Set 和 Wait Set 的状态
四个线程共同去竞争一个锁。
- 一开始 Thread-0 获得锁 lock ,time = 1并且调用了 notify 方法,这个时候看一下 Entry Set 和 Wait Set
Thread-0 执行完后释放锁,这时三个线程竞争锁
- Thread-3 再次获得锁,但是不满足条件,time = 1 调用 wait 方法,将当前线程当如 wait set 中,并且释放锁
4. Thread-2 获得锁,同时不满足条件,time = 1,调用 wait 方法
- Thread-1 获得锁,满足条件,time = 2,调用 notify 方法,如果这时调用的 Thread-2 ,那么就不会出现死锁,但是本次示例调用的 Thread-3
Thread-3 不满足条件,调用 wait 状态,并将当前线程放入 wait set 中,如下图
Entry Set 中没有线程,最终导致死锁。
所以,在使用 wait 和 notify 时,最好使用 notifyAll 方法,避免死锁。
总结:
- wait、notify、notifyAll 都是 object 的方法
- wait 等待会释放锁,sleep 不会释放锁
- wait 可以设置超时时间避免死锁
- 使用 notifyAll 虽然可以避免死锁,但是比较消耗资源,毕竟每次都要唤醒线程