本文首發於個人我的博客:尾尾部落html
本文是我刷了幾十篇一線互聯網校招java後端開發崗位的面經後總結的多線程相關題目,雖然有點小長,可是面試前看一看,相信能幫你輕鬆啃下多線程這塊大骨頭。java
進程是執行着的應用程序,而線程是進程內部的一個執行序列。一個進程能夠有多個線程。線程又叫作輕量級進程。 進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是操做系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是 CPU 調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時一般擁有獨立的內存單元,而線程之間能夠共享內存。使用多線程的編程一般可以帶來更好的性能和用戶體驗,可是多線程的程序對於其餘程序是不友好的,由於它佔用了更多的 CPU 資源。面試
線程間的通訊目的主要是用於線程同步,因此線程沒有像進程通訊中的用於數據交換的通訊機制。數據庫
public class thread1 extends Thread {
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("我是線程"+this.getId());
}
}
public static void main(String[] args) {
thread1 th1 = new thread1();
thread1 th2 = new thread1();
th1.start();
th2.start();
}
}
複製代碼
public class thread2 implements Runnable {
public String ThreadName;
public thread2(String tName){
ThreadName = tName;
}
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(ThreadName);
}
}
public static void main(String[] args) {
// 建立一個Runnable接口實現類的對象
thread2 th1 = new thread2("線程A:");
thread2 th2 = new thread2("線程B:");
// 將此對象做爲形參傳遞給Thread類的構造器中,建立Thread類的對象,此對象即爲一個線程
Thread myth1 = new Thread(th1);
Thread myth2 = new Thread(th2);
// 調用start()方法,啓動線程並執行run()方法
myth1.start();
myth2.start();
}
}
複製代碼
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer>
{
@Override
public Integer call() throws Exception{
int i = 0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
public static void main(String[] args){
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++){
System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
if(i==20){
new Thread(ft,"有返回值的線程").start();
}
}
try{
System.out.println("子線程的返回值:"+ft.get());
} catch (InterruptedException e){
e.printStackTrace();
} catch (ExecutionException e){
e.printStackTrace();
}
}
}
複製代碼
一、採用實現Runnable、Callable接口的方式建立多線程時,線程類只是實現了Runnable接口或Callable接口,還能夠繼承其餘類。缺點是編程稍微複雜,若是要訪問當前線程,則必須使用Thread.currentThread()方法。 二、使用繼承Thread類的方式建立多線程時,編寫簡單,若是須要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this便可得到當前線程。缺點是線程類已經繼承了Thread類,因此不能再繼承其餘父類。 三、Runnable和Callable的區別 (1) Callable規定重寫call(),Runnable重寫run()。 (2) Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。 (3) call方法能夠拋出異常,run方法不能夠。 (4) 運行Callable任務能夠拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。經過Future對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。編程
PS: 具備較高線程優先級的線程對象僅表示此線程具備較多的執行機會,而非優先執行。後端
sleep() 和 yield() 二者的區別: ① sleep()方法會給其餘線程運行的機會,不考慮其餘線程的優先級,所以會給較低優先級線程一個運行的機會。yield()方法只會給相同優先級或者更高優先級的線程一個運行的機會。 ② 當線程執行了 sleep(long millis) 方法,將轉到阻塞狀態,參數millis指定睡眠時間。當線程執行了yield()方法,將轉到就緒狀態。 ③ sleep() 方法聲明拋出InterruptedException異常,而 yield() 方法沒有聲明拋出任何異常。數組
wait、notify、notifyAll是java同步機制中重要的組成部分,結合synchronized關鍵字使用,能夠創建不少優秀的同步模型。這3個方法並非Thread類或者是Runnable接口的方法,而是Object類的3個本地方法。 調用一個Object的wait與notify/notifyAll的時候,必須保證調用代碼對該Object是同步的,也就是說必須在做用等同於synchronized(obj){......}的內部纔可以去調用obj的wait與notify/notifyAll三個方法,不然就會報錯:java.lang.IllegalMonitorStateException:current thread not owner
安全
先說兩個概念:鎖池和等待池 鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權,可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中 @知乎--文龍bash
sleep()方法是線程類(Thread)的靜態方法,致使此線程暫停執行指定時間,將執行機會給其餘線程,可是監控狀態依然保持,到時後會自動恢復(線程回到就緒(ready)狀態),由於調用 sleep 不會釋放對象鎖。wait() 是 Object 類的方法,對此對象調用 wait()方法致使本線程放棄對象鎖(線程暫停執行),進入等待此對象的等待鎖定池,只有針對此對象發出 notify 方法(或 notifyAll)後本線程才進入對象鎖定池準備得到對象鎖進入就緒狀態。服務器
(1)樂觀鎖:很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會去判斷在此期間有沒有人去更新這個數據(可使用版本號等機制)。若是由於衝突失敗就重試。樂觀鎖適用於寫比較少的狀況下,即衝突比較少發生,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。 (2)悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖,效率比較低。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖。
樂觀鎖的實現主要就兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是 Compare and Swap ( CAS )。 CAS:CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。 CAS 操做中包含三個操做數 —— 須要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。若是內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B。不然處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「 我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。 」這其實和樂觀鎖的衝突檢查+數據更新的原理是同樣的。
樂觀鎖是一種思想,CAS是這種思想的一種實現方式。
若是內存地址V初次讀取的值是A,而且在準備賦值的時候檢查到它的值仍然爲A,那咱們就能說它的值沒有被其餘線程改變過了嗎?若是在這段期間它的值曾經被改爲了B,後來又被改回爲A,那CAS操做就會誤認爲它歷來沒有被改變過。這個漏洞稱爲CAS操做的「ABA」問題。ava併發包爲了解決這個問題,提供了一個帶有標記的原子引用類「AtomicStampedReference」,它能夠經過控制變量值的版原本保證CAS的正確性。所以,在使用CAS前要考慮清楚「ABA」問題是否會影響程序併發的正確性,若是須要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。
自旋CAS(不成功,就一直循環執行,直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。
當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖來保證原子性。
什麼是死鎖:兩個進程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖。結果就是兩個進程都陷入了無限的等待中。 產生死鎖的四個必要條件: 互斥條件:一個資源每次只能被一個進程使用。 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。 不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不知足,就不會發生死鎖。 考慮以下情形: (1)線程A當前持有互斥所鎖lock1,線程B當前持有互斥鎖lock2。 (2)線程A試圖獲取lock2,由於線程B正持有lock2,所以線程A會阻塞等待線程B對lock2釋放。 (3)若是此時線程B也在試圖獲取lock1,同理線程也會阻塞。 (4)二者都在等待對方所持有可是雙方都不釋放的鎖,這時便會一直阻塞造成死鎖。 死鎖的解決方法: a 撤消陷於死鎖的所有進程; b 逐個撤消陷於死鎖的進程,直到死鎖不存在; c 從陷於死鎖的進程中逐個強迫放棄所佔用的資源,直至死鎖消失。 d 從另一些進程那裏強行剝奪足夠數量的資源分配給死鎖進程,以解除死鎖狀態
使用多線程的時候,一種很是簡單的避免死鎖的方式就是:指定獲取鎖的順序,並強制線程按照指定的順序獲取鎖。所以,若是全部的線程都是以一樣的順序加鎖和釋放鎖,就不會出現死鎖了
對於過可見性、有序性及原子性問題,一般狀況下咱們能夠經過Synchronized關鍵字來解決這些個問題,不過若是對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操做,對系統的性能有比較大的影響,因此,若是有其餘解決方案,咱們一般都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另外一種解決可見性和有序性問題的方案。對於原子性,須要強調一點,也是你們容易誤解的一點:對volatile變量的單次讀/寫操做能夠保證原子性的,如long和double類型變量,可是並不能保證i++這種操做的原子性,由於本質上i++是讀、寫兩次操做。
問題:操做系統能夠對指令進行重排序,多線程環境下就可能將一個未初始化的對象引用暴露出來,從而致使不可預料的結果 解決原理:volatile關鍵字經過提供「內存屏障」的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。 一、在每一個volatile寫操做前插入StoreStore屏障,在寫操做後插入StoreLoad屏障。 二、在每一個volatile讀操做前插入LoadLoad屏障,在讀操做後插入LoadStore屏障。
問題:可見性問題主要指一個線程修改了共享變量值,而另外一個線程卻看不到 解決原理:(1)修改volatile變量時會強制將修改後的值刷新的主內存中。 (2)修改volatile變量後會致使其餘線程工做內存中對應的變量值失效。所以,再讀取該變量值的時候就須要從新從讀取主內存中的值。
相對於synchronized塊的代碼鎖,volatile應該是提供了一個輕量級的針對共享變量的鎖,當咱們在多個線程間使用共享變量進行通訊的時候須要考慮將共享變量用volatile來修飾。 volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操做,也就不會執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。 使用建議:在兩個或者更多的線程須要訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,不必使用volatile。 因爲使用volatile屏蔽掉了JVM中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。
一、volatile不會進行加鎖操做: volatile變量是一種稍弱的同步機制在訪問volatile變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。 二、volatile變量做用相似於同步變量讀寫操做: 從內存可見性的角度看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量至關於進入同步代碼塊。 三、volatile不如synchronized安全: 在代碼中若是過分依賴volatile變量來控制狀態的可見性,一般會比使用鎖的代碼更脆弱,也更難以理解。僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。通常來講,用同步機制會更安全些。 四、volatile沒法同時保證內存可見性和原子性: 加鎖機制(即同步機制)既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性,緣由是聲明爲volatile的簡單變量若是當前值與該變量之前的值相關,那麼volatile關鍵字不起做用,也就是說以下的表達式都不是原子操做:「count++」、「count = count+1」。
當且僅當知足如下全部條件時,才應該使用volatile變量: 一、對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。 二、該變量沒有包含在具備其餘變量的不變式中。 總結:在須要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,嘗試其餘任何方式都是有風險的。尤爲在、jdK1.5以後,對synchronized同步機制作了不少優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提高。
synchronized能夠保證方法或者代碼塊在運行時,同一時刻只有一個方法能夠進入到臨界區,同時它還能夠保證共享變量的內存可見性。Synchronized主要有如下三個做用:保證互斥性、保證可見性、保證順序性。
修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖。實現原理:指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞), 而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。
public synchronized void increase(){
i++;
}
複製代碼
修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖
public static synchronized void increase(){
i++;
}
複製代碼
修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。實現原理:使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。
static AccountingSync instance=new AccountingSync();
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
複製代碼
Lock是一個接口,它的的實現類提供了比synchronized更普遍意義上鎖操做,它容許用戶更靈活的代碼結構,更多的不一樣特效。Lock的實現類主要有ReentrantLock和ReentrantReadWriteLock。
Lock lock=new ReentrantLock();
lock.lock();
try{
// do something
// 若是有return要寫在try塊中
}finally{
lock.unlock();
}
複製代碼
Condition是Java提供來實現等待/通知的類,Condition類還提供比wait/notify更豐富的功能,Condition對象是由lock對象所建立的。可是同一個鎖能夠建立多個Condition的對象,即建立多個對象監視器。這樣的好處就是能夠指定喚醒線程。notify喚醒的線程是隨機喚醒一個。 Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成大相徑庭的對象,以便經過將這些對象與任意 Lock 實現組合使用,爲每一個對象提供多個等待 set (wait-set)。 其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。 在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統線程的通訊方式,Condition均可以實現,這裏注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。
1.Condition中的await()方法至關於Object的wait()方法,Condition中的signal()方法至關於Object的notify()方法,Condition中的signalAll()至關於Object的notifyAll()方法。 不一樣的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是須要與互斥鎖/共享鎖捆綁使用的。 2.Condition它更強大的地方在於:可以更加精細的控制多線程的休眠與喚醒。對於同一個鎖,咱們能夠建立多個Condition,在不一樣的狀況下使用不一樣的Condition。 例如,假如多線程讀/寫同一個緩衝區:當向緩衝區中寫入數據以後,喚醒"讀線程";當從緩衝區讀出數據以後,喚醒"寫線程";而且當緩衝區滿的時候,"寫線程"須要等待;當緩衝區爲空時,"讀線程"須要等待。 若是採用Object類中的wait(),notify(),notifyAll()實現該緩衝區,當向緩衝區寫入數據以後須要喚醒"讀線程"時,不可能經過notify()或notifyAll()明確的指定喚醒"讀線程",而只能經過notifyAll喚醒全部線程(可是notifyAll沒法區分喚醒的線程是讀線程,仍是寫線程)。 可是,經過Condition,就能明確的指定喚醒讀線程。
synchronized | Lock | |
---|---|---|
存在層次 | Java的關鍵字 | 是一個接口 |
鎖的釋放 | 一、以獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖 | 在finally中必須釋放鎖,否則容易形成線程死鎖 |
鎖的獲取 | 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 | Lock可讓等待鎖的線程響應中斷 |
鎖狀態 | 沒法判斷 | 能夠判斷有沒有成功獲取鎖 |
鎖類型 | 可重入 不可中斷 非公平 | 可重入 可中斷 公平/非公平 |
性能方面,JDK1.5中,synchronized是性能低效的。由於這是一個重量級操做,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。多線程環境下,synchronized的吞吐量降低的很是嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。
到了JDK1.6,synchronize加入了不少優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。致使在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在將來的版本中還有優化餘地,因此仍是提倡在synchronized能實現需求的狀況下,優先考慮使用synchronized來進行同步。
Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。
在沒有實際競爭的狀況下,還可以針對部分場景繼續優化。若是不只僅沒有實際競爭,自始至終,使用鎖的線程都只有一個,那麼,維護輕量級鎖都是浪費的。偏向鎖的目標是,減小無競爭且只有一個線程使用鎖的狀況下,使用輕量級鎖產生的性能消耗。輕量級鎖每次申請、釋放鎖都至少須要一次CAS,但偏向鎖只有初始化時須要一次CAS。 「偏向」的意思是,偏向鎖假定未來只有第一個申請鎖的線程會使用鎖(不會有任何線程再來申請鎖),所以,只須要在Mark Word中CAS記錄owner(本質上也是更新,但初始值爲空),若是記錄成功,則偏向鎖獲取成功,記錄鎖狀態爲偏向鎖,之後當前線程等於owner就能夠零成本的直接得到鎖;不然,說明有其餘線程競爭,膨脹爲輕量級鎖。 偏向鎖沒法使用自旋鎖優化,由於一旦有其餘線程申請鎖,就破壞了偏向鎖的假定。
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。輕量級鎖是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。 使用輕量級鎖時,不須要申請互斥量,僅僅將Mark Word中的部分字節CAS更新指向線程棧中的Lock Record,若是更新成功,則輕量級鎖獲取成功,記錄鎖狀態爲輕量級鎖;不然,說明已經有線程得到了輕量級鎖,目前發生了鎖競爭(不適合繼續使用輕量級鎖),接下來膨脹爲重量級鎖。
重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具有Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責作互斥,後一個用於作線程同步。
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。 可是線程自旋是須要消耗cup的,說白了就是讓cup在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用cup自旋作無用功,因此須要設定一個自旋等待的最大時間。 若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。
自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定:
自適應自旋解決的是「鎖競爭時間不肯定」的問題。JVM很難感知到確切的鎖競爭時間,而交給用戶分析就違反了JVM的設計初衷。自適應自旋假定不一樣線程持有同一個鎖對象的時間基本至關,競爭程度趨於穩定,所以,能夠根據上一次自旋的時間與結果調整下一次自旋的時間。
偏向鎖:無實際競爭,且未來只有第一個申請鎖的線程會使用鎖。 輕量級鎖:無實際競爭,多個線程交替使用鎖;容許短期的鎖競爭。 重量級鎖:有實際競爭,且鎖競爭時間長。 另外,若是鎖競爭時間短,可使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減小線程切換。 若是鎖競爭程度逐漸提升(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,可以提升系統的總體性能。
鎖膨脹的過程:只有一個線程進入臨界區(偏向鎖),多個線程交替進入臨界區(輕量級鎖),多線程同時進入臨界區(重量級鎖)。
AQS便是AbstractQueuedSynchronizer,一個用來構建鎖和同步工具的框架,包括經常使用的ReentrantLock、CountDownLatch、Semaphore等。 AbstractQueuedSynchronizer是一個抽象類,主要是維護了一個int類型的state屬性和一個非阻塞、先進先出的線程等待隊列;其中state是用volatile修飾的,保證線程之間的可見性,隊列的入隊和出對操做都是無鎖操做,基於自旋鎖和CAS實現;另外AQS分爲兩種模式:獨佔模式和共享模式,像ReentrantLock是基於獨佔模式模式實現的,CountDownLatch、CyclicBarrier等是基於共享模式。
若是併發的線程數量不少,而且每一個線程都是執行一個時間很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷燬線程須要時間。 線程池的產生和數據庫的鏈接池相似,系統啓動一個線程的代價是比較高昂的,若是在程序啓動的時候就初始化必定數量的線程,放入線程池中,在須要是使用時從池子中去,用完再放回池子裏,這樣能大大的提升程序性能,再者,線程池的一些初始化配置,也能夠有效的控制系統併發的數量,防止由於消耗過多的內存,而把服務器累趴下。
經過Executors工具類能夠建立各類類型的線程池,以下爲常見的四種:
一、ArrayBlockingQueue 是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。 二、LinkedBlockingQueue 一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列 三、SynchronousQueue 一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。 四、PriorityBlockingQueue 一個具備優先級的無限阻塞隊列。
獲取最新資訊,請關注微信公衆號:南強說晚安
我在參加掘金技術徵文 徵文活動地址