Java——執行緒回顧彙總:同步/生產者消費者模式/定時排程
一個程序可以產生多個執行緒。同多個程序可以共享作業系統的某些資源一樣,同一程序的多個執行緒也可以共享此程序的某些資源(比如:程式碼、資料),所以執行緒又被稱為輕量級程序(lightweight process)。
1. 一個程序內部的一個執行單元,它是程式中的一個單一的順序控制流程。
2. 一個程序可擁有多個並行的(concurrent)執行緒。
3. 一個程序中的多個執行緒共享相同的記憶體單元/記憶體地址空間,可以訪問相同的變數和物件,而且它們從同一堆中分配物件並進行通訊、資料交換和同步操作。
4. 由於執行緒間的通訊是在同一地址空間上進行的,所以不需要額外的通訊機制,這就使得通訊更簡便而且資訊傳遞的速度也更快。
5. 執行緒的啟動、中斷、消亡,消耗的資源非常少。
執行緒和程序的區別
1. 每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換會有較大的開銷。
2. 執行緒可以看成是輕量級的程序,屬於同一程序的執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換的開銷小。
3. 執行緒和程序最根本的區別在於:程序是資源分配的單位,執行緒是排程和執行的單位。
4. 多程序: 在作業系統中能同時執行多個任務(程式)。
5. 多執行緒: 在同一應用程式中有多個順序流同時執行。
6. 執行緒是程序的一部分,所以執行緒有的時候被稱為輕量級程序。
7. 一個沒有執行緒的程序是可以被看作單執行緒的,如果一個程序內擁有多個執行緒,程序的執行過程不是一條線(執行緒)的,而是多條線(執行緒)共同完成的。
8. 系統在執行的時候會為每個程序分配不同的記憶體區域,但是不會為執行緒分配記憶體(執行緒所使用的資源是它所屬的程序的資源),執行緒組只能共享資源。那就是說,除了CPU之外(執行緒在執行的時候要佔用CPU資源),計算機內部的軟硬體資源的分配與執行緒無關,執行緒只能共享它所屬程序的資源。
建立執行緒的方式
1、 繼承Thread類方式的多執行緒
• 優勢:可以繼承其它類,多執行緒可共享同一個Runnable物件
• 劣勢:程式設計方式稍微複雜,如果需要訪問當前執行緒,需要呼叫Thread.currentThread()方
法
2、實現Runnable介面方式的多執行緒
• 優勢:可以繼承其它類,多執行緒可共享同一個Runnable物件
• 劣勢:程式設計方式稍微複雜,如果需要訪問當前執行緒,需要呼叫Thread.currentThread()方
法
3、實現Callable介面
與實行Runnable相比, Callable功能更強大些
• 方法不同,可以有返回值,支援泛型的返回值
• 可以丟擲異常
• 需要藉助FutureTask,比如獲取返回結果
Future介面
可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
FutrueTask是Futrue介面的唯一的實現類
FutureTask 同時實現了Runnable, Future介面。它既可以作為Runnable被執行緒執行,又可以作為
Future得到Callable的返回值
執行緒控制方法
• join ()
阻塞指定執行緒等到另一個執行緒完成以後再繼續執行
• sleep ()
使執行緒停止執行一段時間,將處於阻塞狀態
如果呼叫了sleep方法之後,沒有其他等待執行的執行緒,這個時候當前執行緒不會馬上恢復執行!
• yield ()
讓當前正在執行執行緒暫停,不是阻塞執行緒,而是將執行緒轉入就緒狀態
如果呼叫了yield方法之後,沒有其他等待執行的執行緒,這個時候當前執行緒就會馬上恢復執行!
• setDaemon ()
可以將指定的執行緒設定成後臺執行緒
建立後臺執行緒的執行緒結束時,後臺執行緒也隨之消亡
只能在執行緒啟動之前把它設為後臺執行緒
• interrupt()
並沒有直接中斷執行緒,而是需要被中斷執行緒自己處理
• stop()
結束執行緒,不推薦使用
執行緒狀態
一個執行緒物件在它的生命週期內,需要經歷5個狀態。
▪ 新生狀態(New)
用new關鍵字建立一個執行緒物件後,該執行緒物件就處於新生狀態。處於新生狀態的執行緒有自己的記憶體空間,通過呼叫start方法進入就緒狀態。
▪ 就緒狀態(Runnable)
處於就緒狀態的執行緒已經具備了執行條件,但是還沒有被分配到CPU,處於“執行緒就緒佇列”,等待系統為其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread物件後,它就會進入執行狀態。一旦獲得CPU,執行緒就進入執行狀態並自動呼叫自己的run方法。有4中原因會導致執行緒進入就緒狀態:
1. 新建執行緒:呼叫start()方法,進入就緒狀態;
2. 阻塞執行緒:阻塞解除,進入就緒狀態;
3. 執行執行緒:呼叫yield()方法,直接進入就緒狀態;
4. 執行執行緒:JVM將CPU資源從本執行緒切換到其他執行緒。
▪ 執行狀態(Running)
在執行狀態的執行緒執行自己run方法中的程式碼,直到呼叫其他方法而終止或等待某資源而阻塞或完成任務而死亡。如果在給定的時間片內沒有執行結束,就會被系統給換下來回到就緒狀態。也可能由於某些“導致阻塞的事件”而進入阻塞狀態。
▪ 阻塞狀態(Blocked)
阻塞指的是暫停一個執行緒的執行以等待某個條件發生(如某資源就緒)。有4種原因會導致阻塞:
1. 執行sleep(int millsecond)方法,使當前執行緒休眠,進入阻塞狀態。當指定的時間到了後,執行緒進入就緒狀態。
2. 執行wait()方法,使當前執行緒進入阻塞狀態。當使用nofity()方法喚醒這個執行緒後,它進入就緒狀態。
3. 執行緒執行時,某個操作進入阻塞狀態,比如執行IO流操作(read()/write()方法本身就是阻塞的方法)。只有當引起該操作阻塞的原因消失後,執行緒進入就緒狀態。
4. join()執行緒聯合: 當某個執行緒等待另一個執行緒執行結束後,才能繼續執行時,使用join()方法。
阻塞分為三種:
BLOCKED
被阻塞等待監視器鎖定的執行緒處於此狀態。 處於阻塞狀態的執行緒正在等待監視器鎖定進入同步塊/方法,或者在呼叫Object.wait後重新輸入同步的塊/方法。
WAITING
正在等待另一個執行緒執行特定動作的執行緒處於此狀態。 等待執行緒的執行緒狀態 由於呼叫以下方法之一,執行緒處於等待狀態:
Object.wait沒有超時
Thread.join沒有超時
LockSupport.park
TIMED_WAITING
正在等待另一個執行緒執行動作達到指定等待時間的執行緒處於此狀態。
具有指定等待時間的等待執行緒的執行緒狀態。 執行緒處於定時等待狀態,因為在指定的正等待時間內呼叫以下方法之一:
Thread.sleep
Object.wait與超時
Thread.join與超時
LockSupport.parkNanos
LockSupport.parkUntil
▪ 死亡狀態(Terminated)
死亡狀態是執行緒生命週期中的最後一個階段。執行緒死亡的原因有兩個。一個是正常執行的執行緒完成了它run()方法內的全部工作; 另一個是執行緒被強制終止,如通過執行stop()或destroy()方法來終止一個執行緒(注:stop()/destroy()方法已經被JDK廢棄,不推薦使用)。
當一個執行緒進入死亡狀態以後,就不能再回到其它狀態了。
終止執行緒的典型方式
終止執行緒我們一般不使用JDK提供的stop()/destroy()方法(它們本身也被JDK廢棄了)。通常的做法是提供一個boolean型的終止變數,當這個變數置為false,則終止執行緒的執行。
public class TestThreadCiycle implements Runnable { String name; boolean live = true;// 標記變數,表示執行緒是否可中止; public TestThreadCiycle(String name) { super(); this.name = name; } public void run() { int i = 0; //當live的值是true時,繼續執行緒體;false則結束迴圈,繼而終止執行緒體; while (live) { System.out.println(name + (i++)); } } public void terminate() { live = false; } public static void main(String[] args) { TestThreadCiycle ttc = new TestThreadCiycle("執行緒A:"); Thread t1 = new Thread(ttc);// 新生狀態 t1.start();// 就緒狀態 for (int i = 0; i < 100; i++) { System.out.println("主執行緒" + i); } ttc.terminate(); System.out.println("ttc stop!"); } }
暫停執行緒執行sleep/yield
暫停執行緒執行常用的方法有sleep()和yield()方法,這兩個方法的區別是:
1. sleep()方法:可以讓正在執行的執行緒進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。
2. yield()方法:可以讓正在執行的執行緒直接進入就緒狀態,讓出CPU的使用權。
執行緒的聯合join()
理解為插隊
執行緒A在執行期間,可以呼叫執行緒B的join()方法,讓執行緒B和執行緒A聯合。這樣,執行緒A就必須等待執行緒B執行完畢後,才能繼續執行。
獲取執行緒基本資訊的方法
執行緒的優先順序
1. 處於就緒狀態的執行緒,會進入“就緒佇列”等待JVM來挑選。
2. 執行緒的優先順序用數字表示,範圍從1到10,一個執行緒的預設優先順序是5。
Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5
3. 使用下列方法獲得或設定執行緒物件的優先順序。
int getPriority();
void setPriority(int newPriority);
注意:優先順序低只是意味著獲得排程的概率低。並不是絕對先呼叫優先順序高的執行緒後呼叫優先順序低的執行緒。
守護執行緒/後臺執行緒
setDaemon ()
• 可以將指定的執行緒設定成後臺執行緒
• 建立後臺執行緒的執行緒結束時,後臺執行緒也隨之消亡
• 只能在執行緒啟動之前把它設為後臺執行緒
執行緒同步
由於同一程序的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個資料物件被多個執行緒同時訪問造成的這種問題。
由於我們可以通過 private 關鍵字來保證資料物件只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。
▪ synchronized 方法
通過在方法宣告中加入 synchronized關鍵字來宣告,語法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制對“物件的類成員變數”的訪問:每個物件對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法的物件的鎖方能執行,否則所屬執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的執行緒方能獲得該鎖,重新進入可執行狀態。
▪ synchronized塊
synchronized 方法的缺陷:若將一個大的方法宣告為synchronized 將會大大影響效率。
Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。 塊可以讓我們精確地控制到具體的“成員變數”,縮小同步的範圍,提高效率。
synchronized 塊:通過 synchronized關鍵字來宣告synchronized 塊,語法如下:
synchronized(syncObject) {
//允許訪問控制的程式碼
}
Lock鎖
JDK1.5後新增功能,與採用synchronized相比,lock可提供多種鎖方案,更靈活
java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為 Lock 的多種實現留下了空間,各種實現可能有不同的排程演算法、效能特性或者鎖
定語義。
ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義, 但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。
注意:如果同步程式碼有異常,要將unlock()寫入finally語句塊
• Lock和synchronized的區別
1.Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖
2.Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
3.使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)
• 優先使用順序:
Lock----同步程式碼塊(已經進入了方法體,分配了相應資源)----同步方法(在方法體之外)
死鎖的概念
“死鎖”指的是:多個執行緒各自佔有一些共享資源,並且互相等待其他執行緒佔有的資源才能進行,而導致兩個或者多個執行緒都在等待對方釋放資源,都停止執行的情形。
死鎖的解決方法
死鎖是由於“同步塊需要同時持有多個物件鎖造成”的,要解決這個問題,思路很簡單,就是:同一個程式碼塊,不要同時持有兩個物件鎖。
執行緒併發協作(生產者/消費者模式)——管程法
多執行緒環境下,我們經常需要多個執行緒的併發和協作。這個時候,就需要了解一個重要的多執行緒併發協作模型“生產者/消費者模式”。
Ø 什麼是生產者?
生產者指的是負責生產資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
Ø 什麼是消費者?
消費者指的是負責處理資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
Ø 什麼是緩衝區?
消費者不能直接使用生產者的資料,它們之間有個“緩衝區”。生產者將生產好的資料放入“緩衝區”,消費者從“緩衝區”拿要處理的資料。
緩衝區是實現併發的核心,緩衝區的設定有3個好處:
Ø 實現執行緒的併發協作
有了緩衝區以後,生產者執行緒只需要往緩衝區裡面放置資料,而不需要管消費者消費的情況;同樣,消費者只需要從緩衝區拿資料處理即可,也不需要管生產者生產的情況。 這樣,就從邏輯上實現了“生產者執行緒”和“消費者執行緒”的分離。
Ø 解耦了生產者和消費者
生產者不需要和消費者直接打交道。
Ø 解決忙閒不均,提高效率
生產者生產資料慢時,緩衝區仍有資料,不影響消費者消費;消費者處理資料慢時,生產者仍然可以繼續往緩衝區裡面放置資料 。
執行緒併發協作總結:
執行緒併發協作(也叫執行緒通訊),通常用於生產者/消費者模式,情景如下:
1. 生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件。
2. 對於生產者,沒有生產產品之前,消費者要進入等待狀態。而生產了產品之後,又需要馬上通知消費者消費。
3. 對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新產品以供消費。
4. 在生產者消費者問題中,僅有synchronized是不夠的。
· synchronized可阻止併發更新同一個共享資源,實現了同步;
· synchronized不能用來實現不同執行緒之間的訊息傳遞(通訊)。
5. 那執行緒是通過哪些方法來進行訊息傳遞(通訊)的呢?
6. 以上方法均是java.lang.Object類的方法;
都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常。
執行緒併發協作(生產者/消費者模式)——訊號燈法
通過標誌位來標記。
任務定時排程
通過Timer和Timetask,我們可以實現定時啟動某個執行緒。
java.util.Timer
在這種實現方式中,Timer類作用是類似鬧鐘的功能,也就是定時或者每隔一定時間觸發一次執行緒。其實,Timer類本身實現的就是一個執行緒,只是這個執行緒是用來實現呼叫其它執行緒的。
java.util.TimerTask
TimerTask類是一個抽象類,該類實現了Runnable介面,所以該類具備多執行緒的能力。
在這種實現方式中,通過繼承TimerTask使該類獲得多執行緒的能力,將需要多執行緒執行的程式碼書寫在run方法內部,然後通過Timer類啟動執行緒的執行。
在實際使用時,一個Timer可以啟動任意多個TimerTask實現的執行緒,但是多個執行緒之間會存在阻塞。所以如果多個執行緒之間需要完全獨立的話,最好還是一個Timer啟動一個TimerTask實現。
實際開發中,我們可以使用開源框架quanz,更加方便的實現任務定時排程。實際上,quanz底層原理也就是這個。
JMM中的happens-before規則
當程式在執行過程中,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。
比如i++這句程式碼
當執行緒執行這個語句時,會先從主存當中讀取i的值,然後複製一份到快取記憶體當中,然後CPU執行指令對i進行加1操作,然後將資料寫入快取記憶體,最後將快取記憶體中i最新的值重新整理到主存當中。
這個程式碼在單執行緒中執行任何問題,但在多執行緒中執行就會有問題。在多核CPU中,每條執行緒可能運行於不同的CPU中,因此每個執行緒執行時有自己的快取記憶體(對單核CPU來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。
比如同時有2個執行緒執行這段程式碼,假如初始時i的值為0,那麼我們希望兩個執行緒執行完之後i的值變為2。但是事實會是這樣嗎?
可能存在下面一種情況:初始時,兩個執行緒分別讀取i的值存入各自所在的CPU的快取記憶體當中,然後執行緒1進行加1操作,然後把i的最新值1寫入到記憶體。此時執行緒2的快取記憶體當中i的值還是0,進行加1操作之後,i的值為1,然後執行緒2把i的值寫入記憶體。
最終結果i的值是1,而不是2。這就是著名的快取一致性問題。通常稱這種被多個執行緒訪問的變數為共享變數。
也就是說,如果一個變數在多個CPU中都存在快取(一般在多執行緒程式設計時才會出現),那麼就可能存在快取不一致的問題。
為了解決快取不一致性問題,通常來說有以下2種解決方法:
1)通過在匯流排加LOCK#鎖的方式
2)通過快取一致性協議
這2種方式都是硬體層面上提供的方式。
在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決快取不一致的問題。因為CPU和其他部件進行通訊都是通過匯流排來進行的,如果對匯流排加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如記憶體),從而使得只能有一個CPU能使用這個變數的記憶體。比如上面例子中 如果一個執行緒在執行 i = i +1,如果在執行這段程式碼的過程中,在總線上發出了LCOK#鎖的訊號,那麼只有等待這段程式碼完全執行完畢之後,其他CPU才能從變數i所在的記憶體讀取變數,然後進行相應的操作。這樣就解決了快取不一致的問題。
但是上面的方式會有一個問題,由於在鎖住匯流排期間,其他CPU無法訪問記憶體,導致效率低下。
所以就出現了快取一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個快取中使用的共享變數的副本是一致的。它核心的思想是:當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取。
在併發程式設計中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
可見性:是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
有序性:即程式執行的順序按照程式碼的先後順序執行。
Java記憶體模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推匯出來,那麼它們就不能保證它們的有序性,虛擬機器可以隨意地對它們進行重排序。
happens-before規則
happens-before是JMM最核心的概念,理解happens-before是理解JMM的關鍵。該規則定義了 Java 多執行緒操作的有序性和可見性,防止了編譯器重排序對程式結果的影響。
按照官方的說法:
當一個變數被多個執行緒讀取並且至少被一個執行緒寫入時,如果讀操作和寫操作沒有 HB 關係,則會產生資料競爭問題。 要想保證操作 B 的執行緒看到操作 A 的結果(無論 A 和 B 是否在一個執行緒),那麼在 A 和 B 之間必須滿足 HB 原則,如果沒有,將有可能導致重排序。 當缺少 HB 關係時,就可能出現重排序問題。
兩個操作間具有happens-before關係,並不意味著前一個操作必須要在後一個操作之前執行。happens-before僅僅要求前一個操作對後一個操作可見。appens-before原則和一般意義上的時間先後是不同的。
HB規則
程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
鎖定規則:在監視器鎖上的解鎖操作必須在同一個監視器上的加鎖操作之前執行。
volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作;
傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每一個動作;
執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;
執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;
物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始;
其中,傳遞規則至關重要如何熟練的使用傳遞規則是實現同步的關鍵。
然後,再換個角度解釋 HB:當一個操作 A HB 操作 B,那麼,操作 A 對共享變數的操作結果對操作 B 都是可見的。
同時,如果 操作 B HB 操作 C,那麼,操作 A 對共享變數的操作結果對操作 B 都是可見的。
而實現可見性的原理則是 cache protocol 和 memory barrier。通過快取一致性協議和記憶體屏障實現可見性。
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C
上述存在三個happens-before關係:
A happens-before B
B happens-before C
A happens-before C
在者三個happens-before關係中2和3是必須的,1是不必要的。因此JMM把happens-before要求禁止的重排序分了下面兩類
1.會改變程式執行結果的重排序
2.不會改變程式執行結果的重排序
JMM對這兩種不同性質的重排序,採用了不同的策略,如下:
1.對於會改變程式執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序
2.對於不會改變程式執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)
volatile關鍵字的兩層語義
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
2)禁止進行指令重排序——有序性
第一:使用volatile關鍵字會強制將修改的值立即寫入主存;
第二:使用volatile關鍵字的話,當執行緒2進行修改時,會導致執行緒1的工作記憶體中快取變數stop的快取行無效(反映到硬體層的話,就是CPU的L1或者L2快取中對應的快取行無效);
第三:由於執行緒1的工作記憶體中快取變數stop的快取行無效,所以執行緒1再次讀取變數stop的值時會去主存讀取。
ThreadLocal
ThreadLocal,很多地方叫做執行緒本地變數,也有些地方叫做執行緒本地儲存。可能很多朋友都知道ThreadLocal為變數在每個執行緒中都建立了一個副本,那麼每個執行緒可以訪問自己內部的副本變數。
執行緒共享變數快取如下:
Thread.ThreadLocalMap<ThreadLocal, Object>;
1、Thread: 當前執行緒,可以通過Thread.currentThread()獲取。
2、ThreadLocal:我們的static ThreadLocal變數。
3、Object: 當前執行緒共享變數。
我們呼叫ThreadLocal.get方法時,實際上是從當前執行緒中獲取ThreadLocalMap<ThreadLocal, Object>,然後根據當前ThreadLocal獲取當前執行緒共享變數Object。
ThreadLocal.set,ThreadLocal.remove實際上是同樣的道理。
在多執行緒環境下,每個執行緒都有自己的資料。一個執行緒使用自己的區域性變數比使用全域性變數好,區域性變數只有自己執行緒看得見,不影響其他執行緒。
ThreadLocal 能放一個執行緒級別的變數,本身能夠被多個執行緒共享使用,並且有能達到執行緒安全。所以,TreadLocal就是在多執行緒環境下保證成員變數的安全,常用方法是get/set/initialValue方法。
JDK建議把ThreadLocal定義為private static
public class ThreadLocalTest01 { //private static ThreadLocal<Integer> threadLocal = new ThreadLocal<> (); //更改初始化值 /*private static ThreadLocal<Integer> threadLocal = new ThreadLocal<> () { protected Integer initialValue() { return 200; }; };*/ private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()-> 200); public static void main(String[] args) { //獲取值 System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); //設定值 threadLocal.set(99); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); new Thread(new MyRun()).start(); new Thread(new MyRun()).start(); } public static class MyRun implements Runnable{ public void run() { threadLocal.set((int)(Math.random()*99)); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); } } }
每個執行緒自身的資料,更改不會影響其他執行緒
public class TestThreadLocal { private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()-> 1); public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); for(int i=0;i<2;i++) { new Thread(new MyRun()).start(); } } public static class MyRun implements Runnable{ public MyRun() { threadLocal.set(-100); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); } public void run() { Integer left =threadLocal.get(); System.out.println(Thread.currentThread().getName()+"得到了-->"+left); threadLocal.set(left -1); System.out.println(Thread.currentThread().getName()+"還剩下-->"+threadLocal.get()); } } }
結果:
main
main-->-100
main-->-100
Thread-0得到了-->1
Thread-0還剩下-->0
Thread-1得到了-->1
Thread-1還剩下-->0
InheritableThreadLocal
使用ThreadLocal不能繼承父執行緒的ThreadLocal的內容,而使用InheritableThreadLocal時可以做到的,這就可以很好的在父子執行緒之間傳遞資料了。
對比如下:
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { threadLocal.set(2); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); new Thread(()->{ System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); threadLocal.set(200); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); }) .start(); }
main-->2
Thread-0-->null
Thread-0-->200
private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { threadLocal.set(2); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); new Thread(()->{ System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); threadLocal.set(200); System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get()); }) .start(); }
main-->2
Thread-0-->2
Thread-0-->200
執行緒池
執行緒池能夠對執行緒進行統一分配,調優和監控:
- 降低資源消耗(執行緒無限制地建立,然後使用完畢後銷燬)
- 提高響應速度(減少了建立新執行緒的時間)
- 提高執行緒的可管理性:避免執行緒無限制建立、從而銷耗系統資源,降低系統穩定性,甚至記憶體溢位或者CPU耗盡。
執行緒池的應用場合
• 需要大量執行緒,並且完成任務的時間端
• 對效能要求苛刻
• 接受突發性的大量請求
參考詳見 https://blog.csdn.net/programmer_at/article/details/79799267
可重入鎖
Lock就是可重入鎖的實現
Java多執行緒的wait()方法和notify()方法
這兩個方法是成對出現和使用的,要執行這兩個方法,有一個前提就是,當前執行緒必須獲其物件的monitor(俗稱“鎖”),否則會丟擲IllegalMonitorStateException異常,所以這兩個方法必須在同步塊程式碼裡面呼叫。
wait():阻塞當前執行緒
notify():喚起被wait()阻塞的執行緒
所謂不可重入鎖,即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。
public class ReLockTest { Lock lock = new Lock(); public void a() throws InterruptedException { lock.lock(); doSomething(); lock.unlock(); } //不可重入 public void doSomething() throws InterruptedException { lock.lock(); //................... lock.unlock(); } public static void main(String[] args) throws InterruptedException { ReLockTest test = new ReLockTest(); test.a(); test.doSomething(); } } // 不可重入鎖 不能連續使用鎖 class Lock{ //是否佔用 private boolean isLocked = false; //使用鎖 public synchronized void lock() throws InterruptedException { while(isLocked) { wait(); } isLocked = true; } //釋放鎖 public synchronized void unlock() { isLocked = false; notify(); } }
當前執行緒執行a()方法首先獲取lock,接下來執行doSomething()方法就無法執行doSomething()中的邏輯,必須先釋放鎖。這個例子很好的說明了不可重入鎖。
可重入鎖
所謂可重入,意味著執行緒可以進入它已經擁有的鎖的同步程式碼塊兒。
public class ReLockTest { ReLock lock = new ReLock(); public void a() throws InterruptedException { lock.lock(); System.out.println(lock.getHoldCount()); doSomething(); lock.unlock(); System.out.println(lock.getHoldCount()); } //不可重入 public void doSomething() throws InterruptedException { lock.lock(); System.out.println(lock.getHoldCount()); //................... lock.unlock(); System.out.println(lock.getHoldCount()); } public static void main(String[] args) throws InterruptedException { ReLockTest test = new ReLockTest(); test.a(); Thread.sleep(1000); System.out.println(test.lock.getHoldCount()); } } // 可重入鎖 + 計數器 class ReLock{ //是否佔用 private boolean isLocked = false; private Thread lockedBy = null; //儲存執行緒 private int holdCount = 0; //使用鎖 public synchronized void lock() throws InterruptedException { Thread t = Thread.currentThread(); while(isLocked && lockedBy != t) { wait(); } isLocked = true; lockedBy = t; holdCount ++; } //釋放鎖 public synchronized void unlock()