volatile
是java
虛擬機提供的一種輕量級的同步機制,它有三個重要的特性:
- 保證可見性
- 不保證原子性
- 禁止指令重排
要理解這三個特性,就需要對JMM
(JAVA內(nèi)存模型)有一定的了解才行。
主要解決的問題:
JVM中,每個線程都會存在本地內(nèi)存,本地內(nèi)存是公共內(nèi)存的副本,各個線程的本地內(nèi)存相互隔離,就會存在一個線程對共享變量做了修改,其他線程沒有感知到的情況,從而導(dǎo)致數(shù)據(jù)不一致
一、JMM(JAVA內(nèi)存模型)
JMM
是 Java
虛擬機規(guī)范中所定義的一種內(nèi)存模型,Java
內(nèi)存模型是標準化的,屏蔽掉了底層不同計算機的區(qū)別。也就是說,JMM
是 JVM
中定義的一種并發(fā)編程的底層模型機制。JMM
定義了線程和主內(nèi)存(可以理解為買電腦時8/16G內(nèi)存)之間的抽象關(guān)系:不同線程之間的共享變量存在主內(nèi)存中,而每個線程中存在一個私有的本地內(nèi)存,對共享變量的操作需要將主內(nèi)存中的共享變量拷貝一份到本地內(nèi)存中。也就是說,在每個線程的本地內(nèi)存中存在的是共享變量的副本。
JMM
關(guān)于同步的規(guī)定:
- 1、線程解鎖前,必須把共享變量的值刷新會主內(nèi)存
- 2、線程加鎖前,必須讀取主內(nèi)存中的最新共享變量的值到本地內(nèi)存
- 3、加解鎖是同一把鎖
每個線程在創(chuàng)建時JVM
都會為其分配工作內(nèi)存(也叫??臻g),工作內(nèi)存是每個線程的私有區(qū)域。而java
內(nèi)存模型規(guī)定所有變量都必須存在主內(nèi)存中,主內(nèi)存是共享區(qū)域,所有線程都可以訪問。但是線程對變量的操作必須在工作內(nèi)存中進行,大概流程就是,線程將變量的值從主內(nèi)存拷貝到本地內(nèi)存中,進行操作,然后在將其寫回主內(nèi)存。由于不同線程之間的工作內(nèi)存互不可見,所有線程中的通信必須通過主內(nèi)存來進行。具體過程如下:
由于JMM
這樣的機制,就導(dǎo)致了可見性的問題。
JMM三大特性
- 可見性
- 原子性
- 有序性
二、可見性
內(nèi)存可見性指當一個線程修改了某個變量的值后,其他線程總能知道這個值的變化。
這里用例子來說明一下:
package com.fzkj.juc;
import java.util.concurrent.TimeUnit;
/**
* @DESCRIPTION volatile關(guān)鍵字測試類
*/
public class VolatileTest {
public static void main(String[] args) {
Number number = new Number();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
number.numTo(20);
System.out.println(Thread.currentThread().getName() + ":\t number的值是: " + number.num);
}, "A線程").start();
while(number.num == 0){}
System.out.println(Thread.currentThread().getName() + ":\t number的值是: " + number.num);
}
}
class Number{
int num = 0;
public void numTo(int target){
this.num = target;
}
public void add(){
this.num++ ;
}
}
運行上面的例子就會發(fā)現(xiàn),程序會陷入死循環(huán),永遠不會輸出最后一句話。就是因為A線程中對變量num
的修改對main
線程不可見,導(dǎo)致while
循環(huán)一直進行。
可見性問題常見的解決方案包括:
- 加鎖
- volatile關(guān)鍵字
volatile
對上面代碼進行改造
class Number{
volatile int num = 0;
public void numTo(int target){
this.num = target;
}
public void add(){
this.num++ ;
}
}
這樣在運行上面例子。就不會在陷入死循環(huán)了。
volatile是如何保證可見性的?
其他線程又是如何知道共享變量被修改了呢?
為了解決緩存一致性問題,需要遵循一些協(xié)議,叫做緩存一致性協(xié)議,如:MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。
嗅探
通過嗅探機制來保證及時的知道自己的緩存過期了。
在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,
當處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當前處理器的緩存行設(shè)置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里
由于嗅探機制會不斷的監(jiān)聽總線,打量使用volatile可能會引起總線風暴
三、原子性
在來看另一種情況。
package com.fzkj.juc;
import java.util.concurrent.TimeUnit;
/**
* @DESCRIPTION volatile關(guān)鍵字測試類
*/
public class VolatileTest {
public static void main(String[] args) {
atomicity();
}
// 原子性
public static void atomicity(){
Number num = new Number();
for (int i = 0; i < 10; i++) { // 啟動10個線程
new Thread(() -> {
for (int j = 0; j < 1000; j++) { // 每個線程對num的值操作1000次
num.add();
}
}).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num.num);
}
}
class Number{
int num = 0;
public void numTo(int target){
this.num = target;
}
public void add(){
this.num++ ;
}
}
在上面的例子中,我們啟動了10個線程,每個線程調(diào)用了1000次add方法,對num的值進行1000累加,那么我們期待的最終結(jié)果就是num的值是10000。但是實際上運行程序就會發(fā)現(xiàn),每次的結(jié)果都會比10000少。
這個問題的成因,其實跟jvm有關(guān)系,我們都知道,程序員寫的代碼只是給程序員自己看的,還需要將代碼編譯才是機器執(zhí)行的。一個++操作被編譯成字節(jié)碼文件之后,可以簡化成三個步驟。第一步取值;第二步加一;第三步賦值。所以在高并發(fā)的場景下,就會出現(xiàn)值被覆蓋的情況。
原子性的定義:指在一組操作中,要么全部操作都成功,要么全部操作都失敗。
原子性是JMM
的特性之一,但是volatile
卻并不支持原子性。要想在多線程的環(huán)境下保證原子性,可以使用鎖機制,或者使用原子類(AtomicInteger)。
四、有序性
禁止指令重排就叫做有序性。
什么是指令重排?
為了提高性能,在遵守as-if-serial
語義的情況下,編譯器和處理器往往會對指令做重排序。在多線程的情況下,指令重排可能會導(dǎo)致一些意想不到的情況。
那volatile
是怎么禁止指令的重排序的呢?這里又引出一個新的概念:內(nèi)存屏障
內(nèi)存屏障
內(nèi)存屏障的作用是禁止指令重排序和解決內(nèi)存可見性的問題。
先了解兩個指令:
- store:將緩存中的數(shù)據(jù)刷新到內(nèi)存中
- load:將內(nèi)存存儲的數(shù)據(jù)拷貝到緩存中
JMM
主要將內(nèi)存屏障分為四類
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 確保Load1數(shù)據(jù)的裝載先于Load2 |
StoreStore | Store1;StoreStore;Store2 | 確保Store1立刻刷新數(shù)據(jù)到內(nèi)存的操作先于Store2 |
LoadStore | Load1;LoadStore;Store2 | 確保Load1數(shù)據(jù)裝載先于Store2數(shù)據(jù)刷新 |
StoreLoad | Store1StoreLoad;Load2 | 確保Store1數(shù)據(jù)刷新先于Load2數(shù)據(jù)裝載 |
StoreLoad
被稱為全能屏障,因其同時具備其他三個屏障的效果,但是相對于其他屏障,消耗會多。
了解了這些,下面就來看看volatile
是如何插入內(nèi)存屏障的。
可以看到,文章來源:http://www.zghlxwxcb.cn/news/detail-738802.html
- volatile在讀操作后面加了LoadLoad和LoadStore屏障
- 在寫操作前后分別加了StoreStore和StoreLoad屏障
這就是說,編譯器不會對volatile
讀和讀后面的操作重排序;不會對寫和寫前面的操縱重排序。這樣就保證了volatile
本身的有序性。文章來源地址http://www.zghlxwxcb.cn/news/detail-738802.html
到了這里,關(guān)于Java并發(fā)編程-volatile的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!