java 并发_Java并发防范机制
1.背景
并發(fā)程序開發(fā)不可避免地要涉及多線程、多線程協(xié)作、數據共享和線程安全等問題。在多線程并發(fā)場景下,由于采用數據共享的線程通信模型可能導致多個線程之間并發(fā)時相互干擾,影響到程序的正常邏輯、無法保證正常的結果。為了保證程序在并發(fā)環(huán)境的正確性,有必要對多線程并發(fā)進行防范,因此就有了并發(fā)控制機制。
Java并發(fā)控制機制
并發(fā)防范機制等價于并發(fā)控制機制,同步(有序)機制可以說是并發(fā)防范的一個子集。Java并發(fā)提供了多個維度的并發(fā)防范機制。我們可劃分JVM、JDK 2個層面:
關于線程安全
“線程安全”網上大部分的解釋是:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的。并不能說它不對,但是不夠精確,幾乎獲取不到什么有用信息。
《Java Concurrency In Practice》的作者Brian Goetz為“線程安全”做出了一個比較恰當的定義:當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環(huán)境下的調度和交替執(zhí)行,也不需要進行額外的同步,或者在調用方進行任何其他的協(xié)調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對象是線程安全的。”
這個定義就很嚴謹而且有可操作性,它要求線程安全的代碼都必須具備一個共同特征:代碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程下的調用問題,更無須自己實現(xiàn)任何措施來保證多線程環(huán)境下的正確調用。這點聽起來簡單,但其實并不容易做到。
2.JVM同步機制
volatile
volatile關鍵字的并發(fā)安全性承諾(即聲明為volatile的變量可以做到):
以上描述的是volatile變量在多個線程間的可見性和有序性(禁止指令重排序),說到底volatile變量需要保證volatile寫/讀順序,volatile重排序規(guī)則表如下(JSR-133):
- 首先,若第二個操作是volatile寫,則不允許指令重排序。
- 其次,若第一個操作是volatile讀,同樣不允許指令重排序。
- 最后,當第一個操作是volatile寫,第二個操作volatile讀,則不允許指令重排序。
那么volatile具體是如何做到的?
為了實現(xiàn)volatile的內存語義,JVM采用基于保守策略的JMM內存屏障插入策略。
- 在每個volatile寫操作前面插入StoreStore屏障、在每個volatile寫的后面插入StoreLoad屏障。
- 在每個volatile讀操作后面插入LoadLoad屏障、在每個volatile讀的后面插入LoadStore屏障。
基于保守策略可保證在任意平臺、任意程序都得到正確的volatile語義。
通過加入屏障可以保證volatile寫-讀與鎖的釋放-獲取具有相同的內存效果:鎖的釋放總是先行發(fā)生于獲取鎖;同理,volatile寫總是先行發(fā)生于volatile讀。
synchronized
synchronized是內部鎖(也叫重量級鎖,實際上1.6后它做過優(yōu)化,沒那么重量級了),是Java最重要的同步機制之一。
雖然synchronized可以保證對象和代碼段的線程安全,但僅通過synchonized還不足以控制擁有復雜邏輯的線程交互,為了實現(xiàn)多線程交互,還需要和object的wait()和notify()兩個方法聯(lián)合使用。
synchronized(obj) {while(<?>) {obj.wait()// 收到通知后繼續(xù)執(zhí)行} }synchronzied配合wait()、notify()是并發(fā)編程的基本技能之一。
synchronized關鍵字的并發(fā)安全性承諾:
synchronized是如何做到互斥和保證先行發(fā)生關系的
Java中每個對象都可以作為鎖(對象的鎖)。普通同步方法,鎖是當前實例對象;靜態(tài)同步方法,鎖是當前類的Class對象;同步方法塊,鎖是synchronized括號里配置的對象。這些實例對象、Class對象、配置的對象在鎖范疇內叫Monitor對象。 JVM基于進入和退出Monitor對象來實現(xiàn)臨界區(qū)互斥執(zhí)行和鎖的釋放先行發(fā)生于鎖的獲取的內存語義。
- 代碼塊同步使用monitorenter和monitorexit指令實現(xiàn)。
- 同步方法是通過檢查方法是否標志ACC_SYNCHRONIZED實現(xiàn)。
鎖優(yōu)化
方案1:自旋
首先,分析一下synchronized的性能瓶頸。互斥同步對性能影響最大的是阻塞的實現(xiàn)。線程阻塞和用戶態(tài)內核態(tài)轉換帶來的性能開銷。虛擬機團隊注意到在大部分應用,共享數據的鎖定狀態(tài)只會持續(xù)很短一段時間,如果在這個很短的共享數據鎖定狀態(tài)去掛起和恢復線程是劃不來的,對于多處理器系統(tǒng),當發(fā)現(xiàn)共享資源被鎖定后,能否讓這個線程稍等一會兒,但不放棄處理器執(zhí)行時間呢?答案是肯定的,方案可行,前提是共享資源很快會被釋放。我們只需要讓線程執(zhí)行一個忙等待(自旋),這就是自旋鎖的由來。我們可以通過-XX:+UseSpinning開啟自旋鎖。
其次,自旋鎖不能替代阻塞,自旋鎖對處理器有要求(即多處理器),雖然避免了阻塞但會占用CPU執(zhí)行時間,如果鎖定很短效果會很好,但如果鎖定很長呢?那是否就白白浪費的處理器執(zhí)行時間了。因此自旋的等待時間必須有一個限度,如果自旋超過了限定次數仍然沒有成功獲得鎖,就應該使用傳統(tǒng)方式掛起線程。在虛擬機默認設置中自旋次數是10次,可通過參數-XX:PreBlockSpin來更改。
最后,不過無論是默認值還是用戶指定的自旋次數,對整個Java虛擬機中所有的鎖來說都是相同的。在 JDK 6中對自旋鎖的優(yōu)化,引入了自適應的自旋。自適應意味著自旋的時間不再是固定的了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而允許自旋等待持續(xù)相對更長的時間,比如持續(xù)100次忙循環(huán)。另一方面,如果對于某個鎖,自旋很少成功獲得過鎖,那在以后要獲取這個鎖時將有可能直接省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程序運行時間的增長及性能監(jiān)控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越精準,虛擬機就會變得越來越“聰明”了。
a.Java對象頭和MarkWord設計
首先,synchronized用的鎖是存在Java對象頭里的,對象如果是數組類型,則JVM用3個字寬(一個字寬32bit)存儲對象頭;如果對象是普通類型,則使用2字寬。Java對象頭組成如下所示:
下面,我們看下Mark Word的字段組成情況。
首先,在無鎖狀態(tài)下,32bit Mark Word劃分如下:
在運行期,Mark Word存儲的數據會隨著標志位的變化而變化,如下所示:
以上是32位虛擬機的Mark Word字段分配。
注:無鎖狀態(tài)的Mark Word當有線程獲取Monitor對象時,會拷貝到棧幀的鎖記錄中。
b.鎖的升級過程(鎖膨脹)
從以上分析我們知道鎖有4種狀態(tài):無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài)。這幾種狀態(tài)隨著競爭情況而逐漸升級。鎖只能升級而不能降級,只所以這樣做是為了提高獲取鎖和釋放鎖的效率。
偏向鎖
Hotspot作者發(fā)現(xiàn),大多數情況下,鎖不僅不存在多線程競爭,而且只是由同一線程獲取,為了讓線程獲取鎖的代價更低而引入了偏向鎖。
輕量級鎖
輕量級對性能的提升的前提條件是同步塊可以很快執(zhí)行完成,且系統(tǒng)是多核,這樣只需要忙等輪詢很小一段時間就可以獲取鎖,避免線程阻塞導致的開銷。
重量級鎖
鎖膨脹到重量級鎖后,可能導致線程阻塞,而線程阻塞時需要通過操作系統(tǒng)指令完成的,這種系統(tǒng)調用會導致程序用戶態(tài)內核態(tài)的切換,消耗系統(tǒng)資源。
偏向鎖、輕量級鎖的狀態(tài)轉化及對象Mark Word的關系如下所示。
鎖的整體膨脹過程如下圖所示:
偏向鎖、輕量級、重量級鎖優(yōu)缺點分析
final
final的安全承諾:
怎么做到的:
3.JUC并發(fā)防范機制
ReentrantLock
RetrantLock提供了比synchronized更強大的功能,更好的靈活性。它可以響應中斷、支持超時時間設置、支持公平和非公平策略。
lock.tryLock(5, TimeUtil.SECONDS); lock.lockInterruptibly();ReadWriteLock
讀寫鎖可以有效減少讀寫并發(fā)時的鎖競爭,進而減少線程阻塞提高響應時間。
Condition
Condition用于協(xié)調多線程的復雜協(xié)作,常與Lock配合使用,通過lock.newCondition()可以生成與Lock綁定的Condition實例。
Semaphore
信號量為多線程協(xié)作提供了更加強大的控制方法。信號量是對鎖的擴展,無論是內部鎖synchronized還是重入鎖ReentrantLock,一次僅允許一個線程訪問資源,而信號量則可以指定多個線程同時訪問資源。
構造方法如下:
public Semaphore(int permits) {} public Semaphore(int permits, boolean fair) {}主要方法:
public void acquire() throws InterruptedException {} public void acquireUninterruptibly() {} public boolean tryAcquire() {} public boolean tryAcquire(long timeout, TimeUtil unit) throews InterruptedException {} public void release() {}CountDownLatch
CountDownLatch允許一個或多個線程等待其他線程完成操作。一個線程調用countDown方法happen-before另外一個線程調用await方法。API如下
CountDownLatch latch = new CountDownLath(2); latch.countDown(); latch.await();CyclicBarrier
循環(huán)屏障可以做的事是讓一組線程到達一個屏障(也叫同步點)時被阻塞,直到最后一個現(xiàn)線程到達屏障才會打開。CyclicBarrier可用于多線程計算數據,最后合并計算結果的場景。CyclicBarrier API如下:
CyclicBarrier barrier = new CyclicBarrier(4, this); barrier.await();ThreadLocal
ThreadLocal提供的并發(fā)防范機制有別于以上在數據共享常見下通過加鎖來達到并發(fā)控制,防范線程非安全情況出現(xiàn)(即保證線程安全)。ThreadLocal為每個線程提供變量的獨立副本,從而從根本上杜絕了數據共享,線程之間根本就不會相互干擾,也就不會有線程安全問題。
4.線程安全集合
線程安全集合并不在本次討論范圍。
總結
本文較全面的討論了Java并發(fā)控制機制,在JVM層面通過volatile保證了內存的可見性和volatile寫/讀的先行發(fā)生關系。通過synchronized保證了多線程并發(fā)時對臨界區(qū)的互斥訪問以及鎖的釋放先行發(fā)生于鎖的獲取內存語義,為了提供并發(fā)性能,本文重點分析了內部鎖的膨脹過程。通過final關鍵字保證了構造函數的調用先行發(fā)生于final域的讀取并保證了final域的不可變性。除了JVM層面通過JMM定義的先行發(fā)生順序外,JUC也提供了并發(fā)防范工具,包括:RetrantLock、ReentrantReadWriteLock、Condition、Semaphore、CountDownLatch、CyclicBarrier以及ThreadLocal。
總結
以上是生活随笔為你收集整理的java 并发_Java并发防范机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何将dataset中的值赋值给data
- 下一篇: java事件绑定,Java编程GUI中的