本文主要為記錄和整理為主,在文章最低下會附上原文鏈接提岔。
把我遇到的知識點和問題梳理出來。
1.JAVA并發(fā)編程中的三個概念
1.原子性
2.可見性
3.有序性
原子性
原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行跨细。
在Java中臭猜,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作躺酒,即這些操作是不可被中斷的,要么執(zhí)行蔑歌,要么不執(zhí)行羹应。
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
這四個語句只有語句1是原子性的
其他三句都需要先讀取X變量的值,然后進行其他操作
那么在讀取X變量值后都有可能發(fā)生阻塞次屠,這時就破壞了原子性园匹。
也就是說雳刺,只有簡單的讀取、賦值(而且必須是將數(shù)字賦值給某個變量偎肃,變量之間的相互賦值不是原子操作)才是原子操作煞烫。
不過這里有一點需要注意:在32位平臺下,對64位數(shù)據(jù)的讀取和賦值是需要通過兩個操作來完成的累颂,不能保證其原子性滞详。但是好像在最新的JDK中,JVM已經(jīng)保證對64位數(shù)據(jù)的讀取和賦值也是原子性操作了紊馏。
保證原子性的方法:synchronize和Lock關(guān)鍵字 利用同步鎖料饥,保證一次只能一個線程對變量進行操作。
可見性
對于可見性朱监,Java提供了volatile關(guān)鍵字來保證可見性岸啡。
當(dāng)一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存赫编,當(dāng)有其他線程需要讀取時巡蘸,它會去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性擂送,因為普通共享變量被修改之后悦荒,什么時候被寫入主存是不確定的,當(dāng)其他線程去讀取時嘹吨,此時內(nèi)存中可能還是原來的舊值搬味,因此無法保證可見性。
另外蟀拷,通過synchronized和Lock也能夠保證可見性碰纬,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中问芬。因此可以保證可見性悦析。
有序性
在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”(具體原理在下一節(jié)講述)此衅。另外可以通過synchronized和Lock來保證有序性强戴,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼炕柔,相當(dāng)于是讓線程順序執(zhí)行同步代碼酌泰,自然就保證了有序性媒佣。
下面就來具體介紹下happens-before原則(先行發(fā)生原則):
程序次序規(guī)則:一個線程內(nèi)匕累,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作
volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
傳遞規(guī)則:如果操作A先行發(fā)生于操作B默伍,而操作B又先行發(fā)生于操作C欢嘿,則可以得出操作A先行發(fā)生于操作C
下面我們來解釋一下前4條規(guī)則:
對于程序次序規(guī)則來說衰琐,我的理解就是一段程序代碼的執(zhí)行在單個線程中看起來是有序的。注意炼蹦,雖然這條規(guī)則中提到“書寫在前面的操作先行發(fā)生于書寫在后面的操作”羡宙,這個應(yīng)該是程序看起來執(zhí)行的順序是按照代碼順序執(zhí)行的,因為虛擬機可能會對程序代碼進行指令重排序掐隐。雖然進行重排序狗热,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致的,它只會對不存在數(shù)據(jù)依賴性的指令進行重排序虑省。因此匿刮,在單個線程中,程序執(zhí)行看起來是有序執(zhí)行的探颈,這一點要注意理解熟丸。事實上,這個規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性伪节,但無法保證程序在多線程中執(zhí)行的正確性光羞。
第二條規(guī)則也比較容易理解,也就是說無論在單線程中還是多線程中怀大,同一個鎖如果出于被鎖定的狀態(tài)纱兑,那么必須先對鎖進行了釋放操作,后面才能繼續(xù)進行l(wèi)ock操作叉寂。
第三條規(guī)則是一條比較重要的規(guī)則萍启,也是后文將要重點講述的內(nèi)容。直觀地解釋就是屏鳍,如果一個線程先去寫一個變量勘纯,然后一個線程去進行讀取,那么寫入操作肯定會先行發(fā)生于讀操作钓瞭。
第四條規(guī)則實際上就是體現(xiàn)happens-before原則具備傳遞性驳遵。
JAVA內(nèi)存模型圖(JMM)
Volatile關(guān)鍵字的兩層意思
一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后山涡,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性堤结,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的鸭丛。
2)禁止進行指令重排序竞穷。
Volatile關(guān)鍵字 能保證原子性、可見性鳞溉、有序性嗎瘾带?為什么?舉例子說明
Volatile關(guān)鍵字不能保證原子性熟菲,可以保證可見性看政,能保證部分的有序性朴恳。
比如在多線程環(huán)境下對變量i進行自增操作,假設(shè)初始時i的值為0允蚣,那么操作后i可能為1.
這里有2種方式去理解:
1.首先A線程讀取變量i于颖,值為0 然后發(fā)生阻塞。此時線程B讀取i的值也為0 然后自增操作嚷兔。i=1 把i的最新值1更新到本地共享變量的副本森渐,然后再刷新到主內(nèi)存中去。然后此時再回到A線程冒晰,A線程這個時候也對自己的0值進行加1操作章母,然后更新回副本刷新到主存中去。此時主存中i=1翩剪。這里關(guān)鍵的一個點就是乳怎,當(dāng)線程B進行寫操作后,會使得其他線程的緩存行失效前弯,然后其他線程就會去主存中讀取最新的值蚪缀,這個沒錯。但是 線程A在一開始的時候已經(jīng)把值0從緩存行入棧到自己的棧頂了(底層的指令集的操作)恕出,也就不需要再去讀取緩存行所以緩存行的失效對線程A沒有作用询枚。
2.首先A線程讀取變量i,值為0 然后發(fā)生阻塞浙巫。此時線程B讀取i的值也為0金蜀,然后進行自增操作值為1.然后在進行更新變量副本之前,線程B阻塞的畴。然后回到線程A渊抄,A也進行自增然后把最新值1更新到變量副本刷新回到主內(nèi)存中去。此時回到線程B丧裁,線程B繼續(xù)更新變量副本然后把值刷新到主內(nèi)存中去還是1.
保證可見性是對的护桦,因為當(dāng)volatile關(guān)鍵字修飾的變量被寫操作之后。就會對緩存行失效煎娇,其他的線程再次讀取都會使用到最新的值二庵,保證了可見性。
為什么說是部分的有序性呢缓呛?
因為
1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時催享,在其前面的操作的更改肯定全部已經(jīng)進行,且結(jié)果已經(jīng)對后面的操作可見哟绊;在其后面的操作肯定還沒有進行因妙;
2)在進行指令優(yōu)化時,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行兰迫。
//x、y為非volatile變量
//flag為volatile變量
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5
由于flag變量為volatile變量炬称,那么在進行指令重排序的過程的時候汁果,不會將語句3放到語句1、語句2前面玲躯,也不會講語句3放到語句4据德、語句5后面。但是要注意語句1和語句2的順序跷车、語句4和語句5的順序是不作任何保證的棘利。
并且volatile關(guān)鍵字能保證,執(zhí)行到語句3時朽缴,語句1和語句2必定是執(zhí)行完畢了的善玫,且語句1和語句2的執(zhí)行結(jié)果對語句3、語句4密强、語句5是可見的茅郎。
volatile關(guān)鍵字的一些使用場景
使用volatile必須具備以下2個條件:
1)對變量的寫操作不依賴于當(dāng)前值
2)該變量沒有包含在具有其他變量的不變式中
實際上,這些條件表明或渤,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態(tài)系冗,包括變量的當(dāng)前狀態(tài)。
事實上薪鹦,我的理解就是上面的2個條件需要保證操作是原子性操作掌敬,才能保證使用volatile關(guān)鍵字的程序在并發(fā)時能夠正確執(zhí)行。
1.標(biāo)記狀態(tài)量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
保證了執(zhí)行到inited賦值為true時池磁,Context已經(jīng)初始化完成奔害,線程2再使用的時候就不會出現(xiàn)錯誤
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.單例模式double check
為什么要加volatile關(guān)鍵字,就是為了保證instance的初始化完成之后才會被使用地熄,以免報錯舀武。如果不使用,可能會出現(xiàn)离斩,線程A先new了一個對象 分配了內(nèi)存地址银舱,但是初始化對象的工作沒有完成。此時線程B進來跛梗,instance不為空寻馏。線程B持有instance然后使用的時候報錯。
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
自增操作保證原子性的方法有哪些?
用synchronize關(guān)鍵字
public synchronized void increase() {
inc++;
}
用lock
public class Test {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
}
用原子操作類
public class Test {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
}
總結(jié)一下synchronize lock volatile 和原子性 可見性 有序性的關(guān)系
synchronize和lock能保證可見性的原因是核偿,在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中诚欠。
原文參考鏈接:
Java并發(fā)編程:volatile關(guān)鍵字解析