可見性
- 可見性: 一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到.
- 共享變量: 如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這幾個(gè)線程的共享變量.
Java 內(nèi)存模型(JMM)
Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié).
所有變量都存儲(chǔ)在主內(nèi)存中
每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程使用到的變量副本(主內(nèi)存中該變量的拷貝)
線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行,不能直接從主內(nèi)存中讀寫.
不同線程池之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量,線程間的變量值傳遞需要主內(nèi)存來(lái)完成.
共享變量可見性實(shí)現(xiàn)的原理
線程1對(duì)共享變量的修改要想被線程2及時(shí)看到,必須要經(jīng)過(guò)如下2步驟:
- 把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中,
- 將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中.
Synchronized實(shí)現(xiàn)可見性:
Java語(yǔ)言層面支持的可見性實(shí)現(xiàn)方式:
- synchronized
- volatile
JMM關(guān)于synchronized的兩條規(guī)定:
- 線程解鎖前,必須要共享變量的最新值刷新到主內(nèi)存中,
- 線程加鎖前,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值.
兩條規(guī)定能保證,線程解鎖前對(duì)共享變量的修改在下次加鎖時(shí)對(duì)其他線程的可見性.
線程執(zhí)行互斥代碼的過(guò)程:
- 獲取互斥鎖
- 清空工作內(nèi)存
- 從主內(nèi)存中拷貝變量的最新副本到工作內(nèi)存
- 執(zhí)行代碼
- 將更改后的共享變量值刷新到主內(nèi)存中.
- 釋放互斥鎖.
指令重排序:代碼書寫的順序與實(shí)際執(zhí)行的順序不同,指令重排序是編譯器或處理為了提高程序性能而做的優(yōu)化,分為三種:
- 編譯器優(yōu)化的重排序
- 指令級(jí)并行重排序
- 內(nèi)存系統(tǒng)的重排序
as-if-serial:無(wú)論如何重排序,程序執(zhí)行的結(jié)果都應(yīng)該與代碼順序執(zhí)行的結(jié)果一致(Java編譯器,運(yùn)行時(shí)和處理器都會(huì)保證java在單線程下遵循as-if-serial語(yǔ)義).
重排序不會(huì)給單線程帶來(lái)內(nèi)存可見性的問(wèn)題,但是在多線程中程序交錯(cuò)執(zhí)行時(shí),重排序可能會(huì)造成內(nèi)存可見性的問(wèn)題.
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
System.out.println(number);
}
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
上面的程序可能會(huì)一直運(yùn)行下去,因?yàn)榫€程可能永遠(yuǎn)讀取不到ready的值.也可能輸出為0,因?yàn)榫€程可能看到寫入了ready的值,但是卻沒(méi)有看到number之后寫入的值,這種現(xiàn)象稱為"重排序".
Volatile實(shí)現(xiàn)可見性:
volatile關(guān)鍵字:
- 能夠保證volatile變量的可見性
- 不能保證volatile變量復(fù)合操作的原子性
volatile如何實(shí)現(xiàn)內(nèi)存可見性:
深入來(lái)說(shuō): 通過(guò)加入內(nèi)存屏障(8條)和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn).
- 對(duì)volatile變量執(zhí)行寫操作時(shí),會(huì)在操作后加入一條store屏障指令(強(qiáng)制將變量值刷新到主內(nèi)存中去)
- 對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在操作前加入一條load屏障指令(強(qiáng)制從主內(nèi)存中讀取變量的值)
線程寫volatile變量的過(guò)程:
- 改變線程工作內(nèi)存中的volatile變量副本的值
- 將改變后副本值從工作內(nèi)存刷新到主內(nèi)存中
線程讀volatile變量的過(guò)程
- 從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
- 從工作內(nèi)存中讀取volatile變量的副本
要在多線程中安全使用volatile變量,必須滿足:
- 對(duì)變量的寫入操作不能依賴當(dāng)前值,或者你能確保只有一個(gè)單線程更新變量的值.
- 該變量不會(huì)與其他狀態(tài)變量一起納入不變性條件中
- 在訪問(wèn)變量時(shí)不需要加鎖.
public class VolatileDemo {
private volatile int number = 0;
public int getNumber() {
return number;
}
public void increase() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.number++; //非原子操作導(dǎo)致結(jié)果可能不為500
}
public static void main(String... args) {
final VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 0; i < 500; i++) {
new Thread(new Runnable() {
@Override
public void run() {
volatileDemo.increase();
}
}).start();
}
//主線程給子線程讓出資源
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("number:" + volatileDemo.getNumber());
}
}
想要保證this.number++的原子性操作,有三種方式:
- synchronized同步關(guān)鍵字
- Lock
- AotomicInteger