如果volatile的修飾的是一個引用類型的對象變量,那么對象中定義的一些普通全局變量是否會受到volatile關(guān)鍵字的效果影響呢毛萌?”
接下來,我們就一起來分析下這個問題!讓我們先通過一個例子來回顧下volatile關(guān)鍵字的作用喝滞!
public class VolatitleFoo {
//類變量
final static int max = 5;
static int init_value = 0;
public static void main(String args[]) {
//啟動一個線程朝聋,當(dāng)發(fā)現(xiàn)local_value與init_value不同時,則輸出init_value被修改的值
new Thread(() -> {
int localValue = init_value;
while (localValue < max) {
if (init_value != localValue) {
System.out.printf("The init_value is update ot [%d]\n", init_value);
//對localValue進(jìn)行重新賦值
localValue = init_value;
}
}
}, "Reader").start();
//啟動updater線程囤躁,主要用于對init_value的修改,當(dāng)local_value=5的時候退出生命周期
new Thread(() -> {
int localValue = init_value;
while (localValue < max) {
//修改init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
在上面的代碼示例中荔睹,我們定義了兩個類變量max狸演、init_value,然后在主線程中分別啟動一個Reader線程僻他,一個Updater線程宵距。Updater線程做的事情就是在值小于max的值時以每兩毫秒的速度進(jìn)行自增。而Reader線程則是在感知init_value值發(fā)生變化的情況下進(jìn)行讀取操作吨拗。
期望的效果是線程Updater更新init_value值之后满哪,可以立刻被線程Reader感知到婿斥,從而進(jìn)行輸出顯示。實際運(yùn)行效果如下:
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
實際的運(yùn)行效果是在Updater修改類變量init_value后哨鸭,Reader線程并沒有立馬感知到變化民宿,所以沒有進(jìn)行相應(yīng)的顯示輸出。而原因就在于共享類變量init_value在被線程Updater拷貝到該線程的工作內(nèi)存中后像鸡,Updater對變量init_value的修改都是在工作內(nèi)存中進(jìn)行的活鹰,完成操作后沒有立刻同步回主內(nèi)存,所以Reader線程對其改變并不可見只估。
為了解決線程間對類變量init_value的可見性問題志群,我們將類變量init_value用volatile關(guān)鍵字進(jìn)行下修飾,如下:
static volatile int init_value = 0;
然后我們再運(yùn)行下代碼蛔钙,看看結(jié)果:
The init_value will be changed to [1]
The init_value is update ot [1]
The init_value will be changed to [2]
The init_value is update ot [2]
The init_value will be changed to [3]
The init_value is update ot [3]
The init_value will be changed to [4]
The init_value is update ot [4]
The init_value will be changed to [5]
The init_value is update ot [5]
此時線程Updater對類變量init_value的修改锌云,立馬就能被Reader線程感知到了,這就是volatile關(guān)鍵字的效果吁脱,可以讓共享變量在線程間實現(xiàn)可見桑涎,原因就在于在JVM的語義層面要求被volatile修飾的共享變量,在工作內(nèi)存中的修改要立刻同步回主內(nèi)存豫喧,并且讀取也需要每次都重新從主內(nèi)存中刷新一份到工作內(nèi)存中后才可以操作石洗。
關(guān)于以上適用volatile關(guān)鍵字修飾基本類型的類變量、實例變量的場景紧显,相信大家會比較好理解讲衫。接下來,我們來繼續(xù)改造下代碼:
public class VolatileEntity {
//使用volatile修飾共享資源i
//類變量
final static int max = 5;
int init_value = 0;
public static int getMax() {
return max;
}
public int getInit_value() {
return init_value;
}
public void setInit_value(int init_value) {
this.init_value = init_value;
}
private static class VolatileEntityHolder {
private static VolatileEntity instance = new VolatileEntity();
}
public static VolatileEntity getInstance() {
return VolatileEntityHolder.instance;
}
}
我們將之前代碼中的類變量init_value放到實體類VolatileEntity中孵班,并將其設(shè)計為一個實例變量涉兽,為了便于理解,我們將實體類VolatileEntity設(shè)計為單例模式篙程,確保兩個線程操作的是同一個共享堆內(nèi)存對象枷畏。如下:
public class VolatileEntityTest {
//使用volatile修飾共享資源
private static VolatileEntity volatileEntity = VolatileEntity.getInstance();
private static final CountDownLatch latch = new CountDownLatch(10);
public static void main(String args[]) throws InterruptedException {
//啟動一個線程,當(dāng)發(fā)現(xiàn)local_value與init_value不同時虱饿,則輸出init_value被修改的值
new Thread(() -> {
int localValue = volatileEntity.init_value;
while (localValue < VolatileEntity.max) {
if (volatileEntity.init_value != localValue) {
System.out.printf("The init_value is update ot [%d]\n", volatileEntity.init_value);
//對localValue進(jìn)行重新賦值
localValue = volatileEntity.init_value;
}
}
}, "Reader").start();
//啟動updater線程拥诡,主要用于對init_value的修改,當(dāng)local_value=5的時候退出生命周期
new Thread(() -> {
int localValue = volatileEntity.init_value;
while (localValue < VolatileEntity.max) {
//修改init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
volatileEntity.init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
在上述代碼中線程Updater和Reader此時操作的是類變量VolatileEntity對象中的普通實例變量init_value氮发。在VolatileEntity對象沒被volatile關(guān)鍵字修飾之前渴肉,我們看下運(yùn)行效果:
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
與未被volatile修飾的int類型的類變量效果一樣,線程Updater對VolatileEntity對象中init_value變量的操作也不能立馬被線程Reader可見爽冕。如果此時我們不VolatileEntity類中單獨用volatile關(guān)鍵字修飾init_value變量仇祭,而是直接VolatileEntity對象用volatile關(guān)鍵字修飾,效果會如何呢颈畸?
private static volatile VolatileEntity volatileEntity = VolatileEntity.getInstance();
此時VolatileEntity對象的引用變量被volatile關(guān)鍵字修飾了乌奇,然而其中的普通實例變量init_value并沒有直接被volatile關(guān)鍵字修飾没讲,然后我們在運(yùn)行下代碼看看效果:
The init_value will be changed to [1]
The init_value is update ot [1]
The init_value will be changed to [2]
The init_value is update ot [2]
The init_value will be changed to [3]
The init_value is update ot [3]
The init_value will be changed to [4]
The init_value is update ot [4]
The init_value will be changed to [5]
The init_value is update ot [5]
從實際的運(yùn)行效果上看,雖然我們沒有直接用volatile關(guān)鍵字修飾對象中的類變量init_value礁苗,而是修改了對象的引用爬凑,但是我們看到對象中的普通實例變量仍然實行了線程間的可見性,也就是說間接也相當(dāng)于被volatile關(guān)鍵字修飾了寂屏。所以贰谣,在這里問題也就基本上有了答案,那就是:“被volatile關(guān)鍵字修飾的對象作為類變量或?qū)嵗兞繒r迁霎,其對象中攜帶的類變量和實例變量也相當(dāng)于被volatile關(guān)鍵字修飾了”吱抚。
這個問題主要是考查大家對volatile關(guān)鍵字的理解是否深入,另外也是對Java數(shù)據(jù)存儲結(jié)構(gòu)的考查考廉,雖然可能大家對volatile關(guān)鍵字的作用會有了解秘豹,但是如果突然被問到這樣的問題,如果不加以思考昌粤,在面試中也是很容易被問懵的既绕!