是否真正瞭解synchronized關鍵字java
synchronized關鍵字的使用、原理、優化等面試
一、在Java中,synchronized關鍵字是一個輕量級的同步機制,也是咱們在工做中用得最頻繁的,咱們可使用synchronized修飾一個方法,也能夠用來修飾一個代碼塊。 那麼,你真的瞭解synchronized嗎?是騾子是馬,咱拿出來溜溜。markdown
二、關於synchronized的使用,我相信只要正常作過Android或Java開發的工做,對此必定不會陌生。 那麼請問,在static方法和非static方法前面加synchronized到底有什麼不一樣呢?這種問題,光文字解釋一點說服力都沒有,直接擼個代碼驗證一下。多線程
• static鎖併發
public class SynchronizedTest {
private static int number = 0;
public static void main(String[] args) {
//建立5個線程,製造多線程場景
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
//sleep一個隨機時間
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//調用靜態方法
SynchronizedTest.testStaticSynchronized();
}
}.start();
}
}
/**
* 靜態方法加鎖
*/
public synchronized static void testStaticSynchronized() {
//對number加1操做
number++;
//打印(線程名 + number)
System.out.println(Thread.currentThread().getName() + " -> 當前number爲" + number);
}
}
// logcat日誌
// Thread-4 -> 當前number爲1
// Thread-3 -> 當前number爲2
// Thread-1 -> 當前number爲3
// Thread-0 -> 當前number爲4
// Thread-2 -> 當前number爲5
複製代碼
代碼邏輯很簡單,建立5個線程去經過靜態方法操做number變量,由於是靜態方法,因此直接使用類名調用便可。根據logcat日誌的輸出,作到了同步,沒有併發異常。dom
這裏整理了最近BAT最新面試題,2021船新版本!!須要的朋友能夠點擊:這個,點這個!!,備註:jj。但願那些有須要朋友能在今年第一波招聘潮找到一個本身滿意順心的工做!
ide
• 非static鎖oop
public class SynchronizedTest {
private static int number = 0;
public static void main(String[] args) {
//建立5個線程,製造多線程場景
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
//sleep一個隨機時間
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SynchronizedTest test = new SynchronizedTest();
//經過對象,調用非靜態方法
test.testNonStaticSynchronized();
}
}.start();
}
}
/**
* 非靜態方法加鎖
*/
public synchronized void testNonStaticSynchronized() {
number++;
System.out.println(Thread.currentThread().getName() + " -> " + number);
}
}
// logcat日誌
// Thread-0 -> 當前number爲1
// Thread-4 -> 當前number爲1
// Thread-2 -> 當前number爲3
// Thread-1 -> 當前number爲4
// Thread-3 -> 當前number爲4
複製代碼
這裏的代碼與上面的代碼邏輯如出一轍,惟一改變的就是由於方法沒有使用static修飾,因此使用建立對象並調用方法來操做number變量。看logcat日誌,出現了數據異常,很明顯不加static修飾,是沒辦法保證線程同步的。性能
• 另外咱們再作一個測試,賣個關子,先來看代碼。學習
public class SynchronizedTest {
public static void main(String[] args) {
//建立5個線程調用非靜態方法
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//經過對象調用非靜態方法
SynchronizedTest test = new SynchronizedTest();
test.testNonStaticSynchronized();
}
}.start();
}
//建立5個線程調用靜態方法
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//經過類名調用靜態方法
SynchronizedTest.testStaticSynchronized();
}
}.start();
}
}
/**
* 靜態方法加鎖
*/
public synchronized static void testStaticSynchronized() {
System.out.println("testStaticSynchronized -> running -> " + System.currentTimeMillis());
}
/**
* 非靜態方法加鎖
*/
public synchronized void testNonStaticSynchronized() {
System.out.println("testNonStaticSynchronized -> running -> " + System.currentTimeMillis());
}
}
// logcat日誌
// testNonStaticSynchronized -> running -> 1603433921735
// testNonStaticSynchronized -> running -> 1603433921735
// testStaticSynchronized -> running -> 1603433921735
// testNonStaticSynchronized -> running -> 1603433922740 ----- 注意這裏
// testStaticSynchronized -> running -> 1603433922740 ----- 注意這裏
// testStaticSynchronized -> running -> 1603433922740
// testNonStaticSynchronized -> running -> 1603433923735
// testNonStaticSynchronized -> running -> 1603433924740
// testStaticSynchronized -> running -> 1603433925736
// testStaticSynchronized -> running -> 1603433925736
複製代碼
代碼邏輯是這樣的,各建立5個線程分別執行靜態方法與非靜態方法,看輸出日誌,特別關注一下最後的時間戳。從日誌第咱們能夠發現,從日誌第4行和第5行發現,testNonStaticSynchronized與testStaticSynchronized方法能夠同時執行,那就說明static鎖與非statics鎖互不干預。
通過上面3個demo的分析,基本能夠得出結論了,這裏總結一下。
• 類鎖: 當synchronized修飾一個static方法時,獲取到的是類鎖,做用於這個類的全部對象。
• 對象鎖: 當synchronized修飾一個非static方法時,獲取到的是對象鎖,做用於調用該方法的當前對象。
• 類鎖和對象鎖不一樣,他們之間不會產生互斥。
三、固然,關於如何使用的問題,上面就一筆帶過了,面試官通常也不會問不少,畢竟體現不出面試官的逼格。 正所謂,知其然也要知其因此然,咱們須要要探討的重點是synchronized關鍵字底層是怎麼幫咱們實現同步的?沒錯,這也是面試過程當中問得最多的。synchronized的原理這塊,咱們也分兩種狀況去思考。
• 第一,synchronized修飾代碼塊。
public class SynchronizedTest {
public static void main(String[] args) {
//經過synchronized修飾代碼塊
synchronized (SynchronizedTest.class) {
System.out.println("this is in synchronized");
}
}
}
複製代碼
上面是一段演示代碼,沒有實際功能,爲了可以看得簡單明瞭,就是經過synchronized對一條輸出語句進行加鎖。由於synchronized僅僅是Java提供的關鍵字,那麼要想知道底層原理,咱們須要經過javap命令反編譯class文件,看看他的字節碼到底長啥樣。
看反編譯的結果,着重看紅色標註的地方。咱們能夠清楚地發現,代碼塊同步是使用monitorenter和monitorexit兩個指令完成的,monitorenter指令插入到同步代碼塊的開始位置,monitorexit插入到方法結束處和異常處,被同步的代碼塊由monitorenter指令進入,而後在monitorexit指令處結束。
這裏的重要角色monitor究竟是什麼呢?簡單來講,能夠直接理解爲鎖對象,只不過是虛擬機實現的,底層是依賴於操做系統的Mutex Lock實現。任何Java對象都有一個monitor與之關聯,或者說全部的Java對象天生就能夠成爲monitor,這也就能夠解釋咱們平時在使用synchronized關鍵字時能夠將任意對象做爲鎖的緣由了。
monitorenter
在執行monitorenter時,當前線程會嘗試獲取鎖,若是這個monitor沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,那麼就把鎖的計數器加1,獲取鎖成功,繼續執行下面的代碼。若是獲取鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。
monitorexit
與此對應的,當執行monitorexit指令時,鎖的計數器也會減1,當計數器等於0時,當前線程就釋放鎖,再也不是這個monitor的全部者。這個時候,其餘被這個monitor阻塞的線程即可以嘗試去獲取這個monitor的全部權了。
到這裏,synchronized修飾代碼塊實現同步的原理,我相信你已經搞懂了吧,那趁熱打鐵,繼續看看修飾方法又是怎麼處理的。
• 第二,synchronized修飾方法。
public class SynchronizedTest {
public static void main(String[] args) {
doSynchronizedTest();
}
//經過synchronized修飾方法
public static synchronized void doSynchronizedTest(){
System.out.println("this is in synchronized");
}
}
複製代碼
按照上面的老規矩,直接javap進行反編譯,看字節碼的變化。
從反編譯的結果來看,此次方法的同步並無直接經過指令monitorenter和monitorexit來實現,可是相對於其餘普通的方法,它的方法描述多了一個ACC_SYNCHRONIZED標識符。想必你都能猜出來,虛擬機無非就是根據這個標識符來實現方法同步,其實現原理大體是這樣的:虛擬機調用某個方法時,調用指令首先會檢查該方法的ACC_SYNCHRONIZED訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象,這樣也保證了同步。固然這裏monitor其實會有類鎖和對象鎖兩種狀況,上面就有說到。
關於synchronized的原理,這邊再簡單總結一下。synchronized關鍵字的實現同步分兩種場景,代碼塊同步是使用monitorenter和monitorexit指令的形式,而方法同步使用的ACC_SYNCHRONIZED標識符的形式。但萬變不離其宗,這兩種形式的根本都是基於JVM進入和退出monitor對象鎖來實現操做同步。
四、扛到了這裏,是該小小的開心一下啦,不過並無徹底結束呢! 從上面的原理分析知道,synchronized關鍵字是基於JVM進入和退出monitor對象鎖來實現操做同步,這種搶佔式獲取monitor鎖,性能上鐵定堪憂呀。這時候煩人的面試官又上線了,請問JDK1.6之後對synchronized鎖作了哪些優化?這個問題難度較大,咱們細細地說。
其實在JDK1.6以前,synchronized內部實現的鎖都是重量級鎖,也就是說沒有搶到CPU使用權的線程都得堵塞,然而在程序真正運行過程當中,其實不少狀況下並不須要這麼重,每次都直接堵塞反而會致使更多的線程上下文切換,消耗更多的資源。因此在JDK1.6之後,對synchronized鎖進行了優化,引入偏向鎖,輕量級鎖,重量級鎖的概念。
• 鎖信息
熟悉synchronized原理的同窗應該都知道,當一個線程訪問synchronized包裹的同步代碼塊時,必須獲取monitor鎖才能進入代碼塊,退出或拋出異常時再去釋放monitor鎖。這裏就有問題了,線程須要獲取的synchronized鎖信息是存在哪裏的呢?因此在介紹各類鎖的概念以前,咱們必須先嚐試解答這個疑惑。
在學習JVM時,咱們瞭解過一個對象是由三部分組成的,分別是對象頭、實例數據以及對齊填充。其中對象頭裏又存儲了對象自己運行時數據,包括哈希碼、GC分代年齡,固然還有咱們這裏要講的與鎖相關的標識,好比鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等。
對象頭默認存儲的是無鎖狀態,隨着程序的運行,對象頭裏存儲的數據會隨着鎖標誌位的變化而變化,大體結構以下圖所示。
如今即可以解開上面的疑惑了,原來線程是經過獲取對象頭裏的相關鎖標識來獲取鎖信息的。有了這個基礎,咱們如今能夠上正菜了,看synchronized鎖是怎麼一步一步升級優化的。
• 偏向鎖
鎖是用於併發場景的,然而,在大多數狀況下,鎖其實並不存在多線程競爭,甚至都是由同一個線程屢次獲取,因此沒有必要花太多代價去放在鎖的獲取上,這時偏向鎖就應運而生了。
偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程。當一個線程第一次訪問同步代碼塊並嘗試獲取鎖時,會直接給線程加一個偏向鎖,並在對象頭的鎖記錄裏存儲鎖偏向的線程ID,這樣的話,之後該線程再次進入和退出同步代碼塊時就不須要進行CAS等操做來加鎖和解鎖,只需查看對象頭裏是否存儲着指向當前線程的偏向鎖便可。很明顯,偏向鎖的作法無疑是消除了同一線程競爭鎖的開銷,大大提升了程序的運行性能。
固然,若是在運行過程當中,忽然有其餘線程搶佔該鎖,若是經過CAS操做獲取鎖成功,直接替換對象頭中的線程ID爲新的線程ID,繼續會保持偏向鎖狀態;反之若是沒有搶成功時,那麼持有偏向鎖的線程會被掛起,形成STW現象,JVM會自動消除它身上的偏向鎖,偏向鎖升級爲輕量級鎖。
• 輕量級鎖
輕量級鎖位於偏向鎖與重量級鎖之間,其主要目的是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。自旋鎖就是輕量級鎖的一種典型實現。
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作進入阻塞掛起狀態,它們只須要稍微等一等,其實就是進行自旋操做,等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就進一步避免切換線程引發的消耗。
固然,自旋鎖也不是最終解決方案,好比遇到鎖的競爭很是激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用cpu進行自旋檢查,這對於業務來說就是無用功。若是線程自旋帶來的消耗大於線程阻塞掛起操做的消耗,那麼自旋鎖就弊大於利了,因此這個自旋的次數是個很重要的閾值,JDK1.5默認爲10次,在1.6引入了適應性自旋鎖,適應性自旋鎖意味着自旋的時間再也不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間。
• 重量級鎖
自旋屢次仍是失敗後,通常就直接升級成重量級鎖了,也就是鎖的最高級別了,在上一篇synchronized的原理裏有講到其底層基於monitor對象實現,而monitor的本質又是依賴於操做系統的Mutex Lock實現。這裏其實又涉及到咱們以前有篇文章講過的一個知識,頻繁切換線程的危害?由於操做系統實現線程之間的切換須要進行用戶態到內核態的切換,不用想就知道,切換成本固然就很高了。
當JVM檢查到重量級鎖以後,會把想要得到鎖的線程進行阻塞,插入到一個阻塞隊列,被阻塞的線程不會消耗CPU,可是阻塞或者喚醒一個線程,都須要上面所說的從用戶態轉換到內核態,這個成本比較高,有可能比真正須要執行的同步代碼的消耗還要大。
咱們依次理了一遍,由無鎖到偏向鎖,再到輕量級鎖,最後升級爲重量級鎖,具體升級流程參考下面這張總體圖。這一切的出發點都是爲了優化性能,其實也給咱們一線開發者一個啓示,並非功能實現了,編碼也就結束了,後面還有很長的優化之路等待着你我,加油!
以爲不錯小夥伴記得三連支持哦,後續會持續更新精選技術文章!