1. 程式人生 > >Java高併發系列——檢視閱讀

Java高併發系列——檢視閱讀

Java高併發系列——檢視閱讀 參考 java高併發系列 liaoxuefeng Java教程 CompletableFuture AQS原理沒講,需要找資料補充。 JUC中常見的集合原來沒講,比如ConcurrentHashMap最常用的,後面的都很泛,沒有深入,虎頭蛇尾。 阻塞佇列講得不夠深入。 併發概念詞 同步(Synchronous)和非同步(Asynchronous) 同步和非同步通常來形容一次方法呼叫,同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的操作。 舉例: 拿泡泡麵來說,我們把整個泡泡麵的過程分3個步驟: 1. 燒水 2. 泡麵加調味加蛋 3. 倒開水泡麵 如果我們泡泡麵的時候是按照這三個步驟,等開水開了再加調味加蛋,最後倒開水泡麵,這時候就是同步步驟;而如果我們燒開水的同時把泡麵加調味加蛋準備好,就可以省去燒水的同步等待時間,這就是非同步。 併發(Concurrency)和並行(Parallelism) 併發和並行是兩個非常容易被混淆的概念。他們都可以表示兩個或者多個任務一起執行,但是側重點有所不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是序列的(等待阻塞等),而並行是真正意義上的“同時執行” 。 舉例: 大家排隊在一個咖啡機上接咖啡,交替執行,是併發;兩臺咖啡機上面接咖啡,是並行。 併發說的是在一個時間段內,多件事情在這個時間段內交替執行。 並行說的是多件事情在同一個時刻同時發生。 如果系統內只有一個CPU,而使用多程序或者多執行緒任務,那麼真實環境中這些任務不可能是真實並行的,畢竟一個CPU一次只能執行一條指令,在這種情況下多程序或者多執行緒就是併發的,而不是並行的(作業系統會不停地切換多工) 。 臨界區 臨界區用來表示一種公共資源或者說共享資料,可以被多個執行緒使用,但是每一次只能有一個執行緒使用它,一旦臨界區資源被佔用,其他執行緒要想使用這個資源就必須等待。 阻塞(Blocking)和非阻塞(Non-Blocking) 阻塞和非阻塞通常用來形容很多執行緒間的相互影響。比如一個執行緒佔用了臨界區資源,那麼其他所有需要這個資源的執行緒就必須在這個臨界區中等待。等待會導致執行緒掛起,這種情況就是阻塞。 非阻塞的意思與之相反,它強調沒有一個執行緒可以妨礙其他執行緒執行,所有的執行緒都會嘗試不斷向前執行。 死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock) 死鎖、飢餓和活鎖都屬於多執行緒的活躍性問題 。 死鎖:兩個執行緒都持有獨佔的資源(鎖),同時又互相嘗試獲取對方獨佔的資源(鎖),這時候雙方都沒有釋放自己的獨佔資源,導致永遠也獲取不到阻塞等待下去。 飢餓是指某一個或者多個執行緒因為種種原因無法獲得所要的資源,導致一直無法執行。一種比如它的優先順序可能太低,而高優先順序的執行緒不斷搶佔它需要的資源,導致低優先順序執行緒無法工作。另一種如某一個執行緒一直佔著關鍵資源不放(例子:單執行緒池裡submit一個執行緒任務,而該執行緒又往該單執行緒池裡submit一個新的任務並等待結果返回,因為執行緒池是單執行緒池,所以便一種套娃著),導致其他需要這個資源的執行緒無法正常執行,這種情況也是飢餓的一種。與死鎖相比,飢餓還是有可能在未來一段時間內解決的(比如,高優先順序的執行緒已經完成任務,不再瘋狂執行)。 活鎖:當兩個執行緒都秉承著“謙讓”的原則(導致死迴圈),主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個執行緒間跳動,而沒有一個執行緒可以同時拿到所有資源正常執行。這種情況就是活鎖。 擴充套件 通過jstack檢視到死鎖資訊 1、使用jps找到執行程式碼的程序ID,啟動類名為DeadLockTest(main函式所在類)的程序ID為11084 jps 2、通過jstack命令找到java程序中死鎖的執行緒鎖資訊,執行jstack -l 11084 jstack -l 11084 最後輸出: =================================================== "thread2": at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:331) - waiting to lock <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1) - locked <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2) at java.lang.Thread.run(Thread.java:748) "thread1": at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:282) - waiting to lock <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2) - locked <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1) at java.lang.Thread.run(Thread.java:748) 併發級別 由於臨界區的存在,多執行緒之間的併發必須受到控制。根據控制併發的策略,我們可以把併發的級別分為阻塞、無飢餓、無障礙、無鎖、無等待5種。 阻塞——悲觀鎖 一個執行緒是阻塞的,那麼在其他執行緒釋放資源之前,當前執行緒無法繼續執行。當我們使用synchronized關鍵字或者重入鎖時,我們得到的就是阻塞的執行緒。 synchronize關鍵字和重入鎖都試圖在執行後續程式碼前,得到臨界區的鎖,如果得不到,執行緒就會被掛起等待,直到佔有了所需資源為止。 例子:synchronize或ReentrantLock。 無飢餓(Starvation-Free)——公平與非公平鎖 表示非公平鎖與公平鎖兩種情況 。如果執行緒之間是有優先順序的,那麼執行緒排程的時候總是會傾向於先滿足高優先順序的執行緒。 對於非公平鎖來說,系統允許高優先順序的執行緒插隊。這樣有可能導致低優先順序執行緒產生飢餓。但如果鎖是公平的,按照先來後到的規則,那麼飢餓就不會產生 。 例子:ReentrantLock 預設採用非公平鎖,除非在構造方法中傳入引數 true 。 //預設 public ReentrantLock() { sync = new NonfairSync(); } //傳入true or false public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 無障礙(Obstruction-Free)——樂觀鎖CAS 無障礙是一種最弱的非阻塞排程。兩個執行緒如果無障礙地執行,那麼不會因為臨界區的問題導致一方被掛起。 對於無障礙的執行緒來說,一旦檢測到這種情況,它就會立即對自己所做的修改進行回滾,確保資料安全。但如果沒有資料競爭發生,那麼執行緒就可以順利完成自己的工作,走出臨界區。 無障礙的多執行緒程式並不一定能順暢執行。因為當臨界區中存在嚴重的衝突時,所有的執行緒可能都會不斷地回滾自己的操作,而沒有一個執行緒可以走出臨界區。這種情況會影響系統的正常執行。所以,一種可行的無障礙實現可以依賴一個"一致性標記"來實現。 資料庫中樂觀鎖(通過版本號或者時間戳實現)。表中需要一個欄位version(版本號),每次更新資料version+1,更新的時候將版本號作為條件進行更新,根據更新影響的行數判斷更新是否成功,虛擬碼如下: 1.查詢資料,此時版本號為w_v 2.開啟事務 3.做一些業務操作 //此行會返回影響的行數c 4.update t set version = version+1 where id = 記錄id and version = w_v; 5.if(c>0){ //提交事務 }else{ //回滾事務 } 多個執行緒更新同一條資料的時候,資料庫會對當前資料加鎖,同一時刻只有一個執行緒可以執行更新語句。 無鎖(Lock-Free) 無鎖的併發都是無障礙的。在無鎖的情況下,所有的執行緒都能嘗試對臨界區進行訪問,但不同的是,無鎖的併發保證必然有一個執行緒能夠在有限步內完成操作離開臨界區。 (注意有限步) 在無鎖的呼叫中,一個典型的特點是可能會包含一個無窮迴圈。在這個迴圈中,執行緒會不斷嘗試修改共享變數。如果沒有衝突,修改成功,那麼程式退出,否則繼續嘗試修改。但無論如何,無鎖的並行總能保證有一個執行緒是可以勝出的,不至於全軍覆沒。至於臨界區中競爭失敗的執行緒,他們必須不斷重試,直到自己獲勝。如果運氣很不好,總是嘗試不成功,則會出現類似飢餓的先寫,執行緒會停止。(併發量太大時會出現飢餓,這時候有必要改成阻塞鎖) 下面就是一段無鎖的示意程式碼,如果修改不成功,那麼迴圈永遠不會停止。 while(!atomicVar.compareAndSet(localVar, localVar+1)){ localVal = atomicVar.get();} 無等待——讀寫鎖 無鎖只要求有一個執行緒可以在有限步內完成操作,而無等待則在無鎖的基礎上更進一步擴充套件。無等待要求所有執行緒都必須在有限步內完成,這樣不會引起飢餓問題。如果限制這個步驟的上限,對迴圈次數的限制不同。分為為 1. 有界無等待 2. 執行緒數無關的無等待。 一種典型的無等待結果就是RCU(Read Copy Update)。它的基本思想是,對資料的讀可以不加控制。因此,所有的讀執行緒都是無等待的,它們既不會被鎖定等待也不會引起任何衝突。但在寫資料的時候,先獲取原始資料的副本,接著只修改副本資料(這就是為什麼讀可以不加控制),修改完成後,在合適的時機回寫資料。 並行的兩個重要定律 為什麼要使用並行程式 ? 第一,為了獲得更好的效能; 第二,由於業務模型的需要,確實需要多個執行實體。 關於並行程式對效能的提高定律有二,Amdahl(阿姆達爾)定律和Gustafson(古斯塔夫森 )定律。 加速比定義:加速比 = 優化前系統耗時 / 優化後系統耗時 根據Amdahl定律,使用多核CPU對系統進行優化,優化的效果取決於CPU的數量,以及系統中序列化程式的比例。CPU數量越多,序列化比例越低,則優化效果越好。僅提高CPU數量而不降低程式的序列化比例,也無法提高系統的效能。 根據Gustafson定律,我們可以更容易地發現,如果序列化比例很小,並行化比例很大,那麼加速比就是處理器的個數。只要不斷地累加處理器,就能獲得更快的速度。 總結 Gustafson定律和Amdahl定律的角度不同 Amdahl強調:當序列換比例一定時,加速比是有上限的,不管你堆疊多少個CPU參與計算,都不能突破這個上限。 Gustafson定律強調:如果可被並行化的程式碼所佔比例足夠大,那麼加速比就能隨著CPU的數量線性增長。 總的來說,提升效能的方法:想辦法提升系統並行的比例(減少序列比例),同時增加CPU數量。 附圖: Amdahl公式的推倒過程 其中n表示處理器個數,T表示時間,T1表示優化前耗時(也就是隻有1個處理器時的耗時),Tn表示使用n個處理器優化後的耗時。F是程式中只能序列執行的比例。 Gustafson公式的推倒過程 併發程式設計中JMM相關的一些概念 JMM(JAVA Memory Model:Java記憶體模型),由於併發程式要比序列程式複雜很多,其中一個重要原因是併發程式中資料訪問一致性和安全性將會受到嚴重挑戰。如何保證一個執行緒可以看到正確的資料呢? Q:如何保證一個執行緒可以看到正確的資料呢? A:通過Java記憶體模型管理,JMM關鍵技術點都是圍繞著多執行緒的原子性、可見性、有序性來建立的 。 原子性 原子性是指操作是不可分的,要麼全部一起執行,要麼不執行。java中實現原子操作的方法大致有2種:鎖機制、無鎖CAS機制。 可見性 可見性是指一個執行緒對共享變數的修改,對於另一個執行緒來說是否是可見的。 看一下java執行緒記憶體模型及規則: - 我們定義的所有變數都儲存在 主記憶體中。 - 每個執行緒都有自己 獨立的工作記憶體,裡面儲存該執行緒使用到的變數的副本(主記憶體中該變數的一份拷貝) - 執行緒對共享變數所有的操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫(不能越級) - 不同執行緒之間也無法直接訪問其他執行緒的工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來進行。(同級不能相互訪問) 例子:執行緒需要修改一個共享變數X,需要先把X從主記憶體複製一份到執行緒的工作記憶體,在自己的工作記憶體中修改完畢之後,再從工作記憶體中回寫到主記憶體。如果執行緒對變數的操作沒有刷寫回主記憶體的話,僅僅改變了自己的工作記憶體的變數的副本,那麼對於其他執行緒來說是不可見的。而如果另一個執行緒的變數沒有讀取主記憶體中的新的值,而是使用舊的值的話,同樣的也可以列為不可見。 共享變數可見性的實現原理: 執行緒A對共享變數的修改要被執行緒B及時看到的話,需要進過以下2個步驟: 1.執行緒A在自己的工作記憶體中修改變數之後,需要將變數的值重新整理到主記憶體中 。 2.執行緒B要把主記憶體中變數的值更新到工作記憶體中。 關於執行緒可見性的控制,可以使用volatile、synchronized、鎖來實現。 有序性 有序性指的是程式按照程式碼的先後順序執行。這是因為為了效能優化,編譯器和處理器會進行指令重排序,有時候會改變程式語句的先後順序。 例子: 在單例模式的實現上有一種雙重檢驗鎖定的方式,因為指令重排導致獲取併發時獲取到的單例可能是未正確初始化的單例。程式碼如下: public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } } 我們先看 instance=newSingleton(); 未被編譯器優化的操作: 1. 指令1:分配一款記憶體M 2. 指令2:在記憶體M上初始化Singleton物件 3. 指令3:將M的地址賦值給instance變數 編譯器優化後的操作指令: 1. 指令1:分配一塊記憶體M 2. 指令2:將M的地址賦值給instance變數 3. 指令3:在記憶體M上初始化Singleton物件 現在有2個執行緒,剛好執行的程式碼被編譯器優化過,過程如下: 最終執行緒B獲取的instance是沒有初始化的,此時去使用instance可能會產生一些意想不到的錯誤。 可以使用volatile修飾變數或者換成採用靜態內部內的方式實現單例。 深入理解程序和執行緒 程序 程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。程式是指令、資料及其組織形式的描述,程序是程式的實體。 程序具有的特徵: - 動態性:程序是程式的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的 - 併發性:任何程序都可以同其他進行一起併發執行 - 獨立性:程序是系統進行資源分配和排程的一個獨立單位 - 結構性:程序由程式,資料和程序控制塊三部分組成 執行緒 執行緒是輕量級的程序,是程式執行的最小單元,使用多執行緒而不是多程序去進行併發程式的設計,是因為執行緒間的切換和排程的成本遠遠小於程序。 我們用一張圖來看一下執行緒的狀態圖: Java中執行緒的狀態分為6種 ,在java.lang.Thread中的State列舉中有定義,如: public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;} Java執行緒的6種狀態及切換 1. 初始(NEW):表示剛剛建立的執行緒,但還沒有呼叫start()方法。 2. 執行(RUNNABLE):執行狀態.Java執行緒中將就緒(ready)和執行中(running)兩種狀態籠統的稱為“執行”。 執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的執行緒在獲得CPU時間片後變為執行中狀態(running)。 3. 阻塞(BLOCKED):阻塞狀態,表示執行緒阻塞於鎖。當執行緒在執行的過程中遇到了synchronized同步塊,但這個同步塊被其他執行緒已獲取還未釋放時,當前執行緒將進入阻塞狀態,會暫停執行,直到獲取到鎖。當執行緒獲取到鎖之後,又會進入到執行狀態(RUNNABLE)(維護在同步佇列中) 4. 等待(WAITING):等待狀態。進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。和TIMED_WAITING都表示等待狀態,區別是WAITING會進入一個無時間限制的等,而TIMED_WAITING會進入一個有限的時間等待,那麼等待的執行緒究竟在等什麼呢?一般來說,WAITING的執行緒正式在等待一些特殊的事件,比如,通過wait()方法等待的執行緒在等待notify()方法,而通過join()方法等待的執行緒則會等待目標執行緒的終止。一旦等到期望的事件,執行緒就會再次進入RUNNABLE執行狀態。(維護在等待佇列中) 5. 超時等待(TIMED_WAITING):超時等待狀態。該狀態不同於WAITING,它可以在指定的時間後自行返回。 6. 終止(TERMINATED):結束狀態,表示該執行緒已經執行完畢。 幾個方法的比較 Thread.sleep(long millis),一定是當前執行緒呼叫此方法,當前執行緒進入TIMED_WAITING狀態,但不釋放物件鎖,millis後執行緒自動甦醒進入就緒狀態。作用:給其它執行緒執行機會的最佳方式。 Thread.yield(),一定是當前執行緒呼叫此方法,當前執行緒放棄獲取的CPU時間片,但不釋放鎖資源,由執行狀態變為就緒狀態,讓OS再次選擇執行緒。作用:讓相同優先順序的執行緒輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由使用者指定暫停多長時間。 thread.join()/thread.join(long millis),當前執行緒裡呼叫其它執行緒t的join方法,當前執行緒進入WAITING/TIMED_WAITING狀態,當前執行緒不會釋放已經持有的物件鎖。執行緒t執行完畢或者millis時間到,當前執行緒一般情況下進入RUNNABLE狀態,也有可能進入BLOCKED狀態(因為join是基於wait實現的)。 obj.wait(),當前執行緒呼叫物件的wait()方法,當前執行緒釋放物件鎖,進入等待佇列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。 obj.notify()喚醒在此物件監視器上等待的單個執行緒,選擇是任意性的。notifyAll()喚醒在此物件監視器上等待的所有執行緒。 LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 當前執行緒進入WAITING/TIMED_WAITING狀態。對比wait方法,不需要獲得鎖就可以讓執行緒進入WAITING/TIMED_WAITING狀態,需要通過LockSupport.unpark(Thread thread)喚醒。 執行緒的狀態圖 程序與執行緒的一個簡單解釋 計算機的核心是CPU,整個作業系統就像一座工廠,時刻在執行 。程序就好比工廠的車間 ,它代表CPU所能處理的單個任務 。執行緒就好比車間裡的工人。一個程序可以包括多個執行緒。 車間的空間是工人們共享的,比如許多房間是每個工人都可以進出的。這象徵一個程序的記憶體空間是共享的,每個執行緒都可以使用這些共享記憶體。 每間房間的大小不同,有些房間最多隻能容納一個人,比如廁所。裡面有人的時候,其他人就不能進去了。這代表一個執行緒使用某些共享記憶體時,其他執行緒必須等它結束,才能使用這一塊記憶體。 一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖開啟再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個執行緒同時讀寫某一塊記憶體區域。 還有些房間,可以同時容納n個人,比如廚房。也就是說,如果人數大於n,多出來的人只能在外面等著。這好比某些記憶體區域,只能供給固定數目的執行緒使用。 這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等著了。這種做法叫做"訊號量"(Semaphore),用來保證多個執行緒不會互相沖突。 作業系統的設計,因此可以歸結為三點: (1)以多程序形式,允許多個任務同時執行; (2)以多執行緒形式,允許單個任務分成不同的部分執行; (3)提供協調機制,一方面防止程序之間和執行緒之間產生衝突,另一方面允許程序之間和執行緒之間共享資源。 疑問: Q:thread.join()/thread.join(long millis),當前執行緒裡呼叫其它執行緒t的join方法,當前執行緒進入WAITING/TIMED_WAITING狀態,當前執行緒不會釋放已經持有的物件鎖。執行緒t執行完畢或者millis時間到,當前執行緒一般情況下進入RUNNABLE狀態,也有可能進入BLOCKED狀態(因為join是基於wait實現的)。 呼叫其他執行緒的thread.join()方法,當前執行緒不會釋放已經持有的物件鎖,那如果進入了BLOCKED狀態時會釋放物件鎖麼? 執行緒的基本操作 新建執行緒 start方法是啟動一個執行緒,run方法只會在當前執行緒中序列的執行run方法中的程式碼。 我們可以通過繼承Thread類,然後重寫run方法,來自定義一個執行緒。但考慮java是單繼承的,從擴充套件性上來說,我們實現一個介面來自定義一個執行緒更好一些,java中剛好提供了Runnable介面來自定義一個執行緒。實現Runnable介面是比較常見的做法,也是推薦的做法。 終止執行緒——stop()方法已廢棄 stop方法為何會被廢棄而不推薦使用?stop方法過於暴力,強制把正在執行的方法停止了。 大家是否遇到過這樣的場景:聽著歌寫著程式碼突然斷電了。執行緒正在執行過程中,被強制結束了,可能會導致一些意想不到的後果。可以給大家傳送一個通知,告訴大家儲存一下手頭的工作,將電腦關閉。 執行緒中斷——interrupt()正確的中斷執行緒方法 執行緒中斷並不會使執行緒立即退出,而是給執行緒傳送一個通知,告知目標執行緒,有人希望你退出了!至於目標執行緒接收到通知之後如何處理,則完全由目標執行緒自己決定,這點很重要,如果中斷後,執行緒立即無條件退出,我們又會到stop方法的老問題。 Thread提供了3個與執行緒中斷有關的方法,這3個方法容易混淆,大家注意下: public void interrupt() //中斷執行緒 public boolean isInterrupted() //判斷執行緒是否被中斷 public static boolean interrupted() //判斷執行緒是否被中斷,並清除當前中斷狀態 interrupt()方法是一個例項方法,它通知目標執行緒中斷,也就是設定中斷標誌位為true,中斷標誌位表示當前執行緒已經被中斷了。 isInterrupted()方法也是一個例項方法,它判斷當前執行緒是否被中斷(通過檢查中斷標誌位)。 interrupted()是一個靜態方法,返回boolean型別,也是用來判斷當前執行緒是否被中斷,但是同時會清除當前執行緒的中斷標誌位的狀態。 Q:通過變數控制和執行緒自帶的interrupt方法來中斷執行緒有什麼區別呢? A:如果一個執行緒呼叫了sleep方法,一直處於休眠狀態,通過變數控制,是不能中斷執行緒麼,因為此時執行緒處於睡眠狀態無法執行變數控制語句,此時只能使用執行緒提供的interrupt方法來中斷執行緒了。 例項: public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { //sleep方法由於中斷而丟擲異常之後,執行緒的中斷標誌會被清除(置為false),所以在異常中需要執行this.interrupt()方法,將中斷標誌位置為true this.interrupt(); System.out.println("exception:"+ e.getMessage()); } System.out.println(Thread.currentThread().getName() + " in the end"); break; } } }; t1.setName("interrupt thread"); t1.start(); TimeUnit.SECONDS.sleep(1); //呼叫interrupt()方法之後,執行緒的sleep方法將會丟擲 InterruptedException: sleep interrupted異常。 t1.interrupt(); } 錯誤寫法: 注意:sleep方法由於中斷而丟擲異常之後,執行緒的中斷標誌會被清除(置為false),所以在異常中需要執行this.interrupt()方法,將中斷標誌位置為true 等待(wait)和通知(notify) 為了支援多執行緒之間的協作,JDK提供了兩個非常重要的方法:等待wait()方法和通知notify()方法。這2個方法並不是在Thread類中的,而是在Object類中定義的。這意味著所有的物件都可以呼叫者兩個方法。 (即只有這個物件是被當成鎖來作為多執行緒之間的協作物件,那麼在同步程式碼塊中,執行緒之間就是通過等待wait()方法和通知notify()方法協作。) public final void wait() throws InterruptedException; public final native void notify(); 如果一個執行緒呼叫了object.wait()方法,那麼它就會進出object物件的等待佇列。這個佇列中,可能會有多個執行緒,因為系統可能執行多個執行緒同時等待某一個物件。當object.notify()方法被呼叫時,它就會從這個佇列中隨機選擇一個執行緒,並將其喚醒。這裡希望大家注意一下,這個選擇是不公平的,並不是先等待執行緒就會優先被選擇,這個選擇完全是隨機的。 nofiyAll()方法,它和notify()方法的功能類似,不同的是,它會喚醒在這個等待佇列中所有等待的執行緒,而不是隨機選擇一個。 這裡強調一點,Object.wait()方法並不能隨便呼叫。它必須包含在對應的synchronize語句塊中,無論是wait()方法或者notify()方法都需要首先獲取目標獨享的一個監視器。 等待wait()方法和通知notify()方法工作過程: wait()方法和nofiy()方法的工作流程細節: 圖中其中T1和T2表示兩個執行緒。T1在正確執行wait()方法前,必須獲得object物件的監視器。而wait()方法在執行後,會釋放這個監視器。這樣做的目的是使其他等待在object物件上的執行緒不至於因為T1的休眠而全部無法正常執行。 執行緒T2在notify()方法呼叫前,也必須獲得object物件的監視器。所幸,此時T1已經釋放了這個監視器,因此,T2可以順利獲得object物件的監視器。接著,T2執行了notify()方法嘗試喚醒一個等待執行緒,這裡假設喚醒了T1。T1在被喚醒後,要做的第一件事並不是執行後續程式碼,而是要嘗試重新獲得object物件的監視器,而這個監視器也正是T1在wait()方法執行前所持有的那個。如果暫時無法獲得,則T1還必須等待這個監視器。當監視器順利獲得後,T1才可以在真正意義上繼續執行。 注意:Object.wait()方法和Thread.sleeep()方法都可以讓現場等待若干時間。除wait()方法可以被喚醒外,另外一個主要的區別就是wait()方法會釋放目標物件的鎖,而Thread.sleep()方法不會釋放鎖。 示例: public class WaitNotifyTest { public static Object lock = new Object(); public static void main(String[] args) { new T1("Thread-1").start(); new T2("Thread-2").start(); } static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { synchronized (lock) { System.out.println(this.getName() + " start"); try { System.out.println(this.getName() + " wait"); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + " end"); } } } static class T2 extends Thread { public T2(String name) { super(name); } @Override public void run() { synchronized (lock) { System.out.println(this.getName() + " start"); System.out.println(this.getName() + " notify"); lock.notify(); System.out.println(this.getName() + " end"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + " end,2 second later"); } } } } 輸出: Thread-1 start Thread-1 wait Thread-2 start Thread-2 notify Thread-2 end Thread-2 end,2 second later Thread-1 end 注意下列印結果,T2呼叫notify方法之後,T1並不能立即繼續執行,而是要等待T2釋放objec投遞鎖之後,T1重新成功獲取鎖後,才能繼續執行。因此最後2行日誌相差了2秒(因為T2呼叫notify方法後休眠了2秒)。 可以這麼理解,obj物件上有2個佇列,q1:等待佇列,q2:準備獲取鎖的佇列; 掛起(suspend)和繼續執行(resume)執行緒——方法已廢棄 Thread類中還有2個方法,即執行緒掛起(suspend)和繼續執行(resume),這2個操作是一對相反的操作,被掛起的執行緒,必須要等到resume()方法操作後,才能繼續執行。系統中已經標註著2個方法過時了,不推薦使用。 系統不推薦使用suspend()方法去掛起執行緒是因為suspend()方法導致執行緒暫停的同時,並不會釋放任何鎖資源。此時,其他任何執行緒想要訪問被它佔用的鎖時,都會被牽連,導致無法正常執行(如圖2.7所示)。直到在對應的執行緒上進行了resume()方法操作,被掛起的執行緒才能繼續,從而其他所有阻塞在相關鎖上的執行緒也可以繼續執行。但是,如果resume()方法操作意外地在suspend()方法前就被執行了,那麼被掛起的執行緒可能很難有機會被繼續執行了。並且,更嚴重的是:它所佔用的鎖不會被釋放,因此可能會導致整個系統工作不正常。而且,對於被掛起的執行緒,從它執行緒的狀態上看,居然還是Runnable狀態,這也會影響我們對系統當前狀態的判斷。 等待執行緒結束(join)和謙讓(yeild) 很多時候,一個執行緒的輸入可能非常依賴於另外一個或者多個執行緒的輸出,此時,這個執行緒就需要等待依賴的執行緒執行完畢,才能繼續執行。jdk提供了join()操作來實現等待執行緒結束。 //表示無限等待,當前執行緒會一直等待,直到目標執行緒執行完畢 public final void join() throws InterruptedException; //millis引數用於指定等待時間,如果超過了給定的時間目標執行緒還在執行,當前執行緒也會停止等待,而繼續往下執行。 public final synchronized void join(long millis) throws InterruptedException; 例子:執行緒T1需要等待T2、T3完成之後才能繼續執行,那麼在T1執行緒中需要分別呼叫T2和T3的join()方法。 示例: public class JoinTest { private static int num = 0; public static void main(String[] args) throws InterruptedException { T1 t1 = new T1("T1"); t1.start(); long start = System.currentTimeMillis(); t1.join(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " end .user time ="+(end-start)+" ,get num=" + num); } static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println(this.getName() + " start"); for (int i = 0; i < 9; i++) { num++; } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + " end"); } } } 輸出: T1 start T1 end main end .user time =3003 ,get num=9 Thread.yield()方法。是屈服,放棄,謙讓的意思。 這是一個靜態方法,一旦執行,它會讓當前執行緒讓出CPU,但需要注意的是,讓出CPU並不是說不讓當前執行緒執行了,當前執行緒在出讓CPU後,還會進行CPU資源的爭奪,但是能否再搶到CPU的執行權就不一定了。因此,對Thread.yield()方法的呼叫好像就是在說:我已經完成了一些主要的工作,我可以休息一下了,可以讓CPU給其他執行緒一些工作機會了。 如果覺得一個執行緒不太重要,或者優先順序比較低,而又擔心此執行緒會過多的佔用CPU資源,那麼可以在適當的時候呼叫一下Thread.yield()方法,給與其他執行緒更多的機會。 public static native void yield(); 總結 1. 建立執行緒的4種方式:繼承Thread類;實現Runnable介面;實現Callable介面;使用執行緒池建立。 2. 啟動執行緒:呼叫執行緒的start()方法 3. 終止執行緒:呼叫執行緒的stop()方法,方法已過時,建議不要使用 4. 執行緒中斷相關的方法:呼叫執行緒例項interrupt()方法將中斷標誌置為true;使用執行緒例項方法isInterrupted()獲取中斷標誌;呼叫Thread的靜態方法interrupted()獲取執行緒是否被中斷,此方法呼叫之後會清除中斷標誌(將中斷標誌置為false了) 5. wait、notify、notifyAll方法 6. 執行緒掛起使用執行緒例項方法suspend(),恢復執行緒使用執行緒例項方法resume(),這2個方法都過時了,不建議使用 7. 等待執行緒結束:呼叫執行緒例項方法join() 8. 讓出cpu資源:呼叫執行緒靜態方法yeild() 疑問: Q:方法interrupted()是一個靜態方法,返回boolean型別,也是用來判斷當前執行緒是否被中斷,但是同時會清除當前執行緒的中斷標誌位的狀態。 清除當前執行緒的中斷標誌位的狀態是表示該執行緒可以不中斷了麼?清除當前執行緒的中斷標誌位的狀態是什麼意思,有什麼作用?怎麼使用? Q:三個執行緒交替列印ABC 10次使用wait(),notifyAll()如何實現? volatile與Java記憶體模型 volatile解決了共享變數在多執行緒中可見性的問題,可見性是指一個執行緒對共享變數的修改,對於另一個執行緒來說是否是可以看到的。 使用volatile保證記憶體可見性示例: public class VolatileTest { //public static boolean flag = true; public static volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { T1 t1 = new T1("T1"); t1.start(); //TimeUnit.SECONDS.sleep(3); Thread.sleep(2000); //將flag置為false flag = false; } public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println(this.getName() + " start"); while (VolatileTest.flag) { //奇怪現象,為什麼執行輸出語句在執行一會兒後會讓flag=false讀取到,而 ; 空迴圈卻會導致程式無法終止呢? //個人覺得應該是虛擬機器從解釋執行轉換為編譯執行,這時候會重新讀到flag。 //System.out.println(this.getName() +"endless loop"); ; } System.out.println(this.getName() + " end"); } } } 不加volatile執行上面程式碼,會發現程式無法終止。 Q:t1執行緒中為什麼看不到被主執行緒修改之後的flag? 要解釋這個,我們需要先了解一下java記憶體模型(JMM),Java執行緒之間的通訊由Java記憶體模型(本文簡稱為JMM)控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。 Java記憶體模型的抽象示意圖: 執行緒A需要和執行緒B通訊,必須要經歷下面2個步驟: 1. 首先,執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。 2. 然後,執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。 執行緒t1中為何看不到被主執行緒修改為false的flag的值的原因,有兩種可能: 1. 主執行緒修改了flag之後,未將其重新整理到主記憶體,所以t1看不到 2. 主執行緒將flag重新整理到了主記憶體,但是t1一直讀取的是自己工作記憶體中flag的值,沒有去主記憶體中獲取flag最新的值 使用volatile修飾共享變數,就可以達到上面的效果,被volatile修改的變數有以下特點: 1. 執行緒中讀取的時候,每次讀取都會去主記憶體中讀取共享變數最新的值,然後將其複製到工作記憶體 2. 執行緒中修改了工作記憶體中變數的副本,修改之後會立即重新整理到主記憶體 執行緒組 我們可以把執行緒歸屬到某個執行緒組中,執行緒組可以包含多個執行緒以及執行緒組,執行緒和執行緒組組成了父子關係,是個樹形結構。使用執行緒組可以方便管理執行緒 。(執行緒池是不是更實在一點?) 建立執行緒關聯執行緒組 建立執行緒的時候,可以給執行緒指定一個執行緒組。 建立執行緒組的時候,可以給其指定一個父執行緒組,也可以不指定,如果不指定父執行緒組,則父執行緒組為當前執行緒的執行緒組,系統自動獲取當前執行緒的執行緒組作為預設父執行緒組。java api有2個常用的構造方法用來建立執行緒組: public ThreadGroup(String name) public ThreadGroup(ThreadGroup parent, String name) 第一個構造方法未指定父執行緒組,看一下內部的實現: public ThreadGroup(String name) { this(Thread.currentThread().getThreadGroup(), name); } 批量停止執行緒 呼叫執行緒組interrupt(),會將執行緒組樹下的所有子孫執行緒中斷標誌置為true,可以用來批量中斷執行緒。 建議建立執行緒或者執行緒組的時候,給他們取一個有意義的名字,在系統出問題的時候方面查詢定位。 示例: public class ThreadGroupTest { public static class R1 implements Runnable { @Override public void run() { System.out.println("threadName:" + Thread.currentThread().getName()); while (!Thread.currentThread().isInterrupted()){ ; } System.out.println(Thread.currentThread().getName()+"執行緒停止了"); } } public static void main(String[] args) throws InterruptedException { //threadGroup1未指定父執行緒組,系統獲取了主執行緒的執行緒組作為threadGroup1的父執行緒組,輸出結果中是:main ThreadGroup threadGroup = new ThreadGroup("thread-group-1"); Thread t1 = new Thread(threadGroup, new R1(), "t1"); Thread t2 = new Thread(threadGroup, new R1(), "t2"); t1.start(); t2.start(); TimeUnit.SECONDS.sleep(1); System.out.println("活動執行緒數:" + threadGroup.activeCount()); System.out.println("活動執行緒組:" + threadGroup.activeGroupCount()); System.out.println("執行緒組名稱:" + threadGroup.getName()); ThreadGroup threadGroup2 = new ThreadGroup(threadGroup, "thread-group-2"); Thread t3 = new Thread(threadGroup2, new R1(), "t3"); Thread t4 = new Thread(threadGroup2, new R1(), "t4"); t3.start(); t4.start(); threadGroup.list(); //java.lang.ThreadGroup[name=main,maxpri=10] 主執行緒的執行緒組為main System.out.println(Thread.currentThread().getThreadGroup()); //java.lang.ThreadGroup[name=system,maxpri=10] 根執行緒組為system System.out.println(Thread.currentThread().getThreadGroup().getParent()); //null System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent()); threadGroup.interrupt(); TimeUnit.SECONDS.sleep(2); threadGroup.list(); } } 輸出: threadName:t1 threadName:t2 活動執行緒數:2 活動執行緒組:0 執行緒組名稱:thread-group-1 java.lang.ThreadGroup[name=thread-group-1,maxpri=10] Thread[t1,5,thread-group-1] Thread[t2,5,thread-group-1] java.lang.ThreadGroup[name=thread-group-2,maxpri=10] Thread[t3,5,thread-group-2] Thread[t4,5,thread-group-2] java.lang.ThreadGroup[name=main,maxpri=10] java.lang.ThreadGroup[name=system,maxpri=10] null t2執行緒停止了 t1執行緒停止了 threadName:t4 threadName:t3 t4執行緒停止了 t3執行緒停止了 java.lang.ThreadGroup[name=thread-group-1,maxpri=10] java.lang.ThreadGroup[name=thread-group-2,maxpri=10] 使用者執行緒和守護執行緒 守護執行緒是一種特殊的執行緒,在後臺默默地完成一些系統性的服務,比如垃圾回收執行緒、JIT執行緒都是守護執行緒。與之對應的是使用者執行緒,使用者執行緒可以理解為是系統的工作執行緒,它會完成這個程式需要完成的業務操作。如果使用者執行緒全部結束了,意味著程式需要完成的業務操作已經結束了,系統可以退出了。所以當系統只剩下守護程序的時候,java虛擬機器會自動退出。 java執行緒分為使用者執行緒和守護執行緒,執行緒的daemon屬性為true表示是守護執行緒,false表示是使用者執行緒。 執行緒daemon的預設值 我們看一下建立執行緒原始碼,位於Thread類的init()方法中: Thread parent = currentThread(); this.daemon = parent.isDaemon(); dameon的預設值為為父執行緒的daemon,也就是說,父執行緒如果為使用者執行緒,子執行緒預設也是使用者現場,父執行緒如果是守護執行緒,子執行緒預設也是守護執行緒。 總結 1. java中的執行緒分為使用者執行緒和守護執行緒 2. 程式中的所有的使用者執行緒結束之後,不管守護執行緒處於什麼狀態,java虛擬機器都會自動退出 3. 呼叫執行緒的例項方法setDaemon()來設定執行緒是否是守護執行緒 4. setDaemon()方法必須線上程的start()方法之前呼叫,在後面呼叫會報異常,並且不起效 5. 執行緒的daemon預設值和其父執行緒一樣 示例: public class DaemonThreadTest { public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println(this.getName() + " start ,isDaemon= "+isDaemon()); while (true) { ; } } } public static void main(String[] args) throws InterruptedException { T1 t1 = new T1("T1"); // 設定守護執行緒,需要在start()方法之前進行 // t1.start()必須在setDaemon(true)之後,否則執行會報異常:Exception in thread "main" java.lang.IllegalThreadStateException //t1.start(); //將t1執行緒設定為守護執行緒 t1.setDaemon(true); t1.start(); //當程式中所有的使用者執行緒執行完畢之後,不管守護執行緒是否結束,系統都會自動退出。 TimeUnit.SECONDS.sleep(1); } } 疑問: Q:JIT執行緒? A: JIT一般指準時制。準時制生產方式(Just In Time簡稱JIT ).JIT執行緒在Java中表示即時編譯執行緒,解釋執行。 在Java程式語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的位元組碼(包括需要被解釋的指令的程式)轉換成可以直接傳送給處理器的指令的程式。 執行緒安全和synchronized關鍵字 什麼是執行緒安全? 當多個執行緒去訪問同一個類(物件或方法)的時候,該類都能表現出一致的行為,沒有意想不到的不同結果,那我們就可以所這個類是執行緒安全的。 造成執行緒安全問題的主要誘因有兩點: 1. 一是存在共享資料(也稱臨界資源) 2. 二是存在多條執行緒共同操作共享資料 為了解決這個問題,當存在多個執行緒操作共享資料時,需要保證同一時刻有且只有一個執行緒在操作共享資料,這種方式有個高尚的名稱叫互斥鎖,在 Java 中,關鍵字 synchronized可以保證在同一個時刻,只有一個執行緒可以執行某個方法或者某個程式碼塊(主要是對方法或者程式碼塊中存在共享資料的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代volatile功能) 鎖的互斥性表現線上程嘗試獲取的是否是同一個鎖,相同型別不同例項的物件鎖不互斥,而class類物件的鎖與例項鎖之間也不互斥。 synchronized主要有3種使用方式 1. 修飾例項方法,作用於當前例項,進入同步程式碼前需要先獲取例項的鎖 2. 修飾靜態方法,作用於類的Class物件,進入修飾的靜態方法前需要先獲取類的Class物件的鎖 3. 修飾程式碼塊,需要指定加鎖物件(記做lockobj),在進入同步程式碼塊前需要先獲取lockobj的鎖 synchronized作用於例項物件 synchronize作用於例項方法需要注意: 1. 例項方法上加synchronized,執行緒安全的前提是,多個執行緒操作的是同一個例項,如果多個執行緒作用於不同的例項,那麼執行緒安全是無法保證的 2. 同一個例項的多個例項方法上有synchronized,這些方法都是互斥的,同一時間只允許一個執行緒操作同一個例項的其中的一個synchronized方法 synchronized作用於靜態方法 當synchronized作用於靜態方法時,鎖的物件就是當前類的Class物件。 synchronized同步程式碼塊 方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的程式碼又只有一小部分時使用。加鎖時可以使用自定義的物件作為鎖,也可以使用this物件(代表當前例項)或者當前類的class物件作為鎖 。 疑問: Q:synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代volatile功能),synchronized是怎麼保證可見性的呢? Q:同一個例項的多個例項方法上有synchronized,這些方法都是互斥的,同一時間只允許一個執行緒操作同一個例項的其中的一個synchronized方法.驗證同一時間只允許一個執行緒操作同一個例項的其中的一個synchronized方法是對的。 A:示例有下: public class MethodObject { public synchronized void methodA() throws InterruptedException { System.out.println("methodA start"); TimeUnit.SECONDS.sleep(10); System.out.println("methodA finish"); } public synchronized void methodB() throws InterruptedException { System.out.println("methodB start"); TimeUnit.SECONDS.sleep(5); System.out.println("methodB finish"); } } public class SynchronousTest { public static void main(String[] args) throws InterruptedException { MethodObject mo = new MethodObject(); T1 t1 = new T1("T1", mo); T2 t2 = new T2("T2", mo); t1.start(); TimeUnit.MILLISECONDS.sleep(300); t2.start(); } public static class T1 extends Thread { private MethodObject mo; public T1(String name, MethodObject mo) { super(name); this.mo = mo; } @Override public void run() { try { mo.methodA(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static class T2 extends Thread { private MethodObject mo; public T2(String name, MethodObject mo) { super(name); this.mo = mo; } @Override public void run() { try { mo.methodB(); } catch (InterruptedException e) { e.printStackTrace(); } } } } synchronized實現原理 深入理解Java併發之synchronized實現原理 執行緒中斷的2種方式 1、通過一個volatile修飾的變數控制執行緒中斷 利用volatile控制的變數在多執行緒中的可見性,Java記憶體模型實現。 示例: public class VolatileTest { //public static boolean flag = true; public static volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { T1 t1 = new T1("T1"); t1.start(); //TimeUnit.SECONDS.sleep(3); Thread.sleep(3000); //將flag置為false flag = false; } public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println(this.getName() + " start"); while (VolatileTest.flag) { ; } System.out.println(this.getName() + " end"); } } } 2、通過執行緒自帶的中斷標誌interrupt() 控制 當呼叫執行緒的interrupt()例項方法之後,執行緒的中斷標誌會被置為true,可以通過執行緒的例項方法isInterrupted()獲取執行緒的中斷標誌。 當執行的執行緒處於阻塞狀態時: 1. 呼叫執行緒的interrupt()例項方法,執行緒的中斷標誌會被置為true 2. 當執行緒處於阻塞狀態時,呼叫執行緒的interrupt()例項方法,執行緒內部會觸發InterruptedException異常,並且會清除執行緒內部的中斷標誌(即將中斷標誌置為false) 阻塞狀態處理方法:這時候應該在catch中再呼叫this.interrupt();一次,將中斷標誌置為true。然後在run()方法中通過this.isInterrupted()來獲取執行緒的中斷標誌,退出迴圈break。 總結 1. 當一個執行緒處於被阻塞狀態或者試圖執行一個阻塞操作時,可以使用 Thread.interrupt()方式中斷該執行緒,注意此時將會丟擲一個InterruptedException的異常,同時中斷狀態將會被複位(由中斷狀態改為非中斷狀態)。阻塞狀態執行緒要通過執行緒自帶的中斷標誌interrupt() 控制中斷。 2. 內部有迴圈體,可以通過一個變數來作為一個訊號控制執行緒是否中斷,注意變數需要volatile修飾。 3. 文中的2種方式可以結合起來靈活使用控制執行緒的中斷. 示例: public class InterruptTest1 { public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println(this.getName() + " start"); while (true) { try { //下面模擬阻塞程式碼 TimeUnit.SECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); this.interrupt(); } if (this.isInterrupted()) { break; } } System.out.println(this.getName() + " end"); } } public static void main(String[] args) throws InterruptedException { T1 t1 = new T1("thread1"); t1.start(); TimeUnit.SECONDS.sleep(2); t1.interrupt(); } } 輸出: thread1 start thread1 end java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at com.self.current.InterruptTest1$T1.run(InterruptTest1.java:27) ReentrantLock重入鎖 synchronized的侷限性 synchronized是java內建的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,使用者不需要顯示的釋放鎖,非常方便,然而synchronized也有一定的侷限性,例如: 1. 當執行緒嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞,這個阻塞的過程,使用者無法控制。(synchronized不能響應中斷?) 2. 如果獲取鎖的執行緒進入休眠或者阻塞,除非當前執行緒異常,否則其他執行緒嘗試獲取鎖必須一直等待。(synchronized不能響應中斷?) ReentrantLock ReentrantLock是Lock的預設實現,在聊ReentranLock之前,我們需要先弄清楚一些概念: 1. 可重入鎖:可重入鎖是指同一個執行緒可以多次獲得同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖 2. 可中斷鎖:可中斷鎖是指執行緒在獲取鎖的過程中,是否可以響應執行緒中斷操作。synchronized是不可中斷的,ReentrantLock是可中斷的 3. 公平鎖和非公平鎖:公平鎖是指多個執行緒嘗試獲取同一把鎖的時候,獲取鎖的順序按照執行緒到達的先後順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種都可以實現,不過預設是非公平鎖。 ReentrantLock基本使用 ReentrantLock的使用過程: 1. 建立鎖:ReentrantLock lock = new ReentrantLock(); 2. 獲取鎖:lock.lock() 3. 釋放鎖:lock.unlock(); 對比上面的程式碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操作過程,開發人員必須手動的指定何時加鎖,何時釋放鎖,正是因為這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面程式碼需要注意lock.unlock()一定要放在finally中,否則若程式出現了異常,鎖沒有釋放,那麼其他執行緒就再也沒有機會獲取這個鎖了。 ReentrantLock是可重入鎖 假如ReentrantLock是不可重入的鎖,那麼同一個執行緒第2次獲取鎖的時候由於前面的鎖還未釋放而導致死鎖,程式是無法正常結束的。 1. lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則後面的執行緒無法獲取鎖了;可以將add中的unlock刪除一個事實,上面程式碼執行將無法結束 2. unlock()方法放在finally中執行,保證不管程式是否有異常,鎖必定會釋放 示例: public class ReentrantLockTest { private static int num = 0; private static Lock lock = new ReentrantLock(); public static void add() { lock.lock(); lock.lock(); try { num++; } finally { //lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則後面的執行緒無法獲取鎖 lock.unlock(); lock.unlock(); } } public static class T extends Thread { public T(String name) { super(name); } @Override public void run() { for (int i = 0; i < 1000; i++) { ReentrantLockTest.add(); } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1"); T t2 = new T("t2"); T t3 = new T("t3"); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println("get num =" + num); } } //輸出: get num =3000 ReentrantLock實現公平鎖 在大多數情況下,鎖的申請都是非公平的。這就好比買票不排隊,上廁所不排隊。最終導致的結果是,有些人可能一直買不到票。而公平鎖,它會按照到達的先後順序獲得資源。公平鎖的一大特點是不會產生飢餓現象,只要你排隊,最終還是可以等到資源的;synchronized關鍵字預設是有jvm內部實現控制的,是非公平鎖。而ReentrantLock執行開發者自己設定鎖的公平性,可以實現公平和非公平鎖。 看一下jdk中ReentrantLock的原始碼,2個構造方法: public ReentrantLock() { sync = new NonfairSync();} public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 預設構造方法建立的是非公平鎖。 第2個構造方法,有個fair引數,當fair為true的時候建立的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部肯定需要維護一個有序佇列,因此公平鎖的實現成本比較高,效能相對於非公平鎖來說相對低一些。因此,在預設情況下,鎖是非公平的,如果沒有特別要求,則不建議使用公平鎖。 示例: public class ReentrantLockFairTest { private static int num = 0; //private static Lock lock = new ReentrantLock(false); private static Lock lock = new ReentrantLock(true); public static class T extends Thread { public T(String name) { super(name); } @Override public void run() { for (int i = 0; i < 3; i++) { lock.lock(); try { System.out.println(Thread.currentThread().getName()+" got lock"); } finally { lock.unlock(); } } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1"); T t2 = new T("t2"); T t3 = new T("t3"); t1.start(); t2.start(); t3.start(); } } 輸出: 公平鎖: t1 got lock t1 got lock t2 got lock t2 got lock t3 got lock t3 got lock 非公平鎖: t1 got lock t3 got lock t3 got lock t2 got lock t2 got lock t1 got lock ReentrantLock獲取鎖的過程是可中斷的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有參方法時。 對於synchronized關鍵字,如果一個執行緒在等待獲取鎖,最終只有2種結果: 1. 要麼獲取到鎖然後繼續後面的操作 2. 要麼一直等待,直到其他執行緒釋放鎖為止 而ReentrantLock提供了另外一種可能,就是在等的獲取鎖的過程中(發起獲取鎖請求到還未獲取到鎖這段時間內)是可以被中斷的,也就是說在等待鎖的過程中,程式可以根據需要取消獲取鎖的請求。拿李雲龍平安縣圍點打援來說,當平安縣城被拿下後,鬼子救援的部隊再嘗試救援已經沒有意義了,這時候要請求中斷操作。 關於獲取鎖的過程中被中斷,注意幾點: 1. ReentrankLock中必須使用例項方法 lockInterruptibly()獲取鎖時,線上程呼叫interrupt()方法之後,才會引發 InterruptedException異常 2. 執行緒呼叫interrupt()之後,執行緒的中斷標誌會被置為true 3. 觸發InterruptedException異常之後,執行緒的中斷標誌有會被清空,即置為false 4. 所以當執行緒呼叫interrupt()引發InterruptedException異常,中斷標誌的變化是:false->true->false 例項: public class InterruptTest2 { private static ReentrantLock lock1 = new ReentrantLock(); private static ReentrantLock lock2 = new ReentrantLock(); public static class T1 extends Thread { int lock; public T1(String name, Integer lock) { super(name); this.lock = lock; } @Override public void run() { try { if (lock == 1) { lock1.lockInterruptibly(); TimeUnit.SECONDS.sleep(1); lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); TimeUnit.SECONDS.sleep(1); lock1.lockInterruptibly(); } } catch (InterruptedException e) { //執行緒傳送中斷訊號觸發InterruptedException異常之後,中斷標誌將被清空。 System.out.println(this.getName() + "中斷標誌:" + this.isInterrupted()); e.printStackTrace(); } finally { //R