1 線程安全性
當(dāng)多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式或者這些進程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要額外的同步和協(xié)同,這個類都能表現(xiàn)出正確的行為。
線程安全性主要體現(xiàn)在以下三個方面:
- 原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行操作。
- 可見性:一個線程對主內(nèi)存的修改可以及時的被其它線程觀察到。
- 有序性:一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序的存在,該觀察結(jié)果一般雜亂無序。
1.1 原子性
由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(例外就是long和double的非原子協(xié)定,讀者只要知道這件事就可以了,無須太過于在意這些幾乎不會發(fā)生的例外情況)。
如果應(yīng)用場景需要一個更大范圍的原子性保證,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機未把lock和unlcok操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式使用這兩個操作。這兩個字節(jié)碼指令反映到Java代碼中就是同步塊 - synchronized。
1.1.1 Atomic原子性實現(xiàn)
Atomic的包名為java.util.concurrent.atomic。這個包里面提供了一組原子變量的操作類,這些類可以保證在多線程環(huán)境下,當(dāng)某個線程在執(zhí)行atomic的方法時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執(zhí)行完成,才由JVM從等待隊列中選擇一個線程執(zhí)行。

Atomic包的實現(xiàn)主要基于CAS(Compare And Set)操作,我們開篇提供了一個計數(shù)器的功能,發(fā)現(xiàn)每次執(zhí)行結(jié)果沒有達到預(yù)期,在之前版本做稍許改動
@Slf4j
@ThreadSafe
public class AtomicExample1 {
// 請求總數(shù)
public static int clientTotal = 5000;
// 同時并發(fā)執(zhí)行的線程數(shù)
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();
// count.getAndIncrement();
}
}
//每次能達到預(yù)期效果
我們一起看看AtomicInteger的incrementAndGet的內(nèi)部實現(xiàn),其余幾個方法的原理跟這個相同,在此不再過多的解釋
/**
* 當(dāng)前值自增1
*/
public final int incrementAndGet() {
//Unsafe是JDK內(nèi)部用的工具類。它通過暴露一些Java意義上說“不安全”的功能給Java層代碼
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* AtomicInteger 中的CAS操作就是compareAndSet(),其作用是每次從內(nèi)存中
*根據(jù)內(nèi)存偏移量(valueOffset)取出數(shù)據(jù),將取出的值跟expect 比較,如果數(shù)據(jù)一致就把內(nèi)存中的值改為update。這樣使用CAS就保證了原子操作
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
關(guān)于Atomic我們說最后一點,CAS的ABA問題,如count=1,A線程在獲取值的時候,count值可能被其它線程改變但最終改回成了1。導(dǎo)致與實現(xiàn)思想有偏差,為了解決這個請參考AtomicStampedReference的實現(xiàn),修改對應(yīng)一個版本號
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//在之前CAS操作的基礎(chǔ)上添加了版本號的比較
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
1.1.2 Synchronized原子性實現(xiàn)
- 修飾代碼塊:大括號括起來的代碼,作用于調(diào)用對象。
- 修飾方法:整個方法,作用于調(diào)用對象。
- 修飾靜態(tài)方法:整個靜態(tài)方法,作用于所有對象。
- 修飾類:括號括起來的部分,作用于所有對象。
//修飾代碼塊
synchronized (this) { ... }
//修飾方法
public synchronized void test(int j) { ... }
//修飾靜態(tài)方法
public static synchronized void test2(int j){ ... }
//修飾類
synchronized (SynchronizedExample2.class) { ... }
1.1.3 原子性-對比
-Synchronized:不可中斷鎖,適合競爭不激烈,可讀性好。
-Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態(tài)。
-Atomic:競爭激烈時能維持常態(tài),比Lock性能好;只能同步一個值。
1.2 可見性
Java內(nèi)存模型中,允許編譯器和處理器指令進行重排序,重排序并不會影響到單線程的執(zhí)行,卻會影響到多線程的執(zhí)行。
可見性是指當(dāng)一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。從前面http://www.itdecent.cn/p/9005b30c0fe2章節(jié)我們了解到Java內(nèi)存模型是通過在變量修改后將新值同步回主存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主存作為傳遞媒介的方式來實現(xiàn)可見性的。
導(dǎo)致共享變量在線程間不可見的原因:
- 線程交叉執(zhí)行
- 重排序結(jié)合線程交叉執(zhí)行
- 共享變量更新后的值沒有在工作內(nèi)存與主內(nèi)存及時更新
1.2.1 Synchronized可見性實現(xiàn)
JMM關(guān)于synchronized的兩條規(guī)定
- 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存。
- 線程加鎖時,將清空工作內(nèi)存共享變量的值,從而使用共享變量時需要從主內(nèi)存重新讀取共享變量的值(加鎖和解鎖必須為同一把鎖)
1.2.2 Volatile可見性實現(xiàn)
通過內(nèi)存屏障和禁止重排序優(yōu)化來實現(xiàn)。
- 對Volatile變量寫操作時,會在寫操作后加入一條store屏障指令,將本地內(nèi)存中共享變量值刷新到主內(nèi)存。
-
對Volatile變量讀操作時,會在讀操作前加入一條load指令,從主內(nèi)存讀取共享變量的值。
Volatile寫.png

關(guān)于Volatile變量的可見性,經(jīng)常會被開發(fā)人員誤解,認(rèn)為以下描述成立:"volatile變量對所有線程是立即可見的,對volatile變量所有的寫操作都能立刻反應(yīng)到其它線程中,換句話說,volatile變量在各個線程中是一致的,所以基于volatile變量的運算在并發(fā)下是安全的",這句話的論據(jù)沒有錯,但是論據(jù)并不能得出"基于volatile變量的運算在并發(fā)下是安全的",因為Java里面的運算并非原子操作,導(dǎo)致volatile變量的運算在并發(fā)下一樣不安全,我們還是通過計數(shù)器的例子來說明原因。
@Slf4j
@NotThreadSafe
public class CountExample4 {
// 請求總數(shù)
public static int clientTotal = 5000;
// 同時并發(fā)執(zhí)行的線程數(shù)
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++;
//可能在往主存回寫的時候出現(xiàn)問題
// 1、count
// 2、+1
// 3、count
}
}
1.3 有序性
如果在本線程內(nèi)觀察,所有的操作都是有序的;如果一個線程中觀察另外一個線程,所有操作都是無序的。前半句是指"線程內(nèi)表現(xiàn)為串行的語義",后半句是指"指令重排序"現(xiàn)象和"工作內(nèi)存與主內(nèi)存同步延遲"現(xiàn)象。
Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由"一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作"這條規(guī)則獲得。
1.3.1 Happens-before原則
如果Java內(nèi)存模型中所有的有序性都僅僅靠volatile和synchronized來完成,那么有一些操作將會變得很繁瑣,但是我們在編寫并發(fā)代碼時并沒有感覺到這一點,這是因為Java語言有一個"先行發(fā)生"(happens-before)的原則
//以下操作在線程A執(zhí)行
i = 1;
//以下操作在線程B執(zhí)行
j = i;
//以下操作在線程C執(zhí)行
i = 2;
//假設(shè)線程A中的操作先行發(fā)生于線程B的操作,那么可以確定在線程B操作執(zhí)行后,變量j的值一定等于1,得出結(jié)論依據(jù)有兩個:一是根據(jù)先行發(fā)生原則,i=1的結(jié)果可以被觀察到; 二是線程C還沒有登場;假設(shè)A還是先行于B,但是線程C出現(xiàn)在A、B之間,但是線程C與B沒有先行發(fā)生的關(guān)系,此時j的值就可能變得不確定了,1和2都有可能
下面是Java內(nèi)存模型下一些"天然的"先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就存在。
- 程序次序規(guī)則:在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說,應(yīng)該是控制流順序而不是代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強調(diào)是同一個鎖,而"后面"是指時間上的先后順序。
- volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的"后面"同樣是值時間上的先后順序。
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止監(jiān)測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
-線程中斷規(guī)則: 對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時間的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。 - 對象終結(jié)規(guī)則::一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
-傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
