java多執行緒程式設計問題以及解決辦法

欢乐豆123發表於2024-06-22

java多執行緒程式設計問題以及解決辦法

多執行緒程式設計雖然可以提高程式的效能和響應速度,但也帶來了許多複雜的問題,如競態條件、死鎖、執行緒安全問題、記憶體一致性錯誤等。常用的解決方法包括使用同步機制(如 synchronized 和 ReentrantLock)、執行緒池、volatile 關鍵字、以及合適的執行緒間通訊機制(如 wait/notify 和 Condition)。

下面多執行緒程式設計中常見的一些問題以及對應的解決辦法

一、競態條件(Race Conditions)

競態條件是指多個執行緒同時訪問和修改共享資料時,由於操作順序的不確定性,可能導致資料的不一致或程式行為異常。競態條件是多執行緒程式設計中最常見的問題之一。

1. 示例

 1 public class Counter {
 2     private int count = 0;
 3 
 4     public void increment() {
 5         count++;
 6     }
 7 
 8     public int getCount() {
 9         return count;
10     }
11 
12     public static void main(String[] args) {
13         Counter counter = new Counter();
14         Runnable task = () -> {
15             for (int i = 0; i < 1000; i++) {
16                 counter.increment();
17             }
18         };
19         Thread thread1 = new Thread(task);
20         Thread thread2 = new Thread(task);
21         thread1.start();
22         thread2.start();
23 
24         try {
25             thread1.join();
26             thread2.join();
27         } catch (InterruptedException e) {
28             e.printStackTrace();
29         }
30 
31         // 預期值是2000,但是可能輸出不正確的結果
32         System.out.println(counter.getCount());
33     }
34 }

說明:上面的執行結果是不穩定的

2. 解決辦法

使用同步機制,如 synchronized 關鍵字或顯式鎖(ReentrantLock)來確保對共享資料的訪問是互斥的。

1) synchronized

 1 public class Counter {
 2     private int count = 0;
 3 
 4     public synchronized void increment() {
 5         count++;
 6     }
 7 
 8     public synchronized int getCount() {
 9         return count;
10     }
11 }

2)ReentrantLock

 1 public class ReentrantLockCounter {
 2     public static ReentrantLock lock = new ReentrantLock();
 3     public static int count = 0;
 4 
 5     public void increment() {
 6         count++;
 7     }
 8 
 9     public int getCount() {
10         return count;
11     }
12     /**
13      * @param args
14      * @throws InterruptedException
15      */
16     public static void main(String[] args) throws InterruptedException {
17         ReentrantLockCounter counter = new ReentrantLockCounter();
18         Runnable task = () -> {
19             for (int i = 0; i < 1000; i++) {
20                 lock.lock();
21                 try {
22                     System.out.println(Thread.currentThread().getName()  + " " + i);
23                     counter.increment();
24                 } finally {
25                     lock.unlock();
26                 }
27             }
28         };
29         Thread thread1 = new Thread(task);
30         Thread thread2 = new Thread(task);
31         thread1.start();
32         thread2.start();
33 
34         try {
35             thread1.join();
36             thread2.join();
37         } catch (InterruptedException e) {
38             e.printStackTrace();
39         }
40         
41         System.out.println(counter.getCount());
42     }
43 }

二、死鎖(Deadlock)

死鎖是指兩個或多個執行緒相互等待對方釋放鎖,從而導致執行緒永久阻塞。死鎖通常發生在多個鎖的情況下,當執行緒獲取鎖的順序不一致時容易產生死鎖。

1. 示例

 1 public class DeadlockDemo {
 2     private static final Object lock1 = new Object();
 3     private static final Object lock2 = new Object();
 4 
 5     public static void main(String[] args) {
 6         Thread thread1 = new Thread(() -> {
 7             synchronized (lock1) {
 8                 System.out.println("Thread 1: Holding lock 1...");
 9                 try { Thread.sleep(10); } catch (InterruptedException e) {}
10                 synchronized (lock2) {
11                     System.out.println("Thread 1: Holding lock 1 & 2...");
12                 }
13             }
14         });
15 
16         Thread thread2 = new Thread(() -> {
17             synchronized (lock2) {
18                 System.out.println("Thread 2: Holding lock 2...");
19                 try { Thread.sleep(10); } catch (InterruptedException e) {}
20                 synchronized (lock1) {
21                     System.out.println("Thread 2: Holding lock 2 & 1...");
22                 }
23             }
24         });
25 
26         thread1.start();
27         thread2.start();
28     }
29 }

2. 解決辦法

避免巢狀鎖定,儘量減少鎖的數量,或透過使用 tryLock 方法來避免死鎖。

 1 public class AvoidDeadlockDemo {
 2     private static final Lock lock1 = new ReentrantLock();
 3     private static final Lock lock2 = new ReentrantLock();
 4 
 5     public static void main(String[] args) {
 6         Thread thread1 = new Thread(() -> {
 7             try {
 8                 if (lock1.tryLock() && lock2.tryLock()) {
 9                     System.out.println("Thread 1: Acquired locks...");
10                 }
11             } finally {
12                 lock1.unlock();
13                 lock2.unlock();
14             }
15         });
16 
17         Thread thread2 = new Thread(() -> {
18             try {
19                 if (lock2.tryLock() && lock1.tryLock()) {
20                     System.out.println("Thread 2: Acquired locks...");
21                 }
22             } finally {
23                 lock2.unlock();
24                 lock1.unlock();
25             }
26         });
27 
28         thread1.start();
29         thread2.start();
30     }
31 }

三、執行緒安全問題

多執行緒訪問共享資料時,如果沒有適當的同步機制,可能導致資料的不一致性和程式行為的不可預知性。這類問題統稱為執行緒安全問題。

使用同步機制確保執行緒安全,如 synchronized 關鍵字、顯式鎖(ReentrantLock)或使用執行緒安全的類(如 AtomicInteger)。

還是以競態條件中的累加計算例子來說明:

 1 public class SafeCounter {
 2     private AtomicInteger count = new AtomicInteger(0);
 3 
 4     public void increment() {
 5         count.incrementAndGet();
 6     }
 7 
 8     public int getCount() {
 9         return count.get();
10     }
11 }

、 上下文切換開銷

執行緒切換(Context Switching)是指CPU從一個執行緒切換到另一個執行緒的過程。頻繁的執行緒切換會導致CPU時間片的浪費,降低程式效能。上下文切換的開銷包括儲存和恢復執行緒狀態,以及處理作業系統排程器。

示例:大量建立和銷燬執行緒,或者頻繁的執行緒切換,都會增加上下文切換的開銷。

程式碼如下:

1 public class ContextSwitchDemo {
2     public static void main(String[] args) {
3         for (int i = 0; i < 10000; i++) {
4             new Thread(() -> {
5                 System.out.println("Thread " + Thread.currentThread().getId());
6             }).start();
7         }
8     }
9 }

說明:上面的程式碼會建立10000個執行緒

解決方法: 使用執行緒池來重用執行緒,減少建立和銷燬執行緒的開銷。

程式碼如下:

 1 public class ThreadPoolDemo {
 2     public static void main(String[] args) {
 3         ExecutorService executor = Executors.newFixedThreadPool(10);
 4         for (int i = 0; i < 10000; i++) {
 5             executor.submit(() -> {
 6                 System.out.println("Thread " + Thread.currentThread().getId());
 7             });
 8         }
 9         executor.shutdown();
10     }
11 }

說明:這裡面只會建立10執行緒來迴圈執行任務

五、記憶體一致性錯誤

記憶體一致性錯誤(Memory Consistency Errors)是指由於缺乏適當的同步機制,不同執行緒對共享變數的修改在記憶體中的可見性不一致,導致執行緒讀取到過期或不正確的資料。

1. 示例

 1 public class MemoryConsistencyDemo {
 2     private static boolean ready = false;
 3     private static int number;
 4 
 5     private static class ReaderThread extends Thread {
 6         public void run() {
 7             while (!ready) {
 8                 Thread.yield();
 9             }
10             System.out.println(number);
11         }
12     }
13 
14     public static void main(String[] args) {
15         new ReaderThread().start();
16         number = 42;
17         ready = true;
18     }
19 }

2. 解決辦法

使用 volatile 關鍵字或同步機制來確保變數的可見性。

將ready屬性設定為:private static volatile boolean ready = false;

參考連結:

https://www.aolifu.org/article/thread_question

相關文章