多線程並發操作一直都是學習和工作過程中的難點,一般而言,在多個線程共享資源時,我們通常會使用synchronized代碼塊的同步,並通過wait()、notify()和notifyAll()來喚醒或者等待線程(這三個方法必須使用在同步代碼塊或同步方法中,被同步監視器調用,否則會拋出異常)。
還是通過經典的生產者和消費者案例引出虛假喚醒的問題
public class SpuriousWakeupDemo {
public static void main(String[] args) {
ShareData resources = new ShareData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.decrement();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "consumer").start();
}
}
/**
* 資源類
*/
class ShareData{
private int number = 0;//初始值為零的一個變量
public synchronized void increment() throws InterruptedException {
//1判斷
if (number != 0) {
this.wait();
}
//2干活
++number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1判斷
if (number == 0) {
this.wait();
}
// 2干活
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3通知
this.notifyAll();
}
}
多次測試結果如下:
在main方法中通過匿名內部類的方式創建了兩個線程,一個作為生產者,一個作為消費者。上面這個代碼正常運行,也並沒有出現什么安全問題。但是我們卻忽略了一個重要的點!!過我們們改動代碼,在main方法中多加如兩個生產者和消費者如下:
public class SpuriousWakeupDemo {
public static void main(String[] args) {
ShareData resources = new ShareData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer1").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.decrement();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "consumer1").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer2").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
resources.decrement();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "consumer2").start();
}
}
多次測試,出現了以下結果:
我們發現ShareData類中的共享變量number即使在不等於0的情況下依然在自增,increment()方法明明已經通過了synchronized進行了加鎖,並且在方法內部做了判斷當number!=0就將線程等待,為什么還是會出現number>0的情況?
解決上述問題很簡單,就是將if判斷改為while判斷,上述問題就再也沒有出現。
class ShareData{
private int number = 0;//初始值為零的一個變量
public synchronized void increment() throws InterruptedException {
//1判斷
while (number != 0) {
this.wait();
}
//2干活
++number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1判斷
while (number == 0) {
this.wait();
}
// 2干活
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3通知
this.notifyAll();
}
}
這個問題的關鍵就在於當線程被喚醒時,從哪里運行。當我們使用if作為條件判斷時,分別在wait()方法前后加兩條打印語句
class ShareData{
private int number = 0;//初始值為零的一個變量
public synchronized void increment() throws InterruptedException {
//1判斷
if (number != 0) {
System.out.println("生產線程"+Thread.currentThread().getName()+"准備休眠");
this.wait();
System.out.println("生產線程"+Thread.currentThread().getName()+"休眠結束");
}
//2干活
++number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1判斷
if (number == 0) {
this.wait();
}
// 2干活
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3通知
this.notifyAll();
}
}
控制台打印結果如下:
將if改為while條件后述情況就不會存在,由於是while()循環,所有被等待的線程在獲取到cpu執行權利后一定會進行條件判斷,不會出現兩個生產者交替獲得CPU執行權將number+1的情況(也不會出現兩個消費者交替獲得cpu執行權的情況)
如圖: