你們好,我是Java最全面試題庫
的提褲姐
,今天這篇是JavaSE系列的第十三篇,主要總結了Java中的多線程問題,多線程分爲三篇來說,這篇是第三篇,在後續,會沿着第一篇開篇的知識線路一直總結下去,作到日更!若是我能作到百日百更,但願你也能夠跟着百日百刷,一百天養成一個好習慣。java
對於可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。主要的原理是使用了內存指令。面試
LoadLoad重排序
:一個處理器先執行一個L1讀操做,再執行一個L2讀操做;可是另一個處理器看到的是先L2再L1StoreStore重排序
:一個處理器先執行一個W1寫操做,再執行一個W2寫操做;可是另一個處理器看到的是先W2再W1LoadStore重排序
:一個處理器先執行一個L1讀操做,再執行一個W2寫操做;可是另一個處理器看到的是先W2再L1StoreLoad重排序
:一個處理器先執行一個W1寫操做,再執行一個L2讀操做;可是另一個處理器看到的是先L2再W1在volatile變量寫操做的前面會加入一個Release屏障,而後在以後會加入一個Store屏障,這樣就能夠保證volatile寫跟Release屏障之 前的任何讀寫操做都不會指令重排,而後Store屏障保證了,寫完數據以後,立馬會執行flush處理器緩存的操做 。
在volatile變量讀操做的前面會加入一個Load
屏障,這樣就能夠保證對這個變量的讀取時,若是被別的處理器修改過了,必須得從其餘 處理器的高速緩存(或者主內存)中加載到本身本地高速緩存裏,保證讀到的是最新數據; 在以後會加入一個Acquire
屏障,禁止volatile讀操做以後的任何讀寫操做會跟volatile讀指令重排序。
與volatie讀寫內存屏障對比一下,是相似的意思。Acquire屏障
其實就是LoadLoad屏障 + LoadStore屏障
,Release屏障
其實就是StoreLoad屏障 + StoreStore屏障
算法
CAS(compare and swap)的縮寫。Java利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法,實現原子操做。其它原子操做都是利用相似的特性完成的。
CAS有3個操做數:內存值V
,舊的預期值A
,要修改的新值B
。
當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
CAS的缺點:緩存
在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。多線程
CAS機制所保證的知識一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。併發
這是CAS機制最大的問題所在。框架
AQS,即AbstractQueuedSynchronizer,隊列同步器,它是Java併發用來構建鎖和其餘同步組件的基礎框架。
同步組件對AQS的使用:
AQS是一個抽象類,主是是以繼承的方式使用。
AQS自己是沒有實現任何同步接口的,它僅僅只是定義了同步狀態的獲取和釋放的方法來供自定義的同步組件的使用。從圖中能夠看出,在java的同步組件中,AQS的子類(Sync等)通常是同步組件的靜態內部類,即經過組合的方式使用。
抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如經常使用的ReentrantLock/Semaphore/CountDownLatch
它維護了一個volatile int state(表明共享資源)和一個FIFO(雙向隊列)線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)ide
Semaphore就是一個信號量,它的做用是限制某段代碼塊的併發數。
semaphore有一個構造函數,能夠傳入一個int型整數n,表示某段代碼最多隻有n個線程能夠訪問,若是超出了n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。
由此能夠看出若是Semaphore構造函數中傳入的int型整數n=1,至關於變成了一個synchronized了。函數
public static void main(String[] args) { int N = 8; //工人數 Semaphore semaphore = new Semaphore(5); //機器數目 for(int i=0;i<N;i++) new Worker(i,semaphore).start(); } static class Worker extends Thread{ private int num; private Semaphore semaphore; public Worker(int num,Semaphore semaphore){ this.num = num; this.semaphore = semaphore; } @Override public void run() { try { semaphore.acquire(); System.out.println("工人"+this.num+"佔用一個機器在生產..."); Thread.sleep(2000); System.out.println("工人"+this.num+"釋放出機器"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }
Synchronized是由JVM實現的一種實現互斥同步的方式,查看被Synchronized修飾過的程序塊編譯後的字節碼,會發現,被Synchronized修飾過的程序塊,在編譯先後被編譯器生成了monitorenter和monitorexit兩個字節碼指令。
在虛擬機執行到monitorenter指令時,首先要嘗試獲取對象的鎖:若是這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖,把鎖的計數器+1;
當執行monitorexit指令時,將鎖計數器-1;當計數器爲0時,鎖就被釋放了。若是獲取對象失敗了,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。
Java中Synchronize經過在對象頭設置標誌,達到了獲取鎖和釋放鎖的目的。工具
非公平主要表如今獲取鎖的行爲上,並不是是按照申請鎖的時間先後給等待線程分配鎖的,每當鎖被釋放後,任何一個線程都有機會競爭到鎖,這樣作的目的是爲了提升執行性能,缺點是可能會產生線程飢餓現象。
在Java6以前, Monitor的實現徹底依賴底層操做系統的互斥鎖來實現.
因爲Java層面的線程與操做系統的原生線程有映射關係,若是要將一個線程進行阻塞或喚起都須要操做系統的協助,這就須要從用戶態切換到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代JDK中作了大量的優化。
一種優化是使用自旋鎖
,即在把線程進行阻塞操做以前先讓線程自旋等待一段時間,可能在等待期間其餘線程已經解鎖,這時就無需再讓線程執行阻塞操做,避免了用戶態到內核態的切換。
現代JDK中還提供了三種不一樣的 Monitor實現,也就是三種不一樣的鎖:
這三種鎖使得JDK得以優化 Synchronized的運行,當JVM檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這就是鎖的升級、降級。當沒有競爭出現時,默認會使用偏向鎖。
JVM會利用CAS操做,在對象頭上的 Mark Word部分設置線程ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖,由於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏向鎖能夠下降無競爭開銷。
若是有另外一線程試圖鎖定某個被偏向過的對象,JVM就撤銷偏向鎖,切換到輕量級鎖實現。
輕量級鎖依賴CAS操做 Mark Word來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖不然,進一步升級爲重量級鎖。
synchronized:
是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由JVM實現,用戶不須要顯示的釋放鎖,很是方便。然而synchronized也有一些問題:
當線程嘗試獲取鎖的時候,若是獲取不到鎖會一直阻塞。
若是獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,不然其餘線程嘗試獲取鎖必須一直等待。
ReentrantLock:
ReentrantLock是Lock的實現類,是一個互斥的同步鎖。ReentrantLock是JDK 1.5以後提供的API層面的互斥鎖,須要lock()和unlock()方法配合try/finally語句塊來完成。
等待可中斷避免,出現死鎖的狀況(若是別的線程正持有鎖,會等待參數給定的時間,在等待的過程當中,若是獲取了鎖定,就返回true,若是等待超時,返回false)
公平鎖與非公平鎖多個線程等待同一個鎖時,必須按照申請鎖的時間順序得到鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是建立的非公平鎖,能夠經過參數true設爲公平鎖,但公平鎖表現的性能不是很好。
從功能角度:
ReentrantLock比 Synchronized的同步操做更精細(由於能夠像普通對象同樣使用),甚至實現 Synchronized沒有的高級功能,如:
從鎖釋放角度:
Synchronized在JVM層面上實現的,不但能夠經過一些監控工具監控 Synchronized的鎖定,並且在代碼執行出現異常時,JVM會自動釋放鎖定,可是使用Lock則不行,Lock是經過代碼實現的,要保證鎖定必定會被釋放,就必須將 unLock()放到 finally{}中。
從性能角度: Synchronized早期實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大。可是在Java6中對其進行了很是多的改進,在競爭不激烈時:Synchronized的性能要優於 ReetrantLock;在高競爭狀況下:Synchronized的性能會降低幾十倍,可是 ReetrantLock的性能能維持常態。