并發複習筆記之第四章(多線程使用及其通信)
想看後續請持續關注
以下來源有書籍 深入了解 JVM 虛拟機,java 并發程式設計的藝術,深入淺出多線程,阿裡巴巴技術手冊以及一些公衆号 CS-Notes,JavaGuide,以及一些大廠高頻面試題吐血總結,以及狂神說視訊筆記,目的在于通過問題來複習整個多線程,已下是全部章節,覺得不錯點個贊評論收藏三連一下,您的鼓勵就是我繼續創作的最大動力!!!!文章目錄
- 4.多線程的使用
- 4.1 多線程的四種建立方法
- 4.1.1 實作接口和直接繼承Thread類哪個好?
- 4.1.2 Runnable實作和 Callable 實作有什麼差別
- 4.2 線程間的通信
- 4.2.1 線程通信之 Join()
- 4.2.2 線程通信之wait()/notify()/notifyAll
- 4.2.2.1 wait 為什麼會釋放鎖
- 4.2.2.2 經典的等待通知範例
- 4.2.2.3 虛假喚醒是什麼,如何避免虛假喚醒?
- 4.2.2.4 使用 notify 注意?
- 4.2.3 簡單介紹下 ThreadLocal
4.多線程的使用
4.1 多線程的四種建立方法
- Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
需要實作接口中的 run() 方法。
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
使用 Runnable 執行個體再建立一個 Thread 執行個體,然後調用 Thread 執行個體的 start() 方法來啟動線程。
- 實作 Callable 接口
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
一個類實作 callable 接口 -> 建立一個執行個體 送入 ->FutureTask implements RunnableFuture extends Runnable 放入 Thread 内部
- 繼承 Thread 類
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
同樣也是需要實作 run() 方法,因為 Thread 類也實作了 Runable 接口。
當調用 start() 方法啟動一個線程時,虛拟機會将該線程放入就緒隊列中等待被排程,當一個線程被排程時會執行該線程的 run() 方法。
- 線程池的方法
轉線程池章節
4.1.1 實作接口和直接繼承Thread類哪個好?
- 實作接口好,因為 java 隻有單繼承,如果繼承了之後就無法繼承其它類,但可以實作多個接口
- 類可能隻要求可執行就行,繼承整個 Thread 類開銷過大。
4.1.2 Runnable實作和 Callable 實作有什麼差別
- Runnable執行方法是run(),Callable是call()
- 實作Runnable接口的任務線程無傳回值;實作Callable接口的任務線程能傳回執行結果
- call方法可以抛出異常,run方法若有異常隻能在内部消化
4.2 線程間的通信
4.2.1 線程通信之 Join()
如果一個線程A執行了thread.join()語句,其含義是:目前線程A等待thread線程終止之後才 從thread.join()傳回。
public class Juc {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread; public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
每個線程終止的前提是前驅線程的終止,每個線程等待前驅線程 終止後,才從join()方法傳回,這裡涉及了等待/通知機制(等待前驅線程結束,接收前驅線程結 束通知)。
我們可以看 Join()源碼
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
由源碼我們可以看出,調用 join 方法的線程死去之後,才會從 join()方法處傳回
4.2.2 線程通信之wait()/notify()/notifyAll
方法 | 描述 |
---|---|
wait() | 調用該方法進入 wating 狀态,需要被通知或者中斷後會傳回,注意 wait 方法會釋放鎖 |
notify() | 通知一個在對象等待的線程,從 wait() 方法傳回,特别注意不會立刻釋放鎖,執行完代碼塊才會釋放 |
notifyAll() | 通知所有等待隊列的線程 |
wait() | 逾時等待一會,如果逾時了,就傳回 |
在圖中,WaitThread首先擷取了對象的鎖,然後調用對象的wait()方法,進而放棄了鎖 并進入了對象的等待隊列WaitQueue中,進入等待狀态。由于WaitThread釋放了對象的鎖, NotifyThread随後擷取了對象的鎖,并調用對象的notify()方法,将WaitThread從WaitQueue移到 SynchronizedQueue中,此時WaitThread的狀态變為阻塞狀态。NotifyThread釋放了鎖之後, WaitThread再次擷取到鎖并從wait()方法傳回繼續執行。
4.2.2.1 wait 為什麼會釋放鎖
這是因為,如果沒有釋放鎖,那麼其它線程就無法進入對象的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒挂起的線程,造成死鎖。
4.2.2.2 經典的等待通知範例
synchronized(對象) {
while(條件不滿足) {
對象.wait();
}
對應的處理邏輯
}
synchronized(對象) {
改變條件 對象.notifyAll();
}
4.2.2.3 虛假喚醒是什麼,如何避免虛假喚醒?
虛假喚醒:線上程的 等待/喚醒 的過程中,等待的線程被喚醒後,在條件不滿足的情況依然繼續向下運作了。
為什麼使用 if()會出現虛假喚醒
- 線程被喚醒後,會從 wait() 處開始繼續往下執行;
- while 被掉換成 if 後出現了虛假喚醒,出現了我們不想要的結果;
- 因為if隻會執行一次,執行完會接着向下執行if()外邊的而while不會,直到條件滿足才會向下執行while()外邊的
4.2.2.4 使用 notify 注意?
- notify()或notifyAll()方法調用後,等待線程依舊不會從wait()傳回,需要調用notify()或 notifAll()的線程釋放鎖之後,等待線程才有機會從wait()傳回。注意與 wait 方法
4.2.3 簡單介紹下 ThreadLocal
通常情況下,我們建立的變量是可以被任何一個線程通路并修改的。如果想實作每一個線程都有自己的專屬本地變量該如何解決呢? JDK 中提供的
ThreadLocal
類正是為了解決這樣的問題。
ThreadLocal
類主要解決的就是讓每個線程綁定自己的值,可以将
ThreadLocal
類形象的比喻成存放資料的盒子,盒子中可以存儲每個線程的私有資料。
如果你建立了一個
ThreadLocal
變量,那麼通路這個變量的每個線程都會有這個變量的本地副本,這也是
ThreadLocal
變量名的由來。他們可以使用
get()
和
set()
方法來擷取預設值或将其值更改為目前線程所存的副本的值,進而避免了線程安全問題。