天天看點

java多線程3:原子性,可見性,有序性

java多線程3:原子性,可見性,有序性

概念

在了解線程安全問題之前,必須先知道為什麼需要并發,并發給我們帶來什麼問題。

為什麼需要并發,多線程?

時代的召喚,為了更充分的利用多核CPU的計算能力,多個線程程式可通過提高處理器的資源使用率來提升程式性能。

友善業務拆分,異步處理業務,提高應用性能。

多線程并發産生的問題?

大量的線程讓CPU頻繁上下文切換帶來的系統開銷。

臨界資源線程安全問題(共享,可變)。

容易造成死鎖。

注意:當多個線程執行一個方法時,該方法内部的局部變量并不是臨界資源,因為這些局部變量是在每個線程的私有棧中,是以不具有共享性質,不會導緻線程安全問題。

可見性

多線程通路同一個變量時,如果有一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。這是因為為了保證多個CPU之間的高速緩存是一緻的,作業系統會有一個緩存一緻性協定,volatile就是通過OS的緩存一緻性協定政策來保證了共享變量在多個線程之間的可見性。

public class ThreadDemo2 {

private static boolean flag = false;

public void thread_1(){
    flag = true;
    System.out.println("線程1已對flag做出改變");
}

public void thread_2(){
    while (!flag){
    }
    System.out.println("線程2->flag已被修改,成功打斷循環");
}

public static void main(String[] args) {
    ThreadDemo2 threadDemo2 = new ThreadDemo2();
    Thread thread2 = new Thread(()->{
        threadDemo2.thread_2();
    });
    Thread thread1= new Thread(()->{
        threadDemo2.thread_1();
    });
    thread2.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread1.start();
}           

}

執行結果

線程1已對flag做出改變

代碼無論執行多少次,線程2的輸出語句都不會被列印。為flag添加volatile修飾後執行,線程2執行的語句被列印

線程2->flag已被修改,成功打斷循環

局限:volatile隻是保證共享變量的可見性,無法保證其原子性。多個線程并發時,執行共享變量i的i++操作<==> i = i + 1,這是分兩步執行,并不是一個原子性操作。根據緩存一緻性協定,多個線程讀取i并對i進行改變時,其中一個線程搶先獨占i進行修改,會通知其他CPU我已經對i進行修改,把你們高速緩存的值設為無效并重新讀取,在并發情況下是可能出現資料丢失的情況的。

public class ThreadDemo3 {

private volatile static int count = 0;
public static void main(String[] args) {
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(()->{
            for (int j = 0; j < 1000; ++j){
                count++;
            }
        });
        thread.start();
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("count執行的結果為->" + count);
}           

count執行的結果為->9561

注意:這個結果是不固定的,有時10000,有時少于10000。

原子性

就像戀人一樣同生共死,表現在多線程代碼中程式一旦開始執行,就不會被其他線程幹擾要嘛一起成功,要嘛一起失敗,一個操作不可被中斷。在上文的例子中,為什麼執行結果不一定等于10000,就是因為在count++是多個操作,1.讀取count值,2.對count進行加1操作,3.計算的結果再指派給count。這幾個操作無法構成原子操作的,在一個線程讀取完count值時,另一個線程也讀取他并給它指派,根據緩存一緻性協定通知其他線程把本次讀取的值置為無效,是以本次循環操作是無效的,我們看到的值不一定等于10000,如何進行更正---->synchronized關鍵字

private volatile static int count = 0;
private static Object object = new Object();
public static void main(String[] args) {
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(()->{
            for (int j = 0; j < 1000; ++j){
                synchronized (object){
                    count++;
                }
            }
        });
        thread.start();
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("count執行的結果為->" + count);
}           

count執行的結果為->10000

加鎖後,線程在争奪執行權就必須擷取到鎖,目前線程就不會被其他線程所幹擾,保證了count++的原子性,至于synchronized為什麼能保證原子性,篇幅有限,下一篇在介紹。

有序性

jmm記憶體模型允許編譯器和CPU在單線程執行結果不變的情況下,會對代碼進行指令重排(遵守規則的前提下)。但在多線程的情況下卻會影響到并發執行的正确性。

public class ThreadDemo4 {

private static int x = 0,y = 0;
private static int a = 0,b = 0;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
    for (;;){
        i++;
        x = 0;y = 0;
        a = 0;b = 0;
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                waitTime(10000);
                a = 1;
                x = b;
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("第" + i + "次執行結果(" + x + "," + y + ")");
        if (x == 0 && y == 0){
            System.out.println("在第" + i + "次發生指令重排,(" + x + "," + y + ")");
            break;
        }
    }
}
public static void waitTime(int time){
    long start = System.nanoTime();
    long end;
    do {
        end = System.nanoTime();
    }while (start + time >= end);
}
           

第1次執行結果(0,1)

第2次執行結果(1,0)

....

第35012次執行結果(0,1)

第35013次執行結果(0,0)

在第35013次發生指令重排,(0,0)

如何解決上訴問題哪?volatile的另一個作用就是禁止指令重排優化,它的底層是記憶體屏障,其實就是一個CPU指令,一個辨別,告訴CPU和編譯器,禁止在這個辨別前後的指令執行重排序優化。記憶體屏障的作用有兩個,一個就是上文所講的保證變量的記憶體可見性,第二個保證特定操作的執行順序。

補充

指令重排序:Java語言規範規定JVM線程内部維持順序化語義,程式的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以和代碼順序不一緻。JVM根據處理器特性,适當的堆機器指令進行重排序,使機器指令更符号CPU的執行特性,最大限度發揮機器性能。

as-if-serial語義:不管怎麼重排序,單線程程式的執行結果不能被改變,編譯器和處理器都必須遵守這個原則。

happens-before原則:輔助保證程式執行的原子性,可見性和有序性的問題,判斷資料是否存在競争,線程是否安全的依據(JDK5)

1. 程式順序原則,即在一個線程内必須保證語義串行性,也就是說按照代碼順序執行。

2. 鎖規則 解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說, 如果對于一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)。

3. volatile規則 volatile變量的寫,先發生于讀,這保證了volatile變量的可見性,簡單 的了解就是,volatile變量在每次被線程通路時,都強迫從主記憶體中讀該變量的值,而當 該變量發生變化時,又會強迫将最新的值重新整理到主記憶體,任何時刻,不同的線程總是能 夠看到該變量的最新值。

4. 線程啟動規則 線程的start()方法先于它的每一個動作,即如果線程A在執行線程B的 start方法之前修改了共享變量的值,那麼當線程B執行start方法時,線程A對共享變量 的修改對線程B可見

5. 傳遞性 A先于B ,B先于C 那麼A必然先于C

6. 線程終止規則 線程的所有操作先于線程的終結,Thread.join()方法的作用是等待目前 執行的線程終止。假設線上程B終止之前,修改了共享變量,線程A從線程B的join方法 成功傳回後,線程B對共享變量的修改将對線程A可見。

7. 線程中斷規則 對線程 interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中 斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。

原文位址

https://www.cnblogs.com/dslx/p/12690366.html