synchronized 同步鎖(java)例項解析
0 引言
在多執行緒應用場景中,同步鎖是一種非常重要的機制,例如:ID號的分配,多個客戶端分別與服務端建立連線,客戶端併發請求的情況下,為提升吞吐量,服務端一般採用多執行緒處理請求,若無同步鎖機制,不同執行緒分配到相同ID號的情況將不可避免,而這種情況與預期相違背。
1.java多執行緒簡述
Java中執行緒的建立一般有三種形式,最常見的是繼承Thread類覆寫run()方法的方式,此外還有兩種,這個問題並非本文關注點,這裡就不做展開了,我重點介紹一下繼承Thread類覆寫run()方法的方式:【繼承Thread類,重寫該類的run()方法】
如以下程式碼所示,繼承Thread類,通過重寫run()方法定義了一個新的執行緒類MyThread,其中run()方法的方法體代表了執行緒需要完成的任務,稱之為【執行緒執行體】。當建立此執行緒類的物件時,一個新的執行緒得以建立,並進入到執行緒【新建狀態】。通過呼叫執行緒物件引用的start()方法,使得該執行緒進入到【就緒狀態】,此時此執行緒並不一定會馬上得以執行,這取決於CPU排程時機,這個時機並沒有明顯的規律,表面上看似隨機,內在機理與JVM有關,java官方並未就此說明,當然,某種程度上這並不重要。我們看一下上面程式碼某一次的執行結果:public class ThreadIntroduction { public static void main(String[] args) { for(int index=0;index<3;index++) { MyThread temp=new MyThread(); temp.start(); } } } class MyThread extends Thread { @Override public void run() { for(int i=0;i<5;i++) { System.out.println(Thread.currentThread().getName()+": ["+i+"]"); } } }
如上圖所示,多執行緒場景下,執行緒的執行順序(cpu排程的順序)與程式中的客觀順序並沒有直接關係,且每次執行結果都不相同,具有一定的隨機性。
2.java 同步鎖機制介紹
java同步鎖的關鍵字為:synchronized,其應用主要有兩種方式:[synchronized 方法]和[synchronized 塊],顧名思義,其區別在於作用範圍不同,以下分別對兩種方式進行介紹,首先我們看一個例子類:
public class TestForSynchronized { static int ID=0; //測試方法01-synchronized塊(物件級) public String setID_01() { synchronized(this) { ID++; return "setID_01() ID No.:"+ID; } } //測試方法02-synchronized塊(類級別) public String setID_02() { synchronized(TestForSynchronized.class) { ID++; return "setID_02() ID No.:"+ID; } } //測試方法03-synchronized 方法 public synchronized String setID_03() { ID++; return "setID_03() ID No.:"+ID; } //普通方法 public String commonMethod() { return "commonMethod ID No."+ID; } }
如上程式所示,分別體現了synchronized塊(物件級別和類級別),synchronized方法,普通方法的定義方式,
2.1.synchronized 方法
通過在方法的定義中加入synchronized關鍵字來定義synchronized方法,例如:
public synchronized String setID(),這就是一個返回值為String型別的synchronized方法,對於一個應用了synchronized機制的類來說,以上面定義的類TestForSynchronized 為例,它的每一個例項(物件)都具有單一的鎖,當通過這個(例項)物件呼叫它的任何synchronized方法,或者這個例項(物件)執行synchronized塊的時候,這個例項(物件)就會被加鎖,即:
- 在多執行緒場景下,對於某一個類例項(物件)tempObject,如果多個執行緒併發通過tempObject訪問其synchronized方法或者synchronized塊時,任何一個時刻只有一個執行緒處於可執行狀態,因為同一時刻只有一個執行緒能夠獲取該例項的唯一的鎖,其它執行緒都會被阻塞,直到synchronized方法返回或者synchronized塊執行完畢,鎖才會被佔用的執行緒釋放,此前被阻塞的執行緒才有機會獲得該物件的鎖;
- 在多執行緒場景下,對於某一個類例項(物件)tempObject,如果一個執行緒獲得tempObject的鎖,在一個synchronized方法返回或者一個synchronized塊執行完畢後,便會將鎖釋放,而不會繼續持有鎖,即使該執行緒接下來仍需執行該例項tempObject的其它synchronized方法或者synchronized塊;
我們以具體的例子來驗證一下上述介紹(類例項為上面定義的TestForSynchronized):
驗證<1>中論點:
public class MainClass
{
public static void main(String[] args)
{
// 建立10個執行緒來呼叫【同一個】TestForSynchronized例項(物件)
TestForSynchronized temp=new TestForSynchronized();
for(int index=0;index<10;index++)
{
MyThread_01 thread=new MyThread_01(temp);
thread.start();
}
}
}
class MyThread_01 extends Thread
{
TestForSynchronized testObject;
public MyThread_01(TestForSynchronized testObject)
{
this.testObject=testObject;
}
@Override
public void run()
{
try
{
Thread.sleep(0);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--"+testObject.commonMethod());
System.out.println(Thread.currentThread().getName()+"--"+testObject.setID_01());
}
}
某次執行的輸出結果:
驗證<2>中論點:
將上面驗證<1>中的commonMethod換成synchronized方法setID_03:
public class MainClass
{
public static void main(String[] args)
{
// 建立10個執行緒來呼叫【同一個】TestForSynchronized例項(物件)
TestForSynchronized temp=new TestForSynchronized();
for(int index=0;index<10;index++)
{
MyThread_01 thread=new MyThread_01(temp);
thread.start();
}
}
}
class MyThread_01 extends Thread
{
TestForSynchronized testObject;
public MyThread_01(TestForSynchronized testObject)
{
this.testObject=testObject;
}
@Override
public void run()
{
try
{
Thread.sleep(0);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--"+testObject.setID_01());
System.out.println(Thread.currentThread().getName()+"--"+testObject.setID_03());
}
}
某次執行的輸出結果:
2.2.synchronized 塊
通過上面的介紹,相信讀者已經對synchronized塊和synchronized方法有了一定理解,這裡我們再補充介紹一下synchronized塊。前已述及,synchronized塊和synchronized方法,最明顯的區別在於【作用域】,synchronized方法的作用域更大一些,某些場景下,作用域太大是一種缺陷,例如:對於一個很複雜,程式碼量比較大的方法,如果將其定義為synchronized方法,那麼,由於同步鎖的機制制約,多執行緒場景下,效率將會顯得低下;如果,需要同步鎖機制保障的僅僅只是一小段程式碼的話,完全可以採用synchronized塊來解決。
synchronized塊的定義方式:synchronized(syncObject)
synchronized(syncObject)
{
//允許控制的程式碼塊
}
synchronized塊具有以下特點:
- synchronized塊必須獲得了syncObject的鎖才能執行這塊程式碼,具體獲得鎖的機制如前面分析;
- 當多個執行緒併發訪問某個例項syncObject的synchronized(this)塊時,任何一個時刻只有一個執行緒能夠持有該例項的鎖,執行synchronized(this)塊,其它執行緒將被阻塞,直到執行完畢釋放鎖;
- 多執行緒場景下,當某個執行緒訪問例項syncObject的synchronized(this)塊時,其它執行緒可以訪問例項syncObject的非synchronized(this)塊和非synchronized方法;
2.3.特殊的synchronized 塊(重要!!!)
如我們在例子類TestForSynchronized中定義的方法setID_02():
//測試方法02-synchronized塊(類級別)
public String setID_02()
{
synchronized(TestForSynchronized.class)
{
ID++;
return "setID_02() ID No.:"+ID;
}
}
在synchronized塊中,並不是this(物件級),而是class(類級別),相較於物件級別,類級別具有更嚴格的同步約束,主要有以下幾點:
- 前已述及,採用了synchronized機制的類,其每一個例項都有一個鎖(物件級別的鎖),事實上,類也有唯一的鎖,換言之,採用了synchronized機制的類有一個類鎖;
- 多執行緒場景下,當某一執行緒獲得類鎖(注意:不再是物件鎖),其它執行緒將被阻塞,將無法呼叫或訪問該類的所有方法和域,包括靜態方法和靜態變數;
- 對於含有靜態方法和靜態變數的程式碼塊的同步,類鎖的嚴格約束在多執行緒場景下非常實用,應用也較多。
3.類鎖的應用例項
基於在TestForSynchronized類中定義了synchronized塊的方法setID_02,執行如下程式碼:public class MainClass
{
public static void main(String[] args)
{
//建立10個執行緒,每個執行緒各建立一個TestForSynchronized例項(物件)
for(int index=0;index<10;index++)
{
MyThread_02 thread=new MyThread_02();
thread.start();
}
}
}
class MyThread_02 extends Thread
{
@Override
public void run()
{
//每個執行緒均建立一個TestForSynchronized例項
TestForSynchronized temp=new TestForSynchronized();
System.out.println(Thread.currentThread().getName()+"--"+temp.setID_02());
}
}
某次執行的輸出結果:
4. 互斥鎖mutex
上面介紹的“類鎖”雖然可以最大限度的避免併發場景下的衝突,但是,其過於嚴格:多執行緒場景下,當某一執行緒獲得類鎖(注意:不再是物件鎖),其它執行緒將被阻塞,將無法呼叫或訪問該類的所有方法和域,包括靜態方法和靜態變數。
事實上,對於一個類,不可能所有屬性和方法都涉及併發問題,因此,類鎖過於嚴格的限制會極大的影響效能。鑑於此,本節介紹另外一種鎖:互斥鎖mutex,其定義方式如下:
public class TestForSynchronized
{
//定義一個靜態物件
private static Object mutex = new Object();
//
//省略其它屬性的定義
public void TestMethod()
{
synchronized (mutex)
{
//涉及併發問題的程式碼塊
}
}
}
由於mutex為靜態型別,對於TestForSynchronized類的所有物件,當需要訪問TestMethod的同步塊時都必須獲得mutex物件的鎖,然而,mutex屬於類,有且僅有一個鎖,從而可以保證任意一個時刻只有一個執行緒可以訪問這個同步塊。