對於Java開發者來講synchronized關鍵字確定不陌生,對它的用法咱們可能已經能信手扭來了,可是咱們真的對它深刻了解嗎?html
雖然網上有不少文章都已經將synchronized關鍵字的用法和原理講明白了,可是我仍是想根據我我的的認識,來跟你們夥來聊一聊這個關鍵字。java
我不想上來就搞什麼實現原理,咱們來一塊兒看看synchronized的用法,再由淺到深的聊聊synchronized的實現原理,從而完全來完全掌握它。面試
咱們都知道synchronized關鍵字是Java語言級別提供的鎖,它能夠爲代碼提供有序性和可見性的保。spring
synchronized做爲一個互斥鎖,一次只能有一個線程在訪問,咱們也能夠把synchronized修飾的區域看做是一個臨界區,臨界區內只能有一個線程在訪問,當訪問線程退出臨界區,另外一個線程才能訪問臨界區資源。設計模式
一、怎麼用網絡
synchronized通常有兩種用法:synchronized 修飾方法和 synchronized 代碼塊。數據結構
咱們就經過下面的例子,一塊兒感覺一下synchronized的使用,感覺一下synchronized這個鎖到底鎖的是什麼。多線程
public class TestSynchronized { private final Object object = new Object(); //修飾靜態方法 public synchronized static void methodA() { System.out.println("methodA....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //代碼塊synchronized(object) public void methodB() { synchronized (this) { System.out.println("methodB....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } //代碼塊synchronized(class) public void methodC() { synchronized (TestSynchronized.class) { System.out.println("methodC....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } //修飾普通法法 public synchronized void methodD() { System.out.println("methodD....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //修飾普通的object public void methodE() { synchronized (object) { System.out.println("methodE....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
咱們上面的例子基本上包含了synchronized的全部使用方法,咱們經過運行這個例子,看一下方法的打印順序是怎麼樣的。併發
首先咱們調用同一個對象的 methodB和methodD 方法,來對比下 synchronized (this)和 synchronized method(){} 這兩種方式。oracle
final TestSynchronized obj = new TestSynchronized(); new Thread(() -> { obj.methodB(); }).start(); new Thread(() -> { //obj.methodB(); obj.methodD(); }).start();
不論是兩個線程調用同一個方法,仍是不一樣的方法,咱們經過運行代碼能夠看到,控制檯都是先打印methodB.....等了一秒鐘纔打印出另外一個線程調用的方法輸出結果。
爲何會先打印了methodB過一會纔打印methodD呢?先看下圖,咱們就以調用不一樣方法爲例。
咱們文章剛開始也介紹了synchronized的做用其實至關於一把鎖,其實咱們也能夠看作是一個臨界區,經過代碼的運行結果,咱們看到這裏先打印了methodB....,過了一會纔打印了methodD方法。
咱們能夠感受到這兩個線程訪問的好像是同一塊臨界區,否則的話,控制檯應該幾乎同時打印出來methodB....和methodD...,這個的話咱們也能夠本身運行上面的例子,來看一下打印的前後順序。
咱們也能夠經過代碼來分析一下,this指的是什麼呢?
this指的是調用這個方法的對象,那調用synchronized method()的又是被實例化出來的對象,因此當在同一個實例對象調用synchronized method()和synchronized(this)的時候,使用的是一個臨界區,也就是咱們所說的使用的同一個鎖。
這裏的話我我的以爲臨界區的這個概念應該會比較好理解一點。咱們能夠把synchronized method()和 synchronized()修飾的代碼都當作一個臨界區,若是調用synchronized修飾的方法對象和synchronized代碼塊裏面傳入的參數是同一個對象(這裏咱們說的是同一個對象是指他們的hashCode是相等的),則代表使用的是同一個臨界區,不然就不是。
那咱們來繼續看看下面的這個
final TestSynchronized obj = new TestSynchronized(); final TestSynchronized obj1= new TestSynchronized(); new Thread(() -> { //obj.methodC(); obj1.methodC(); }).start(); new Thread(() -> { //obj.methodC(); TestSynchronized.methodA(); }).start();
無論咱們使用obj1 仍是 obj 調用methodC方法,或者是obj調用methodC()和obj1調用methodC()方法,打印的順序都是先打印methodC.....過一秒纔打印出來另外的一個輸出。
那這裏的話其實和上面使用object對象相似,只不過這裏換成了Class對象。代碼在運行時,只會生成一個Class對象。咱們知道static修飾方法時,那方法就屬於類方法,因此這裏的話synchronized(Object.Class)和 statci synchronized method()都是使用的Object.class做爲鎖。
這裏咱們就不一一去舉例說明了,可能你們夥也能知道我想傳達的意思,有興趣的小夥伴能夠本身動手跑一跑代碼,看一下結果。在這裏的話我就直接給出告終論了。
1)當一個線程訪問同一個object對象中的synchronized(this)代碼塊或synchronized method()方法時,或者一個線程訪問object的synchronized(this)同步代碼塊,另外線程訪問synchronized method()時都會被阻塞。
一次只能有一個線程獲得執行。
另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。 當一個線程訪問一個對象的synchronized(object)代碼塊或synchronized method()時,其餘線程能夠同時訪問這個對象的非synchronized(obj)或synchronized method()方法。 這裏須要注意的是對於同一個對象
2)當一個線程訪問static synchronized method()修飾的靜態方法或synchronized()代碼塊,裏面的參數是一個Classs時,另一個線程訪問 synchronized(Object.class)或 訪問 static synchronized method()都會被阻塞。
當一個線程訪問synchronized修飾的靜態方法是,其餘線程能夠同時訪問其餘synchronized 修飾的非靜態方法,或者者是非synchronized(Object.class)。注意的是這裏的class必須是同一個class
這裏要說明一下其實 Object.class == Object.getClass(); .class 表明了一個Class的對象。這裏的Class不是Java中的關鍵字,而是一個類。
二、 能夠解決什麼問題
咱們上面已經瞭解synchronized的一些用法,咱們前面其實也介紹過synchronized能夠解決多線程併發訪問共享變量時帶來可見性、原子性問題。除此以外呢,其實還能夠利用synchronized和wait()/notify()來實現線程的交替順序執行。咱們就經過下面的例子或者圖片看一下。
下面一個例子是經典的經典的打印ABC的問題
public class TestABC implements Runnable{ private String name; private Object pre; private Object self; public TestABC(String name,Object pre,Object self){ this.name = name; this.pre = pre; this.self = self; } @Override public void run(){ int count = 10; while(count>0){ synchronized (pre) { synchronized (self) { System.out.print(name); count --; //釋放鎖,開啓下一次的條件 self.notify(); } try { //給以前的數據加鎖 pre.wait(); } catch (Exception e) { // TODO: handle exception } } } } public static void main(String[] args) throws InterruptedException { Object a = new Object(); Object b = new Object(); Object c = new Object(); Thread pa = new Thread(new TestABC("A",c,a)); Thread pb = new Thread(new TestABC("B",a,b)); Thread pc = new Thread(new TestABC("C",b,c)); pa.start(); TimeUnit.MILLISECONDS.sleep(100); pb.start(); TimeUnit.MILLISECONDS.sleep(100); pc.start(); } }
可能有人一開始理解不了,這是什麼鬼代碼,其實我剛開始學習這個synchronized關鍵字時,也沒有很好地能理解這個快代碼,那就來一塊兒分析看一下。
上圖是我利用一個桌面程序跑出來的效果,可是它這邊只能是在同一個鎖上進行的,沒有辦法模擬多個鎖,可是咱們能夠看到wait()/notify()帶來的效果。
當正在執行的線程調用wait()的時候,線程會主動讓出來鎖的歸屬權,咱們也能夠理解爲離開了臨界區,那其餘線程就能夠進入到這個臨界區。
調用wait()的線程,只能經過被調用notify()才能喚醒,喚醒以後又能夠從新去或者取臨界區的執行權。
那經過上圖就更好解釋示例代碼是如何運行的了。
咱們啓動線程的時候是按照A、B、C這樣的前後順序來啓動的。當A線程執行完之後,這裏會在c臨界區等待被喚醒,也就是左上角的步驟3,一樣線程B執行完之後會在a臨界區等待被喚醒,一樣線程C會在b臨界區等待被喚醒。
當線程按照這個順序啓動完成之後,以後的線程調度就交由CPU去進行執行順序是不肯定的,可是當線程C執行完之後,會喚醒在c臨界區等待的線程A,而線程B會一直被阻塞,直到在a臨界區上等待的線程被喚醒(也就是執行a.notify()),才能從新執行。同理,其餘兩個線程也是如此,這樣就完成了線程的順序執行。
經過上述所說,咱們可能大概也許對synchronized有那麼一點感受了。其實synchronized就是一個鎖(也能夠理解爲臨界區),那synchronized究竟是如何實現的呢?synchronized到底鎖住的是什麼呢?
這是咱們接下來要說的主要內容。
咱們在說內存模型的時候,提到了Java內存模型中提供了8個原子操做,其中有兩個操做是lock和unlock,這兩個原子操做在Java中使用了兩個更高級的指令moniterenter和moniterexit來實現的,synchronized實現線程的互斥就是經過這兩個指令實現的,可是synchronized 修飾方法 以及synchronized代碼塊實現還有稍微的有一些區別,那我就來看看這兩個實現的區別。
一、同步方法
咱們經過javap命令對咱們的Java代碼進行反編譯一下,咱們能夠看到以下圖的字節碼
咱們經過反編譯之後的字節碼沒有發現任何和鎖有關的線索。不要着急,咱們經過javap -v 命令來反編譯看一下
咱們發現methodD()方法中有一個flags屬性,裏面有一個ACC_SYNCHRONIZED,這個看起來好像和synchronized有些關係。
經過查資料發現JVM規範對於synchronized同步方法的一些說明:
資料1:https://docs.oracle.com/javas...
資料2:https://docs.oracle.com/javas...
其大體意思能夠歸納爲如下幾點
這裏給出了method_info的一些詳細說明,能夠參官方文檔。
https://docs.oracle.com/javas...
二、同步代碼塊
咱們經過反編譯咱們上面的代碼,獲得methodC的字節碼以下。
這裏咱們能夠看到有兩個指令moniterenter和moniterexit,JVM規範對於這兩個指令的給出了說明
Monitorenter
Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread waits until the object is unlocked,
每一個對象都有一個監視器(Monitor)與它相關聯,執行moniterenter指令的線程將得到與objectref關聯的監視器的全部權,若是另外一個線程已經擁有與objectref關聯的監視器,則當前線程將等待直到對象被解鎖爲止。
Monitorexit
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
當執行monitorexit的時候,和該線程關聯的監視器的計數就減1,若是計數爲0則退出監視器,該線程則再也不是監視器的全部者。
三、synchronized的實現
JVM規範中也說到每個對象都有一個與之關聯的Monitor,接下來咱們來看看到底他們之間有什麼關聯。
對象內存結構
HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
HotSpot虛擬機對象的對象頭部分包括兩類信息。第一類是用於存儲對象自身的運行時數據,如哈 希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
另一部分是類型指針,即對象指向它的類型元數據的指針,Java虛擬機經過這個指針來肯定該對象是哪一個類的實例。
咱們所說的鎖標識就存儲在Mark Word中,其結構以下。
其中標誌位10對應的指針,就是指向Monitor對象的,monitor是由ObjectMonitor實現的,其主要數據結構以下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)
ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有兩個隊列,_WaitSet
和 _EntryList
,用來保存ObjectWaiter對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner
指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList
集合,當線程獲取到對象的monitor 後進入 _Owner
區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1。
若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。
以下圖所示
由此看來,monitor對象存在於每一個Java對象的對象頭中(存儲的指針的指向),synchronized鎖即是經過這種方式互斥的。
原文連接:
https://juejin.im/post/5e8abd...
文源網絡,僅供學習之用,若有侵權,聯繫刪除。我將面試題和答案都整理成了PDF文檔,還有一套學習資料,涵蓋Java虛擬機、spring框架、Java線程、數據結構、設計模式等等,但不只限於此。
關注公衆號【java圈子】獲取資料,還有優質文章每日送達。