wait/notify/notifyAll 方法的使用注意事项?

为什么 wait 必须在 synchronized 保护的同步代码中使用?

在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。

 synchronized (obj) {
     while (condition does not hold) {
         obj.wait();
     }
     ... // Perform action appropriate to condition
}

那么设计成这样有什么好处呢?

先看一段代码:

// 典型的生产者消费者的思想
class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void add(String data) {
        buffer.add(data);
        notify();  
    }
    
    public String remove() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

这段代码的问题在哪里呢?这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:

1,首先,消费者线程调用 remove 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表 buffer 是空的,则消费者线程进入等待,但是在消费者线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。

2,此时生产者线程开始运行,执行了 add 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。

3,此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。

虽然刚才消费者判断了 buffer.isEmpty 条件,但真正执行 wait 方法时,之前的 buffer.isEmpty 的结果已经过期了,不再符合最新的场景了,因为这里的 判断-执行 不是一个原子操作,它在中间被打断了,是线程不安全的。假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 add 方法内的 notify 的唤醒。

我们看到正是因为 wait 方法所在的 remove() 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作,那么此时整个程序就很容易出错。

关键点:不是一个原子操作,它在中间被打断了,是线程不安全的。

Fix 一下上面的代码,这样写就对了:

public void add(String data) {
   synchronized (this) {
      buffer.add(data);
      notify();
   }
}
 
public String remove() throws InterruptedException {
   synchronized (this) {
        while (buffer.isEmpty()) {
             wait();
        }
        return buffer.remove();
   }
}

另外,wait 方法会释放 monitor 锁,这也要求我们必须首先进入到 synchronized 内持有这把锁。

再来看第二个问题,为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

主要有两点原因:

1,Java 中每个对象都有一把称之为 monitor 监视器的锁,每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。

2,如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程在持有多把锁的情况下,如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

再来看第三个问题,wait/notify 和 sleep 方法的异同?

我们先说相同点:

1,它们都可以让线程阻塞。

2,它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

但是它们也有很多的不同点:

1,wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。

2,在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。

3,sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。

4,wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

补充几点:

public final native void notify();

public final native void notifyAll();

public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

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");
    }
    // 此处对于纳秒的处理不精准,只是简单增加了1毫秒,
    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

其中有 3 个方法是 native 的,也就是由虚拟机本地的 c 代码执行的。

wait() 方法:wait() 是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以 wait() 必须要写在 synchronized 代 码块中,否则会报异常。

notify() 方法:也需要写在 synchronized 代码块中,调用对象的 notify()notifyAll() 这两个方法也需要先获得该对象的锁,然后去唤醒等待该对象锁的线程。notify 唤醒 等待池 中的一个线程,将这个线程放入该对象的锁池中。对象的锁池中的线程可以去竞争得到 对象锁,然后开始执行。

关键词:等待池,对象的锁池。

如果是通过 notify() 唤起的线程,那先进入 wait() 的线程会先被唤起来,并非随机唤醒;notifyAll() 则是唤起所有等待的线程。

假设有两个线程,线程 A 执行 wait() 方法时,线程 A 会被挂起,挂起意味着 wait() 后面的代码得不到执行。

当线程 B 执行 notify() 方法时,会唤醒被挂起的线程A,这个时候等待 B 线程执行完它所在方法之后,线程 A 被唤醒了,线程 A 才可以继续执行 wait() 方法后面的代码。

 @Test
fun test() {
    val lock = Object()

    Thread {
        synchronized(lock) {
            try {
                println("${Thread.currentThread().name} get lock")
                TimeUnit.SECONDS.sleep(1)
                lock.wait()
                println("${Thread.currentThread().name} waked up")
            } catch (ex: InterruptedException) {
                ex.printStackTrace()
            }
        }
    }.apply {
        name = "thread-A"
    }.start()

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

推荐阅读更多精彩内容