Java併發:synchronized 實例方法同步/靜態方法同步/實例方法中的同步塊/靜態方法中的同步塊 理解

Java中的同步塊用synchronized標記。同步塊在Java中是同步在某個對象上。所有同步在一個對象上的同步塊在同時只能被一個線程進入並執行操作。所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。

並且存在4中不同的同步塊
1.實例方法
2.靜態方法
3.實例方法中的同步塊
4.靜態方法中的同步塊

在分別講這4個同步塊的區別前需要首先了解下Java中synchronized的原理。
Java的同步機制是通過對對象(資源)加鎖實現的,舉個栗子:
現在有一個對象A,線程T1和T2都會修改對象A的值。
首先假設線程T1比線程T2更早運行到修改對象A的代碼。
此時線程T1修改對象A的流程應該是這樣的:線程T1從主存中獲取對象A,並在自己緩存中存下對象A的副本,然後修改自己緩存中的對象A的副本。修改完畢後將自己緩存中的對象A的副本賦值給主存中的對象A,實現對象A的修改。
但是在線程T1執行修改對象A時,線程T2可能也跑到了修改對象A值的代碼,而此時線程T1還在修改自己緩存中的對象A的副本,還沒有將副本更新到主存中的對象A,但是線程T2又不知道,所以直接就從主存去取對象A了,這樣就出現了競態條件
直觀點,如果A=1,T1&T2都是將A+1,那麼我們期望執行完T1&T2後A=3,但是上面栗子中最後A=2,因此我們爲了防止這種競態條件的出現,就需要給對象A加鎖。
在上面的例子中當線程T1從主存獲取對象A時,對象A就會加鎖,這時如果線程T2要從主存中取對象A時就會被阻塞,直到線程T1完成對主存中對象A的修改,這時鎖會被打開,線程T2纔可以調用對象A。

實例方法

實例方法(類的非靜態方法)同步是同步在擁有該方法的對象上的,即加鎖會加在對象上。
比如:有多個線程調用一個對象的多個函數(限定下,只有同步函數會被多個線程重複調用)時。任何一個線程在調用函數時將會判斷這個函數是否是同步函數(即加了synchronized關鍵字),如果是,則會去檢查該方法的對象(擁有者)是否已經加鎖,如果加鎖了,該線程就會阻塞,等待該對象解鎖;如果是同步函數且對象沒有加鎖,則會繼續運行,並給對象加鎖;如果不是同步函數的話,就不需要去檢查對象是否加鎖,直接可以繼續運行。

可以看下示例:
現在有sTest 類,包含2個(add,add2)同步實例(非靜態)方法,一個(addX1)實例(非靜態)方法。

public class sTest {
    public synchronized void add(){
        for(int i=0 ;i<100;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    public synchronized void add2(){
        for(int i=100 ;i<200;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    public void addX1(){
        for(int i=200 ;i<300;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

現在新建4個線程,並分別命名爲thread1-4,所有線程全都只執行sTest 的實例對象Test的一個方法。其中1運行add,2運行add2,3運行add,4運行addX1。

public class SynchronizedTest {
    public static void main(String[] args){ 
        final sTest Test=new sTest();

        Thread thread1 = new Thread(new Runnable(){
            @Override
            public void run() {
                Test.add();
            }
        },"thread1");

        Thread thread2 = new Thread(new Runnable(){
            @Override
            public void run() {
                Test.add2();
            }
        },"thread2");

        Thread thread3 = new Thread(new Runnable(){
            @Override
            public void run() {
                Test.add();
            }
        },"thread3");

        Thread thread4 = new Thread(new Runnable(){
            @Override
            public void run() {
                Test.addX1();
            }
        },"thread4");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

大致運行結果應該會符合以下說明:
開頭一段是Thread1和Thread4混合交錯的輸出
等Thread1輸出完畢後是Thread3或者Thread2的輸出
Thread3或者Thread2輸出完畢後是Thread2或者Thread3

以上,也可以自己嘗試創建多個sTest 的對象,然後在多個線程中運行不同對象的同一個同步函數。你會發現輸出應該是混合交錯的,即只會對對象加鎖。


靜態方法同步

靜態方法同步是同步在靜態方法的類對象(不是類的實例,是指保存類信息的對象)上的,事實上,Java虛擬機中一個類只能對應一個類對象,所以只允許一個線程執行同一個類靜態同步方法。其實和實例方法同步是類似的,只不過實例方法同步是在類實例上加鎖,靜態方法同步是在類對象上加鎖。
示例:

public class sTest {
    public static synchronized void addJ1(){
        for(int i=400 ;i<500;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    public static synchronized void addJ2(){
        for(int i=500 ;i<600;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class SynchronizedTest {
    public static void main(String[] args){ 
        Thread thread1 = new Thread(new Runnable(){
            @Override
            public void run() {
                sTest.addJ1();
            }
        },"thread1");

        Thread thread2 = new Thread(new Runnable(){
            @Override
            public void run() {
                sTest.addJ2();
            }
        },"thread2");

        thread1.start();
        thread2.start();
    }
}

結果:線程1調用完靜態同步函數addJ1後,線程2纔會從阻塞中恢復並調用完靜態同步函數addJ2
這裏寫圖片描述


實例方法中的同步塊

非同步函數裏的同步塊例子

public class MyClass {
   public void function(){
      synchronized(X){
          ......
      }
   }
 }

有時候我們可能只需要給某個函數中的一塊代碼加同步,這時就可以使用同步塊方式實現。在上例中,我麼可以看到用括號把一個對象X括了起來,在同步塊中,被括號括起來的對象叫做監視對象,即Java會給這個對象X加鎖以實現同步。
一次只有一個線程能夠在同步於同一個監視對象的Java方法內執行。
栗子

public class sTest {
        //實例方法同步
        public synchronized void add(){
            for(int i=0 ;i<100;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
        //實例方法中的同步塊
        public void addQ1(){
            synchronized(this){
                for(int i=600 ;i<700;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
        public void addQ2(){
            synchronized(this){
                for(int i=700 ;i<800;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
        public void addQ3(String s){
            synchronized(s){
                for(int i=800 ;i<900;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
        public void addQ4(String s){
            synchronized(s){
                for(int i=900 ;i<1000;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
}

首先我們建兩個線程並分別執行addQ1和addQ2

public static void main(String[] args){ 
        final sTest sTest =new sTest();
        final String s = "1";
        Thread thread1 = new Thread(new Runnable(){
            @Override
            public void run() {
                sTest.addQ1();
            }
        },"thread1");

        Thread thread2 = new Thread(new Runnable(){
            @Override
            public void run() {
                sTest.addQ2();
            }
        },"thread2");

        thread1.start();
        thread2.start();
    }

結果,因爲監視對象爲this,即類實例sTest,而thread1和thread2執行的都是同一個對象sTest。所以會出現如下圖結果,執行完一個線程的方法前另一個線程會被阻塞。當然,這個栗子其實和實例方法同步裏的一樣,都是對類實例加鎖,也可是嘗試在thread2中執行add函數,結果也是會出現阻塞。
同樣的,新建一個String變量s,然後在thread1和thread2中調用addQ3(s)和addQ4(s),也能看到同樣的結果。
這裏寫圖片描述


靜態方法中的同步塊

和實例方法中的同步塊是一樣的。
栗子:

//靜態方法同步
public static synchronized void addJ1(){
            for(int i=400 ;i<500;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
//靜態方法中的同步塊
public static void addS1(){
            synchronized(sTest.class){
                for(int i=1000 ;i<1100;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }

addJ1,addS1這兩個方法不允許同時被線程訪問。
與實例方法中的同步塊一樣,當傳入同一個對象時,這兩個片段不允許同時被線程訪問。

public static void addS1(String s){
            synchronized(s){
                for(int i=1000 ;i<1100;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
public static void addS2(String s){
            synchronized(s){
                for(int i=1000 ;i<1100;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章