Java多線程之線程安全問(wèn)題詳解
一.線程安全概念
實(shí)際結(jié)果和預(yù)期結(jié)果不一樣,就是線程不安全
多線程帶給我們效率提升的同時(shí),也為我們帶來(lái)了風(fēng)險(xiǎn),因?yàn)槎嗑€程的搶占式執(zhí)行,帶來(lái)的隨機(jī)性。
count++看起來(lái)是一行代碼,實(shí)際上對(duì)應(yīng)了三個(gè)cpu指令
++操作本質(zhì)上要分為三步
1.load指令:先把內(nèi)存中的count值讀取到CPU的寄存器中
2. add指令:把CPU寄存器里的數(shù)值+1操作
3.save指令:把寄存器中的內(nèi)容保存回內(nèi)存上
我們想使用兩個(gè)線程將一個(gè)變量同時(shí)增加5000次
此時(shí)兩個(gè)線程的并發(fā)執(zhí)行,就相當(dāng)于兩組load add save指令并發(fā)執(zhí)行。所以這兩組指令的執(zhí)行順序存在了許多可能性。
package Thread;
public class Demo1{
private static int count=0;
public static void main(String[] args)throws InterruptedException{
Object obj=new Object();
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
每運(yùn)行一次,結(jié)果都是不同的,如下所示,這是我分別運(yùn)行三次的結(jié)果



推演圖(有很多種可能性)

一個(gè)線程的load必須在一個(gè)線程的save之后
二.線程安全問(wèn)題原因和如何解決線程安全問(wèn)題
最根本的原因: 搶占式執(zhí)行,隨機(jī)調(diào)度
我們上述線程代碼之所以不安全,因?yàn)樯婕暗轿覀儍蓚€(gè)線程同時(shí)去修改一個(gè)相同的變量。
一個(gè)線程修改一個(gè)變量,安全
多個(gè)線程讀取同一個(gè)變量,安全
多個(gè)線程修改多個(gè)不同的變量,安全
1.根本:操作系統(tǒng)對(duì)于線程的調(diào)度是隨機(jī)的,搶占式執(zhí)行
操作系統(tǒng)的底層設(shè)定,咱們左右不了
2.多線程同時(shí)修改同一個(gè)變量
和代碼的結(jié)構(gòu)直接相關(guān),可以通過(guò)調(diào)整代碼結(jié)構(gòu),規(guī)避一些線程不安全的代碼
(不夠通用,有的情況下,有時(shí)需要多線程修改同一個(gè)變量)
3.修改操作不是原子的
(java中解決線程安全問(wèn)題,最主要的方案)
計(jì)算機(jī)中的鎖和生活中的鎖,是同樣的概念,互斥/排他
加鎖,讓不是原子的,打包成一個(gè)原子的操作
一旦把鎖加上了,其他人要想加鎖,就得阻塞等待。
(eg:上廁所關(guān)門)不允許暴力拆鎖
加鎖操作,不是把線程鎖死到 cpu 上,禁止這個(gè)線程被調(diào)度,而是禁止其他線程重新加這個(gè)鎖,避免其他線程的操作在當(dāng)前線程執(zhí)行過(guò)程中,插隊(duì)
在代碼中如何實(shí)現(xiàn)呢?如下所示
用synchronized(){}將count++包裹起來(lái)

然后再寫一個(gè)locker對(duì)象,更改后的代碼塊如下所示
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(locker){
count++;
}
}
});兩個(gè)線程,針對(duì)同一個(gè)對(duì)象加鎖,才會(huì)產(chǎn)生互斥效果,(一個(gè)線程加上鎖了,另一個(gè)線程就得阻塞等待,等到第一個(gè)線程釋放鎖,才有機(jī)會(huì))
運(yùn)行結(jié)果為

三.Synchronized監(jiān)管鎖monitor lock
synchronized的核心作用就是確保??同一時(shí)刻只有一個(gè)線程??能夠執(zhí)行被它修飾的代碼(方法或代碼塊),其他線程必須等待當(dāng)前線程釋放鎖后才能繼續(xù)執(zhí)行。
在 Java 中,synchronized 關(guān)鍵字的底層實(shí)現(xiàn)依賴于監(jiān)視器鎖(Monitor Lock),它是一種基于 JVM 內(nèi)置的同步機(jī)制,用于保證多線程環(huán)境下共享資源的原子性、可見(jiàn)性和有序性。當(dāng)使用 synchronized 時(shí)若出現(xiàn)異常,錯(cuò)誤信息中常提到 “監(jiān)視器鎖”(如 IllegalMonitorStateException),這與它的特性和使用規(guī)則直接相關(guān)。
如圖所示


使用鎖的時(shí)候拋出一些異常,可能就會(huì)看到監(jiān)視器鎖這樣的報(bào)錯(cuò)信息
監(jiān)視器鎖(Monitor Lock)的核心特性
1. 可重入性(Reentrancy)
- 定義:一個(gè)線程已經(jīng)獲取了某個(gè)對(duì)象的監(jiān)視器鎖后,再次請(qǐng)求該對(duì)象的鎖時(shí)可以直接獲取(無(wú)需重新競(jìng)爭(zhēng)),即允許同一線程多次持有同一把鎖。
- 作用:避免死鎖(例如遞歸調(diào)用中,線程不會(huì)因重復(fù)獲取自己已持有的鎖而阻塞)。
代碼示例
(線程調(diào)用 methodA 獲取鎖后,調(diào)用 methodB 時(shí)可直接重入,無(wú)需等待。)
package Threadtest;
//synichronized 的可重入性代碼舉例
class Counter2 {
private int count = 0;
public void add() {
synchronized (this) { // 第一把鎖:this
count++;
}
}
public int get() {
return count;
}
}
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter = new Counter2();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized(counter){
counter.add();
}
}
});
t1.start();
t1.join();
System.out.println("count = " + counter.get());
}
}
2. 其他關(guān)鍵特性
- 獨(dú)占性:同一時(shí)間只能有一個(gè)線程持有監(jiān)視器鎖,其他線程嘗試獲取時(shí)會(huì)被阻塞(進(jìn)入等待隊(duì)列)。
- 釋放機(jī)制:鎖的釋放是自動(dòng)的 —— 當(dāng)同步代碼塊執(zhí)行完畢、拋出未捕獲異常或執(zhí)行
wait()時(shí),線程會(huì)釋放鎖。 - 關(guān)聯(lián)對(duì)象:監(jiān)視器鎖與具體對(duì)象綁定(
synchronized修飾非靜態(tài)方法時(shí)鎖是this,修飾靜態(tài)方法時(shí)鎖是類對(duì)象Class)。
四.死鎖的出現(xiàn)原因
兩次加鎖
死鎖(間接與監(jiān)視器鎖相關(guān))
死鎖代碼示例是在synichronized可重入性的代碼示例上的做的修改,可以通過(guò)觀察兩者的區(qū)別,更好的學(xué)習(xí)死鎖
觸發(fā)原因:多個(gè)線程相互持有對(duì)方需要的鎖,且都不釋放,導(dǎo)致無(wú)限阻塞。
本質(zhì):監(jiān)視器鎖的獨(dú)占性導(dǎo)致資源競(jìng)爭(zhēng)無(wú)法化解。
代碼觸發(fā)死鎖,線程就卡住了
構(gòu)成死鎖的原因
(1)鎖是互斥的
(2)鎖不可搶占/剝奪
注:1/2都是鎖的基本特性,所以只能通過(guò)破壞3/4打破死鎖
(3)請(qǐng)求和保持--保持鎖狀態(tài)的前提下,再請(qǐng)求另一把鎖
(4)循環(huán)等待
死鎖是怎么解決的
(1)加鎖的時(shí)候,不要嵌套,即避免鎖嵌套(打破3
(2)約定加鎖順序(打破4,如何約定,下文中有舉例解釋
(3)銀行家算法

class Counter2 {
private int count = 0;
// 新增第二把鎖
private final Object lock2 = new Object();
public void add() {
synchronized (this) { // 第一把鎖:this
synchronized (lock2) { // 第二把鎖:lock2
count++;
}
}
}
public int get() {
return count;
}
}
public class Demo19 {
public static void main(String[] args) {
Counter2 counter = new Counter2();
// 線程1:先拿this鎖,再等lock2鎖
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
// 線程2:先拿lock2鎖,再等this鎖
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (counter.lock2) { // 先獲取lock2鎖
synchronized (counter) { // 再嘗試獲取this鎖
// 此處可執(zhí)行任意操作(如空邏輯),關(guān)鍵是鎖的順序
}
}
}
});
t1.start();
t2.start();
// 等待線程執(zhí)行(實(shí)際死鎖時(shí)此代碼不會(huì)執(zhí)行到,但用于演示結(jié)構(gòu))
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + counter.get());
}
}死鎖經(jīng)典問(wèn)題:哲學(xué)家就餐問(wèn)題
描述 5 位哲學(xué)家圍坐在圓桌旁,每人左右各有一根筷子,思考時(shí)放下筷子,就餐時(shí)需要同時(shí)拿起左右兩根筷子。若每位哲學(xué)家同時(shí)拿起左手筷子,再等待右手筷子,會(huì)導(dǎo)致所有人都持有一根筷子并等待另一根,形成死鎖。

解決該問(wèn)題的方案(破壞死鎖條件)
死鎖的 4 個(gè)必要條件:互斥、持有并等待、不可剝奪、循環(huán)等待。只需破壞其中一個(gè)即可避免死鎖,以下是兩種經(jīng)典方案:
方案 1:固定筷子獲取順序(破壞 “循環(huán)等待”)
讓所有哲學(xué)家先拿編號(hào)小的筷子,再拿編號(hào)大的筷子,避免循環(huán)等待。
方案 2:最多允許 4 位哲學(xué)家同時(shí)拿筷子(破壞 “持有并等待”)
限制同時(shí)拿筷子的哲學(xué)家數(shù)量,保證至少有一位能拿到兩根筷子,用完后釋放資源。
五.什么是原子性?
我們把一段代碼想象成一個(gè)房間,每個(gè)線程就是要進(jìn)入這個(gè)房間的人。如果沒(méi)有任何機(jī)制保證,A進(jìn)入房間之后,還沒(méi)有出來(lái);B 是不是也可以進(jìn)入房間,打斷 A 在房間里的隱私。這個(gè)就是不具備原子性的。那我們應(yīng)該如何解決這個(gè)問(wèn)題呢?是不是只要給房間加一把鎖,A 進(jìn)去就把門鎖上,其他人是不是就進(jìn)不來(lái)了。這樣就保證了這段代碼的原子性了。
不保證原子性會(huì)給多線程帶來(lái)什么問(wèn)題?
一個(gè)線程正在對(duì)一個(gè)變量操作,中途其他線程插進(jìn)來(lái)了,對(duì)這個(gè)操作造成了打斷,可能會(huì)造成結(jié)果的錯(cuò)誤。這和線程的搶占式調(diào)度有關(guān),如果不是搶占式,就算不是原子性,也問(wèn)題不大。
六.內(nèi)存可見(jiàn)性
ava內(nèi)存模型(JMM): java虛擬機(jī)規(guī)定了java內(nèi)存模型。
目的: 屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,實(shí)現(xiàn)java程序在各平臺(tái)下都達(dá)成一致的并發(fā)效果
這個(gè)時(shí)候代碼就會(huì)出現(xiàn)問(wèn)題。
七.指令重排序
指令重排序?qū)嶋H上也是編譯器優(yōu)化,簡(jiǎn)單的來(lái)說(shuō),就是我們把一個(gè)東西寫的太爛了,JVM.CPU指令集會(huì)對(duì)其進(jìn)行優(yōu)化。
編譯器對(duì)于指令重排序的前提是 “保持邏輯不發(fā)生變化”. 這一點(diǎn)在單線程環(huán)境下比較容易判斷, 但是在多線程環(huán)境下就沒(méi)那么容易了, 多線程的代碼執(zhí)行復(fù)雜程度更高, 編譯器很難在編譯階段對(duì)代碼的執(zhí)行效果進(jìn)行預(yù)測(cè), 因此激進(jìn)的重排序很容易導(dǎo)致優(yōu)化后的邏輯和之前不等價(jià)
八.內(nèi)存可見(jiàn)性
什么是可見(jiàn)性
可見(jiàn)性:一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到
共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這幾個(gè)線程的共享變量
volatile如何實(shí)現(xiàn)內(nèi)存的可見(jiàn)性
當(dāng)編譯器優(yōu)化處bug,使用這個(gè)關(guān)鍵字修飾的變量,就屬于“易失”,必須每次重新讀取內(nèi)存
深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的
對(duì)volatile變量執(zhí)行寫操作時(shí),會(huì)在寫操作后加入一條store屏障指令
對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在讀操作前加入一條load屏障指令
總結(jié)volatile核心作用:
- 可見(jiàn)性:一個(gè)線程修改
volatile變量后,其他線程能立即看到最新值。 - 有序性:禁止指令重排,保證代碼執(zhí)行順序與預(yù)期一致。
- 不保證原子性:不能替代鎖解決多線程并發(fā)修改問(wèn)題。
public class VolatileDemo {
// 用 volatile 修飾共享變量,保證多線程可見(jiàn)性
private static volatile boolean flag = false;
public static void main(String[] args) {
// 線程1:等待 flag 變?yōu)?true
new Thread(() -> {
System.out.println("線程1啟動(dòng),等待 flag 變?yōu)?true...");
while (!flag) {
// 循環(huán)等待,直到 flag 被修改
}
System.out.println("線程1檢測(cè)到 flag 為 true,退出循環(huán)");
}).start();
// 主線程:休眠1秒后修改 flag
try {
Thread.sleep(1000); // 確保線程1先啟動(dòng)
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("主線程已將 flag 修改為 true");
}
}九.synchronized和volatile比較
volatile不需要加鎖,比synchronized更輕量級(jí),不會(huì)阻塞線程
從內(nèi)存可見(jiàn)性角度講,volatile讀相當(dāng)于加鎖,volatile寫相當(dāng)于解鎖
synchronized既能保證可見(jiàn)性,又能保證原子性,而volatile只能保證可見(jiàn)性,無(wú)法保證原子性;
總結(jié)
到此這篇關(guān)于Java多線程之線程安全問(wèn)題詳解的文章就介紹到這了,更多相關(guān)Java線程安全問(wèn)題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 實(shí)戰(zhàn)項(xiàng)目之小說(shuō)在線閱讀系統(tǒng)的實(shí)現(xiàn)流程
讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)前臺(tái)閱讀后臺(tái)管理的小說(shuō)在線閱讀系統(tǒng),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平2021-11-11
SpringBoot集成Liquibase的詳細(xì)步驟
Liquibase 是一個(gè)強(qiáng)大的數(shù)據(jù)庫(kù)版本控制工具,能夠與 Spring Boot2024-12-12
無(wú)縫集成,以簡(jiǎn)化數(shù)據(jù)庫(kù)遷移和管理的過(guò)程,以下是從開(kāi)始集成到最終運(yùn)行的詳細(xì)步驟,需要的朋友可以參考下
Mybatis中的config.xml配置文件詳細(xì)解析
這篇文章主要介紹了詳解Mybatis-config.xml配置文件,需要的朋友可以參考下2017-12-12
SWT(JFace)體驗(yàn)之ApplicationWindow
SWT(JFace)體驗(yàn)之ApplicationWindow2009-06-06
mybatis教程之查詢緩存(一級(jí)緩存二級(jí)緩存和整合ehcache)
這篇文章主要介紹了mybatis教程之查詢緩存(一級(jí)緩存二級(jí)緩存和整合ehcache),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05

