今天來和大家討論一下並行多執行緒方面的知識,像死鎖啊,生命週期啊,執行緒池啊,可重用啊都有涉及,希望對大家有所幫助。
身為一個部落格博主,最近逛了一下圈子,發現好多大佬都在分享並行多執行緒方面的技術。今天咱也跟一下風,寫一篇關於並行多執行緒方面的文章,本篇文章由淺入深,主要是讓大家能夠清楚的認識他們,希望對大家有所幫助,有什麼意見或建議大家評論區裡見。
另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,下方只是部分截圖 想要資料的話也可以點選795983544領取 暗號CSDN。
執行緒是作業系統能夠進行運算排程的最小單位,它被包含在程序之中,是程序中的實際運作單位。程式設計師可以通過它進行多處理器程式設計,你可以使用多執行緒對運算密集型任務提速。比如,如果一個執行緒完成一個任務要100毫秒,那麼用十個執行緒完成改任務只需10毫秒。
執行緒是程序的子集,一個程序可以有很多執行緒,每條執行緒並行執行不同的任務。不同的程序使用不同的記憶體空間,而所有的執行緒共用一片相同的記憶體空間。每個執行緒都擁有單獨的棧記憶體用來儲存本地資料。
兩種方式:java.lang.Thread 類的範例就是一個執行緒但是它需要呼叫java.lang.Runnable介面來執行,由於執行緒類本身就是呼叫的Runnable介面所以你可以繼承java.lang.Thread 類或者直接呼叫Runnable介面來重寫run()方法實現執行緒。
1.volatile它所修飾的變數不保留拷貝,直接存取主記憶體中的。
在Java記憶體模型中,有main memory,每個執行緒也有自己的memory (例如暫存器)。為了效能,一個執行緒會在自己的memory中保持要存取的變數的副本。這樣就會出現同一個變 量在某個瞬間,在一個執行緒的memory中的值可能與另外一個執行緒memory中的值,或者main memory中的值不一致的情況。 一個變數宣告為volatile,就意味著這個變數是隨時會被其他執行緒修改的,因此不能將它cache線上程memory中。
2.synchronized
當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼。
一、當兩個並行執行緒存取同一個物件object中的這個synchronized(this)同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。
二、然而,當一個執行緒存取object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以存取該object中的非synchronized(this)同步程式碼塊。
三、尤其關鍵的是,當一個執行緒存取object的一個synchronized(this)同步程式碼塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼塊的存取將被阻塞。
四、當一個執行緒存取object的一個synchronized(this)同步程式碼塊時,它就獲得了這個object的物件鎖。結果,其它執行緒對該object物件所有同步程式碼部分的存取都被暫時阻塞。
五、以上規則對其它物件鎖同樣適用.
當我們在Java程式中新建一個執行緒時,它的狀態是New。當我們呼叫執行緒的start()方法時,狀態被改變為Runnable。執行緒排程器會為Runnable執行緒池中的執行緒分配CPU時間並且講它們的狀態改變為Running。其他的執行緒狀態還有Waiting,Blocked 和Dead。
每一個執行緒都是有優先順序的,一般來說,高優先順序的執行緒在執行時會具有優先權,但這依賴於執行緒排程的實現,這個實現是和作業系統相關的(OS dependent)。我們可以定義執行緒的優先順序,但是這並不能保證高優先順序的執行緒會在低優先順序的執行緒前執行。執行緒優先順序是一個int變數(從1-10),1代表最低優先順序,10代表最高優先順序。
死鎖是指兩個以上的執行緒永遠阻塞的情況,這種情況產生至少需要兩個以上的執行緒和兩個以上的資源。
分析死鎖,我們需要檢視Java應用程式的執行緒轉儲。我們需要找出那些狀態為BLOCKED的執行緒和他們等待的資源。每個資源都有一個唯一的id,用這個id我們可以找出哪些執行緒已經擁有了它的物件鎖。
避免巢狀鎖,只在需要的地方使用鎖和避免無限期等待是避免死鎖的通常辦法。
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。一個執行緒安全的計數器類的同一個範例物件在被多個執行緒使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,執行緒安全和非執行緒安全的。Vector 是用同步方法來實現執行緒安全的, 而和它相似的ArrayList不是執行緒安全的。
Java提供了很豐富的API但沒有為停止執行緒提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由於潛在的死鎖威脅因此在後續的JDK版本中他們被棄用了,之後Java API的設計者就沒有提供一個相容且執行緒安全的方法來停止一個執行緒。當run() 或者 call() 方法執行完的時候執行緒會自動結束,如果要手動結束一個執行緒,你可以用volatile 布林變數來退出run()方法的迴圈或者是取消任務來中斷執行緒
ThreadLocal用於建立執行緒的本地變數,我們知道一個物件的所有執行緒會共用它的全域性變數,所以這些變數不是執行緒安全的,我們可以使用同步技術。但是當我們不想使用同步的時候,我們可以選擇ThreadLocal變數。
每個執行緒都會擁有他們自己的Thread變數,它們可以使用get()\set()方法去獲取他們的預設值或者線上程內部改變他們的值。ThreadLocal範例通常是希望它們同執行緒狀態關聯起來是private static屬性。
Thread.sleep()使當前執行緒在指定的時間處於「非執行」(Not Runnable)狀態。執行緒一直持有物件的監視器。比如一個執行緒當前在一個同步塊或同步方法中,其它執行緒不能進入該塊或方法中。如果另一執行緒呼叫了interrupt()方法,它將喚醒那個「睡眠的」執行緒。
注意:sleep()是一個靜態方法。這意味著只對當前執行緒有效,一個常見的錯誤是呼叫t.sleep(),(這裡的t是一個不同於當前執行緒的執行緒)。即便是執行t.sleep(),也是當前執行緒進入睡眠,而不是t執行緒。t.suspend()是過時的方法,使用suspend()導致執行緒進入停滯狀態,該執行緒會一直持有物件的監視器,suspend()容易引起死鎖問題。
object.wait()使當前執行緒出於「不可執行」狀態,和sleep()不同的是wait是object的方法而不是thread。呼叫object.wait()時,執行緒先要獲取這個物件的物件鎖,當前執行緒必須在鎖物件保持同步,把當前執行緒新增到等待佇列中,隨後另一執行緒可以同步同一個物件鎖來呼叫object.notify(),這樣將喚醒原來等待中的執行緒,然後釋放該鎖。基本上wait()/notify()與sleep()/interrupt()類似,只是前者需要獲取物件鎖。
當所有執行緒阻塞,或者由於需要的資源無效而不能處理,不存在非阻塞執行緒使資源可用。JavaAPI中執行緒活鎖可能發生在以下情形:
1,當所有執行緒在程式中執行Object.wait(0),引數為0的wait方法。程式將發生活鎖直到在相應的物件上有執行緒呼叫Object.notify()或者Object.notifyAll()。
2,當所有執行緒卡在無限迴圈中。
java.util.Timer是一個工具類,可以用於安排一個執行緒在未來的某個特定時間執行。Timer類可以用安排一次性任務或者週期任務。
java.util.TimerTask是一個實現了Runnable介面的抽象類,我們需要去繼承這個類來建立我們自己的定時任務並使用Timer去安排它的執行。
同步集合與並行集合都為多執行緒和並行提供了合適的執行緒安全的集合,不過並行集合的可延伸性更高。
在Java1.5之前程式設計師們只有同步集合來用且在多執行緒並行的時候會導致爭用,阻礙了系統的擴充套件性。
Java5介紹了並行集合像ConcurrentHashMap,不僅提供執行緒安全還用鎖分離和 內部分割區等現代技術提高了可延伸性。
同步塊是更好的選擇,因為它不會鎖住整個物件(當然你也可以讓它鎖住整個物件)。同步方法會鎖住整個物件,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行並需要等待獲得這個物件上的鎖。
建立執行緒要花費昂貴的資源和時間,如果任務來了才建立執行緒那麼響應時間會變長,而且一個程序能建立的執行緒數有限。
為了避免這些問題,在程式啟動的時候就建立若干執行緒來響應處理,它們被稱為執行緒池,裡面的執行緒叫工作執行緒。
從JDK1.5開始,Java API提供了Executor框架讓你可以建立不同的執行緒池。比如單執行緒池,每次處理一個任務;數目固定的執行緒池或者是快取執行緒池(一個適合很多生存期短的任務的程式的可延伸執行緒池)。
這兩個方法是Swing API 提供給Java開發者用來從當前執行緒而不是事件派發執行緒更新GUI元件用的。InvokeAndWait()同步更新GUI元件,比如一個進度條,一旦進度更新了,進度條也要做出相應改變。如果進度被多個執行緒跟蹤,那麼就呼叫invokeAndWait()方法請求事件派發執行緒對元件進行相應更新。而invokeLater()方法是非同步呼叫更新元件的。
忙迴圈就是程式設計師用迴圈讓一個執行緒等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙迴圈不會放棄CPU,它就是在執行一個空迴圈。這麼做的目的是為了保留CPU快取。
在多核系統中,一個等待執行緒醒來的時候可能會在另一個核心執行,這樣會重建快取。為了避免重建快取和減少等待重建的時間就可以使用它了。
Java記憶體模型規定和指引Java程式在不同的記憶體架構、CPU和作業系統間有確定性地行為。它在多執行緒的情況下尤其重要。Java記憶體模型對一個執行緒所做的變動能被其它執行緒可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程式設計師在並行程式設計時思路更清晰。比如,先行發生關係確保了:
執行緒內的程式碼能夠按先後順序執行,這被稱為程式次序規則。
對於同一個鎖,一個解鎖操作一定要發生在時間上後發生的另一個鎖定操作之前,也叫做管程鎖定規則。
前一個對volatile的寫操作在後一個volatile的讀操作之前,也叫volatile變數規則。
一個執行緒內的任何操作必需在這個執行緒的start()呼叫之後,也叫作執行緒啟動規則。
一個執行緒的所有操作都會線上程終止之前,執行緒終止規則。
一個物件的終結操作必需在這個物件構造完成之後,也叫物件終結規則。
可傳遞性
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多執行緒的中斷機制是用內部標識來實現的,呼叫Thread.interrupt()來中斷一個執行緒就會設定中斷標識為true。當中斷執行緒呼叫靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。
非靜態方法isInterrupted()用來查詢其它執行緒的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何丟擲InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個執行緒的中斷狀態都有可能被其它執行緒呼叫中斷來改變。
同步集合與並行集合都為多執行緒和並行提供了合適的執行緒安全的集合,不過並行集合的可延伸性更高。在Java1.5之前程式設計師們只有同步集合來用且在多執行緒並行的時候會導致爭用,阻礙了系統的擴充套件性。Java5介紹了並行集合像ConcurrentHashMap,不僅提供執行緒安全還用鎖分離和內部分割區等現代技術提高了可延伸性。
不管是同步集合還是並行集合他們都支援執行緒安全,他們之間主要的區別體現在效能和可延伸性,還有他們如何實現的執行緒安全上。
同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他們並行的實現(ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)會慢得多。造成如此慢的主要原因是鎖, 同步集合會把整個Map或List鎖起來,而並行集合不會。並行集合實現執行緒安全是通過使用先進的和成熟的技術像鎖剝離。
比如ConcurrentHashMap 會把整個Map 劃分成幾個片段,只對相關的幾個片段上鎖,同時允許多執行緒存取其他未上鎖的片段。
同樣的,CopyOnWriteArrayList 允許多個執行緒以非同步的方式讀,當有執行緒寫的時候它會將整個List複製一個副本給它。
如果在讀多寫少這種對並行集合有利的條件下使用並行集合,這會比使用同步集合更具有可伸縮性。
建立執行緒要花費昂貴的資源和時間,如果任務來了才建立執行緒那麼響應時間會變長,而且一個程序能建立的執行緒數有限。為了避免這些問題,在程式啟動的時候就建立若干執行緒來響應處理,它們被稱為執行緒池,裡面的執行緒叫工作執行緒。從JDK1.5開始,Java API提供了Executor框架讓你可以建立不同的執行緒池。比如單執行緒池,每次處理一個任務;數目固定的執行緒池或者是快取執行緒池(一個適合很多生存期短的任務的程式的可延伸執行緒池)
執行緒池的作用,就是在呼叫執行緒的時候初始化一定數量的執行緒,有執行緒過來的時候,先檢測初始化的執行緒還有空的沒有,沒有就再看當前執行中的執行緒數是不是已經達到了最大數,如果沒有,就新分配一個執行緒去處理。
就像餐館中吃飯一樣,從裡面叫一個服務員出來;但如果已經達到了最大數,就相當於服務員已經用盡了,那沒得辦法,另外的執行緒就只有等了,直到有新的「服務員」為止。
執行緒池的優點就是可以管理執行緒,有一個高度中樞,這樣程式才不會亂,保證系統不會因為大量的並行而因為資源不足掛掉。
活鎖:一個執行緒通常會有會響應其他執行緒的活動。如果其他執行緒也會響應另一個執行緒的活動,那麼就有可能發生活鎖。同死鎖一樣,發生活鎖的執行緒無法繼續執行。然而執行緒並沒有阻塞——他們在忙於響應對方無法恢復工作。這就相當於兩個在走廊相遇的人:甲向他自己的左邊靠想讓乙過去,而乙向他的右邊靠想讓甲過去。可見他們阻塞了對方。甲向他的右邊靠,而乙向他的左邊靠,他們還是阻塞了對方。
死鎖:兩個或更多執行緒阻塞著等待其它處於死鎖狀態的執行緒所持有的鎖。死鎖通常發生在多個執行緒同時但以不同的順序請求同一組鎖的時候,死鎖會讓你的程式掛起無法完成任務。
死鎖的發生必須滿足以下四個條件:
互斥條件:一個資源每次只能被一個程序使用。
請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
三種用於避免死鎖的技術:
加鎖順序(執行緒按照一定的順序加鎖)
加鎖時限(執行緒嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己佔有的鎖)
死鎖檢測
1,notify()和notifyAll()都是Object物件用於通知處在等待該物件的執行緒的方法。
2,void notify(): 喚醒一個正在等待該物件的執行緒。
3,void notifyAll(): 喚醒所有正在等待該物件的執行緒。
兩者的最大區別在於:
notifyAll使所有原來在該物件上等待被notify的執行緒統統退出wait的狀態,變成等待該物件上的鎖,一旦該物件被解鎖,他們就會去競爭。
notify他只是選擇一個wait狀態執行緒進行通知,並使它獲得該物件上的鎖,但不驚動其他同樣在等待被該物件notify的執行緒們,當第一個執行緒執行完畢以後釋放物件上的鎖,此時如果該物件沒有再次使用notify語句,即便該物件已經空閒,其他wait狀態等待的執行緒由於沒有得到該物件的通知,繼續處在wait狀態,直到這個物件發出一個notify或notifyAll,它們等待的是被notify或notifyAll,而不是鎖。
Java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為Java 類,而不是作為語言的特性來實現。這就為Lock 的多種實現留下了空間,各種實現可能有不同的排程演演算法、效能特性或者鎖定語意。 ReentrantLock 類實現了Lock ,它擁有與synchronized 相同的並行性和記憶體語意,但是新增了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。(換句話說,當許多執行緒都想存取共用資源時,JVM可以花更少的時候來排程執行緒,把更多時間用在執行執行緒上。)
Reentrant 鎖意味著什麼呢?簡單來說,它有一個與鎖相關的獲取計數器,如果擁有鎖的某個執行緒再次得到鎖,那麼獲取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。這模仿了synchronized 的語意;如果執行緒進入由執行緒已經擁有的監控器保護的synchronized 塊,就允許執行緒繼續進行,當執行緒退出第二個(或者後續)synchronized塊的時候,不釋放鎖,只有執行緒退出它進入的監控器保護的第一個synchronized 塊時,才釋放鎖。
讀寫鎖可以用於 「多讀少寫」 的場景,讀寫鎖支援多個讀操作並行執行,寫操作只能由一個執行緒來操作
ReadWriteLock對向資料結構相對不頻繁地寫入,但是有多個任務要經常讀取這個資料結構的這類情況進行了優化。ReadWriteLock使得你可以同時有多個讀取者,只要它們都不試圖寫入即可。如果寫鎖已經被其他任務持有,那麼任何讀取者都不能存取,直至這個寫鎖被釋放為止。
ReadWriteLock 對程式效能的提高主要受制於如下幾個因素:
1,資料被讀取的頻率與被修改的頻率相比較的結果。
2,讀取和寫入的時間
3,有多少執行緒競爭
4,是否在多處理機器上執行
現在的網際網路公司都很重視高並行高可用技術,理論知識需要我們去清楚理解,而最重要的還是工作中所運用到的。那句話說的好,面試造火箭,工作擰螺絲,希望大家都有造火箭的一天,那就說明你已經很牛皮了。其實我寫了這麼多,只是我自己的總結,並不一定適用於所有人,相信經過一些面試,大家都會有這些感觸。
另外本人整理收藏了20年多家公司面試知識點整理 共127頁的PDF 以及各種知識點整理 免費分享給大家,想要資料的話點選795983544暗號CSDN自行領取。今天的分享就到這了,希望大家多多支援我,一個不甘平凡的小碼農!