多线程面试题之原子性、可见性、有序性
面試官:“對java并發(fā)了解怎么樣?”?
應(yīng)聘者:“還可以…” ?
面試官:“為了保證線程安全,Java并發(fā)有哪幾個基本特性呢?” ?
應(yīng)聘者:“有三條基本性質(zhì),原子性、可見性、有序性” ?
面試官:? “具體解釋下這三個特性?” ?
應(yīng)聘者:“bala。bala。bala。。”?
Java內(nèi)存模型是圍繞著并發(fā)過程中如何處理原子性、可見性、有序性這三個特征來建立的,下面是這三個特性的實(shí)現(xiàn)原理:原子性(Atomicity)
由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、use、assign、store和write六個,大致可以認(rèn)為基礎(chǔ)數(shù)據(jù)類型的訪問和讀寫是具備原子性的。如果應(yīng)用場景需要一個更大范圍的原子性保證,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock與unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱匿地使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是同步塊—synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。
Java中的原子操作包括:
1)除long和double之外的基本類型的賦值操作?
2)所有引用reference的賦值操作?
3)java.concurrent.Atomic.* 包中所有類的一切操作。
但是java對long和double的賦值操作是非原子操作!long和double占用的字節(jié)數(shù)都是8,也就是64bits。在32位操作系統(tǒng)上對64位的數(shù)據(jù)的讀寫要分兩步完成,每一步取32位數(shù)據(jù)。這樣對double和long的賦值操作就會有問題:如果有兩個線程同時寫一個變量內(nèi)存,一個進(jìn)程寫低32位,而另一個寫高32位,這樣將導(dǎo)致獲取的64位數(shù)據(jù)是失效的數(shù)據(jù)。因此需要使用volatile關(guān)鍵字來防止此類現(xiàn)象。volatile本身不保證獲取和設(shè)置操作的原子性,僅僅保持修改的可見性。但是java的內(nèi)存模型保證聲明為volatile的long和double變量的get和set操作是原子的。
public?class?UnatomicLong?implements?Runnable?{????private?static?long?test?=?0;????private?final?long?val;????public?UnatomicLong(long?val)?{????????this.val?=?val;????}????@Override????public?void?run()?{????????while?(!Thread.interrupted())?{????????????test?=?val;?//兩個線程都試圖將自己的私有變量val賦值給類私有靜態(tài)變量test????????}????}????public?static?void?main(String[]?args)?{????????Thread?t1?=?new?Thread(new?UnatomicLong(-1));????????Thread?t2?=?new?Thread(new?UnatomicLong(0));????????System.out.println(Long.toBinaryString(-1));????????System.out.println(pad(Long.toBinaryString(0),?64));????????t1.start();????????t2.start();????????long?val;????????while?((val?=?test)?==?-1?||?val?==?0)?{????????//如果靜態(tài)成員test的值是-1或0,說明兩個線程操作沒有交叉????????}????????System.out.println(pad(Long.toBinaryString(val),?64));????????System.out.println(val);????????t1.interrupt();????????t2.interrupt();????}????//?prepend?0s?to?the?string?to?make?it?the?target?length????private?static?String?pad(String?s,?int?targetLength)?{????????int?n?=?targetLength?-?s.length();????????for?(int?x?=?0;?x?<?n;?x++)?{????????????s?=?"0"?+?s;????????}????????return?s;????}}運(yùn)行發(fā)現(xiàn)程序在while循環(huán)時進(jìn)入了死循環(huán),這是因?yàn)槭褂玫腏VM是64bits。在64位JVM中double和long的賦值操作是原子操作。?
在eclipse中修改jre為一個32bit的JVM地址,則會有如下運(yùn)行結(jié)果:
可見性(Visibility)
可見性就是指當(dāng)一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方法來實(shí)現(xiàn)可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每使用前立即從內(nèi)存刷新。因?yàn)槲覀兛梢哉fvolatile保證了線程操作時變量的可見性,而普通變量則不能保證這一點(diǎn)。
除了volatile之外,Java還有兩個關(guān)鍵字能實(shí)現(xiàn)可見性,它們是synchronized。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)”這條規(guī)則獲得的,而final關(guān)鍵字的可見性是指:被final修飾的字段是構(gòu)造器一旦初始化完成,并且構(gòu)造器沒有把“this”引用傳遞出去,那么在其它線程中就能看見final字段的值。
Lock也可以保證可見性,因?yàn)樗梢员WC任一時刻只有一個線程能訪問共享資源,并在其釋放鎖之前將修改的變量刷新到內(nèi)存中。有序性(Ordering)
Java內(nèi)存模型中的程序天然有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行語義”,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存主主內(nèi)存同步延遲”現(xiàn)象。
Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則來獲得的,這個規(guī)則決定了持有同一個鎖的兩個同步塊只能串行地進(jìn)入。
先行發(fā)生原則:
如果Java內(nèi)存模型中所有的有序性都只靠volatile和synchronized來完成,那么有一些操作將會變得很啰嗦,但是我們在編寫Java并發(fā)代碼的時候并沒有感覺到這一點(diǎn),這是因?yàn)镴ava語言中有一個“先行發(fā)生”(Happen-Before)的原則。這個原則非常重要,它是判斷數(shù)據(jù)是否存在競爭,線程是否安全的主要依賴。
先行發(fā)生原則是指Java內(nèi)存模型中定義的兩項(xiàng)操作之間的依序關(guān)系,如果說操作A先行發(fā)生于操作B,其實(shí)就是說發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包含了修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。它意味著什么呢?如下例:
//線程A中執(zhí)行i?=?1;//線程B中執(zhí)行j?=?i;//線程C中執(zhí)行i?=?2;假設(shè)線程A中的操作”i=1“先行發(fā)生于線程B的操作”j=i“,那么我們就可以確定在線程B的操作執(zhí)行后,變量j的值一定是等于1,結(jié)出這個結(jié)論的依據(jù)有兩個,一是根據(jù)先行發(fā)生原則,”i=1“的結(jié)果可以被觀察到;二是線程C登場之前,線程A操作結(jié)束之后沒有其它線程會修改變量i的值。現(xiàn)在再來考慮線程C,我們依然保持線程A和B之間的先行發(fā)生關(guān)系,而線程C出現(xiàn)在線程A和B操作之間,但是C與B沒有先行發(fā)生關(guān)系,那么j的值可能是1,也可能是2,因?yàn)榫€程C對應(yīng)變量i的影響可能會被線程B觀察到,也可能觀察不到,這時線程B就存在讀取到過期數(shù)據(jù)的風(fēng)險,不具備多線程的安全性。
下面是Java內(nèi)存模型下一些”天然的“先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對它們進(jìn)行隨意地重排序。
a.程序次序規(guī)則(Pragram Order Rule):在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)結(jié)構(gòu)。
b.管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個鎖,而”后面“是指時間上的先后順序。
c.volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀取操作,這里的”后面“同樣指時間上的先后順序。
d.線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
e.線程終于規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束,Thread.isAlive()的返回值等作段檢測到線程已經(jīng)終止執(zhí)行。
f.線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測是否有中斷發(fā)生。
g.對象終結(jié)規(guī)則(Finalizer Rule):一個對象初始化完成(構(gòu)造方法執(zhí)行完成)先行發(fā)生于它的finalize()方法的開始。
g.傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
一個操作”時間上的先發(fā)生“不代表這個操作會是”先行發(fā)生“,那如果一個操作”先行發(fā)生“是否就能推導(dǎo)出這個操作必定是”時間上的先發(fā)生“呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先后順序與先生發(fā)生原則之間基本沒有什么關(guān)系,所以衡量并發(fā)安全問題一切必須以先行發(fā)生原則為準(zhǔn)。
轉(zhuǎn)載于:https://www.cnblogs.com/jobbible/p/10374390.html
總結(jié)
以上是生活随笔為你收集整理的多线程面试题之原子性、可见性、有序性的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 调用人脸识别 虹软ArcFace2
- 下一篇: 二进制状态压缩相关操作