前言
valatile 關(guān)鍵字用到的不多,不過(guò)在源碼中時(shí)常能夠看到,對(duì)其既熟悉又陌生,熟悉的是這個(gè)名字,陌生的它的用意和用法,那么我們就對(duì)其一探究竟。
內(nèi)存模型相關(guān)概念
計(jì)算機(jī)在執(zhí)行程序時(shí),每條指令都是在CPU中執(zhí)行的,過(guò)程中勢(shì)必會(huì)涉及到數(shù)據(jù)的讀取和寫入,而數(shù)據(jù)卻是存放在主存(物理內(nèi)存)當(dāng)中,CPU的速度特別快,但是內(nèi)存的讀取操作相對(duì)于CPU的運(yùn)算速度來(lái)說(shuō)很慢,如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過(guò)和內(nèi)存的交互來(lái)進(jìn)行,會(huì)大大降低指令執(zhí)行的速度。因此在CPU里就有了Cache高速緩存。
- CPU先把需要的數(shù)據(jù)從內(nèi)存中讀取復(fù)制一份到Cache
- CPU進(jìn)行計(jì)算的時(shí)候直接從Cache讀取數(shù)據(jù)和寫入數(shù)據(jù)
- 結(jié)束后將Cache緩存中的數(shù)據(jù)刷新到主存
多線程的問題
現(xiàn)如今我們的手機(jī)都是多核的,也就是說(shuō)同時(shí)有幾顆CPU在工作,每顆CPU都有自己的Cache高速緩存,這樣也就產(chǎn)生了一個(gè)問題:
- CPU1 和 CPU2 先將count變量讀到各自的Cache中,假設(shè)都為1
- CPU1 和 CPU2 分別開啟一個(gè)線程對(duì)count進(jìn)行自增操作,每個(gè)線程都有一塊獨(dú)立內(nèi)存空間存放count的副本,自增后的count副本都為2
- 自增完畢后,將結(jié)果副本寫回內(nèi)存,count等于2
進(jìn)行了兩次自增之后,寫回內(nèi)存的結(jié)果等于2,而自己想要的結(jié)果應(yīng)該是count=3,這就是多線程并發(fā)帶來(lái)的問題。
并發(fā)編程的三要素
1. 原子性
原子性,即一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
一個(gè)經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:
如果從賬戶A向賬戶B轉(zhuǎn)賬100元,那么必然要有兩個(gè)操作:
- 從賬戶A減去100元
- 往賬戶B加上100元
設(shè)想一個(gè),這兩個(gè)操作不具備原子性,如果從賬戶A減去100元之后,操作突然中止,這就會(huì)造成,賬戶A雖然減去了100元,但是賬戶B并沒有收到轉(zhuǎn)過(guò)來(lái)的100元。所以這兩個(gè)操作必須要具備原子性。
2. 可見性
可見性,指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看到修改的值
比如執(zhí)行線程A的是CPU1,執(zhí)行線程B的是CPU2,當(dāng)線程A去修改一個(gè)值 value = 5,會(huì)先把value的初始值加載到CPU1的Cache中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中value的值變?yōu)榱?0,卻沒有立即寫入到主存中。
而當(dāng)線程B去主存獲取 value 的值,并加載到線程B的高速緩存中,此時(shí)內(nèi)存當(dāng)中的value還是5,那么就會(huì)使得獲取到的值是5,而不是修改的值10。
線程A對(duì)變量value修改之后,線程B沒有立即看到線程A修改的值,這也就是可見性的問題。對(duì)于可見性,Java提供了 volatile
關(guān)鍵字來(lái)保證可見性,被volatile
修飾的共享變量,它將保證修改的值會(huì)立即更新到主存,當(dāng)有其他線程需要讀取的時(shí)候,也會(huì)去內(nèi)存讀取最新值。
3. 有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
JVM在真正執(zhí)行代碼的時(shí)候不一定會(huì)保證按照代碼的書寫順序進(jìn)行,可能會(huì)發(fā)生指令重排序。
指令重排序
一般來(lái)說(shuō),處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語(yǔ)句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。處理器在進(jìn)行重排序時(shí)會(huì)考慮指令之間的數(shù)據(jù)依賴性,如果兩個(gè)語(yǔ)句之間沒有數(shù)據(jù)依賴性,可能就會(huì)被重排序。
指令重排序不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。
volatile關(guān)鍵字
- volatile 保證可見性
- volatile 不能確保原子性
- volatile 保證有序性
普通的lock:
public class AtomTest {
int res = 0;
boolean lock = true;
private void setLock() {
res = 130;
lock = false;
}
private void readInfo() {
while (lock) {
};
System.out.println("res = " + res);
}
public static void main(String[] args) throws InterruptedException {
final AtomTest atom = new AtomTest();
Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
atom.readInfo();
}
});
Thread lockThread = new Thread(new Runnable() {
@Override
public void run() {
atom.setLock();
}
});
readThread.start();
TimeUnit.SECONDS.sleep(1);
lockThread.start();
readThread.join();
lockThread.join();
// join 方法使等待線程結(jié)果,才結(jié)束程序
System.out.println("end");
}
}
沒有被volatile
修飾的lock,修改的僅僅是副本的值,而沒有立馬寫入到內(nèi)存中去,使readThread一直在死循環(huán)中,程序也就無(wú)法結(jié)束。
volatile修飾的lock:
public class AtomTest {
int res = 0;
volatile boolean lock = true;
private void setLock() {
res = 130;
lock = false;
}
private void readInfo() {
while (lock) {
};
System.out.println("res = " + res);
}
public static void main(String[] args) throws InterruptedException {
final AtomTest atom = new AtomTest();
Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
atom.readInfo();
}
});
Thread lockThread = new Thread(new Runnable() {
@Override
public void run() {
atom.setLock();
}
});
readThread.start();
TimeUnit.SECONDS.sleep(1);
lockThread.start();
readThread.join();
lockThread.join();
// join 方法使等待線程結(jié)果,才結(jié)束程序
System.out.println("end");
}
}
打印結(jié)果:
res = 130
end
給lock加了volatile關(guān)鍵字修飾之后,當(dāng)lockThread對(duì)lock做了修改之后,會(huì)立即更新修改到內(nèi)存中,readThread發(fā)現(xiàn)副本已經(jīng)過(guò)期,重新從內(nèi)存獲取lock的新值,跳出循環(huán),執(zhí)行完成。