1. 程式人生 > >Java併發程式設計系列(二)-synchronized同步鎖

Java併發程式設計系列(二)-synchronized同步鎖

synchronized基本用法

從一個簡單的例子入手

 1 public class C1 {
 2     private int count = 10;
 3 
 4     public void foo() {
 5         count--;
 6         System.out.println(Thread.currentThread().getName() + " count=" + count);
 7     }
 8 
 9     public static void main(String[] args) {
10         C1 c1 = new C1();
11 for (int i = 0; i < 5; i++) { 12 new Thread(c1::foo).start(); 13 } 14 } 15 }

類裡有個count變數初始是10.foo方法執行一次,count減1,打印出當前執行緒名和count.

程式入口方法new出一個C1的例項,開啟5個執行緒同時去執行foo方法.

執行結果:

執行兩次結果不一樣,第一次符合預期,每次減1.第二次出了狀況,9沒了,出現了兩次8.

 原因:Thread-0執行緒執行了count--,count變成了9,還沒有列印結果.這時Thread-1執行緒也進來了,執行count--,count變成了8.然後Thread-0開始列印結果,這時count是8,輸出的是8.然後Thread-1開始列印結果,這時count也是8,所以輸出了兩次8.

多執行幾次,還會出現不同的結果.單執行緒執行沒問題的程式碼,到了多執行緒環境下.執行結果就會變得不可預期.

synchronized關鍵字就是用來解決這種問題的.

 1 public class C1 {
 2     private int count = 10;
 3     private Object o = new Object();
 4 
 5     public void foo1() {
 6         synchronized (o) {
 7             count--;
 8             System.out.println(Thread.currentThread().getName() + " count=" + count);
9 } 10 } 11 12 public static void main(String[] args) { 13 C1 c1 = new C1(); 14 for (int i = 0; i < 5; i++) { 15 new Thread(c1::foo1).start(); 16 } 17 }

執行結果:

無論執行多少次,結果都是符合預期的.

將count--和列印結果放在synchronized的大括號裡,表示這是一個原子操作,不能分割.執行到一半的時候,別的執行緒是進不來的,只能阻塞.等到這個執行緒執行完大括號的程式碼時,會釋放鎖.這時候別的執行緒才能執行這段程式碼.

像這種簡單的場景每次想加鎖時還要new一個物件,感覺有點多此一舉啊.所以還有別的寫法,可以鎖定當前物件this.靜態方法沒有this物件,所以靜態方法鎖定的是類物件.下面幾種方法,對於這個場景,效果是一樣的.

 1 public class C1 {
 2     private int count = 10;
 3     private Object o = new Object();
 4 
 5     public void foo() {
 6         count--;
 7         System.out.println(Thread.currentThread().getName() + " count=" + count);
 8     }
 9 
10     public void foo1() {
11         synchronized (o) {
12             count--;
13             System.out.println(Thread.currentThread().getName() + " count=" + count);
14         }
15     }
16 
17     //鎖定this
18     public void foo2() {
19         synchronized (this) {
20             count--;
21             System.out.println(Thread.currentThread().getName() + " count=" + count);
22         }
23     }
24 
25     //將synchronized加到方法宣告上 同樣鎖定的是this
26     public synchronized void foo3() {
27         count--;
28         System.out.println(Thread.currentThread().getName() + " count=" + count);
29     }
30 
31     private static int sCount = 10;
32     private static Object so = new Object();
33 
34     public static void bar() {
35         sCount--;
36         System.out.println(Thread.currentThread().getName() + " sCount=" + sCount);
37     }
38 
39     public static void bar1() {
40         synchronized (so) {
41             sCount--;
42             System.out.println(Thread.currentThread().getName() + " sCount=" + sCount);
43         }
44     }
45 
46     //靜態方法鎖定類物件
47     public static void bar2() {
48         synchronized (C1.class) {
49             sCount--;
50             System.out.println(Thread.currentThread().getName() + " sCount=" + sCount);
51         }
52     }
53 
54     //將synchronized加到靜態方法的方法宣告上 同樣鎖定的是類物件
55     public synchronized static void bar3() {
56         sCount--;
57         System.out.println(Thread.currentThread().getName() + " sCount=" + sCount);
58     }
59 }

synchronized鎖定的物件

 1 public class C2 {
 2     private int count = 10;
 3     private int count1 = 10;
 4     private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
 5 
 6     public synchronized void foo() {
 7         System.out.println("foo() " + simpleDateFormat.format(new Date()));
 8         try {
 9             Thread.sleep(100);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         count--;
14         System.out.println(Thread.currentThread().getName() + " count=" + count);
15         System.out.println("foo() end " + simpleDateFormat.format(new Date()));
16     }
17 
18     public synchronized void foo1() {
19         System.out.println("foo1() " + simpleDateFormat.format(new Date()));
20         try {
21             Thread.sleep(100);
22         } catch (InterruptedException e) {
23             e.printStackTrace();
24         }
25         count1--;
26         System.out.println(Thread.currentThread().getName() + " count1=" + count1);
27         System.out.println("foo1() end " + simpleDateFormat.format(new Date()));
28     }
29 
30     public static void main(String[] args) {
31         C2 c2 = new C2();
32         new Thread(c2::foo).start();
33         new Thread(c2::foo1).start();
34     }
35 }

執行結果:

可以看到,兩個方法修改兩個變數,他們之間應該是沒有影響的.但現在第二個方法要等到第一個方法執行完才執行.

出現這種情況是因為,synchronized雖然加到了各自的方法上,但最終鎖是加到了this物件上.

synchronized同步鎖可以這麼來理解:當一個執行緒要執行synchronized大括號內的程式碼時,這會有一個看門的(synchronized)告訴執行緒,想要執行這段程式碼,你得去看看那邊的那個物件(this)上有沒有鎖,沒鎖你才能執行.同時你得在那個物件上掛個鎖,這樣別的執行緒就進不來了.

這樣,Thread-0進入foo方法時,就在this上掛了個鎖.這時Thread-1來執行foo1方法,雖然和Thread-0執行的不是同一塊程式碼甚至不是同一個方法.但是也被synchronized攔了下來,讓他去看this物件上有沒有鎖.這時this上有鎖.所以只能阻塞,等Thread-0執行完才能執行.

所以,修改兩個變數想互不干涉.只能鎖定兩個物件.

 1 public class C2 {
 2     private int count = 10;
 3     private int count1 = 10;
 4     private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
 5 
 6     private Object o = new Object();
 7     private Object o1 = new Object();
 8 
 9     public void bar() {
10         synchronized (o) {
11             System.out.println("bar() " + simpleDateFormat.format(new Date()));
12             try {
13                 Thread.sleep(100);
14             } catch (InterruptedException e) {
15                 e.printStackTrace();
16             }
17             count--;
18             System.out.println(Thread.currentThread().getName() + " count=" + count);
19             System.out.println("bar() end " + simpleDateFormat.format(new Date()));
20         }
21     }
22 
23     public void bar1() {
24         synchronized (o1) {
25             System.out.println("bar1() " + simpleDateFormat.format(new Date()));
26             try {
27                 Thread.sleep(100);
28             } catch (InterruptedException e) {
29                 e.printStackTrace();
30             }
31             count1--;
32             System.out.println(Thread.currentThread().getName() + " count1=" + count1);
33             System.out.println("bar1() end " + simpleDateFormat.format(new Date()));
34         }
35     }
36 
37     public static void main(String[] args) {
38         C2 c2 = new C2();
39         new Thread(c2::bar).start();
40         new Thread(c2::bar1).start();
41     }
42 }

執行結果:

可以看到bar1沒有等到bar執行完才進入方法.

 synchronized鎖定在堆記憶體

 1 public class C3 {
 2     private int count = 10;
 3     private Object o = new Object();
 4     private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
 5 
 6     public void foo() {
 7         synchronized(o){
 8             System.out.println("foo() " + simpleDateFormat.format(new Date()));
 9             //此處高能
10             o=new Object();
11             try {
12                 Thread.sleep(100);
13             } catch (InterruptedException e) {
14                 e.printStackTrace();
15             }
16             count--;
17             System.out.println(Thread.currentThread().getName() + " count=" + count);
18             System.out.println("foo() end " + simpleDateFormat.format(new Date()));
19         }
20     }
21 
22     public void bar(){
23         synchronized (o){
24             System.out.println("bar() " + simpleDateFormat.format(new Date()));
25             //此處高能
26             o=new Object();
27             try {
28                 Thread.sleep(100);
29             } catch (InterruptedException e) {
30                 e.printStackTrace();
31             }
32             count--;
33             System.out.println(Thread.currentThread().getName() + " count=" + count);
34             System.out.println("bar() end " + simpleDateFormat.format(new Date()));
35         }
36     }
37 
38     public static void main(String[] args) {
39         C3 c3=new C3();
40         new Thread(c3::foo).start();
41         new Thread(c3::bar).start();
42     }
43 }

執行結果:

上面兩個方法同樣的邏輯:記錄開始和結束時間,count--,列印執行緒名和數量,synchronized鎖定的都是o.

從執行結果看,哇完全是同時執行的嘛,根本就沒有阻塞,根本就沒有原子.

注意註釋高能處o=new Object().就是上完鎖之後,他把地址給換了,指向了一個沒有鎖的記憶體推.

所以重新理解下這個過程:

當一個執行緒要執行synchronized程式碼塊的程式碼時,先去棧裡找o這個物件的引用地址,然後根據地址去堆裡找new Object()分配的記憶體,有鎖的話阻塞等待,直到解鎖.沒鎖的話就在這個區域的推記憶體上加一個鎖.這樣別的執行緒就不能執行這塊程式碼了.但上面的例子裡,執行緒把鎖加上之後,又new了一個Object,在堆裡新分配了一塊記憶體,把o處的引用地址改成了這裡.所以第二個執行緒通過o來找堆記憶體的時候,找到的是這個新分配的堆記憶體,自然是沒有加鎖的.這樣就不會阻塞等待.直接去執行了

髒讀問題

通過下面程式碼瞭解髒讀是怎麼產生的,當然髒讀不一定就是問題,允不允許髒讀要看業務.

 1 public class C4 {
 2 
 3     private int balance = 0;
 4 
 5     public synchronized void addBalance(int money) {
 6         try {
 7             Thread.sleep(100);
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         }
11         balance += money;
12     }
13 
14     public void showBalance() {
15         System.out.println(balance);
16     }
17 
18     public synchronized void showBalanceSync() {
19         System.out.println(balance);
20     }
21 
22     public static void main(String[] args) {
23         C4 c4 = new C4();
24         new Thread(() -> c4.addBalance(2)).start();
25         new Thread(c4::showBalance).start();
26         try {
27             Thread.sleep(100);
28         } catch (InterruptedException e) {
29             e.printStackTrace();
30         }
31         new Thread(c4::showBalance).start();
32 
33         try {
34             Thread.sleep(1000);
35         } catch (InterruptedException e) {
36             e.printStackTrace();
37         }
38         System.out.println("---分割線---");
39         new Thread(() -> c4.addBalance(3)).start();
40         new Thread(c4::showBalanceSync).start();
41     }
42 }

執行結果:

先執行餘額加2,馬上去讀,讀書來是0.睡100毫秒再去讀,讀出來的才是2.因為寫餘額的方法加鎖了,但是讀的方法沒加鎖.所以寫入餘額還沒執行完的時候去讀,讀的是以前的.

分割線下面的部分呼叫的是加同步鎖的讀餘額,寫入餘額加3沒執行完的時候,讀方法時阻塞的.等寫完後才開始讀,所以讀出來的是5.

synchronized重入

 1 public class C5 {
 2 
 3     private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
 4 
 5     public synchronized void foo(){
 6         System.out.println("foo() " + simpleDateFormat.format(new Date()));
 7         try {
 8             Thread.sleep(100);
 9         } catch (InterruptedException e) {
10             e.printStackTrace();
11         }
12         System.out.println("foo() end " + simpleDateFormat.format(new Date()));
13     }
14 
15     public synchronized void bar(){
16         System.out.println("bar() " + simpleDateFormat.format(new Date()));
17         try {
18             Thread.sleep(100);
19         } catch (InterruptedException e) {
20             e.printStackTrace();
21         }
22         //當前執行緒內執行foo()
23         foo();
24         System.out.println("bar() end " + simpleDateFormat.format(new Date()));
25     }
26 
27     public static void main(String[] args) {
28         C5 c5=new C5();
29         new Thread(c5::bar).start();
30     }
31 }

執行結果:

類裡有兩個同步方法,都鎖定的是當前物件this.執行bar的時候加了鎖.在bar方法中呼叫了foo,而foo也是同步方法,也需要判斷this上有沒有鎖.那這時foo方法會阻塞嗎.從執行結果上看,並沒有阻塞.因為bar和foo方法在同一個執行緒內執行,當執行緒執行foo方法時去記憶體堆裡看有沒有鎖時,這時有鎖,但這個鎖是剛才自己掛上去的,所以就繼續往下執行了.如果不這樣做,方法就永遠執行不了,foo方法需要bar釋放鎖才能執行,而bar方法沒有執行完foo不會釋放鎖.所以可以理解成鎖的使用權在當前執行緒.

為了說明這個問題,看下面例子

public class C5 {

    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");

    public synchronized void foo(){
        System.out.println("foo() " + simpleDateFormat.format(new Date()));
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo() end " + simpleDateFormat.format(new Date()));
    }

    public synchronized void bar1(){
        System.out.println("bar1() " + simpleDateFormat.format(new Date()));
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //新開一個執行緒執行foo()
        new Thread(this::foo).start();
        System.out.println("bar1() end " + simpleDateFormat.format(new Date()));
    }

    public static void main(String[] args) {
        C5 c5=new C5();
        new Thread(c5::bar1).start();
    }
}

執行結果:

和上面唯一的區別就是bar1方法呼叫foo的時候,是開了一個執行緒呼叫的.如結果所示,執行foo的時候阻塞了,bar1執行完釋放鎖之後,foo才開始執行.

synchronized死鎖

最簡單的死鎖情況如下例所示

 1 public class C6 {
 2 
 3     private Object o = new Object();
 4     private Object o1 = new Object();
 5 
 6     public void foo() {
 7         System.out.println("foo()");
 8         synchronized (o) {
 9             try {
10                 Thread.sleep(100);
11             } catch (InterruptedException e) {
12                 e.printStackTrace();
13             }
14             System.out.println("foo() lock o");
15             synchronized (o1){
16                 System.out.println("foo() lock o1");
17             }
18         }
19     }
20 
21     public void bar(){
22         System.out.println("bar()");
23         synchronized (o1){
24             System.out.println("bar() lock o1");
25             synchronized (o){
26                 System.out.println("bar() lock o");
27             }
28         }
29     }
30 
31     public static void main(String[] args){
32         C6 c6=new C6();
33         new Thread(c6::foo).start();
34         new Thread(c6::bar).start();
35     }

執行結果:

這個結果是永遠也執行不完的.因為死鎖了.o等著o1釋放,o1等著o釋放.天長地久海枯石爛.

synchronized鎖會在丟擲異常時被釋放

 1 public class C7 {
 2 
 3     private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
 4 
 5     public synchronized void foo(){
 6         System.out.println("foo() "+simpleDateFormat.format(new Date()));
 7         //處理邏輯1耗時100毫秒
 8         try {
 9             Thread.sleep(100);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         foo1();
14         //處理邏輯2耗時100毫秒
15         try {
16             Thread.sleep(100);
17         } catch (InterruptedException e) {
18             e.printStackTrace();
19         }
20     }
21 
22     private void foo1(){
23         throw new RuntimeException("假設處理foo1邏輯的時候出了異常");
24     }
25 
26     public synchronized void bar(){
27         System.out.println("bar() "+simpleDateFormat.format(new Date()));
28     }
29 
30     public static void main(String[] args) {
31         C7 c7=new C7();
32         new Thread(c7::foo).start();
33         /**
34          * 正常foo執行需要200毫秒 200毫秒後釋放鎖 bar執行
35          * 現在foo執行到100毫秒時 foo1執行 foo1執行時丟擲了異常 鎖被釋放
36          * 所以bar實際在foo執行100毫秒後執行
37          */
38         new Thread(c7::bar).start();
39     }
40 }

執行結果:

注意執行時間,相差101毫秒,遠不到200毫秒.