前言
volatile的使用與線程安全關係密切,主要作用是使變量在多個線程間可見,另外也有防止指令重排的作用。
比如主內存中有變量a=0,線程1設置a=10,線程2再操作a的時候,是以a=10的基礎上進行操作,否則會影響邏輯!
volatile的可見性
要了解volatile的可見性,首先得了解java內存模型:
java內存模型
Java內存模型由Java虛擬機規範定義,用來屏蔽各個平臺的硬件差異。簡單來說:
1. 所有變量儲存在主內存。
2. 每條線程擁有自己的工作內存,其中保存了主內存中線程使用到的變量的副本。
3. 線程不能直接讀寫主內存中的變量,所有操作均在工作內存中完成。
線程,主內存,工作內存的交互關係如下圖所示
如下列代碼所示,rt啓動之後修改isRunning的值爲false,此時while循環不會停止,因爲run方法裏得不到改變之後的isRunning。
解決:使用volatile修飾isRunning,這樣當isRunning的值改變之後,會立即刷新到主內存裏,工作內存也能立即獲取到新的值
public class RunThread extends Thread {
private boolean isRunning = true;
private void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run () {
System.out.println("進入run方法");
while(isRunning == true){
//...
}
System.out.println("線程停止");
}
public static void main(String[] args) {
RunThread rt = new RunThread();
rt.start();
try {
Thread.sleep(3000);
rt.setRunning(false);
System.out.println("isRunning的值已經被設置成false");
Thread.sleep(1000);
System.out.println(rt.isRunning);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
volatile能防止指令重排
如下列代碼所示,這是單例模式的雙檢鎖寫法
public class SingletonTest {
private volatile static SingletonTest instance = null;
private SingletonTest() { }
public static SingletonTest getInstance() {
if(instance == null) {
synchronized (SingletonTest.class){
if(instance == null) {
instance = new SingletonTest(); //非原子操作
}
}
}
return instance;
}
}
我們看到instance用了volatile修飾,由於 instance = new SingletonTest();可分解爲:
1.memory =allocate(); //分配對象的內存空間
2.ctorInstance(memory); //初始化對象
3.instance =memory; //設置instance指向剛分配的內存地址
操作2依賴1,但是操作3不依賴2,所以有可能出現1,3,2的順序,當出現這種順序的時候,雖然instance不爲空,但是對象也有可能沒有正確初始化,會出錯。
而使用volatile修飾instance之後,不會出現亂序的行爲!
volatile不保證原子性以及解決方式
1.什麼是原子性?
下列語句中,哪些是原子性操作?
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
語句1 是直接將數值 10 賦值給 x,也就是說線程執行這個語句的會直接將數值 10 寫入到工作內存中;
語句2 實際上包含兩個操作,它先要去讀取 x 的值,再將 x 的值寫入工作內存。雖然,讀取 x 的值以及 將 x 的值寫入工作內存這兩個操作都是原子性操作,但是合起來就不是原子性操作了;
同樣的,x++ 和 x = x+1 包括3個操作:讀取 x 的值,進行加 1 操作,寫入新的值。
只有 語句1 的操作具備原子性。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)纔是原子操作!
2.舉例
如下列代碼所示,使用volatile修飾的變量count,利用10個線程分別對count進行++操作,而根據上面的表述,++操作不是原子操作!每個線程加1000個數,打印的結果中一定有一個是10000纔是對的,但是實際上並不是這樣!因爲volatile不保證原子性!
public class VolatileNoAtomic extends Thread{
private static volatile int count;
// private static AtomicInteger count = new AtomicInteger(0);
private static void addCount(){
for (int i = 0; i < 1000; i++) {
count ++;
// count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < arr.length; i++) {
arr[i].start();
}
}
}
解決:
方法1:使用原子類Atomic類的系列對象,這樣既不會阻塞,又能保證原子性!
方法2:使用synchronized修飾addCount方法,這樣做的話,線程同步之後會有阻塞,運行時間加長,而且volatile將會失效,不建議這麼改
方法3:使用Lock加鎖,當然,跟方法2一樣的有阻塞