摘要:這個規(guī)則比較好理解,無論是在單線程環(huán)境還是多線程環(huán)境,一個鎖處于被鎖定狀態(tài),那么必須先執(zhí)行操作后面才能進行操作。線程啟動規(guī)則獨享的方法先行于此線程的每一個動作。
1. 指令重排序
關于指令重排序的概念,比較復雜,不好理解。我們從一個例子分析:
public class SimpleHappenBefore { /** 這是一個驗證結果的變量 */ private static int a=0; /** 這是一個標志位 */ private static boolean flag=false; public static void main(String[] args) throws InterruptedException { //由于多線程情況下未必會試出重排序的結論,所以多試一些次 for(int i = 0; i < 1000; i++){ ThreadA threadA=new ThreadA(); ThreadB threadB=new ThreadB(); threadA.start(); threadB.start(); //這里等待線程結束后,重置共享變量,以使驗證結果的工作變得簡單些. threadA.join(); threadB.join(); a = 0; flag = false; } } static class ThreadA extends Thread{ public void run(){ a = 1; flag = true; } } static class ThreadB extends Thread{ public void run(){ if(flag){ a = a * 1; } if(a == 0){ System.out.println("ha,a==0"); } } } }
一個簡單的展示Happen-Before的例子.
這里有兩個共享變量:a和flag,初始值分別為0和false.在ThreadA中先給a=1,然后flag=true.
如果按照有序的話,那么在ThreadB中如果if(flag)成功的話,則應該a=1,而a=a*1之后a仍然為1,下方的if(a==0)應該永遠不會為真,永遠不會打印.
但實際情況是:在試驗100次的情況下會出現(xiàn)0次或幾次的打印結果,而試驗1000次結果更明顯,有十幾次打印.
1.1. 什么是指令重排序在虛擬機層面,為了盡可能減少內存操作速度遠慢于CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規(guī)則(這規(guī)則后面再敘述)將程序編寫順序打亂——即寫在后面的代碼在時間順序上可能會先執(zhí)行,而寫在前面的代碼會后執(zhí)行——以盡可能充分地利用CPU。
拿上面的例子來說:假如不是a=1的操作,而是a=new byte[1024*1024](分配1M空間),那么它會運行地很慢,此時CPU是等待其執(zhí)行結束呢,還是先執(zhí)行下面那句flag=true呢?顯然,先執(zhí)行flag=true可以提前使用CPU,加快整體效率,當然這樣的前提是不會產生錯誤(什么樣的錯誤后面再說)。雖然這里有兩種情況:后面的代碼先于前面的代碼開始執(zhí)行;前面的代碼先開始執(zhí)行,但當效率較慢的時候,后面的代碼開始執(zhí)行并先于前面的代碼執(zhí)行結束。不管誰先開始,總之后面的代碼在一些情況下存在先結束的可能。
不管怎么重排序,單線程程序的執(zhí)行結果不能被改變。編譯器、運行時和處理器都必須遵守“as-if-serial”語義。拿個簡單例子來說,
public void execute(){ int a = 0; int b = 1; int c = a+b; }
這里a=0,b=1兩句可以隨便排序,不影響程序邏輯結果,但c=a+b這句必須在前兩句的后面執(zhí)行。
2. happen-before在JMM中,如果一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。
happens-before原則非常重要,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù),依靠這個原則,我們解決在并發(fā)環(huán)境下兩操作之間是否可能存在沖突的所有問題。
happens-before原則定義如下:
如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
兩個操作之間存在happens-before關系,并不意味著一定要按照happens-before原則制定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果與按照happens-before關系來執(zhí)行的結果一致,那么這種重排序并不非法。
重排序在多線程環(huán)境下出現(xiàn)的概率還是挺高的,在關鍵字上有volatile和synchronized可以禁用重排序,除此之外還有一些規(guī)則,也正是這些規(guī)則,使得我們在平時的編程工作中沒有感受到重排序的壞處。
程序次序規(guī)則(Program Order Rule):在一個線程內,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準確地說應該是控制流順序而不是代碼順序,因為要考慮分支、循環(huán)等結構。
一段代碼在單線程中執(zhí)行的結果是有序的。注意是執(zhí)行結果,因為虛擬機、處理器會對指令進行重排序(重排序后面會詳細介紹)。雖然重排序了,但是并不會影響程序的執(zhí)行結果,所以程序最終執(zhí)行的結果與順序執(zhí)行的結果是一致的。故而這個規(guī)則只對單線程有效,在多線程環(huán)境下無法保證正確性。
監(jiān)視器鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個對象鎖的lock操作。這里強調的是同一個鎖,而“后面”指的是時間上的先后順序,如發(fā)生在其他線程中的lock操作。
這個規(guī)則比較好理解,無論是在單線程環(huán)境還是多線程環(huán)境,一個鎖處于被鎖定狀態(tài),那么必須先執(zhí)行unlock操作后面才能進行l(wèi)ock操作。
volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作發(fā)生于后面對這個變量的讀操作,這里的“后面”也指的是時間上的先后順序。
這是一條比較重要的規(guī)則,它標志著volatile保證了線程可見性。通俗點講就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量,那么這個寫操作一定是happens-before讀操作的。
線程啟動規(guī)則(Thread Start Rule):Thread獨享的start()方法先行于此線程的每一個動作。
假定線程A在執(zhí)行過程中,通過執(zhí)行ThreadB.start()來啟動線程B,那么線程A對共享變量的修改在接下來線程B開始執(zhí)行后確保對線程B可見。
線程終止規(guī)則(Thread Termination Rule):線程中的每個操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值檢測到線程已經終止執(zhí)行。
線程中斷規(guī)則(Thread Interruption Rule):對線程interrupte()方法的調用優(yōu)先于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否已中斷。
對象終結原則(Finalizer Rule):一個對象的初始化完成(構造函數(shù)執(zhí)行結束)先行發(fā)生于它的finalize()方法的開始。
假定線程A在執(zhí)行的過程中,通過制定ThreadB.join()等待線程B終止,那么線程B在終止之前對共享變量的修改在線程A等待返回后可見。
傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結論。
體現(xiàn)了happens-before原則具有傳遞性
如果不符合以上規(guī)則,那么在多線程環(huán)境下就不能保證執(zhí)行順序等同于代碼順序,也就是“如果在本線程中觀察,所有的操作都是有序的;如果在一個線程中觀察另外一個線程,則不符合以上規(guī)則的都是無序的”,因此,如果我們的多線程程序依賴于代碼書寫順序,那么就要考慮是否符合以上規(guī)則,如果不符合就要通過一些機制使其符合,最常用的就是synchronized、Lock以及volatile修飾符。
上面八條是原生Java滿足Happens-before關系的規(guī)則,但是我們可以對他們進行推導出其他滿足happens-before的規(guī)則:
將一個元素放入一個線程安全的隊列的操作Happens-Before從隊列中取出這個元素的操作
將一個元素放入一個線程安全容器的操作Happens-Before從容器中取出這個元素的操作
在CountDownLatch上的倒數(shù)操作Happens-Before CountDownLatch#await()操作
釋放Semaphore許可的操作Happens-Before獲得許可操作
Future表示的任務的所有操作Happens-Before Future#get()操作
向Executor提交一個Runnable或Callable的操作Happens-Before任務開始執(zhí)行操作
3. Volatilehappen-before原則是JMM中非常重要的原則,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù),保證了多線程環(huán)境下的可見性。
volatile相當于synchronized的弱實現(xiàn),類似于synchronized的語義,但是沒有鎖機制。在JDK及開源框架隨處可見,但是在JDK6之后synchronized關鍵字性能被大幅優(yōu)化之后,幾乎沒有使用了場景。
3.1. 語義第一條語義:JMM不會對volatile指令的操作進行重排序。這個保證了對volatile變量的操作時按照指令的出現(xiàn)順序執(zhí)行的。
第二條語義是保證線程間變量的可見性,簡單地說就是當線程A對變量X進行了修改后,在線程A后面執(zhí)行的其他線程能看到變量X的變動,更詳細地說是要符合以下兩個規(guī)則:
線程對變量進行修改之后,要立刻回寫到主內存。
線程對變量讀取的時候,要從主內存中讀,而不是緩存。
雖然volatile字段保證了可見性,但是由于缺少同步機制,所以volatile的字段的操作不是原子性的,并不能保證線程安全。
3.2. 應用原則volatile是在synchronized性能低下的時候提出的。如今synchronized的效率已經大幅提升,所以volatile存在的意義不大。
如今非volatile的共享變量,在訪問不是超級頻繁的情況下,已經和volatile修飾的變量有同樣的效果了。
volatile不能保證原子性,這點是大家沒太搞清楚的,所以很容易出錯。
volatile可以禁止重排序
通常應用場景如下:
volatile boolean done = flase; //... while(!done){ // ... }4. CAS 4.1. 鎖機制存在的問題
在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
如果一個優(yōu)先級高的線程等待一個優(yōu)先級低的線程釋放鎖會導致優(yōu)先級倒置,引起性能風險。
獨占鎖是一種悲觀鎖,synchronized就是一種獨占鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
4.2. 原理CAS 操作包含三個操作數(shù)——內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。”
通常將 CAS 用于同步的方式是從地址 V 讀取值 A,執(zhí)行多步計算來獲得新值 B,然后使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。
CAS其底層是通過CPU的1條指令來完成3個步驟,因此其本身是一個原子性操作,不存在其執(zhí)行某一個步驟的時候而被中斷的可能。
從性能角度考慮:
如果使用鎖來進行并發(fā)控制,當某一個線程(T1)搶占到鎖之后,那么其他線程再嘗試去搶占鎖時就會被掛起,當T1釋放鎖之后,下一個線程(T2)再搶占到鎖后并且重新恢復到原來的狀態(tài)大約需要經過8W個時鐘周期。
假設我們業(yè)務代碼本身并不具備很復雜的操作,執(zhí)行整個操作可能就花費3-10個時鐘周期左右,那么當我們使用無鎖操作時,線程T1和線程T2對共享變量進行并發(fā)的CAS操作,假設T1成功了,T2最多再執(zhí)行一次,它執(zhí)行多次的所消耗的時間遠遠小于由于線程所掛起到恢復所消耗的時間,因此無鎖的CAS操作在性能上要比同步鎖高很多。
示例代碼:
public class SimulatedCAS { private int value; public synchronized int getValue() { return value; } public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (value == expectedValue) value = newValue; return oldValue; } }
非阻塞算法:一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。
基于CAS的并發(fā)算法稱為非阻塞算法,CAS 操作成功還是失敗,在任何一種情況中,它都在可預知的時間內完成。如果 CAS 失敗,調用者可以重試 CAS 操作或采取其他適合的操作。下面顯示了重新編寫的計數(shù)器類來使用 CAS 替代鎖定:
public class CasCounter { private SimulatedCAS value; public int getValue() { return value.getValue(); } public int increment() { int oldValue = value.getValue(); while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue) oldValue = value.getValue(); return oldValue + 1; } }
無論是直接的還是間接的,幾乎 java.util.concurrent 包中的所有類都使用原子變量,而不使用同步。類似 ConcurrentLinkedQueue 的類也使用原子變量直接實現(xiàn)無等待算法,而類似 ConcurrentHashMap 的類使用 ReentrantLock 在需要時進行鎖定。然后, ReentrantLock 使用原子變量來維護等待鎖定的線程隊列。
4.3. 缺陷CAS策略有如下需要注意的事項:
在線程搶占資源特別頻繁的時候(相對于CPU執(zhí)行效率而言),會造成長時間的自旋,耗費CPU性能。
有ABA問題(即在更新前的值是A,但在操作過程中被其他線程更新為B,又更新為A),這時當前線程認為是可以執(zhí)行的,其實是發(fā)生了不一致現(xiàn)象,如果這種不一致對程序有影響(真正有這種影響的場景很少,除非是在變量操作過程中以此變量為標識位做一些其他的事,比如初始化配置),則需要使用AtomicStampedReference(除了對更新前的原值進行比較,也需要用更新前的stamp標志位來進行比較)。
只能對一個變量進行原子性操作。如果需要把多個變量作為一個整體來做原子性操作,則應該使用AtomicReference來把這些變量放在一個對象里,針對這個對象做原子性操作。
5. 引用java的多線程機制引用
【死磕Java并發(fā)】-----Java內存模型之happens-before
深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則
流行的原子
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67708.html
摘要:純分享直接上干貨操作系統(tǒng)并發(fā)支持進程管理內存管理文件系統(tǒng)系統(tǒng)進程間通信網絡通信阻塞隊列數(shù)組有界隊列鏈表無界隊列優(yōu)先級有限無界隊列延時無界隊列同步隊列隊列內存模型線程通信機制內存共享消息傳遞內存模型順序一致性指令重排序原則內存語義線程 純分享 , 直接上干貨! 操作系統(tǒng)并發(fā)支持 進程管理內存管...
摘要:內存模型是圍繞著在并發(fā)過程中如何處理原子性可見性和有序性這個特征來建立的,我們來看下哪些操作實現(xiàn)了這個特性。可見性可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。 Java內存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這3個特征來建立的,我們來看下哪些操作實現(xiàn)了這3個特性。 原子性(atomicity): 由Java內存模型來直接保證原子性變量操作包括...
摘要:并發(fā)編程關鍵字解析解析概覽內存模型的相關概念并發(fā)編程中的三個概念內存模型深入剖析關鍵字使用關鍵字的場景內存模型的相關概念緩存一致性問題。事實上,這個規(guī)則是用來保證程序在單線程中執(zhí)行結果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。 Java并發(fā)編程:volatile關鍵字解析 1、解析概覽 內存模型的相關概念 并發(fā)編程中的三個概念 Java內存模型 深入剖析volatile關鍵字 ...
摘要:前言今天的筆記來了解一下原子操作以及中如何實現(xiàn)原子操作。概念原子本意是不能被進一步分割的最小粒子,而原子操作意為不可被中斷的一個或一系列操作。處理器實現(xiàn)原子操作處理器會保證基本內存操作的原子性。 showImg(https://segmentfault.com/img/bVVIRA?w=1242&h=536); 前言 今天的筆記來了解一下原子操作以及Java中如何實現(xiàn)原子操作。 概念 ...
摘要:本文探討并發(fā)中的其它問題線程安全可見性活躍性等等。當閉鎖到達結束狀態(tài)時,門打開并允許所有線程通過。在從返回時被叫醒時,線程被放入鎖池,與其他線程競爭重新獲得鎖。 本文探討Java并發(fā)中的其它問題:線程安全、可見性、活躍性等等。 在行文之前,我想先推薦以下兩份資料,質量很高:極客學院-Java并發(fā)編程讀書筆記-《Java并發(fā)編程實戰(zhàn)》 線程安全 《Java并發(fā)編程實戰(zhàn)》中提到了太多的術語...
閱讀 1867·2019-08-29 16:44
閱讀 2172·2019-08-29 16:30
閱讀 780·2019-08-29 15:12
閱讀 3531·2019-08-26 10:48
閱讀 2659·2019-08-23 18:33
閱讀 3780·2019-08-23 17:01
閱讀 1943·2019-08-23 15:54
閱讀 1302·2019-08-23 15:05