日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java 并发_Java并发防范机制

發(fā)布時間:2024/7/23 java 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 并发_Java并发防范机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1.背景

并發(fā)程序開發(fā)不可避免地要涉及多線程、多線程協(xié)作、數據共享和線程安全等問題。在多線程并發(fā)場景下,由于采用數據共享的線程通信模型可能導致多個線程之間并發(fā)時相互干擾,影響到程序的正常邏輯、無法保證正常的結果。為了保證程序在并發(fā)環(huán)境的正確性,有必要對多線程并發(fā)進行防范,因此就有了并發(fā)控制機制。

Java并發(fā)控制機制

并發(fā)防范機制等價于并發(fā)控制機制,同步(有序)機制可以說是并發(fā)防范的一個子集。Java并發(fā)提供了多個維度的并發(fā)防范機制。我們可劃分JVM、JDK 2個層面:

  • JVM層面主要指關鍵字同步原語(volatile、synchronized、final)。通過字節(jié)碼指令禁止指令重排序來保證順序一致性。
  • JDK層面是JUC并發(fā)包,比如基于隊列同步器實現(xiàn)的重入鎖RetrantLock、讀寫鎖ReentrantReadWriteLock,此外還有Semaphore、CountDownLatch、CyclicBarrier等并發(fā)工具(本質還是鎖)、原子操作類(比如AtomicInteger)、ThreadLocal線程局部變量(無鎖防并發(fā)方案)、線程安全的并發(fā)容器(ConcurrentHashMap、BlockingQueue等)。
  • 關于線程安全

    “線程安全”網上大部分的解釋是:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的。并不能說它不對,但是不夠精確,幾乎獲取不到什么有用信息。

    《Java Concurrency In Practice》的作者Brian Goetz為“線程安全”做出了一個比較恰當的定義:當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環(huán)境下的調度和交替執(zhí)行,也不需要進行額外的同步,或者在調用方進行任何其他的協(xié)調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對象是線程安全的。”

    這個定義就很嚴謹而且有可操作性,它要求線程安全的代碼都必須具備一個共同特征:代碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程下的調用問題,更無須自己實現(xiàn)任何措施來保證多線程環(huán)境下的正確調用。這點聽起來簡單,但其實并不容易做到。

    2.JVM同步機制

    volatile

    volatile關鍵字的并發(fā)安全性承諾(即聲明為volatile的變量可以做到):

  • 線程對volatile變量的修改,可以及時反應到其他線程(對volatile變量的寫入可以及時作用到主內存,其他線程讀取volatile變量也是直接從主內存讀取)。
  • volatile變量的讀寫有序性(JVM通過字節(jié)碼指令enterexit禁止指令重排序來保證有序性),即兩線程并發(fā)使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ā)安全性承諾:

  • 臨界區(qū)互斥執(zhí)行。
  • 鎖的釋放先行發(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),大多數情況下,鎖不僅不存在多線程競爭,而且只是由同一線程獲取,為了讓線程獲取鎖的代價更低而引入了偏向鎖。

  • 當一個線程訪問同步塊時,首先會判斷鎖的狀態(tài),如果是01,且允許偏向,則進入第2步,否則進入第4步。
  • 獲取鎖,在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時,不需要進行CAS操作來加鎖和解鎖,只需要簡單測試一下對象頭Mark Word是否存儲了指向當前線程的偏向鎖。如果偏向鎖沒有設置,且此時鎖標志位為01,則嘗試CAS設置偏向鎖。
  • 偏向鎖的撤銷使用了一種等待競爭出現(xiàn)才釋放鎖的機制,當有其它線程競爭鎖時,持有偏向鎖的線程需要等待全局安全點(沒有正在執(zhí)行的字節(jié)碼這個點),它會暫停擁有偏向鎖的線程,判斷線程的活躍狀態(tài),如果不活躍,則設置為無鎖狀態(tài),否則升級。
  • 輕量級鎖

    輕量級對性能的提升的前提條件是同步塊可以很快執(zhí)行完成,且系統(tǒng)是多核,這樣只需要忙等輪詢很小一段時間就可以獲取鎖,避免線程阻塞導致的開銷。

  • 在代碼即將進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標志位為“01”狀態(tài)),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方為這份拷貝加了一個Displaced前綴,即Displaced Mark Word)。
  • 虛擬機將使用CAS操作嘗試把對象的Mark Word更新為指向Lock Record的指針。如果這個更新動作成功了,即代表該線程擁有了這個對象的鎖,并且對象Mark Word的鎖標志位(Mark Word的最后兩個比特)將轉變?yōu)椤?0”,表示此對象處于輕量級鎖定狀態(tài)。
  • 如果這個更新操作失敗了,那就意味著至少存在一條線程與當前線程競爭獲取該對象的鎖。虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是,說明當前線程已經擁有了這個對象的鎖,那直接進入同步塊繼續(xù)執(zhí)行就可以了,否則就說明這個鎖對象已經被其他線程搶占了,空轉輪詢一段時間鎖要膨脹為重量級鎖,鎖標志的狀態(tài)值變?yōu)椤?0”,跳轉到7。
  • 重量級鎖

  • 此時Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也必須進入阻塞狀態(tài)。
  • 鎖膨脹到重量級鎖后,可能導致線程阻塞,而線程阻塞時需要通過操作系統(tǒng)指令完成的,這種系統(tǒng)調用會導致程序用戶態(tài)內核態(tài)的切換,消耗系統(tǒng)資源。

    偏向鎖、輕量級鎖的狀態(tài)轉化及對象Mark Word的關系如下所示。

    鎖的整體膨脹過程如下圖所示:

    偏向鎖、輕量級、重量級鎖優(yōu)缺點分析

    final

    final的安全承諾:

  • final對象只在初始化構建時進行賦值,實例化成功后不允許改變其值,從根本上避免了并發(fā)寫入帶來的線程安全問題。
  • 讀一個對象的final域之前,一定會先這個對象的引用,如果引入對象不為null,則final域一定被初始化了,
  • 怎么做到的:

  • JMM禁止編譯器把final域的寫重排序到構造函數之外,實現(xiàn)方法是在final域寫之后,構造函數return前插入一個StoreStore屏障。
  • 讀對象final域之前插入LoadLoad屏障,保證讀對象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.線程安全集合

  • ConcurrentHashMap是線程安全且高效的HashMap。
  • BlockingQueue常用語生產者消費者場景、是線程安全的Queue。
  • 線程安全集合并不在本次討論范圍。

    總結

    本文較全面的討論了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并发防范机制的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。