下面代碼是線程不安全的代碼,請問為什麼很難跑出不安全的樣例?

我想跑出線程不安全案例,可是為什麼跑出來都是線程安全的,請問問題出在哪裡?

public class UnsafeSequence {
private int value;

public UnsafeSequence(int value){
this.value = value;
}

/**返回一個唯一的數值*/
public int getValue(){
return value++;
}

public static void main(String[] args){
UnsafeSequence unsafeSequence = new UnsafeSequence(0);

for (int i = 0; i&<100;i++){ new Thread() { public void run() { try { sleep(100); for (int i = 0;i&<2;i++) System.out.println(Thread.currentThread().getName()+":"+unsafeSequence.getValue()); }catch (InterruptedException ex){ } } }.start(); } } }


首先題主請務必糾正一個誤解:線程不安全指的是「不保證會得到預期安全的行為」,而不是「保證不會得到預期安全的行為」。這是個邏輯問題而不是Java、多線程問題。

當您觀察到一次實驗的結果與預期的「安全」行為一致時,並不代表它就是安全的了。「不保證會得到預期安全的行為」也允許有些時候可以運行得到預期的結果,只是「沒有保證」而已。

然而沒有保證就是頭疼的地方了。您聽說過因為沒有正確同步而在大並發條件下在HashMap.get()里死循環的案例不?這就是「不保證安全」,然後很多時候似乎也運行得到了預期的結果,然後放上生產環境壓一壓就垮的案例。

@木女孩說得對。這個並發量級太低了。

另外代碼在被JIT編譯過後更容易觀察到各種有趣的結果。所以把題主的例子稍微調整了一下結構來幫助它預熱一小下:

public class UnsafeSequence {
private int value;

public UnsafeSequence(int value) {
this.value = value;
}

public int getValue(){
return value++;
}

private static class MyRunnable implements Runnable {
private final UnsafeSequence seq;

MyRunnable(UnsafeSequence seq) {
this.seq = seq;
}

@Override
public void run() {
try {
Thread.sleep(100);
for (int i = 0; i &< 1000; i++) { System.out.println(Thread.currentThread().getName() + ":" + seq.getValue()); } } catch (InterruptedException ex) { } } } // warm up MyRunnable.run(), make sure MyRunnable.run() is JIT compiled private static void warmUp() { UnsafeSequence unsafeSequence = new UnsafeSequence(0); MyRunnable r = new MyRunnable(unsafeSequence); // warm up MyRunnable.run() for (int i = 0; i &< 100; i++) { r.run(); } } public static void main(String[] args) { warmUp(); UnsafeSequence unsafeSequence = new UnsafeSequence(0); MyRunnable r = new MyRunnable(unsafeSequence); for (int i = 0; i &< 5000; i++) { new Thread(r).start(); } } }

截取一段輸出給題主參考:

Thread-1439:68297
Thread-1439:585663
Thread-1439:585664
Thread-1439:585665
Thread-1439:585666
Thread-1439:585667
Thread-1439:585668
Thread-1438:68295
Thread-1438:585670
Thread-1438:585671


1. 既然是概率性問題,那麼充分測試,如上回答應加大循環次數

2. 條件問題:保證多線程並發,檢查一下cpu是否為多核(如果是虛擬機有可能只分到1核)


次數太少,跑個十萬次就有問題了


說明機器太好了,開兩個遊戲試試


@匿名用戶

value++,在無編譯優化下,會執行如下指令

0x40058b &: mov 0x200aa7(%rip),%eax # 0x601038 &

0x400591 &: add $0x1,%eax

0x400594 &: mov %eax,0x200a9e(%rip) # 0x601038 &

有訪存(當然可能是cacheline),送入寄存器,然後加入CPU計算單元計算,計算完成之後送入結果寄存器,最後寫存。中間有很多過程,如果多CPU執行,結果肯定不是預期的。


你是在線程方法裡面sleep,你在unsafe對象方法裡面sleep就可以看出不是安全了!


看題主的代碼,首先睡100毫秒,然後等一起運行,以誘發線程爭搶。

首先說說為什麼會有線程不安全的問題,在於每次value++,可以看作成三個步驟

1.get value

2.++operation

3.set value

在存在線程安全的代碼中上述過程斗可能被打斷,導致了可能值不是預期的那樣,

比如在T1得到值為2然後++的過程時,突然唄T2打斷,然後T2 全執行完,然後執行T1剩餘部分,這時實際++兩次,卻只有一次效果。這樣就有了不安全問題。實際過程中可能再任何時候被打斷,可能好好進函數就被打斷。

現在我們再說說為什麼觀察不到,或者說很滿觀察到的問題。題主這個例子肯定可以觀察到,不過這個發生條件比較苛刻,或者重複幾千次更多的運行這個例子,凡是存在線程安全問題的代碼,沒有觀察到並不代表沒問題。

觀察不到這個問題在於線程調度,在一段時間內,調度線程根據當前可用資源(CPU 是否忙於計算)給每個線程一段運行時間段,在這個時間段內某個線程執行其代碼,然後在下一個時間段分配給別的線程運行。時間段的大小根據線程優先順序個當前cpu的可用資源決定,所以當CPU 不是很繁忙的時候,所分配的時間段可以某線程完全執行完其代碼,但是當CPU 非常忙,時間段會小,那麼不能保證某個線程的代碼執行完,這裡的執行完意思是操作的原子性,所以當你寫的100個線程依據現在基本4核i5性能來說小意思,所以線程的爭搶並不嚴重。


public class UnsafeSequence {

private int value;

public UnsafeSequence(int value){
this.value = value;
}

/**返回一個唯一的數值*/
public int getValue(){
return value--;
}

public static void main(String[] args){
UnsafeSequence unsafeSequence = new UnsafeSequence(50);
for (int i = 0; i&<100;i++){ new Thread() { public void run() { try { if(unsafeSequence.getValue() &<= 0){ System.out.println("-----------"); }else{ System.out.println(Thread.currentThread().getName()+":"+unsafeSequence.getValue()); } }catch (Exception ex){ } } }.start(); } } }

運行結果:


樣本數量太少。


本人水平不高,只想問一下,操作共同數據只有value++這一行代碼,應該不會出現安全問題吧


推薦閱讀:

Linux的epoll使用LT+非阻塞IO和ET+非阻塞IO有效率上的區別嗎?
自己寫的 Tiny web server "Bad file descriptor"出錯?
多線程下epoll如何保證event.data.ptr不成為野指針?
Android的界面組件不能被子線程訪問是什麼意思呢?

TAG:Java | Java虛擬機JVM | Java編程 | 多線程 | 線程安全 |