在多線程編程中,synchronized關鍵字很是常見,當咱們須要進行「同步」操做時,咱們不少時候須要該該關鍵字對代碼塊或者方法進行鎖定。被synchronized鎖定的代碼塊,只能同時有一條線程訪問該代碼塊。編程
上面是不少人的認識,固然也是我以前對synchronized關鍵字的淺顯認識,其實上面的觀點存在必定的誤差。在參考了不少文章以及本身動手測試過相關代碼後,我以爲有必要記錄下本身對synchronized關鍵字的一些理解,在這個過程,會簡單說說synchronized關鍵字的具體實現原理。api
1、synchronized:synchronized是鎖代碼塊仍是鎖對象?多線程
synchronized具備同步的功能,更準確說是具備互斥的鎖功能,那麼,它究竟是鎖定了相關的代碼塊仍是說鎖定了對象數據?答案是鎖對象。下面就從synchronized修飾方法和修飾具體代碼塊兩方面理解這個結論:synchronized是鎖對象的。併發
一、synchronized修飾方法框架
廢話很少說,先簡單看測試代碼:異步
代碼一jvm
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test1 test1 = new Test1(); new Thread(new Runnable() { public void run() { try { test1.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { public void run() { test1.secondMethod(); } }).start(); } class Test1{ public synchronized void firstMethod() throws InterruptedException{ System.out.println("firstMethod"); Thread.sleep(2000); } public void secondMethod(){ System.out.println("secondMethod"); } }
輸出結果: firstMethod secondMethod 或者 secondMethod firstMethod 測試
而後,看代碼二優化
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test1 test1 = new Test1(); new Thread(new Runnable() { public void run() { try { test1.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { public void run() { test1.thirdMethod(); } }).start(); } } class Test1{ public synchronized void firstMethod() throws InterruptedException{ System.out.println("firstMethod"); Thread.sleep(2000); } public synchronized void thirdMethod(){ System.out.println("thirdMethod"); } }
結果一直是:firstMethod thirdMethodthis
因此,咱們能夠得出如下結論(在理解該結論前讀者能夠先假設每一個對象均有一個鎖對象,具體後面講解synchronized關鍵字原理時會講解):
synchronize修飾方法時(非靜態方法,靜態方法稍後討論),表示某個線程執行到該方法時會鎖定當前對象,其餘線程不能夠調用該對象中含有synchronized關鍵字的方法,由於這些線程的這些方法要執行前提是要得到該對象的鎖。
就上面的例子具體說:
在代碼一中,因爲secondMethod方法沒有synchronized關鍵字修飾,該方法執行無需獲取當前對象的鎖,因此secondMethod和firstMethod執行是並行的;
在代碼二中,firstMethod和thirdMethod方法均有synchronized關鍵字修飾,兩個方法執行前提是能夠獲取當前對象的鎖,因此二者是沒法同時進行的,由於同一個對象中只有一把鎖。
ok,理解上面的例子以後,估計讀者理解下面的代碼就簡單不少了,好,上代碼:
代碼三:修飾靜態方法時的情形(只有一個方法有static修飾)
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test2 test2 = new Test2(); new Thread(new Runnable() { public void run() { try { test2.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { public void run() { test2.thirdMethod(); } }).start(); } } class Test2{ public static synchronized void firstMethod() throws InterruptedException{ System.out.println("firstMethod"); Thread.sleep(2000); } public synchronized void thirdMethod(){ System.out.println("thirdMethod"); } }
執行結果是:firstMethod thirdMethod 或者thirdMethod firstMethod
代碼四:(都有static修飾)
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test2 test2 = new Test2(); new Thread(new Runnable() { public void run() { try { test2.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { public void run() { test2.thirdMethod(); } }).start(); } } class Test2{ public static synchronized void firstMethod() throws InterruptedException{ System.out.println("firstMethod"); Thread.sleep(2000); } public static synchronized void thirdMethod(){ System.out.println("thirdMethod"); } }
執行結果是:firstMethod thirdMethod
從上面結果咱們能夠知道:synchronized修飾靜態方法時,它會鎖定Class實例對象。
因此代碼三中因爲firstMethod和thirdMethod鎖定對象不一樣,因此他們能夠並行執行;代碼四中兩個方法都鎖定了class對象,因此沒法並行執行。
好,上面一大坨的文字,總結一句話起來就是這樣:
synchronized修飾靜態方法時,它表示鎖定class對象;修飾動態方法時,表示鎖定當前對象(this)。
二、synchronized修飾代碼塊
其實,上面的講解已經很明確指出了一個事實:synchronized關鍵字就是表示當前代碼鎖住了某個對象,其餘須要獲取該鎖(即被synchronized修飾)的代碼塊須要被執行,必須獲取該對象的鎖,即等到synchronized釋放鎖(其實就是改代碼塊執行完),它纔有可能被執行,只是當synchronized修飾代碼塊時,必須顯式地指出它鎖定了哪一個對象而已。上代碼:
代碼五:
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test3 test3 = new Test3(); new Thread(new Runnable() { public void run() { try { test3.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { public void run() { test3.thirdMethod(); } }).start(); } } class Test3{ public void firstMethod() throws InterruptedException{ synchronized (this) { System.out.println("firstMethod"); Thread.sleep(2000); } } public void thirdMethod(){ synchronized (this) { System.out.println("thirdMethod"); } } }
上面代碼和代碼二五執行效果任何區別,實現的功能也是同樣的。
固然,與直接修飾方法相比,synchronized修飾代碼塊時,this能夠換成其餘對象,咱們鎖定對象的代碼塊也能夠粒度更小。
好,上面一堆廢話其實歸根到底都是說明一個道理:synchronized修飾的代碼,它表示鎖住了某個對象,被該關鍵字修飾的其餘線程(強調其餘線程,稍後講爲何)的方法若是要執行,也必須獲得該鎖,即synchronized代碼塊執行完。
上面強調其餘線程的意思是,若是同一個線程中被synchronized的方法,則無需獲取該鎖,這是合理的,具體看下面例子估計就明白我在說什麼了:
public class SynchronizedTest1 { public static void main(String [] argStrings){ final Test4 test4 = new Test4(); new Thread(new Runnable() { public void run() { try { test4.firstMethod(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } class Test4{ public synchronized void firstMethod() throws InterruptedException{ System.out.println("firstMethod"); Thread.sleep(2000); secondMethod(); } public synchronized void secondMethod(){ System.out.println("secondMethod"); } }
結果是:firstMethod secondMethod
好吧,我也以爲我在講廢話,確實這是顯而易見的結果:被synchronized修飾的方法若是互相嵌套調用,同一條線程中並不須要等待上一個synchronized塊執行完,畢竟,這樣會陷入死循環。獲取鎖或者說鎖定對象,是針對不一樣線程而言的。
2、synchronized原理詳解
一、首先理解幾個鎖概念:對象鎖(也就重量鎖),輕量鎖,偏向鎖
A、對象鎖(重量鎖)
在多線程環境下,大多數(後面會將爲何是大多數修飾)每一個對象都會有一個monitor對象(關於monitor具體能夠查看jdk api文檔),這個對象其實就是上面咱們解釋synchronized關鍵字時對應的鎖。這個對象鎖負責管理全部訪問該對象的線程,具體管理模型圖能夠參考下面的示意圖:
對於訪問該對象的線程,併發狀況下,沒有搶到鎖(即對象訪問權)的線程,會被monitor丟進list這個隊列進行等候(固然,自旋鎖狀況例外,後面會具體講解什麼卵是自旋鎖)。而所謂的公平鎖與非公平鎖,就是在notify喚醒list中的等候線程時,list中的線程是按照排隊順序得到鎖仍是一塊兒搶該對象的鎖,前者是公平的,先等候的線程先得到鎖;後者則是不公平的,搶不搶到鎖,和線程等待時間無關,與運氣有關。
B、輕量鎖
顯然,重量鎖對於每一個對象都要維護一個monitor對象,開銷確定是挺大的,jvm對此進行了一些優化,這即是輕量鎖出現的緣由。
輕量鎖大概是這樣一種概念:儘管程序是多線程環境,可是若是訪問當前對象的,該對象並不會new一個monitor,而是在對象的頭部用一個標識字段(貌似是兩位的二進制數)表示鎖,這個就是輕量鎖。在線程嘗試訪問該對象時,該線程會將當前線程私有空間中的對應鎖標識字拷貝到對象頭部,若是修改爲功,表示只有一條線程訪問該對象該對象繼續採用輕量鎖;若是發現輕量鎖已經被其餘線程佔有鎖定,jvm會將輕量鎖升級爲重量鎖。
C、偏向鎖
偏向鎖是比輕量鎖更輕量的鎖:當jvm發現當前程序是但線程運行時,變會對對象採起偏向鎖,固然,一旦發現有synchronized多線程執行情形,jvm會把偏向鎖上升爲輕量鎖。
二、synchronized如何實現
瞭解了上面提到的幾個鎖的概念後,理解synchronized實現原理就很是容易了。
在多線程環境下,對象對應的monitor管理着須要訪問該對象的全部線程,monitor中會有個變量存儲synchronized獲取次數,在一個線程的中的某個synchronized代碼塊獲取monitor以後,該變量數加一,對於synchronized嵌套情形同樣如此:有多少個synchronized,對應的變量值就是多少,對於其餘線程若是想得到monitor,必須等到當前佔有monitor線程的全部synchronized塊均執行完,每執行完一個synchronized塊,標識變量值減一,變爲0時,其餘線程就能夠開始搶鎖了。
因此,全部的管理工做均由monitor完成,它是synchronized實現的核心。
三、其餘知識的補充
A、自旋鎖
上面提到,monitor在管理每一個阻塞線程的時候,會把它們放進一個隊列裏面,這是針對非自旋鎖,當monitor被設置爲自旋鎖鎖時,線程不會被掛起放進隊列,而是在作循環,直到獲取monitor的訪問權。因此自旋鎖有什麼存在必要呢?其實,線程的掛起和喚醒的調度過程是須要耗費cpu的,當線程持有鎖的時間很是短時,咱們不必將等待線程掛起,這樣會致使線程的頻繁掛起和喚醒,浪費了cpu資源。固然,自旋鎖中線程在作循環時確定也會消耗資源的,因此如何選擇仍是要衡量調度線程花費大仍是線程原地循環花費大。
另外,針對上面情形,還有一種鎖叫作自適應自旋鎖,它大概解決了上面提到的一些問題;
B、自適應自旋鎖
自適應自旋鎖其實就是在自旋鎖的基礎上加了個自旋數的計數:當循環了必定次數後,該線程尚未獲取鎖時,它就被丟進隊列裏掛起。
好的,synchronized的總結就到這裏了,明天會再總結一篇多線程實現異步框架的文章,到時候也歡迎各位訪問哈!
另外,本票文章不足之處也請各位大佬多多指教,歡迎拆臺!