等待唤醒(wait / notify)机制

若是一个线程从头至尾执行完也不和别的线程打交道的话,那就不会有各类安全性问题了。可是协做愈来愈成为社会发展的大势,一个大任务拆成若干个小任务以后,各个小任务之间可能也须要相互协做最终才能执行完整个大任务。因此各个线程在执行过程当中能够相互通讯,所谓通讯就是指相互交换一些数据或者发送一些控制指令,好比一个线程给另外一个暂停执行的线程发送一个恢复执行的指令,下边详细看都有哪些通讯方式。java

volatile和synchronized

可变共享变量是自然的通讯媒介,也就是说一个线程若是想和另外一个线程通讯的话,能够修改某个在多线程间共享的变量,另外一个线程经过读取这个共享变量来获取通讯的内容。安全

因为原子性操做、内存可见性和指令重排序的存在,java提供了volatilesynchronized的同步手段来保证通讯内容的正确性,假如没有这些同步手段,一个线程的写入不能被另外一个线程当即观测到,那这种通讯就是不靠谱的~多线程

wait/notify机制

故事背景

也不知道是那个遭天杀的给咱们学校厕所的坑里塞了个塑料瓶,致使楼道里如黄河泛滥通常,臭味熏天。更加悲催的是整个楼只有这么一个厕所,比这个更悲催的是这个厕所里只有一个坑!!!!!好吧,让咱们用java来描述一下这个厕所:ide

public class Washroom {

    private volatile boolean isAvailable = false;    //表示厕所是不是可用的状态

    private Object lock = new Object(); //厕所门的锁

    public boolean isAvailable() {
        return isAvailable;
    }

    public void setAvailable(boolean available) {
        this.isAvailable = available;
    }

    public Object getLock() {
        return lock;
    }
}

isAvailable字段表明厕所是否可用,因为厕所损坏,默认是false的,lock字段表明这个厕所门的锁。须要注意的是isAvailable字段被volatile修饰,也就是说有一个线程修改了它的值,它能够当即对别的线程可见~this

因为厕所资源宝贵,英明的学校领导当即拟定了一个修复任务:线程

public class RepairTask implements Runnable {

    private Washroom washroom;

    public RepairTask(Washroom washroom) {
        this.washroom = washroom;
    }

    @Override
    public void run() {

        synchronized (washroom.getLock()) {
            System.out.println("维修工 获取了厕所的锁");
            System.out.println("厕所维修中,维修厕所是一件辛苦活,须要很长时间。。。");

            try {
                Thread.sleep(5000L);    //用线程sleep表示维修的过程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            washroom.setAvailable(true);        //维修结束把厕所置为可用状态
            System.out.println("维修工把厕所修好了,准备释放锁了");
        }
    }
}

这个维修计划的内容就是当维修工进入厕所以后,先把门锁上,而后开始维修,维修结束以后把WashroomisAvailable字段设置为true,以表示厕所可用。翻译

与此同时,一群急得像热锅上的蚂蚁的家伙在厕所门前打转转,他们想作神马不用我明说了吧😏😏:code

public class ShitTask implements Runnable {

    private Washroom washroom;

    private String name;

    public ShitTask(Washroom washroom, String name) {
        this.washroom = washroom;
        this.name = name;
    }

    @Override
    public void run() {
        synchronized (washroom.getLock()) {
            System.out.println(name + " 获取了厕所的锁");
            while (!washroom.isAvailable()) {
                // 一直等
            }
            System.out.println(name + " 上完了厕所");
        }
    }
}

这个ShitTask描述了上厕所的一个流程,先获取到厕所的锁,而后判断厕所是否可用,若是不可用,则在一个死循环里不断的判断厕所是否可用,直到厕所可用为止,而后上完厕所释放锁走人。对象

而后咱们看看现实世界都发生了什么吧:排序

public class Test {
    public static void main(String[] args) {
        Washroom washroom = new Washroom();
        new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
        new Thread(new ShitTask(washroom, "猫爷"), "GRANDPA-CAT-THREAD").start();
        new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();
    }
}

学校先让维修工进入厕所维修,而后包括狗哥、猫爷、王尼妹在内的上厕所大军就开始围着厕所打转转的旅程,咱们看一下执行结果:

维修工 获取了厕所的锁
厕所维修中,维修厕所是一件辛苦活,须要很长时间。。。
维修工把厕所修好了,准备释放锁了
王尼妹 获取了厕所的锁
王尼妹 上完了厕所
猫爷 获取了厕所的锁
猫爷 上完了厕所
狗哥 获取了厕所的锁
狗哥 上完了厕所

看起来没有神马问题,可是再回头看看代码,发现有两处特别别扭的地方:

  1. 在main线程开启REPAIR-THREAD线程后,必须调用sleep方法等待一段时间才容许上厕所线程开启。

    若是REPAIR-THREAD线程和其余上厕所线程一起开启的话,就有可能上厕所的人,好比狗哥先获取到厕所的锁,而后维修工压根儿连厕所也进不去。可是真实状况可能真的这样的,狗哥先到了厕所,而后维修工才到。不过狗哥的处理应该不是一直待在厕所里,而是先出来等着,啥时候维修工说修好了他再进去。因此这点有些别扭~

  2. 在一个上厕所的人获取到厕所的锁的时候,必须不断判断WashroomisAvailable字段是否为true

    若是一我的进入到厕所发现厕所仍然处在不可用状态的话,那它应该在某个地方休息,啥时候维修工把厕所修好了,再叫一下等着上厕所的人就行了嘛,不必本身不停的去检查厕所是否被修好了。

总结一下,就是一个线程在获取到锁以后,若是指定条件不知足的话,应该主动让出锁,而后到专门的等待区等待,直到某个线程完成了指定的条件,再通知一下在等待这个条件完成的线程,让它们继续执行。

若是你以为上边这句话比较绕的话,我来给你翻译一下:当上狗哥获取到厕所门锁以后,若是厕所处于不可用状态,那就主动让出锁,而后到等待上厕所的队伍里排队等待,直到维修工把厕所修理好,把厕所的状态置为可用后,维修工再通知须要上厕所的人,然他们正常上厕所。

具体使用方式

为了实现这个构想,java里提出了一套叫wait/notify的机制。当一个线程获取到锁以后,若是发现条件不知足,那就主动让出锁,而后把这个线程放到一个等待队列等待去,等到某个线程把这个条件完成后,就通知等待队列里的线程他们等待的条件知足了,能够继续运行啦!

若是不一样线程有不一样的等待条件肿么办,总不能都塞到同一个等待队列里吧?是的,java里规定了每个锁都对应了一个等待队列,也就是说若是一个线程在获取到锁以后发现某个条件不知足,就主动让出锁而后把这个线程放到与它获取到的锁对应的那个等待队列里,另外一个线程在完成对应条件时须要获取同一个锁,在条件完成后通知它获取的锁对应的等待队列。这个过程意味着锁和等待队列创建了一对一关联。

怎么让出锁而且把线程放到与锁关联的等待队列中以及怎么通知等待队列中的线程相关条件已经完成java已经为咱们规定好了。咱们知道,其实就是个对象而已,在全部对象的老祖宗类Object中定义了这么几个方法:

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException

public final void notify();
public final void notifyAll();

各个方法的详细说明以下:

方法名 说明
wait() 在线程获取到锁后,调用锁对象的本方法,线程释放锁而且把该线程放置到与锁对象关联的等待队列
wait(long timeout) wait()方法类似,只不过等待指定的毫秒数,若是超过指定时间则自动把该线程从等待队列中移出
wait(long timeout, int nanos) 与上边的同样,只不过超时时间粒度更小,即指定的毫秒数加纳秒数
notify() 通知一个在与该锁对象关联的等待队列的线程,使它从wait()方法中返回继续往下执行
notifyAll() 与上边的相似,只不过通知该等待队列中的全部线程

了解了这些方法的意思之后咱们再来改写一下ShitTask

public class ShitTask implements Runnable {

    // ... 为节省篇幅,省略相关字段和构造方法

    @Override
    public void run() {
        synchronized (washroom.getLock()) {
            System.out.println(name + " 获取了厕所的锁");
            while (!washroom.isAvailable()) {
                try {
                    washroom.getLock().wait();  //调用锁对象的wait()方法,让出锁,并把当前线程放到与锁关联的等待队列
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(name + " 上完了厕所");
        }
    }
}

看,原来咱们在判断厕所是否可用的死循环里加了这么一段代码:

washroom.getLock().wait();

这段代码的意思就是让出厕所的锁,而且把当前线程放到与厕所的锁相关联的等待队列里。

而后咱们也须要修改一下维修任务:

public class RepairTask implements Runnable {

    // ... 为节省篇幅,省略相关字段和构造方法

    @Override
    public void run() {

        synchronized (washroom.getLock()) {
            System.out.println("维修工 获取了厕所的锁");
            System.out.println("厕所维修中,维修厕所是一件辛苦活,须要很长时间。。。");

            try {
                Thread.sleep(5000L);    //用线程sleep表示维修的过程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            washroom.setAvailable(true);    //维修结束把厕所置为可用状态
            
            washroom.getLock().notifyAll(); //通知全部在与锁对象关联的等待队列里的线程,它们能够继续执行了
            System.out.println("维修工把厕所修好了,准备释放锁了");
        }
    }
}

你们能够看出来,咱们在维修结束后加了这么一行代码:

washroom.getLock().notifyAll();

这个代码表示将通知全部在与锁对象关联的等待队列里的线程,它们能够继续执行了。

在使用java的wait/notify机制修改了ShitTaskRepairTask后,咱们在复原一下整个现实场景:

public class Test {
    public static void main(String[] args) {
        Washroom washroom = new Washroom();
        new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
        new Thread(new ShitTask(washroom, "猫爷"), "GRANDPA-CAT-THREAD").start();
        new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();
    }
}

在这个场景中,咱们能够刻意让着急上厕所的先到达了厕所,维修工最后抵达厕所,来看一下加了wait/notify机制的代码的执行结果是:

狗哥 获取了厕所的锁
猫爷 获取了厕所的锁
王尼妹 获取了厕所的锁
维修工 获取了厕所的锁
厕所维修中,维修厕所是一件辛苦活,须要很长时间。。。
维修工把厕所修好了,准备释放锁了
王尼妹 上完了厕所
猫爷 上完了厕所
狗哥 上完了厕所

从执行结果能够看出来,狗哥、猫爷、王尼妹虽然先到达了厕所而且获取到锁,可是因为厕所处于不可用状态,因此都先调用wait()方法让出了本身得到的锁,而后躲到与这个锁关联的等待队列里,直到维修工修完了厕所,通知了在等待队列中的狗哥、猫爷、王尼妹,他们才又开始继续执行上厕所的程序~

通用模式

通过上边的厕所案例,你们应该对wait/notify机制有了大体了解,下边咱们总结一下这个机制的通用模式。首先看一下等待线程的通用模式:

  1. 获取对象锁。

  2. 若是某个条件不知足的话,调用锁对象的wait方法,被通知后仍要检查条件是否知足。

  3. 条件知足则继续执行代码。

通用的代码以下:

synchronized (对象) {
    处理逻辑(可选)
    while(条件不知足) {
        对象.wait();
    }
    处理逻辑(可选)
}

除了判断条件是否知足和调用wait方法之外的代码,其余的处理逻辑是可选的。

下边再来看通知线程的通用模式:

  1. 得到对象的锁。
  2. 完成条件。
  3. 通知在等待队列中的等待线程。
synchronized (对象) {
    完成条件
    对象.notifyAll();、
}

小贴士:别忘了同步方法也是使用锁的喔,静态同步方法的锁对象是该类的Class对象,成员同步方法的锁对象是this对象。因此若是没有刻意强调,下边所说的同步代码块也包含同步方法。

了解了wait/notify的通用模式以后,使用的时候须要特别当心,须要注意下边这些方面:

  • 必须在同步代码块中调用waitnotify或者notifyAll方法。

    有的童鞋会有疑问,为啥wait/notify机制的这些方法必须都放在同步代码块中才能调用呢?wait方法的意思只是让当前线程中止执行,把当前线程放在等待队列里,notify方法的意思只是从等待队列里移除一个线程而已,跟加锁有什么关系?

    答:由于wait方法是运行在等待线程里的,notify或者notifyAll是运行在通知线程里的。而执行wait方法前须要判断一下某个条件是否知足,若是不知足才会执行wait方法,这是一个先检查后执行的操做,不是一个原子性操做,因此若是不加锁的话,在多线程环境下等待线程和通知线程的执行顺序多是这样的:

    img

    也就是说当等待线程已经判断条件不知足,正要执行wait方法,此时通知线程抢先把条件完成而且调用了notify方法,以后等待线程才执行到wait方法,这会致使等待线程永远停留在等待队列而没有人再去notify它。因此等待线程中的判断条件是否知足、调用wait方法和通知线程中完成条件、调用notify方法都应该是原子性操做,彼此之间是互斥的,因此用同一个锁来对这两个原子性操做进行同步,从而避免出现等待线程永久等待的尴尬局面。

    若是不在同步代码块中调用waitnotify或者notifyAll方法,也就是说没有获取锁就调用wait方法,就像这样:

    对象.wait();

    是会抛出IllegalMonitorStateException异常的。

  • 在同步代码块中,必须调用获取的锁对象的waitnotify或者notifyAll方法。

    也就是说不能随便调用一个对象的waitnotify或者notifyAll方法。好比等待线程中的代码是这样的:

    synchronized (对象1) {
        while(条件不知足) {
            对象2.wait();    //随便调用一个对象的wait方法
        }
    }
  • 通知线程中的代码是这样的:

    synchronized (对象1) {
        完成条件
        对象2.notifyAll();
    }

    对于代码对象2.wait(),表示让出当前线程持有的对象2的锁,而当前线程持有的是对象1的锁,因此这么写是错误的,也会抛出IllegalMonitorStateException异常的。意思就是若是当前线程不持有某个对象的锁,那它就不能调用该对象的wait方法来让出该锁。因此若是想让等待线程让出当前持有的锁,只能调用对象1.wait()。而后这个线程就被放置到与对象1相关联的等待队列中,在通知线程中只能调用对象1.notifyAll()来通知这些等待的线程了。

  • 在等待线程判断条件是否知足时,应该使用while,而不是if

    也就是说在判断条件是否知足的时候要使用while

    while(条件不知足) { //正确✅
        对象.wait();
    }

    而不是使用if

    if(条件不知足) { //错误❌
        对象.wait();
    }

    这个是由于在多线程条件下,可能在一个线程调用notify以后当即又有一个线程把条件改为了不知足的状态,好比在维修工把厕所修好以后通知你们上厕所吧的瞬间,有一个小屁孩以迅雷不及掩耳之势又给厕所坑里塞了个瓶子,厕所又被置为不可用状态,等待上厕所的仍是须要再判断一下条件是否知足才能继续执行。

  • 在调用完锁对象的notify或者notifyAll方法后,等待线程并不会当即从wait()方法返回,须要调用notify()或者notifyAll()的线程释放锁以后,等待线程才从wait()返回继续执行。

    也就是说若是通知线程在调用完锁对象的notify或者notifyAll方法后还有须要执行的代码,就像这样:

    synchronized (对象) {
        完成条件
        对象.notifyAll();
        ... 通知后的处理逻辑
    }

    须要把通知后的处理逻辑执行完成后,把锁释放掉,其余线程才能够从wait状态恢复过来,从新竞争锁来执行代码。比方说在维修工修好厕所并通知了等待上厕所的人们以后,他尚未从厕所出来,而是在厕所的墙上写了 "XXX到此一游"之类的话以后才从厕所出来,从厕所出来才表明着释放了锁,狗哥、猫爷、王尼妹才开始争抢进入厕所的机会。

  • notify方法只会将等待队列中的一个线程移出,而notifyAll方法会将等待队列中的全部线程移出。

    你们能够把上边代码中的notifyAll方法替换称notify方法,看看执行结果~

wait和sleep的区别

眼尖的小伙伴确定发现,waitsleep这两个方法均可以让线程暂停执行,并且都有InterruptedException的异常说明,那么它们的区别是啥呢?

  • waitObject的成员方法,而sleepThread的静态方法。

    只要是做为锁的对象均可以在同步代码块中调用本身的wait方法,sleepThread的静态方法,表示的是让当前线程休眠指定的时间。

  • 调用wait方法须要先得到锁,而调用sleep方法是不须要的。

    再一次强调,必定要在同步代码块中调用锁对象的wait方法,前提是要得到锁!前提是要得到锁!前提是要得到锁!而sleep方法随时调用~

  • 调用wait方法的线程须要用notify来唤醒,而sleep必须设置超时值。

  • 线程在调用wait方法以后会先释放锁,而sleep不会释放锁。

    这一点多是最重要的一点不一样点了吧,狗哥、猫爷、王尼妹这些线程一开始是获取到厕所的锁了,可是调用了wait方法以后主动把锁让出,从而让维修工得以进入厕所维修。若是狗哥在发现厕所是不可用的条件时选择调用sleep方法的话,线程是不会释放锁的,也就是说维修工没法得到厕所的锁,也就修不了厕所了~ 你们必定要谨记这一点啊!

总结

  1. 线程间须要经过通讯才能协做解决某个复杂的问题。

  2. 可变共享变量是自然的通讯媒介,可是使用的时候必定要保证线程安全性,一般使用volatile变量或synchronized来保证线程安全性。

  3. 一个线程在获取到锁以后,若是指定条件不知足的话,应该主动让出锁,而后到专门的等待区等待,直到某个线程完成了指定的条件,再通知一下在等待这个条件完成的线程,让它们继续执行。这个机制就是wait/notify机制。

  4. 等待线程的通用模式:

    synchronized (对象) {
        处理逻辑(可选)
        while(条件不知足) {
            对象.wait();
        }
        处理逻辑(可选)
    }

    能够分为下边几个步骤:

. - 获取对象锁。

  • 若是某个条件不知足的话,调用锁对象的wait方法,被通知后仍要检查条件是否知足。
  • 条件知足则继续执行代码。
  1. 通知线程的通用模式:

    synchronized (对象) {
        完成条件
        对象.notifyAll();、
    }

    能够分为下边几个步骤:

    • 得到对象的锁。

    • 完成条件。

    • 通知在等待队列中的等待线程。

  2. waitsleep的区别

    • wait是Object的成员方法,而sleep是Thread的静态方法。
    • 调用wait方法须要先得到锁,而调用sleep方法是不须要的。
    • 调用wait方法的线程须要用notify来唤醒,而sleep必须设置超时值。
    • 线程在调用wait方法以后会先释放锁,而sleep不会释放锁。
相关文章
相关标签/搜索