Java內(nèi)存模型可見(jiàn)性問(wèn)題相關(guān)解析
這篇文章主要介紹了Java內(nèi)存模型可見(jiàn)性問(wèn)題相關(guān)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
前言
之前的文章中講到,JMM是內(nèi)存模型規(guī)范在Java語(yǔ)言中的體現(xiàn)。JMM保證了在多核CPU多線(xiàn)程編程環(huán)境下,對(duì)共享變量讀寫(xiě)的原子性、可見(jiàn)性和有序性。
本文就具體來(lái)講講JMM是如何保證共享變量訪問(wèn)的可見(jiàn)性的。
什么是可見(jiàn)性問(wèn)題
我們從一段簡(jiǎn)單的代碼來(lái)看看到底什么是可見(jiàn)性問(wèn)題。
public class VolatileDemo {
boolean started = false;
public void startSystem(){
System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
started = true;
System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
}
public void checkStartes(){
if (started){
System.out.println("system is running, time:"+System.currentTimeMillis());
}else {
System.out.println("system is not running, time:"+System.currentTimeMillis());
}
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
Thread startThread = new Thread(new Runnable() {
@Override
public void run() {
demo.startSystem();
}
});
startThread.setName("start-Thread");
Thread checkThread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
demo.checkStartes();
}
}
});
checkThread.setName("check-Thread");
startThread.start();
checkThread.start();
}
}
上面的列子中,一個(gè)線(xiàn)程來(lái)改變started的狀態(tài),另外一個(gè)線(xiàn)程不停地來(lái)檢測(cè)started的狀態(tài),如果是true就輸出系統(tǒng)啟動(dòng),如果是false就輸出系統(tǒng)未啟動(dòng)。那么當(dāng)start-Thread線(xiàn)程將狀態(tài)改成true后,check-Thread線(xiàn)程在執(zhí)行時(shí)是否能立即“看到”這個(gè)變化呢?答案是不一定能立即看到。這邊我做了很多測(cè)試,大多數(shù)情況下是能“感知”到started這個(gè)變量的變化的。但是偶爾會(huì)存在感知不到的情況。請(qǐng)看下下面日志記錄:
start-Thread begin to start system, time:1577079553515 start-Thread success to start system, time:1577079553516 system is not running, time:1577079553516 ==>此處start-Thread線(xiàn)程已經(jīng)將狀態(tài)設(shè)置成true,但是check-Thread線(xiàn)程還是沒(méi)檢測(cè)到 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519
上面的現(xiàn)象可能會(huì)讓人比較困惑,為什么有時(shí)候check-Thread線(xiàn)程能感知到狀態(tài)的變化,有時(shí)候又感知不到變化呢?這個(gè)現(xiàn)象就是在多核CPU多線(xiàn)程編程環(huán)境下會(huì)出現(xiàn)的可見(jiàn)性問(wèn)題。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線(xiàn)程還有自己的工作內(nèi)存,線(xiàn)程在工作內(nèi)存中保存的值是主內(nèi)存中值的副本,線(xiàn)程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存。等到線(xiàn)程對(duì)變量操作完畢之后會(huì)將變量的最新值刷新回到主內(nèi)存。
但是何時(shí)刷新這個(gè)最新值又是隨機(jī)的。所以就有可能一個(gè)線(xiàn)程已經(jīng)將一個(gè)共享變量更新了,但是還沒(méi)刷新回主內(nèi)存,那么這時(shí)其他對(duì)這個(gè)變量進(jìn)行讀寫(xiě)的線(xiàn)程就看不到這個(gè)最新值。這個(gè)就是多CPU多線(xiàn)程編程環(huán)境下的可見(jiàn)性問(wèn)題。也是上面代碼會(huì)出現(xiàn)問(wèn)題的原因。
JMM對(duì)可見(jiàn)性問(wèn)題的保證
在多CPU多線(xiàn)程編程環(huán)境下,對(duì)共享變量的讀寫(xiě)會(huì)出現(xiàn)可見(jiàn)性問(wèn)題。但是幸好JMM提供了相應(yīng)的技術(shù)手段來(lái)幫我們規(guī)避這些問(wèn)題,可以讓程序正確運(yùn)行。JMM針對(duì)可見(jiàn)性問(wèn)題,主要提供了如下手段:
- volatile關(guān)鍵字
- synchronized關(guān)鍵字
- Lock鎖
- CAS操作(原子操作類(lèi))
volatile關(guān)鍵字
使用volatile關(guān)鍵字修飾一個(gè)變量可以保證變量的可見(jiàn)性。所以對(duì)于上面的代碼,我們只需要簡(jiǎn)單的修改下代碼就可以讓程序正確運(yùn)行了。
private volatile boolean started = false;
使用volatile修飾一個(gè)共享變量可以達(dá)到如下的效果:
一旦線(xiàn)程對(duì)這個(gè)共享變量的副本做了修改,會(huì)立馬刷新最新值到主內(nèi)存中去;
一旦線(xiàn)程對(duì)這個(gè)共享變量的副本做了修改,其他線(xiàn)程中對(duì)這個(gè)共享變量拷貝的副本值會(huì)失效,其他線(xiàn)程如果需要對(duì)這個(gè)共享變量進(jìn)行讀寫(xiě),必須重新從主內(nèi)存中加載。
那么volatile具體是怎么達(dá)到上面兩個(gè)效果的呢?其實(shí)volatile底層使用的是內(nèi)存屏障來(lái)保證可見(jiàn)性的。
內(nèi)存屏障(英語(yǔ):Memory barrier),也稱(chēng)內(nèi)存柵欄,內(nèi)存柵障,屏障指令等,是一類(lèi)同步屏障指令,是CPU或編譯器在對(duì)內(nèi)存隨機(jī)訪問(wèn)的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫(xiě)操作都執(zhí)行后才可以開(kāi)始執(zhí)行此點(diǎn)之后的操作。大多數(shù)現(xiàn)代計(jì)算機(jī)為了提高性能而采取亂序執(zhí)行,這使得內(nèi)存屏障成為必須。
語(yǔ)義上,內(nèi)存屏障之前的所有寫(xiě)操作都要寫(xiě)入內(nèi)存;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫(xiě)操作的結(jié)果。因此,對(duì)于敏感的程序塊,寫(xiě)操作之后、讀操作之前可以插入內(nèi)存屏障。
對(duì)內(nèi)存屏障做下簡(jiǎn)單的總結(jié):
- 內(nèi)存屏障是一個(gè)指令級(jí)別的同步點(diǎn);
- 內(nèi)存屏障之前的寫(xiě)操作都必須立馬刷新回主內(nèi)存;
- 內(nèi)存屏障之后的讀操作都必須從主內(nèi)存中讀取最新值;
- 在有內(nèi)存屏障的地方,會(huì)禁止指令重排序,即屏障下面的代碼不能跟屏障上面的代碼交換執(zhí)行順序,即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成。
synchronized關(guān)鍵字
使用synchronized代碼塊或者synchronized方法也可以保證共享變量的可見(jiàn)性。只要如下修改上面的代碼,我們就能得到正確的執(zhí)行結(jié)果。
public synchronized void startSystem(){
System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
value = 2;
started = true;
System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
}
public synchronized void checkStartes(){
if (started){
System.out.println("system is running, time:"+System.currentTimeMillis());
}else {
System.out.println("system is not running, time:"+System.currentTimeMillis());
}
}
當(dāng)線(xiàn)程釋放鎖時(shí),JMM會(huì)把該線(xiàn)程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。當(dāng)線(xiàn)程獲取鎖時(shí),JMM會(huì)把該線(xiàn)程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。我們發(fā)現(xiàn)鎖具有和volatile一致的內(nèi)存語(yǔ)義,所以使用synchronized也可以實(shí)現(xiàn)共享變量的可見(jiàn)性。
Lock接口
使用Lock相關(guān)的實(shí)現(xiàn)類(lèi)也可以保證共享變量的可見(jiàn)性。其實(shí)現(xiàn)原理和synchronized的實(shí)現(xiàn)原理類(lèi)似,這邊也就不再贅述了。
CAS機(jī)制(Atomic類(lèi))
使用原子操作類(lèi)也可以保證共享變量操作的可見(jiàn)性。所以我們只要如下修稿上面的代碼就行了。
private AtomicBoolean started = new AtomicBoolean(false);
原子操作類(lèi)底層使用的是CAS機(jī)制。Java中CAS機(jī)制每次都會(huì)從主內(nèi)存中獲取最新值進(jìn)行compare,比較一致之后才會(huì)將新值set到主內(nèi)存中去。而且這個(gè)整個(gè)操作是一個(gè)原子操作。所以CAS操作每次拿到的都是主內(nèi)存中的最新值,每次set的值也會(huì)立即寫(xiě)到主內(nèi)存中。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java DWR內(nèi)存泄漏問(wèn)題解決方案
- macOS上使用gperftools定位Java內(nèi)存泄漏問(wèn)題及解決方案
- Java內(nèi)存泄漏問(wèn)題處理方法經(jīng)驗(yàn)總結(jié)
- 解決Java導(dǎo)入excel大量數(shù)據(jù)出現(xiàn)內(nèi)存溢出的問(wèn)題
- 完美解決java讀取大文件內(nèi)存溢出的問(wèn)題
- 詳解Java中synchronized關(guān)鍵字的死鎖和內(nèi)存占用問(wèn)題
- 解析Java的JNI編程中的對(duì)象引用與內(nèi)存泄漏問(wèn)題
- JAVA程序內(nèi)存溢出問(wèn)題原因分析
- Java中典型的內(nèi)存泄露問(wèn)題和解決方法
- 基于Java 數(shù)組內(nèi)存分配的相關(guān)問(wèn)題
- Java 內(nèi)存安全問(wèn)題的注意事項(xiàng)
相關(guān)文章
Java?常量池詳解之class文件常量池?和class運(yùn)行時(shí)常量池
這篇文章主要介紹了Java?常量池詳解之class文件常量池?和class運(yùn)行時(shí)常量池,常量池主要存放兩大類(lèi)常量:字面量,符號(hào)引用,本文結(jié)合示例代碼對(duì)java class常量池相關(guān)知識(shí)介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
mybatis plus動(dòng)態(tài)數(shù)據(jù)源切換及查詢(xún)過(guò)程淺析
這篇文章主要介紹了mybatis plus動(dòng)態(tài)數(shù)據(jù)源切換及查詢(xún)過(guò)程淺析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
關(guān)于Java整合RocketMQ實(shí)現(xiàn)生產(chǎn)消費(fèi)詳解
這篇文章主要介紹了關(guān)于Java整合RocketMQ實(shí)現(xiàn)生產(chǎn)消費(fèi)詳解,RocketMQ作為一款純java、分布式、隊(duì)列模型的開(kāi)源消息中間件,支持事務(wù)消息、順序消息、批量消息、定時(shí)消息、消息回溯等,需要的朋友可以參考下2023-05-05
java輸入多個(gè)數(shù)據(jù)(不確定),排序,并求最大值的方法
今天小編就為大家分享一篇java輸入多個(gè)數(shù)據(jù)(不確定),排序,并求最大值的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
spring單元測(cè)試下模擬rabbitmq的實(shí)現(xiàn)
這篇文章主要介紹了spring單元測(cè)試下模擬rabbitmq的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05

