線程通信:以去Bank 取錢爲例
wait/notify
只能由同一個對象鎖的持有者線程調用,寫在同步代碼塊裏面。
wait():使當前線程等待,加入該對象的等待集合中,並釋放當前對象的鎖。wait() 後面的代碼是不執行的!
notify/notifyAll: 喚醒一個或所有正在等待這個對象鎖的線程。
代碼示例:
public void waitNotifyNormalTest() throws InterruptedException {
new Thread(() -> {
try {
synchronized (this) {
while (obj == null) {//防止僞喚醒,條件判斷放入對象鎖中
System.out.println("1、木有錢,進入等待");
wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2、取到錢");
}).start();
//主線程休眠,子線程先執行,進入wait(),並讓出鎖:this
Thread.sleep(3000L);
synchronized (this) {
obj = new Object();
notifyAll();
System.out.println("3、通知消費者");
}
}
注意:雖然wait會自動解鎖,但是對調用wait(),notify()/notifyAll() 有順序要求,如果在notify() 調用只有才調用的wait(),線程會永遠處於WAITING 狀態,代碼示例如下:
/**
* wait/notify 中先執行notify,在執行wait,無法喚醒等待的線程進入死鎖狀態
*
* @throws InterruptedException
*/
public void waitNotifyDeadLockTest() throws InterruptedException {
new Thread(() -> {
while (obj == null) {
try {
//子線程休眠5s,時間長於主線程休眠時間,因此主線程先拿到鎖,先執行notifyAll()
Thread.sleep(5000L);
synchronized (this) {
System.out.println("1、木有錢,進入等待");
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("2、取到錢");
}).start();
//主線程休眠3s
Thread.sleep(3000L);
synchronized (this) {
obj = new Object();
notifyAll();
System.out.println("3、通知消費者");
}
}
注意此處一定爲JDK 8 lamda 表達式的寫法 ,如果寫爲如下代碼,則在run() 中的this與主函數的this不爲同一個對象,也有違背wait(),notify()/notifyAll() 的使用條件
public void waitNotifyDeadLockTest() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
while (obj == null) {
try {
//子線程休眠5s,時間長於主線程休眠時間,因此主線程先拿到鎖,先執行notifyAll()
Thread.sleep(5000L);
synchronized (this) {
System.out.println("1、木有錢,進入等待");
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2、取到錢");
}
}
}).start();
//主線程休眠3s
Thread.sleep(3000L);
synchronized (this) {
obj = new Object();
notifyAll();
System.out.println("3、通知消費者");
}
}
park/unpark
線程調用park則需等待,unpark則喚醒。
- park()/unpark() 無調用順序要求;
- 多次調用unpark() 後,不會疊加,再調用park() ,線程會直接運行而不是等待;
- 在多次調用unpark() 後,再多次調用park(),第一次會直接運行,後續的調用會進入等待;
- 需要注意的是:如果在同步代碼塊中調用park() 則不會釋放鎖,因此使用時請注意!
正常使用park(),unpark()的示例:
public void parkUnparkNormalTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while (obj == null) {
System.out.println("1.木有錢,進入等待");
LockSupport.park();
}
System.out.println("2.取到錢,回家!");
});
consumerThread.start();
Thread.sleep(3000L);
obj = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3.通知消費者");
}
使用park(),unpark() 出現死鎖的場景:
/**
* 如果未使用lamda表達式的寫法,在重寫run() 中調用的this並非局部變量的this
*
* @throws InterruptedException
*/
public void parkUnparkDeadLockTest() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
if (obj == null) { // 如果木有錢,進入等待
System.out.println("1、木有錢,進入等待");
// 當前線程拿到鎖,然後掛起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、取到錢,回家");
});
consumerThread.start();
Thread.sleep(3000L);
obj = new Object();
synchronized (this) {
LockSupport.unpark(consumerThread);
System.out.println("3.通知消費者");
}
}
僞喚醒
僞喚醒是指線程並非因爲notify,notifyAll,unpark 等API調用而喚醒,而是更底層的原因導致。
使用場景:在前面的parkUnparkDeadLockTest() 中子線程使用if 語句來判斷是否進入等待狀態可能會遇到僞喚醒問題,因此應該爲在循環中檢查等待條件 ,原因是處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。
線程封閉
多線程訪問共享可變數據時,涉及到線程間的數據同步問題。然而線程封閉是指:將數據封閉在各自的線程中從而避免使用同步的技術。線程封閉具體現爲:ThreadLocal,局部變量。
ThreadLocal
ThreadLocal 是JAVA 中一種特殊的變量
它是一個線程級別的變量,每個線程都有一個ThreadLocal,在併發模式下絕對安全的變量。
它會自動在每一個線程上創建一個T的副本,副本之間彼此獨立,互不影響。可以使用ThreadLocal存儲一些參數,以便在線程中多個方法使用,用來代替方法傳參的做法。
相當於一個Map<Thread,T> 每個線程要用這個T的時候,就用當前線程取Map對應的值。
ThreadLocal使用方式如下:
public class ThreadLocalTest {
/**
* 每個線程都有一個獨立的副本,互不干擾
*/
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new ThreadLocalTest().test();
}
public void test() throws InterruptedException {
threadLocal.set("大家好,我是主線程");
String value = threadLocal.get();
System.out.println("線程執行之前,主線程取到的值:" + value);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程第一次獲取的值爲:" + threadLocal.get());
threadLocal.set("Hello,我是子線程!");
System.out.println("子線程第二次獲取的值爲:" + threadLocal.get());
}
}).start();
Thread.sleep(5000L);
System.out.println("線程執行結束,主線程獲取的值爲:" + threadLocal.get());
}
}
輸出結果爲:
線程執行之前,主線程取到的值:大家好,我是主線程
子線程第一次獲取的值爲:null
子線程第二次獲取的值爲:Hello,我是子線程!
線程執行結束,主線程獲取的值爲:大家好,我是主線程
局部變量/ 棧封閉
之前在JVM執行原理你是否已經get中講到線程獨佔部分的本地方法棧就是屬於棧封閉。局部變量的固有屬性之一就是封閉在線程中。