线程安全问题的 3 种解决方案!
作者 | 磊哥
來(lái)源 | Java面試真題解析(ID:aimianshi666)
轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone)
線程安全是指某個(gè)方法或某段代碼,在多線程中能夠正確的執(zhí)行,不會(huì)出現(xiàn)數(shù)據(jù)不一致或數(shù)據(jù)污染的情況,我們把這樣的程序稱(chēng)之為線程安全的,反之則為非線程安全的。在 Java 中,解決線程安全問(wèn)題有以下 3 種手段:
使用線程安全類(lèi),比如 AtomicInteger。
加鎖排隊(duì)執(zhí)行
使用 synchronized 加鎖。
使用 ReentrantLock 加鎖。
使用線程本地變量 ThreadLocal。
接下來(lái)我們逐個(gè)來(lái)看它們的實(shí)現(xiàn)。
線程安全問(wèn)題演示
我們創(chuàng)建一個(gè)變量 number 等于 0,之后創(chuàng)建線程 1,執(zhí)行 100 萬(wàn)次 ++ 操作,同時(shí)再創(chuàng)建線程 2 執(zhí)行 100 萬(wàn)次 -- 操作,等線程 1 和線程 2 都執(zhí)行完之后,打印 number 變量的值,如果打印的結(jié)果為 0,則說(shuō)明是線程安全的,否則則為非線程安全的,示例代碼如下:
public?class?ThreadSafeTest?{//?全局變量private?static?int?number?=?0;//?循環(huán)次數(shù)(100W)private?static?final?int?COUNT?=?1_000_000;public?static?void?main(String[]?args)?throws?InterruptedException?{//?線程1:執(zhí)行 100W 次?++?操作Thread?t1?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{number++;}});t1.start();//?線程2:執(zhí)行 100W 次?--?操作Thread?t2?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{number--;}});t2.start();//?等待線程?1?和線程?2,執(zhí)行完,打印?number?最終的結(jié)果t1.join();t2.join();System.out.println("number 最終結(jié)果:"?+?number);} }以上程序的執(zhí)行結(jié)果如下圖所示:從上述執(zhí)行結(jié)果可以看出,number 變量最終的結(jié)果并不是 0,和預(yù)期的正確結(jié)果不相符,這就是多線程中的線程安全問(wèn)題。
解決線程安全問(wèn)題
1.原子類(lèi)AtomicInteger
AtomicInteger 是線程安全的類(lèi),使用它可以將 ++ 操作和 -- 操作,變成一個(gè)原子性操作,這樣就能解決非線程安全的問(wèn)題了,如下代碼所示:
import?java.util.concurrent.atomic.AtomicInteger;public?class?AtomicIntegerExample?{//?創(chuàng)建?AtomicIntegerprivate?static?AtomicInteger?number?=?new?AtomicInteger(0);//?循環(huán)次數(shù)private?static?final?int?COUNT?=?1_000_000;public?static?void?main(String[]?args)?throws?InterruptedException?{//?線程1:執(zhí)行 100W 次?++?操作Thread?t1?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?++?操作number.incrementAndGet();}});t1.start();//?線程2:執(zhí)行 100W 次?--?操作Thread?t2?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?--?操作number.decrementAndGet();}});t2.start();//?等待線程?1?和線程?2,執(zhí)行完,打印?number?最終的結(jié)果t1.join();t2.join();System.out.println("最終結(jié)果:"?+?number.get());} }以上程序的執(zhí)行結(jié)果如下圖所示:
2.加鎖排隊(duì)執(zhí)行
Java 中有兩種鎖:synchronized 同步鎖和 ReentrantLock 可重入鎖。
2.1 同步鎖synchronized
synchronized 是 JVM 層面實(shí)現(xiàn)的自動(dòng)加鎖和自動(dòng)釋放鎖的同步鎖,它的實(shí)現(xiàn)代碼如下:
public?class?SynchronizedExample?{//?全局變量private?static?int?number?=?0;//?循環(huán)次數(shù)(100W)private?static?final?int?COUNT?=?1_000_000;public?static?void?main(String[]?args)?throws?InterruptedException?{//?線程1:執(zhí)行 100W 次?++?操作Thread?t1?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?加鎖排隊(duì)執(zhí)行synchronized?(SynchronizedExample.class)?{number++;}}});t1.start();//?線程2:執(zhí)行 100W 次?--?操作Thread?t2?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?加鎖排隊(duì)執(zhí)行synchronized?(SynchronizedExample.class)?{number--;}}});t2.start();//?等待線程?1?和線程?2,執(zhí)行完,打印?number?最終的結(jié)果t1.join();t2.join();System.out.println("number 最終結(jié)果:"?+?number);} }以上程序的執(zhí)行結(jié)果如下圖所示:
2.2 可重入鎖ReentrantLock
ReentrantLock 可重入鎖需要程序員自己加鎖和釋放鎖,它的實(shí)現(xiàn)代碼如下:
import?java.util.concurrent.locks.ReentrantLock;/***?使用?ReentrantLock?解決非線程安全問(wèn)題*/ public?class?ReentrantLockExample?{//?全局變量private?static?int?number?=?0;//?循環(huán)次數(shù)(100W)private?static?final?int?COUNT?=?1_000_000;//?創(chuàng)建?ReentrantLockprivate?static?ReentrantLock?lock?=?new?ReentrantLock();public?static?void?main(String[]?args)?throws?InterruptedException?{//?線程1:執(zhí)行 100W 次?++?操作Thread?t1?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{lock.lock();????//?手動(dòng)加鎖number++;???????//?++?操作lock.unlock();??//?手動(dòng)釋放鎖}});t1.start();//?線程2:執(zhí)行 100W 次?--?操作Thread?t2?=?new?Thread(()?->?{for?(int?i?=?0;?i?<?COUNT;?i++)?{lock.lock();????//?手動(dòng)加鎖number--;???????//?--?操作lock.unlock();??//?手動(dòng)釋放鎖}});t2.start();//?等待線程?1?和線程?2,執(zhí)行完,打印?number?最終的結(jié)果t1.join();t2.join();System.out.println("number 最終結(jié)果:"?+?number);} }以上程序的執(zhí)行結(jié)果如下圖所示:
3.線程本地變量ThreadLocal
使用 ThreadLocal 線程本地變量也可以解決線程安全問(wèn)題,它是給每個(gè)線程獨(dú)自創(chuàng)建了一份屬于自己的私有變量,不同的線程操作的是不同的變量,所以也不會(huì)存在非線程安全的問(wèn)題,它的實(shí)現(xiàn)代碼如下:
public?class?ThreadSafeExample?{//?創(chuàng)建?ThreadLocal(設(shè)置每個(gè)線程中的初始值為?0)private?static?ThreadLocal<Integer>?threadLocal?=?ThreadLocal.withInitial(()?->?0);//?全局變量private?static?int?number?=?0;//?循環(huán)次數(shù)(100W)private?static?final?int?COUNT?=?1_000_000;public?static?void?main(String[]?args)?throws?InterruptedException?{//?線程1:執(zhí)行 100W 次?++?操作Thread?t1?=?new?Thread(()?->?{try?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?++?操作threadLocal.set(threadLocal.get()?+?1);}//?將?ThreadLocal?中的值進(jìn)行累加number?+=?threadLocal.get();}?finally?{threadLocal.remove();?//?清除資源,防止內(nèi)存溢出}});t1.start();//?線程2:執(zhí)行 100W 次?--?操作Thread?t2?=?new?Thread(()?->?{try?{for?(int?i?=?0;?i?<?COUNT;?i++)?{//?--?操作threadLocal.set(threadLocal.get()?-?1);}//?將?ThreadLocal?中的值進(jìn)行累加number?+=?threadLocal.get();}?finally?{threadLocal.remove();?//?清除資源,防止內(nèi)存溢出}});t2.start();//?等待線程?1?和線程?2,執(zhí)行完,打印?number?最終的結(jié)果t1.join();t2.join();System.out.println("最終結(jié)果:"?+?number);} }以上程序的執(zhí)行結(jié)果如下圖所示:
總結(jié)
在 Java 中,解決線程安全問(wèn)題的手段有 3 種:
1.使用線程安全的類(lèi),如 AtomicInteger 類(lèi);
2.使用鎖 synchronized 或 ReentrantLock 加鎖排隊(duì)執(zhí)行;
3.使用線程本地變量 ThreadLocal 來(lái)處理。
是非審之于己,毀譽(yù)聽(tīng)之于人,得失安之于數(shù)。
公眾號(hào):Java面試真題解析
面試合集:https://gitee.com/mydb/interview
往期推薦面試突擊36:線程安全問(wèn)題是如何產(chǎn)生的?
每周匯總 | Java面試題(共35篇)2022版
面試突擊35:如何判斷線程池已經(jīng)執(zhí)行完所有任務(wù)了?
總結(jié)
以上是生活随笔為你收集整理的线程安全问题的 3 种解决方案!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一文掌握Redisson分布式锁原理|干
- 下一篇: Zookeeper 的 5 大核心知识点