目錄
一、引入線程安全?👇
二、?線程安全👇
1、線程安全概念?🔍
2、線程不安全的原因?🔍
搶占式執行(罪魁禍首,萬惡之源)導致了線程之間的調度是“隨機的”
多個線程修改同一個變量?
?修改操作,不是原子的(不可分割的最小單位)?
內存可見性,引起的線程不安全?
指令重排序,引起的線程不安全
三、解決之前的線程不安全問題👇
1、synchronized 關鍵字-監視器鎖monitor lock??🔍
1)synchronized 的特性?
(1) 互斥
(2)刷新內存
(3) 可重入
2)synchronized 使用示例?
1) 直接修飾普通方法:
?2) 修飾靜態方法:
3) 修飾代碼塊: 明確指定鎖哪個對象.
2、volatile 關鍵字?(保證內存可見性)?🔍
?1)引入volatile
2)volatile不保證原子性
💡?總結:
一、引入線程安全?👇
執行以下代碼:
package threading;
class Counter {public int count = 0;void increase() {count++;}
}public class ThreadDemo23 {public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}
可以觀察代碼,我們對兩個線程分別累加50000次,結果應該為100000,但是大家看運行結果并非如此,而且可以發現,每次運行的結果都不一樣,這是什么原因呢??
答案就是涉及到了線程安全問題
?
?
本質原因:線程在系統中的調度是無序的/隨機的(搶占式執行的)?
二、?線程安全👇
1、線程安全概念?🔍
? ? 如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線 程安全的。?
2、線程不安全的原因?🔍
-
搶占式執行(罪魁禍首,萬惡之源)導致了線程之間的調度是“隨機的”
-
多個線程修改同一個變量?
一個線程修改同一個變量=>安全
多個線程讀取同一個變量=>安全
多個線程修改不同變量=> 安全?
-
?修改操作,不是原子的(不可分割的最小單位)?
什么是原子性🐶
我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證,A進入房間之后,還沒有出來;B 是不是也可以進入房間,打斷 A 在房間里的隱私。這個就是不具備原子性的。
那我們應該如何解決這個問題呢?🐶
是不是只要給房間加一把鎖,A 進去就把門鎖上,其他人是不是就進 不來了。這樣就保證了這段代碼的原子性了。 有時也把這個現象叫做同步互斥,表示操作是互相排斥的。
一條 java 語句不一定是原子的,也不一定只是一條指令 比如剛才我們看到的 n++,其實是由三步操作組成的: 1. 從內存把數據讀到 CPU 2. 進行數據更新 3. 把數據寫回到 CPU
不保證原子性會給多線程帶來什么問題🐶
如果一個線程正在對一個變量操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。 這點也和線程的搶占式調度密切相關. 如果線程不是 "搶占" 的, 就算沒有原子性, 也問題不大.
-
內存可見性,引起的線程不安全?
? ? ? ? ? 可見性指, 一個線程對共享變量值的修改,能夠及時地被其他線程看到.
-
指令重排序,引起的線程不安全
三、解決之前的線程不安全問題👇
1、synchronized 關鍵字-監視器鎖monitor lock??🔍
1)對文章初始代碼進行修改:既可以保證 ++ 操作就是原子的,不受影響啦
可以將{ }視為廁所,進表示加鎖,出表示解鎖
void increase() {//鎖有倆個核心操作,加鎖和解鎖//進入該代碼塊就會觸發加鎖,出了代碼塊,就會觸發解鎖synchronized (this) {count++;}}
?注意:此處的this指的就是counter對象
因此,在上述代碼中,兩個線程實在競爭同一個鎖對象,就會產生鎖競爭。
再執行上述代碼:就是100000
疑惑為啥以上操作為什么可以解決線程安全問題呢??🐶
1)synchronized 的特性?
(1) 互斥
? ? ? synchronized 會起到互斥效果, 某個線程執行到某個對象的 synchronized 中時, 其他線程如果也執行到 同一個對象 synchronized 就會阻塞等待.
進入 synchronized 修飾的代碼塊, 相當于 加鎖
退出 synchronized 修飾的代碼塊, 相當于 解鎖?
理解 "阻塞等待". 針對每一把鎖, 操作系統內部都維護了一個等待隊列. 當這個鎖被某個線程占有的時候, 其他線程嘗 試進行加鎖, 就加不上了, 就會阻塞等待, 一直等到之前的線程解鎖之后, 由操作系統喚醒一個新的 線程, 再來獲取到這個鎖.
注意: 上一個線程解鎖之后, 下一個線程并不是立即就能獲取到鎖. 而是要靠操作系統來 "喚醒". 這 也就是操作系統線程調度的一部分工作.
假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C 都在阻塞隊列中排隊等待. 但是當 A 釋放鎖之后, 雖然 B 比 C 先來的, 但是 B 不一定就能 獲取到鎖, 而是和 C 重新競爭, 并不遵守先來后到的規則.?
(2)刷新內存
synchronized 的工作過程:?
- 獲得互斥鎖
- 從主內存拷貝變量的最新副本到工作的內存
- 執行代碼
- 將更改后的共享變量的值刷新到主內存
- 釋放互斥鎖 所以 synchronized 也能保證內存可見性.?
(3) 可重入
synchronized 同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題;?
理解 "把自己鎖死" :一個線程沒有釋放鎖, 然后又嘗試再次加鎖.?
按照之前對于鎖的設定, 第二次加鎖的時候, 就會阻塞等待. 直到第一次的鎖被釋放, 才能獲取到第 二個鎖. 但是釋放第一個鎖也是由該線程來完成, 結果這個線程已經躺平了, 啥都不想干了, 也就無 法進行解鎖操作. 這時候就會死鎖
當然,Java 中的 synchronized 是 可重入鎖, 因此沒有上面的問題
?示例:
static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();} }
increase 和 increase2 兩個方法都加了 synchronized,
此處的 synchronized 都是針對 this 當前對象加鎖的.
在調用 increase2 的時候, 先加了一次鎖, 執行到 increase 的時候, 又加了一次鎖. (上個鎖還沒釋 放, 相當于連續加兩次鎖)
這個代碼是完全沒問題的. 因為 synchronized 是可重入鎖.
注意:在可重入鎖的內部, 包含了 "線程持有者" 和 "計數器" 兩個信息.
如果某個線程加鎖的時候, 發現鎖已經被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續獲取 到鎖, 并讓計數器自增.
解鎖的時候計數器遞減為 0 的時候, 才真正釋放鎖. (才能被別的線程獲取到)?
2)synchronized 使用示例?
?synchronized 本質上要修改指定對象的 "對象頭". 從使用角度來看, synchronized 也勢必要搭配一個具 體的對象來使用.
1) 直接修飾普通方法:
鎖的 SynchronizedDemo 對象🐶
public class SynchronizedDemo {public synchronized void methond() {}
}
?2) 修飾靜態方法:
鎖的 SynchronizedDemo 類的對象🐶
public class SynchronizedDemo {public synchronized static void method() {}
}
3) 修飾代碼塊: 明確指定鎖哪個對象.
鎖當前對象🐶
public class SynchronizedDemo {public void method() {synchronized (this) {}}
}
鎖類對象🐶
類對象是啥:Counter.class
.java源代碼文件,javac =>.class(二進制字節碼文件),JVM就可以執行.class文件了,類對象就可以表示這個.class文件的內容~~(描述了類的方方面面的詳細信息,比如誒的名字,類的屬性,類的方法,)
public class SynchronizedDemo {public void method() {synchronized (SynchronizedDemo.class) {}}
}
2、volatile 關鍵字?(保證內存可見性)🔍
?1)引入volatile
所謂內存可見性,就是多線程環境下,編譯器對于代碼優化,產生了誤判,從而引起了bug,進一步導致了代碼的bug
因此我們可以加上 volatile(讓編譯器對這個場景暫停優化) , 強制讀寫內存. 速度是慢了, 但是數據變的更準確了.每次都是從內存中重新讀取數據
觀察以下代碼:
package threading;import java.util.Scanner;public class ThreadDemo24 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("循環結束,t1結束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("請輸入一個整數");//t2通過控制臺輸入一個整數,yidanyonghushurule非0的值,此時t1的循環就會立即結束,從而t1線程就會退出flag = scanner.nextInt();});t1.start();t2.start();}
}
?我們預期的效果應該是輸入一個非零的數,線程t1就會停止,但實際上仍然在執行,處在RUNNABLE狀態
?為什么會出現以上問題呢?內存可見性的鍋!!!
讓我們分析一下代碼:
直接訪問工作內存(實際是 CPU 的寄存器或者 CPU 的緩存), 速度 非常快, 但是可能出現數據不一致的情況.
?
volatile public static int flag = 0;
加上volatile關鍵字,此時編譯器就可以保證每次都是重新從內存讀取flag變量的值,
此時t2修改flag,t1就可以立即感知到了,t1就可以正確退了
2)volatile不保證原子性
這個是最初的演示線程安全的代碼.
給 increase 方法去掉 synchronized
給 count 加上 volatile 關鍵字.
static class Counter {volatile public int count = 0;void increase() {count++;}
}
public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);
}
此時可以看到, 最終 count 的值仍然無法保證是 100000?
💡?總結:
- volatile不保證原子性
- volatile也能禁止指令重排序
- volatile 適用一個線程讀,一個線程寫的情況
- synchronized則是多個線程寫?
- volatile 和 synchronized 有著本質的區別. synchronized 能夠保證原子性(也能保證內存可見性), volatile 保證的是內存可見 性
static class Counter {public int flag = 0;
}
public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (true) {synchronized (counter) {if (counter.flag != 0) {break;}}// do nothing}System.out.println("循環結束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("輸入一個整數:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}
上面代碼:
去掉 flag 的 volatile
給 t1 的循環內部加上 synchronized, 并借助 counter 對象加鎖.?
運行結果是可以正常結束的
因此 synchronized是可以保證內存可見性的
?
?補充: 指令重排序,也是編譯器優化的策略,調整了代碼的執行順序,讓程序更高效,前提也是保證整體邏輯不變
?💡?總結:(面試題)
線程不安全的原因:
【根本原因】==操作系統上的線程是“搶占式執行”的,線程調度是隨機的,==這是線程不安全的一個主要原因。隨機調度會導致在多線程環境下,程序的執行順序不確定,程序員必須確保無論哪種執行順序,代碼都能正常運行。
【代碼結構】共享資源:多個線程同時訪問并修改共享的數據或資源。當多個線程同時訪問和修改共享資源時容易引發競態條件和數據不一致的問題。
①一個線程修改一個變量是安全的
②多個線程修改一個變量是不安全的
③多個線程修改不同變量是安全的
【直接原因】多線程操作不是“原子的”。多線程操作中的原子性指的是一個操作是不可中斷的,要么全部執行完成,要么都不執行,不能被其他線程干擾。這對于并發編程非常重要,因為如果一個操作在執行過程中被中斷,可能導致數據不一致或者其他意外情況發生。(在上述多線程操作中,count++操作不是“原子的”,而是由多個CPU指令組成的,一個線程執行這些指令時,可能會在執行過程中被搶占,從而給其他線程“可乘之機”。要保證原子性操作,每個CPU指令都應該是“原子的”,即要么完全執行,要么完全不執行。)
內存可見性問題:在多線程環境下調用不可重入的函數(即不支持多線程調用的函數),可能導致數據混亂或程序崩潰。
指令重排序問題:在多線程環境下,由于編譯器或處理器對指令進行重排序優化,可能導致預期之外的程序行為。
?線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。
線程不安全就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據。
? ? ? ? ? ? ? ? ? ? ? ??