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