什么是執行緒安全
一個類可以被多個執行緒安全呼叫那么這個類就是執行緒安全的,
根據執行緒共享資料的安全程度可以分為以下五類執行緒安全:
- 不可變
- 絕對執行緒安全
- 相對執行緒安全
- 執行緒兼容
- 執行緒對立
結尾有
彩蛋哦
不可變(Immutable)
不可變(Immutable)的物件一定是執行緒安全的,不需要再采取任何的執行緒安全保障措施,只要一個不可變的物件被正確地構建出來,永遠也不會看到它在多個執行緒之中處于不一致的狀態,
不可變的型別:
- final 關鍵字修飾的基本資料型別
- String
- 列舉型別
- Number 部分子類,如 Long 和 Double等數值包裝型別,BigInteger 和 BigDecimal 等大資料型別,但同為 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的,
- 集合型別,可以使用
Collections.unmodifiableXXX()方法來獲取一個不可變的集合,
獲取不可變集合代碼如下:
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", 1);
}
}
由于
unmodifiableMap是不可變的,因此使用put方法時會報出UnsupportedOperationException例外
絕對執行緒安全
不管運行時環境如何,呼叫者都不需要任何額外的同步措施這就是絕對執行緒安全,
相對執行緒安全
相對執行緒安全需要保證對這個物件單獨的操作是執行緒安全的,在呼叫的時候不需要欄位外的保障措施,但是對于一些特定順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性,
java里大部分的執行緒安全類就是
相對執行緒安全的,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包裝的集合等,
例如:如果一個執行緒洗掉Vector 中的一個元素,另一個執行緒試圖獲取這個被洗掉的元素,會拋出ArrayIndexOutOfBoundsException例外,
實體代碼如下:
public class VectorUnsafeDemo {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 100; i++) {
vector.add(i);
}
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
});
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
});
executorService.shutdown();
}
}
}
解決方式:為洗掉和獲取進行同步(這里使用
synchronized)
代碼如下:
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
});
執行緒兼容
執行緒兼容是指物件本身并不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在并發環境中可以安全地使用,我們平常說一個類不是執行緒安全的,絕大多數時候指的是這一種情況,Java API 中大部分的類都是屬于執行緒兼容的,如與前面的 Vector 和 HashTable 相對應的集合類 ArrayList 和 HashMap 等,
執行緒對立
執行緒對立是指無論呼叫端是否采取了同步措施,都無法在多執行緒環境中并發使用的代碼,由于 Java 語言天生就具備多執行緒特性,執行緒對立這種排斥多執行緒的代碼是很少出現的,而且通常都是有害的,應當盡量避免,
實作執行緒安全的方式
互斥同步(阻塞同步)
互斥同步方式實作執行緒安全也是我們編程中最常用的實作方式,主要是使用 synchronized 和 ReentrantLock,
如果想要初步了解 synchronized 和 ReentrantLock ,可以參考:
聊一聊執行緒互斥與同步的那些事(以實體解釋synchronized與ReentrantLock)
互斥同步方式是屬于阻塞方式,是一種悲觀的并發策略,性能上不如非阻塞同步方案,無論共享資料是否真的會出現競爭,它都要進行加鎖(這里討論的是概念模型,實際上虛擬機會優化掉很大一部分不必要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的執行緒需要喚醒等操作,
悲觀策略:不做就不會做錯,(出錯的人總是那些干活的人,不干活的人是不會犯錯的)
非阻塞同步
非阻塞同步方案目前主流的有 CAS,Atomic類,
CAS
CAS是基于沖突檢測的樂觀并發策略 ,如果沒有其它執行緒爭用共享資料,那操作就成功了,否則采取補償措施(不斷地重試,直到成功為止),這種樂觀的并發策略的許多實作都不需要將執行緒阻塞,因此這種同步操作稱為非阻塞同步, 樂觀鎖需要操作和沖突檢測這兩個步驟具備原子性,這里就不能再使用互斥同步來保證了,只能靠硬體來完成,硬體支持的原子性操作最典型的是: 比較并交換(Compare-and-Swap,CAS),CAS 指令需要有 3 個運算元,分別是記憶體地址 V、舊的預期值 A 和新值 B,當執行操作時,只有當 V 的值等于 A,才將 V 的值更新為 B,
硬體速度是高于軟體速度的,因此
CAS是比同步互斥的方式性能更佳,
Atomic類
其實原子類的很多方法都是使用了Unsafe的CAS做非阻塞同步,因此在一定程度上說原子類只是CAS的一種JDK實作,我們不需要關注內部實作,直接使用即可,但是需要明白原子類實作執行緒安全的機制是非阻塞的,性能高于 synchronized 加鎖的物件,
無同步方案
上面說的兩種方式都是同步方案的兩種解決方案,而這種方案是 無同步,原理很簡單:
如果一個方法本來就不涉及共享資料,那它自然就無須任何同步措施去保證正確性
實作無同步 的解決方案主要有 :堆疊封閉 ,執行緒本地存盤 和 可重入代碼,
堆疊封閉
首先,java使用堆疊封閉的方案有 :
- JUC執行緒池: FutureTask詳解
- JUC執行緒池: ThreadPoolExecutor詳解
- JUC執行緒池:ScheduledThreadPool詳解
- JUC執行緒池: Fork/Join框架詳解
多個執行緒訪問同一個方法的區域變數時,不會出現執行緒安全問題,因為區域變數存盤在虛擬機堆疊中,屬于執行緒私有的,
示例代碼如下:
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
結果如下:
100
100
執行緒本地存盤(Thread Local Storage)
本地存盤的java實作 : Java 并發 - ThreadLocal詳解
如果一段代碼中所需要的資料必須與其他代碼共享,那就看看這些共享資料的代碼是否能保證在同一個執行緒中執行,如果能保證,我們就可以把共享資料的可見范圍限制在同一個執行緒之內,這樣,無須同步也能保證執行緒之間不出現資料爭用的問題,
符合這種特點的應用并不少見,大部分使用消費佇列的架構模式(如“生產者-消費者”模式)都會將產品的消費程序盡量在一個執行緒中消費完,其中最重要的一個應用實體就是經典 Web 互動模型中的“一個請求對應一個服務器執行緒”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多 Web 服務端應用都可以使用執行緒本地存盤來解決執行緒安全問題,
可以使用 java.lang.ThreadLocal 類來實作執行緒本地存盤功能,
對于以下代碼,thread1 中設定 threadLocal 為 1,而 thread2 設定 threadLocal 為 2,過了一段時間之后,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響,
示例代碼如下:
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
結果如下:
1
可重入代碼(Reentrant Code)
這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞回呼叫它本身),而在控制權回傳后,原來的程式不會出現任何錯誤,
可重入代碼有一些共同的特征,例如不依賴存盤在堆上的資料和公用的系統資源、用到的狀態量都由引數中傳入、不呼叫非可重入的方法等,
彩蛋

超神學院語錄
誰也不知道,自己在戰爭中會是怎樣的人,第一天,我看到驚慌,奔跑,觸目驚心;第二天,一些人堅持不住,退變成魔;第三天,有些人奮不顧身,忘記自我;第四天,我們發誓,一定要贏回來,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/281007.html
標籤:其他
