violate與線程安全

 

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關鍵字,或者使用鎖的機制,就能實現內存的可見性了。
 
 
 
 
 
 
代碼如下:
 
/**
 * volatile 關鍵字,使一個變量在多個線程間可見
 * A B線程都用到一個變量,java默認是A線程中保留一份copy,這樣如果B線程修改了該變量,則A線程未必知道
 * 使用volatile關鍵字,會讓所有線程都會讀到變量的修改值
 *
 * 在下面的代碼中,running是存在於堆內存的t對象中
 * 當線程t1開始運行的時候,會把running值從內存中讀到t1線程的工作區,在運行過程中直接使用這個copy,並不會每次都去
 * 讀取堆內存,這樣,當主線程修改running的值之後,t1線程感知不到,所以不會停止運行
 *
 * 使用volatile,將會強制所有線程都去堆內存中讀取running的值
 *
 * 可以閱讀這篇文章進行更深入的理解
 *
 * volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,也就是說volatile不能替代synchronized
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.concurrent.TimeUnit;
 
public class T01_HelloVolatile {
/*volatile*/ boolean running = true; //對比一下有無volatile的情況下,整個程序運行結果的區別
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();
 
new Thread(t::m, "t1").start();
 
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
 
t.running = false;
}
}
 
 
 
在把running的violate的去掉的時候,發現running不可見,結果如下:
 
使用violate之後的結果如下:
 
 
 
2DCL(Double Check Lock)
public class Singleton {
    Private  violate static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {    // 線程二檢測到instance不爲空
            synchronized (Singleton.class) {
                if(null == instance) {
                    instance = new Singleton();    // 線程一被指令重排,先執行了賦值,但還沒執行完構造函數(即未完成初始化)
                }
            }
        }
 
        return instance;    // 後面線程二執行時將引發:對象尚未初始化錯誤
 
    }
}
 
使用violate的原因:防止synchronized指令重排序,詳見https://blog.csdn.net/zhouzhou_98/article/details/106259552
 
 
violate具有防止指令重排,但不具備原子性
/**
 * volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,也就是說volatile不能替代synchronized
 * 運行下面的程序,並分析結果
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.ArrayList;
import java.util.List;
 
public class T04_VolatileNotSync {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}
public static void main(String[] args) {
T04_VolatileNotSync t = new T04_VolatileNotSync();
 
List<Thread> threads = new ArrayList<Thread>();
 
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
 
threads.forEach((o)->o.start());
 
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
 
}
}
 
 
結果並沒有等於10萬
 
原因violate不具備原子性,存在多個線程使用一樣的值進行++操作,解決辦法使用synchronized
 
 
重新修改的代碼:
/**
 * 對比上一個程序,可以用synchronized解決,synchronized可以保證可見性和原子性,volatile只能保證可見性
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.ArrayList;
import java.util.List;
 
 
public class T05_VolatileVsSync {
/*volatile*/ int count = 0;
 
synchronized void m() {
for (int i = 0; i < 10000; i++)
count++;
}
 
public static void main(String[] args) {
T05_VolatileVsSync t = new T05_VolatileVsSync();
 
List<Thread> threads = new ArrayList<Thread>();
 
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
 
threads.forEach((o) -> o.start());
 
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
}
 
}
 
 
 
2 CAS
 
/**
 * 解決同樣的問題的更高效的方法,使用AtomXXX類
 * AtomXXX類本身方法都是原子性的,但不能保證多個方法連續調用是原子性的
 * @author mashibing
 */
package com.mashibing.juc.c_018_00_AtomicXXX;
 
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
 
 
public class T01_AtomicInteger {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
 
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++)
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
 
public static void main(String[] args) {
T01_AtomicInteger t = new T01_AtomicInteger();
 
List<Thread> threads = new ArrayList<Thread>();
 
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
 
threads.forEach((o) -> o.start());
 
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
}
 
}
 
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問題目前採用的策略。
 
經典的CAS問題:
小明在提款機,提取了50元,因爲提款機問題,
有兩個線程,同時把餘額從100變爲50
線程1(提款機):獲取當前值100,期望更新爲50,
線程2(提款機):獲取當前值100,期望更新爲50,
線程1成功執行,線程2某種原因block了,這時,某人給小明匯款50
 
線程3(默認):獲取當前值50,期望更新爲100,
 
這時候線程3成功執行,餘額變爲100,
 
線程2從Block中恢復,獲取到的也是100,compare之後,繼續更新餘額爲50!!!
 
此時可以看到,實際餘額應該爲100(100-50+50),但是實際上變爲了50(100-50+50-50)
這就是ABA問題帶來的成功提交。
 
 
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章