面試題-多執行緒篇
執行緒排程
執行緒是cpu任務排程的最小執行單位,每個執行緒擁有自己獨立的程式計數器、虛擬機器棧、本地方法棧
執行緒狀態:建立、就緒、執行、阻塞、死亡
2、執行緒狀態切換
方法 | 作用 | 區別 |
---|---|---|
start | 啟動執行緒,由虛擬機器自動排程執行run()方法 | 執行緒處於就緒狀態 |
run | 執行緒邏輯程式碼塊處理,JVM排程執行 | 執行緒處於執行狀態 |
sleep | 讓當前正在執行的執行緒休眠(暫停執行) | 不釋放鎖 |
wait | 使得當前執行緒等待 | 釋放同步鎖 |
notify | 喚醒在此物件監視器上等待的單個執行緒 | 喚醒單個執行緒 |
notifyAll | 喚醒在此物件監視器上等待的所有執行緒 | 喚醒多個執行緒 |
yiled | 停止當前執行緒,讓同等優先權的執行緒執行 | 用Thread類呼叫 |
join | 使當前執行緒停下來等待,直至另一個呼叫join方法的執行緒終止 | 用執行緒物件呼叫 |
3、阻塞喚醒過程
阻塞:
這三個方法的呼叫都會使當前執行緒阻塞。該執行緒將會被放置到對該Object的請求等待佇列中,然後讓出當前對Object所擁有的所有的同步請求。執行緒會一直暫停所有執行緒排程,直到下面其中一種情況發生:
① 其他執行緒呼叫了該Object的notify方法,而該執行緒剛好是那個被喚醒的執行緒;
② 其他執行緒呼叫了該Object的notifyAll方法;
喚醒:
執行緒將會從等待佇列中移除,重新成為可排程執行緒。它會與其他執行緒以常規的方式競爭物件同步請求。一旦它重新獲得物件的同步請求,所有之前的請求狀態都會恢復,也就是執行緒呼叫wait的地方的狀態。執行緒將會在之前呼叫wait的地方繼續執行下去。
為什麼要出現在同步程式碼塊中:
由於wait()屬於Object方法,呼叫之後會強制釋放當前物件鎖,所以在wait()
呼叫時必須拿到當前物件的監視器monitor物件。因此,wait()方法在同步方法/程式碼塊中呼叫。
4、wait和sleep區別
-
wait 方法必須在 synchronized 保護的程式碼中使用,而 sleep 方法並沒有這個要求。
-
wait 方法會主動釋放 monitor 鎖,在同步程式碼中執行 sleep 方法時,並不會釋放 monitor 鎖。
-
wait 方法意味著永久等待,直到被中斷或被喚醒才能恢復,不會主動恢復,sleep 方法中會定義一個時間,時間到期後會主動恢復。
-
wait/notify 是 Object 類的方法,而 sleep 是 Thread 類的方法。
5、建立執行緒方式
實現 Runnable 介面(優先使用)
public class RunnableThread implements Runnable { @Override public void run() {System.out.println('用實現Runnable介面實現執行緒');}}
實現Callable介面(有返回值可丟擲異常)
class CallableTask implements Callable<Integer> { @Override public Integer call() throws Exception { return new Random().nextInt();}}
繼承Thread類(java不支援多繼承)
public class ExtendsThread extends Thread { @Override public void run() {System.out.println('用Thread類實現執行緒');}}
使用執行緒池(底層都是實現run方法)
static class DefaultThreadFactory implements ThreadFactory { DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() +"-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0); if (t.isDaemon()) t.setDaemon(false); //是否守護執行緒 if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); //執行緒優先順序 return t; }}
執行緒池
優點:通過複用已建立的執行緒,降低資源損耗、執行緒可以直接處理佇列中的任務加快響應速度、同時便於統一監控和管理。
1、執行緒池建構函式
/*** 執行緒池建構函式7大引數*/public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler) {}
引數介紹:
引數 | 作用 |
---|---|
corePoolSize | 核心執行緒池大小 |
maximumPoolSize | 最大執行緒池大小 |
keepAliveTime | 執行緒池中超過 corePoolSize 數目的空閒執行緒最大存活時間; |
TimeUnit | keepAliveTime 時間單位 |
workQueue | 阻塞任務佇列 |
threadFactory | 新建執行緒工廠 |
RejectedExecutionHandler | 拒絕策略。當提交任務數超過 maxmumPoolSize+workQueue 之和時,任務會交給RejectedExecutionHandler 來處理 |
2、執行緒處理任務過程:
- 當執行緒池小於corePoolSize,新提交任務將建立一個新執行緒執行任務,即使此時執行緒池中存在空閒執行緒。
- 當執行緒池達到corePoolSize時,新提交任務將被放入 workQueue 中,等待執行緒池中任務排程執行。
- 當workQueue已滿,且 maximumPoolSize 大於 corePoolSize 時,新提交任務會建立新執行緒執行任務。
- 當提交任務數超過 maximumPoolSize 時,新提交任務由 RejectedExecutionHandler 處理。
- 當執行緒池中超過corePoolSize 執行緒,空閒時間達到 keepAliveTime 時,關閉空閒執行緒 。
3、執行緒拒絕策略
執行緒池中的執行緒已經用完了,無法繼續為新任務服務,同時,等待佇列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。
JDK 內建的拒絕策略如下:
AbortPolicy:直接丟擲異常,阻止系統正常執行。可以根據業務邏輯選擇重試或者放棄提交等策略。
CallerRunsPolicy :只要執行緒池未關閉,該策略直接在呼叫者執行緒中,運行當前被丟棄的任務。
不會造成任務丟失,同時減緩提交任務的速度,給執行任務緩衝時間。
DiscardOldestPolicy :丟棄最老的一個請求,也就是即將被執行的任務,並嘗試再次提交當前任務。
DiscardPolicy :該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。
4、Execuors類實現執行緒池
- newSingleThreadExecutor():只有一個執行緒的執行緒池,任務是順序執行,適用於一個一個任務執行的場景
- newCachedThreadPool():執行緒池裡有很多執行緒需要同時執行,60s內複用,適用執行很多短期非同步的小程式或者負載較輕的服務
- newFixedThreadPool():擁有固定執行緒數的執行緒池,如果沒有任務執行,那麼執行緒會一直等待,適用執行長期的任務。
- newScheduledThreadPool():用來排程即將執行的任務的執行緒池
- **newWorkStealingPool()**:底層採用forkjoin的Deque,採用獨立的任務佇列可以減少競爭同時加快任務處理
因為以上方式都存在弊端:
FixedThreadPool 和 SingleThreadExecutor : 允許請求的佇列⻓度為 Integer.MAX_VALUE,會導致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許建立的執行緒數量為 Integer.MAX_VALUE,會導致OOM。
手動建立的執行緒池底層使用的是ArrayBlockingQueue可以防止OOM。
5、執行緒池大小設定
- CPU 密集型(n+1)
CPU 密集的意思是該任務需要大量的運算,而沒有阻塞,CPU 一直全速執行。
CPU 密集型任務儘可能的少的執行緒數量,一般為 CPU 核數 + 1 個執行緒的執行緒池。
- IO 密集型(2*n)
由於 IO 密集型任務執行緒並不是一直在執行任務,可以多分配一點執行緒數,如 CPU * 2
也可以使用公式:CPU 核心數 *(1+平均等待時間/平均工作時間)。
執行緒安全
1、樂觀鎖,CAS思想
java樂觀鎖機制:
樂觀鎖體現的是悲觀鎖的反面。它是一種積極的思想,它總是認為資料是不會被修改的,所以是不會對資料上鎖的。但是樂觀鎖在更新的時候會去判斷資料是否被更新過。樂觀鎖的實現方案一般有兩種(版本號機制和CAS)。樂觀鎖適用於讀多寫少的場景,這樣可以提高系統的併發量。在Java中 java.util.concurrent.atomic下的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。
樂觀鎖,大多是基於資料版本 (Version)記錄機制實現。即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來 實現。 讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提 交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料 版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。
CAS思想:
CAS就是compare and swap(比較交換),是一種很出名的無鎖的演算法,就是可以不使用鎖機制實現執行緒間的同步。使用CAS執行緒是不會被阻塞的,所以又稱為非阻塞同步。CAS演算法涉及到三個操作:
需要讀寫記憶體值V;進行比較的值A;準備寫入的值B
當且僅當V的值等於A的值等於V的值的時候,才用B的值去更新V的值,否則不會執行任何操作(比較和替換是一個原子操作-A和V比較,V和B替換),一般情況下是一個自旋操作,即不斷重試
缺點:
ABA問題-知乎
高併發的情況下,很容易發生併發衝突,如果CAS一直失敗,那麼就會一直重試,浪費CPU資源
原子性:
功能限制CAS是能保證單個變數的操作是原子性的,在Java中要配合使用volatile關鍵字來保證執行緒的安全;當涉及到多個變數的時候CAS無能為力;除此之外CAS實現需要硬體層面的支援,在Java的普通使用者中無法直接使用,只能藉助atomic包下的原子類實現,靈活性受到了限制
2、synchronized底層實現
使用方法:主要的三種使⽤⽅式
修飾例項⽅法: 作⽤於當前物件例項加鎖,進⼊同步程式碼前要獲得當前物件例項的鎖
修飾靜態⽅法: 也就是給當前類加鎖,會作⽤於類的所有物件例項,因為靜態成員不屬於任何⼀個例項物件,是類成員。
修飾程式碼塊: 指定加鎖物件,對給定物件加鎖,進⼊同步程式碼庫前要獲得給定物件的鎖。
總結:synchronized鎖住的資源只有兩類:一個是物件,一個是類。
底層實現:
物件頭是我們需要關注的重點,它是synchronized實現鎖的基礎,因為synchronized申請鎖、上鎖、釋放鎖都與物件頭有關。物件頭主要結構是由Mark Word
組成,其中Mark Word
儲存物件的hashCode、鎖資訊或分代年齡或GC標誌等資訊。
鎖也分不同狀態,JDK6之前只有兩個狀態:無鎖、有鎖(重量級鎖),而在JDK6之後對synchronized進行了優化,新增了兩種狀態,總共就是四個狀態:無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,其中無鎖就是一種狀態了。鎖的型別和狀態在物件頭Mark Word
中都有記錄,在申請鎖、鎖升級等過程中JVM都需要讀取物件的Mark Word
資料。
同步程式碼塊是利用 monitorenter 和 monitorexit 指令實現的,而同步方法則是利用 flags 實現的。
3、ReenTrantLock底層實現
由於ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高階功能
使用方法:
基於API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成
底層實現:
ReenTrantLock的實現是一種自旋鎖,通過迴圈呼叫CAS操作來實現加鎖。它的效能比較好也是因為避免了使執行緒進入核心態的阻塞狀態。想盡辦法避免執行緒進入核心的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。
和synchronized區別:
1、底層實現:synchronized 是JVM層面的鎖,是Java關鍵字,通過monitor物件來完成(monitorenter與monitorexit),ReentrantLock 是從jdk1.5以來(java.util.concurrent.locks.Lock)提供的API層面的鎖。
2、實現原理****:synchronized 的實現涉及到鎖的升級,具體為無鎖、偏向鎖、自旋鎖、向OS申請重量級鎖;ReentrantLock實現則是通過利用CAS**(CompareAndSwap)自旋機制保證執行緒操作的原子性和volatile保證資料可見性以實現鎖的功能。
3、是否可手動釋放:synchronized 不需要使用者去手動釋放鎖,synchronized 程式碼執行完後系統會自動讓執行緒釋放對鎖的佔用; ReentrantLock則需要使用者去手動釋放鎖,如果沒有手動釋放鎖,就可能導致死鎖現象。
4、是否可中斷synchronized是不可中斷型別的鎖,除非加鎖的程式碼中出現異常或正常執行完成; ReentrantLock則可以中斷,可通過trylock(long timeout,TimeUnit unit)設定超時方法或者將lockInterruptibly()放到程式碼塊中,呼叫interrupt方法進行中斷。
5、是否公平鎖synchronized為非公平鎖 ReentrantLock則即可以選公平鎖也可以選非公平鎖,通過構造方法new ReentrantLock時傳入boolean值進行選擇,為空預設false非公平鎖,true為公平鎖,公平鎖效能非常低。
4、公平鎖和非公平鎖區別
公平鎖:
公平鎖自然是遵循FIFO(先進先出)原則的,先到的執行緒會優先獲取資源,後到的會進行排隊等待
優點:所有的執行緒都能得到資源,不會餓死在佇列中。適合大任務
缺點:吞吐量會下降,佇列裡面除了第一個執行緒,其他的執行緒都會阻塞,cpu喚醒阻塞執行緒的開銷大
非公平鎖:
多個執行緒去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待佇列,如果能獲取到,就直接獲取到鎖。
優點:可以減少CPU喚醒執行緒的開銷,整體的吞吐效率會高點,CPU也不必取喚醒所有執行緒,會減少喚起執行緒的數量。
缺點:你們可能也發現了,這樣可能導致佇列中間的執行緒一直獲取不到鎖或者長時間獲取不到鎖
公平鎖效率低原因:
公平鎖要維護一個佇列,後來的執行緒要加鎖,即使鎖空閒,也要先檢查有沒有其他執行緒在 wait,如果有自己要掛起,加到佇列後面,然後喚醒佇列最前面執行緒。這種情況下相比較非公平鎖多了一次掛起和喚醒。
執行緒切換的開銷,其實就是非公平鎖效率高於公平鎖的原因,因為非公平鎖減少了執行緒掛起的機率,後來的執行緒有一定機率逃離被掛起的開銷。
5、使用層面鎖優化
【1】減少鎖的時間:
不需要同步執行的程式碼,能不放在同步快裡面執行就不要放在同步快內,可以讓鎖儘快釋放;
【2】減少鎖的粒度:
它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增加並行度,從而降低鎖競爭。它的思想也是用空間來換時間;java中很多資料結構都是採用這種方法提高併發操作的效率,比如:
ConcurrentHashMap:
java中的ConcurrentHashMap在jdk1.8之前的版本,使用一個Segment 陣列:Segment< K,V >[] segments
Segment繼承自ReenTrantLock,所以每個Segment是個可重入鎖,每個Segment 有一個HashEntry< K,V >陣列用來存放資料,put操作時,先確定往哪個Segment放資料,只需要鎖定這個Segment,執行put,其它的Segment不會被鎖定;所以陣列中有多少個Segment就允許同一時刻多少個執行緒存放資料,這樣增加了併發能力。
【3】鎖粗化:
大部分情況下我們是要讓鎖的粒度最小化,鎖的粗化則是要增大鎖的粒度;
假如有一個迴圈,迴圈內的操作需要加鎖,我們應該把鎖放到迴圈外面,否則每次進出迴圈,都進出一次臨界區,效率是非常差的;
【4】使用讀寫鎖:
ReentrantReadWriteLock 是一個讀寫鎖,讀操作加讀鎖,可併發讀,寫操作使用寫鎖,只能單執行緒寫;
【5】使用CAS:
如果需要同步的操作執行速度非常快,並且執行緒競爭並不激烈,這時候使用cas效率會更高,因為加鎖會導致執行緒的上下文切換,如果上下文切換的耗時比同步操作本身更耗時,且執行緒對資源的競爭不激烈,使用volatiled+cas操作會是非常高效的選擇;
6、系統層面鎖優化
自適應自旋鎖:
自旋鎖可以避免等待競爭鎖進入阻塞掛起狀態被喚醒造成的核心態和使用者態之間的切換的損耗,它們只需要等一等(自旋),但是如果鎖被其他執行緒長時間佔用,一直不釋放CPU,死等會帶來更多的效能開銷;自旋次數預設值是10
對上面自旋鎖優化方式的進一步優化,它的自旋的次數不再固定,其自旋的次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,這就解決了自旋鎖帶來的缺點
鎖消除:
鎖削除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行削除。Netty中無鎖化設計pipeline中channelhandler會進行鎖消除的優化。
鎖升級:
偏向鎖:
如果執行緒已經佔有這個鎖,當他在次試圖去獲取這個鎖的時候,他會已最快的方式去拿到這個鎖,而不需要在進行一些monitor操作,因為在大部分情況下是沒有競爭的,所以使用偏向鎖是可以提高效能的;
輕量級鎖:
在競爭不激烈的情況下,通過CAS避免執行緒上下文切換,可以顯著的提高效能。
重量級鎖:
重量級鎖的加鎖、解鎖過程造成的損耗是固定的,重量級鎖適合於競爭激烈、高併發、同步塊執行時間長的情況。
7、ThreadLocal原理
ThreadLocal簡介:
通常情況下,我們建立的變數是可以被任何⼀個執行緒訪問並修改的。如果想實現每⼀個執行緒都有⾃⼰的
專屬本地變數該如何解決呢? JDK中提供的 ThreadLocal 類正是為了解決這樣的問題。類似作業系統中的TLAB
原理:
首先 ThreadLocal 是一個泛型類,保證可以接受任何型別的物件。因為一個執行緒內可以存在多個 ThreadLocal 物件,所以其實是 ThreadLocal 內部維護了一個 Map ,是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類。
最終的變數是放在了當前執行緒的 ThreadLocalMap
中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變數值。
我們使用的 get()、set() 方法其實都是呼叫了這個ThreadLocalMap類對應的 get()、set() 方法。例如下面的
如何使用:
1)儲存使用者Session
private static final ThreadLocal threadSession = new ThreadLocal();
2)解決執行緒安全的問題
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()
ThreadLocal記憶體洩漏的場景
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,⽽ value 是強引⽤。弱引用的特點是,如果這個物件持有弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。 假如我們不做任何措施的話,value 永遠⽆法被GC 回收,如果執行緒長時間不被銷燬,可能會產⽣記憶體洩露。
ThreadLocalMap實現中已經考慮了這種情況,在呼叫 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現記憶體洩漏,那只有在出現了 key 為 null 的記錄後,沒有手動呼叫 remove() 方法,並且之後也不再呼叫 get()、set()、remove() 方法的情況下。因此使⽤完ThreadLocal ⽅法後,最好⼿動調⽤ remove() ⽅法。
8、HashMap執行緒安全
死迴圈造成 CPU 100%
HashMap 有可能會發生死迴圈並且造成 CPU 100% ,這種情況發生最主要的原因就是在擴容的時候,也就是內部新建新的 HashMap 的時候,擴容的邏輯會反轉雜湊桶中的節點順序,當有多個執行緒同時進行擴容的時候,由於 HashMap 並非執行緒安全的,所以如果兩個執行緒同時反轉的話,便可能形成一個迴圈,並且這種迴圈是連結串列的迴圈,相當於 A 節點指向 B 節點,B 節點又指回到 A 節點,這樣一來,在下一次想要獲取該 key 所對應的 value 的時候,便會在遍歷連結串列的時候發生永遠無法遍歷結束的情況,也就發生 CPU 100% 的情況。
所以綜上所述,HashMap 是執行緒不安全的,在多執行緒使用場景中推薦使用執行緒安全同時效能比較好的 ConcurrentHashMap。
9、String不可變原因
-
可以使用字串常量池,多次建立同樣的字串會指向同一個記憶體地址
-
可以很方便地用作 HashMap 的 key。通常建議把不可變物件作為 HashMap的 key
-
hashCode生成後就不會改變,使用時無需重新計算
-
執行緒安全,因為具備不變性的物件一定是執行緒安全的
記憶體模型
Java 記憶體模型(Java Memory Model,JMM)就是一種符合記憶體模型規範的,遮蔽了各種硬體和作業系統的訪問差異的,保證了 Java 程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。
JMM 是一種規範,是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。目的是保證併發程式設計場景中的原子性、可見性和有序性。
原子性:
在 Java 中,為了保證原子性,提供了兩個高階的位元組碼指令 Monitorenter 和 Monitorexit。這兩個位元組碼,在 Java 中對應的關鍵字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 來保證方法和程式碼塊內的操作是原子性的。
可見性:
Java 中的 Volatile 關鍵字修飾的變數在被修改後可以立即同步到主記憶體。被其修飾的變數在每次使用之前都從主記憶體重新整理。因此,可以使用 Volatile 來保證多執行緒操作時變數的可見性。除了 Volatile,Java 中的 Synchronized 和 Final 兩個關鍵字也可以實現可見性。只不過實現方式不同
有序性
在 Java 中,可以使用 Synchronized 和 Volatile 來保證多執行緒之間操作的有序性。區別:Volatile 禁止指令重排。Synchronized 保證同一時刻只允許一條執行緒操作。
1、volatile底層實現
作用:
保證資料的“可見性”:被volatile修飾的變數能夠保證每個執行緒能夠獲取該變數的最新值,從而避免出現數據髒讀的現象。
禁止指令重排:在多執行緒操作情況下,指令重排會導致計算結果不一致
底層實現:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”
lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;
2)它會強制將對快取的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的快取行無效。
單例模式中volatile的作用:
防止程式碼讀取到instance不為null時,instance引用的物件有可能還沒有完成初始化。
class Singleton{ private volatile static Singleton instance = null; //禁止指令重排 private Singleton() { } public static Singleton getInstance() { if(instance==null) { //減少加鎖的損耗 synchronized (Singleton.class) { if(instance==null) //確認是否初始化完成 instance = new Singleton(); } } return instance; }}
2、AQS思想
AQS的全稱為(AbstractQueuedSynchronizer)抽象的佇列式的同步器,是⼀個⽤來構建鎖和同步器的框架,使⽤AQS能簡單且⾼效地構造出應⽤⼴泛的⼤量的同步器,如:基於AQS實現的lock, CountDownLatch、CyclicBarrier、Semaphore需解決的問題:
狀態的原子性管理執行緒的阻塞與解除阻塞佇列的管理
AQS核⼼思想是,如果被請求的共享資源空閒,則將當前請求資源的執行緒設定為有效的⼯作執行緒,並且將共享資源設定為鎖定狀態。如果被請求的共享資源被佔⽤,那麼就需要⼀套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是⽤CLH(虛擬的雙向佇列)佇列鎖實現的,即將暫時獲取不到鎖的執行緒加⼊到佇列中。
lock:
是一種可重入鎖,除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多執行緒死鎖的方法。預設為非公平鎖,但可以初始化為公平鎖; 通過方法 lock()與 unlock()來進行加鎖與解鎖操作;
CountDownLatch:
通過計數法(倒計時器),讓一些執行緒堵塞直到另一個執行緒完成一系列操作後才被喚醒;該⼯具通常⽤來控制執行緒等待,它可以讓某⼀個執行緒等待直到倒計時結束,再開始執⾏。具體可以使用countDownLatch.await()來等待結果。多用於多執行緒資訊彙總。
CompletableFuture:
通過設定引數,可以完成CountDownLatch同樣的多平臺響應問題,但是可以針對其中部分返回結果做更加靈活的展示。
CyclicBarrier:
字面意思是可迴圈(Cyclic)使用的屏障(Barrier)。他要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活,執行緒進入屏障通過CyclicBarrier的await()方法。可以用於批量傳送訊息佇列資訊、非同步限流。
Semaphore:
訊號量主要用於兩個目的,一個是用於多個共享資源的互斥作用,另一個用於併發執行緒數的控制。SpringHystrix限流的思想
3、happens-before
用來描述和可見性相關問題:如果第一個操作 happens-before 第二個操作,那麼我們就說第一個操作對於第二個操作是可見的
常見的happens-before:volatile 、鎖、執行緒生命週期。