來源:blog.csdn.net/u014454538/article/details/98515807
1. Java中的執行緒安全
- Java執行緒安全:狹義地認為是多執行緒之間共享資料的訪問,
- Java語言中各種操作共享的資料有5種型別:不可變、絕對執行緒安全、相對執行緒安全、執行緒兼容、執行緒獨立
① 不可變
- 不可變(Immutable) 的物件一定是執行緒安全的,不需要再采取任何的執行緒安全保障措施,
- 只要能正確構建一個不可變物件,該物件永遠不會在多個執行緒之間出現不一致的狀態,
- 多執行緒環境下,應當盡量使物件成為不可變,來滿足執行緒安全,
如何實作不可變?
- 如果共享資料是基本資料型別,使用final關鍵字對其進行修飾,就可以保證它是不可變的,
- 如果共享資料是一個物件,要保證物件的行為不會對其狀態產生任何影響,
- String是不可變的,對其進行substring()、replace()、concat()等操作,回傳的是新的String物件,原始的String物件的值不受影響,而如果對StringBuffer或者StringBuilder物件進行substring()、replace()、append()等操作,直接對原物件的值進行改變,
- 要構建不可變物件,需要將內部狀態變數定義為final型別,如
java.lang.Integer類中將value定義為final型別,
Java 面試題最全整理:https://www.javastack.cn/mst/
private final int value;
常見的不可變的型別:
- final關鍵字修飾的基本資料型別
- 列舉型別、String型別
- 常見的包裝型別:Short、Integer、Long、Float、Double、Byte、Character等
- 大資料型別:BigInteger、BigDecimal
注意:原子類 AtomicInteger 和 AtomicLong 則是可變的,
對于集合型別,可以使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合,
- 通過
Collections.unmodifiableMap(map)獲的一個不可變的Map型別, Collections.unmodifiableXXX()先對原始的集合進行拷貝,需要對集合進行修改的方法都直接拋出例外,
例如,如果獲得的不可變map物件進行put()、remove()、clear()操作,則會拋出UnsupportedOperationException例外,
② 絕對執行緒安全
絕對執行緒安全的實作,通常需要付出很大的、甚至不切實際的代價,
Java API中提供的執行緒安全,大多數都不是絕對執行緒安全,
例如,對于陣列集合Vector的操作,如get()、add()、remove()都是有synchronized關鍵字修飾,有時呼叫時也需要手動添加同步手段,保證多執行緒的安全,
下面的代碼看似不需要同步,實際運行程序中會報錯,
import java.util.Vector;
/**
* @Author: lucy
* @Version 1.0
*/
public class VectorTest {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
while(true){
for (int i = 0; i < 10; i++) {
vector.add(i);
}
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<vector.size();i++){
System.out.println("洗掉vector中的第" + i+"個元素");
vector.remove(i);
}
}
}).start();
while (Thread.activeCount()>20)
return;
}
}
}
出現ArrayIndexOutOfBoundsException例外,原因:某個執行緒恰好洗掉了元素i,使得當前執行緒無法訪問元素i,
Exception in thread "Thread-1109" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1
at java.util.Vector.remove(Vector.java:831)
at VectorTest$2.run(VectorTest.java:28)
at java.lang.Thread.run(Thread.java:745)
需要將對元素的get和remove構造成同步代碼塊:
synchronized (vector){
for (int i = 0; i < vector.size(); i++) {
System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
}
}
synchronized (vector){
for (int i=0;i<vector.size();i++){
System.out.println("洗掉vector中的第" + i+"個元素");
vector.remove(i);
}
}
③ 相對執行緒安全
- 相對執行緒安全需要保證對該物件的單個操作是執行緒安全的,在必要的時候可以使用同步措施實作執行緒安全,
- 大部分的執行緒安全類都屬于相對執行緒安全,如Java容器中的Vector、HashTable、通過
Collections.synchronizedXXX()方法包裝的集合,
④ 執行緒兼容
- Java中大部分的類都是執行緒兼容的,通過添加同步措施,可以保證在多執行緒環境中安全使用這些類的物件,
- 如常見的ArrayList、HashTableMap都是執行緒兼容的,
⑤ 執行緒對立
- 執行緒對立是指:無法通過添加同步措施,實作多執行緒中的安全使用,
- 執行緒對立的常見操作有:Thread類的suspend()和resume()(已經被JDK宣告廢除),
System.setIn()和System.setOut()等,
2. Java的列舉型別
通過enum關鍵字修飾的資料型別,叫列舉型別,
- 列舉型別的每個元素都有自己的序號,通常從0開始編號,
- 可以通過values()方法遍歷列舉型別,通過name()或者toString()獲取列舉型別的名稱
- 通過ordinal()方法獲取列舉型別中元素的序號
public class EnumData {
public static void main(String[] args) {
for (Family family : Family.values()) {
System.out.println(family.name() + ":" + family.ordinal());
}
}
}
enum Family {
GRADMOTHER, GRANDFATHER, MOTHER, FATHER, DAUGHTER, SON;
}

可以將列舉型別看做普通的class,在里面定義final型別的成員變數,便可以為列舉型別中的元素賦初值,
要想獲取列舉型別中元素實際值,需要為成員變數添加getter方法,
雖然列舉型別的元素有了自己的實際值,但是通過ordinal()方法獲取的元素序號不會發生改變,
public class EnumData {
public static void main(String[] args) {
for (Family family : Family.values()) {
System.out.println(family.name() + ":實際值" + family.getValue() +
", 實際序號" + family.ordinal());
}
}
}
enum Family {
GRADMOTHER(3), GRANDFATHER(4), MOTHER(1), FATHER(2), DAUGHTER(5), SON(6);
private final int value;
Family(int value) {
this.value = https://www.cnblogs.com/javastack/archive/2022/11/21/value;
}
public int getValue() {
return value;
}
}

3. Java執行緒安全的實作
① 互斥同步
互斥同步(Mutex Exclusion & Synchronization)是一種常見的并發正確性保障手段,
- 同步:多個執行緒并發訪問共享資料,保證共享資料同一時刻只被一個(或者一些,使用信號量)執行緒使用,
- 互斥:互斥是實作同步的一種手段,主要的互斥實作方式:臨界區(Critical Section)、互斥量(Mutex)、信號量(Semaphore),
同步與互斥的關系:
- 互斥是原因,同步是結果,
- 同步是目的,互斥是方法,
Java中,最基本的實作互斥同步的手段是synchronized關鍵字,其次是JUC包中的ReentrantLock,
關于synchronized關鍵字:
- 編譯后的同步塊,開始處會添加monitorenter指令,結束處或例外處會添加monitorexit指令,
- monitorenter和monitorexit指令中都包含一個參考型別的引數,分別指向加鎖或解鎖的物件,如果是同步代碼塊,則為synchronized括號中明確指定的物件;如果為普通方法,則為當前實體物件;如果為靜態方法,則為類對應的class物件,
- JVM執行monitorenter指令時,要先嘗試獲取鎖:如果物件沒被鎖定或者當前執行緒已經擁有該物件的鎖,則鎖計數器加1;否則獲取鎖失敗,進入阻塞狀態,等待持有鎖的執行緒釋放鎖,
- JVM執行monitorexit指令時,鎖計數器減1,直到計數器的值為0,鎖被釋放,(synchronized是支持重進入的)
- 由于阻塞或者喚醒執行緒都需要從用戶態(User Mode)切換到核心態(Kernel Mode),有時鎖只會被持有很短的時間,沒有必要進行狀態轉換,可以讓執行緒在阻塞之前先自旋等待一段時間,超時未獲取到鎖才進入阻塞狀態,這樣可以避免頻繁的切入到核心態,其實,就是后面自旋鎖的思想,
關于ReentrantLock:
- 與synchronized關鍵字相比,它是API層面的互斥鎖(lock()、unlock()、try...finally),
- 與synchronized關鍵字相比,具有可中斷、支持公平與非公平性、可系結多個Condition物件的高級功能,
- 由于synchronized關鍵字被優化,二者的性能差異并不是很大,如果不是想使用ReentrantLock的高級功能,優先考慮使用synchronized關鍵字,
② 非阻塞同步
(1)CAS概述
互斥同步最大的性能問題是執行緒的阻塞和喚醒,因此又叫阻塞同步,
互斥同步采用悲觀并發策略:
- 多執行緒并發訪問共享資料時,總是認為只要不加正確的同步措施,肯定會出現問題,
- 無論共享資料是否存在競爭,都會執行加鎖、用戶態和心態的切換、維護鎖計數器、檢查是否有被阻塞的執行緒需要喚醒等操作,
隨著硬體指令集的發展,我們可以采用基于沖突檢測的樂觀并發策略:
- 先進行操作,如果不存在沖突(即沒有其他執行緒爭用共享資料),則操作成功,
- 如果有其他執行緒爭用共享資料,產生了沖突,使用其他的補償措施,
- 常見的補償措施:不斷嘗試,直到成功為止,比如回圈的CAS操作,
樂觀并發策略的許多實作都不需要將執行緒阻塞,這種同步操作叫做非阻塞同步,
非阻塞同步依靠的硬體指令集:前三條是比較久遠的指令,后兩條是現代處理器新增的,
- 測驗和設定(Test and Set)
- 獲取并增加(Fetch and Increment)
- 交換(Swap)
- 比較并交換(Compare and Swap,即CAS)
- 加載鏈接/條件存盤(Load Linked/ Store Conditional,即LL/SC)
什么是CAS?
- CAS,即Compare and Swap,需要借助處理器的cmpxchg指令完成,
- CAS指令需要三個運算元:記憶體位置V(Java中可以簡單的理解為變數的記憶體地址)、舊的期待值A、新值B,
- CAS指令執行時,當且僅當V符合舊的預期值A,處理器才用新值B更新V的值;否則,不執行更新,
- 不管是否更新V的值,都回傳V的舊值,整個處理程序是一個原子操作,
原子操作:所謂的原子操作是指一個或一系列不可被中斷的操作,
Java中的CAS操作:
- Java中的CAS操作由
sun.misc.Unsafe中的compareAndSwapInt()、compareAndSwapLong()等幾個方法包裝提供,實際無法呼叫這些方法,需要采用反射機制才能使用, - 在實際的開發程序中,一般通過其他的Java API呼叫它們,如JUC包原子類中的
compareAndSet(expect, update)、getAndIncrement()等方法,這些方法內部都使用了Unsafe類的CAS操作, - Unsafe類的CAS操作,通過JVM的即時編譯器編譯后,是一條與平臺相關的CAS指令,
除了偏向鎖,Java中其他鎖的實作方式都是用了回圈的CAS操作,
(2)通過回圈的CAS實作原子操作
通過++i或者i++可以實作計數器的自增,在多執行緒環境下,這樣使用是非執行緒安全的,
public class UnsafeCount {
private int i = 0;
private static final int THREADS_COUNT = 200;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
UnsafeCount counter = new UnsafeCount();
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
counter.count();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("多執行緒呼叫計數器i,運行后的值為: " + counter.i);
}
public void count() {
i++;
}
}
運行以上的代碼發現:當執行緒數量增加,每個執行緒呼叫計數器的次數變大時,每次運行的結果是錯誤且不固定的,


為了實作實在一個多執行緒環境下、執行緒安全的計數器,需要使用AtomicInteger的原子自增運算,
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCount {
private AtomicInteger atomic = new AtomicInteger(0);
private static final int THREAD_COUNT = 200;
public static void main(String[] args) {
SafeCount counter = new SafeCount();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j=0;j<10000;j++){
counter.count();
}
}
});
threads[i].start();
}
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println("多執行緒呼叫執行緒安全的計數器atomic:"+counter.atomic);
}
public void count() {
// 呼叫compareAnSet方法,使用回圈的CAS操作實作計數器的原子自增
for (; ; ) {
int expect = atomic.get();
int curVal = expect + 1;
if (atomic.compareAndSet(expect, curVal)) {
break;
}
}
}
}

與非執行緒安全的計數器相比,執行緒安全的計數器有以下特點:
- 將int型別的計數器變數i,更換成具有CAS操作的AtomicInteger型別的計數器變數atomic,
- 進行自增運算時,通過回圈的CAS操作實作atomic的原子自增,
- 先通過atomic.get()獲取expect的值,將expect加一得到新值,然后通過
atomic.compareAndSet(expect, curVal)這一方法實作CAS操作, - 其中compareAndSet()回傳的true或者false,表示此次CAS操作是否成功,如果回傳false,則不停地重復執行CAS操作,直到操作成功,
上面的count方法實作的AtomicInteger原子自增,可以只需要呼叫incrementAndGet()一個方法就能實作,
public void count() {
// 呼叫incrementAndGet方法,實作AtomicInteger的原子自增
atomic.incrementAndGet();
}
因為incrementAndGet()方法,封裝了通過回圈的CAS操作實作AtomicInteger原子自增的代碼,
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
(3)CAS操作存在的問題
1. ABA問題
- 在執行CAS操作更新共享變數的值時,如果一個值原來是A,被其他執行緒改成了B,然后又改回成了A,對于該CAS操作來說,它完全感受不到共享變數值的變化,這種操作漏洞稱為CAS操作的ABA問題,
- 解決該問題的思路是,為變數添加版本號,每次更新時版本號遞增,這種場景下就成了1A --> 2B --> 3A,CAS操作就能檢測到共享變數的ABA問題了,
- JUC包中,也提供了相應的帶標記的原子參考類AtomicStampedReference來解決ABA問題,
- AtomicStampedReference的compareAndSet()方法會首先比較期待的參考是否等于當前參考,然后檢查期待的標記是否等于當前標記,如果全部相等,則以原子操作的方式將新的參考和新的標記更新到當前值中,
- 但是AtomicStampedReference目前比較雞肋,如果想解決AB問題,可以使用鎖,
2. 回圈時間過長,開銷大
回圈的CAS操作如果長時間不成功,會給CPU帶來非常大的執行開銷,
3. 只能保證一個共享變數的原子操作
- 只對一個共享變數執行操作時,可以通過回圈的CAS操作實作,如果是多個共享變數,回圈的CAS操作無法保證操作的原子性,
- 取巧的操作:將多個共享變數合為一個變數進行CAS操作,JDK1.5開始,提供了AtomicReference類保證參考物件之間的原子性,可以將多個變數放在一個物件中進行CAS操作,
③ 無同步方案
同步只是保證共享資料爭用時正確性的一種手段,如果不存在共享資料,自然無須任何同步措施,
(1)堆疊封閉
多個執行緒訪問同一個方法的區域變數時,不會出現執行緒安全問題,
因為方法中的區域變數不會逃出該方法而被其他執行緒訪問,因此可以看做JVM堆疊中資料,屬于執行緒私有,
(2)可重入代碼(Reentrant Code)
可重入代碼又叫純代碼(Pure Code),可在代碼執行的任何時候中斷他它,轉去執行另外一段代碼(包括遞回呼叫它本身),控制權回傳后,原來的程式不會出現任何錯誤,
所有可重入的代碼都是執行緒安全,并非所有執行緒安全的代碼都是可重入的,
可重入代碼的共同特征:
- 不依賴存盤在堆上的資料和公用的系統資源
- 用到的狀態量都由引數中傳入
- 不呼叫非可重用的方法
如何判斷代碼是否具備可重入性?如果一個方法,它的回傳結果是可預測的,只要輸入了相同的資料,就都能回傳相同的結果,那它就滿足可重入性,當然也就是執行緒安全的,
(3)執行緒本地存盤(TLS)
執行緒本地存盤(Thread Local Storage):
- 如果一段代碼中所需要的資料必須與其他代碼共享,那就看看這些共享資料的代碼是否能保證在同一個執行緒中執行,
- 如果能保證,我們就可以把共享資料的可見范圍限制在同一個執行緒內,
- 這樣,無須同步也能保證執行緒之間不出現資料爭用的問題,
TLS的重要應用實體:經典的Web互動模型中,一個請求對應一個服務器執行緒,使得Web服務器應用可以使用,
Java中沒有關鍵字可以將一個變數定義為執行緒所獨享,但是Java中創建了java.lang.ThreadLocal類提供執行緒本地存盤功能,
- 每一個執行緒內部都包含一個ThreadLocalMap物件,該物件將ThreadLocal物件的hashCode值作為key,即ThreadLocal.threadLocalHashCode,將本地執行緒變數作為value,構成鍵值對,
- ThreadLocal物件是當前執行緒ThreadLocalMap物件的訪問入口,通過
threadLocal.set()為本地執行緒添加獨享變數;通過threadLocal.get()獲取本地執行緒獨享變數的值, - ThreadLocal、ThreadLocalMap、Thread的關系:Thread物件中包含ThreadLocalMap物件,ThreadLocalMap物件中包含多個鍵值對,每個鍵值對的key是ThreadLocal物件的hashCode,value是本地執行緒變數,

ThreadLocal的編程實體:
- 想為某個執行緒添加本地執行緒變數,必須通過ThreadLocal物件在該執行緒中進行添加,構造出的鍵值對自動存入該執行緒的map中;
- 想要獲取某個執行緒的本地執行緒變數,必須在該執行緒中獲取,會自動查詢該執行緒的map,獲得ThreadLocal物件對應的value,
- 通過ThreadLocal物件重復為某個執行緒添加鍵值對,會覆寫之前的value,
public class TLS {
public static void main(String[] args) {
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// 設定當前執行緒的本地執行緒變數
threadLocal1.set("thread1");
threadLocal2.set(1);
System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
// 使用完畢后要洗掉,避免記憶體泄露
threadLocal1.remove();
threadLocal2.remove();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal1.set("thread2");
threadLocal2.set(2);
System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
threadLocal1.remove();
threadLocal2.remove();
}
});
thread1.start();
thread2.start();
// 沒有通過ThreadLocal為主執行緒添加過本地執行緒變數,獲取到的內容都是null
System.out.println(threadLocal1.get()+": "+threadLocal2.get());
}
}

對ThreadLocal的正確理解:
- ThreadLocal適用于執行緒需要有自己的實體變數,該實體變數可以在多個方法中被使用,但是不能被其他執行緒共享的場景,
- 由于不存在資料共享,何談同步?因此ThreadLocal 從理論上講,不是用來解決多執行緒并發問題的,
ThreadLocal的實作:
最原始的想法:ThreadLocal維護執行緒與實體的映射,既然通過ThreadLocal物件為執行緒添加本地執行緒變數,那就將ThreadLocalMap放在ThreadLocal中,

原始想法存在的缺陷:多執行緒并發訪問ThreadLocal中的Map,需要添加鎖,這是, JDK 未采用該方案的一個原因,
優化后的方法:Thread維護ThreadLocal與實體的映射,Map是每個執行緒所私有,只能在當前執行緒通過ThreadLocal物件訪問自身的Map,不存在多執行緒并發訪問同一個Map的情況,也就不需要鎖,
優化后存在記憶體泄露的情況:JDK1.8中,ThreadLocalMap每個Entry對ThreadLocal物件是弱參考,對每個實體是強參考,當ThreadLocal物件被回收后,該Entry的鍵變成null,但Entry無法被移除,使得實體被Entry參考無法回收,造成記憶體泄露,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/537680.html
標籤:其他
上一篇:mybatis-plus入門
下一篇:10python字典
