Java多線程編程中的并發(fā)安全問題及解決方法
線程安全性
線程安全是指我們所寫的代碼在并發(fā)情況下使用時,總是能表現(xiàn)出正確的行為;反之,未實(shí)現(xiàn)線程安全的代碼,表現(xiàn)的行為是不可預(yù)知的,有可能正確,
實(shí)現(xiàn)線程安全的方式:
線程封閉
就是把對象封裝到一個線程里,只有這一個線程能看到此對象。實(shí)現(xiàn)線程封閉的方式如下:
棧封閉
這里是指每個線程自己的線程棧,方法的局部變量就是在線程棧中的,對于其他線程是不可見的
ThreadLocal
各個線程Thread對象維護(hù)了一份Map,對于其他線程是不可見的
無狀態(tài)的類
沒有任何成員變量的類,就叫無狀態(tài)的類,這種類一定是線程安全的。
讓類不可變
沒有成員變量的類畢竟是少數(shù),我們還可以讓類的成員變量不可變,給他們加上final
關(guān)鍵字
如果成員變量是一個對象,final不能保證類的安全性,因?yàn)殡m然對象的引用不會變,但是在堆上的對象實(shí)例可能被多個線程同時修改,沒有正確處理的情況下,對象實(shí)例在堆中的數(shù)據(jù)是不可預(yù)知的。
加鎖或CAS
synchronized、顯示鎖Look、原子Atomic操作、CAS機(jī)制等等
死鎖
定義
是指兩個或兩個以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖。
- 死鎖是必然發(fā)生在多操作者(M>=2 個)爭奪多個資源(N>=2 個,且 N<=M) 才會發(fā)生這種情況。
- 爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產(chǎn)生死鎖;
- 爭奪者對拿到的資源不放手,也不能被掠奪。
學(xué)術(shù)化的定義。死鎖的發(fā)生必須具備以下四個必要條件。
- 互斥條件:指進(jìn)程對所分配到的資源進(jìn)行排它性使用,即在一段時間內(nèi)某資源只由一個進(jìn)程占用。如果此時還有其它進(jìn)程請求資源,則請求者只能等待, 直至占有資源的進(jìn)程用畢釋放。
- 請求和保持條件:指進(jìn)程已經(jīng)保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進(jìn)程占有,此時請求進(jìn)程阻塞,但又對自己已獲得的其它資源保持不放。
- 不剝奪條件:指進(jìn)程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
- 環(huán)路等待條件:指在發(fā)生死鎖時,必然存在一個進(jìn)程——資源的環(huán)形鏈, 即進(jìn)程集合{P0,P1,P2,···,Pn}中的 P0 正在等待一個 P1 占用的資源;P1 正在等待 P2 占用的資源,……,Pn 正在等待已被 P0 占用的資源。
只要打破四個必要條件之一就能有效預(yù)防死鎖的發(fā)生。
- 打破互斥條件:改造獨(dú)占性資源為虛擬資源,大部分資源已無法改造。
- 打破不可搶占條件:當(dāng)一進(jìn)程占有一獨(dú)占性資源后又申請一獨(dú)占性資源而無 法滿足,則退出原占有的資源。
- 打破占有且申請條件:采用資源預(yù)先分配策略,即進(jìn)程運(yùn)行前申請全部資源, 滿足則運(yùn)行,不然就等待,這樣就不會占有且申請。
- 打破循環(huán)等待條件:實(shí)現(xiàn)資源有序分配策略,對所有設(shè)備實(shí)現(xiàn)分類編號,所有進(jìn)程只能采用按序號遞增的形式申請資源。
避免死鎖常見的算法有有序資源分配法、銀行家算法。
實(shí)現(xiàn)一個死鎖
/** * @Description: 實(shí)現(xiàn)一個簡單的死鎖程序 */ public class DeadLookTest { private static Object o1 = new Object(); private static Object o2 = new Object(); public void fastLock() throws InterruptedException { synchronized(o1){ Thread.sleep(2000); System.out.println("fast"); synchronized (o2){ System.out.println("----"); } } } public void postLock() throws InterruptedException { synchronized(o2){ Thread.sleep(2000); System.out.println("post"); synchronized (o1){ System.out.println("----"); } } } public static void main(String[] args) throws InterruptedException { DeadLookTest deadlook = new DeadLookTest(); // 新開一個線程去調(diào)用其中一個方法 new Thread(() -> { try { deadlook.postLock(); } catch (InterruptedException e) { } }).start(); deadlook.fastLock(); } }
查看死鎖
使用jstack
命令查看死鎖結(jié)果
C:\Users\Administrator>jps
4784 DeadLookTest
9808 RemoteMavenServer36
2052 Launcher
2692 Jps
8572C:\Users\Administrator>jstack 4784
......
Found one Java-level deadlock:
=============================
"Thread-0":
waiting to lock monitor 0x0000025d5a58fd88 (object 0x0000000716edb910, a java.lang.Object),
which is held by "main"
"main":
waiting to lock monitor 0x0000025d5a58e998 (object 0x0000000716edb920, a java.lang.Object),
which is held by "Thread-0"Java stack information for the threads listed above:
===================================================
"Thread-0":
at cn.tulingxueyuan.safe.dl.DeadLookTest.postLock(DeadLookTest.java:30)
- waiting to lock <0x0000000716edb910> (a java.lang.Object)
- locked <0x0000000716edb920> (a java.lang.Object)
at cn.tulingxueyuan.safe.dl.DeadLookTest.lambda$main$0(DeadLookTest.java:39)
at cn.tulingxueyuan.safe.dl.DeadLookTest$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"main":
at cn.tulingxueyuan.safe.dl.DeadLookTest.fastLock(DeadLookTest.java:20)
- waiting to lock <0x0000000716edb920> (a java.lang.Object)
- locked <0x0000000716edb910> (a java.lang.Object)
at cn.tulingxueyuan.safe.dl.DeadLookTest.main(DeadLookTest.java:43)Found 1 deadlock.
解決死鎖
我們現(xiàn)在通過上面的命令找到了產(chǎn)生死鎖的位置,那么如何取解決死鎖嘞?我們知道產(chǎn)生死鎖的原因如下:
- 死鎖是必然發(fā)生在多操作者(M>=2 個)爭奪多個資源(N>=2 個,且 N<=M) 才會發(fā)生這種情況。
- 爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產(chǎn)生死鎖;
- 爭奪者對拿到的資源不放手,也不能被掠奪。
第一個條件一般都是業(yè)務(wù)必須要,那么打破死鎖就要從下面的兩個條件去解決
- 保證爭奪鎖資源的順序一樣。
在實(shí)際的開發(fā)中可能會存在比較隱蔽的加鎖順序,比如鎖對象作為方法參數(shù)傳遞,如下所示
private static void businessDo(Object first,Object second) throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (first){ System.out.println(threadName + " get first"); Thread.sleep(1000); synchronized (second){ System.out.println(threadName + " get second"); } } } // 然后兩個線程,在調(diào)用的時候傳遞的參數(shù)順序卻不一樣 businessDo(No1,No2); businessDo(No2,No1);
解決上面這種問題的方式是:在加鎖前,在方法中做一個內(nèi)部的排序
public class SafeOperate { private static Object No13 = new Object();//第一個鎖 private static Object No14 = new Object();//第二個鎖 private static Object tieLock = new Object();//第三把鎖 public void transfer(Object first,Object second) throws InterruptedException { int firstHash = System.identityHashCode(first); int secondHash = System.identityHashCode(second); if(firstHash<secondHash){ synchronized (first){ System.out.println(Thread.currentThread().getName()+" get "+first); Thread.sleep(100); synchronized (second){ System.out.println(Thread.currentThread().getName()+" get "+second); } } }else if(secondHash<firstHash){ synchronized (second){ System.out.println(Thread.currentThread().getName()+" get"+second); Thread.sleep(100); synchronized (first){ System.out.println(Thread.currentThread().getName()+" get"+first); } } }else{ // 萬一兩個對象的hash值一樣,那么就引入第三把鎖,誰先搶到第三把鎖就去進(jìn)行前兩兩把鎖的加鎖 synchronized (tieLock){ synchronized (first){ synchronized (second){ System.out.println(Thread.currentThread().getName()+" get"+first); System.out.println(Thread.currentThread().getName()+" get"+second); } } } } } }
對拿到的鎖資源嘗試釋放
這種方式對于synchronized是不適用的,因?yàn)樗荒玫芥i誓不罷休。使用ReentrantLock,使用其中的tryLock(long time, TimeUnit unit)
方法,在指定的時間中如果還沒有拿到鎖就去進(jìn)行釋放的邏輯
大致是實(shí)現(xiàn)邏輯如下所示
while(true){ if(No13.tryLock()){ System.out.println(threadName +" get 13"); // 如果沒有拿到No14的鎖,那么No13的鎖也釋放 try{ if(No14.tryLock()){ try{ System.out.println(threadName +" get 14"); System.out.println("zhouYuDo do work------------"); break; }finally{ No14.unlock(); } } }finally { No13.unlock(); } } // 如果不加休眠機(jī)制,那么就比較容易產(chǎn)生活鎖 Thread.sleep(1000); }
其他線程安全問題
活鎖
兩個線程在嘗試拿鎖的機(jī)制中,發(fā)生多個線程之間互相謙讓,不斷發(fā)生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因?yàn)槟貌坏?,而將本來已?jīng)持有的鎖釋放的過程。
解決辦法:每個線程休眠隨機(jī)數(shù),錯開拿鎖的時間。
線程饑餓
低優(yōu)先級的線程,總是拿不到執(zhí)行時間
單例模式
- DCL雙重檢測機(jī)制
- volatile關(guān)鍵字禁止指令重排
public class HungrySingleton { //創(chuàng)建 SingletonHungry 的一個對象 private static volatile HungrySingleton instance; // 讓構(gòu)方法私有,這樣該類就不會被其它類實(shí)例化 private HungrySingleton() { } //獲取唯一可用的對象 public static HungrySingleton getInstance() { if(null == instance) { synchronized{ if(null == instance) { instance = new LazySingleton(); } } } return instance; } }
到此這篇關(guān)于Java多線程編程中的并發(fā)安全問題及解決方法的文章就介紹到這了,更多相關(guān)Java多線程并發(fā)安全內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot-admin整合Quartz實(shí)現(xiàn)動態(tài)管理定時任務(wù)的過程詳解
Quartz是一款Java編寫的開源任務(wù)調(diào)度框架,同時它也是Spring默認(rèn)的任務(wù)調(diào)度框架,它的作用其實(shí)類似于Timer定時器以及ScheduledExecutorService調(diào)度線程池,這篇文章主要介紹了Springboot-admin整合Quartz實(shí)現(xiàn)動態(tài)管理定時任務(wù),需要的朋友可以參考下2023-04-04Springboot事件和bean生命周期執(zhí)行機(jī)制實(shí)例詳解
這篇文章主要介紹了Springboot事件和bean的生命周期執(zhí)行機(jī)制,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03如何使用Comparator比較接口實(shí)現(xiàn)ArrayList集合排序
這篇文章主要介紹了如何使用Comparator比較接口實(shí)現(xiàn)ArrayList集合排序問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12SpringMVC攔截器實(shí)現(xiàn)登錄認(rèn)證
這篇文章主要介紹了SpringMVC攔截器實(shí)現(xiàn)登錄認(rèn)證的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Springboot整合Swagger2后訪問swagger-ui.html 404報錯問題解決方案
這篇文章主要介紹了Springboot整合Swagger2后訪問swagger-ui.html 404報錯,本文給大家分享兩種解決方案,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06Lombok使用@Tolerate實(shí)現(xiàn)沖突兼容問題
這篇文章主要介紹了Lombok使用@Tolerate實(shí)現(xiàn)沖突兼容問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08