Java併發編程基礎(上)

介紹

Java 是一種功能強大、用途廣泛的編程語言。Java併發是指多個線程同時執行程序,共享資源和數據。通過synchronized關鍵字、Lock接口等實現線程同步,避免競態條件和數據不一致問題。併發編程提高系統性能和資源利用率,然而併發編程帶來了同步、線程安全等挑戰,以及避免死鎖和競爭條件等常見陷阱。

基本概念

首先,爲了理解和使用 Java 中的併發編程打下基礎。併發編程對於利用現代多核處理器的強大功能以及創建能夠同時並行執行任務的響應靈敏且高效的應用程序至關重要。

概念 解釋
Thread 線程代表 Java 程序內的獨立執行路徑。線程允許併發和並行執行代碼。Java通過Thread類支持多線程。
Runnable 該Runnable接口用於定義可由線程執行的代碼。它提供了一種封裝線程應執行的任務或作業的方法。
Synchronization 塊和方法等同步機制synchronized用於控制對代碼關鍵部分的訪問,防止多個線程同時訪問它們。
Locks and Mutexes 鎖(例如,ReentrantLock)是用於管理對共享資源的訪問的顯式機制,允許線程獲取和釋放鎖以進行受控訪問。
Race Conditions 當兩個或多個線程同時訪問共享數據時,就會出現競爭條件,最終結果取決於執行順序,從而導致不可預測的行爲。正確的同步可以防止競爭情況。
Data Race 數據競爭是一種特定類型的競爭條件,其中兩個或多個線程同時訪問共享數據,並且至少其中一個線程修改數據。數據競爭可能會導致未定義的行爲,應該避免。
Deadlocks 當兩個或多個線程被阻塞,等待永遠不會被釋放的資源時,就會發生死鎖。識別和避免死鎖對於併發編程至關重要。
Atomic Operations 原子操作是線程安全的操作,可以在不受其他線程干擾的情況下執行。Java 提供了原子類,例如AtomicInteger和AtomicReference。
Thread Local Storage 線程本地存儲允許每個線程擁有自己的變量副本,該副本與其他線程隔離。它對於存儲特定於線程的數據很有用。
Volatile 該volatile關鍵字確保對變量的更改對所有線程都可見。它用於多個線程無需同步訪問的變量。
Java Memory Model (JMM) JMM 定義了線程如何與內存交互的規則和保證,確保一個線程所做的更改對其他線程的可見性。

Thread和Runnable

Thread類是創建和管理線程的基礎類。它允許使用者在應用程序中定義和運行併發任務或進程。線程代表獨立的執行邏輯,可以同時執行任務,從而可以在程序中實現並行性。

/**  
 * 測試線程  
 */  
class TestThread extends Thread {  
  
    /**  
    * 重寫run方法  
    */  
    @Override  
    void run() {  
        for (int i = 1; i < 3; i++) {  
            println("線程: ${Thread.currentThread().getName()} 計數: $i")//輸出線程名和計數  
        }  
    }  
  
    static void main(String[] args) {  
        //創建兩個線程  
        TestThread thread1 = new TestThread()  
        TestThread thread2 = new TestThread()  
        //啓動線程  
        thread1.start()  
        thread2.start()  
        println "主線程結束"  
    }  
}

控制檯輸出:

主線程結束
線程: Thread-1 計數: 1
線程: Thread-0 計數: 1
線程: Thread-1 計數: 2
線程: Thread-0 計數: 2

Runnable接口是一個函數式接口,表示可以由線程併發執行的任務或代碼段。它提供了一種定義線程應運行的代碼的方法,而無需顯式擴展該類Thread。實現Runnable接口可以更好地分離關注點並提高代碼的可重用性。

/**  
 * 測試runnable接口類  
 */  
class TestRunnable implements Runnable {  
  
    /**  
    * 重寫run方法  
    */  
    @Override  
    void run() {  
        for (int i = 1; i < 3; i++) {  
            println("線程: ${Thread.currentThread().getName()} 計數: $i")//輸出線程名和計數  
        }  
    }  
  
    static void main(String[] args) {  
        //創建兩個接口實例  
        TestRunnable runnable1 = new TestRunnable()  
        TestRunnable runnable2 = new TestRunnable()  
        //創建兩個線程  
        Thread thread1 = new Thread(runnable1)  
        Thread thread2 = new Thread(runnable2)  
        //啓動線程  
        thread1.start()  
        thread2.start()  
        println "主線程結束"  
    }  
}

控制檯輸出:

主線程結束
線程: Thread-1 計數: 1
線程: Thread-0 計數: 1
線程: Thread-0 計數: 2
線程: Thread-1 計數: 2

Thread狀態表示線程在其生命週期中可能處於的不同階段或條件:

線程狀態 描述
NEW 線程處於NEW已創建但尚未開始執行時的狀態。它尚未具備運行資格,尚未獲取任何系統資源。
RUNNABLE 線程處於RUNNABLE可以運行的狀態,Java虛擬機(JVM)已經爲它的執行分配了資源。但是,它當前可能尚未執行。
BLOCKED 線程處於BLOCKED等待獲取監視器鎖以進入同步塊或方法的狀態。它被另一個持有鎖的線程阻塞。
WAITING 線程處於WAITING等待滿足特定條件才能繼續執行的狀態。它可能會無限期地等待,直到收到另一個線程的通知。
TIMED_WAITING 與狀態類似WAITING,狀態中的線程TIMED_WAITING正在等待特定條件。但它有一個超時時間,RUNNABLE超時後會自動過渡到。
TERMINATED 線程處於TERMINATED已完成執行或已顯式終止時的狀態。一旦終止,線程就無法重新啓動或再次運行。
線程生命週期相關方法:
方法 描述
start() 通過調用線程的方法來啓動線程的執行run()。當start()調用時,線程從NEW狀態轉換到RUNNABLE狀態,並開始併發執行。這是啓動新線程的主要方法。
wait() 用於使線程自願放棄其持有的監視器鎖。應該從同步塊或方法中調用它。線程進入該WAITING狀態並釋放鎖,直到收到另一個線程的通知。
notify()/notifyAll() wait()用於喚醒在同一對象上使用該方法正在等待的一個/所有線程。它允許一個/所有等待線程轉換回狀態RUNNABLE,讓它們有機會繼續。
join() 允許一個線程等待另一線程完成。當一個線程調用join()另一個線程時,它將阻塞,直到目標線程完成執行。
yield() 向 JVM 表明當前線程願意讓出當前的 CPU 時間以允許其他線程運行。這是一個提示,實際行爲取決於 JVM 的實現。
sleep() 將當前線程的執行暫停指定的時間(以毫秒爲單位)。它允許您在程序中引入延遲,通常用於計時目的。
interrupt() 通過設置線程的中斷狀態來中斷線程的執行。它可用於請求線程正常終止或以自定義方式處理中斷。如果線程正在等待、睡眠或阻塞,InterruptedException則會拋出異常。如果您在中斷線程級別捕獲異常,請通過調用手動設置其中斷狀態Thread.currentThread().interrupt()並拋出異常,以便在更高級別進行處理。

同步關鍵字

synchronized關鍵字用於創建同步代碼塊,確保Thread一次只有一個可以執行它們。它提供了一種控制對程序關鍵部分的訪問的方法,防止多個線程同時訪問它們。

要進入同步塊,必須獲取對象監視器上的鎖。對象的監視器是一種同步機制,提供 Object 實例的鎖定功能。執行此操作後,塊中包含的所有代碼都可以進行獨佔和原子操作。退出 synchronized 塊後,鎖將返回到對象的監視器以供其他線程獲取。如果無法立即獲取鎖,則執行 Thread 會等待,直到獲取到這個鎖。

/**  
 * 測試線程,用於測試synchronize關鍵字  
 */  
class TestThread extends Thread {  
  
    static int count = 0//計數器,用於統計循環次數,這裏是共享資源  
  
    static Object lock = new Object()//鎖對象,用於同步代碼塊  
  
    /**  
     * 重寫run方法  
     */  
    @Override  
    void run() {  
        for (int i = 0; i < 10; i++) {  
            synchronized (lock) { // 同步代碼塊  
                count++//計數器加1  
            }  
        }  
    }  
  
    static void main(String[] args) {  
        TestThread thread1 = new TestThread()//創建線程實例  
        TestThread thread2 = new TestThread()//創建線程實例  
        thread1.start()//啓動線程  
        thread2.start()//啓動線程  
        thread1.join()//等待線程1結束  
        thread2.join()//等待線程2結束  
        println "count = $count"//輸出count的值  
        println "主線程結束"  
  
    }  
}

控制檯輸出:

count = 20
主線程結束

synchronized關鍵字也可以在方法級別上指定。對於非靜態方法,鎖是從該方法所屬的對象實例的監視器中獲取的;對於靜態方法,則是從該方法所屬類的Class對象的監視器中獲取的。

/**  
 * 計數器加1  
 * @return  
 */  
synchronized def increase() {  
    count++//計數器加1  
}  
  
/**  
 * 計數器減1  
 * @return  
 */  
static synchronized decrease() {  
    count--//計數器減1  
}

鎖是可重入的,因此如果線程已經持有鎖,它可以再次成功獲取鎖。

/**  
 * 這個方法演示synchronize重入, 一個線程可以多次進入同一個對象的synchronized方法  
 * @return  
 */  
synchronized def step() {  
    step1()  
    step2()  
}  
  
synchronized def step1() {  
  
}  
  
synchronized def step2() {  
  
}

wait/notify/notifyAll

wait()使用, notify(),方法同步訪問功能/資源的最常見模式notifyAll()是條件循環。讓我們看一個示例,演示如何使用wait()notify()協調兩個線程來打印備用數字:

public class WaitNotifyExample {
    private static final Object lock = new Object();
    private static boolean isOddTurn = true;
 
    public static void main(String[] args) {
        Thread oddThread = new Thread(() -> {
            for (int i = 1; i <= 10; i += 2) {
                synchronized (lock) {
                    while (!isOddTurn) {
                        try {
                            lock.wait(); // Wait until it's the odd thread's turn
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    System.out.println("Odd: " + i);
                    isOddTurn = false; // Satisfy the waiting condition
                    lock.notify(); // Notify the even thread
                }
            }
        });
 
        Thread evenThread = new Thread(() -> {
            for (int i = 2; i <= 10; i += 2) {
                synchronized (lock) {
                    while (isOddTurn) {
                        try {
                            lock.wait(); // Wait until it's the even thread's turn
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    System.out.println("Even: " + i);
                    isOddTurn = true; // Satisfy the waiting condition
                    lock.notify(); // Notify the odd thread
                }
            }
        });
 
        oddThread.start();
        evenThread.start();
    }
}

控制檯輸出:

Odd: 1
Even: 2
Odd: 3
Even: 4
Odd: 5
Even: 6
Odd: 7
Even: 8
Odd: 9
Even: 10

注意事項:

  • 爲了在一個對象上使用wait()notify()notifyAll(),需要首先獲取該對象的鎖——我們的兩個線程在該lock對象上同步以獲取其鎖。
  • 始終在檢查正在等待的條件的循環內等待。如果另一個線程在等待開始之前滿足條件,這可以解決計時問題,並且還可以保護您的代碼免受虛假喚醒 - 我們的兩個線程都在由標誌控制的循環內等待isOddTurn
  • notify()在調用/之前始終確保滿足等待條件notifyAll()。如果不這樣做將導致通知,但沒有線程能夠逃脫其等待循環——我們的兩個線程都滿足isOddTurn另一個線程繼續的標誌。

volatile

當一個變量被聲明爲volatile時,它保證對該變量的任何讀寫操作都直接在主內存上執行,確保原子更新和對所有線程的變化可見性。換句話說,JMM對於事件“寫入volatile變量”和任何後續“讀取volatile變量”都應用了“happens-before”關係。因此,變量的任何後續讀取都將看到最近寫入的值。

ThreadLocal 類

ThreadLocal是一個提供線程局部變量的類。線程局部變量是每個線程唯一的變量,這意味着訪問變量的每個線程ThreadLocal都會獲得該變量自己的獨立副本。當使用者有需要爲每個線程隔離和單獨維護的數據時,這非常有用,而且還可以減少對共享資源的爭用,這通常會導致性能瓶頸。它通常用於存儲用戶會話、數據庫連接和線程特定狀態等值,而無需在方法之間顯式傳遞它們。

ThreadLocal以下是如何使用存儲和檢索線程特定數據的簡單示例:

public class ThreadLocalExample {
/**  
 * 創建一個ThreadLocal對象,使用withInitial方法設置初始值  
 */  
static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(() -> System.currentTimeMillis())  
  
static void main(String[] args) {  
    for (i in 0..<3) {//創建3個線程  
        new Thread({//每個線程都會打印出threadLocal的值  
            println threadLocal.get()  
        }).start()  
        sleep(1000)//休眠1秒  
    }  
}
}

控制檯輸出:

1708155410499
1708155411511
1708155412516

不可變對象

不可變對象是指其狀態在創建後無法修改的對象。一旦不可變對象被初始化,其內部狀態在其整個生命週期中保持不變。此屬性使不可變對象本質上是線程安全的,因爲它們不能被多個線程同時修改,從而消除了同步的需要。

創建不可變對象涉及幾個關鍵步驟:

  1. 創建類final:防止繼承並確保該類不能被子類化。
  2. 將所有字段聲明爲final:將所有實例變量標記爲,以final確保它們僅初始化一次,通常在構造函數中初始化。
  3. 無 setter 方法:不提供允許修改對象狀態的 setter 方法。
  4. 安全發佈this構建過程中引用不會逃逸。
  5. 沒有可變對象:如果類包含對可變對象(可以更改其狀態的對象)的引用,請確保這些引用不公開或允許外部修改。
  6. 將所有字段設爲私有:通過將字段設爲私有來封裝字段以限制直接訪問。
  7. 在修改狀態的方法中返回一個新對象:不修改現有對象,而是創建一個具有所需更改的新對象並返回它。
  
/**  
 * 不可變對象  
 */  
final class ImmutablePerson {  
  
    final String name  
  
    final int age  
  
    final List<ImmutablePerson> family  
  
    ImmutablePerson(String name, int age, List<ImmutablePerson> family) {  
        this.name = name  
        this.age = age  
        List<ImmutablePerson> copy = new ArrayList<>(family)// 構造函數中的參數family是一個可變的集合,所以需要進行防禦性複製  
        this.family = Collections.unmodifiableList(copy)// 通過unmodifiableList方法,返回一個不可變的集合  
//        在構造方法中,“this”不會傳遞到任何地方  
    }  
  
    String getName() {  
        name  
    }  
  
    int getAge() {  
        age  
    }  
  
    // 沒有set方法,所以對象的狀態不會改變  
  
    // 通過返回一個新的對象來實現修改  
    ImmutablePerson withAge(int newAge) {  
        return new ImmutablePerson(this.name, newAge)  
    }  
  
    // 爲簡單起見,沒有 toString、hashCode 和 equals 方法  
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章