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


概念

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

       為什么需要並發,多線程?

  1. 時代的召喚,為了更充分的利用多核CPU的計算能力,多個線程程序可通過提高處理器的資源利用率來提升程序性能。
  2. 方便業務拆分,異步處理業務,提高應用性能。

   多線程並發產生的問題?

  1. 大量的線程讓CPU頻繁上下文切換帶來的系統開銷。
  2. 臨界資源線程安全問題(共享,可變)。
  3. 容易造成死鎖。

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

可見性

 多線程訪問同一個變量時,如果有一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。這是因為為了保證多個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執行的語句被打印

執行結果

線程1已對flag做出改變
線程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關鍵字

public class ThreadDemo3 {
    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()方法檢測線程是否中斷。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM