一、定義:什麼是線程安全性
當多個線程訪問某個類時,不管運行時環境採用 何種調度方式 或者這些進程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類就是線程安全的。
二、線程安全性的三個體現
- 原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操作(Atomic、CAS算法、synchronized、Lock)
- 可見性:一個主內存的線程如果進行了修改,可以及時被其他線程觀察到(synchronized、volatile)
- 有序性:如果兩個線程不能從 happens-before原則 觀察出來,那麼就不能觀察他們的有序性,虛擬機可以隨意的對他們進行重排序,導致其觀察觀察結果雜亂無序(happens-before原則)
三、線程安全性:原子性
3.1、原子性 — Atomic包
在Java jdk中裏面提供了很多Atomic類
- AtomicXXX:CAS、Unsafe.compareAndSwapInt
- AtomicLong、LongAdder
- AtomicReference、AtomicReferenceFieldUpdater
- AtomicStampReference:CAS的ABA問題
由於CAS原語的直接操作與計算機底層的聯繫很大,CAS原語有三個參數,內存地址
、期望值
、新值
。我們在Java中一般不去直接寫CAS相關的代碼,JDK爲我們封裝在AtomicXXX中,因此,我們直接使用就可以了。
詳解CAS:https://blog.csdn.net/v123411739/article/details/79561458?spm=a2c4e.10696291.0.0.322b19a4UMRo6b
我們在 java.util.concurrent.atomic
目錄中可以看到我們這些類,包下提供了AtomicBoolean
、AtomicLong
、AtomicInteger
三種原子更新基本類型和一個比較好玩的類AtomicReference
,這些類都有一個共同點,都支持CAS,以AtomicInteger
爲重點講解。
3.1.1、AtomicInteger
AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減
以下是AtomicIntege
基本包括的方法:
public final int getAndSet(int newValue) //給AtomicInteger設置newValue並返回加oldValue
public final boolean compareAndSet(int expect, int update) //如果輸入的值和期望值相等就set並返回true/false
public final int getAndIncrement() //對AtomicInteger原子的加1並返回當前自增前的value
public final int getAndDecrement() //對AtomicInteger原子的減1並返回自減之前的的value
public final int getAndAdd(int delta) //對AtomicInteger原子的加上delta值並返加之前的value
public final int incrementAndGet() //對AtomicInteger原子的加1並返回加1後的值
public final int decrementAndGet() //對AtomicInteger原子的減1並返回減1後的值
public final int addAndGet(int delta) //給AtomicInteger原子的加上指定的delta值並返回加後的值
示例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class AtomicIntegerExample {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
//獲取線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//定義信號量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
}
}
這裏我們使用請求總數爲:5000
,同時執行的併發線程數爲:200
,我們最終需要得到結果爲:5000
,這個執行結果纔算正確。
查看返回結果:
13:43:26.473 [main] INFO com.mmall.concurrency.example.atomic.AtomicIntegerExample - count:5000
最後結果是 5000
表示是線程安全的。
我們來看看 AtomicInteger
底層代碼中到底爲我們做了什麼?首先我們來看 AtomicInteger.incrementAndGet()
方法
public class AtomicInteger extends Number implements java.io.Serializable{
/**
* 對AtomicInteger原子的加1並返回加1後的值
* @return 更新的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
AtomicInteger
調用了java底層的 unsafe.getAndAddInt()
方法,這裏是實現CAS 的關鍵。
incrementAndGet()
是將自增後的值返回,還有一個方法getAndIncrement()
是將自增前的值返回,分別對應++i
和i++
操作。同樣的decrementAndGet()
和getAndDecrement()
則對--i
和i--
操作。
Unsafe類是在sun.misc包下,不屬於Java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基於
Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運行效率,增強Java語言底層操作
能力方面起了很大的作用。Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。
過度的使用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。通常我們最好也不
要使用Unsafe類,除非有明確的目的,並且也要對它有深入的瞭解才行。
再來看 Unsafe.getAndAddInt()
方法
/*
* 其中getIntVolatile和compareAndSwapInt都是native方法
* getIntVolatile是獲取當前的期望值
* compareAndSwapInt就是我們平時說的CAS(compare and swap),通過比較如果內存區的值沒有改變,那麼就用新值直接給該內存區賦值
*/
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;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
我們可以看到getAndAddInt(Object var1, long var2, int var4)
,傳進來的第一個參數是當前的一個對象,也就是我們的:count.incrementAndGet()
,在getAndAddInt()
中,var1就是count,var2就是當前的值,比如當前循環中count值爲 2,var4爲每次遞增1
其次getAndAddInt()
方法中涉及到的兩個方法調用都定義爲native,即java底層實現的本地方法
- getIntVolatile():獲取保存當前對象count的主存地址的引用(注意不是對象的值,是引用)。
- compareAndSwapInt():比較當前對象的值和底層該對象的值是否相等,如果相等,則將當前對象值加1,如果不相等,則重新去獲取底層該對象的值,這個方法的實現就是CPU的CAS(compare and swap)操作。
我們知道volatile
具有一致性的特徵,但是它不具備原子性,爲什麼AtomicInteger
卻同時具備一致性和原子性,原來在AtomicInteger
源碼中實現了這樣一串代碼:private volatile int value;
,在AtomicInteger
內部實現就使用了volatile
關鍵字,這就是爲什麼執行CAS(對CAS有疑問的點擊此處)操作的時候,從底層獲取的數據就是最新的數據:
如果當前要保存的值和內存中最新的值不相等的話,說明在這個過程中被其他線程修改了,只
能獲取更新當前值爲最新值,再那這個當前值再去和重新去內存獲取的最新值比較,直到二者
相等的時候,才完成+1的過程.
使用AtomicInteger
的好處在於,它不同於sychronized
關鍵字或lock
用鎖的形式來實現原子性,加鎖會影響性能,而是採用循環比較的形式來提高性能。
3.1.2、AtomicLong
AtomicLong
是作用是對長整形進行原子操作,依靠底層的cas來保障原子性的更新數據,在要添加或者減少的時候,會使用死循環不斷地cas到特定的值,從而達到更新數據的目的。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class AtomicLongExample {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;
public static AtomicLong count = new AtomicLong(0);
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
// count.getAndIncrement();
}
}
執行結果:
14:59:38.978 [main] INFO com.mmall.concurrency.example.atomic.AtomicLongExample - count:5000
最後結果是 5000
表示是線程安全的。
3.1.3、AtomicBoolean
AtomicBoolean位於java.util.concurrent.atomic包下,是java提供給的可以保證數據的原子性操作的一個類
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class AtomicBooleanExample {
private static AtomicBoolean isHappened = new AtomicBoolean(false);
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
}
private static void test() {
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
}
返回結果:
15:04:54.954 [pool-1-thread-2] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - execute
15:04:54.971 [main] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - isHappened:true
這裏我們發現log.info("execute");
,在代碼中只執行了一次,並且isHappened:true
的值爲true,這是爲啥呢?
這裏是因爲當程序第一次compareAndSet()
的時候,使得isHappend
變爲了true,因爲原子性的關係,沒有其他線程進行干擾,通過使用AtomicBoolean,我們使某段代碼只執行一次。
3.1.4、AtomicReference
AtomicReference
和AtomicInteger
非常類似,不同之處就在於AtomicInteger
是對整數的封裝,底層採用的是compareAndSwapInt
實現CAS,比較的是數值是否相等,而AtomicReference
則對應普通的對象引用,底層使用的是compareAndSwapObject
實現CAS,比較的是兩個對象的地址是否相等。也就是它可以保證你在修改對象引用時的線程安全性。
多個線程之間的操作無論採用何種執行時序或交替方式,都要保證不變性條件不被破壞,要
保持狀態的一致性,就需要在單個原子操作中更新相關的狀態變量。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class AtomicReferenceExample {
private static AtomicReference<Integer> count = new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0, 2);
count.compareAndSet(0, 1);
count.compareAndSet(1, 3);
count.compareAndSet(2, 4);
count.compareAndSet(3, 5);
log.info("count:{}", count.get());
}
}
大家覺得我們輸出的結果會是多少?
返回結果:
15:26:59.680 [main] INFO com.mmall.concurrency.example.atomic.AtomicReferenceExample - count:4
爲什麼是4呢?
首先我們 要說的是public final boolean compareAndSet(V expect, V update)
這個方法,這個方法主要的作用是通過比對兩個對象,然後更新爲新的對象,這裏的比對兩個對象,比對的方式不是equals
而是==
,意味着比對的是內存的中地址。
1、首先我們創建count
的初始化爲0
2、在main方法中 count.compareAndSet(0, 2);
,判斷count爲0時賦值爲2
3、在count.compareAndSet(0, 1);
和 count.compareAndSet(1, 3);
判斷count是否爲1或者0,因爲上一步我們已經賦值爲2了,所以判斷不成立
4、在count.compareAndSet(2, 4);
判斷count是否爲2,等式成立
5、最好輸出結果爲4
count.compareAndSet(0, 2); //count=0?賦值 2,判斷成立,此時count=0,更新後爲2
count.compareAndSet(0, 1); //count=0?賦值 1,判斷不成立,此時count=2
count.compareAndSet(1, 3); //count=1?賦值 3,判斷不成立,此時count=2
count.compareAndSet(2, 4); //count=2?賦值 4,判斷成立,此時count=2,更新後count=4
count.compareAndSet(3, 5); //count=3?賦值 5,判斷不成立,此時count=4
所以我們輸出結果爲:4
3.1.5、CAS中ABA問題的解決
CAS並非完美的,它會導致ABA問題,例如:當前內存的值一開始是A,被另外一個線程先改爲B然後再改爲A,那麼當前線程訪問的時候發現是A,則認爲它沒有被其他線程訪問過。在某些場景下這樣是存在錯誤風險的。比如在鏈表中。
如何解決這個ABA問題呢,大多數情況下樂觀鎖的實現都會通過引入一個版本號標記這個對象,每次修改版本號都會變話,比如使用時間戳作爲版本號,這樣就可以很好的解決ABA問題。
在JDK中提供了AtomicStampedReference
類來解決這個問題,這個類維護了一個int類型的標記stamp,每次更新數據的時候順帶更新一下stamp。
3.2、原子性 — synchronized
synchronized是一種同步鎖,通過鎖實現原子操作。
1、修飾代碼塊:大括號括起來的代碼,作用於調用的對象
2、修飾方法:整個方法,作用於調用的對象
3、修飾靜態方法:整個靜態方法,作用於所有對象
4、修飾類:括號括起來的部分,作用於所有對象
詳細可以查看,我寫的關於:synchronized的博客,因爲寫過所以就不做過多描述。
3.3、原子性 — 對比
- Atomic:競爭激烈時能維持常態,比Lock性能好, 只能同步一個值
- synchronized:不可中斷鎖,適合競爭不激烈,可讀性好的情況
- Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態
四、線程安全性:可見性
簡介:一個線程對主內存的修改可以及時被其他線程觀察到
導致共享變量在線程間不可見的原因:
1.線程交叉執行
2.重新排序結合線程交叉執行
3.共享變量更新後的值沒有在工作內存中與主內存間及時更新
4.1 可見性 — syncronized
JMM關於syncronized
的兩條規定:
- 線程解鎖前,必須把共享變量的最新值刷新到主內存中
- 線程加鎖時,將清空工作內存中共享變量的值,從而使得使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖)
由於syncronized可以保證原子性及可見性,變量只要被syncronized修飾,就可以放心的使用
4.2 可見性 — volatile
通過加入內存屏障
和禁止重排序
優化來實現可見性。
具體實現過程:
- 對
volatile
變量寫操作時,會在寫操作後加入一條store
屏障指令,將本地內存中的共享變量值刷新到主內存 - 對
volatile
變量讀操作時,會在讀操作前加入一條load
屏障指令,從主內存中讀取共享變量
volatile
不能保證操作的原子性,也就是不能保證線程安全性, 如果需要使用volatile
必須滿足以下兩個條件:
- 對變量的寫操作不依賴與變量當前的值。
- 該變量沒有包含在具有其他變量的不變的式子中。
所以volatile
修飾的變量適合作爲狀態標記量。
注:以下圖片爲資料中獲取,如有雷同,純屬巧合
示例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class VolatileExample {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;
public static volatile int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
}
}
返回結果:
16:12:01.404 [main] INFO com.mmall.concurrency.example.count.VolatileExample4 - count:4986
通過執行代碼我們可以發現,返回結果並不是我們想看到的5000,說明這個是線程不安全的類
主要是因爲當我們執行conut++
時分成了三步:
1、取出當前內存count值,這時count值時最新的
2、+1操作
3、重新寫回主存
例如:有兩個線程同時在執行count++
,兩個內存都執行了第一步,比如當前count值爲99,它們都讀到了這個count值,然後兩個線程分別執行了+1
,並寫回主存,這樣就丟掉了一次+1
的操作。
五、線程安全性:有序性
- 在JMM中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
- 通過volatile、synchronized、lock保證有序性
5.1 happens-before原則
- 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
- 鎖定規則:一個
unLock
操作先行發生於後面對同一個鎖的Lock()
操作,也就是說只有先解鎖才能對下面的線程進行加鎖 - volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作
- 傳遞規則:如果操作A先行發生與操作B,而操作B先行發生於操作C,則操作A先行發生於操作C
- 線程啓動規則:
Thread對象
的start()
方法先行發生於此線程的每一個動作,一個線程只有執行了start()
方法後才能做其他的操作 - 線程終端規則:對線程
interrupt()
方法的調用先行發生與被中斷線程的代碼檢測到中斷事件的發生(只有執行了interrupt()
方法纔可以檢測到中斷事件的發生) - 線程終結規則:線程中所有操作都先行發生於線程的終止檢測,我們可以通過
Thread.join()
方法結束,Thread.isAlive()
的返回值手段檢測到線程已經終止執行 - 對象終結規則:一個對象的初始化完成先行發生於他的
finalize()
方法的開始
六、線程安全性:總結
- 原子性:Atomic包、CAS算法、synchronized、Lock
原子性做了互斥方法,同一個線程只能有一個進行操作
- 可見性:synchronized、volatile
一個主內存的線程如果進行了修改,可以及時被其他線程觀察到,介紹了volatile如何被觀察到的
- 有序性:happens-before原則
happens-before原則,觀察結果,如果兩個線程不能偶從happens-before原則觀察出來,那麼就不能觀察他們的有序性,虛擬機可以隨意的對他們進行重排序