java 併發編程的思考

程序開發中,經常會對某個資源進行併發讀寫,進而導致幻讀,髒讀,不可重複讀等問題,解決思路就是封鎖技術,本節就聊聊java併發編程中的主要技術。

基礎

 1)通過在總線加LOCK#鎖的方式

是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。因爲CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。比如上面例子中 如果一個線程在執行 i = i +1,如果在執行這段代碼的過程中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼完全執行完畢之後,其他CPU才能從變量i所在的內存讀取變量,然後進行相應的操作。這樣就解決了緩存不一致的問題。但是上面的方式會有一個問題,由於在鎖住總線期間,其他CPU無法訪問內存,導致效率低下

 2)通過緩存一致性協議

最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

原子性

即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行

可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值

有序性

有序性:即程序執行的順序按照代碼的先後順序執行,但是一般處理器會指令重排序。

 happens-before 原則保證了程序最終結果會和代碼順序執行結果相同,說白了就是數據依賴性。

JMM

Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。執行線程必須先在自己的工作線程中對變量i所在的緩存行進行賦值操作,然後再寫入主存當中。而不是直接將數值10寫入主存當中

原子性:

Java內存模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

可見性

對於可見性,Java提供了volatile關鍵字來保證可見性

有序性

在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

  在Java裏面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。

  另外,Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱爲 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。

  下面就來具體介紹下happens-before原則(先行發生原則):

  • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
  • volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
  • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  • 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

併發技術探討:

volatile:

volatile在多處理器開發中保證了共享變量的“ 可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。

volatile是cpu級別的操作,不牽扯線程的上下文切換和cpu緩存的刷新。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低。

synchronized:

synchronized通過鎖機制實現同步。當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存,
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。

 synchronized實現原理:

synchronized是基於Monitor來實現同步的,感覺要補充一下操作系統的東西了。

Monitor從兩個方面來支持線程之間的同步:

  • 互斥執行

  • 協作

monitor機制:

  monitor是爲了解決信號量機制的不足,將共享變量及對共享變量能夠進行的所有操作(還有信號量:wait和siginal)集中在一個模塊中。

monitor特點:

任何進程只能通過調用管程提供的過程入口才能進入管程訪問共享數據;

任何時刻,僅允許一個進程在管程中執行某個內部過程

對共享變量互斥操作

操作的同步控制

monitor結構

  • monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處
  • 每個monitorenter必須有對應的monitorexit與之配對
  • 任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
  • Java中的鎖[原理、鎖優化、CAS、AQS]

monitorenter

每一個對象都有一個monitor,一個monitor只能被一個線程擁有。當一個線程執行到monitorenter指令時會嘗試獲取相應對象的monitor,獲取規則如下:

  • 如果monitor的進入數爲0,則該線程可以進入monitor,並將monitor進入數設置爲1,該線程即爲monitor的擁有者。

  • 如果當前線程已經擁有該monitor,只是重新進入,則進入monitor的進入數加1,所以synchronized關鍵字實現的鎖是可重入的鎖。

  • 如果monitor已被其他線程擁有,則當前線程進入阻塞狀態,直到monitor的進入數爲0,再重新嘗試獲取monitor。

monitorexit

只有擁有相應對象的monitor的線程才能執行monitorexit指令。每執行一次該指令monitor進入數減1,當進入數爲0時當前線程釋放monitor,此時其他阻塞的線程將可以嘗試獲取該monitor。

Monitor 的工作機理

  • 線程進入同步方法中。

  • 爲了繼續執行臨界區代碼,線程必須獲取 Monitor 鎖。如果獲取鎖成功,將成爲該監視者對象的擁有者。任一時刻內,監視者對象只屬於一個活動線程(The Owner)

  • 擁有監視者對象的線程可以調用 wait() 進入等待集合(Wait Set),同時釋放監視鎖,進入等待狀態。

  • 其他線程調用 notify() / notifyAll() 接口喚醒等待集合中的線程,這些等待的線程需要重新獲取監視鎖後才能執行 wait() 之後的代碼。

  • 同步方法執行完畢了,線程退出臨界區,並釋放監視鎖。

synchronized具體實現

1、同步代碼塊採用monitorenter、monitorexit指令顯式的實現。

2、同步方法則使用ACC_SYNCHRONIZED標記符隱式的實現。

鎖存放的位置

鎖標記存放在Java對象頭的Mark Word中。

Java中的鎖[原理、鎖優化、CAS、AQS]

Java對象頭長度

Java中的鎖[原理、鎖優化、CAS、AQS]

32位JVM Mark Word 結構

Java中的鎖[原理、鎖優化、CAS、AQS]

32位JVM Mark Word 狀態變化

Java中的鎖[原理、鎖優化、CAS、AQS]

JavaSE1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。

在JavaSE1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。

鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。

偏向鎖:

無鎖競爭的情況下爲了減少鎖競爭的資源開銷,引入偏向鎖。

鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴展成一個範圍更大的鎖。

鎖消除(Lock Elimination):鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除。

適應性自旋(Adaptive Spinning):自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個循環。另一方面,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。

2.2.4 鎖的優缺點對比

Java中的鎖[原理、鎖優化、CAS、AQS]

lock:

3.1、隊列同步器(AQS)

隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步組件的基礎框架。

3.1.1、它使用了一個int成員變量表示同步狀態。

Java中的鎖[原理、鎖優化、CAS、AQS]

 

3.1.2、通過內置的FIFO雙向隊列來完成獲取鎖線程的排隊工作。

  • 同步器包含兩個節點類型的應用,一個指向頭節點,一個指向尾節點,未獲取到鎖的線程會創建節點線程安全(compareAndSetTail)的加入隊列尾部。同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點。

    Java中的鎖[原理、鎖優化、CAS、AQS]

     

  • 未獲取到鎖的線程將創建一個節點,設置到尾節點。如下圖所示:

Java中的鎖[原理、鎖優化、CAS、AQS]

 

  • 首節點的線程在釋放鎖時,將會喚醒後繼節點。而後繼節點將會在獲取鎖成功時將自己設置爲首節點。如下圖所示:

    Java中的鎖[原理、鎖優化、CAS、AQS]

     

3.1.3、獨佔式/共享式鎖獲取

獨佔式:有且只有一個線程能獲取到鎖,如:ReentrantLock。</pre>

共享式:可以多個線程同時獲取到鎖,如:CountDownLatch

獨佔式

  • 每個節點自旋觀察自己的前一節點是不是Header節點,如果是,就去嘗試獲取鎖。

    Java中的鎖[原理、鎖優化、CAS、AQS]

     

  • 獨佔式鎖獲取流程:

Java中的鎖[原理、鎖優化、CAS、AQS]

共享式:

  • 共享式與獨佔式的區別:

    Java中的鎖[原理、鎖優化、CAS、AQS]

  • 共享鎖獲取流程:

Java中的鎖[原理、鎖優化、CAS、AQS]

cas:

CAS,在Java併發應用中通常指CompareAndSwap或CompareAndSet,即比較並交換。

1、CAS是一個原子操作,它比較一個內存位置的值並且只有相等時修改這個內存位置的值爲新的值,保證了新的值總是基於最新的信息計算的,如果有其他線程在這期間修改了這個值則CAS失敗。CAS返回是否成功或者內存位置原來的值用於判斷是否CAS成功。

2、JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實現的。

優點:

  • 競爭不大的時候系統開銷小。

缺點:

【1】循環時間長開銷大。CAS長時間自旋不成功,給CPU帶來很大的性能開銷。解決方法:JVM能支持pause指令,效率會有一定的提升。
【2】只能保證一個共享變量的原子操作。對多個共享變量操作時,不能保證原子性。 解決方法:加鎖;共享變量合併成一個共享變量
【3】ABA的問題。解決方法就是:增加版本號,每次使用的時候版本號+1,每次變量更新的時候版本號+1。java提供AtomicStampzedReference來解決ABA問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章