线程的通知与等待

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有的类都需的方法放在了Object类里面,其中就包含要说的通知与等待。

1.wait()方法

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回。

​ 1.其他线程调用了该共享对象的 notify()或者 notifyAll() 方法。

​ 2.其他线程调用了该线程的 interrupt() 方法,该线程抛出 InterruptedException 异常返回

另外需要注意的是,如果调用 wait() 方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出 IllegalMonitorStateException异常。

那么一个线程如何才能获取一个共享变量的监视器锁呢?

​ 1.执行synchronized同步代码块时使用该共享变量作为参数。

synchronized(共享变量){
    // doSomething
}

​ 2.调用该共享变量的方法,并且该方法使用了 synchronized 修饰。

synchronized void method(int a, int b){
    // doSomething
}

另外需要注意的时,一个线程可以从挂起状态变为可以运行的状态(也就是被唤醒),即使该线程没有被其他线程调用notify(), notifyAll() 方法进行通知,或者被中断,或者等待超时。也就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒状态的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件就是满足了唤醒该线程的条件。

synchronized (obj) {
    while(条件不满足){
        obj.wait();
    }
}

如上代码呢也是经典的调用共享变量wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后再while 循环内调用obj的wait()方法。

下面从一个简单的生产者和消费者例子来加深下理解。如下面代码所示,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法不会抛出 IllegalMonitorStateException 异常。如果当前队列没有空闲容量则会调用wait()方法挂起当前线程,这里使用循环是为了避免上面说的虚假唤醒。假如当前线程被虚假唤醒了,但是队列还是没有空余的容量,那么当前线程还是会调用wait()方法把自己挂起。

public class ObjectMethodTest {

    Queue<String> queue = new LinkedList<String>();

    int MAX_SIZE = 1; // 假设队列长度只有1 , 只能存放一条数据


    public void produce(){
        synchronized (queue){

            // 队列满则等待队列空间
            while (queue.size() == MAX_SIZE) {
                // 挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素。
                try {
                    queue.wait();
                    System.out.println("-----等待消费----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


            queue.add("hahaha");
            queue.notifyAll();
        }
    }

    public void consume(){
        synchronized (queue) {

            while (queue.size() == 0) {
                // 挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素。
                try {
                    queue.wait();
                    System.out.println("-----等待生产-----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 消费元素, 并通知唤醒生产者
            System.out.println("消费成功:" + queue.poll());
            queue.notifyAll();
        }
    }

    public static void main(String[] args) {


        ObjectMethodTest objectMethodTest = new ObjectMethodTest();

        // 10 个生产线程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::produce).start();
        }

        // 10 个消费线程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::consume).start();
        }
    }
}
image-20200111212730839

在如上代码中,假如生产者线程A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产的线程和消费的线程 都将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现队列已满会调用wait()方法阻塞挂起自己,然后就会释放掉获取到的queue上的锁,防止发生死锁。

另外需要注意的是,当前线程调用共享变量的wait()方法后指挥释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的,接下来看例子。

public class WaitTest {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 创建线程
        Thread threadA = new Thread(() -> {

            try {
                synchronized (resourceA) {
                    System.out.println("threadA 获取到resourceA的锁");

                    synchronized (resourceB){
                        System.out.println("threadA 获取到 resourceB的锁");

                        System.out.println("threadA 释放掉 resourceA的锁");
                        resourceA.wait();
                    }
                }
            }catch (Exception ex) {
                ex.printStackTrace();
            }
        });


        // 创建线程
        Thread threadB = new Thread(() -> {

            try {
                Thread.sleep(1000);

                synchronized (resourceA) {
                    System.out.println("threadB 获取到resourceA的锁");

                    System.out.println("threadB 尝试获取resourceB的锁*****");
                    synchronized (resourceB){
                        System.out.println("threadB 获取到 resourceB的锁");

                        System.out.println("threadB 释放掉 resourceA的锁");
                        resourceA.wait();
                    }
                }
            }catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }
}

image-20200111215813652

如上代码在main方法里 启动了 A,B两个线程,为了让A先获取到锁,这里让线程B休眠了1s,线程A先后获取到了共享变量resourceA和resourceB上的锁,然后调用了resourceA的wait()方法阻塞挂起自己,阻塞自己后线程A释放掉了获取到的resourceA上的锁。

线程B休眠后结束后会先尝试获取resourceA上的锁,如果当前线程A里边还没有调用resourceA的wait()方法阻塞挂起释放掉该锁,那么线程B就会被阻塞,如果线程A释放了resourceA的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A中没有释放锁,所以导致线程B尝试获取resourceB上的锁时会被阻塞。

以上就证明了当前线程调用共享变量对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有其他共享对象的监视器锁并不会被释放。

这里再举个例子说明当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常返回。

public class WaitNotifyInterrupt {
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread testThread = new Thread(() -> {
            try {
                System.out.println("----begin----");

                // 阻塞当前线程
                synchronized (obj) {
                    obj.wait();
                }

                System.out.println("---end---");
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        testThread.start();

        Thread.sleep(1000);


        System.out.println("开始阻断 testThread");
        testThread.interrupt();
        System.out.println("阻塞 testThread 完毕");
    }
}
image-20200112161109221

如上代码,testThread调用了共享变量obj的wait()方法后阻塞挂起了自己,然后主线程休眠1s后中断了testThread线程,中断后testThread再obj.wait()处抛出了java.lang.InterruptedException 异常而返回并终止。

2.wait(long timeout)方法

该方法相比于wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享变量的该方法挂起后,没有再指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是因为超时而返回。如果将timeoout设置为0那么则和wait()方法效果一样,因为wait()方法内部就是调用了wait(0),需要注意的是,如果在调用该方法时,传递了一个负的timeout则会抛出IllegalArgumentException异常。

3.wait(long timeout, int nanos)方法

在内部调用的是wait(long timeout)函数,如下代码只用nanos>0时才使timeout参数递增1。

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

4.notify()方法

一个线程调用共享对象的notify方法后,会唤醒一个在该共享变量上调用wait系列方法后被阻塞挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享变量的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才能继续执行。

类似wait()系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出 IllegalMonitorStateException异常。

5.notifyAll()方法

不同于在共享变量上调用notify(),会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量由于调用wait()系列方法而被挂起的线程。

下面举个例子来说明notify()和notifyAll()方法具体含义以及一些需要注意的地方。

public class NotifyTest {

    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 创建线程
        Thread threadA = new Thread(() -> {
            // 获取resourceA的共享资源锁
            synchronized (resourceA) {
                System.out.println("threadA 获取到 resourceA 的锁");

                try {
                    System.out.println("threadA 开始调用resourceA的wait()方法进行阻塞挂起");
                    resourceA.wait();
                    System.out.println("threadA 结束等待");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        // 创建线程
        Thread threadB = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("threadB 获取到 resourceA 的锁");

                try {
                    System.out.println("threadB 开始调用resourceA的wait()方法进行阻塞挂起");
                    resourceA.wait();
                    System.out.println("threadB 结束等待");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        // 创建线程
        Thread threadC = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("threadC 开始调用 resourceA的notify()方法");
                resourceA.notify();
            }
        });

        // 启动线程
        threadA.start();
        threadB.start();

        Thread.sleep(1000);

        threadC.start();

        // 等待线程执行结束
        threadA.join();
        threadB.join();
        threadC.join();

        System.out.println("end---------------------");

    }
}
image-20200112170749487

如上代码开启了三个线程,其中A,B线程分别调用了resourceA的wait()方法,线程C在主线程休眠1s后调用了notify()方法。主线程休息1s是为了保证让线程A,B全部执行完wait()方法后再调用线程C的notify()方法。

这个例子试图再线程A和线程B都因调用共享资源resourceA的wait()方法而被阻塞后,让线程C调用resourceA的notify()方法,从而唤醒线程A,B。但是从执行结果来看,只有一个线程A被唤醒,线程B依然在阻塞挂起状态。

从输出结果可知线程调度器这次先调度了线程A占用Cpu来运行,线程A先获取到resourceA的资源所,然后调用wait()方法阻塞挂起,释放锁,而后线程B获取到资源锁,调用resourceA的wait()阻塞挂起。然后线程C调用notify()方法,尝试唤醒线程,这回激活resourceA的阻塞集合里面的一个线程,这里激活了线程A,所以线程A方法执行完毕并返回了。线程B则继续在阻塞等待中。如果把notify()方法换成notifyAll()结果会这样。

image-20200112171809381

换成notifyAll()方法后,可以看到都得到了唤醒。因为上边也说过了notifyAll()方法会唤醒共享变量内所有的等待线程。这里就是唤醒了resourceA的等待集合里所有线程。只是线程B先抢到了resourceA上的锁,然后返回。然后线程A抢到也进行了返回。

尝试把主线程里面的休眠1s去掉,看一下执行结果。

image-20200112172207861

线程B没有正常被唤醒。

这是因为线程C可能比线程B先执行了。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放到阻塞集合,则该线程不会被唤醒的,指挥唤醒执行notifyAll()方法前阻塞集合里的所有线程。

对wait(),notify(),notifyAll()的说明结束了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容