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對象了,從而也就不會有數據不一致的問題了。
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐。