Java 多线程编程 是指在一个 Java 应用程序中同时执行多个独立的任务(或代码路径)。线程是操作系统调度的最小执行单元,而多线程编程允许程序更有效地利用 CPU 资源,提高程序的响应性和吞吐量,尤其是在现代多核处理器环境中。
核心思想:将一个程序分解为多个独立的执行流,并发地运行以提高效率和响应性。这要求开发者妥善处理线程间的协作与资源竞争,以避免数据不一致、死锁等问题。
一、为什么需要多线程编程? 在单线程环境中,程序任务按顺序执行。如果一个任务耗时较长(例如 I/O 操作、复杂计算),整个程序就会“卡住”,直到该任务完成。多线程编程旨在解决这些问题:
提高程序响应性 :在图形用户界面 (GUI) 应用程序中,可以将耗时操作放在后台线程执行,主线程(UI 线程)保持响应,提升用户体验。
提高系统吞吐量 :在服务器端应用中,可以同时处理多个客户端请求,从而提高服务器的处理能力。
充分利用多核 CPU 资源 :现代处理器普遍拥有多核。多线程允许程序将计算任务分解为可并行执行的部分,从而利用所有可用的 CPU 核心,显著缩短总执行时间。
简化编程模型 :对于某些复杂任务,将其分解为相互独立的子任务并通过多线程实现,可能比单线程模型更容易理解和实现。
二、线程的创建与启动 在 Java 中,创建和启动线程主要有两种方式:
2.1 继承 Thread 类 通过继承 java.lang.Thread 类并重写其 run() 方法来定义线程的执行逻辑。
优点 :
缺点 :
Java 不支持多重继承,如果业务类已经继承了其他类,就不能再继承 Thread 类。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class MyThread extends Thread { private String threadName; public MyThread (String name) { this .threadName = name; System.out.println("Creating " + threadName); } @Override public void run () { System.out.println("Running " + threadName); try { for (int i = 4 ; i > 0 ; i--) { System.out.println("Thread: " + threadName + ", " + i); Thread.sleep(50 ); } } catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted." ); } System.out.println("Thread " + threadName + " exiting." ); } public static void main (String[] args) { MyThread thread1 = new MyThread ("Thread-1" ); thread1.start(); MyThread thread2 = new MyThread ("Thread-2" ); thread2.start(); } }
2.2 实现 Runnable 接口 通过实现 java.lang.Runnable 接口并实现其 run() 方法来定义线程的执行逻辑,然后将 Runnable 实例传递给 Thread 类的构造器。
优点 :
克服了单继承的限制,业务类可以继承其他类。
Runnable 接口定义了任务,Thread 类定义了执行任务的机制,实现了任务与线程的解耦 。
同一个 Runnable 实例可以被多个 Thread 对象共享,便于处理共享资源。
缺点 :
相对于继承 Thread 类,创建和启动稍微复杂一点点。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class MyRunnable implements Runnable { private String threadName; public MyRunnable (String name) { this .threadName = name; System.out.println("Creating " + threadName); } @Override public void run () { System.out.println("Running " + threadName); try { for (int i = 4 ; i > 0 ; i--) { System.out.println("Runnable: " + threadName + ", " + i); Thread.sleep(50 ); } } catch (InterruptedException e) { System.out.println("Runnable " + threadName + " interrupted." ); } System.out.println("Runnable " + threadName + " exiting." ); } public static void main (String[] args) { MyRunnable runnable1 = new MyRunnable ("Runnable-1" ); Thread thread1 = new Thread (runnable1); thread1.start(); MyRunnable runnable2 = new MyRunnable ("Runnable-2" ); Thread thread2 = new Thread (runnable2); thread2.start(); } }
2.3 使用 Callable 和 Future (带返回值) 上述两种方式的 run() 方法都没有返回值。如果需要线程执行完任务后返回一个结果,可以使用 java.util.concurrent.Callable 接口和 java.util.concurrent.Future。
Callable 接口类似于 Runnable,但它的 call() 方法可以返回一个结果,并且可以抛出异常。
Future 对象用于表示异步计算的结果。它提供了 get() 方法来获取 Callable 任务的返回值,该方法会阻塞直到结果可用。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.util.concurrent.*;class SumTask implements Callable <Integer> { private int number; public SumTask (int number) { this .number = number; } @Override public Integer call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= number; i++) { sum += i; Thread.sleep(10 ); } System.out.println("Task for " + number + " finished." ); return sum; } public static void main (String[] args) throws ExecutionException, InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(2 ); Future<Integer> future1 = executor.submit(new SumTask (10 )); Future<Integer> future2 = executor.submit(new SumTask (5 )); System.out.println("Result for 10: " + future1.get()); System.out.println("Result for 5: " + future2.get()); executor.shutdown(); } }
三、线程的生命周期 线程的生命周期通常包含以下六种状态:
graph TD
A[NEW - 新建] --> B{RUNNABLE - 可运行}
B --> C[RUNNING - 运行中]
C --> B
C --> D[BLOCKED - 阻塞]
D --> B
C --> E[WAITING - 等待]
E --> B
C --> F[TIMED_WAITING - 定时等待]
F --> B
C --> G[TERMINATED - 终止]
B --> G
E --> G
F --> G
D --> G
NEW (新建) :当线程对象被创建(new Thread()),但尚未调用 start() 方法时。
RUNNABLE (可运行) :当线程调用了 start() 方法后,等待 CPU 调度。线程可能正在运行,也可能在等待运行。
RUNNING (运行中) :线程正在执行任务。在 Java 中,RUNNABLE 状态包含了 RUNNING 状态。
BLOCKED (阻塞) :线程被阻塞,等待获取某个监视器锁(例如,进入 synchronized 块或方法)。
WAITING (等待) :线程无限期等待另一个线程执行特定操作(例如,调用 Object.wait()、Thread.join()、LockSupport.park())。
TIMED_WAITING (定时等待) :线程在指定的时间内等待另一个线程执行特定操作(例如,调用 Thread.sleep(long millis)、Object.wait(long millis)、Thread.join(long millis)、LockSupport.parkNanos()、LockSupport.parkUntil())。
TERMINATED (终止) :线程的 run() 方法执行完毕或因异常退出。
四、线程同步与并发问题 多线程编程的复杂性主要来源于共享资源的竞争 。当多个线程同时访问和修改同一个共享变量或资源时,可能导致数据不一致性、竞态条件 (Race Condition) 等问题。
并发问题示例 : 两个线程同时对一个计数器进行递增操作,如果不同步,最终结果可能小于预期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Counter { private int count = 0 ; public void increment () { count++; } public int getCount () { return count; } }
为了解决这些问题,Java 提供了多种线程同步机制:
4.1 synchronized 关键字 synchronized 关键字用于实现互斥锁 (Mutex Lock) ,确保在任何时刻只有一个线程能够执行特定的代码块或方法。
同步方法 :synchronized 修饰非静态方法,锁住的是当前实例对象 this。
同步静态方法 :synchronized 修饰静态方法,锁住的是当前类的 Class 对象。
同步代码块 :可以指定任意对象作为锁。synchronized(object) { ... }
示例 :解决上述 Counter 问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class SynchronizedCounter { private int count = 0 ; public synchronized void incrementMethod () { count++; } public void incrementBlock () { synchronized (this ) { count++; } } public static synchronized void staticIncrement () { } public int getCount () { return count; } }
4.2 volatile 关键字 volatile 关键字用于修饰变量。它保证了对该变量的读写操作具有可见性 (Visibility) ,即一个线程对 volatile 变量的修改,对其他线程是立即可见的。 此外,volatile 还禁止了指令重排序优化。
注意 :volatile 只能保证可见性,不能保证原子性。对于复合操作(如 count++),仍然需要 synchronized 或 java.util.concurrent.atomic 包下的类来保证原子性。
适用场景 :作为状态标志位,或者对单次写、多次读的共享变量进行同步。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class VolatileFlag { private volatile boolean running = true ; public void stop () { running = false ; } public void run () { System.out.println(Thread.currentThread().getName() + " starting..." ); while (running) { } System.out.println(Thread.currentThread().getName() + " stopped." ); } public static void main (String[] args) throws InterruptedException { VolatileFlag task = new VolatileFlag (); Thread worker = new Thread (task::run, "WorkerThread" ); worker.start(); Thread.sleep(100 ); task.stop(); } }
4.3 java.util.concurrent.locks 包 Java 提供了更灵活、更强大的锁机制,位于 java.util.concurrent.locks 包中,最常用的是 ReentrantLock。
ReentrantLock :可重入锁,具有与 synchronized 相同的基本行为,但提供了更丰富的功能:
可中断锁 :线程可以尝试获取锁,如果长时间未获取到,可以选择放弃。
公平锁 :可以实现公平性,让等待时间最长的线程优先获取锁。
条件变量 :与 Condition 接口配合,实现更复杂的线程间通信(await() / signal())。
尝试获取锁 :tryLock() 方法可以尝试获取锁,如果失败则立即返回,不会阻塞。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;class ReentrantLockCounter { private int count = 0 ; private final Lock lock = new ReentrantLock (); public void increment () { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount () { return count; } }
4.4 java.util.concurrent.atomic 包 这个包提供了一些原子类,用于在多线程环境下无锁 (lock-free) 地执行原子操作。它们内部使用 CAS (Compare-And-Swap) 操作实现。
AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 等。
优点 :
性能优于锁 :在竞争不激烈的情况下,性能通常优于 synchronized 和 ReentrantLock。
避免死锁 :无锁操作不会导致死锁。
示例 :解决 Counter 问题
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.concurrent.atomic.AtomicInteger;class AtomicCounter { private AtomicInteger count = new AtomicInteger (0 ); public void increment () { count.incrementAndGet(); } public int getCount () { return count.get(); } }
五、线程间协作 当一个线程需要等待另一个线程完成某个条件或操作时,就需要线程间协作。
5.1 wait(), notify(), notifyAll() (配合 synchronized) 这些方法是 Object 类的方法,必须在 synchronized 代码块或方法中调用。
wait():当前线程释放对象锁,进入等待状态,直到被 notify() 或 notifyAll() 唤醒,或者超时。
notify():随机唤醒一个在该对象上调用 wait() 的线程。
notifyAll():唤醒在该对象上调用 wait() 的所有线程。
经典问题 :生产者-消费者模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import java.util.LinkedList;import java.util.Queue;class ProducerConsumer { private static final int CAPACITY = 5 ; private final Queue<Integer> queue = new LinkedList <>(); public void produce () throws InterruptedException { int value = 0 ; while (true ) { synchronized (this ) { while (queue.size() == CAPACITY) { System.out.println("Queue is full, Producer waiting..." ); wait(); } System.out.println("Producer produced: " + value); queue.offer(value++); notifyAll(); Thread.sleep(100 ); } } } public void consume () throws InterruptedException { while (true ) { synchronized (this ) { while (queue.isEmpty()) { System.out.println("Queue is empty, Consumer waiting..." ); wait(); } int value = queue.poll(); System.out.println("Consumer consumed: " + value); notifyAll(); Thread.sleep(100 ); } } } public static void main (String[] args) { ProducerConsumer pc = new ProducerConsumer (); Runnable producerTask = () -> { try { pc.produce(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Runnable consumerTask = () -> { try { pc.consume(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; new Thread (producerTask, "Producer-1" ).start(); new Thread (consumerTask, "Consumer-1" ).start(); } }
5.2 Condition (配合 ReentrantLock) Condition 接口提供了比 Object 的 wait(), notify() 更精细的控制。一个 ReentrantLock 可以关联多个 Condition 对象,使得不同条件的线程可以分批等待和唤醒。
await():类似于 wait()。
signal():类似于 notify()。
signalAll():类似于 notifyAll()。
示例 :使用 Condition 改进生产者-消费者模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import java.util.LinkedList;import java.util.Queue;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;class ProducerConsumerWithCondition { private static final int CAPACITY = 5 ; private final Queue<Integer> queue = new LinkedList <>(); private final Lock lock = new ReentrantLock (); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void produce () throws InterruptedException { int value = 0 ; while (true ) { lock.lock(); try { while (queue.size() == CAPACITY) { System.out.println("Queue is full, Producer waiting..." ); notFull.await(); } System.out.println("Producer produced: " + value); queue.offer(value++); notEmpty.signalAll(); Thread.sleep(100 ); } finally { lock.unlock(); } } } public void consume () throws InterruptedException { while (true ) { lock.lock(); try { while (queue.isEmpty()) { System.out.println("Queue is empty, Consumer waiting..." ); notEmpty.await(); } int value = queue.poll(); System.out.println("Consumer consumed: " + value); notFull.signalAll(); Thread.sleep(100 ); } finally { lock.unlock(); } } } public static void main (String[] args) { ProducerConsumerWithCondition pc = new ProducerConsumerWithCondition (); Runnable producerTask = () -> { try { pc.produce(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Runnable consumerTask = () -> { try { pc.consume(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; new Thread (producerTask, "Producer-1" ).start(); new Thread (consumerTask, "Consumer-1" ).start(); } }
5.3 join() 方法 一个线程可以在另一个线程上调用 join() 方法,以等待该线程执行完毕。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class JoinDemo { public static void main (String[] args) throws InterruptedException { Thread workerThread = new Thread (() -> { System.out.println("Worker thread started." ); try { Thread.sleep(2000 ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker thread finished." ); }); workerThread.start(); System.out.println("Main thread waiting for worker thread to finish..." ); workerThread.join(); System.out.println("Worker thread joined. Main thread continues." ); } }
六、线程池 (Thread Pool) 每次创建和销毁线程都会带来一定的开销。线程池是一种管理和复用线程的机制,它可以预先创建一组线程,当有任务到来时,直接从池中获取空闲线程执行,任务执行完毕后线程归还池中,而不是销毁。
优点 :
降低资源消耗 :减少了线程创建和销毁的开销。
提高响应速度 :任务无需等待新线程的创建即可立即执行。
提高线程可管理性 :统一管理、分配、调优和监控线程。
提供更多功能 :如定时执行、周期执行等。
Java 通过 java.util.concurrent.Executors 工具类提供了多种线程池:
newFixedThreadPool(int nThreads):创建固定大小的线程池。
newCachedThreadPool():创建可缓存的线程池,按需创建新线程,空闲线程会被回收。
newSingleThreadExecutor():创建只有一个线程的线程池,确保任务按顺序执行。
newScheduledThreadPool(int corePoolSize):创建支持定时及周期性任务执行的线程池。
核心类 :ThreadPoolExecutor,它是所有线程池实现的基础。
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;class ThreadPoolDemo { public static void main (String[] args) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(3 ); for (int i = 0 ; i < 10 ; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " started by " + Thread.currentThread().getName()); try { Thread.sleep(200 ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task " + taskId + " finished by " + Thread.currentThread().getName()); }); } executor.shutdown(); if (!executor.awaitTermination(1 , TimeUnit.MINUTES)) { System.err.println("ThreadPool did not terminate in the specified time." ); } System.out.println("All tasks finished, Thread Pool Shut Down." ); } }
七、Java 内存模型 (JMM - Java Memory Model) JMM 定义了 Java 程序中各种变量(线程共享的变量)的访问规则,它决定了一个线程对共享变量的写入何时对另一个线程可见。JMM 的主要目标是在保证程序正确性的前提下,尽可能地提高处理器使用效率 。
JMM 的主要特性 :
原子性 (Atomicity) :指一个操作是不可中断的,要么全部执行成功,要么全部不执行。例如,x = 10 是原子操作,count++ 不是。java.util.concurrent.atomic 包提供了原子操作。
可见性 (Visibility) :指一个线程对共享变量的修改,对其他线程是立即可见的。volatile、synchronized、final 都可以保证可见性。
有序性 (Ordering) :指程序执行的顺序。处理器和编译器为了提高性能,可能会对指令进行重排序。JMM 规定了在多线程环境下,哪些重排序是允许的,哪些是禁止的。
volatile 变量的读写操作具有特殊排序规则,可以防止与其相关的指令重排序。
synchronized 块的进入和退出也具有内存屏障,可以保证其内部代码的有序性。
** Happens-Before 原则**: JMM 通过 Happens-Before (先行发生) 原则来保证多线程操作的可见性和有序性。如果一个操作 Happens-Before 另一个操作,那么第一个操作的结果对第二个操作是可见的,并且第一个操作的执行顺序在第二个操作之前。常见的 Happens-Before 规则包括:
程序次序规则 :在一个线程内,前面的操作 Happens-Before 后面的操作。
监视器锁规则 :对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
volatile 变量规则 :对一个 volatile 字段的写操作 Happens-Before 于后续对这个 volatile 字段的读操作。
Thread.start() 规则 :Thread 对象的 start() 方法 Happens-Before 于该线程的任意操作。
Thread.join() 规则 :线程的所有操作 Happens-Before 于对该线程 join() 方法的成功返回。
传递性 :如果 A Happens-Before B,B Happens-Before C,那么 A Happens-Before C。
理解 JMM 对于编写正确的并发程序至关重要,特别是当涉及到复杂的共享状态和并发数据结构时。
八、并发工具类 (java.util.concurrent) java.util.concurrent 包(通常称为 J.U.C 包)提供了大量高级的并发构建块,极大地简化了多线程编程。除了上面提到的 ExecutorService、Callable、Future、Lock、Condition、Atomic 类,还有:
并发集合 :ConcurrentHashMap (线程安全的 HashMap)、CopyOnWriteArrayList (写时复制列表)、BlockingQueue (阻塞队列,用于生产者-消费者模式)。
同步器 :
CountDownLatch (倒计时门闩):允许一个或多个线程等待其他线程完成一组操作。
CyclicBarrier (循环屏障):允许多个线程相互等待,直到所有线程都到达一个公共屏障点。
Semaphore (信号量):控制同时访问特定资源的线程数量。
Exchanger (交换器):允许两个线程在某个点交换对象。
九、总结 Java 多线程编程是现代高性能、高并发应用开发的核心。它通过允许程序同时执行多个任务来充分利用硬件资源,提高系统的响应性和吞吐量。然而,多线程也带来了复杂的同步和并发问题。Java 提供了从底层 synchronized、volatile 到 java.util.concurrent 包中丰富的高级工具,帮助开发者有效地管理线程、同步共享资源、协调线程间协作。深入理解线程的生命周期、同步机制(synchronized、Lock、Atomic)、线程间协作(wait/notify、Condition)以及 Java 内存模型 (JMM) 是编写健壮、高效并发程序的关键。正确选择和使用这些工具,能够构建出稳定且性能优异的并发应用。