鎖與線程安全

synchronized的4種應用方式

synchronized關鍵字最主要有以下3種應用方式,都是作用在對象上

  1. 修飾類,作用範圍:synchronized括號內, 作用對象:類的所有對象;synchronized(Service.class){ }
  2. 修改靜態方法,作用範圍:整個靜態方法, 作用對象:類的所有對象;
  3. 修飾方法,被修飾的同步方法,作用範圍:整個方法, 作用對象:調用這個方法的對象;
    1. 缺點:A線程執行一個長時間任務,B線程必須等待
  4. 修飾代碼塊,被修飾的代碼塊同步語句塊,作用範圍:大括號內的代碼, 作用對象:調用這個代碼塊的對象;
    1. 優點:減少鎖範圍,耗時的代碼放外面,可以異步調用

synchronized底層 每個對象有一個監視器鎖(monitor),當monitor被佔用時處於鎖定狀態

1.線程執行monitor enter指令時嘗試獲取monitor的所有權:

1、如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者。

2、如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數+1.

3、如果其他線程已經佔用monitor,該線程進入阻塞狀態,直到monitor的進入數爲0,再嘗試獲取monitor的所有權

2.線程執行monitor exit指令

monitor的進入數-1,如果-1後進入數爲0,線程退出monitor;其他被monitor阻塞的線程嘗試獲取 monitor

每個線程獲得鎖後,要完成變量 copy到工作內存->   修改->   刷新主存   的過程,纔會釋放它得到的鎖,達到線程安全。

  1. 鎖住(lock)
  2. 主->從 將需要的數據從主內存拷貝到自己的工作內存(read and load)
  3. 修改 根據程序流程讀取或者修改相應變量值(use and assign)
  4. 從->主 將自己工作內存中修改了值的變量拷貝回主內存(store and write)
  5. 釋放對象鎖(unlock)

線程安全:

  1. 當多個線程訪問某個類,其始終能表現出正確的行爲
  2. 採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,限制其他線程訪問,直到鎖釋放

 

synchronized和volatile區別 鎖的目標:互斥和可見性

1、鎖提供了兩種主要特性:互斥性(mutual exclusion) 和可見性(visibility)。

  互斥即一次只允許一個線程持有某個鎖,使用該共享數據。

  可見性確保新值立即同步到主存,每次使用前立即從主內存刷新

2、在Java中,爲了保證多線程讀寫數據時保證數據的一致性,可以採用兩種方式:

  synchronized同步:釋放鎖之前會將對變量的修改刷新到主存當中;阻塞

  volatile關鍵字:確保新值立即同步到主存,每次使用前立即從主內存刷新;非阻塞

3.區別

1)volatile非阻塞,synchronized只有當前線程可以訪問修飾的變量,其他線程阻塞

2)volatile僅能修飾變量,synchronized則可以使用在變量,方法.

3)volatile僅能實現變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性(操作不可分割)

volatile詳解

1.實現對所有線程的可見性

volatile保證新值立即同步到主存,每次使用前立即從主內存刷新

   編譯器爲了加快程序運行的速度,對一些變量的寫操作會先在(工作內存)寄存器或者是CPU緩存上進行,最後才寫入內存,這個過程中,變量的新值對其他線程是不可見的

volatile適用場景: 某線程修改某個狀態變量,通知其他線程做別的事

確保只有單一的線程修改變量的值 或 運算結果不依賴當前變量值

變量不需要與其他的狀態變量共同參與不變約束

2.實現禁止指令重排序優化

  1. java不能保證變量賦值操作的順序與代碼中一致
  2. volatile修飾的變量相當於生成內存屏障,重排序時不能把後面的指令排到屏障之前

3.無法實現 i++ 原子操作

A讀取 i 後,B也讀取 i ,此時A進行 +1,B的 i 就變了

原子類如何解決:CAS,B在進行+1時,檢查此時的 i 跟主存的 i 是否一致,一致才+1

Java中的鎖優化 編碼、JDK

編碼 鎖優化

  1. 減少鎖持有時間 
    1. 使用同步代碼塊,而非同步方法;
  2. 減小鎖粒度
    1. JDK1.6中 ConcurrentHashMap採取對segment加鎖而不是整個map加鎖,提高併發性;
  3. 鎖分離  讀鎖之間不互斥
    1. 根據同步操作的性質,把鎖劃分爲的讀鎖和寫鎖,讀鎖之間不互斥,提高了併發性

JDK1.6 鎖優化 synchronized底層

1.引入偏向鎖、輕量級鎖

  1. 鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級
  2. 鎖可以升級不可降級,提高 獲得鎖和釋放鎖 效率
  3. “輕量級鎖”和“偏向鎖”作用:減少 獲得鎖和釋放鎖 的性能消耗

優點

缺點

適用場景

偏向鎖

記錄線程iD,加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

自旋方式競爭,競爭的線程不會阻塞,提高了程序的響應速度CAS嘗試將頭部的二進制位修改00

如果始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度非常快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

 

2.鎖粗化 

如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,如循環體內,很耗性能

       加鎖同步的範圍擴展到整個操作序列的外部:第一個append到最後一個append

3.鎖消除 逃逸分析的數據的支持

        編譯器判斷到一段代碼中,堆上的數據不會逃逸出當前線程,可以認爲是線程安全的,不必加鎖

4.自旋與自適應自旋:想要獲取鎖的線程做幾個空循環 10

.爲什麼引入:

輕量級鎖失敗後,線程會在操作系統層面掛起,

操作系統實現線程之間的切換時,需要從用戶態轉換到核心態,狀態轉換耗時

.解決方法:

假設不久當前的線程可以獲得鎖,虛擬機會讓當前想要獲取鎖的線程做幾個空循環,可能是50個循環或100循環

.結果:

如果得到鎖,就順利進入臨界區;如果不能,就將線程在操作系統層面掛起,升級爲重量級鎖

.JDK做的優化:自適應自旋

當線程在獲取輕量級鎖時CAS操作失敗時,通過自旋讓線程等待,避免線程切換的開銷

自旋是需要消耗CPU的,如果一直獲取不到鎖,線程一直自旋,浪費CPU資源

線程如果自旋成功了,下次自旋的次數會更多,自旋失敗了,自旋的次數就會減少。

CAS底層實現原理

CAS:Compare and Swap, 翻譯成比較並交換 

CAS需要在:操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,最終都會返回內存地址,且是原子操作

需要3個操作數:內存地址V,舊預期值A、新值B

當且僅當V符合預期值A時(即V存儲的值無變化),用B更新A,否則不執行更新,最終都返回內存地址V

死鎖

多個線程因競爭資源而造成僵局(互相等待),無法向前推進

產生的原因

1) 系統資源的競爭

系統不可剝奪資源,數量不足以滿足多個進程運行,使得進程在運行過程中,因競爭資源而陷入僵局

2) 進程推進順序非法

請求和釋放資源的順序不當,也同樣會導致死鎖。如,互相申請各佔有的資源。

信號量使用不當也會造成死鎖。進程間彼此相互等待消息,結果也會使得這 些進程間無法繼續向前推進。

3) 死鎖產生的必要條件

產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。

  • 資源互斥條件:資源互斥,即某資源僅爲一個進程佔有
  • 資源不可剝奪條件:進程所獲得的資源在未使用完畢之前,只能是主動釋放,不能被其他進程強行奪走
  • 保持和請求條件:進程已經保持了一個資源,又提出了新的資源請求,而該資源已被其他進程佔有
  • 循環等待條件:進程資源循環等待

如何避免死鎖

  1. 加鎖順序(線程按照一定的順序加鎖)
    1. 按照順序加鎖是一種死鎖預防機制,需要事先知道所有會用到的鎖
  2. 加鎖時限(超時則放棄)
    1. 獲取鎖時加上時限,超過時限則放棄請求,並釋放鎖,等待一段隨機的時間再重試
  3. 死鎖檢測與恢復
    1. 操作系統中:系統爲進程分配資源,不採取任何限制性措施,提供檢測和解脫死鎖的手段

死鎖檢測:當一個線程請求鎖失敗時,遍歷鎖的關係圖檢測死鎖。

死鎖恢復

  1. 撤消進程,剝奪資源
  2. 線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖
  3. 死鎖發生的時候設置隨機的優先級;如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。

鎖模式包括: 

  1. 共享鎖:(讀取)用戶可以併發讀取數據,但不能獲取寫鎖,直到釋放所有讀鎖。
  2. 排他鎖(寫鎖):加上寫鎖後,其他線程無法加任何鎖;寫鎖可以讀和寫
  3. 更新鎖: 防止死鎖而設立,轉換讀鎖爲寫鎖之前的準備,僅一個線程可獲得更新鎖

樂觀鎖:認爲數據一般情況下不會造成衝突,在數據提交更新時,才進行數據的衝突檢測;

如果衝突,返回I信息讓用戶決定如何去做。實現方式:記錄數據版本。

悲觀鎖:操作數據時上鎖保護,限制其他線程訪問,直到該鎖釋放。關係型數據庫鎖機制,行鎖、頁鎖、表鎖,都是在做操作之前先上鎖。

鎖的粒度: 都是悲觀鎖

  1. 行鎖: 粒度最小,併發性最高
  2. 頁鎖:鎖定一頁。25個行鎖可升級爲一個頁鎖。
  3. 表鎖:粒度大,併發性低
  4. 數據庫鎖:控制整個數據庫操作

重入鎖 Reentrantlock

  1. 無阻塞的同步機制,實現了獨佔功能
  2. 加鎖和解鎖都需要顯式寫出,實現了Lock接口,注意一定要在適當時候unlock
  3. 添加了輪詢鎖、定時鎖等候和可中斷鎖特性;
  4. 提供了一個Condition(條件)類,用來實現分組喚醒線程
  5. 默認使用非公平鎖,可插隊跳過對線程隊列的處理
    1. ReentrantLock的內部類Sync繼承了AQS,分爲公平鎖FairSync和非公平鎖NonfairSync。
  • 公平鎖:線程獲取鎖的順序和調用lock的順序一樣,FIFO;
  • 非公平鎖:線程獲取鎖的順序和調用lock的順序無關

公平鎖爲了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷;直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快

不要拋棄 synchronized

  1. 易忘記 finally 塊釋放鎖,對程序有害
  2. synchronized 管理鎖定和釋放時,能標識死鎖或者其他異常行爲的來源,利於調試
  3. Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的性能就差不多

使用場景

對鎖進行更精確的控制,分組喚醒

輪詢鎖、定時鎖、可中斷鎖

 

介紹Condition    Java多線程系列--“JUC鎖”06之 Condition條件

對鎖進行更精確的控制

Condition中的await()方法相當於Object的wait()方法

Condition中的signal()方法相當於Object的notify()方法

Condition中的signalAll()相當於Object的notifyAll()方法

Condition函數列表

造成當前線程在接到信號或被中斷之前一直處於等待狀態 void await() // 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態 boolean await(long time, TimeUnit unit) // 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態 long awaitNanos(long nanosTimeout) // 造成當前線程在接到信號之前一直處於等待狀態 void awaitUninterruptibly() // 造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態 boolean awaitUntil(Date deadline) // 喚醒一個等待線程 void signal() // 喚醒所有等待線程 void signalAll()

介紹AbstractQueuedSynchronizer  一個用來構建鎖和同步工具的框架

  1. 使用int類型的volatile變量維護同步狀態(state),
  2. 使用Node實現FIFO隊列存放阻塞的等待線程,來完成線程的排隊執行
  3. 圍繞state提供兩種基本操作“獲取”和“釋放”
  4. 組合AQS對象的方式實現鎖的語義

AQS與鎖(如Lock)的對比:

  • 鎖是面向使用者的,鎖定義了用戶調用的接口,隱藏了實現細節;
  • AQS是鎖的實現者,通過用AQS簡化了鎖的實現屏蔽了同步狀態管理,線程的排隊,等待喚醒的底層操作。
  • 簡而言之,鎖是面向使用者,AQS是鎖的具體實現者。

 

 

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