【JavaEE】線程安全
- 一、引出線程安全
- 二、引發線程安全的原因
- 三、解決線程安全問題
- 3.1 synchronized關鍵字(解決修改操作不是原子的)
- 3.1.1 synchronized的特性
- 3.1.1 synchronized的使用事例
- 3.2 volatile 關鍵字(解決內存可見性)
- 四、死鎖
- 4.1 可重入
- 4.2 兩個線程出現的死鎖
- 4.3 哲學家就餐問題
- 4.4 造成死鎖的原因
博客結尾有此篇博客的全部代碼!!!
一、引出線程安全
舉例:
public class Demo1 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; 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="+count);//結果:56154}
}
這段代碼運行結束發現結果不是理論值:100000,而是每次運行完出現一個新的數。
在計算機操作系統中,count++;在寄存器中分為三步:讀取,加一,寫回這三步。
假設:兩個線程按照這樣進行,那么得到的count的最終值就是理論值100000。
但往往事實不是這樣這樣的,CPU資源調度是隨機的!!!很有可能是這樣的(這里只列舉一種情況給大家示范一下):
首先t1線程和t2 線程分別從內存中讀取count值,此時兩個線程讀取到的count值都是0,然后t1線程進行加一操作后寫回到內存中,t2線程也是進行加一操作后寫回到內存中,t2線程得到的count值將t1得到的count覆蓋,這樣count經過兩個線程的加一操作之后值還是1!
二、引發線程安全的原因
- 【根本原因】操作系統對于線程的調度是隨機的,搶占式執行
- 多個線程同時修改同一變量
- 修改操作不是原子的(事務中的原子性)
- 內存可見性,引起的線程不安全
- 指令重排序,引起的線程不安全
三、解決線程安全問題
- 由于線程調度是隨機的,這個不是我們可以左右的;
- 我們確保多個線程不同時修改同一變量
主要帶大家學習引發第三個和第四個引起線程安全的解決方法:
3.1 synchronized關鍵字(解決修改操作不是原子的)
引發線程安全第三個原因是:修改操作不是原子的;關鍵字:synchronized將修改操作“鎖”在一起(相當于將讀取,加一,寫回三個操作綁定在一起,三操作要么全部執行,要么全部不執行)
3.1.1 synchronized的特性
- 互斥
synchronized 會起到互斥效果, 某個線程執?到某個對象的 synchronized 中時, 其他線程如果也執?到同?個對象 synchronized 就會阻塞等待.
語法:
synchronized(變量){
//修改操作
}
? 進? synchronized 修飾的代碼塊, 相當于 加鎖
? 退出 synchronized 修飾的代碼塊, 相當于 解鎖
public class Demo2 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (lock){count++;}}}); Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (lock){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);//結果:count=10000}
}
當加上鎖之后,count值就是10000!
這里需要注意的事:
synchronized(變量)里面的這個變量必須是相同的變量,否則就不會發生阻塞等待!!!
事例 :
public class Demo3 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (lock1){count++;}}}); Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (lock2){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);//結果:count=70738}}
- 可重入
可重入就是指一個線程連續針對一個對象加多次鎖,不會出現“死鎖”現象稱為可重入。
synchronized (block) {synchronized(block) {//代碼} //右大括號}2
} //右大括號}1
按理來說,在進入第一個synchronized的時候,加上了一把鎖,此時已經是“鎖定狀態”,當我們進入到第二個synchronized的時候要加鎖,就發生“阻塞等待”,就要等到第一個鎖走到右大括號}1解完鎖才能加,然而第一個鎖走到右大括號}1解鎖,又需要第二把鎖創建走完到右大括號}2。
這是線程就卡死了,這就是死鎖。
Java大佬發現了這個問題,所以將synchronized設為可重入鎖,這樣就不會出現死鎖的問題。
? 如果某個線程加鎖的時候, 發現鎖已經被?占?, 但是恰好占?的正是??(這個鎖是自己加的), 那么仍然可以繼續獲取到鎖, 并讓計數器?增.
? 解鎖的時候計數器遞減為 0 的時候, 才真正釋放鎖. (才能被別的線程獲取到)
3.1.1 synchronized的使用事例
- 修飾代碼塊
鎖定任意對象
鎖住當前對象
public class SynchronizedDemo {public void method() {synchronized (this) {}}}
- 直接修飾普通?法
public class SynchronizedDemo {public synchronized void methond() {}}
- 修飾靜態?法
public class SynchronizedDemo {public synchronized static void methond() {}}
3.2 volatile 關鍵字(解決內存可見性)
volatile可以保證內存可見性,只能修飾變量。并且volatile不能保證原子性
計算機運行代碼/程序的時候,訪問數據常常要從內存中訪問(定義變量時變量就儲存在內存中),然而CPU從內存中讀取數據相比于從寄存器中讀取數據要慢上很多(幾千上萬倍),CPU在進行讀/寫內存的時候速度就會降低。
為了解決這個問題,提高效率,編譯器就可能會對代碼優化,把一些本來要讀取內存的操作,優化為讀取寄存器,減少讀取內存的次數。這就會導致內存可見性問題。
以我們接下來的代碼為例------當CPU從自身寄存器中讀取成千上萬次發現count一直是0,此時編譯器就將代碼優化,讓count一直等于0,所以接下來線程1中一直處于循環狀態,盡管線程2中已經將count修改為1!
public class Demo4 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Thread t1 = new Thread(() -> {while (count == 0) {}System.out.println("循環結束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}count = 1;});t1.start();t2.start();System.out.println("main 線程結束");}
}
這里修改的方法:給線程1中的程序加入sleep,讓它休眠時間大于線程2的休眠時間,這樣它讀取的count就是1,編譯器就不會進行優化,循環就會結束!
Thread t1 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}while (count == 0) {}System.out.println("循環結束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}count = 1;});
volatile解決內存可見性問題:
public class Demo5 {private volatile static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (count == 0) {}System.out.println("循環結束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}count = 1;});t1.start();t2.start();System.out.println("main 線程結束");}
}
四、死鎖
死鎖是一個非常嚴重的bug,它會讓你的代碼在執行到這塊卡住!!!
4.1 可重入
public class Demo6 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (lock) {synchronized (lock) {count++;}}}System.out.println("循環結束");});t1.start();t1.join();System.out.println(count);}
}
由于Java提出了可重入的概念,所以這段代碼執行的到這里并沒有卡住!但在C++中,沒有引入可重入的概念,所以C++這里寫出這樣的代碼就會出現死鎖!!!
4.2 兩個線程出現的死鎖
假設t1線程先拿醋,t2線程先拿醬,兩個線程都將醋和醬已經加上自己的鎖了,然后t1線程嘗試拿醬,t2線程嘗試拿醋,此時就會出現死鎖!!!
public class Demo7 {public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {synchronized (lock1) {System.out.println("t1拿到醋了!!!");synchronized (lock2) {System.out.println("t1拿到醬了!!!");}}});Thread t2 = new Thread(() -> {synchronized (lock2) {System.out.println("t2拿到醬了!!!");synchronized (lock1) {System.out.println("t2拿到醋了!!!");}}});t1.start();t2.start();t1.join();t2.join();System.out.println("main 線程結束!!!");}
}
如果將兩個鎖改成并列就不會出現死鎖!
Thread t2 = new Thread(() -> {synchronized (lock2) {System.out.println("t2拿到醬了!!!");}synchronized (lock1) {System.out.println("t2拿到醋了!!!");}});
4.3 哲學家就餐問題
相當于是兩個線程出現死鎖的進階(M個線程,N把鎖):
5個哲學家(5個線程),5只筷子(5把鎖),哲學家坐在圓桌邊,桌上放有面條,每只筷子放在每個哲學家的中間。
每個哲學家,會做兩件事:
- 思考人生.放下筷子,啥都不干
- 吃面條.拿起左右兩側的兩根筷子,開始吃面條,
哲學家啥時候吃面條,啥時候思考人生,是隨機的
哲學家吃面條啥時候吃完,也是隨機的,
哲學家正在吃面條的過程中,會持有左右兩側的筷子。此時相鄰的哲學家如果也想吃面條,就需要阻塞等待,
當出現極端情況,每個哲學家都想吃面條,都拿起自己左手邊的筷子,并且不會在沒吃到面條情況下放下筷子,這時就是死鎖了。
4.4 造成死鎖的原因
- 互斥使用(鎖的基本特性):當一個線程拿到一把鎖后,另一個線程要拿到這把鎖就要阻塞等待;
- 不可搶占(鎖的基本特性):當一把鎖被線程拿到后,其他線程不能搶占,只能等線程自己釋放鎖;
- 請求保持(代碼結構):當一個線程拿到一把鎖后,再去拿其它鎖的時候,已經被拿到的鎖不會被釋放;
- 循環/環路 等待(代碼結構):阻塞等待的依賴關系形成環了。
此篇博客的全部代碼!!!