Volatile關(guān)鍵字
volatile
是Java虛擬機(jī)提供的 輕量級(jí)
的同步機(jī)制.何為 輕量級(jí)
呢哨啃,這要相對(duì)于 synchronized
來說号阿。Volatile有如下三個(gè)特點(diǎn)迁沫。
要搞清楚上面列舉的名詞 可見性
原子性
指令重排
的含義我們需要首先弄清楚JMM(Java內(nèi)存模型是怎么回事)
JMM規(guī)定了內(nèi)存主要?jiǎng)澐譃?主內(nèi)存
和 工作內(nèi)存
兩種芦瘾。此處的主內(nèi)存和工作內(nèi)存跟JVM內(nèi)存劃分(堆、棧集畅、方法區(qū))是在不同的層次上進(jìn)行的旅急,如果非要對(duì)應(yīng)起來,主內(nèi)存對(duì)應(yīng)的是Java堆中的對(duì)象實(shí)例部分牡整,工作內(nèi)存對(duì)應(yīng)的是棧中的部分區(qū)域,從更底層的來說溺拱,主內(nèi)存對(duì)應(yīng)的是硬件的物理內(nèi)存逃贝,工作內(nèi)存對(duì)應(yīng)的是寄存器和高速緩存.
JVM在設(shè)計(jì)時(shí)候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內(nèi)存迫摔,對(duì)性能影響比較大沐扳,所以每條線程擁有各自的工作內(nèi)存,工作內(nèi)存中的變量是主內(nèi)存中的一份 拷貝
句占,線程對(duì)變量的讀取和寫入沪摄,直接在工作內(nèi)存中操作,而不能直接去操作主內(nèi)存中的變量。但是這樣就會(huì)出現(xiàn)一個(gè)問題杨拐,當(dāng)一個(gè)線程修改了自己工作內(nèi)存中變量祈餐,對(duì)其他線程是不可見的,會(huì)導(dǎo)致線程不安全的問題哄陶。因?yàn)镴MM制定了一套標(biāo)準(zhǔn)來保證開發(fā)者在編寫多線程程序的時(shí)候帆阳,能夠控制什么時(shí)候內(nèi)存會(huì)被同步給其他線程。
各個(gè)線程對(duì)主內(nèi)存中共享變量的操作都是各個(gè)線程各自拷貝到自己的工作內(nèi)存進(jìn)行操作后再寫回主內(nèi)存中的屋吨。
這就可能存在一個(gè)線程A修改了共享變量X的值但還未寫回主內(nèi)存時(shí)蜒谤,另一個(gè)線程B又對(duì)準(zhǔn)內(nèi)存中同一個(gè)共享變量X進(jìn)行操作,但此時(shí)A線程工作內(nèi)存中共享變量X對(duì)線程B來說并不是可見至扰,這種工作內(nèi)存與主內(nèi)存同步存在延遲現(xiàn)象就造成了可見性問題鳍徽。
通過代碼來看下可見性的問題
package com.dpb.spring.aop.demo;
import java.util.concurrent.TimeUnit;
/**
* 可見性問題分析
*/
public class VolatileDemo1 {
public static void main(String[] args){
final MyData myData = new MyData();
// 開啟一個(gè)新的線程
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "開始了...");
try{TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
// 在子線程中修改了變量的信息 修改的本線程在工作內(nèi)存中的數(shù)據(jù)
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "更新后的數(shù)據(jù)是:"+myData.number);
},"BBB").start();
// 因?yàn)樵谄渌€程中修改的信息主線程的工作內(nèi)存中的數(shù)據(jù)并沒有改變所以此時(shí)number還是為0
while(myData.number == 0){
// 會(huì)一直卡在此處
//System.out.println("1111");
}
System.out.println(Thread.currentThread().getName()+"\t number = " + myData.number);
}
}
class MyData{
// 沒有用volatile來修飾
int number = 0;
public void addTo60(){
this.number = 60;
}
}
效果如下:
通過 volatile
來解決此問題
我們可以發(fā)現(xiàn)當(dāng)變量被 volatile
修飾的時(shí)候,在子線程的工作內(nèi)存中的變量被修改后其他線程中對(duì)應(yīng)的變量是可以立馬知道的敢课。這就是我們講的可見性
原子性是 不可分割
阶祭, 完整性
,也即某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí)翎猛,中間不可以被加塞或者分割,需要整體完成胖翰,要么同時(shí)成功,要么同時(shí)失敗.
volatile是 不支持
原子性的,接下來我們可以驗(yàn)證下切厘。
import java.util.concurrent.TimeUnit;
/**
* 可見性問題分析
*/
public class VolatileDemo2 {
public static void main(String[] args){
final MyData2 myData = new MyData2();
for (int i = 1; i <= 20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
// 等待子線程執(zhí)行完成
while(Thread.activeCount() > 2){
Thread.yield();
}
// 在主線程中獲取統(tǒng)計(jì)的信息值
System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number);
}
}
class MyData2{
// 操作的變量被volatile修飾了
volatile int number = 0;
public void addPlusPlus(){
number++;
}
}
執(zhí)行的效果
根據(jù)正常的邏輯在開啟的20個(gè)子線程萨咳,每個(gè)執(zhí)行1000遍累加,得到的結(jié)果應(yīng)該是20000疫稿,但是我們發(fā)現(xiàn)運(yùn)行的結(jié)果大概率會(huì)比我們期望的要小培他,而且變量也已經(jīng)被volatile修飾了。說明并沒有滿足我們要求的原子性遗座。這種情況下我們要保證操作的原子性舀凛,我們有兩個(gè)選擇
通過synchronized來實(shí)現(xiàn)
通過
JUC
下的AtomicInteger
來實(shí)現(xiàn)
synchronized的實(shí)現(xiàn)是重量級(jí)的,影響并發(fā)的效率途蒋,所以我們通過AtomicInteger來實(shí)現(xiàn)猛遍。
package com.dpb.spring.aop.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 可見性問題分析
*/
public class VolatileDemo2 {
public static void main(String[] args){
final MyData2 myData = new MyData2();
for (int i = 1; i <= 20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
myData.addPlusPlus();
myData.addAtomicPlus();
}
},String.valueOf(i)).start();
}
// 等待子線程執(zhí)行完成
while(Thread.activeCount() > 2){
Thread.yield();
}
// 在主線程中獲取統(tǒng)計(jì)的信息值
System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number);
System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.atomicInteger.get());
}
}
class MyData2{
// 操作的變量被volatile修飾了
volatile int number = 0;
// AtomicInteger 來保證操作的原子性
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus(){
number++;
}
public void addAtomicPlus(){
atomicInteger.getAndIncrement();
}
}
效果:
注意
:通過效果發(fā)現(xiàn) AtomicInteger
在多線程環(huán)境下處理的數(shù)據(jù)和我們期望的結(jié)果是一致的都是 20000
.說明實(shí)現(xiàn)的操作的原子性。
有序性
計(jì)算機(jī)在執(zhí)行程序時(shí)号坡,為了提高性能懊烤,編譯器和處理器常常會(huì)對(duì)指令做重排,一般分以下3種:
1.單線程環(huán)境里面確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致宽堆。
2.處理器在進(jìn)行重排序時(shí)必須考慮指令之間的數(shù)據(jù)依賴性腌紧。
3.多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在畜隶,兩個(gè)線程中使用的變量能否保證一致性是無法確定的壁肋,結(jié)果無法預(yù)測(cè)号胚。
案例代碼
package com.dpb.spring.aop.demo;
public class SortDemo {
int a = 0;
boolean flag = false;
public void fun1(){
a = 1; // 語句1
flag = true; // 語句2
}
public void fun2(){
if(flag){
a = a + 5; // 語句3
System.out.println("a = " + a );
}
}
}
注意:
在多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在浸遗,兩個(gè)線程中使用的變量能否保證一致性是無法確定的猫胁,結(jié)果無法預(yù)測(cè)。
指令重排小結(jié):
volatile實(shí)現(xiàn)禁止指令重排優(yōu)化乙帮,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象杜漠。
先了解一個(gè)概念, 內(nèi)存屏障
又稱 內(nèi)存柵欄
察净,是一個(gè)CPU指令驾茴,它的作用有兩個(gè):
是保證特定操作的執(zhí)行順序
是保證某些變量的內(nèi)存可見性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見性)
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則告訴編譯器和CPU氢卡,不管什么指令都不能和這條Memory Barrier指令重新排序锈至,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。內(nèi)存屏障另外一個(gè)作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù)译秦,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本峡捡。
線程安全的總結(jié):
工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的
可見性問題
,可以使用synchronized或volatile關(guān)鍵字解決筑悴,他們都可以使一個(gè)線程修改后的變量立即對(duì)其他線程可見们拙。對(duì)于指令重排導(dǎo)致的
可見性問題
和有序性問題
,可以利用volatile關(guān)鍵字解決阁吝,因?yàn)関olatile的另外一個(gè)作用就是禁止重排序優(yōu)化砚婆。