1. 什麼是線程安全性?
當多個線程訪問某個類,不管運行時環境採用何種調度方式或者這些線程如何交替執行,並且在主調代碼中不需
要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類爲線程安全的。----《併發編程實戰》
什麼是線程不安全?
多線程併發訪問時,得不到正確的結果。
2. 從字節碼角度剖析線程不安全操作
javac -encoding UTF-8 UnsafeThread.java 編譯成.class
編譯命令:
javac -encoding UTF-8 UnSafeThread.java
將class文件反編譯爲字節碼:
javap -c UnSafeThread.class
javap -c UnsafeThread.class 進行反編譯,得到相應的字節碼指令
0: getstatic #2 獲取指定類的靜態域,並將其押入棧頂
3: iconst_1 將int型1押入棧頂
4: iadd 將棧頂兩個int型相加,將結果押入棧頂
5: putstatic #2 爲指定類靜態域賦值
8: return
例子中,產生線程不安全問題的原因: num++ 不是原子性操作,被拆分成好幾個步驟,在多線程併發執行的
情況下,因爲cpu調度,多線程快遞切換,有可能兩個同一時刻都讀取了同一個num值,之後對它進行+1操
作,導致線程安全性。
3. 原子性操作
3.1什麼是原子性操作
一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
3.2例子:
A想要從自己的帳戶中轉1000塊錢到B的帳戶裏。那個從A開始轉帳,到轉帳結束的這一個過程,稱之爲一個事務。在這個事務裏,要做如下操作: 從A的帳戶中減去1000塊錢。如果A的帳戶原來有3000塊錢,現在就變成2000塊錢了。
在B的帳戶里加1000塊錢。如果B的帳戶如果原來有2000塊錢,現在則變成3000塊錢了。如果在A的帳
戶已經減去了1000塊錢的時候,忽然發生了意外,比如停電什麼的,導致轉帳事務意外終止了,而此時B的帳
戶裏 還沒有增加1000塊錢。那麼,我們稱這個操作失敗了,要進行回滾。回滾就是回到事務開始之前的狀
態,也就是回到A的帳戶還沒減1000塊的狀態,B的帳戶的原來的狀態。此時A的帳戶仍然有3000塊,B的帳
戶仍然有 2000塊。
通俗點講:操作要成功一起成功、要失敗大家一起失敗如何把非原子性操作變成原子性
volatile關鍵字僅僅保證可見性,並不保證原子性 synchronize關機字,使得操作具有原子性
4. 深入理解synchronized
內置鎖
<font color="#000066"> 每個java對象都可以用做一個實現同步的鎖,這些鎖稱爲內置鎖</font> 線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。
互斥鎖
<font color="#000066">內置鎖是一個互斥鎖,這就是意味着最多隻有一個線程能夠獲得該鎖,</font>當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,直到線程B釋放這個鎖,如果B線程不釋放這個鎖,那麼A線程將永遠等待下去。
修飾普通方法:鎖住對象的實例
修飾靜態方法:鎖住整個類
修飾代碼塊: 鎖住一個對象 synchronized (lock) 即synchronized後面括號裏的內容
public class SynDemo {
/**
* 不要用synchronized修飾靜態方法 因爲他是鎖住的整個類
*/
// public synchronized void out() throws InterruptedException {
// Thread.sleep(5000L);
// System.out.println(Thread.currentThread().getName());
// }
private Object lock = new Object();
public void out() throws InterruptedException {
synchronized(lock){
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
SynDemo synDemo = new SynDemo();
SynDemo synDemo2 = new SynDemo();
new Thread(()->{
try {
synDemo.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
synDemo2.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
5. volatile關鍵字及其使用場景
能且僅能修飾變量
保證該變量的可見性,volatile關鍵字僅僅保證可見性,並不保證原子性
<font color="#000066">禁止指令重排序</font>
A、B兩個線程同時讀取volatile關鍵字修飾的對象,A讀取之後,修改了變量的值,修改後的值,對B線程來說,
是可見
使用場景
- 1:作爲線程開關
- 2:單例,修飾對象實例,禁止指令重排序
/**
* volatile
* 1:作爲線程開關 2:單例,修飾對象實例,禁止指令重排序
*/
public class VolatileDemo implements Runnable {
private static volatile boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new VolatileDemo());
thread.start();
Thread.sleep(1000L);
flag=false;
}
}
6. 單例與線程安全
餓漢式--本身線程安全
在類加載的時候,就已經進行實例化,無論之後用不用到。如果該類比較佔內存,之後又沒用到,就白白浪費
了資源。
/**
* 餓漢式單例
* 本身就是線程安全的
*/
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
private HungerSingleton() {
}
public static void main(String[] args) {
HungerSingleton hungerSingleton = new HungerSingleton();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(HungerSingleton.getInstance());
}).start();
}
}
}
懶漢式 -- 最簡單的寫法是非線程安全的
在需要的時候再實例化
/**
* 懶漢式單例 線程安全的寫法
*/
public class LazySingleton {
//這裏一定要使用volatile,可以禁止指令重排序
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//判斷實例是否爲空,爲空才實例化
if (lazySingleton == null) {
//模擬實例化耗時操作
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
// 否則直接返回
return lazySingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
7.如何避免線程安全性問題
線程安全性問題成因
- 多線程環境
- 多個線程操作同一共享資源
- 對該共享資源進行了非原子性操作
7.1如何避免
打破成因中三點任意一點
1:多線程環境--將多線程改單線程(必要的代碼,加鎖訪問)
2:多個線程操作同一共享資源--不共享資源(ThreadLocal、不共享、操作無狀態化、不可變)
3:對該共享資源進行了非原子性操作-- 將非原子性操作改成原子性操作(加鎖、使用JDK自帶的原子性操作的類、JUC提供的相應的併發工具類)