??????點(diǎn)進(jìn)來你就是我的人了
博主主頁:??????戳一戳,歡迎大佬指點(diǎn)!歡迎志同道合的朋友一起加油喔??????
目錄
前言
1. 造成線程不安全的原因有哪些呢?
1.1什么是原子性
1.2什么是內(nèi)存可見性
1.3共享變量可見性實(shí)現(xiàn)的原理
?1.4 什么是指令重排序
2.解決線程安全問題
2.1 引入關(guān)鍵字synchronized解決線程不安全問題
(1)?synchronized的使用方法(鎖)
(2)synchronized的作用
?(3)優(yōu)化后的代碼(加鎖后)
2.2. 關(guān)于鎖/同步監(jiān)視器的總結(jié)(重點(diǎn)掌握):
總結(jié)1:認(rèn)識(shí)同步監(jiān)視器(鎖) ? ----- ?synchronized(同步監(jiān)視器){ }
總結(jié)2:同步代碼塊的執(zhí)行過程(重點(diǎn)理解)
總結(jié)3:多個(gè)代碼塊使用同一個(gè)同步監(jiān)視器(鎖)
2.3. 引入volatile解決線程安全問題
(1)?volatile保證內(nèi)存可見性
(2) volatile禁止指令重排序
前言
? ? ? ??在多線程環(huán)境下如果說代碼運(yùn)行的結(jié)果是符合我們預(yù)期的,即該代碼在單線程中運(yùn)行得到的結(jié)果,那么就說這個(gè)程序是線程安全的,否則就是線程不安全的.下面帶大家仔細(xì)給大家講解一下線程不安全問題!
1. 造成線程不安全的原因有哪些呢?
1)搶占式執(zhí)行,調(diào)度過程隨機(jī)(也是萬惡之源,無法解決)
2)多個(gè)線程同時(shí)修改同一個(gè)變量(可以適當(dāng)調(diào)整代碼結(jié)構(gòu),避免這種情況)
3)針對(duì)變量的操作,不是原子的(加鎖,synchronized)
4)內(nèi)存可見性,一個(gè)線程頻繁讀,一個(gè)線程寫(使用volatile)
5)指令重排序(使用synchronized加鎖或者volatile禁止指令重排序)
1.1什么是原子性
案例引入
我們把一段代碼想象成一個(gè)房間,每個(gè)線程就是要進(jìn)入這個(gè)房間的人。如果沒有任何機(jī)制保證,A進(jìn)入房間之后,還沒有出來;B 是不是也可以進(jìn)入房間,打斷 A 在房間里的隱私。這個(gè)就是不具備原子性的!
那我們應(yīng)該如何解決這個(gè)問題呢?是不是只要給房間加一把鎖,A 進(jìn)去就把門鎖上,其他人是不是就進(jìn)不來了。這樣就保證了這段代碼的原子性了。
有時(shí)也把這個(gè)現(xiàn)象叫做同步互斥,表示操作是互相排斥的。
一條 java 語句不一定是原子的,也不一定只是一條指令(例如++操作,內(nèi)部三條指令構(gòu)成)
原子性是指一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行,并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
一組操作(一行或多行代碼)是不可拆分的最小執(zhí)行單位,就表示這組操作是具有原子性的
多個(gè)線程多次的并發(fā)并行的對(duì)一個(gè)共享變量操作時(shí),該操作就不具有原子性
在Java中,對(duì)基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。
上面一句話雖然看起來簡單,但是理解起來并不是那么容易??聪旅嬉粋€(gè)例子:
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
注意:其實(shí)只有語句1是原子性操作,其他三個(gè)語句都不是原子性操作。
- 語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個(gè)語句的會(huì)直接將數(shù)值10寫入到工作內(nèi)存中。
- 語句2實(shí)際上包含2個(gè)操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存,雖然讀取x的值以及將x的值寫入工作內(nèi)存,這2個(gè)操作都是原子性操作,但是合起來就不是原子性操作了。
- 同樣的,x++和 x = x+1包括3個(gè)操作:讀取x的值,進(jìn)行加1操作,寫入新的值
也就是說,只有簡單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量,變量之間的相互賦值不是原子操作)才是原子操作。
從上面可以看出,Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作,如果要實(shí)現(xiàn)更大范圍操作的原子性,可以通過synchronized和Lock來實(shí)現(xiàn)。
由于synchronized和Lock能夠保證任一時(shí)刻只有一個(gè)線程執(zhí)行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。
1.2什么是內(nèi)存可見性
多個(gè)線程工作的時(shí)候都是在自己的工作內(nèi)存中來執(zhí)行操作的,線程之間是不可見的
1. 線程之間的共享變量存在主內(nèi)存(實(shí)際內(nèi)存)
2. 每一個(gè)線程都有自己的工作內(nèi)存(CPU寄存器+緩存)
3. 線程讀取共享變量時(shí),先把變量從主存拷貝到工作內(nèi)存,再從工作內(nèi)存讀取數(shù)據(jù)
4. 線程修改共享變量時(shí),先修改工作內(nèi)存中的變量值,再同步到主內(nèi)存
?
注意:
(1)線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行,不能繞過工作內(nèi)存直接從主內(nèi)存中讀寫變量
(2)不同線程之間無法直接訪問其他線程工作內(nèi)存中的變量,線程之間變量值的傳遞需要通過主內(nèi)存來完成
此時(shí)引入了兩個(gè)問題
????????為啥要整這么多內(nèi)存?
????????為啥要這么麻煩的拷來拷去?
1) 為啥整這么多內(nèi)存?
實(shí)際并沒有這么多 "內(nèi)存". 這只是 Java 規(guī)范中的一個(gè)術(shù)語, 是屬于 "抽象" 的叫法.
所謂的 "主內(nèi)存" 才是真正硬件角度的 "內(nèi)存". 而所謂的 "工作內(nèi)存", 則是指 CPU 的寄存器和高速緩存
2) 為啥要這么麻煩的拷來拷去?
因?yàn)?CPU 訪問自身寄存器的速度以及高速緩存的速度, 遠(yuǎn)遠(yuǎn)超過訪問內(nèi)存的速度(快了 3 - 4 個(gè)數(shù)量級(jí), 也就是幾千倍, 上萬倍).
比如某個(gè)代碼中要連續(xù) 10 次讀取某個(gè)變量的值, 如果 10 次都從內(nèi)存讀, 速度是很慢的. 但是如果只是第一次從內(nèi)存讀, 讀到的結(jié)果緩存到 CPU 的某個(gè)寄存器中, 那么后 9 次讀數(shù)據(jù)就不必直接訪問內(nèi)存了. 效率就大大提高了.
那么接下來問題又來了, 既然訪問寄存器速度這么快, 還要內(nèi)存干啥??
答案就是一個(gè)字: 貴
值的一提的是, 快和慢都是相對(duì)的. CPU 訪問寄存器速度遠(yuǎn)遠(yuǎn)快于內(nèi)存, 但是內(nèi)存的訪問速度又遠(yuǎn)遠(yuǎn)快于硬盤.對(duì)應(yīng)的, CPU 的價(jià)格最貴, 內(nèi)存次之, 硬盤最便宜
1.3共享變量可見性實(shí)現(xiàn)的原理
線程1對(duì)共享變量的修改要想被線程2及時(shí)看到,必須要經(jīng)過如下的2個(gè)步驟
(1)把工作內(nèi)存1中更新過的共享變量刷新到主內(nèi)存中
(2)將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中
變量傳遞順序
?
?1.4 什么是指令重排序
JVM翻譯字節(jié)碼指令,CPU執(zhí)行機(jī)器碼指令,都可能發(fā)生重排序來優(yōu)化執(zhí)行效率
比如有這樣三步操作:(1) 去前臺(tái)取U盤 (2) 去教室寫作業(yè) (3) 去前臺(tái)取快遞
JVM會(huì)對(duì)指令優(yōu)化,也就是重排序,新的順序?yàn)?1)(3)(2),這樣就可以少跑一次前臺(tái),以此提高效率,這就叫做指令重排序.
編譯器對(duì)于指令重排序的前提是 "保持邏輯不發(fā)生變化". 這一點(diǎn)在單線程環(huán)境下比較容易判斷, 但
是在多線程環(huán)境下就沒那么容易了, 多線程的代碼執(zhí)行復(fù)雜程度更高, 編譯器很難在編譯階段對(duì)代
碼的執(zhí)行效果進(jìn)行預(yù)測(cè), 因此激進(jìn)的重排序很容易導(dǎo)致優(yōu)化后的邏輯和之前不等價(jià).
重排序是一個(gè)比較復(fù)雜的話題, 涉及到 CPU 以及編譯器的一些底層工作原理, 此處不做過多討論
?
2.解決線程安全問題
引入count++問題
class Counter {
private int count =0;
public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter =new Counter();
Thread t1 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
運(yùn)行上述代碼我們會(huì)發(fā)現(xiàn)每次都結(jié)果是小于100000的,因?yàn)樯厦鎯蓚€(gè)線程在實(shí)際對(duì)count進(jìn)行++操作的時(shí)候并不滿足原子性,導(dǎo)致最終的結(jié)果一直不是我們想要的,這就是由于不滿足原子性所導(dǎo)致的線程不安全問題!!!
count++操作,本質(zhì)上是有三個(gè)CPU指令構(gòu)成
1.load,把內(nèi)存中的數(shù)據(jù)讀到CPU寄存器中
2.add,就是把寄存器中的值進(jìn)行+1運(yùn)算
3.save,把寄存器中的值寫回到內(nèi)存中
? ?由于CPU的搶占式執(zhí)行,導(dǎo)致兩個(gè)線程同時(shí)進(jìn)行count++操作的時(shí)候,內(nèi)部的三個(gè)CPU指令不能完整一次性執(zhí)行完,例如在第一個(gè)線程在執(zhí)行的時(shí)候先讀取共享變量count的值到自己的寄存器中,還沒來得及修改,第二個(gè)線程獲取到了CPU的執(zhí)行權(quán)開始執(zhí)行,此時(shí)線程2線讀取共享變量到自己的工作內(nèi)存(寄存器中)進(jìn)行修改,最后再同步到主內(nèi)存(就是更新共享變量count的值),當(dāng)線程2執(zhí)行完畢后,線程1再次獲得CPU的執(zhí)行權(quán)繼續(xù)執(zhí)行未完成的操作,將自己寄存器中的count進(jìn)行修改再同步到主內(nèi)存中,此時(shí)由于兩次修改實(shí)際上只修改成功一次,這就是由于原子性引起的線程不安全問題!
2.1 引入關(guān)鍵字synchronized解決線程不安全問題
(1)?synchronized的使用方法(鎖)
修飾方法:修飾普通方法時(shí),關(guān)鍵字在public前后都可,鎖對(duì)象是 this,也就是誰調(diào)用誰上鎖。修飾靜態(tài)方法時(shí),鎖對(duì)象是類對(duì)象。
修飾代碼塊:修飾代碼塊時(shí),顯式(手動(dòng))指定鎖對(duì)象。
對(duì)于構(gòu)造方法來說,如果加鎖,不能直接加在方法上,但是內(nèi)部可以使用代碼塊的方法,來加鎖。
代碼演示
//修飾普通方法
public synchronized void doSomething(){
//...
}
//修飾代碼塊
public void doSomething(){
synchronized (this) {
//...
}
}
//修飾靜態(tài)方法(與下面效果相同都是鎖類對(duì)象)
public static synchronized void doSomething(){
//...
}
//修飾靜態(tài)方法
public static void doSomething(){
synchronized (A.class) {
//...
}
}
(2)synchronized的作用
sychronized是基于對(duì)象頭加鎖的,特別注意:不是對(duì)代碼加鎖,所說的加鎖操作就是給這個(gè)對(duì)象的對(duì)象頭里設(shè)置了一個(gè)標(biāo)志位
一個(gè)對(duì)象在同一時(shí)間只能有一個(gè)線程獲取到該對(duì)象的鎖
sychronized保證了原子性,可見性,有序性(這里的有序不是指指令重排序,而是具有相同鎖的代碼塊按照獲取鎖的順序執(zhí)行)
(2.1) 互斥性
synchronized 會(huì)起到互斥效果, 某個(gè)線程執(zhí)行到某個(gè)對(duì)象的 synchronized 中時(shí), 其他線程如果也執(zhí)行到同一個(gè)對(duì)象 synchronized 就會(huì)阻塞等待
進(jìn)入 synchronized 修飾的代碼塊, 相當(dāng)于 加鎖
退出 synchronized 修飾的代碼塊, 相當(dāng)于 解鎖
下面圖加深理解:
阻塞等待:
針對(duì)每一把鎖, 操作系統(tǒng)內(nèi)部都維護(hù)了一個(gè)等待隊(duì)列. 當(dāng)這個(gè)鎖被某個(gè)線程占有的時(shí)候,其他線程嘗試進(jìn)行加鎖, 就加不上了,就會(huì)阻塞等待, 一直等到之前的線程解鎖之后, 由操作系統(tǒng)喚醒一個(gè)新的線程, 再來獲取到這個(gè)鎖!
(2.2)?刷新主存
synchronized鎖住共享變量時(shí)的工作流程:
??獲得互斥鎖
??從主存拷貝最新的變量到工作內(nèi)存
??對(duì)變量執(zhí)行操作
??將修改后的共享變量的值刷新到主存
??釋放互斥鎖
(2.3)?可重入性
synchronized是可重入鎖
同一個(gè)線程可以多次申請(qǐng)成功一個(gè)對(duì)象鎖
在可重入鎖的內(nèi)部, 包含了 "線程持有者" 和 "計(jì)數(shù)器" 兩個(gè)信息.
如果某個(gè)線程加鎖的時(shí)候, 發(fā)現(xiàn)鎖已經(jīng)被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續(xù)獲取
到鎖, 并讓計(jì)數(shù)器自增.
解鎖的時(shí)候計(jì)數(shù)器遞減為 0 的時(shí)候, 才真正釋放鎖. (才能被別的線程獲取到)
????????可重入鎖的意義就是降低程序員負(fù)擔(dān)(使用成本來提高開發(fā)效率),代價(jià)就是程序的開銷增大(維護(hù)鎖屬于哪個(gè)線程,并且加減計(jì)數(shù),降低了運(yùn)行效率)?
如下圖:
?(3)優(yōu)化后的代碼(加鎖后)
class Counter {
private int count =0;
synchronized public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter =new Counter();
Thread t1 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
2.2. 關(guān)于鎖/同步監(jiān)視器的總結(jié)(重點(diǎn)掌握):
總結(jié)1:認(rèn)識(shí)同步監(jiān)視器(鎖) ? ----- ?synchronized(同步監(jiān)視器){ }
??必須是引用數(shù)據(jù)類型,不能是基本數(shù)據(jù)類型
??也可以創(chuàng)建一個(gè)專門的同步監(jiān)視器,沒有任何業(yè)務(wù)含義?
??一般使用共享資源做同步監(jiān)視器即可 ??
??在同步代碼塊中不能改變同步監(jiān)視器對(duì)象的引用???盡量不要String和包裝類Integer做同步監(jiān)視器?
??建議使用final修飾同步監(jiān)視器
總結(jié)2:同步代碼塊的執(zhí)行過程(重點(diǎn)理解)
1)第一個(gè)線程來到同步代碼塊,發(fā)現(xiàn)同步監(jiān)視器open狀態(tài),需要close,然后執(zhí)行其中的代碼
2)第一個(gè)線程執(zhí)行過程中,發(fā)生了線程切換(處于阻塞就緒狀態(tài)),第一個(gè)線程失去了cpu,但是沒有開鎖(open)
3)第二個(gè)線程獲取了cpu,來到了同步代碼塊,發(fā)現(xiàn)同步監(jiān)視器close狀態(tài),無法執(zhí)行其中的代碼,第二個(gè)線程也進(jìn)入阻塞狀態(tài)
4)第一個(gè)線程再次獲取CPU,接著執(zhí)行后續(xù)的代碼;同步代碼塊執(zhí)行完畢,釋放鎖open
5)第二個(gè)線程也再次獲取cpu,來到了同步代碼塊,發(fā)現(xiàn)同步監(jiān)視器open狀態(tài),拿到鎖并且上鎖,由阻塞狀態(tài)進(jìn)入就緒狀態(tài),再進(jìn)入運(yùn)行狀態(tài),重復(fù)第一個(gè)線程的處理過程(加鎖)
強(qiáng)調(diào):同步代碼塊中能發(fā)生CPU的切換嗎?能?。?! 但是后續(xù)的被執(zhí)行的線程也無法執(zhí)行同步代碼塊(因?yàn)殒i仍舊close)?
總結(jié)3:多個(gè)代碼塊使用同一個(gè)同步監(jiān)視器(鎖)
1)多個(gè)代碼塊使用了同一個(gè)同步監(jiān)視器(鎖),鎖住一個(gè)代碼塊的同時(shí),也鎖住所有使用該鎖的所有代碼塊,其他線程無法訪問其中的任何一個(gè)代碼塊?
2)多個(gè)代碼塊使用了同一個(gè)同步監(jiān)視器(鎖),鎖住一個(gè)代碼塊的同時(shí),也鎖住所有使用該鎖的所有代碼塊, 但是沒有鎖住使用其他同步監(jiān)視器的代碼塊,其他線程有機(jī)會(huì)訪問其他同步監(jiān)視器的代碼塊
2.3. 引入volatile解決線程安全問題
(1)?volatile保證內(nèi)存可見性
引入一個(gè)線程不安全的場(chǎng)景:
當(dāng)一個(gè)線程對(duì)一個(gè)變量進(jìn)行讀取操作,同時(shí)另一個(gè)線程針對(duì)這個(gè)變量進(jìn)行修改,此時(shí)讀到的值不一定是修改后的值,這是編譯器在多線程環(huán)境下優(yōu)化時(shí)產(chǎn)生了誤判,從而引起了bug
代碼演示:
class Sign{
public boolean flag = false;
}
public class ThreadDemo4{
public static void main(String[] args) {
Sign sign = new Sign();
Thread t1 = new Thread(()->{
while(!sign.flag){
}
System.out.println("執(zhí)行完畢");
});
Thread t2 = new Thread(()->{
sign.flag = true;
});
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
運(yùn)行上述代碼我們會(huì)發(fā)現(xiàn),程序會(huì)一直運(yùn)行,while感知不到flag的變化。原因就是,執(zhí)行到線程2的時(shí)候,while一直循環(huán)跑了好多遍,flag一直是false,所以編譯器對(duì)代碼進(jìn)行優(yōu)化,默認(rèn)為程序不變,不再從內(nèi)存中讀取flag的值,而是讀取寄存器中不變的flag的值,等到線程2執(zhí)行到flag變量后,盡管修改掉了內(nèi)存中flag的值,但是寄存器中的flag依舊為原來的值,所以while一直感知到的flag是沒變的,一直循環(huán)跑。
那么如何解決該問題呢?
用volatile來修飾變量,通過保證內(nèi)存可見性來解決上述問題,每次讀取用volatile修飾的變量的值,都會(huì)從主內(nèi)存中讀取該變量。
通俗地講:volatile變量在每次被線程訪問時(shí),都強(qiáng)迫從主內(nèi)存中重讀該變量的值,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫線程將最新的值刷新到主內(nèi)存。這樣在任何時(shí)刻,不同的線程總能看到該變量的最新值。
那么,線程修改volatile變量的過程:
(1)改變線程工作內(nèi)存中volatile變量副本的值
(2)將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存
線程讀volatile變量的值的過程:
(1)從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
(2)從工作內(nèi)存中讀取volatile變量的副本
(2) volatile禁止指令重排序
我們這里拿實(shí)例化一個(gè)對(duì)象舉例
SomeObject s=new SomeObject();? //保證對(duì)象實(shí)例化正確
1.堆里申請(qǐng)內(nèi)存空間,初始化為0x0
2.對(duì)象初始化工作:構(gòu)造代碼塊,屬性的定義時(shí)初始化,構(gòu)造方法(這才算是一個(gè)正確對(duì)象)
3.賦值給s
volatile禁止重排序,只能1->2->3,如果1->3->2在3到2之間有線程(線程調(diào)度隨機(jī))使用對(duì)象,其對(duì)象是錯(cuò)的即出現(xiàn)問題。
能準(zhǔn)確的表明其作用是單列模式:(這個(gè)我們后面會(huì)再講)
單列模式分為餓漢模式(在類加載期間就進(jìn)行對(duì)象實(shí)例化),懶漢模式(第一次用到時(shí)進(jìn)行對(duì)象的實(shí)例化)文章來源:http://www.zghlxwxcb.cn/news/detail-434314.html
其懶漢模式實(shí)現(xiàn)如下:假如多個(gè)線程走先判斷對(duì)象沒有實(shí)例化,對(duì)類加鎖(一個(gè)線程持有鎖,但這是不知道是否實(shí)例化),所以要再判斷是否實(shí)例化,沒有實(shí)例化進(jìn)行實(shí)例化,實(shí)例化了就返回對(duì)象,這里volatile就是要確保實(shí)例化正確。文章來源地址http://www.zghlxwxcb.cn/news/detail-434314.html
到了這里,關(guān)于【多線程基礎(chǔ)】 線程安全及解決方案(看這一篇就夠了)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!