可見(jiàn)性介紹
可見(jiàn)性:一個(gè)線程對(duì)共享變量的值的修改宙刘,能夠及時(shí)地被其他線程看到。
共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本杠览,那么這個(gè)變量就是這幾個(gè)線程的共享變量写烤。
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)存中的變量胚想,線程間變量值的傳遞需要通過(guò)主內(nèi)存來(lái)完成
共享變量可見(jiàn)性實(shí)現(xiàn)的原理
線程1對(duì)共享變量的修改要想被線程2及時(shí)看到琐凭,必須要經(jīng)過(guò)如下2個(gè)步驟:
- 把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中
- 將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中
練習(xí)題:
A、通過(guò)synchronized和volatile都可以實(shí)現(xiàn)可見(jiàn)性 √
B浊服、不同線程之間可以直接訪問(wèn)其他線程工作內(nèi)存中的變量 ×
C统屈、線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行 √
D、所有的變量都存儲(chǔ)在主內(nèi)存中 √
要實(shí)現(xiàn)共享變量的可見(jiàn)性牙躺,必須保證兩點(diǎn):
- 線程修改后的共享變量值能夠及時(shí)從工作內(nèi)存刷新主內(nèi)存中
- 其他線程能夠及時(shí)把共享變量的最新值從主內(nèi)存更新到自己的工作內(nèi)存中
Java語(yǔ)言層面支持的可見(jiàn)性實(shí)現(xiàn)方式:
- synchronized
- volatile
synchronized能夠?qū)崿F(xiàn)
- 原子性(通過(guò)同步實(shí)現(xiàn))
- 內(nèi)存可見(jiàn)性
JMM關(guān)于synchronized的兩條規(guī)定:
- 線程解鎖前愁憔,必須把共享變量的最新值刷新到主內(nèi)存中
- 線程加鎖時(shí),將清空工作內(nèi)存中共享變量的值孽拷,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)
線程解鎖前對(duì)共享變量的修改在下一次加鎖時(shí)對(duì)其他線程可見(jiàn)
線程執(zhí)行互斥代碼的過(guò)程:
- 1吨掌、獲得互斥鎖
- 2、清空工作內(nèi)存
- 3脓恕、從主內(nèi)存拷貝變量的最新副本到工作內(nèi)存
- 4膜宋、執(zhí)行代碼
- 5、將更改后的共享變量的值刷新到主內(nèi)存
- 6炼幔、釋放互斥鎖
重排序:代碼書(shū)寫(xiě)的順序與實(shí)際執(zhí)行的順序不同秋茫,指令重排序是編譯器或處理器為了提高程序性能而做的優(yōu)化
1、編譯器優(yōu)化的重排序(編譯器優(yōu)化)
2乃秀、指令集并行重排序(處理器優(yōu)化)
3肛著、內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)
as-if-serial:無(wú)論如何重排序,程序執(zhí)行的結(jié)果應(yīng)該與代碼順序執(zhí)行的結(jié)果一致(Java編譯器跺讯、運(yùn)行時(shí)和處理器都會(huì)保證Java在單線程下遵循as-if-serial語(yǔ)義)
int num1 = 1 ; // 第1行代碼
int num2 = 2 ; // 第2行代碼
int sum = num1 + num2 ; // 第3行代碼
單線程:第1枢贿、2行的順序可以重排,但第3行不能
重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題
多線程中程序交錯(cuò)執(zhí)行時(shí)抬吟,重排序可能會(huì)造成內(nèi)存可見(jiàn)性問(wèn)題
public class SynchronizedDemo {
// 共享變量
private boolean ready = false;
private int result = 0;
private int number = 1;
// 寫(xiě)操作
public void write() {
ready = true; // 1.1
number = 2; // 1.2
}
// 讀操作
public void read() {
if (ready) { // 2.1
result = number * 3; // 2.2
}
System.out.println("result的值為:" + result);
}
// 內(nèi)部線程類(lèi)
private class ReadWriteThread extends Thread {
// 根據(jù)構(gòu)造方法中傳入的flag參數(shù)萨咕,確定線程執(zhí)行讀操作還是寫(xiě)操作
private boolean flag;
public ReadWriteThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
// 構(gòu)造方法中傳入true,執(zhí)行寫(xiě)操作
write();
} else {
// 構(gòu)造方法中傳入false火本,執(zhí)行讀操作
read();
}
}
}
public static void main(String[] args) {
// 啟動(dòng)線程執(zhí)行寫(xiě)操作
SynchronizedDemo synDemo = new SynchronizedDemo();
synDemo.new ReadWriteThread(true).start();
// 程序并不牽涉線程交叉執(zhí)行的問(wèn)題危队,加入synchronized關(guān)鍵詞result也有可能為0,加入休眠操作钙畔,等主線程蘇醒茫陆,寫(xiě)線程基本執(zhí)行完,所以主線程繼續(xù)往下執(zhí)行擎析,啟動(dòng)讀線程簿盅』酉拢基本可以保證結(jié)果為6。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 啟動(dòng)線程執(zhí)行讀操作
synDemo.new ReadWriteThread(false).start();
}
}
結(jié)果可能出現(xiàn):
result的值為:0
result的值為:6
導(dǎo)致共享變量在線程間不可見(jiàn)的原因:
1桨醋、線程的交叉執(zhí)行
2棚瘟、重排序結(jié)合線程交叉執(zhí)行
3、共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新
synchronized解決方案:
1喜最、原子性保證:線程的交叉執(zhí)行
2偎蘸、原子性保證:重排序結(jié)合線程交叉執(zhí)行
3、可見(jiàn)性保證:共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新
練習(xí)題:
A瞬内、當(dāng)兩個(gè)并發(fā)線程訪問(wèn)同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí)迷雪,一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行〕娴√
B章咧、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問(wèn)該object中的非synchronized(this)同步代碼塊能真。 √
C赁严、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問(wèn)不會(huì)被阻塞粉铐。 ×
D误澳、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖秦躯。結(jié)果忆谓,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問(wèn)都被暫時(shí)阻塞。 √
volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性:
深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的踱承。
- 對(duì)volatile變量執(zhí)行寫(xiě)操作時(shí)倡缠,會(huì)在寫(xiě)操作后加入一條store屏障指令
- 對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在寫(xiě)操作前加入一條load屏障指令
通俗的講:volatile變量在每次被線程訪問(wèn)時(shí)茎活,都強(qiáng)迫從主內(nèi)存中重讀該變量的值昙沦,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫線程將最新的值刷新到主內(nèi)存载荔。這樣任何時(shí)刻盾饮,不同的線程總能看到該變量的最新值。
線程寫(xiě)volatile變量的過(guò)程:
1懒熙、改變線程工作內(nèi)存中volatile變量副本的值
2丘损、將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存中
線程讀volatile變量的過(guò)程:
1、從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
2工扎、從工作內(nèi)存中讀取volatile變量的副本
volatile不能保證volatile變量復(fù)合操作的原子性:
private int number = 0 ;
number ++ ; // 不是原子性
1徘钥、 讀取number的值
2、 將number的值加1
3肢娘、 寫(xiě)入最新的number的值
synchronized(this){
number ++ ;
}
加入synchronized呈础,變?yōu)樵硬僮?/p>
private volatile int number = 0 ;
變?yōu)関olatile變量舆驶,無(wú)法保證原子性
public class VolatileDemo {
private volatile int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100); // 更多出現(xiàn)小于500的現(xiàn)象
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
number++;
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果還有子線程在運(yùn)行,主線程就讓出CPU資源而钞,直到所有的子線程都運(yùn)行完了沙廉,主線程再繼續(xù)往下執(zhí)行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
結(jié)果出現(xiàn):number:494
synchronized實(shí)現(xiàn)number變量的原子性
public class VolatileDemo {
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100); // 更多出現(xiàn)小于500的現(xiàn)象
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this) { // 放在increase()方法前,由于休眠臼节,程序可能需要執(zhí)行很長(zhǎng)時(shí)間蓝仲,這里縮小鎖粒度,則利用此寫(xiě)法synchronized不僅保證了number變量的可見(jiàn)性官疲,還保證了number++的原子性
number++;
}
}
/**
* @param args
*/
public static void main(String[] args) {
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
ReentrantLock實(shí)現(xiàn)number變量在線程中的原子性
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VolatileDemo {
private Lock lock = new ReentrantLock(); // 用ReentrantLock實(shí)現(xiàn)number變量在線程中的原子性
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100); // 更多出現(xiàn)小于500的現(xiàn)象
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock(); // 加鎖 - 相當(dāng)于進(jìn)入synchronized代碼塊
try {
this.number++;
} finally { // 鎖內(nèi)部操作可能會(huì)拋出異常,為保證鎖一定被釋放
lock.unlock(); // 釋放鎖 - 相當(dāng)于退出synchronized代碼塊
}
}
/**
* @param args
*/
public static void main(String[] args) {
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
volatile適用場(chǎng)合:
要在多線程中安全的使用volatile變量亮隙,必須同時(shí)滿足:
1途凫、對(duì)變量的寫(xiě)入操作不依賴(lài)其當(dāng)前值
- 不滿足:number++ 、 count = count * 5
- 滿足:boolean變量溢吻、記錄溫度變化的變量等
2维费、該變量沒(méi)有包含在具體其他變量的不變式中
不滿足:不變式 low < up
volatile不需要加鎖,比synchronized更輕量級(jí)促王,不會(huì)阻塞線程犀盟;
從內(nèi)存可見(jiàn)性角度講,volatile讀相當(dāng)于加鎖蝇狼,volatile寫(xiě)相當(dāng)于解鎖阅畴;
synchronized既能保證可見(jiàn)性,又能保證原子性迅耘,而volatile只能保證可見(jiàn)性贱枣,無(wú)法保證原子性。
A volatile是保證被修飾變量的可見(jiàn)性颤专,同時(shí)也保證原子操作 ×
B Java中沒(méi)有提供檢測(cè)與避免死鎖的專(zhuān)門(mén)機(jī)制纽哥,但應(yīng)用程序員可以采用某些策略防止死鎖的發(fā)生 √
C JAVA中對(duì)共享數(shù)據(jù)操作的并發(fā)控制是采用加鎖技術(shù) √
D 共享數(shù)據(jù)的訪問(wèn)權(quán)限都必須定義為private √
- final也可以保證內(nèi)存可見(jiàn)性
問(wèn):即使沒(méi)有保證可見(jiàn)性的措施,很多時(shí)候共享變量依然能夠在主內(nèi)存和工作內(nèi)存間得到即使的更新栖秕?
答:一般只有在短時(shí)間內(nèi)高并發(fā)的情況下才會(huì)出現(xiàn)變量得不到及時(shí)更新的情況春塌,因?yàn)镃PU在執(zhí)行時(shí)會(huì)很快地刷新緩存,所以一般情況下很難看到這種問(wèn)題簇捍。
對(duì)64位(long只壳、double)變量的讀寫(xiě)可能不是原子操作:
- Java內(nèi)存模型允許JVM將沒(méi)有被volatile修飾的64位數(shù)據(jù)類(lèi)型的讀寫(xiě)操作劃分為兩次32位的讀寫(xiě)操作來(lái)進(jìn)行。
導(dǎo)致問(wèn)題:有可能會(huì)出現(xiàn)讀取到“半個(gè)變量”的情況
解決辦法:加volatile關(guān)鍵字
synchronized和volatile比較:
- volatile比synchronized更輕量級(jí)
- volatile沒(méi)有synchronized使用的廣泛