1. 程式人生 > >Java 併發程式設計系列之帶你瞭解多執行緒

Java 併發程式設計系列之帶你瞭解多執行緒

早期的計算機不包含作業系統,它們從頭到尾執行一個程式,這個程式可以訪問計算機中的所有資源。在這種情況下,每次都只能執行一個程式,對於昂貴的計算機資源來說是一種嚴重的浪費。

作業系統出現後,計算機可以執行多個程式,不同的程式在單獨的程序中執行。作業系統負責為各個獨立的程序分配各種資源。並且不同的程序間可以通過一些通訊機制來交換資料,比如:套接字、訊號處理器、共享記憶體、訊號量等。

一、瞭解多執行緒

1.1 程序與執行緒

想必大家都聽說過這兩個名詞,它們之間有什麼聯絡與不同呢?

記得當時上作業系統課時,書上有這麼一句話:程序是獨立擁有 cpu 資源的最 小單位,執行緒是接受 cpu 排程的最小單位。

網上看了那麼多的解釋,還是覺得書上說的最簡單明瞭。

程序中可以同時存在多個程式控制流程的執行緒,執行緒會共享程序範圍內的資源,因此一個程序可以有多個執行緒。打個比方,程序就像一個大工廠,而執行緒就是工廠的工人,多個工人負責協同完成工廠的任務。

1.2 多執行緒的優勢

  • 發揮多處理器的強大能力
  • 簡化建模過程
  • 簡化非同步事件處理機制
  • 響應更靈敏的使用者介面

1.3 多執行緒安全性問題

多執行緒是一把雙刃劍,使用多執行緒時意味著對開發人員有一定的技術要求。可見學會了多執行緒技術,就能寫出更優雅的程式碼了,哈哈~

多個執行緒同時訪問意味著“共享”變數,這些“共享”變數往往在其生命週期內是可變的,這樣以來就極易出現多執行緒安全性問題。

什麼是執行緒安全,這裡給一個比較準確的定義:當多個執行緒訪問某各類時,這個類始終都能表現出正確的行為,那麼就稱這個類是執行緒安全的。

二、Java 記憶體模型

為什麼要在多執行緒專題裡涉及到 Java 記憶體模型呢?我覺得它可以幫助我們更好的理解多執行緒的工作機制。

比如很多人都聽說過或瞭解過 volatile 關鍵字,都知道它能保證記憶體可見性問題,但是理解起來總是太過抽象。如果你瞭解了 Java 記憶體模型,它可以很好的幫助你理解這些問題。
在這裡插入圖片描述
Java 記憶體模型規定了所有的變數都儲存在主記憶體中(Main Memory)中(可以類比為物理硬體的主記憶體)。每條執行緒都有自己的工作記憶體(Working Memory),執行緒的工作記憶體中儲存了主記憶體中的變數的拷貝,執行緒對變數的所有操作(讀取、賦值)都必須在自己的工作記憶體中進行,而不能直接讀寫主記憶體中的變數。

不同的執行緒之間無法訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。

如果你對 Java 記憶體區域瞭解的話,很容易就會想工作記憶體、主記憶體與 Java 記憶體區域中的堆、棧、方法區是否有一定關係呢?

如果在例項變數的角度來看,主記憶體對應於 Java 堆中的物件例項資料部分,而工作記憶體則對應於虛擬機器棧中的部分割槽域。從更低層次來看,主記憶體直接對應於物理硬體的記憶體。

三、解決執行緒安全性問題

使用 Java 建立執行緒的幾種方式這裡就不再贅述了,我們來了解幾種處理多執行緒安全性問題的方法。

3.1 synchronized 關鍵字

Java 提供了一種內建的鎖機制(synchronized 關鍵字)來保證原子性與記憶體可見性。關於原子性與記憶體可見性問題會在講述 volatile 關鍵字時詳細的介紹,感興趣的可以關注後面的文章。

Java 的內建鎖是一種互斥鎖,意味著最多隻有一個執行緒能持有這種鎖。當執行緒 A 和執行緒 B 同時訪問臨界資源,如果執行緒 A 獲取鎖,執行緒 B 就必須等待或阻塞,A 不釋放 B 就只能等待下去。

由於每次只有一個執行緒執行內建鎖內的程式碼,因此被 synchronized 關鍵字保護的臨界資源會以原子(一組不可分割的單元)的方式執行,多個執行緒間執行時不會受到干擾,原子性與資料庫事務有相同的含義。

synchronized 關鍵字可以用來修飾方法和程式碼塊,如果修飾非靜態方法和同步程式碼塊,使用的鎖是當前物件,如果修飾靜態方法和靜態程式碼塊,使用的是當前類的 Class 物件作為鎖。

使用方式如下

public class SyncTest {
    private Integer num = 1;
    
    public int numAutoIncrement() {
        synchronized (this) {
            return num ++;
        }
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        // 開啟 10 個執行緒
        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

3.2 使用原子類

jdk1.5 之後 java.util.concurrent.atomic 包下引入了諸如 AtomicIntegerAtomicLong 等特殊的原子性變數類。

這些變數類底層使用 CAS 演算法與 volatile 關鍵字確保對所有的計數器操作都能保證原子性與可見性,也就解決了多執行緒安全問題。

使用方式如下

public class SyncTest {
    private AtomicInteger atomicInteger = new AtomicInteger(1);
    
    public int numAutoIncrement() {
        return atomicInteger.getAndIncrement();
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

3.3 使用顯示 Lock 鎖

jdk1.5 之前,解決多執行緒共享物件訪問的機制只有 synchronizedvolatile。jdk1.5 新增了一種新的機制:ReentrantLockReentrantLock 並不是為替代內建鎖而生的,當內建鎖機制不適用時,可以考慮使用這種更靈活的加鎖機制。

使用方式如下

public class SyncTest {
    private Lock lock = new ReentrantLock();
    private Integer num = 1;
    
    public int numAutoIncrement() {
        lock.lock();
        try {
            return num++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

從上面的程式碼可以看出來,使用顯示鎖機制相對內建鎖要麻煩一些,使用時要注意必須在 finally 語句塊中釋放鎖,否則當出現異常時,獲取到的鎖永遠不會被釋放,在程式碼中存在這種情況無疑是一種定時炸彈。

顯示鎖相較於內建鎖還提供了等待可中斷、公平與非公平等功能,當然還有一個優點是顯示鎖更加靈活。

PS:
這篇博文中很多知識點拿出來都可以單獨寫一篇文章,這裡只是總結了一部分重要的知識點,先讓大家對多執行緒有一個感性的認識。後面會陸續的補充併發程式設計系列的文章。如果大家覺得寫得還行的話,請持續關注後面的文章。

參考資料

《Java 併發程式設計實戰》
《深入理解 Java 虛擬機器》