大話線程安全與線程安全的實現(xiàn)方式
點擊上方藍色字體,選擇“標星公眾號”
優(yōu)質文章,第一時間送達
什么是線程安全
一個類可以被多個線程安全調用那么這個類就是線程安全的。
根據(jù)線程共享數(shù)據(jù)的安全程度可以分為以下五類線程安全:
(1)不可變
(2)絕對線程安全
(3)相對線程安全
(4)線程兼容
(5)線程對立
結尾有彩蛋哦
不可變(Immutable)
不可變(Immutable)的對象一定是線程安全的,不需要再采取任何的線程安全保障措施。只要一個不可變的對象被正確地構建出來,永遠也不會看到它在多個線程之中處于不一致的狀態(tài)。
不可變的類型:
final 關鍵字修飾的基本數(shù)據(jù)類型
String
枚舉類型
Number 部分子類,如 Long 和 Double等數(shù)值包裝類型,BigInteger 和 BigDecimal 等大數(shù)據(jù)類型。但同為 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異常
絕對線程安全
不管運行時環(huán)境如何,調用者都不需要任何額外的同步措施這就是絕對線程安全。
相對線程安全
相對線程安全需要保證對這個對象單獨的操作是線程安全的,在調用的時候不需要做額外的保障措施。但是對于一些特定順序的連續(xù)調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
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);
}
}
});
線程兼容
線程兼容是指對象本身并不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在并發(fā)環(huán)境中可以安全地使用,我們平常說一個類不是線程安全的,絕大多數(shù)時候指的是這一種情況。Java API 中大部分的類都是屬于線程兼容的,如與前面的 Vector 和 HashTable 相對應的集合類 ArrayList 和 HashMap 等。
線程對立
線程對立是指無論調用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼。由于 Java 語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現(xiàn)的,而且通常都是有害的,應當盡量避免。
實現(xiàn)線程安全的方式
互斥同步(阻塞同步)
互斥同步方式實現(xiàn)線程安全也是我們編程中最常用的實現(xiàn)方式。主要是使用 synchronized 和 ReentrantLock。
如果想要初步了解 synchronized 和 ReentrantLock ,可以參考:
聊一聊線程互斥與同步的那些事(以實例解釋synchronized與ReentrantLock)
互斥同步方式是屬于阻塞方式,是一種悲觀的并發(fā)策略,性能上不如非阻塞同步方案。無論共享數(shù)據(jù)是否真的會出現(xiàn)競爭,它都要進行加鎖(這里討論的是概念模型,實際上虛擬機會優(yōu)化掉很大一部分不必要的加鎖)、用戶態(tài)核心態(tài)轉換、維護鎖計數(shù)器和檢查是否有被阻塞的線程需要喚醒等操作。
悲觀策略:不做就不會做錯。(出錯的人總是那些干活的人,不干活的人是不會犯錯的)
非阻塞同步
非阻塞同步方案目前主流的有 CAS,Atomic類。
CAS
CAS是基于沖突檢測的樂觀并發(fā)策略 ,如果沒有其它線程爭用共享數(shù)據(jù),那操作就成功了,否則采取補償措施(不斷地重試,直到成功為止)。這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要將線程阻塞,因此這種同步操作稱為非阻塞同步。樂觀鎖需要操作和沖突檢測這兩個步驟具備原子性,這里就不能再使用互斥同步來保證了,只能靠硬件來完成。硬件支持的原子性操作最典型的是: 比較并交換(Compare-and-Swap,CAS)。CAS 指令需要有 3 個操作數(shù),分別是內存地址 V、舊的預期值 A 和新值 B。當執(zhí)行操作時,只有當 V 的值等于 A,才將 V 的值更新為 B。
硬件速度是高于軟件速度的,因此CAS是比同步互斥的方式性能更佳。
Atomic類
其實原子類的很多方法都是使用了Unsafe的CAS做非阻塞同步,因此在一定程度上說原子類只是CAS的一種JDK實現(xiàn),我們不需要關注內部實現(xiàn),直接使用即可,但是需要明白原子類實現(xiàn)線程安全的機制是非阻塞的,性能高于 synchronized 加鎖的對象。
無同步方案
上面說的兩種方式都是同步方案的兩種解決方案,而這種方案是 無同步。原理很簡單:
如果一個方法本來就不涉及共享數(shù)據(jù),那它自然就無須任何同步措施去保證正確性
實現(xiàn)無同步 的解決方案主要有 :棧封閉 ,線程本地存儲 和 可重入代碼。
棧封閉
首先,java使用棧封閉的方案有 :
(1)JUC線程池: FutureTask詳解
(2)JUC線程池: ThreadPoolExecutor詳解
(3)JUC線程池:ScheduledThreadPool詳解
(4)JUC線程池: Fork/Join框架詳解
多個線程訪問同一個方法的局部變量時,不會出現(xiàn)線程安全問題,因為局部變量存儲在虛擬機棧中,屬于線程私有的。
示例代碼如下:
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實現(xiàn) :Java 并發(fā) - ThreadLocal詳解
如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行。如果能保證,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題。
符合這種特點的應用并不少見,大部分使用消費隊列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程盡量在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的“一個請求對應一個服務器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多 Web 服務端應用都可以使用線程本地存儲來解決線程安全問題。
可以使用 java.lang.ThreadLocal 類來實現(xiàn)線程本地存儲功能。
對于以下代碼,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),可以在代碼執(zhí)行的任何時刻中斷它,轉而去執(zhí)行另外一段代碼(包括遞歸調用它本身),而在控制權返回后,原來的程序不會出現(xiàn)任何錯誤。
可重入代碼有一些共同的特征,例如不依賴存儲在堆上的數(shù)據(jù)和公用的系統(tǒng)資源、用到的狀態(tài)量都由參數(shù)中傳入、不調用非可重入的方法等。
彩蛋

————————————————
版權聲明:本文為CSDN博主「keep-go-on」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權協(xié)議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:
https://blog.csdn.net/qq_26462567/article/details/116142714
鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布
??????
??長按上方微信二維碼 2 秒
感謝點贊支持下哈 
