线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析
场景—两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
synchronized 方案
package com.sync;
//创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
while (number != 0) { //判断是否是0 如果不是0 等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number是0 就+1操作
number++;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
while (number != 1) {
this.wait();
}
//干活
number--;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.incr();//+1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"AA").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"BB").start();
}
}
上述代码是只有两个线程,一个是+1操作,一个是 -1操作
控制台的效果如上,乍一看没什么问题,但是如果将两个线程变成四个线程,又该当如何呢
package com.sync;
//创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if(number != 0) { //判断是否是0 如果不是0 等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number是0 就+1操作
number++;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
if(number != 1) {
this.wait();
}
//干活
number--;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.incr();//+1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"AA").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"BB").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"CC").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"DD").start();
}
}
控制台效果如上图,发现有复数,与开始的需求并不一样,这就是所谓的虚假唤醒问题
在wait端,我们必须把判断布尔条件和wait()放到while循环中,而不能用if语句,原因是可能会引起虚假唤醒。
那么,究竟什么是虚假唤醒,导致虚假唤醒的原因又是什么呢?
什么是虚假唤醒?
举个例子,我们现在有一个生产者-消费者队列和三个线程。
1) 1号线程从队列中获取了一个元素,此时队列变为空。
2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。
3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。
4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
5) 然而可能出现这样的情况:当2号线程准备获得队列的锁,去获取队列中的元素时,此时1号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,1号线程便获得队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,然后释放队列锁。
6) 等到2号线程获得队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,所以对于2号线程而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。
使用while()判断的原因
如果用if判断,多个等待线程在满足if条件时都会被唤醒(虚假的),但实际上条件并不满足,生产者生产出来的消费品已经被第一个线程消费了。
这就是我们使用while去做判断而不是使用if的原因:因为等待在条件变量上的线程被唤醒有可能不是因为条件满足而是由于虚假唤醒。所以,我们需要对条件变量的状态进行不断检查直到其满足条件,不仅要在 wait() 方法 前检查条件是否成立,在 wait() 之后也要检查。
纠正后的代码
package com.sync;
//创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
while (number != 0) { //判断是否是0 如果不是0 等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number是0 就+1操作
number++;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
while (number != 1) {
this.wait();
}
//干活
number--;
System.out.println( Thread.currentThread().getName() + "::" + number );
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.incr();//+1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"AA").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"BB").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"CC").start();
//创建线程
new Thread( () -> {
for (int i = 0; i <= 10; i++) {
try {
share.decr();//-1
}catch (InterruptedException e){
e.printStackTrace();
}
}
} ,"DD").start();
}
}
控制台结果如上图,此时虚假问题已经解决
Lock方案
package com.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
while(number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
控制台如上图:
虚假唤醒
正常操作:
假设AA(+1),BB(-1),CC(+1),DD(-1)
开始AA获得了 +1
如果CC抢到了这个线程,就是等待,
如果AA再抢到了这个线程,还是等待
如果BB抢到了,则 -1 操作
非正常操作:
开始AA获得了 +1
如果CC抢到了这个线程,就是等待,
结果AA又抢到了+1,执行如下结果
此时CC被唤醒了,上一个CC是在等待,则此时CC已经醒来了
而if判断只判断一次,解决办法是给if判断变成while判断,
当在线程中醒来时,while将继续判断