Java多線程編程(2)--多線程編程的術語與概念

一.串行、併發和並行

  爲了更清楚地解釋這三個概念,我們來舉一個例子。假設我們有A、B、C三項工作要做,那麼我們有以下三種方式來完成這些工作:

  第一種方式,先開始做工作A,完成之後再開始做工作B,以此類推,知道完成工作C。在這種情況下實際上只需要投入一個人。
  第二種方式,先開始做工作A,做了一會之後再開始做工作B;工作B做了一會再開始做工作C,工作C做了一會又重新開始做工作A,以此類推,直到所有工作都完成。這樣看上去像是在同時進行三個工作一樣,但是這種方式可以只投入一個人。
  第三種方式需要投入三個人,每個人負責一項工作,這三個人在同一時刻齊頭並進地完成這些事情。這種方式比其他兩種方式都要快。
  在軟件開發領域,這三種方式分別被稱爲串行、併發和並行。串行就是一次一個任務、每個任務完成之後再進行下一個任務的行爲,這種方式耗費的時間往往是最長的。併發就是在一段時間內以交替的方式去完成多個任務,它可能會加速任務的執行,也可能和串行消耗相同的時間。例如,如果一個任務在等待某些資源或者執行某些耗時但不佔用CPU的操作(例如IO操作),這段時間讓CPU去處理其他任務,就不會白白浪費時間,從而縮短整個程序的執行時間;而如果每個任務都是CPU密集型任務,那麼使用併發並不會比串行快多少。並行就是以齊頭並進的方式同時處理多個任務,它一定會縮短程序的執行時間。
  從硬件的角度來說,在一個處理器一次只能夠運行一個線程的情況下,由於處理器可以使用時間片分配的技術來實現在同一段時間內運行多個線程,因此一個處理器就可以實現併發。而並行則需要靠多個處理器在同一時刻各自運行一個線程來實現。
  多線程編程的實質就是將任務的處理方式由串行改爲併發或並行,以提高程序對CPU資源的利用率,最大限度地使用系統資源。至於多線程實現的到底是併發還是並行,則要視具體情況而定。在CPU比較繁忙,資源不足的時候,操作系統只爲一個含有多線程的進程分配僅有的CPU資源,這些線程就會爲自己儘量多搶時間片,這就是通過多線程實現併發,線程之間會競爭CPU資源爭取執行機會。而在CPU資源比較充足的時候,一個進程內的多線程,可以被分配到不同的CPU資源,這就是通過多線程實現並行。這個分配過程是操作系統的行爲,不可人爲控制。所有,如果有人問我我所寫的多線程是併發還是並行的?我會說,都有可能。

二.競態

  多線程編程中經常遇到的一個問題就是對於同樣的輸入,程序的輸出有時候是正確的而有時候卻是錯誤的。這種一個計算結果的正確性與時間順序有關的現象就被稱爲競態(Race Condition)。
  下面的例子中,在主線程中創建了4個線程,每個線程都執行相同的任務,即調用20次計數器並輸出計數器的值。

// Code 2-1

public class RaceConditionDemo {
    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 20; i++) {
                System.out.println(Thread.currentThread().getName() + " : " + Counter.getInstance().count());
            }
        };
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);
        Thread thread4 = new Thread(task);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class Counter {
    private static final Counter INSTANCE = new Counter();
    private int countValue = 0;

    private Counter() {}

    int count() {
        if (countValue >= 100) {
            countValue = 1;
        } else {
            countValue++;
        }
        return countValue;
    }

    static Counter getInstance() {
        return INSTANCE;
    }
}

  下圖是某次運行後得到的結果(偶現,非必現):


  理論上來說,每次調用Counter類的count方法,得到的計數值都是不一樣的。但是上面的結果中,卻出現了兩個一模一樣的計數值。上述程序的輸出有時候是正確的而有時候是錯誤的,可見該程序在多線程環境下運行出現了競態。
  下面我們來分析一下爲什麼會出現競態。因爲每個線程裏只用到了計數器的count方法,因此,count()是導致競態的直接因素。進一步來說,導致競態的常見因素是多個線程在沒有采取任何控制措施的情況下併發地更新、讀取同一個共享變量。count()所訪問的實例變量countValue就是這樣一個例子:多個線程通過調用count()併發地訪問countValue,顯然這些線程沒有采取任何控制措施。
  count()中的語句“countValue++”看起來像是一個操作,但它實際上相當於如下僞代碼所表示的3個指令:
load(countValue, reg);  //指令1:將變量countValue的值從內存讀到寄存器reg
increment(reg);         //指令2:將寄存器reg的值加1
store(countValue, reg); //指令3:將寄存器reg的內容寫入countValue對應的內存空間

  如果每個線程都等其他線程完成指令1、2、3之後再調用這個方法,那麼就不會產生問題。但實際上,這些指令是有可能同時在不同線程裏執行的。比如說,兩個線程可能在同一時間讀取到countValue的同一個值,一個線程對countValue所做的更新也可能“覆蓋”其他線程對該變量所做的更新,這些都有可能導致各個線程拿到相同的計數值。
  根據上述分析我們可以更進一步來定義競態:競態(Race Condition)是指計算的正確性依賴於相對時間順序或者線程的交錯。根據這個定義可知,競態不一定就導致計算結果的不正確,它只是不排除計算結果時而正確時而錯誤的可能。

三.線程安全性

  如果我們以單線程的方式去調用count()方法,那麼,我們可以發現該程序的輸出總是正確的。一般而言,如果一個類在單線程環境下能夠運作正常,並且在多線程環境下,在其使用方不必爲其做任何改變的情況下也能運作正常,那麼我們就稱其是線程安全的。反之,如果一個類在單線程環境下運作正常而在多線程環境下則無法正常運作,那麼這個類就是非線程安全的。因此, 一個類如果能夠導致競態,那麼它就是非線程安全的;而一個類如果是線程安全的,那麼它就不會導致競態。下面是《Java併發編程實戰》一書中給出的對於線程安全的定義:

當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

  使用一個類的時候我們必須先弄清楚這個類是否是線程安全的。因爲這關係到我們如何正確使用這些類。Java標準庫中的一些類如ArrayList、HashMap和SimpleDateFormat,都是非線程安全的,在多線程環境下直接使用它們可能導致一些非預期的結果,甚至是一些災難性的結果。一般來說,Java標準庫中的類在其API文檔中會說明其是否是線程安全的(沒有說明其是否是線程安全的,則可能是也可能不是線程安全的)。
  從線程安全的定義上我們不難看出,如果一個線程安全的類在多線程環境下能夠正常運作,那麼它在單線程環境下也能正常運作。既然如此,那爲什麼不乾脆把所有的類都做成線程安全的呢?是否將一個類做成線程安全的,從某種程度上來說是一個設計上的權衡的結果或決定:一方面,一個類是否需要是線程安全的與這個類預期被使用的方式有關,比如,我們希望一個類總是隻能被一個線程獨自使用,那麼就沒有必要將這個類做成線程安全的。其次,把一個類做成線程安全的往往是有額外代價的。
  一個類如果不是線程安全的,我們就說它在多線程環境下直接使用存在線程安全問題。線程安全問題概括來說表現爲3個方面:原子性、可見性和有序性。

四.原子性

  原子的字面意思是不可分割的。對於涉及共享變量訪問的操作,若該操作從其執行線程以外的任意線程來看是不可分割的,那麼該操作就是原子操作,相應地我們稱該操作具有原子性。所謂“不可分割”,其中一個含義是指訪問某個共享變量的操作從其執行線程以外的任何線程來看,該操作要麼已經執行結束要麼尚未發生,即其他線程不會“看到”該操作執行了部分的中間效果。
  在生活中我們可以找到的一個原子操作的例子就是人們從ATM機提取現金:儘管從ATM軟件的角度來說,一筆取款交易涉及扣減戶賬戶餘額、吐出鈔票、新增交易記錄等一系列操作,但是從用戶的角度來看ATM取款就是一個操作。該操作要麼成功了,即我們拿到現金(賬戶餘額會被扣減)這個操作發生過了;要麼失敗了,即我們沒有拿到現金,這個操作就像從來沒有發生過一樣(賬戶餘額也不會被扣減)。除非ATM軟件有缺陷,否則我們不會遇到吐鈔口吐出部分現金而我們的賬戶餘額卻被扣除這樣的部分結果。
  總的來說,Java中有兩種方式來實現原子性。一種是使用鎖(Lock)。鎖具有排他性,即它能夠保障一個共享變量在任意時刻只能夠被一個線程訪問。這就排除了多個線程在同一時刻訪問同一個共享變量而導致干擾與衝突的可能,即消除了競態。另一種是利用處理器提供的專門CAS(Compare-and-Swap)指令。CAS指令實現原子性的方式與鎖實現原子性的方式實質上是相同的,差別在於鎖通常是在軟件這一層次實現的,而CAS是直接在硬件(處理器和內存)這一層次實現的,它可以被看作“硬件鎖”。
  在Java語言中,long型和double型以外的任何類型的變量的寫操作都是原子操作,即對基礎類型(long、double除外)的變量和引用型變量的寫操作都是原子的。這點是由Java語言規範(Java Language Specification)規定,由Java虛擬機具體實現。一個long/double型變量的讀/寫操作在32位Java虛擬機下可能會被分解爲兩個子步驟(比如先寫低32位,再寫高32位)來實現,這就導致一個線程對long/double型變量進行的寫操作的中間結果可以被其他線程所觀察到,即此時針對long/double類型的變量的訪問操作不是原子操作。儘管如此,Java語言規範特別地規定對於volatile關鍵字修飾的long/double類型變量的寫操作具有原子性。因此,我們只需要用volatile關鍵字(下一篇文章會進一步介紹該關鍵字)修飾可能被多個線程訪問的long/double類型的變量,就可以保障對該變量的寫操作的原子性。

五.可見性

  在多線程環境下,一個線程對某個共享變量進行更新之後,後續訪問該變量的線程可能無法立刻讀取到這個更新的結果,甚至永遠也無法讀取到這個更新的結果。這就是線程安全問題的另外一個表現形式:可見性。
  下面看一個可見性的例子:

// Code 2-2
public class VisibilityDemo {
    public static void main(String[] args) {
        UselessThread uselessThread = new UselessThread();
        uselessThread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        uselessThread.cancel();
    }
}

class UselessThread extends Thread {
    private boolean cancelled = false;

    @Override
    public void run() {
        System.out.println("Task has been started.");
        while (!cancelled) {}
        System.out.println("Task has been cancelled.");
    }

    public void cancel() {
        cancelled = true;
    }
}

  上面的程序中,主線程在uselessThread線程啓動,此時該線程會輸出“Task has been started.”,一秒後,主線程會調用uselessThread的cancel方法,也就是將uselessThread的calcelled變量置爲true。理論上來說,此時uselessThread的run方法中的while循環會結束,並在輸出“Task has been cancelled.”後結束線程。然而,運行該程序,我們會看到如下輸出:

Task has been started.

  我們發現,程序並沒有輸出“Task has been cancelled.”,程序仍然一直在運行(如果沒有出現這種現象可以在java命令後加上-server參數)。這種現象只有一種解釋,那就是run方法中的while陷入了死循環。也就是說,子線程uselessThread讀到的cancel變量值始終是false,儘管主線程已經將這個變量的值更新爲true。可見,這裏產生了可見性問題,即main線程對共享變量cancelled的更新對子線程uselessThread不可見。
  上述例子中的可見性問題是因爲代碼沒有給JIT編譯器足夠的提示而使得其認爲狀態變量cancelled只有一個線程對其進行訪問,從而導致JIT編譯器爲了避免重複讀取狀態變量cancelled以提高代碼的運行效率,而將run方法中的while循環優化成與如下代碼等效的機器碼:

if (!cancelled) {
    while (true) {}
}

  不幸的是,此時這種優化導致了死循環,也就是我們所看到的程序一直運行而沒有退出。
  另一方面,可見性問題與計算機的存儲系統有關。程序中的變量可能會被分配到寄存器而不是主內存中進行存儲。每個處理器都有其寄存器,而一個處理器無法讀取另外一個處理器上的寄存器中的內容。因此,如果兩個線程分別運行在不同的處理器上,而這兩個線程所共享的變量卻被分配到寄存器上進行存儲,那麼可見性問題就會產生。另外,即便某個共享變量是被分配到主內存中進行存儲的,也不能保證該變量的可見性。這是因爲處理器對主內存的訪問並不是直接訪問,而是通過其高速緩存子系統進行的。如果高速緩存子系統中的內容沒有及時更新,那麼處理器讀取到的值仍然有可能是一箇舊值,這同樣會導致可見性問題。

處理器並不是直接與主內存打交道而執行內存的讀、寫操作,而是通過定義寄存器、高速緩存、寫緩衝器和無效化隊列等部件執行內存的讀、寫操作的。從這個角度來看,這些部件相當於主內存的副本,因此本書爲了敘述方便將這些部件統稱爲處理器對主內存的緩存,簡稱處理器緩存。

  雖然一個處理器的高速緩存中的內容不能被另外一個處理器直接讀取,但是一個處理器可以通過緩存一致性協議(Cache Coherence Protocol)來讀取其他處理器的高速緩存中的數據,並將讀到的數據更新到該處理器的高速緩存中。這種一個處理器從其自身處理器緩存以外的其他存儲部件中讀取數據並將其更新到該處理器的高速緩存的過程,我們稱之爲緩存同步,這些存儲部件包括處理器的高速緩存、主內存。緩存同步使得一個處理器上運行的線程可以讀取到另外一個處理器上運行的線程對共享變量所做的更新,即保障了可見性。因此,爲了保障可見性,我們必須使一個處理器對共享變量所做的更新最終被寫入該處理器的高速緩存或者主內存中(而不是始終停留在其寫緩衝器中),這個過程被稱爲沖刷處理器緩存。並且,一個處理器在讀取共享變量的時候,如果其他處理器在此之前已經更新了該變量.那麼該處理器必須從其他處理器的高速緩存或者主內存中對相應的變量進行緩存同步。這個過程被稱爲刷新處理器緩存。因此,可見性的保障是通過使更新共享變量的處理器執行沖刷處理器緩存的動作,並使讀取共享變量的處理器執行刷新處理器緩存的動作來實現的。
  那麼,在Java平臺中我們如何保證可見性呢?實際上,使用volatile關鍵字就可以保證可見性。對於Code 2-2所示的代碼,我們只需要在實例變量cancelled的聲明中添加一個volatile關鍵字即可:

private volatile boolean cancelled = false;

  這裏,volatile關鍵字所起到的一個作用就是,提示JIT編譯器被修飾的變量可能被多個線程共享,以阻止JIT編譯器做出可能導致程序運行不正常的優化。另外一個作用就是讀取一個volatile關鍵字修飾的變量會使相應的處理器執行刷新處理器緩存的動作,寫一個volatile關鍵字修飾的變量會使相應的處理器執行沖刷處理器緩存的動作,從而保障了可見性。
  對於同一個共享變量而言,一個線程更新了該變量的值之後,其他線程能夠讀取到這個更新後的值,那麼這個值就被稱爲該變量的相對新值。如果讀取這個共享變量的線程在讀取並使用該變量的時候其他線程無法更新該變量的值,那麼該線程讀取到的相對新值就被稱爲該變量的最新值。可見性的保障僅僅意味着一個線程能夠讀取到共享變量的相對新值,而不能保障該線程能夠讀取到相應變量的最新值。
  針對原子性,Java語言規範中還定義了兩條與線程的啓動和停止有關的規範:

  1. 父線程在啓動子線程之前對共享變量的更新對於子線程來說是可見的;
  2. 一個線程終止後該線程對共享變量的更新對於調用該線程的join方法的線程而言是可見的。

六.有序性

  有序性指在某些情況下一個處理器上運行的一個線程所執行的內存訪問操作在另外一個處理器上運行的其他線程看來是亂序的。所謂亂序,是指內存訪問操作的順序看起來像是發生了變化。在進一步介紹有序性這個概念之前,我們需要先介紹重排序的概念。

重排序的概念

  順序結構是編程中的一種基本結構,它表示我們希望某個操作必須先於另外一個操作得以執行。另外,兩個操作即便是可以用任意一種順序執行,但是反映在代碼上這兩個操作也總是有先後關係。但是在多核處理器的環境下,這種操作執行順序可能是沒有保障的:編譯器可能改變兩個操作的先後順序;處理器可能不是完全依照程序的目標代碼所指定的順序執行指令;另外,一個處理器上執行的多個操作,從其他處理器的角度來看其順序可能與目標代碼所指定的順序不一致。這種現象就叫作重排序。
  重排序是對內存訪問有關的操作(讀和寫)所做的一種優化,它可以在不影響單線程程序正確性的情況下提升程序的性能。但是,它可能對多線程程序的正確性產生影響,即它可能導致線程安全問題。與可見性問題類似,重排序也不是必然出現的。
  重排序的潛在來源有許多,包括編譯器(在Java平臺中這基本上指JIT編譯器)、處理器和存儲子系統(包括寫緩衝器、高速緩存)。爲了便於下面的講解,我們先定義幾個與內存操作順序有關的術語:

  • 源代碼順序:源代碼中所指定的內存訪問操作順序。
  • 程序順序:在給定處理器上運行的目標代碼所指定的內存訪問操作順序。
  • 執行順序:內存訪問操作在給定處理器上的實際執行順序。
  • 感知順序:給定處理器所感知到的該處理器及其他處理器的內存訪間操作發生的順序。

  在此基礎上,我們將重排序劃分爲指令重排序和存儲子系統重排序兩種,如下表所示:

指令重排序

  在源代碼順序與程序順序不一致,或者程序順序與執行順序不一致的情況下,我們就說發生了指令重排序。指令重排序是一種動作,它確確實實地對指令的順序做了調整,其重排序的對象是指令。

Java平臺包含兩種編譯器:靜態編譯器(javac)和動態編譯器(JIT編譯器)。前者的作用是將Java源代碼(.java文本文件)編譯爲字節碼(.class二進制文件),它是在代碼編譯階段介入的。後者的作用是將字節碼動態編譯爲Java虛擬機宿主機的本地代碼(機器碼),它是在Java程序運行過程中介入的。

  來看下面的程序:

// Code 2-3
public class PossibleReordering {
    private static int a;
    private static int b;
    private static int x;
    private static int y;

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            a = 1;
            x = b;
        });
        Thread threadB = new Thread(() -> {
            b = 1;
            y = a;
        });
        threadA.start();
        threadB.start();
        try {
            threadA.join();
            threadB.join();
            System.out.printf("(%d,%d)", x, y);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  由於線程A可以在線程B開始之前就執行完成,線程B也有可能在線程A開始之前就完成,二者也有可能交替執行,因此,程序最終會輸出什麼是不確定的。但是,按照我們的認知,每個線程內的操作應該是按照代碼的順序來執行的。也就是說,a=1應該是在x=b之前執行的,b=1應該是在y=a之前執行的。我們可以對這幾個操作進行簡單的排列來分析最終的輸出結果:

操作1 操作2 操作3 操作4 結果
a=1 x=b b=1 y=a (0,1)
a=1 b=1 x=b y=a (1,1)
a=1 b=1 y=a x=b (1,1)
b=1 y=a a=1 x=b (1,0)
b=1 a=1 y=a x=b (1,1)
b=1 a=1 x=b y=a (1,1)

  可以看到,在沒有正確同步的情況下,程序輸出(1,0)、(0,1)或(1,1)都是有可能的。但奇怪的是,程序還可以輸出(0,0),這個結果不屬於上面分析的任何一種情況。由於上面的每個線程中的各個操作之間不存在數據流依賴性,可能會發生指令重排序,因此這些操作有可能會亂序執行。下圖給出了一種可能由重排序導致的交替執行方式,在這種情況中會輸出(0,0)。

  由此可以看出,重排序可能導致線程安全問題。當然,這並不表示重排序本身是錯誤的,而是我們的程序本身有問題:我們的程序沒有使用或者沒有正確地使用線程同步機制。不過,重排序也不是必然出現的,上面的(0,0)是在程序大概運行了50000次左右纔出現了一次。儘管如此,我們並不能忽視重排序帶來的潛在的風險。
  在其他編譯型語言(如C++)中,編譯器是可能導致指令重排序的。在Java平臺中,靜態編譯器(javac)基本上不會執行指令重排序,而JIT編譯器則可能執行指令重排序。
  處理器也可能執行指令重排序,這使得執行順序與程序順序不一致。處理器對指令進行重排序也被稱爲處理器的亂序執行在條件允許的情況下,直接運行當前有能力立即執行的後續指令,避開獲取下一條指令所需數據時造成的等待。通過亂序執行的技術,處理器可以大大提高執行效率。處理器的指令重排序並不會對單線程程序的正確性產生影響,但是它可能導致多線程程序出現非預期的結果。

存儲子系統重排序

  主內存(RAM)相對於處理器是一個慢速設備。爲了避免其拖後腿,處理器並不是直接訪問主內存,而是通過高速緩存訪問主內存的。在此基礎上,現代處理器還引入了寫緩衝器以提高寫高速緩存操作的效率。有的處理器(如Intel的x86處理器)對所有的寫主內存的操作都是通過寫緩衝器進行的。這裏,我們將寫緩衝器和高速緩存統稱爲存儲子系統,它其實是處理器的子系統。
  即使在處理器嚴格依照程序順序執行兩個內存訪問操作的情況下9,在存儲子系統的作用下其他處理器對這兩個操作的感知順序仍然可能與程序順序不一致,即這兩個操作的執行順序看起來像是發生了變化。這種現象就是存儲子系統重排序,也被稱爲內存重排序。
  指令重排序的重排序對象是指令,它實實在在地對指令的順序進行涸整,而存儲子系統重排序是一種現象而不是一種動作,它並沒有真正對指令執行順序進行調整,而只是造成了一種指令的執行順序像是被調整過一樣的現象,其重排序的對象是內存操作的結果。
  從處理器的角度來說,讀內存操作的實質是從指定的RAM地址加載數據(通過高速緩存加載)到寄存器,因此讀內存操作通常被稱爲Load, 寫內存操作的實質是將數據存儲到指定地址表示的RAM存儲單元中,因此寫內存操作通常被稱爲Store。所以,內存重排序實際上只有以下4種可能:

  內存重排序可能導致線程安全問題。假設處理器Processor 0和處理器Processor 1上的兩個線程按照下圖所示的交錯順序各自執行其代碼,其中data、ready是這兩個線程的共享變量,其初始值分別爲0和false。Processor 0上的線程所執行的處理邏輯是更新數據data並在此之後將相應的更新標誌ready的值設爲true。Processor 1上的線程所執行的處理邏輯是當數據更新標誌ready的值不爲true時無限等待直到ready的值爲true纔將data
的值打印出來。

  假設Processor 0依照程序順序先後執行S1和S2,那麼S1和S2的操作結果會被先後寫入寫緩衝器中。但是由於某些處理器的寫緩衝器爲了提高將其中的內容寫入高速緩存的效率而不保證寫操作結果先入先出的順序,即較晚到達寫緩衝器的寫操作結果可能更早地被寫入高速緩存,因此S2的操作結果可能先於S1的操作結果被寫入高速緩存,即S1被重排序到S2之後(內存重排序)。這就導致了Processor 1上的線程讀取到ready的值爲true時,由於S1的操作結果仍然停留在Processor 0的寫緩衝器之中,而一個處理器並不能讀取到另外一個處理器的寫緩衝器中的內容,因此Processor 1上的線程讀取到的data值仍然是0。可見,此時內存重排序導致了Processor 1上的線程的處理邏輯無法達到其預期目標,即導致了線程安全問題。

保證內存訪問的順序性

  如何避免重排序導致的線程安全問題呢?需要了解的是,我們無法從物理上完全禁用重排序而使得處理器完全依照源代碼順序執行指令,因爲那樣性能太低。但是,我們可以從邏輯上有選擇性地禁止重排序,即重排序要麼不發生,要麼即使發生了也不會影響多線程程序的正確性。
  從底層的角度來說,禁止重排序是通過調用處理器提供相應的指令(內存屏障)來實現的。當然,Java作爲一個跨平臺的語言,它會替我們與這類指令打交道,而我們只需要使用語言本身提供的機制即可。前面我們提到的volatile關鍵字、synchronized關鍵字都能夠實現有序性。有關volatile關鍵字、synchronized關鍵字以及重排序,我們會在後續的文章中進行更深入的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章