1 violate
線程可見性:
可見性是值一個線程對共享變量的修改,對於另一個線程來說是否是可以看到的。
爲什麼會出現這種問題呢?
我們知道,java線程通信是通過共享內存的方式進行通信的,而我們又知道,爲了加快執行的速度,線程一般是不會直接操作內存的,而是操作緩存。
java線程內存模型:
實際上,線程操作的是自己的工作內存,而不會直接操作主內存。如果線程對變量的操作沒有刷寫會主內存的話,僅僅改變了自己的工作內存的變量的副本,那麼對於其他線程來說是不可見的。而如果另一個變量沒有讀取主內存中的新的值,而是使用舊的值的話,同樣的也可以列爲不可見。
對於jvm來說,主內存是所有線程共享的java堆,而工作內存中的共享變量的副本是從主內存拷貝過去的,是線程私有的局部變量,位於java棧中。
那麼我們怎麼知道什麼時候工作內存的變量會刷寫到主內存當中呢?
這就涉及到java的happens-before關係了。
在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。
簡單來說,只要滿足了happens-before關係,那麼他們就是可見的。
例如:
線程A中執行i=1,線程B中執行j=i。如果線程A的操作和線程B的操作滿足happens-before關係,那麼j就一定等於1,否則j的值就是不確定的。
happens-before關係如下:
1.程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
2.鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;
3.volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作;
4.傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
5.線程啓動規則:Thread對象的start()方法先行發生於此線程的每個一個動作;
6.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
7.線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
從上面的happens-before規則,顯然,一般只需要使用volatile關鍵字,或者使用鎖的機制,就能實現內存的可見性了。
代碼如下:
在把running的violate的去掉的時候,發現running不可見,結果如下:
使用violate之後的結果如下:
2DCL(Double Check Lock)
使用violate的原因:防止synchronized指令重排序,詳見https://blog.csdn.net/zhouzhou_98/article/details/106259552
violate具有防止指令重排,但不具備原子性
2 CAS
AtomicXXX的原理是基於CAS,CAS(Compare-and-Swap),即比較並替換,是一種實現併發算法時常用到的技術。CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。
CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。(也稱作樂觀鎖)
CAS最大的問題在於ABA問題:
ABA:如果另一個線程修改V值假設原來是A,先修改成B,再修改回成A。當前線程的CAS操作無法分辨當前V值是否發生過變化。
關於ABA問題我想了一個例子:在你非常渴的情況下你發現一個盛滿水的杯子,你一飲而盡。之後再給杯子裏重新倒滿水。然後你離開,當杯子的真正主人回來時看到杯子還是盛滿水,他當然不知道是否被人喝完重新倒滿。解決這個問題的方案的一個策略是每一次倒水假設有一個自動記錄儀記錄下,這樣主人回來就可以分辨在她離開後是否發生過重新倒滿的情況。這也是解決ABA問題目前採用的策略。
|