Java高并發(fā)--CPU多級緩存與Java內(nèi)存模型
主要是學習慕課網(wǎng)實戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記
CPU多級緩存
為什么需要CPU緩存:CPU的頻率太快,以至于主存跟不上,這樣在處理器時鐘周期內(nèi),CPU常常需要等待主存,浪費了資源。所有緩存的出現(xiàn)是為了緩解CPU和主存之間速度不匹配的問題——將運算所需數(shù)據(jù)復制到緩存中,使得運算能快速進行;當運算結(jié)束后再將緩存同步回內(nèi)存中,這樣處理器無需等待緩慢的內(nèi)存讀寫。

緩存并非存儲了所有的數(shù)據(jù),那么它存在的意義是什么?
- 時間局部性:如果某個數(shù)據(jù)被訪問,那么它在不久的將來有可能被再次訪問
- 空間局部性:如果某個數(shù)據(jù)被訪問,那么與它相鄰的數(shù)據(jù)很快可能被訪問
Java內(nèi)存模型
下圖中是JVM中堆和棧的關系,在不同的線程中,可能有多個變量,它們指向的是堆上的同一個對象,這些變量都是該對象的“私有拷貝”。私有表示僅在當前線程可訪問,拷貝是說這是該對象的一個引用。

線程A和線程B之間如果要通信的話,必須經(jīng)歷以下兩個步驟:
- 線程A將本地內(nèi)存(工作內(nèi)存)中的變量刷新到主內(nèi)存中
- 主內(nèi)存將變量復制到本地內(nèi)存B中,使得線程B在讀取變量時更快

Java內(nèi)存模型中,所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(與高速緩存類比),線程對變量的所有操作都必須在工作內(nèi)存中進行,不能直接讀寫主內(nèi)存中的變量;不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需通過主內(nèi)存完成。
如果將Java內(nèi)存模型和Java堆、棧比較,主內(nèi)存對應Java堆中的對象實例部分,工作內(nèi)存對應虛擬機棧中的部分。
主內(nèi)存和工作內(nèi)存之間需要交互,Java內(nèi)存模型中有8種原子操作:
- lock:作用于主內(nèi)存變量,將其標識為線程獨占。
- unlock:作用于主內(nèi)存變量,將其從鎖定狀態(tài)釋放,釋放后才可被其他線程鎖定。
- read:作用于主內(nèi)存的變量,將一個變量從主內(nèi)存中傳輸?shù)焦ぷ鲀?nèi)存中,以便隨后的load動作使用。
- load:作用于工作內(nèi)存中的變量,把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use:作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳遞給執(zhí)行引擎,當虛擬機需要使用到變量的值的字節(jié)碼指令時會執(zhí)行這個操作。
- assign:作用于工作內(nèi)存的變量,把一個從執(zhí)行引擎接受到的值賦給工作內(nèi)存中的變量。當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store:作用于工作內(nèi)存中的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便之后write操作使用。
- write:作用于主內(nèi)存中的變量,把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。
如果一個變量從主內(nèi)存復制到工作內(nèi)存,必須先執(zhí)行read然后執(zhí)行l(wèi)oad操作(read和load之間允許插入其他操作,只要保證這個順序即可);如果要把變量從工作內(nèi)存同步回主內(nèi)存中,需要先執(zhí)行store操作然后執(zhí)行write操作(store和write之間允許插入其他操作,只要保證這個順序即可)。

最后來看一個線程不安全的例子
package com.shy.concurrency;
import com.shy.concurrency.annotations.NotThreadSafe;
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;
/**
* @author Haiyu
* @date 2018/12/16 16:08
*/
@Slf4j
@NotThreadSafe
public class ConcurrencyTest {
// 請求總數(shù)
public static int requestTotal = 5000;
// 并發(fā)量,同時進入臨界區(qū)的線程數(shù)量
public static int concurrentTotal = 20;
// 計數(shù)器
public static int count = 0;
@NotThreadSafe
private static void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
// 信號量,控制并發(fā)數(shù)
final Semaphore semaphore = new Semaphore(concurrentTotal);
// 倒計數(shù)器,在這里用來阻塞主線程直到計數(shù)器為0
final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
// requestTotal次請求,每一次請求自增1,按理說最后count的值是requestTotal
// 但是在并發(fā)下,多個線程同時執(zhí)行了add()使得多次自增值只增加了1,導致最后的結(jié)果比requestTotal小
for (int i = 0; i < requestTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
// 最多可同時concurrentTotal個線程同時執(zhí)行
add();
semaphore.release();
} catch (InterruptedException e) {
log.error("exception", e);
}
// 每自增一次,計數(shù)器減1
countDownLatch.countDown();
});
}
// 主線程在countdownLatch上等待,等計數(shù)器為0后才能執(zhí)行
countDownLatch.await();
log.info("count:{}", count);
executorService.shutdown();
}
}
總共發(fā)起5000次請求(5000個線程),每次請求對count變量做自增操作。顯然在并發(fā)下,主線程等5000個線程執(zhí)行完畢后count一般是小于5000的。原因如下:
以兩個線程為例,它們在同一時刻從主內(nèi)存中讀取count的值并裝載到各自的工作內(nèi)存中,此時count的值是一樣的,假設都是10。此時線程A先自增,將自增后的值更新到工作內(nèi)存,最后刷回主內(nèi)存,count變成了11;而在線程B也進行同樣的自增操作,注意之前線程B已經(jīng)讀取過count的值了,此時在B的工作內(nèi)存中的count還是等于10的,接著B也更新count,最后刷回主內(nèi)存中,count變成11。也就是說明明執(zhí)行了兩次自增,最后count只增大了1。因此在并發(fā)下,多次add可能只會有一次自增。
semaphore信號量用于控制并發(fā)量,即同時進入臨界區(qū)的操作同一個共享資源的線程數(shù)。
semaphore.acquire(); // 可以認為是獲得鎖
// other code
semaphore.release(); // 可以認為是釋放了鎖
semaphore.acquire()和semaphore.release()之間的代碼可以認為是臨界區(qū),這里指定了可以同時20個線程進入臨界區(qū),換種說法就是并發(fā)量是20。
如果將semaphore允許的并發(fā)量改成1,那么就相當于任意時刻只能有一個線程執(zhí)行add操作,5000個線程井然有序的按照先后順序執(zhí)行add,不存在同時執(zhí)行的情況,這種情況下最后的結(jié)果總是5000,某種意義上變成了串行。
解決上面的線程不安全問題,除了可以將semaphore的并發(fā)量控制為1;還可以使用重入鎖,synchronized關鍵字,原子變量AtomicInteger等。