volatile是java虛擬機提供的一種輕量級的同步機制,那么volatile到底是怎么實現(xiàn)輕量級同步的?
可見性
什么是可見性?這個得從java內(nèi)存模型說起
java內(nèi)存模型
JMM把內(nèi)存條的內(nèi)存定義為主存,CPU的高速緩存定義為工作內(nèi)存,線程在運算前會把主存的共享數(shù)據(jù)拷貝一份副本到自己的工作內(nèi)存,完成運算后再把數(shù)據(jù)更新到主存,但每個線程間的工作內(nèi)存的數(shù)據(jù)是不共享的,也就是線程1修改了數(shù)據(jù)再回寫到主存,線程2是不知道的
代碼說話
public class VolatileVisibilityTest {
public static void main(String[] args) {
Data data = new Data();
//線程T更新number的值
new Thread(() -> {
try {
//先睡3秒棺滞,讓main線程讀取到number的原始值
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setNumber(100);
System.out.println(Thread.currentThread().getName() + ": 修改number的值為" + data.getNumber());
}, "T").start();
//線程main讀取number的值,如果線程main能感知到線程T修改了number的值,將會結(jié)束循環(huán),否則一直在死循環(huán)中
while (data.getNumber() == 0) {}
System.out.println(Thread.currentThread().getName() + ": 結(jié)束循環(huán),number的值已變?yōu)? + data.getNumber());
}
}
class Data {
private /*volatile*/ int number = 0;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
大家試著運行一下volatile注釋前和注釋后的區(qū)別
在注釋volatile前,main線程是能感知到T線程對number的值做出了修改的
在注釋volatile后,main線程無感知T線程對number的值做出的修改,一直在循環(huán)中
不保證原子性
注意,volatile是輕量級的同步機制,所以不保證原子性,先上代碼
public class VolatileAtomicityTest {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
//開啟20個線程,每條線程執(zhí)行1000次number++,理論上最后的結(jié)果應該是20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.increment();
}
}, "Thread--" + i).start();
}
//當只有main線程和GC線程存活才結(jié)束循環(huán),否則放棄執(zhí)行權(quán)
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(data.getNumber());
}
}
public class Data {
private volatile int number = 0;
public void increment() {
number++;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
這段代碼執(zhí)行多次,只有偶爾的結(jié)果是20000,其他結(jié)果都是小于20000.但是若是給increment方法加上synchronized同步鎖,則執(zhí)行多次結(jié)果都是20000,這證明volatile不保證原子性
我們都知道number++不是原子性的,對應的字節(jié)碼如下:
getfield //從主存拷貝到工作內(nèi)存
iadd //自增
putfield //回寫到主存
假設(shè)主存內(nèi)的number=0,當線程1執(zhí)行了字節(jié)碼getfield和iadd后,準備執(zhí)行putfield時被掛起,線程2執(zhí)行g(shù)etfield得到number的值仍然為0,并執(zhí)行iadd,這個時候切又回線程1,雖然number加了volatile關(guān)鍵字,線程2修改了number的值線程1是能感知到的,但線程1已經(jīng)開始執(zhí)行putfield了,所以回寫主存number=1,線程2也回寫主存number=1,這時候就發(fā)生了寫丟失.
所以單靠volatile是無法保證原子性的
有序性
先來說說啥是指令重排,編譯器和虛擬機會重新排列無數(shù)據(jù)依賴的語句來優(yōu)化程序,也就是說,你寫的代碼虛擬機不一定是按順序執(zhí)行的
public class TestRearrangement {
private int a = 0;
private boolean flag = false;
public void method1() {
a = 1; //語句1
flag = true; //語句2
}
public void method2() {
if (flag) {
a = a + 5;
System.out.println("a=" + a);
}
}
}
代碼中的語句1和語句2,因為它們之間沒有數(shù)據(jù)依賴,在指令重排時,很有可能是先執(zhí)行語句2再執(zhí)行語句1.這在單線程環(huán)境下沒毛病,但在多線程環(huán)境下,語句1和語句2的順序很可能會影響method2方法執(zhí)行的結(jié)果.例如線程1先執(zhí)行語句2然后掛起,線程2執(zhí)行method2,那么a的值就會是5,如果線程1先執(zhí)行語句1,那么線程2執(zhí)行完method2,a的值為6
若給a和flag都加上volatile關(guān)鍵字,編譯器就不會對相關(guān)代碼進行重排優(yōu)化
應用場景
懶漢式單例
首先來看看懶漢式單例的寫法
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { //第6行
synchronized (Singleton.class) {
if (instance == null) { //第8行
instance = new Singleton();
}
}
}
return instance;
}
}
為什么加了synchronized還要加volatile呢?關(guān)鍵點在于instance = new Singleton()這行代碼
instance = new Singleton();
// 可以分解為以下三個步驟
1 memory=allocate();// 分配內(nèi)存 相當于c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設(shè)置s指向剛分配的地址
// 上述三個步驟可能會被重排序為 1-3-2妆档,也就是:
1 memory=allocate();// 分配內(nèi)存 相當于c的malloc
3 s=memory //設(shè)置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象
因為synchronized不能保證有序,所以instance = new Singleton()的指令有可能會被重新排序,當重新排序為1-3-2,線程1執(zhí)行完指令3時,instance已經(jīng)被分配了內(nèi)存地址,所以instance!=null;這個時候線程1掛起,線程2訪問到第6行代碼if (instance == null),因為instance!=null所以直接return instance,但此時instance對象還沒創(chuàng)建好,因為線程1的指令2還沒執(zhí)行完,所以此時的instance只是具有內(nèi)存地址但卻是空對象
所以,單例的懶漢式寫法要加上volatile關(guān)鍵字