java 线程 notify、notifyAll

前言

在 《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: 唤醒一个线程

  1. 当单个线程等待的时候调用它的时候那么之前的单个线程会被唤醒
  2. 如果是多个线程在等待这个对象锁那么就随机唤醒一个线程
Wakes up a single thread that is waiting on this object's monitor.

notifyAll:唤醒所有的线程

  1. notifyAll()只会唤醒那些等待抢占指定object’s monitor的线程,其他线程则不会被唤醒。
  2. notifyAll()只会一个一个的唤醒,而并非统一唤醒。因为在同一时间内,只有一个线程能够持有object’s monitor
  3. notifyAll()只是随机的唤醒线程,并非有序唤醒。
Wakes up all threads that are waiting on this object's monitor

wait:把自己放到了都是等待这个对象锁的等待队列中然后放弃对这个对象锁的所有权,为禁用对象休眠直到下面四事件

  1. notify
  2. notifyAll
  3. 线程中断
  4. 超时
* 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
  1. 执行前 Entry SetWait Set 的状态
    四个线程共同去竞争一个锁。
    在这里插入图片描述
  2. 一开始 Thread-0 获得锁 lock ,time = 1并且调用了 notify 方法,这个时候看一下 Entry SetWait Set
    Thread-0 执行完后释放锁,这时三个线程竞争锁
    在这里插入图片描述
  3. Thread-3 再次获得锁,但是不满足条件,time = 1 调用 wait 方法,将当前线程当如 wait set 中,并且释放锁
    在这里插入图片描述4. Thread-2 获得锁,同时不满足条件,time = 1,调用 wait 方法
    在这里插入图片描述
  4. 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 虽然可以避免死锁,但是比较消耗资源,毕竟每次都要唤醒线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值