synchronized鎖住的到底是什么以及用法作用


前言:現在網上很多文章講synchronized的鎖這個鎖那個,讓人很是迷糊,那么synchronized鎖住的到底是什么呢?

作用

synchronized主要可以用來解決以下幾個問題:

  • 解決變量內存可見性問題:保證共享變量的修改的可以及時的刷新到主存中。實現方式為:被synchronized修飾的方法或者代碼塊中是用到的所有變量。都不會從當前線程本地中獲取,而是直接從主存讀,另外在退出synchronized修飾的方法或者代碼塊之后,就會把變化刷新到主存中。這種方式就可以解決,變量的內存可見性問題。
  • 互斥問題:確保線程互斥的訪問同步代碼,被synchronized修飾的代碼和方法同時只允許一個線程訪問。鎖住了當前的對象

用法

一般來說,synchronized有三種用法,分別是:

  • 普通方法
  • 靜態方法
  • 代碼塊

在說明這三種用法之前,要先說一個概念,就是synchronized鎖住的是對象!!!對於普通的方法,鎖住的是當前對象實例的對象。對於靜態方法,因為靜態方法是和類的Class相關聯的,因此鎖住的是當前類的Class對象。下面代碼中,所有的運行結果都是基於這個概念

不加鎖時

public  class SynchronizedTest1 {

    private static   int value = 0;
    public    void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            value++;
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public   void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest1 test = new SynchronizedTest1();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}


運行結果

Thread-0---Method 1 start
Thread-1---Method 2 start
Thread-1---Method 2 execute---value:2
Thread-1---Method 2 end
Thread-0---Method 1 execute---value:2
Thread-0---Method 1 end

可以看到不加鎖的時候,沒有采取任何的同步措施,結果是線程之間搶占式的運行。沒有線程安全性可言。線程0在休眠的時候還沒執行完就被線程1給搶占了。第一次遞增的時候,value的值應當是打印1,但是因為此時線程休眠,被其他線程搶了,然后再把value遞增了一次,因此,兩次的value都變成了2。

對方法進行加鎖

單對象雙同步方法

/**
 * 對方法進行加鎖
 */
public class SynchronizedTest2 {

    private static   int value = 0;
    public synchronized void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            value++;
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public  synchronized   void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest2 test = new SynchronizedTest2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

運行結果:

Thread-0---Method 1 start
Thread-0---Method 1 execute---value:1
Thread-0---Method 1 end
Thread-1---Method 2 start
Thread-1---Method 2 execute---value:2
Thread-1---Method 2 end

可以看到,上述兩個線程都是嚴格按照順序來執行的,即便是method1方法休眠了3秒,method2也不能獲取執行權,因為被鎖住的是test這個對象,並且是通過同一個對象去調用的,所以調用之前都需要先去競爭同一個對象上的鎖(monitor),也就只能互斥的獲取到鎖,因此,method1和method2只能順序的執行。必須等method1執行完畢之后,被鎖住的test對象才會被釋放,給method2執行,synchronized保證了在方法執行完畢之前,test實例對象中只會有有一個線程執行對象中的方法,因此value的值可以正常的遞增,保證了線程安全性。

雙對象單個同步方法

下面通過一個例子來說明,synchronized鎖的是當前實例的對象,而對另一個實例的對象毫無影響,因為根本不是同一把鎖。

我們把主函數的代碼改成如下這樣,我們new了兩個對象,並且讓這兩個線程分別訪問這兩個對象的method1方法,如果synchronized鎖的不是實例對象的話,會嚴格按照執行順序來執行代碼,線程1執行完method1之后,線程2才會再執行method1,然后兩次打印的值分別是1和2。但是結果真的是這樣嗎?

 public static void main(String[] args) {
        final SynchronizedTest2 test = new SynchronizedTest2();
        final SynchronizedTest2 test2 = new SynchronizedTest2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method1();
            }
        }).start();
    }

運行結果:

Thread-0---Method 1 start
Thread-1---Method 1 start
Thread-0---Method 1 execute---value:2
Thread-0---Method 1 end
Thread-1---Method 1 execute---value:2
Thread-1---Method 1 end

注意看線程的編號,執行過程是這樣的,首先線程1執行test對象中的method1,線程1休眠的時候,線程2獲得執行權。執行test2對象中的method1。為什么可以執行method1呢?因為這是兩把不同的鎖,synchronized鎖的是不同的對象,不存在訪問時候互斥的問題,各玩各的,所以根本不會有影響。因此,線程1休眠的時候,線程2獲取執行權,自然可以去執行test2對象的method方法了。這也印證了synchronized鎖的是對象。

單對象同步普通方法

如果是這種情況的話,一個方法有synchronized一個是普通方法,那么synchronized方法被線程1執行,普通方法被線程2執行,相互之間不會有影響,因為方法2沒有加鎖,方法1需要讀對象的鎖,而方法2不用所以可以直接執行,

public class SynchronizedTest4 {

private int value = 0;
    public synchronized void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public   void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest4 test = new SynchronizedTest4();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

運行結果

Thread-0---Method 1 start
Thread-1---Method 2 start
Thread-1---Method 2 execute---value:2
Thread-0---Method 1 execute---value:2
Thread-1---Method 2 end
Thread-0---Method 1 end

用圖解釋:

對靜態方法加鎖

在談對靜態方法進行加鎖之前,先要回顧一下一個概念, 被static修飾的成員變量和成員方法獨立於該類的任何實例對象。也就是說,它不依賴類特定的實例,被類的所有實例共享。因此,static所屬的對象是該類的Class類對象,一個類只會有一個Class類對象。詳細的可以看我這篇文章

所以對於被synchronized修飾的靜態方法,它鎖住的對象就是這個Class對象,而且這把鎖只有一個,意味着不管是多少個線程,new了多少個實例對象, 訪問的都是同一把鎖。看代碼

兩個同步靜態

public class SynchronizedTest5 {

    private static int value = 0;
    public synchronized static void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public synchronized  static   void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest5 test = new SynchronizedTest5();
        final SynchronizedTest5 test2 = new SynchronizedTest5();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method2();
            }
        }).start();
    }
}

運行結果

Thread-0---Method 1 start
Thread-0---Method 1 execute---value:1
Thread-0---Method 1 end
Thread-1---Method 2 start
Thread-1---Method 2 execute---value:2
Thread-1---Method 2 end

上述的代碼,new了兩個實例對象,每個對象訪問的是不同的方法。如果按照之前的思維來看,這兩個線程的鎖應當是不關聯的,在線程1休眠的時候,線程2應當會獲得cpu的執行權。但是事實卻是還是要等線程1執行完畢才會執行線程2,並且可以看到,value的值也是線程安全的遞增,因此可以驗證,對於靜態方法,調用的時候需要獲取同一個類上鎖(由於每個類只對應一個class對象),鎖住的對象是類的Class對象。因此只能順序執行。

非靜態同步

public class SynchronizedTest5 {

    private static int value = 0;
    public synchronized static void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            value++;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public    synchronized    void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            value++;
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest5 test = new SynchronizedTest5();
//        final SynchronizedTest5 test2 = new SynchronizedTest5();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

運行結果:

Thread-0---Method 1 start
Thread-1---Method 2 start
Thread-0---Method 1 execute---value:2
Thread-0---Method 1 end
Thread-1---Method 2 execute---value:2
Thread-1---Method 2 end

由上面的代碼可以看到,method1休眠的時候,method2拿到了執行權,可以繼續執行,而不是讓method1一直阻塞下去,這是因為這是兩把鎖,method1是靜態方法,是屬於SynchronizedTest5.class對象的鎖,而method2方法也加了synchronized,但是是屬於實例對象test的鎖,不會互相影響,因此也是各玩各的,看圖理解

對代碼塊加鎖

public class SynchronizedTest6 {

    private static int value = 0;

    public  void method1(){
        System.out.println(Thread.currentThread().getName() +"---Method 1 start");
        try {
            synchronized (this){
                value++;
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() +"---Method 1 execute---value:"+value);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 1 end");
    }

    public  void method2(){
        System.out.println(Thread.currentThread().getName() +"---Method 2 start");
        try {
            synchronized (this){
                value++;
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() +"---Method 2 execute---value:"+value);
            }


        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +"---Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest6 test = new SynchronizedTest6();
//        final SynchronizedTest5 test2 = new SynchronizedTest5();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

運行結果:

Thread-0---Method 1 start
Thread-1---Method 2 start
Thread-0---Method 1 execute---value:1
Thread-0---Method 1 end
Thread-1---Method 2 execute---value:2
Thread-1---Method 2 end

和synchronized加在方法上差不多的意思,需要線程1先把代碼執行完畢,釋放鎖,線程2才能進入代碼塊,因為鎖的也是當前的實例對象,其他的情況,按照上面所說的都可以總結出來

synchronized是可重入鎖

為什么么synchronized是可重入鎖,理由很簡單,因為他必須是可重入鎖。我們假設這樣一種情況,子類繼承了父類,然后重寫了父類的方法,在我們平時的開發中,通過super.xxx()調用父類的方法是一件很常見的需求,如果synchronized不可重入,那么調用super.xxx的時候,就會出錯,這從設計上來說就不合理。那么這個時候其實問題也來了,synchronized的對於子父類,鎖的又是誰呢?看代碼

class SynchronizedTest8{
    public synchronized void method1() {
        System.out.println(this.toString());
    }
}
public class SynchronizedTest7 extends SynchronizedTest8{
    @Override
    public synchronized void method1() {
        System.out.println(super.toString());
        System.out.println(this.toString());
        super.method1();
    }
    public static void main(String[] args) {
        SynchronizedTest7 test7 = new SynchronizedTest7();
        System.out.println(test7.toString());
        test7.method1();
    }
}

運行結果

com.black.synchronize.SynchronizedTest7@4554617c
com.black.synchronize.SynchronizedTest7@4554617c
com.black.synchronize.SynchronizedTest7@4554617c
com.black.synchronize.SynchronizedTest7@4554617c

看后面的地址就知道,這四個都是同一個對象,super和this是同一個引用,而且父類的this也是。並且都是當前的這個子類的對象,所以子類調用父類synchronized方法,也是對子類對象進行上鎖,所以才會說鎖住的是同一個對象,這也很好理解,因為方法調用是在子類發起的,所以鎖子類的對象,而父類都沒有實例對象,因此當然是鎖子類的對象了。也就是super本身仍然是子類的引用,只不過它可以調用到父類的方法或變量。另外經實驗發現,即便子類重寫的方法不加synchronized,也是可以調用父類的synchronized方法,理由也很簡單,因為都是一個對象鎖,同一把鎖,只是過程不一樣而已,一開始調用子類方法的時候,發現不需要同步也就不用加鎖,調用父類synchronized方法的時候,發現要同步,這時把鎖給加上就行了。也就滿足了可重入鎖的條件了。代碼就不貼了,只需要把子類的synchronized關鍵字去了,運行結果也是一樣的

總結:

通過以上大量的代碼演示,可以知道,synchronized的一些常見用法,然后可以推出synchronized到底鎖的是什么?對於普通的方法,synchronized鎖的是當前調用的實例對象,例如test.method1(),可以理解成這樣,synchronized(test){},而對於靜態方法synchronized,鎖的則是當前類的Class對象,並且這個對象只有一個,所以鎖也是只有一把,所有的實例對象訪問的這個同步方法,實際上讀的都是當前這個類的類對象。正是基於這個鎖的概念,因此synchronized可以實現上面所說的內存可見性和互斥性作用,從而實現線程安全。但是synchronized畢竟還是一個重量級的鎖,性能比較低,因為synchronized是互斥的,所以在切換線程的時候,線程上下文切換會引起大量的性能開銷。也正是因為這個性能原因飽受詬病,因此后面有了鎖的升級過程。這個問題后面再講


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM