Java?synchronized與死鎖深入探究
1.synchronized的特性
1). 互斥性
當某個線程執(zhí)行到 synchronized 所修飾的對象時 , 該線程對象會加鎖(lock) , 其他線程如果執(zhí)行到同一個對象的 synchronized 就會產(chǎn)生阻塞等待.
- 進入 synchronized 修飾的代碼塊 , 相當于加鎖.
- 退出 synchronized 修飾著代碼塊 , 相當于解鎖.
synchronized 使用的鎖存儲在Java對象里 , 可以理解為每個對象在內(nèi)存中存儲時 , 都有一塊內(nèi)存表示當前的鎖定狀態(tài).類似于公廁的"有人" , "無人".
如果是"無人"狀態(tài) , 此時就可以使用 , 使用時需設置為"有人"狀態(tài).
如果是"有人"狀態(tài) , 此時就需要排隊等待.
如果理解阻塞等待?
針對每一把鎖 , 操作系統(tǒng)都會維護一個等待隊列 , 當一個線程獲取到這個鎖之后 , 其他線性再嘗試獲取這個鎖 , 就會獲取不到鎖 , 陷入阻塞等待. 一直等到之前這個線程釋放鎖后 , 操作系統(tǒng)才會喚醒其他線程來再次競爭這個鎖.
2)可重入
synchronized 對同一個線程來說是可重入的 , 不會出現(xiàn)把自己鎖死的情況.
如何理解把自己鎖死?
觀察下面這段代碼可以發(fā)現(xiàn) , 當某個線程調(diào)用add方法時 , 就會對 this 對象先加鎖 , 接著進入代碼塊又會對 this 對象再次嘗試加鎖. 站在 this 對象的角度 , 它認為自己已經(jīng)被另外的線程占用了 , 那么第二次加鎖是否需要阻塞等待呢? 如果運行上述情況 , 那么這個鎖就是可重入的 , 否則就是不可重入的.不可重入鎖會導致出現(xiàn)死鎖 , 而Java中的 synchronized 是可重入鎖 , 因此沒有上述問題.
synchronized public void add(){ synchronized (this) { count++; } }
在可重入鎖內(nèi)部 , 包含了"線程持有者"和"計數(shù)器"兩個信息.
- 如果每個線程加鎖時 , 發(fā)現(xiàn)鎖以及被占用了 , 但加鎖的人是它自己 , 那么仍然可以獲取到鎖 , 讓計數(shù)器自增.
- 解鎖的時候當計數(shù)器遞減到0時 , 才真正釋放鎖.
2.synchronized使用示例:
1). 修飾普通方法
鎖的是 Counter 對象.
class Counter{ public int count; synchronized public void add(){ count++; } }
2). 修飾靜態(tài)方法
鎖的是 Counter 類
class Counter{ public int count; synchronized public static void add(){ count++; } }
3).修飾代碼塊.明確指定鎖哪個對象
鎖當前對象:
class Counter{ public int count; public void add(){ synchronized (this) { count++; } } }
鎖類對象:
class Counter{ public int count; public void add(){ synchronized (Counter.class) { count++; } } }
類鎖和對象鎖有什么區(qū)別?
顧名思義 , 對象鎖用來鎖住當前對象 , 類鎖用來鎖住當前類.如果一個類有多個實例對象 , 那么如果對其中一個對象加鎖 , 別的線程只會在訪問這個對象時阻塞等待 , 訪問其他對象時沒有影響.但如果是類鎖 , 那么當一個線程對這個類加鎖后 , 其他線程訪問該類的所有對象都要阻塞等待.
3.Java標準庫中的線程安全類
Java 標準庫中有很多線程是不安全的 , 這些類可能涉及多線程修改共享數(shù)據(jù) , 卻又沒有任何加鎖措施.
- ArrayList
- LinkedList
- HashMap
- HashSet
- TreeSet
- StringBuilder
但還有一些是線程安全的 , 使用一些鎖機制來控制.
- Vector
- HashTable
- CurrentHashMap
- StringBuffer
@Override @IntrinsicCandidate public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
這些線程之所以不加鎖是因為 , 加鎖會損失部分性能.
4.死鎖是什么
死鎖是這樣一種情況 , 多個線程同時被阻塞 , 其中一個或全部都在等待某個資源被釋放.由于線程被無限期的阻塞 , 因此程序不可能正常終止.
死鎖的三個典型情況
1). 一個線程一把鎖 , 連續(xù)加兩次 , 如果鎖是不可重入鎖 , 就會死鎖. Java中的synchronized和ReentranLock 都是可重入鎖 , 因此不會出現(xiàn)上述問題.
2). 兩個線程兩把鎖 , t1 和 t2 線程各種先針對鎖A和鎖B加鎖 , 再嘗試獲取對方的鎖.
例如 , 張三和女神去吃餃子 , 需要蘸醋和醬油 , 張三拿到醋 , 女神拿到醬油 , 張三對女神說:"你先把醬油給我 , 我用完就把醋給你" , 女神對張三說:"你先把醋給我 , 我用完就把醬油給你". 這時兩人爭執(zhí)不下 , 就構(gòu)成了死鎖 , 醋和醬油就是兩把鎖 , 張三和女生就是兩個線程.
public static void main(String[] args) { Object jiangyou = new Object(); Object cu = new Object(); Thread zhangsan = new Thread(()->{ synchronized (jiangyou){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (cu ){ System.out.println("張三把醬油和醋都拿到了"); } } }); Thread nvsheng = new Thread(()->{ synchronized (cu ){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou){ System.out.println("女神把醬油和醋都拿到了"); } } }); zhangsan.start(); nvsheng.start(); }
執(zhí)行代碼后 , 發(fā)現(xiàn)沒有打印任何日志 , 說明沒有線程拿到兩把鎖.
通過jconsole查看線程的情況:
3)多個線程多把鎖
例如常見經(jīng)典案例--"哲學家就餐問題"
假設有五個哲學家圍著桌子吃飯 , 每個人中間放一個筷子 , 哲學家有兩種狀態(tài) , 1.思考人生(相當于線程的阻塞狀態(tài)) , 2.拿起筷子吃面條(相當于線程獲取到鎖執(zhí)行計算) , 由于操作系統(tǒng)的隨機調(diào)度 , 這五個哲學家隨時都可能想吃面條 , 也隨時都可能思考人生 , 但是想要吃面條就得同時拿起左右兩個筷子.
假設同一時刻 , 所有哲學家同時拿起左手的筷子 , 所有的哲學家都拿不起右手的筷子 , 就會產(chǎn)生死鎖.
死鎖是一個嚴重的"BUG" , 導致一個程序的線程"卡死"無法正常工作.
5.如果避免死鎖
死鎖的四個必要條件:
1.互斥使用: 當資源被一個線程占有時 , 別的線程不能使用
2.不可搶占: 資源請求者不能從資源獲取者手中奪取資源 , 只能等資源占有者主動釋放.
3.請求和保持: 當資源請求者請求獲取別的資源時 , 保存對原有資源的占有.
4.循環(huán)等待: 即存在一個等待隊列 , P1占有P2的資源 , P2占有P3的資源 , P3占有P1的資 源, 這樣就形成一個等待回路.
當上述四個條件都成立就會形成死鎖 , 當然破壞其中一個條件也可以打破死鎖 , 對于synchronized 來說 , 前三個條件是鎖的基本特性 , 因此想要打破死鎖只能從"循環(huán)等待"入手.
如何破除死鎖?
如果我們給鎖編號 , 然后指定一個固定的順序來加鎖(必然從小到大) , 任意線程加多把鎖的時候都遵循上述順序, 此時循環(huán)等待自然破除.
因此解決哲學家就餐問題就可以給每個筷子編號 , 每個人都遵守"先拿小的再拿大的順序".此時1號哲學家和2號哲學家為了競爭筷子其中一個人就會阻塞等待 , 這時5號哲學家就有了可乘之機 , 5號哲學家拿起4號和5號筷子吃完面條 , 四號哲學家重復上述操作也吃完面條 , 這樣就完美的打破了循環(huán)等待的問題.
同樣 , 最初的張三和女神吃餃子問題也是同樣的解決方式 , 規(guī)定兩人都按"先拿醋再拿餃子"的順序執(zhí)行 , 就可以完美解決死鎖問題.
public static void main(String[] args) { Object jiangyou = new Object(); Object cu = new Object(); Thread zhangsan = new Thread(()->{ synchronized (cu){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou ){ System.out.println("張三把醬油和醋都拿到了"); } } }); Thread nvsheng = new Thread(()->{ synchronized (cu ){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou){ System.out.println("女神把醬油和醋都拿到了"); } } }); zhangsan.start(); nvsheng.start(); }
到此這篇關于Java synchronized與死鎖深入探究的文章就介紹到這了,更多相關Java synchronized 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java?synchronized同步關鍵字工作原理
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級鎖實現(xiàn)過程淺析
- Java synchronized重量級鎖實現(xiàn)過程淺析
- Java @Transactional與synchronized使用的問題
- Java synchronized與CAS使用方式詳解
- 淺析Java關鍵詞synchronized的使用
- synchronized及JUC顯式locks?使用原理解析
- java鎖synchronized面試常問總結(jié)
- Java?HashTable與Collections.synchronizedMap源碼深入解析
- Java?Synchronized鎖的使用詳解
- AQS加鎖機制Synchronized相似點詳解
- Java必會的Synchronized底層原理剖析
- 一個例子帶你看懂Java中synchronized關鍵字到底怎么用
- 詳解Java?Synchronized的實現(xiàn)原理
- Synchronized?和?ReentrantLock?的實現(xiàn)原理及區(qū)別
- Java同步鎖synchronized用法的最全總結(jié)
相關文章
SpringBoot2零基礎到精通之JUnit 5與指標監(jiān)控
SpringBoot是一種整合Spring技術棧的方式(或者說是框架),同時也是簡化Spring的一種快速開發(fā)的腳手架,本篇讓我們一起學習JUnit 5與指標監(jiān)控2022-03-03SpringBoot同一接口多個實現(xiàn)類配置的實例詳解
這篇文章主要介紹了SpringBoot同一接口多個實現(xiàn)類配置的實例詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11將字符串數(shù)字格式化為樣式1,000,000,000的方法
這篇文章主要介紹了將字符串數(shù)字格式化為樣式1,000,000,000的方法,有需要的朋友可以參考一下2014-01-01JAVA?ServLet創(chuàng)建一個項目的基本步驟
Servlet是Server Applet的簡稱,是運行在服務器上的小程序,用于編寫Java的服務器端程序,它的主要作用是接收并響應來自Web客戶端的請求,下面這篇文章主要給大家介紹了關于JAVA?ServLet創(chuàng)建一個項目的基本步驟,需要的朋友可以參考下2024-03-03