java多线程编程问题以及解决办法
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