
前言
Java內(nèi)存模型(JMM)給我們介紹了在當(dāng)代不同的硬件架構(gòu)情況下,多線程程序需要關(guān)注什么問(wèn)題以及如何利用JMM來(lái)正確的處理這些問(wèn)題。
多線程帶來(lái)的問(wèn)題
多線程程序主要關(guān)注兩個(gè)問(wèn)題:
(1)共享變量可見(jiàn)性問(wèn)題
(2)代碼重排序一致性問(wèn)題
Java內(nèi)存模型的關(guān)鍵點(diǎn)
JMM已經(jīng)保證了as-if-serial原則,也就是Java的程序在單線程情況下,不管JIT做不做重排序,也不管代碼指令在幾個(gè)CPU上執(zhí)行,看到的最終結(jié)果必須和代碼順序執(zhí)行的結(jié)果保持一致。
但是在多線程的情況下,如何才能正確的處理的變量可見(jiàn)性問(wèn)題和重排序的一致性問(wèn)題?
關(guān)鍵在于理解和運(yùn)用下面的兩塊內(nèi)容:
(1)happens-before相關(guān)
(2)data race相關(guān)
關(guān)于Memory Consistency Errors
Memory Consistency Errors中文含義是:內(nèi)存一致性錯(cuò)誤,指的的是多線程環(huán)境下,對(duì)于同一個(gè)共享變量的值在不同的線程看到的視圖不一致。
偽代碼如下:
int counter = 0;
此時(shí)A線程正在執(zhí)行:
counter++;
然后過(guò)了幾秒后,B線程打印這個(gè)值:
System.out.println(counter);
此時(shí)B線程的打印結(jié)果很大可能是0,但A線程里面其實(shí)這個(gè)值已經(jīng)是1了,這就是典型的內(nèi)存一致性錯(cuò)誤。這情況種只能通過(guò)happens-before規(guī)則來(lái)避免。
關(guān)于happens-before
happens-before是JMM里面保證在一個(gè)線程里面執(zhí)行的action(讀或者寫(xiě))的結(jié)果,可以在隨后的其他線程里面立馬可見(jiàn)的一系列規(guī)則。比如 x happens-before y ,那么不管x和y是不是在同一個(gè)線程里面,JMM都會(huì)保證對(duì)于x的update都會(huì)立馬里面對(duì)y線程可見(jiàn),也就是x總會(huì)先于y執(zhí)行,前提是兩者必須有happens-before關(guān)系,否則就會(huì)出現(xiàn)上面的內(nèi)存一致性錯(cuò)誤的問(wèn)題。
如何建立happens-before關(guān)系? 這里面有幾條規(guī)則:
(1) 單線程中的程序執(zhí)行結(jié)果與代碼的順序執(zhí)行結(jié)果保持一致。
你能會(huì)好奇,難道單線程不是順序執(zhí)行的嗎? 答案是的確不一定按照順序執(zhí)行,這個(gè)跟硬件的指令重排序有關(guān),目的是為了優(yōu)化性能讓cpu更快的執(zhí)行指令,但有happens-before保證,所以結(jié)果跟代碼順序執(zhí)行的結(jié)果保持一致,這是最基礎(chǔ)的保證,也是最重要的保證。
(2)同一個(gè)鎖的unlock操作,在其他線程lock后,變量是可見(jiàn)的。
class LockRule {
private int value = 0;
public synchronized void setValue(int value) {
this.value = value;
}
public synchronized int getValue() {
return value;
}
}
也就是在A線程中執(zhí)行setValue操作,在B線程中執(zhí)行g(shù)etValue方法是可以看到變化的,注意這里一定是同一個(gè)監(jiān)視器才可以,比如上面這段代碼就是用對(duì)象做為監(jiān)視器。此外ReentrantLock鎖也具有相同的語(yǔ)義。
(3)volatile修飾的變量,在一個(gè)線程update后,立刻對(duì)其他的線程可見(jiàn)。這個(gè)不多說(shuō),前面的文章介紹過(guò)。
(4)關(guān)于Thread的start方法,是指在一個(gè)線程A中啟動(dòng)另外另外一個(gè)線程B時(shí),A里面所有的變量對(duì)B是可見(jiàn)的,最常見(jiàn)的就是我們?cè)趈ava的main線程中啟動(dòng)的線程是可以看到啟動(dòng)之前所有的main線程的變量的。底層是啟動(dòng)前把所有內(nèi)容都同步到主內(nèi)存里面了,然后新的線程會(huì)從主內(nèi)存里面拷貝一份數(shù)據(jù)到自己的cache,所以是可見(jiàn)的。
(5)關(guān)于Thread的join方法,同樣道理,比如我在java的main線程里面聲明了一個(gè)線程B,然后調(diào)用
B.start() //啟動(dòng)B線程
B.join() //main線程等待B線程結(jié)束
此時(shí)在B線程里面修改了成員變量,在B線程結(jié)束的時(shí)候,main線程是可以直接看到最終變化的。這是一個(gè)線程結(jié)束的時(shí)候會(huì)把自己緩存的值給刷新到主內(nèi)存,所以感知了B線程結(jié)束的主線程是可以看到所有變化的。
(6)關(guān)于Thread的interrupt方法,同樣道理,在java的main線程中,比如我在java的main線程里面聲明了一個(gè)線程B,然后調(diào)用
B.start() //啟動(dòng)B線程
B.interrupt() //打斷B線程,此時(shí)B線程的是可以看到主線程的修改的狀態(tài)
(7)對(duì)于實(shí)例的finalize()方法,當(dāng)實(shí)例的構(gòu)造方法執(zhí)行完畢之后,如果再執(zhí)行finalize()方法,此時(shí)實(shí)例里面的所有變量不管有多少線程修改過(guò)對(duì)finalize()方法都是可見(jiàn)的。
(8)傳遞性規(guī)則: 如果 A happens-before B 并且 B happens-before C, 那么 A happens-before C
關(guān)于data race
data race又叫數(shù)據(jù)競(jìng)爭(zhēng),在這里指的多個(gè)線程之間沒(méi)有符合的happens-before規(guī)則,但是它們又需要修改同一個(gè)共享變量,比如上面的counter的例子,最終會(huì)造成內(nèi)存一致性的問(wèn)題,這種情況下可以通過(guò)Java自帶的一些鎖機(jī)制來(lái)避免。
關(guān)于上篇文章遺留問(wèn)題
在上篇文章中,我遺留了一個(gè)問(wèn)題,那就在下面的代碼中:
private static boolean keepRunning=true;
public static void main(String[] args) throws Exception {
new Thread(
()->{
while (keepRunning){
//System.out.println();
}
}
).start();
Thread.sleep(1000);
keepRunning=false;
}
如果我把while循環(huán)里面的打印語(yǔ)句去掉,那么即使沒(méi)有volatile關(guān)鍵字,程序也可以結(jié)束循環(huán),為什么? 其實(shí)答案就在今天的知識(shí)里面,因?yàn)榇蛴≌Z(yǔ)句會(huì)鎖住當(dāng)前的實(shí)例,源碼如下:
public void println(boolean x) {
synchronized (this) {
print(x);
newLine();
}
}
對(duì)應(yīng)到上面的happens-before的第二條規(guī)則就很容易的解釋通了。
總結(jié)
本篇文章主要介紹了Java內(nèi)存模型主要描述的問(wèn)題以及解決多線程環(huán)境下的問(wèn)題思路,我們了解和學(xué)習(xí)了什么是內(nèi)存一致性錯(cuò)誤,happens-before的規(guī)則,數(shù)據(jù)競(jìng)爭(zhēng)的內(nèi)容,掌握了這些知識(shí)將非常有助于我們深入到Java并發(fā)編程的世界,希望大家可以有所收獲。