java線程總結--synchronized關鍵字,原理以及相關的鎖
在多線程編程中,synchronized關鍵字非常常見,當我們需要進行“同步”操作時,我們很多時候需要該該關鍵字對代碼塊或者方法進行鎖定。被synchronized鎖定的代碼塊,只能同時有一條線程訪問該代碼塊。
上面是很多人的認識,當然也是我之前對synchronized關鍵字的淺顯認識,其實上面的觀點存在一定的偏差。在參考了很多文章以及自己動手測試過相關代碼後,我覺得有必要記錄下自己對synchronized關鍵字的一些理解,在這個過程,會簡單說說synchronized關鍵字的具體實現原理。
一、synchronized:synchronized是鎖代碼塊還是鎖對象?
synchronized具有同步的功能,更準確說是具有互斥的鎖功能,那麽,它到底是鎖定了相關的代碼塊還是說鎖定了對象數據?答案是鎖對象。下面就從synchronized修飾方法和修飾具體代碼塊兩方面理解這個結論:synchronized是鎖對象的。
1、synchronized修飾方法
廢話不多說,先簡單看測試代碼:
代碼一
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 thirdMethod
所以,我們可以得出以下結論(在理解該結論前讀者可以先假設每個對象均有一個鎖對象,具體後面講解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)。
2、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塊執行完,畢竟,這樣會陷入死循環。獲取鎖或者說鎖定對象,是針對不同線程而言的。
二、synchronized原理詳解
1、首先理解幾個鎖概念:對象鎖(也就重量鎖),輕量鎖,偏向鎖
A、對象鎖(重量鎖)
在多線程環境下,大多數(後面會將為什麽是大多數修飾)每個對象都會有一個monitor對象(關於monitor具體可以查看jdk api文檔),這個對象其實就是上面我們解釋synchronized關鍵字時對應的鎖。這個對象鎖負責管理所有訪問該對象的線程,具體管理模型圖可以參考下面的示意圖:
對於訪問該對象的線程,並發情況下,沒有搶到鎖(即對象訪問權)的線程,會被monitor丟進list這個隊列進行等候(當然,自旋鎖情況例外,後面會具體講解什麽卵是自旋鎖)。而所謂的公平鎖與非公平鎖,就是在notify喚醒list中的等候線程時,list中的線程是按照排隊順序獲得鎖還是一起搶該對象的鎖,前者是公平的,先等候的線程先獲得鎖;後者則是不公平的,搶不搶到鎖,和線程等待時間無關,與運氣有關。
B、輕量鎖
顯然,重量鎖對於每個對象都要維護一個monitor對象,開銷肯定是挺大的,jvm對此進行了一些優化,這便是輕量鎖出現的原因。
輕量鎖大概是這樣一種概念:盡管程序是多線程環境,但是如果訪問當前對象的,該對象並不會new一個monitor,而是在對象的頭部用一個標識字段(貌似是兩位的二進制數)表示鎖,這個就是輕量鎖。在線程嘗試訪問該對象時,該線程會將當前線程私有空間中的對應鎖標識字拷貝到對象頭部,如果修改成功,表示只有一條線程訪問該對象該對象繼續采用輕量鎖;如果發現輕量鎖已經被其他線程占有鎖定,jvm會將輕量鎖升級為重量鎖。
C、偏向鎖
偏向鎖是比輕量鎖更輕量的鎖:當jvm發現當前程序是但線程運行時,變會對對象采取偏向鎖,當然,一旦發現有synchronized多線程執行情形,jvm會把偏向鎖上升為輕量鎖。
2、synchronized如何實現
了解了上面提到的幾個鎖的概念後,理解synchronized實現原理就非常容易了。
在多線程環境下,對象對應的monitor管理著需要訪問該對象的所有線程,monitor中會有個變量存儲synchronized獲取次數,在一個線程的中的某個synchronized代碼塊獲取monitor之後,該變量數加一,對於synchronized嵌套情形一樣如此:有多少個synchronized,對應的變量值就是多少,對於其他線程如果想獲得monitor,必須等到當前占有monitor線程的所有synchronized塊均執行完,每執行完一個synchronized塊,標識變量值減一,變為0時,其他線程就可以開始搶鎖了。
所以,所有的管理工作均由monitor完成,它是synchronized實現的核心。
3、其他知識的補充
A、自旋鎖
上面提到,monitor在管理每個阻塞線程的時候,會把它們放進一個隊列裏面,這是針對非自旋鎖,當monitor被設置為自旋鎖鎖時,線程不會被掛起放進隊列,而是在做循環,直到獲取monitor的訪問權。所以自旋鎖有什麽存在必要呢?其實,線程的掛起和喚醒的調度過程是需要耗費cpu的,當線程持有鎖的時間非常短時,我們沒必要將等待線程掛起,這樣會導致線程的頻繁掛起和喚醒,浪費了cpu資源。當然,自旋鎖中線程在做循環時肯定也會消耗資源的,所以如何選擇還是要衡量調度線程花費大還是線程原地循環花費大。
另外,針對上面情形,還有一種鎖叫做自適應自旋鎖,它大概解決了上面提到的一些問題;
B、自適應自旋鎖
自適應自旋鎖其實就是在自旋鎖的基礎上加了個自旋數的計數:當循環了一定次數後,該線程還沒有獲取鎖時,它就被丟進隊列裏掛起。
java線程總結--synchronized關鍵字,原理以及相關的鎖