多线程 - wait notify
回顾:
线程安全 --> 死锁,是在进行多线程的时候,比较常见的问题,也是比较严重的问题!!!
1. 一个线程,一把锁。这个线程连续对这个锁加锁两次,如果这个锁不是可重入锁,就容易发生死锁现象。
2. 两个线程,两把锁。线程 1 获取锁 A,线程 2 获取锁 B,之后,两个线程在不解开本来获取的锁的前提下,再彼此尝试获取对方的锁,就会发生死锁现象。
3. N 个线程 M 把锁。哲学家就餐问题中是 N 个线程 N 把锁,如果是 N 个线程 M 把锁,场景变换一下,也会可能构造出死锁的。
死锁的必要条件:其中最重要的一点是:循环等待。 ==》 避免死锁的方案:约定加锁的顺序!
内存可见性问题:
一个线程读,一个线程写。在读的过程中,编译器可能会优化成读寄存器(这里也可能优化为读缓存了...),就导致写线程做出的修改,线程感知不到... ==》 使用 volatile 关键字来强制编译器提高准确度。
JMM 引入一些更加抽象的概念,重新表述了上述的过程,在 Java 规范文档中,表述更加严谨,通过引入主内存,工作内存这样的术语,表示了计算机实际的内存一起 CPU 寄存器 + CPU 缓存
wait 和 notify
wait 和 notify 翻译过来意思是 等待 和 通知 机制,和 join 用途类似,因为多个线程之间是 随机调度,抢占式调度的。
引入 wait 和 notify,就是为了能够从应用层面上,干预到多个不同线程代码的执行顺序,这里说的干预,不是影响系统的线程调度策略(系统内核中的线程调度,仍然是无序调度)
相当于是在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让执行的线程,先把对应的代码执行完了。
举一个栗子讲:
有 1 2 3 4 5 号大兄弟去 ATM 机中办业务,1 号大兄弟,长得人高马大的,兄弟们也就让他先进去了,当 1 号大兄弟进去取钱的时候,会有一种情况是 ATM 机里面没有钱,所以 1 号大兄弟就会先出来,等到有钱的时候再进去取,此时,其他大兄弟就会取竞争这个锁,争取进入到 ATM 中办理他们的业务,但是实际上,刚刚释放锁的 1 号大兄弟,他还会参与到锁的竞争中,此时,就完全有可能,是 1 号大兄弟,重新又拿到了锁,如此反复,此时就会导致一个情况:1 号大兄弟,反复获取锁,但是又无法完成实质性的逻辑,其他的大兄弟,又无法拿到锁,这个情况,称为“线程饿死”,也可以称为“线程饥饿”。
线程饥饿这样的问题,属于是“概率性”的时间,不像死锁这样,一旦出现了之后,程序就一定会挂了,但是也会极大的影响其他线程的运行(占着茅坑不拉屎...)。
而且,像上述 ATM 的情况发生的概率,还是挺高的。
1 号大兄弟拿到了锁,处于 RUNNABLE 状态,其他线程因为锁冲突出现阻塞,BLOCKED 状态,近水楼台先得月,其他线程,需要系统唤醒之后,才能参与到锁竞争的,1 号大兄弟是不用被唤醒的,他直接就能竞争(当然这里到底是谁能竞争到,也是一个复杂的过程)。
这种情况当然也是 bug 了,虽然没有死锁那么严重,但是也是需要处理的...
这里的关键在于,1 号大兄弟,当他发现自己要执行的逻辑,前提条件不具备(ATM 里没钱),在这个情况下,1 号大兄弟,就应该主动的放弃对锁的争夺 / 主动放弃去 cpu 上调度执行(进入阻塞),一直等到,条件具备了(其他线程给 ATM 里存钱了),此时再接触阻塞,参与锁竞争。
此时,就可以使用到 wait 和 notify 了。
让 1 号大兄弟,发现当前他要执行的逻辑的前提条件不具备,则 wait,当其他线程让条件满足之后,再通过 notify 唤醒 1 号大兄弟...
1. 线程之间的调度是无序的。
2. 1 号大兄弟,当前要完成的工作是想去 ATM 中取钱,进入 ATM 要加锁,但取钱的前提是 ATM 中里面是有钱的。如果有钱,就取走了,自然循环就 break 结束了,取钱动作就完了,如果没钱,就要再试依次,由于 1 号大兄弟也不知道什么时候有钱,就只能反复的进行尝试...
如果上面的代码中,ATM 是没钱的,此时 1 号大兄弟,就会反复快速的尝试加锁解锁,这个过程中,其他线程就大概率的很难再继续获取到锁了。
所以我们期望应该改成:
wait 的内部做了三件事:
1. 释放锁
2. 进入阻塞等待
(1 2 两件事之后,其他线程就有机会拿到锁了)
3. 当其他线程调用 notify 的时候,wait 就会解除阻塞,并重新获取到锁。
wait 和 join
join 是等待另一个线程执行完,当下线程才继续执行。
wait 则是等待另一个线程通过 notify 进行通知(不要求另一个线程一定执行完)
wait 的调用
和 synchronized 一样,随便一个对象,都可以进行 wait,即 wait 是 Object 中的方法
但如果,直接调用 wait,会出现异常:
逐渐分析异常信息: 不合法的 监视器(也就是锁)状态异常。
因为我们刚刚说了 wait 要做的三件事:1. 释放锁 2.进入阻塞等待 3. 当其他线程调用 notify 的时候,wait 就会接触阻塞,并重新获取到锁。
即,wait 一进来的时候,就要先释放锁,释放锁的前提是,wait 能先拿到锁。 ==》wait 必须要放到 synchronized 里面使用。
每个对象里都有一把锁
调用 wait 的对象,必须和 synchronized 中的锁对象是一致的!!!
因此,wait 解锁必然是解的 object 的锁,后续 wait 被唤醒之后,重新获取锁,当然还是获取到 object 的锁!!!在其他线程中调用 notify 的时候,也是需要使用同样的对象 --> object.notify()
wiat 演示:
如上图代码,如果仅仅在一个线程中调用 wait 而没有在其他线程中调用 notify 方法,则只会打印“wait 之前”,且代码就在 wait 这里阻塞了。
此时打开 jconsole 观察:
补充: wait 和 sleep join 都是一类的,都可能会被 interrupt 提前唤醒。
wait 的话,放到 synchronized 里面,是因为要释放锁,前提是先加上锁。
notify 其实可以不妨到 synchronized 中,不需要先加锁的(但是 Java 中特别约定要把 notify 放到 synchronized 里面了)(操作系统的原生 API 中也有 wait 和 notify 系统原生的 wait 需要先加锁,notify 不需要先加锁)
wait 和 notify 示例代码:
t1 线程启动,就会调用 wait,从而 t1 线程进入阻塞等待
t2 线程启动,就会先 sleep,sleep 时间到之后,再进行 notify 唤醒 t1
注意: t2 线程中的 sleep 要放在 synchronized 的外面,否则,由于随即调度,t1 t2 的运行顺序不确定,就有可能会 t2 先拿到锁,此时 t1 就没执行到 wait,t2 就 先 notify 了,结果就不符合预期了。(需要保证,代码是先执行 wait 后执行 notify)如果先 notify 虽然不会有副作用(不会有什么异常)但是 wait 就无法被唤醒,逻辑上出现问题。
上述代码的执行过程的理解:
1. t1 执行起来之后,就会先立即拿到锁,并且打印“wait 之前”,ing进入 wait 方法(wait 方法会做两件事:释放锁 + 阻塞等待)
2. t2 执行起来之后,先进性 sleep(5000) (这个 sleep 就可以让 t 能够先拿到锁)
3. t2 sleep 结束之后,由于 t1 是wait 状态,锁是释放的,t2 就能拿到锁。接下来打印 t2 中的“notify 之前”,然后执行 notify 操作,这个操作就能够唤醒 t1 (此时 t1 就从 WAITING 状态恢复回来了)
4. 但是由于 t2 此时还没有释放锁呢,t1 WAITING 恢复之后,尝试获取锁,就可能出现一个小小的阻塞,这个阻塞是由于锁竞争引起的(注意:肉眼很难看到 BLOCKED 状态,这个状态的变换是非常快的)
5. t2 执行完 t2.notify 之后,释放锁,t2 执行完毕,t1 的 wait 也就可以重新获取到锁了,继续执行打印“wait 之后”。
补充:
1. wait 提供了两个版本:
第一个版本中,wait 是“死等“,即,如果没有其他线程中调用 notify 方法的话,该线程就会一直等下去。第二个是带有超时间的等,单位是 ms,当进入 wait 的时候,最多等待 timeout ms,如果这个时间内,没有其他线程进行 notify 操作,那我们也不等了,继续进行。
死等这样的策略,我们一般是不会使用的...
2. wait 和 notify 彼此之间是通过 object 对象联系起来的。
object1.wait() <==> object2.notify() 此时是无法唤醒的!!!必须是两个对象一致才能唤醒!!!
3. notifyAll ==》 唤醒这个对象上所有等待的线程。
假设有很多线程,都使用同一个对象 wait,针对这个对象进行 notifyAll,此时就会全部都唤醒。
但是注意,这些线程在 wait 返回的时候,要重新获取锁,就会因为锁的竞争,从而使得这些线程实际上是一个一个串行执行的(谁先拿到锁,谁后拿到锁,也是不确定的),相比之下,还是更倾向于使用 notify,notifyAll 全部唤醒线程之后,不太好控制。
4. wait 和 sleep 的区别:
wait 提供了一个带有超时时间的版本,sleep 也能指定时间,都是时间到,就可以继续执行了,解除阻塞了。wait 和 sleep 都可以被提前唤醒(即时间没有到,也可以被唤醒),wait 通过 notify 唤醒, sleep 通过 interrupt 唤醒。
使用 wait 最主要的目标:一定是 不知道 要等多长时间的前提下使用,所谓超时时间,其实是”兜底的“。(大多数情况下,wait 都是在超时时间内就被唤醒了)
使用 sleep,一定是知道要等多长时间的前提下使用的,虽然能提前唤醒,但是通过异常唤醒,这个操作不应该作为”正确的业务流程“。
一般情况下,不使用 wait 来代替 sleep。