synchronized的4種應用方式
synchronized關鍵字最主要有以下3種應用方式,都是作用在對象上
- 修飾類,作用範圍:synchronized括號內, 作用對象:類的所有對象;synchronized(Service.class){ }
- 修改靜態方法,作用範圍:整個靜態方法, 作用對象:類的所有對象;
- 修飾方法,被修飾的同步方法,作用範圍:整個方法, 作用對象:調用這個方法的對象;
- 缺點:A線程執行一個長時間任務,B線程必須等待
- 修飾代碼塊,被修飾的代碼塊同步語句塊,作用範圍:大括號內的代碼, 作用對象:調用這個代碼塊的對象;
- 優點:減少鎖範圍,耗時的代碼放外面,可以異步調用
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到工作內存-> 修改-> 刷新主存 的過程,纔會釋放它得到的鎖,達到線程安全。
- 鎖住(lock)
- 主->從 將需要的數據從主內存拷貝到自己的工作內存(read and load)
- 修改 根據程序流程讀取或者修改相應變量值(use and assign)
- 從->主 將自己工作內存中修改了值的變量拷貝回主內存(store and write)
- 釋放對象鎖(unlock)
線程安全:
- 當多個線程訪問某個類,其始終能表現出正確的行爲
- 採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,限制其他線程訪問,直到鎖釋放
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.實現禁止指令重排序優化
- java不能保證變量賦值操作的順序與代碼中一致
- volatile修飾的變量相當於生成內存屏障,重排序時不能把後面的指令排到屏障之前
3.無法實現 i++ 原子操作
A讀取 i 後,B也讀取 i ,此時A進行 +1,B的 i 就變了
原子類如何解決:CAS,B在進行+1時,檢查此時的 i 跟主存的 i 是否一致,一致才+1
Java中的鎖優化 編碼、JDK
編碼 鎖優化
- 減少鎖持有時間
- 使用同步代碼塊,而非同步方法;
- 減小鎖粒度
- JDK1.6中 ConcurrentHashMap採取對segment加鎖而不是整個map加鎖,提高併發性;
- 鎖分離 讀鎖之間不互斥
- 根據同步操作的性質,把鎖劃分爲的讀鎖和寫鎖,讀鎖之間不互斥,提高了併發性
JDK1.6 鎖優化 synchronized底層
1.引入偏向鎖、輕量級鎖
- 鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級
- 鎖可以升級不可降級,提高 獲得鎖和釋放鎖 效率
- “輕量級鎖”和“偏向鎖”作用:減少 獲得鎖和釋放鎖 的性能消耗
鎖 |
優點 |
缺點 |
適用場景 |
偏向鎖 |
記錄線程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) 死鎖產生的必要條件
產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。
- 資源互斥條件:資源互斥,即某資源僅爲一個進程佔有
- 資源不可剝奪條件:進程所獲得的資源在未使用完畢之前,只能是主動釋放,不能被其他進程強行奪走
- 保持和請求條件:進程已經保持了一個資源,又提出了新的資源請求,而該資源已被其他進程佔有
- 循環等待條件:進程資源循環等待
如何避免死鎖
- 加鎖順序(線程按照一定的順序加鎖)
- 按照順序加鎖是一種死鎖預防機制,需要事先知道所有會用到的鎖
- 加鎖時限(超時則放棄)
- 獲取鎖時加上時限,超過時限則放棄請求,並釋放鎖,等待一段隨機的時間再重試
- 死鎖檢測與恢復
- 操作系統中:系統爲進程分配資源,不採取任何限制性措施,提供檢測和解脫死鎖的手段
死鎖檢測:當一個線程請求鎖失敗時,遍歷鎖的關係圖檢測死鎖。
死鎖恢復
- 撤消進程,剝奪資源
- 線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖
- 死鎖發生的時候設置隨機的優先級;如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。
鎖模式包括:
- 共享鎖:(讀取)用戶可以併發讀取數據,但不能獲取寫鎖,直到釋放所有讀鎖。
- 排他鎖(寫鎖):加上寫鎖後,其他線程無法加任何鎖;寫鎖可以讀和寫
- 更新鎖: 防止死鎖而設立,轉換讀鎖爲寫鎖之前的準備,僅一個線程可獲得更新鎖
樂觀鎖:認爲數據一般情況下不會造成衝突,在數據提交更新時,才進行數據的衝突檢測;
如果衝突,返回I信息讓用戶決定如何去做。實現方式:記錄數據版本。
悲觀鎖:操作數據時上鎖保護,限制其他線程訪問,直到該鎖釋放。關係型數據庫鎖機制,行鎖、頁鎖、表鎖,都是在做操作之前先上鎖。
鎖的粒度: 都是悲觀鎖
- 行鎖: 粒度最小,併發性最高
- 頁鎖:鎖定一頁。25個行鎖可升級爲一個頁鎖。
- 表鎖:粒度大,併發性低
- 數據庫鎖:控制整個數據庫操作
重入鎖 Reentrantlock
- 無阻塞的同步機制,實現了獨佔功能
- 加鎖和解鎖都需要顯式寫出,實現了Lock接口,注意一定要在適當時候unlock
- 添加了輪詢鎖、定時鎖等候和可中斷鎖特性;
- 提供了一個Condition(條件)類,用來實現分組喚醒線程
- 默認使用非公平鎖,可插隊跳過對線程隊列的處理
- ReentrantLock的內部類Sync繼承了AQS,分爲公平鎖FairSync和非公平鎖NonfairSync。
- 公平鎖:線程獲取鎖的順序和調用lock的順序一樣,FIFO;
- 非公平鎖:線程獲取鎖的順序和調用lock的順序無關
公平鎖爲了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷;直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快
不要拋棄 synchronized
- 易忘記 finally 塊釋放鎖,對程序有害
- synchronized 管理鎖定和釋放時,能標識死鎖或者其他異常行爲的來源,利於調試
- 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 一個用來構建鎖和同步工具的框架
- 使用int類型的volatile變量維護同步狀態(state),
- 使用Node實現FIFO隊列存放阻塞的等待線程,來完成線程的排隊執行
- 圍繞state提供兩種基本操作“獲取”和“釋放”
- 組合AQS對象的方式實現鎖的語義
AQS與鎖(如Lock)的對比:
- 鎖是面向使用者的,鎖定義了用戶調用的接口,隱藏了實現細節;
- AQS是鎖的實現者,通過用AQS簡化了鎖的實現屏蔽了同步狀態管理,線程的排隊,等待喚醒的底層操作。
- 簡而言之,鎖是面向使用者,AQS是鎖的具體實現者。