Java高并發(fā)--CPU多級(jí)緩存與Java內(nèi)存模型

Java高并發(fā)--CPU多級(jí)緩存與Java內(nèi)存模型

主要是學(xué)習(xí)慕課網(wǎng)實(shí)戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記

CPU多級(jí)緩存

為什么需要CPU緩存:CPU的頻率太快,以至于主存跟不上,這樣在處理器時(shí)鐘周期內(nèi),CPU常常需要等待主存,浪費(fèi)了資源。所有緩存的出現(xiàn)是為了緩解CPU和主存之間速度不匹配的問(wèn)題——將運(yùn)算所需數(shù)據(jù)復(fù)制到緩存中,使得運(yùn)算能快速進(jìn)行;當(dāng)運(yùn)算結(jié)束后再將緩存同步回內(nèi)存中,這樣處理器無(wú)需等待緩慢的內(nèi)存讀寫。

image

緩存并非存儲(chǔ)了所有的數(shù)據(jù),那么它存在的意義是什么?

  • 時(shí)間局部性:如果某個(gè)數(shù)據(jù)被訪問(wèn),那么它在不久的將來(lái)有可能被再次訪問(wèn)
  • 空間局部性:如果某個(gè)數(shù)據(jù)被訪問(wèn),那么與它相鄰的數(shù)據(jù)很快可能被訪問(wèn)

Java內(nèi)存模型

下圖中是JVM中堆和棧的關(guān)系,在不同的線程中,可能有多個(gè)變量,它們指向的是堆上的同一個(gè)對(duì)象,這些變量都是該對(duì)象的“私有拷貝”。私有表示僅在當(dāng)前線程可訪問(wèn),拷貝是說(shuō)這是該對(duì)象的一個(gè)引用。

image

線程A和線程B之間如果要通信的話,必須經(jīng)歷以下兩個(gè)步驟:

  • 線程A將本地內(nèi)存(工作內(nèi)存)中的變量刷新到主內(nèi)存中
  • 主內(nèi)存將變量復(fù)制到本地內(nèi)存B中,使得線程B在讀取變量時(shí)更快
image

Java內(nèi)存模型中,所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(與高速緩存類比),線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存中的變量;不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需通過(guò)主內(nèi)存完成

如果將Java內(nèi)存模型和Java堆、棧比較,主內(nèi)存對(duì)應(yīng)Java堆中的對(duì)象實(shí)例部分,工作內(nèi)存對(duì)應(yīng)虛擬機(jī)棧中的部分。

主內(nèi)存和工作內(nèi)存之間需要交互,Java內(nèi)存模型中有8種原子操作:

  • lock:作用于主內(nèi)存變量,將其標(biāo)識(shí)為線程獨(dú)占。
  • unlock:作用于主內(nèi)存變量,將其從鎖定狀態(tài)釋放,釋放后才可被其他線程鎖定。
  • read:作用于主內(nèi)存的變量,將一個(gè)變量從主內(nèi)存中傳輸?shù)焦ぷ鲀?nèi)存中,以便隨后的load動(dòng)作使用。
  • load:作用于工作內(nèi)存中的變量,把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  • use:作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,當(dāng)虛擬機(jī)需要使用到變量的值的字節(jié)碼指令時(shí)會(huì)執(zhí)行這個(gè)操作。
  • assign:作用于工作內(nèi)存的變量,把一個(gè)從執(zhí)行引擎接受到的值賦給工作內(nèi)存中的變量。當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
  • store:作用于工作內(nèi)存中的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便之后write操作使用。
  • write:作用于主內(nèi)存中的變量,把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。

如果一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,必須先執(zhí)行read然后執(zhí)行l(wèi)oad操作(read和load之間允許插入其他操作,只要保證這個(gè)順序即可);如果要把變量從工作內(nèi)存同步回主內(nèi)存中,需要先執(zhí)行store操作然后執(zhí)行write操作(store和write之間允許插入其他操作,只要保證這個(gè)順序即可)。

image

最后來(lái)看一個(gè)線程不安全的例子

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 {
    // 請(qǐng)求總數(shù)
    public static int requestTotal = 5000;
    // 并發(fā)量,同時(shí)進(jìn)入臨界區(qū)的線程數(shù)量
    public static int concurrentTotal = 20;
    // 計(jì)數(shù)器
    public static int count = 0;

    @NotThreadSafe
    private static void add() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 信號(hào)量,控制并發(fā)數(shù)
        final Semaphore semaphore = new Semaphore(concurrentTotal);
        // 倒計(jì)數(shù)器,在這里用來(lái)阻塞主線程直到計(jì)數(shù)器為0
        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        // requestTotal次請(qǐng)求,每一次請(qǐng)求自增1,按理說(shuō)最后count的值是requestTotal
        // 但是在并發(fā)下,多個(gè)線程同時(shí)執(zhí)行了add()使得多次自增值只增加了1,導(dǎo)致最后的結(jié)果比requestTotal小
        for (int i = 0; i < requestTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    // 最多可同時(shí)concurrentTotal個(gè)線程同時(shí)執(zhí)行
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {
                    log.error("exception", e);
                }
                // 每自增一次,計(jì)數(shù)器減1
                countDownLatch.countDown();
            });
        }
        // 主線程在countdownLatch上等待,等計(jì)數(shù)器為0后才能執(zhí)行
        countDownLatch.await();
        log.info("count:{}", count);
        executorService.shutdown();
    }
}

總共發(fā)起5000次請(qǐng)求(5000個(gè)線程),每次請(qǐng)求對(duì)count變量做自增操作。顯然在并發(fā)下,主線程等5000個(gè)線程執(zhí)行完畢后count一般是小于5000的。原因如下:

以兩個(gè)線程為例,它們?cè)谕粫r(shí)刻從主內(nèi)存中讀取count的值并裝載到各自的工作內(nèi)存中,此時(shí)count的值是一樣的,假設(shè)都是10。此時(shí)線程A先自增,將自增后的值更新到工作內(nèi)存,最后刷回主內(nèi)存,count變成了11;而在線程B也進(jìn)行同樣的自增操作,注意之前線程B已經(jīng)讀取過(guò)count的值了,此時(shí)在B的工作內(nèi)存中的count還是等于10的,接著B也更新count,最后刷回主內(nèi)存中,count變成11。也就是說(shuō)明明執(zhí)行了兩次自增,最后count只增大了1。因此在并發(fā)下,多次add可能只會(huì)有一次自增。

semaphore信號(hào)量用于控制并發(fā)量,即同時(shí)進(jìn)入臨界區(qū)的操作同一個(gè)共享資源的線程數(shù)。

semaphore.acquire(); // 可以認(rèn)為是獲得鎖
// other code
semaphore.release(); // 可以認(rèn)為是釋放了鎖

semaphore.acquire()semaphore.release()之間的代碼可以認(rèn)為是臨界區(qū),這里指定了可以同時(shí)20個(gè)線程進(jìn)入臨界區(qū),換種說(shuō)法就是并發(fā)量是20。

如果將semaphore允許的并發(fā)量改成1,那么就相當(dāng)于任意時(shí)刻只能有一個(gè)線程執(zhí)行add操作,5000個(gè)線程井然有序的按照先后順序執(zhí)行add,不存在同時(shí)執(zhí)行的情況,這種情況下最后的結(jié)果總是5000,某種意義上變成了串行。

解決上面的線程不安全問(wèn)題,除了可以將semaphore的并發(fā)量控制為1;還可以使用重入鎖,synchronized關(guān)鍵字,原子變量AtomicInteger等。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容