多线程基础小问

对于多线程我自认为有所了解,但是到探究其中细节还是会一脸懵逼。
在经过一段时间系统学习后,回头去看多线程的基础知识,发现之前有很多地方理解错误。
这里提出多线程的几个小问题,希望能抛砖引玉,让大家借此有更加深入的思考。

wait和sleep的差异?

这个问题看上去很简单,大家都能说出一二三来。
a.wait是Object的方法;sleep是Thread方法。
b.wait只能在同步代码中调用。
c.wait会释放出锁;sleep不会释放出锁。

这次我们就展开深入下:

为什么要设置wait是Object的方法;sleep是Thread方法?

个人理解是这样的,java语言的创造者在设计时考虑这两个函数的使用场景。
在运行一个线程中,我们只需要对Thread这个对象使用sleep函数(用来休眠线程),但是我们可能会用多个对象进行锁管理(即在一个线程中使用多个锁)。
如果把wait设置成Thread方法,那么在一个线程中只能用Thread对象的wait和notify,非常有局限性(相当于一个线程中只能使用一个锁)。

例如下面这样的场景,银行给用户开金卡需要两个条件:存款10000,今年消费次数超过5次。我们在这两个条件未达到的时候,分别使用wait(),等待对应notify。
如果wait是Thread方法的话,就不能实现这样的需求。

1
2
3
4
5
6
7
8
9
10
11
synchronized (notEnoughDeposit) {
while(deposit < 10000) {
notEnoughDeposit.wait();
}
synchronized (notEnoughConsumeCount) {
while(consumeCount < 5) {
notEnoughConsumeCount.wait();
}
}
// 办理金卡
}

sleep只能在Thread中使用,不需要考虑多个对象嵌套的调用。同时结合“最小职责”的设计原则,sleep()就设置成Thread的方法。

为什么wait只能在同步代码中调用?

我们看下wait使用的具体场景,下面代码是没有加入同步块:

1
2
3
4
5
6
7
while(deposit < 10000) {
notEnoughDeposit.wait();
}
while(consumeCount < 5) {
notEnoughConsumeCount.wait();
}
// 办理金卡

这里就会存在一个问题,如果没有在wait()外围加入同步块,在办理金卡的时候deposit可能会变化。用户在中间的时间空隙(条件判断和真正办卡中间)中可能再去消费,导致卡中余额小于10000,不符合金卡办理条件。
在java规范中,如果wait和notify外围没有同步块,会抛出ILLegalMonitorStateException。
所以java程序设计总是需要保证 notify 和 wait 操作的原子性,这也就是为什么wait只能在同步代码中调用的原因。

怎么理解wait会释放出锁;sleep不会释放出锁?

wait调用后,会释放出锁。如果当前其他线程正在阻塞状态的话会被唤醒,唤醒后其他线程就继续操作。
sleep就相当于当前线程休眠一段时间,不会释放出锁,其他阻塞的线程自然也获取不到锁。这里特别要注意的是,sleep并不代表会一直占用系统CPU。因为操作系统底层也有对应的优化机制,检查到当前线程sleep,会转而执行那些非阻塞的线程。

锁的机制?

我们先来看下这张图,再理清其中的概念:

什么是锁?

锁是对象内存堆头部的一部分数据。

什么是监视器(monitor)?

监视器是一种虚拟的概念,一个锁只能被一个线程持有,其他线程阻塞等待。获得这个锁的线程处于监视器中。

什么是等待池(wait set)?

调用了某个对象的wait()方法,线程就会释放该对象的锁后,进入到了该对象的等待池中。

什么是阻塞队列(entry set)?

线程获取锁失败后,进入到阻塞队列。当持有锁的线程释放锁后,阻塞队列中一个线程会获取该锁,然后进入监视器。

明白这些概念后,我们再看一张图,这里面表示锁操作的关键流程

下面是每一步对应的操作(注:下面第x步不是顺序执行)
第1步:线程A尝试获取object锁,获取锁成功,进入Monitor成功
第2步:线程A持有锁,线程B尝试获取object锁,获取锁失败,进入Monitor失败
第3步:线程A持有锁,完成操作释放object锁,线程B离开阻塞队列,尝试获取object锁
第4步:线程A持有锁,在运行过程中调用wait,释放object锁,同时进入等待池
第5步:线程B持有锁,在运行过程中调动notify或者notifyAll,会触发第6步操作,等待线程其他操作完毕,才释放object锁
第6步:线程A之前调用wait,处于等待池中,在接受notify或者notifyAll后,会进入阻塞队列,在之后进行第3步操作

以上就是常见的锁操作,包括获取锁、释放锁、wait、notify。

notify和notifyAll的差异?

从字面上理解,notify就是随机唤醒一个wait的线程,notifyAll唤醒所有wait的线程。

那么继续问下去,两者是否可以调换使用?

我们先来看下面的代码:

1
2
3
4
5
6
7
if(!condition) {
obj.wait();
}

while (!condition) {
obj.wait();
}

这里有两种写法在一种情况下有差别,就是一个线程被错误的唤醒后,if代码块会继续执行下去,但是后续执行都是错误的。而while代码块会再次验证条件是否满足,发现没有满足条件的话,选择再次wait。

这个有个建议:永远在while循环里而不是if语句下使用wait。
这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。

假死的情况

在while的基础上,如果使用notify错误的唤醒了一个wait的线程。它在接下去的while判断中,发现仍旧没有达到条件,这个线程重新选择wait。同时其他线程没有被唤醒,这就会导致所有线程一直假死在那边。
而使用notifyAll则不会出现这种情况,因为所有wait的线程都会被唤醒,总有一个线程能重新通过条件判断(这一点要开发者自己保障),程序会继续运行下去。

corresponding wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!