【併發程式設計】- 執行緒篇
執行緒
-
1. 簡介
-
1)定義
-
現代作業系統在執行一個程式時,會為其建立一個程序。例如,啟動一個Java程式,作業系統就會建立一個Java程序。現代作業系統排程的最小單元是執行緒,也叫輕量級程序(LightWeightProcess),在一個程序裡可以建立多個執行緒,這些執行緒都擁有各自的計數器、堆疊和區域性變數等屬性,並且能夠訪問共享的記憶體變數。處理器在這些執行緒上快速切換,讓使讀者感覺到這些執行緒在同時執行。
-
2) 那麼為何要使用多執行緒
-
更多的處理器核心
-
更快的響應
-
更好的程式設計模型
-
3)優先順序
-
由於是作業系統給執行緒分配時間片的處理方式。那麼便可以通過優先順序設定處理的先後,確保處理器不會被獨佔。
-
4)狀態
- 狀態變換圖
-
2. 啟動與終止
-
1)啟動與中斷
-
啟動:當前執行緒(即parent執行緒)同步告知Java虛擬機器,只要執行緒規劃器空閒,應立即啟動呼叫start()方法的執行緒。
-
中斷:即是執行緒的一個標識位屬性,通過呼叫該執行緒的interrupt()方法對其進行中斷操作。也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位,當丟擲InterruptedException之前,
Java虛擬機器會先將該執行緒的中斷標識位清除
,即此時呼叫isInterrupted()方法將會返回false。 -
2)安全地終止執行緒(優雅)
-
通過設定一個boolean變數控制
public class Shutdown { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); // 睡眠1秒,main執行緒對CountThread進行中斷,使CountThread能夠感知中斷而結束 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two, "CountThread"); countThread.start(); // 睡眠1秒,main執行緒對Runner two進行取消,使CountThread能夠感知on為falseer結束 TimeUnit.SECONDS.sleep(1); two.cancel(); } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " + i); } public void cancel() { on = false; } } }
-
3. 執行緒間通訊
-
1)volatile和synchronized
- java在多個執行緒訪問一個物件或物件的成員變數,每個執行緒擁有其拷貝,將其放入各自的快取中,這樣可以加速程式的執行,故執行緒看到的變數並不一定是最新的。假如需要最新的,此時需要通過
volatile
通知執行緒間從共享記憶體
中獲取,並重新整理回各自的工作記憶體
,即是保證了對所有執行緒對變數訪問的可見性。 - 關鍵字
synchronized
可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。- 同步塊的實現使用了
monitorenter
和monitorexit
指令。 - 同步方法則是依靠方法修飾符上的
ACC_SYNCHRONIZED
來完成的。
- 同步塊的實現使用了
本質:對一個物件的監視器(monitor
)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。
-
任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。
-
-
2)等待/通知機制
-
等待/通知機制,是指一個執行緒A呼叫了物件O的
wait()
方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()
或者notifyAll()
方法,執行緒A收到通知後從物件O的wait()
方法返回,進而執行後續操作。
- 解決兩個問題
- 確保及時性
- 降低開銷
- 方法
notify
: 通知一個物件上等待的執行緒,使其從wait方法返回,而返回的前提是執行緒獲取到了物件的鎖。notifyAll
:通知所有等待在該物件上的執行緒。wait
: 呼叫該方法的執行緒進入WAITING
狀態,只有等待另外執行緒的通知或被中斷才會返回,需要注意,呼叫wait
方法後,會釋放物件的鎖。wait(long)
: 超時等待一段時間,這裡的引數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回。wait(long,int)
: 對於超時時間更細粒度的控制,可以達到納秒。
- 使用注意事項
- 使用
wait()
、notify()
和notifyAll()
時需要先對呼叫物件加鎖。 - 呼叫
wait()
方法後,執行緒狀態由RUNNING
變為WAITING
,並將當前執行緒放置到物件的等待佇列。 notify()
或notifyAll()
方法呼叫後,等待執行緒依舊不會從wait()
返回,需要呼叫notify()
或notifAll()
的執行緒釋放鎖之後,等待執行緒才有機會從wait()
返回。notify()
方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll()
方法則是將等待佇列中所有的執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING
變為BLOCKED
。- 從
wait()
方法返回的前提是獲得了呼叫物件的鎖。
- 使用
上圖中,WaitThread首先獲取了物件的鎖,然後呼叫物件的
wait()
方法,從而放棄了鎖並進入了物件的等待佇列WaitQueue中,進入等待狀態。由於WaitThread釋放了物件的鎖,NotifyThread隨後獲取了物件的鎖,並呼叫物件的notify()
方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()
方法返回繼續執行。
-
3)Thread.join()
定義:如果一個執行緒A執行了thread.join()語句,當前執行緒A等待thread執行緒終止之後才從thread.join()返回。
-
4)ThreadLocal
ThreadLocal
,即執行緒變數,是一個以ThreadLocal
物件為鍵、任意物件為值的儲存結構。ThreadLocal
為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。- 每個Thread物件內部都維護了一個ThreadLocalMap這樣一個
ThreadLocal
的Map,可以存放若干個ThreadLocal。 - 當我們在呼叫get()方法的時候,先獲取當前執行緒,然後獲取到當前執行緒的ThreadLocalMap物件,如果非空,那麼取出ThreadLocal的value,否則進行初始化,初始化就是將initialValue的值set到
ThreadLocal
中。 - 當我們呼叫set()方法的時候,很常規,就是將值設定進
ThreadLocal
中。
- 每個Thread物件內部都維護了一個ThreadLocalMap這樣一個
- 採用
ThreadLocal
根本就沒有競爭。
記憶體洩露:
實際上ThreadLocalMap
中使用的key為ThreadLocal的弱引用,弱引用的特點是,如果這個物件只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。所以如果ThreadLocal
沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來ThreadLocalMap
中使用這個ThreadLocal
的key也會被清理掉。但是,value是強引用,不會被清理,這樣一來就會出現key為null的value。ThreadLocalMap
實現中已經考慮了這種情況,在呼叫set()、get()、remove()方法的時候,會清理掉key為null的記錄。如果說會出現記憶體洩漏,那只有在出現了key為null的記錄後,沒有手動呼叫remove()方法,並且之後也不再呼叫get()、set()、remove()方法的情況下。