并发编程大师系列之:wait/notify/notifyAll/condition

/ 并发编程 / 0 条评论 / 303人围观
  1. wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
  2. 调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)。
  3. 调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程。
  4. 调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程。
  5. 如果调用某个对象的wait()方法,当前线程必须拥有这个对象的锁,因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。
  6. 调用某个对象的wait()方法,相当于让当前线程交出此对象的锁,然后进入等待状态,等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁);
  7. notify()方法能够唤醒一个正在等待该对象锁的线程,当有多个线程都在等待该对象的锁的话,则只能唤醒其中一个线程,具体唤醒哪个线程由虚拟机确定。
  8. nofityAll()方法能够唤醒所有正在等待该对象的monitor的线程,这一点与notify()方法是不同的。

一个线程被唤醒不代表立即获取了对象的monitor,只有等调用完notify()或者notifyAll()并退出synchronized块,释放对象锁后,其余线程才可获得锁执行。

例证:

/**
 * @author 70KG
 * @Title: Test02
 * @Description: test
 * @date 2018/7/5下午9:49
 */
public class Test02 {

    public static Object object = new Object();

    public static void main(String[] args) {

        // 启动两个线程
        Thread thread1 = new Thread1("1号");
        Thread thread2 = new Thread2("2号");

        thread1.start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread2.start();
    }


    // 线程1,处于等待状态
    static class Thread1 extends Thread {

        Thread1(String name) {
            this.setName(name);
        }

        @Override
        public void run() {
            synchronized (object) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                }
                System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
            }
        }
    }

    // 线程2调用notify
    static class Thread2 extends Thread {

        Thread2(String name) {
            this.setName(name);
        }

        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println("线程" + Thread.currentThread().getName() + "调用了object.notify()");
            }
            System.out.println("线程" + Thread.currentThread().getName() + "释放了锁");
        }
    }

}

结果都是一样的,验证了上面的内容。

线程2号调用了object.notify()
线程2号释放了锁
线程1号获取到了锁

notify和notifyAll例子:

调用wait方法必须在同步块中进行。用线程来监听快递的信息,包括里程数的变化和地点的变化。

/**
 * @author 70KG
 * @Title: Express
 * @Description: 快递类
 * @date 2018/7/4下午10:27
 */
public class Express {

    // 始发地
    private final static String CITY = "ShangHai";

    // 里程变化
    private int km;

    // 地点变化
    private String site;

    Express() {

    }

    Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    // 里程数变化,会唤起线程
    public synchronized void changeKm() {
        this.km = 101;
        notify();
    }

    // 地点变化会唤起线程
    public synchronized void changeSite() {
        this.site = "BeiJing";
        notify();
    }

    // 用来监听里程数的变化
    public synchronized void waitKm() {
        while (this.km <= 100) {
            try {
                wait();
                System.out.println(Thread.currentThread().getId() + "-号监听===里程变化===的线程被唤醒了。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getId() + "-号监听===里程变化===的线程去做相应的事了");
    }

    // 用来监听地点的变化
    public synchronized void waitSite() {
        while (CITY.equals(this.site)) {
            try {
                wait();
                System.out.println(Thread.currentThread().getId() + "-号监听===地点变化===的线程被唤醒了。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getId() + "-号监听===地点变化===的线程去做相应的事了");
    }

}
/**
 * @author itachi
 * @Title: Test
 * @Description: 测试
 * @date 2018/7/4下午10:40
 */
public class Test {

    // 初始化快递
    private static Express express = new Express(0, "ShangHai");

    // 用来监听里程数变化的线程
    static class CheckKm implements Runnable {
        @Override
        public void run() {
            express.waitKm();
        }
    }

    // 用来监听地点变化的线程
    static class CheckSite implements Runnable {
        @Override
        public void run() {
            express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException{
        // 启动三个线程去监听里程数的变化
        for (int i = 0; i <= 2; i++) {
            new Thread(new CheckKm()).start();
        }

        // 启动三个线程去监听地点的变化
        for (int i = 0; i <= 2; i++) {
            new Thread(new CheckSite()).start();
        }

        // 主线程睡眠一秒,异常信息抛出去
        Thread.sleep(1000);

        // 让快递的地点发生变化
        express.changeSite();
    }

}

测试结果:

9-号监听===里程变化===的线程被唤醒了。。。

可见虽然是让地点发生了变化,但却随机唤醒了一个监听里程数变化的线程,并且使用整个程序处于无限等待状态。

如果将notify换成notifyAll的话,运行结果:

14-号监听===地点变化===的线程被唤醒了。。。
14-号监听===地点变化===的线程去做相应的事了
13-号监听===地点变化===的线程被唤醒了。。。
13-号监听===地点变化===的线程去做相应的事了
12-号监听===地点变化===的线程被唤醒了。。。
12-号监听===地点变化===的线程去做相应的事了
11-号监听===里程变化===的线程被唤醒了。。。
10-号监听===里程变化===的线程被唤醒了。。。
9-号监听===里程变化===的线程被唤醒了。。。

三个监视地点的线程都被唤醒了,各自去做各自的事情了,未被唤醒的用来监听里程数变化的线程依然处于监听状态,因为它的里程数没有变化。

总结起来等待和通知的标准范式:

等待方:

  1. 获取对象的锁
  2. 循环里面判断条件是否满足,不满足调用wait方法继续等待
  3. 条件满足的话就去执行相应的业务逻辑

通知方:

  1. 获取对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程

yeild,sleep,wait,notify,notifyAll对锁的影响

  1. 线程在执行yeild方法后,持有的锁是不会释放的
  2. 线程在执行sleep方法后,持有的锁是不会释放的
  3. 线程必须持有锁才可以调用wait方法,并且执行完后立即释放锁
  4. 线程在执行notify或者notifyAll本身是不会释放锁的,只有等待syn代码块执行完毕后,才会释放锁。

condition需要用到lock,lock和Synchronized需要放在一起,后面会提到。