對java多線程里Synchronized的思考


    Synchronized這個關鍵字在多線程里經常會出現,哪怕做到架構師級別了,在考慮並發分流時,也經常會用到它。在本文里,將通過一些代碼實驗來驗證它究竟是“鎖”什么。

    在啟動多個線程后,它們有可能會並發地執行某個方法或某塊代碼,從而可能會發生不同線程同時修改同塊存儲空間內容的情況,這就會造成數據錯誤。   

1    //需要同步的對象類
2    class SynObject {
3        // 定義兩個屬性
4        int i;
5        int j;
6        // 把兩個屬性同時加1
7        public void add() {
8            i++;
9            // 睡眠500毫秒
10            try {
11                Thread.sleep(500);
12            } catch (InterruptedException e) {
13                e.printStackTrace();
14            }
15            j++;
16        // 打印當前i,j的值
17        System.out.println("Operator:+  Data:i=" + i + ",j=" + j);
18      }
19      // 把兩個屬性同時減1
20      public void minus() {
21        i--;
22        // 睡眠500毫秒
23        try {
24            Thread.sleep(500);
25        } catch (InterruptedException e) {
26             e.printStackTrace();
27        }
28        j--;
29        // 打印當前i,j的值
30        System.out.println("Operator:-  Data:i=" + i + ",j=" + j);
31      }
32    }

    從上文的第2到第32行里,我們定義了一個SynObject類,在其中的第3和第4行里,我們定義了i和j兩個屬性。

    在第7行的add方法里,我們是把i和j兩個屬性的值都加1,為了提升該方法被搶占的概率,在第11行里,我們通過sleep方法讓該線程睡眠500毫秒。

    同樣地我們在第20行定義了minus方法,在其中我們是把i和j都減1,同樣在第24行添加了sleep方法。    

33    class SynThreadAdd extends Thread {
34        // 需要同步的對象
35        SynObject o;
36        // 接受需要操作的那個對象的代參構造函數
37        public SynThreadAdd(SynObject o) {
38            this.o = o;
39        }
40        // 覆寫線程對象的run方法定義真正的執行邏輯
41        public void run() {
42            for (int i = 0; i < 3; i++) {
43                o.add();
44            }
45        }
46    }

    在第33行里,我們通過extends Thread的方式創建了一個線程對象SynThreadAdd,在第37行的構造函數里,設置待操作的對象o,在第41行的run方法里,我們通過了一個for循環調用了SynObject對象的add方法,對其中的i和j屬性進行加的操作。    

47    class SynThreadMinus extends Thread {
48        SynObject o;
49        public SynThreadMinus(SynObject o) {
50            this.o = o;
51        }
52        public void run() {
53            for (int i = 0; i < 3; i++) {
54                o.minus();
55            }
56        }
57    }

    第47行的SynThreadMinus對象和剛才定義的SynThreadAdd對象很相似,同樣是通過extends Thread的方式創建了一個線程對象,不同的是,在第52行的run方法里,是通過一個for循環調用了SynObject對象的minus方法,對其中的i和j屬性進行減操作。    

58    public class ThreadError {
59        // 測試主函數
60        public static void main(String args[]) {
61            // 實例化需要同步的對象
62            SynObject o = new SynObject();
63            // 實例化兩個並行操作該同步對象的線程
64            Thread t1 = new SynThreadAdd(o);
65            Thread t2 = new SynThreadMinus(o);
66            // 啟動兩個線程
67            t1.start();
68            t2.start();
69        }
70    }

    在main函數里,我們在第62行里創建了一個SynObject對象,在第64和65行里分別創建了SynThreadAdd和SynThreadMinus這兩個線程對象,並在67和68這兩行里啟動了這兩個線程。

    我們來看下運行結果,如果大家多次運行,每次的結果會不相同,但不影響下文的講解。    

1    Operator:+  Data:i=0,j=1
2    Operator:-  Data:i=1,j=0
3    Operator:+  Data:i=0,j=1
4    Operator:-  Data:i=1,j=0
5    Operator:-  Data:i=0,j=-1
6    Operator:+  Data:i=0,j=0

    在第1行里,我們看到的是執行完add方法后的輸出,奇怪的是,在這個方法里,我們明明是對i和j這兩個對象進行加操作,按理說應當i和j都是1,但這里的值確出乎我們意料,同樣地,第2到第5行的輸出里,i和j的值也不一致。

    原因出在多線程競爭上,這里的兩個線程t1和t2會分別通過add和minus方法操作SynObject對象里的i和j,在多線程並發的情況下,完全有可能按如下表7.1所列的次序執行上述代碼。

次序

t1的動作

t2的動作

i

j

1

通過t1.start();方法啟動

 

0

0

2

 

通過t2.start();方法啟動

 

 

3

t1通過run方法執行o.add操作

 

0

0

4

在add方法里執行i++

 

1

0

5

在add方法里執行sleep方法進入到阻塞狀態

 

1

0

6

處於阻塞狀態

t2通過run方法執行o.minus操作

1

0

7

處於阻塞狀態

在minus方法里執行i--

0

0

8

處於阻塞狀態

在minus方法里執行sleep方法進入到阻塞狀態

0

0

9

sleep時間到,恢復執行

處於阻塞狀態

0

0

10

執行j++並輸出i和j

處於阻塞狀態

0

1

    上表解釋了為什么在第1行輸出里i和j不一致的原因,從中我們能看到,一旦t1通過add方法操作SynObject類型的o對象后,t2線程通過minus方法,也有機會同時地操作這個對象,這樣, t1的add方法沒執行完(尚未完全地完成對i和j操作),t2的minus方法就插進來並發地操作同一個SynObject類型o對象,所以就導致了數據不一致的問題。這里我們解釋了第1行的輸出,后繼輸出的不一致現象是由於同樣的原因造成的。

    也就是說,在多線程並發的情況下,多個線程有可能會像上例那樣,通過不同的方法同時更改同一個資源(一般把它叫臨界資源),這樣就會造成臨界資源紊亂的情況。

    為了避免這樣的問題,我們可以在SyncObject類的add和minus方法前加上synchronized關鍵字,改寫后的SynObject類代碼如下所示。    

1    class SynObject {
2        // 定義兩個屬性,這部分代碼不變
3        int i;
4        int j;
5        // 給這個方法加上了synchronized關鍵字,而且sleep時間是5秒
6        public synchronized  void add() {
7            i++;
8            // 睡眠5秒
9            try {
10                Thread.sleep(5000);
11            } catch (InterruptedException e) {
12                e.printStackTrace();
13           }
14         j++;
15         // 打印當前i,j的值
16         System.out.println("Operator:+  Data:i=" + i + ",j=" + j);
17        }
18        // 也加了synchronized關鍵字
19         public  synchronized  void minus() {
20            i--;
21            //依然是睡眠500毫秒
22            try {
23                Thread.sleep(500);
24            } catch (InterruptedException e) {
25                e.printStackTrace();
26            }
27            j--;
28            // 打印當前i,j的值
29            System.out.println("Operator:-  Data:i=" + i + ",j=" + j);
30        }

    這里我們是把synchronized關鍵字作用到方法上。在給出正確的講解前,我們先列個似是而非的錯誤的說法,這些錯誤的說法看上去很有迷惑性,請大家在閱讀后一定要明辨是非。

    錯誤說法:如果我們把synchronized作用在方法上,那么就相當於給這個方法加了鎖,也就是說在一個時間段里只可能有一個線程來訪問這個方法。

    反駁的依據:我們用反證法,假設上述說法是正確的,加上synchronized后,假設add和minus方法是只能同時被一個線程調用,那么有這種情況,t1調用add,t2調用minus,(這符合假設的說法)由於add里睡眠時間是5秒,而minus是0.5秒,這樣minus方法還是有足夠多的時間來修改j的值,從而會導致i和j不一致,但我們不論運行多少次程序,均不會再出現i和j不一致的情況,所以這種說法是錯的。

    正確的說法:一旦給方法加了synchronized,就相當於給調用該方法的對象加了鎖,比如這里的add方法加了synchronized,調用的寫法是o.add();,也就是說是給o對象加了把鎖,在o.add調用結束之前,其它線程是無法得到o對象的控制和訪問權的。

    正確說法的依據:在調用add方法時,哪怕我們在其中sleep了5秒(大家甚至可以修改成睡眠10秒,效果更有說明意義),在這5秒里哪怕我們給了t2線程足夠多的時間讓它有機會去執行minus去造成i和j不一致,但從輸出結果上來看,不會出現i和j不一致的現象。正是因為給o對象加了鎖,那么在執行add時就不怕其它線程來搶占o對象了,從而也就不會有數據不一致的問題了。

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐。 


免責聲明!

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



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