1. 程式人生 > >synchronized 同步鎖(java)例項解析

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屬於類,有且僅有一個鎖,從而可以保證任意一個時刻只有一個執行緒可以訪問這個同步塊。


5.後記

限於水平,難免疏漏,請看官多多指正,等到抽出時間再進行補充。 【特別說明】有任何疑問,請掃描二維碼提問: