天天看點

多線程與高并發深入底層橫向對比為什麼需要程序、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

為什麼需要程序、線程?

首先,我們來回憶一下馮諾依曼計算機體系。目前計算機主要是基于馮諾依曼體系結構設計的,下面就簡單分析一下馮諾依曼體系結構的計算機是如何工作的,首先下面的圖就是馮諾依曼體系結構圖。

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

馮諾依曼計算機體系結構,目前流程計算機組成如下:

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

引發的問題:如果單純依靠上述體系來做計算機處理,性能不高。主要瓶頸在IO上,比如磁盤IO、網絡IO等的讀取、寫入;而CPU性能非常高,往往是大部分時間是閑置的,就是沒有充分利用了CPU的性能。如下圖所示:

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

為了解決此問題,科學家、計算家們通過兩種方式優化了計算性能不高問題:1、增加記憶體、緩存;2、優化軟體結構

導入了記憶體、緩存(1級、2級、3級)、寄存器;需要資料各層進行複制

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

引入了程序、線程,讓線程搶占式的利用CPU資源;需要接管計算機硬體資源,任務排程、程序管理【線程】;在Java 層面即是synchronized、volatile、CAS、Lock、并發工具類、CountDownLatch、CyclicBarrier、Future、線程池等。

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

程式運作的底層原理

  • 程式是什麼?QQ.exe,PowerPoint.exe
  • 程序是什麼?程式啟動,進入記憶體,資源配置設定的基本單元
  • 線程是什麼?程式執行的基本機關
  • 程式如何開始運作? CPU 讀指令 - PC(存儲指令位址),讀資料到寄存器 Register,計算,回寫到記憶體;然後指向下一條指令
  • 線程如何進行排程?Linux 線程排程器(OS)作業系統
  • 線程切換的概念是什麼?Context Switch CPU 儲存現場執行新線程,恢複現場,繼續執行原線程這樣的一個過程

緩存、寄存器

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

從CPU的計算單元(ALU)到:

Registers  < 1 ns
L1 cache 約 1ns
L2 cache 約 3ns
L3 cache 約 15ns
main memory 約 80ns

根據空間局部性原理,按塊讀取,程式局部性原理,可以提高效率;充分發揮總線、CPU針腳等一次性讀取更多資料的能力。

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

MESI Cache 緩存一緻性協定

緩存一緻協定是Modified、Exclusive、Shared、Invalid 四個單詞的縮寫,詳細知識參考:https://www.cnblogs.com/z00377750/p/9180644.html

Intel 的緩存一緻性協定叫MESI,其他處理器的緩存一緻性協定不叫MESI,叫:MSI、MOSI、Synapse Firefly Dragon

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

緩存行Cache Line:

  • 緩存行越大,局部性空間效率越高,但讀取時間慢
  • 緩存行越小,局部性空間效率越低,但讀取時間快
  • 取一個折中值,目前多用:64位元組【工業實踐得出的結論】

根據緩存行一緻性協定,如果x的值被修改,在多線程環境下需要需要被通知到另外的其他線程。這個通知過程是需要花費時間的。

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

代碼示例1:資料在同一個緩存行,運作32728

/*
    兩個線程操作在同一個緩存行的資料進行疊加計算,檢視時間耗時。緩存行大小64位元組,一個long類型8位元組
    根據緩存一緻性協定,當同一緩存行資料更改了,必須通知其他線程,是以時間都花在了通知上面去了
 */
public class T01_CacheLinePadding_Deprecated {
    public static long COUNT = 1_0000_10000L;

    private static class T {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
            latch.countDown();
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
            latch.countDown();
        }, "t1");

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start)/100_1000);
    }
}
           

代碼示例2:資料不在同一個緩存行,運作16402,較示例1性能提升近1倍

/*
    兩個線程操作在不在同一個緩存行的資料進行疊加計算,檢視時間耗時。緩存行大小64位元組,一個long類型8位元組
    整體耗時較demo01 短,即快很多。
    因為資料作了填充,保證兩個資料不在同一個緩存行中,是以不會觸發緩存一緻性問題,是以也就沒有需要互相通知的問題。
    x,y肯定不在同一緩存行
    JDK 1.7
 */
public class T02_CacheLinePadding_Deprecated {
    public static long COUNT = 1_0000_0000;

    // 定義p1~p7,確定x在同個緩存行
    private static class T {
        private long p1,p2,p3,p4,p5,p6,p7;
        public volatile long x = 0L;
        private long p9,p10,p11,p12,p13,p14,p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
            latch.countDown();
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
            latch.countDown();
        }, "t2");

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_000);
    }
}
           

在項目中的應用有:disruptor、jdk中的

線程安全問題

因為CPU與外圍IO存在較大的性能差異,需要提高計算機的性能

  • 硬體
    • 加了記憶體、多級緩存,多核CPU緩存之間是不共享資料,引發了可見性問題
    • 因為CPU 對線程任務的切換,引發了原子性問題
    • CPU 要更加高效地去執行線程中的任務,引發了有序性的問題
  • 軟體
    • 增加了程序管理
      • 增加了線程切換
線程安全問題 描述 解決
1.原子性 表示不可分割 ---> 是因為多任務線程的切換 ---> 提高CPU使用率 ---> 提高并發性能 synchronized,不僅僅能夠解決原子性不可分割,還保證可見性
2.可見性 其實本質上是CPU緩存  --->  多核CPU緩存之間是不共享 ---> 可見性的問題 volatile 保證多線程可見性,禁止CPU去緩存
3.有序性 解釋了CPU為何會把我們寫的Java代碼做一個重排序的操作 volatile 禁止指令重排序

超線程:一個ALU對應多個PC Registers 所謂的四核八線程。注意:線程不是越多越好,線程切換需要時間。

鎖的概念

1、為什麼需要鎖?

CPU是亂序執行,CPU在進行讀等待的同時執行指令,是CPU亂序的根源;不是亂,而是提高效率。是以多線程環境下,對共享資料操作會引發産生資料線程安全的問題。

2、什麼是鎖?鎖解決什麼問題?

鎖是邏輯概念,保證資料一緻性。在同一個時刻,保證一個線程操作共享資料。在Java 開發中使用Synchronized 進行上鎖。

持有鎖的線程進行工作,那不持有鎖的線程咋辦?

  • 忙等待;輕量級,自旋鎖,需要消耗CPU資源;适合競争不激烈
  • 進隊列等待;重量級,需要作業系統進行排程;适合競争激烈

在JDK 早期,Synchronized 是直接向作業系統申請鎖,整個操作非常重;在JDK後期,對Synchronized 關鍵字鎖進行了更新,就是鎖有個更新的過程

Synchronized知識較多,特别花費了較多時間進行整理,下載下傳連結:Synchronized_思維導圖(全面).xmind.zip

案例一:多線程操作共享資料m進行++操作,最終導緻與理想結果不一緻問題

// 多線程,并發情況下對int m變量進行累加,出現的資料被寫覆寫。指派不是原子性的
public class T00_CasAndUnsafe {

    private static int m = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    m++;
                }
                latch.countDown();
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);
    }
}
           

解決方式一:為了解決資料共享問題,可以使用鎖對共享資料加鎖,如下示例:

// 多線程,并發情況下對 int m變量進行累加,資料回寫使用synchronized進行同步加鎖; 實作了資料的一緻性

public class T03_CasAndUnsafe3 {

    private static /*volatile*/ int m = 0;

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[10];
        CountDownLatch latch = new CountDownLatch(threads.length);

        Object o = new Object();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                synchronized (o) {
                    for (int j = 0; j < 10000; j++) {
                        m++;
                    }
                    latch.countDown();
                }
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);

        long end = System.currentTimeMillis();

        long spend = end - start;
        System.out.println("spend:" + spend + " ms");
    }
}
           

解決方式二:為了解決資料共享問題,可以使用自旋鎖CAS對共享資料加鎖,如下示例:

// 多線程,并發情況下對 AtomicInteger m變量進行累加, 實作了資料的一緻性
public class T04_AtomicInteger {

    private static AtomicInteger m = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[10];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    m.getAndIncrement(); // m++;
                }
                latch.countDown();
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);

        long end = System.currentTimeMillis();

        long spend = end - start;
        System.out.println("spend:" + spend + " ms");
    }
}
           

CAS比較并交換,CAS原理圖如下:

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

CAS 引發的問題

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念

文章最後,給大家推薦一些受歡迎的技術部落格連結:

  1. JAVA相關的深度技術部落格連結
  2. Flink 相關技術部落格連結
  3. Spark 核心技術連結
  4. 設計模式 —— 深度技術部落格連結
  5. 機器學習 —— 深度技術部落格連結
  6. Hadoop相關技術部落格連結
  7. 超全幹貨--Flink思維導圖,花了3周左右編寫、校對
  8. 深入JAVA 的JVM核心原了解決線上各種故障【附案例】
  9. 請談談你對volatile的了解?--最近小李子與面試官的一場“硬核較量”
  10. 聊聊RPC通信,經常被問到的一道面試題。源碼+筆記,包懂
  11. 深入聊聊Java 垃圾回收機制【附原理圖及調優方法】

歡迎掃描下方的二維碼或 搜尋 公衆号“大資料進階架構師”,我們會有更多、且及時的資料推送給您,歡迎多多交流!

多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念
多線程與高并發深入底層橫向對比為什麼需要程式、線程?程式運作的底層原理緩存、寄存器MESI Cache 緩存一緻性協定線程安全問題鎖的概念