本文主要為記錄和整理為主,在文章最低下會附上原文鏈接。
把我遇到的知識點和問題梳理出來。
1.JAVA并發編程中的三個概念
1.原子性
2.可見性
3.有序性
原子性
原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
這四個語句只有語句1是原子性的
其他三句都需要先讀取X變量的值,然后進行其他操作
那么在讀取X變量值后都有可能發生阻塞,這時就破壞了原子性。
也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。
不過這里有一點需要注意:在32位平臺下,對64位數據的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性。但是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操作了。
保證原子性的方法:synchronize和Lock關鍵字 利用同步鎖,保證一次只能一個線程對變量進行操作。
可見性
對于可見性,Java提供了volatile關鍵字來保證可見性。
當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
有序性
在Java里面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼,自然就保證了有序性。
下面就來具體介紹下happens-before原則(先行發生原則):
程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
鎖定規則:一個unLock操作先行發生于后面對同一個鎖額lock操作
volatile變量規則:對一個變量的寫操作先行發生于后面對這個變量的讀操作
傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C
下面我們來解釋一下前4條規則:
對于程序次序規則來說,我的理解就是一段程序代碼的執行在單個線程中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生于書寫在后面的操作”,這個應該是程序看起來執行的順序是按照代碼順序執行的,因為虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。
第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出于被鎖定的狀態,那么必須先對鎖進行了釋放操作,后面才能繼續進行lock操作。
第三條規則是一條比較重要的規則,也是后文將要重點講述的內容。直觀地解釋就是,如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發生于讀操作。
第四條規則實際上就是體現happens-before原則具備傳遞性。
JAVA內存模型圖(JMM)
Volatile關鍵字的兩層意思
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
Volatile關鍵字 能保證原子性、可見性、有序性嗎?為什么?舉例子說明
Volatile關鍵字不能保證原子性,可以保證可見性,能保證部分的有序性。
比如在多線程環境下對變量i進行自增操作,假設初始時i的值為0,那么操作后i可能為1.
這里有2種方式去理解:
1.首先A線程讀取變量i,值為0 然后發生阻塞。此時線程B讀取i的值也為0 然后自增操作。i=1 把i的最新值1更新到本地共享變量的副本,然后再刷新到主內存中去。然后此時再回到A線程,A線程這個時候也對自己的0值進行加1操作,然后更新回副本刷新到主存中去。此時主存中i=1。這里關鍵的一個點就是,當線程B進行寫操作后,會使得其他線程的緩存行失效,然后其他線程就會去主存中讀取最新的值,這個沒錯。但是 線程A在一開始的時候已經把值0從緩存行入棧到自己的棧頂了(底層的指令集的操作),也就不需要再去讀取緩存行所以緩存行的失效對線程A沒有作用。
2.首先A線程讀取變量i,值為0 然后發生阻塞。此時線程B讀取i的值也為0,然后進行自增操作值為1.然后在進行更新變量副本之前,線程B阻塞。然后回到線程A,A也進行自增然后把最新值1更新到變量副本刷新回到主內存中去。此時回到線程B,線程B繼續更新變量副本然后把值刷新到主內存中去還是1.
保證可見性是對的,因為當volatile關鍵字修飾的變量被寫操作之后。就會對緩存行失效,其他的線程再次讀取都會使用到最新的值,保證了可見性。
為什么說是部分的有序性呢?
因為
1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;
2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行。
//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關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。
volatile關鍵字的一些使用場景
使用volatile必須具備以下2個條件:
1)對變量的寫操作不依賴于當前值
2)該變量沒有包含在具有其他變量的不變式中
實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態,包括變量的當前狀態。
事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程序在并發時能夠正確執行。
1.標記狀態量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
保證了執行到inited賦值為true時,Context已經初始化完成,線程2再使用的時候就不會出現錯誤
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.單例模式double check
為什么要加volatile關鍵字,就是為了保證instance的初始化完成之后才會被使用,以免報錯。如果不使用,可能會出現,線程A先new了一個對象 分配了內存地址,但是初始化對象的工作沒有完成。此時線程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關鍵字
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();
}
}
總結一下synchronize lock volatile 和原子性 可見性 有序性的關系
synchronize和lock能保證可見性的原因是,在釋放鎖之前會將對變量的修改刷新到主存當中。
原文參考鏈接:
Java并發編程:volatile關鍵字解析