摘要:有可能,會造成優先級反轉或者饑餓現象。悲觀鎖在中的使用,就是利用各種鎖。對于而言,其是獨享鎖。偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。
理解鎖的基礎知識
如果想要透徹的理解java鎖的來龍去脈,需要先了解以下基礎知識。
基礎知識之一:鎖的類型按照其性質分類
公平鎖/非公平鎖公平鎖是指多個線程按照申請鎖的順序來獲取鎖。非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者饑餓現象。對于Java ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在于吞吐量比公平鎖大。對于Synchronized而言,也是一種非公平鎖。由于其并不像ReentrantLock是通過AQS的來實現線程調度,所以并沒有任何辦法使其變成公平鎖。
樂觀鎖/悲觀鎖樂觀鎖與悲觀鎖不是指具體的什么類型的鎖,而是指看待并發同步的角度。悲觀鎖認為對于同一個數據的并發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。因此對于同一個數據的并發操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖的并發操作一定會出問題。樂觀鎖則認為對于同一個數據的并發操作,是不會發生修改的。在更新數據的時候,會采用嘗試更新,不斷重新的方式更新數據。樂觀的認為,不加鎖的并發操作是沒有事情的。從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。悲觀鎖在Java中的使用,就是利用各種鎖。樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。
獨享鎖/共享鎖獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指該鎖可被多個線程所持有。對于Java ReentrantLock而言,其是獨享鎖。但是對于Lock的另一個實現類ReentrantReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證并發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。對于Synchronized而言,當然是獨享鎖。
互斥鎖/讀寫鎖上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現?;コ怄i在Java中的具體實現就是ReentrantLock,讀寫鎖在Java中的具體實現就是ReentrantReadWriteLock
可重入鎖可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。對于Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是Reentrant Lock重新進入鎖。對于Synchronized而言,也是一個可重入鎖??芍厝腈i的一個好處是可一定程度避免死鎖。
基礎知識之二:java線程阻塞的代價java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,因為用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束后切換回用戶態繼續工作。
如果線程狀態切換是一個高頻操作時,這將會消耗很多CPU處理時間;
如果對于那些需要同步的簡單的代碼塊,獲取鎖掛起操作消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然非常糟糕的。
synchronized會導致爭用不到鎖的線程進入阻塞狀態,所以說它是java語言中一個重量級的同步操縱,被稱為重量級鎖,為了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啟用了自旋鎖,他們都屬于樂觀鎖。
明確java線程切換的代價,是理解java中各種鎖的優缺點的基礎之一。
基礎知識之三:CASCAS(Compare and swap)比較和替換是設計并發算法時用到的一種技術。簡單來說,比較和替換是使用一個期望值和一個變量的當前值進行比較,如果當前變量的值與我們期望的值相等,就使用一個新值替換當前變量的值。
Java5以來,你可以使用java.util.concurrent.atomic包中的一些原子類來使用CPU中的這些功能:
private AtomicBoolean locked = new AtomicBoolean(false); public boolean lock() { return locked.compareAndSet(false, true); }
locked變量不再是boolean類型而是AtomicBoolean。這個類中有一個compareAndSet()方法,它使用一個期望值和AtomicBoolean實例的值比較,和兩者相等,則使用一個新值替換原來的值。在這個例子中,它比較locked的值和false,如果locked的值為false,則把修改為true。
如果值被替換了,compareAndSet()返回true,否則,返回false。
CAS的ABA問題
進程P1在共享變量中讀到值為A
P1被搶占了,進程P2執行
P2把共享變量里的值從A改成了B,再改回到A,此時被P1搶占。
P1回來看到共享變量里的值沒有被改變,于是繼續執行。
這個例子你可能沒有看懂,維基百科上給了一個活生生的例子——
你拿著一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女, 然后她很暖昧地挑逗著你,并趁你不注意的時候,把用一個一模一樣的 手提箱和你那裝滿錢的箱子調了個包,然后就離開了,你看到你的手提 箱還在那,于是就提著手提箱去趕飛機去了。小結
前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖,
不同的鎖有不同特點,每種鎖只有在其特定的場景下,才會有出色的表現,java中沒有哪種鎖能夠在所有情況下都能有出色的效率,引入這么多鎖的原因就是為了應對不同的情況;
前面講到了重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬于樂觀鎖,所以現在你就能夠大致理解了他們的適用范圍,但是具體如何使用這幾種鎖呢,就要看后面的具體分析他們的特性;
synchronized的實現機制Java SE1.6里synchronized一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
自旋鎖自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
但是線程自旋是需要消耗cpu的,說白了就是讓cpu在做無用功,如果一直獲取不到鎖,那線程也不能一直占用cpu自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。
自旋鎖的優缺點自旋鎖盡可能的減少線程的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起再喚醒的操作的消耗,這些操作會導致線程發生兩次上下文切換!
但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用cpu做無用功,占著XX不XX,同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要cpu的線程又不能獲取到cpu,造成cpu的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖的開啟JDK1.6中-XX:+UseSpinning開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7后,去掉此參數,由jvm控制;
Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。
偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,
大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之后,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因為線程之前除了互斥之外也可能發生同步關系,被同步的兩個線程(一前一后)對共享對象鎖的競爭很可能是沒有沖突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價還是蠻大的,因此這里應當理解為一種類似時間戳的identifier)
如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
它通過消除資源無競爭情況下的同步,進一步提高了程序的運行性能。
訪問Mark Word中偏向鎖的標識是否設置成1,鎖標志位是否為01,確認為可偏向狀態。
如果為可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟5,否則進入步驟3。
如果線程ID并未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置為當前線程ID,然后執行5;如果競爭失敗,執行4。
如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會導致stop the word)
執行同步代碼。
偏向鎖的釋放偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態,撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態。
偏向鎖的適用場景始終只有一個線程在執行同步塊,在它沒有執行完釋放鎖之前,沒有其它線程去執行同步塊,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖,撤銷偏向鎖的時候會導致stop the word操作;
在有鎖的競爭時,偏向鎖會多做很多額外操作,尤其是撤銷偏向所的時候會導致進入安全點,安全點會導致stw,導致性能下降,這種情況下應當禁用;
開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關閉偏向鎖:-XX:-UseBiasedLocking
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;
加鎖線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。
解鎖輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。則要在釋放鎖的同時喚醒被掛起的線程。
總結偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,并且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。
第一步,檢查MarkWord里面是不是放的自己的ThreadId ,如果是,表示當前線程是處于 “偏向鎖”.跳過輕量級鎖直接執行同步體。
第二步,如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的線程根據MarkWord里面現有的ThreadId,通知之前線程暫停,之前線程將Markword的內容置為空。
第三步,兩個線程都把對象的HashCode復制到自己新建的用于存儲鎖的記錄空間,接著開始通過CAS操作,把共享對象的MarKword的內容修改為自己新建的記錄空間的地址的方式競爭MarkWord.
第四步,第三步中成功執行CAS的獲得資源,失敗的則進入自旋.
第五步,自旋的線程在自旋過程中,成功獲得資源(即之前獲的資源的線程執行完成并釋放了共享資源),則整個狀態依然處于輕量級鎖的狀態,如果自旋失敗 第六步,進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待之前線程執行完成并喚醒自己.
如果線程爭用激烈,那么應該禁用偏向鎖。
鎖優化以上介紹的鎖不是我們代碼中能夠控制的,但是借鑒上面的思想,我們可以優化我們自己線程的加鎖操作;
減少鎖的時間不需要同步執行的代碼,能不放在同步快里面執行就不要放在同步快內,可以讓鎖盡快釋放;
減少鎖的粒度它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增加并行度,從而降低鎖競爭。它的思想也是用空間來換時間;
java中很多數據結構都是采用這種方法提高并發操作的效率:
ConcurrentHashMap的鎖分段技術
LinkedBlockingQueue也體現了這樣的思想,在隊列頭入隊,在隊列尾出隊,入隊和出隊使用不同的鎖,相對于LinkedBlockingArray只有一個鎖效率要高;
鎖粗化鎖的粗化則是要增大鎖的粒度;
在以下場景下需要粗化鎖的粒度:
假如有一個循環,循環內的操作需要加鎖,我們應該把鎖放到循環外面,否則每次進出循環,都進出一次臨界區,效率是非常差的;
ReentrantReadWriteLock 是一個讀寫鎖,讀操作加讀鎖,可以并發讀,寫操作使用寫鎖,只能單線程寫;
讀寫分離CopyOnWriteArrayList 、CopyOnWriteArraySet
我們可以對CopyOnWrite容器進行并發的讀,而不需要加鎖,因為當前容器不會添加任何元素,而是操作容器的副本。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
如果需要同步的操作執行速度非???,并且線程競爭并不激烈,這時候使用cas效率會更高,因為加鎖會導致線程的上下文切換,如果上下文切換的耗時比同步操作本身更耗時,且線程對資源的競爭不激烈,使用volatiled+cas操作會是非常高效的選擇;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68674.html
摘要:共享資源臨界資源修飾實例方法輸出結果上述代碼與前面不同的是我們同時創建了兩個新實例,然后啟動兩個不同的線程對共享變量進行操作,但很遺憾操作結果是而不是期望結果。 線程安全是并發編程中的重要關注點,應該注意到的是,造成線程安全問題的主要誘因有兩點 一是存在共享數據(也稱臨界資源) 二是存在多條線程共同操作共享數據 因此為了解決這個問題,我們可能需要這樣一個方案,當存在多個線程操作共享...
摘要:注意,和都是隨機選擇一個線程,解除其阻塞狀態,可能會造成死鎖。生產者線程向隊列插入元素,消費者線程從隊列取出元素。當添加時隊列已滿或取出時隊列為空,阻塞隊列導致線程阻塞。里面有個小技巧,一個線程搜索完畢時向阻塞隊列填充,讓所有線程能停下來。 多線程對共享數據的讀寫涉及到同步問題,鎖和條件是線程同步的強大工具。鎖用來保護代碼片段(臨界區),任何時刻只能有一個線程執行被保護的代碼。條件對象...
閱讀 2845·2021-10-21 09:38
閱讀 2751·2021-10-11 10:59
閱讀 3022·2021-09-27 13:36
閱讀 1649·2021-08-23 09:43
閱讀 790·2019-08-29 14:14
閱讀 3034·2019-08-29 12:13
閱讀 3203·2019-08-29 12:13
閱讀 310·2019-08-26 12:24