導致並發程序出問題的根本原因是什么?


CPU、內存、IO 設備的讀寫速度差異巨大,表現為 CPU 的速度 > 內存的速度 > IO 設備的速度。

 

程序的性能瓶頸在於速度最慢的 IO 設備的讀寫,也就是說當涉及到 IO 設備的讀寫,再怎么提升 CPU 和內存的速度也是起不到提升性能的作用。

 

為了更好地利用 CPU 的高性能

  • 計算機體系結構,給 CPU 增加了緩存,均衡 CPU 和內存的速度差異
  • 操作系統,增加了進程與線程,分時復用 CPU,均衡 CPU 和 IO 設備的速度差異
  • 編譯器,增加了指令執行重排序,更好地利用緩存,提高程序的執行速度

 

基於以上優化,給並發編程帶來了三大問題。

 

1、 CPU 緩存,在多核 CPU 的情況下,帶來了可見性問題

可見性:一個線程對共享變量的修改,另一個線程能夠立刻看到修改后的值

看下面代碼,啟動兩個線程,一個線程當 stop 變量為 true 時,停止循環,一個線程啟動就設置 stop 變量為 true。

package constxiong.concurrency.a014;

/**
 * 測試可見性問題
 * @author ConstXiong
 */
public class TestVisibility {

    //是否停止 變量
    private static boolean stop = false;
    
    public static void main(String[] args) throws InterruptedException {
        //啟動線程 1,當 stop 為 true,結束循環
        new Thread(() -> {
            System.out.println("線程 1 正在運行...");
            while (!stop) ;
            System.out.println("線程 1 終止");
        }).start();
        
        //休眠 10 毫秒
        Thread.sleep(10);
        
        //啟動線程 2, 設置 stop = true
        new Thread(() -> {
            System.out.println("線程 2 正在運行...");
            stop = true;
            System.out.println("設置 stop 變量為 true.");
        }).start();
    }
    
}

 

打印結果:

 

這個就是因為 CPU 緩存導致的可見性導致的問題。線程 2 設置 stop 變量為 true,線程 1 在 CPU 1上執行,讀取的 CPU 1 緩存中的 stop 變量仍然為 false,線程 1 一直在循環執行。

示意如圖:

可以通過 volatile、synchronized、Lock接口、Atomic 類型保障可見性。

 

2、操作系統對當前執行線程的切換,帶來了原子性問題

原子性:一個或多個指令在 CPU 執行的過程中不被中斷的特性

看下面的一段代碼,線程 1 和線程 2 分別對變量 count 增加 10000,但是結果 count 的輸出卻不是 20000

package constxiong.concurrency.a014;

/**
 * 測試原子性問題
 * @author ConstXiong
 */
public class TestAtomic {
    
    //計數變量
    static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //線程 1 給 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 結束");
        });
        
        //線程 2 給 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 結束");
        });
        
        //啟動線程 1
        t1.start();
        //啟動線程 2
        t2.start();
        
        //等待線程 1 執行完成
        t1.join();
        //等待線程 2 執行完成
        t2.join();
        
        //打印 count 變量
        System.out.println(count);
    }
    
}

 

打印結果:

thread t2 count 加 10000 結束
thread t1 count 加 10000 結束
11377

 

 

這個就是因為線程切換導致的原子性問題。

Java 代碼中 的 count++ ,至少需要三條 CPU 指令:

  • 指令 1:把變量 count 從內存加載到 CPU 的寄存器
  • 指令 2:在寄存器中執行 count + 1 操作

  • 指令 3:+1 后的結果寫入 CPU 緩存 或 內存

即使是單核的 CPU,當線程 1 執行到指令 1 時發生線程切換,線程 2 從內存中讀取 count 變量,此時線程 1 和線程 2 中的 count 變量值是相等,都執行完指令 2 和指令 3,寫入的 count 的值是相同的。從結果上看,兩個線程都進行了 count++,但是 count 的值只增加了 1。

指令執行與線程切換

 

3、編譯器指令重排優化,帶來了有序性問題

有序性:程序按照代碼執行的先后順序

看下面這段代碼,復現指令重排帶來的有序性問題。

package constxiong.concurrency.a014;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 測試有序性問題
 * @author ConstXiong
 */
public class TestOrderliness {
    
    static int x;//靜態變量 x
    static int y;//靜態變量 y

    public static void main(String[] args) throws InterruptedException {
        Set<String> valueSet = new HashSet<String>();//記錄出現的結果的情況
        Map<String, Integer> valueMap = new HashMap<String, Integer>();//存儲結果的鍵值對
        
        //循環 1000 萬次,記錄可能出現的 v1 和 v2 的情況
        for (int i = 0; i < 10000000; i++) {
            //給 x y 賦值為 0
            x = 0; 
            y = 0; 
            valueMap.clear();//清除之前記錄的鍵值對
            Thread t1 = new Thread(() -> {
                int v1 = y;//將 y 賦值給 v1 ----> Step1
                x = 1;//設置 x 為 1  ----> Step2
                valueMap.put("v1", v1);//v1 值存入 valueMap 中  ----> Step3
            }) ;
            
            Thread t2 = new Thread(() -> {
                int v2 = x;//將 x 賦值給 v2  ----> Step4
                y = 1;//設置 y 為 1  ----> Step5
                valueMap.put("v2", v2);//v2 值存入 valueMap 中  ----> Step6
            });
            
            //啟動線程 t1 t2
            t1.start();
            t2.start();
            //等待線程 t1 t2 執行完成
            t1.join();
            t2.join();
            
            //利用 Set 記錄並打印 v1 和 v2 可能出現的不同結果
            valueSet.add("(v1=" + valueMap.get("v1") + ",v2=" + valueMap.get("v2") + ")");
            System.out.println(valueSet);
        }
    }
    
}

 

打印結果出現四種情況:

v1=0,v2=0 的執行順序是 Step1 和 Step 4 先執行

v1=1,v2=0 的執行順序是 Step5 先於 Step1 執行

v1=0,v2=1 的執行順序是 Step2 先於 Step4 執行

v1=1,v2=1 出現的概率極低,就是因為 CPU 指令重排序造成的。Step2 被優化到 Step1 前,Step5 被優化到 Step4 前,至少需要成立一個。

指令重排,可能會發生在兩個沒有相互依賴關系之間的指令。

 

 

 來一道刷了進BAT的面試題?


免責聲明!

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



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