Java 多线程编程 是指在一个 Java 应用程序中同时执行多个独立的任务(或代码路径)。线程是操作系统调度的最小执行单元,而多线程编程允许程序更有效地利用 CPU 资源,提高程序的响应性和吞吐量,尤其是在现代多核处理器环境中。

核心思想:将一个程序分解为多个独立的执行流,并发地运行以提高效率和响应性。这要求开发者妥善处理线程间的协作与资源竞争,以避免数据不一致、死锁等问题。


一、为什么需要多线程编程?

在单线程环境中,程序任务按顺序执行。如果一个任务耗时较长(例如 I/O 操作、复杂计算),整个程序就会“卡住”,直到该任务完成。多线程编程旨在解决这些问题:

  1. 提高程序响应性:在图形用户界面 (GUI) 应用程序中,可以将耗时操作放在后台线程执行,主线程(UI 线程)保持响应,提升用户体验。
  2. 提高系统吞吐量:在服务器端应用中,可以同时处理多个客户端请求,从而提高服务器的处理能力。
  3. 充分利用多核 CPU 资源:现代处理器普遍拥有多核。多线程允许程序将计算任务分解为可并行执行的部分,从而利用所有可用的 CPU 核心,显著缩短总执行时间。
  4. 简化编程模型:对于某些复杂任务,将其分解为相互独立的子任务并通过多线程实现,可能比单线程模型更容易理解和实现。

二、线程的创建与启动

在 Java 中,创建和启动线程主要有两种方式:

2.1 继承 Thread

通过继承 java.lang.Thread 类并重写其 run() 方法来定义线程的执行逻辑。

优点

  • 直接操作 Thread 对象,代码结构清晰。

缺点

  • 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 使用 CallableFuture (带返回值)

上述两种方式的 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);

// 提交 Callable 任务
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();
}
}

三、线程的生命周期

线程的生命周期通常包含以下六种状态:

  1. NEW (新建):当线程对象被创建(new Thread()),但尚未调用 start() 方法时。
  2. RUNNABLE (可运行):当线程调用了 start() 方法后,等待 CPU 调度。线程可能正在运行,也可能在等待运行。
  3. RUNNING (运行中):线程正在执行任务。在 Java 中,RUNNABLE 状态包含了 RUNNING 状态。
  4. BLOCKED (阻塞):线程被阻塞,等待获取某个监视器锁(例如,进入 synchronized 块或方法)。
  5. WAITING (等待):线程无限期等待另一个线程执行特定操作(例如,调用 Object.wait()Thread.join()LockSupport.park())。
  6. TIMED_WAITING (定时等待):线程在指定的时间内等待另一个线程执行特定操作(例如,调用 Thread.sleep(long millis)Object.wait(long millis)Thread.join(long millis)LockSupport.parkNanos()LockSupport.parkUntil())。
  7. 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;
}
}
// 假设两个线程并发调用 increment()
// 线程1: 读取 count (0) -> 修改 count (1) -> 写入 count (1)
// 线程2: 读取 count (0) -> 修改 count (1) -> 写入 count (1)
// 预期结果 2,实际结果 1

为了解决这些问题,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++;
}
}

// 同步静态方法,锁住 SynchronizedCounter.class
public static synchronized void staticIncrement() {
// ...
}

public int getCount() {
return count;
}
}

4.2 volatile 关键字

volatile 关键字用于修饰变量。它保证了对该变量的读写操作具有可见性 (Visibility),即一个线程对 volatile 变量的修改,对其他线程是立即可见的。
此外,volatile 还禁止了指令重排序优化。

注意volatile 只能保证可见性,不能保证原子性。对于复合操作(如 count++),仍然需要 synchronizedjava.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; // 保证 running 变量的可见性

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); // 等待 worker 线程启动并运行一段时间
task.stop(); // 主线程修改 running 标志
}
}

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) 操作实现。

  • AtomicIntegerAtomicLongAtomicBooleanAtomicReference 等。

优点

  • 性能优于锁:在竞争不激烈的情况下,性能通常优于 synchronizedReentrantLock
  • 避免死锁:无锁操作不会导致死锁。

示例:解决 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 接口提供了比 Objectwait(), 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(); // 生产者在 notFull 条件上等待
}
System.out.println("Producer produced: " + value);
queue.offer(value++);
notEmpty.signalAll(); // 队列不空,通知所有在 notEmpty 条件上等待的消费者
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(); // 消费者在 notEmpty 条件上等待
}
int value = queue.poll();
System.out.println("Consumer consumed: " + value);
notFull.signalAll(); // 队列不满,通知所有在 notFull 条件上等待的生产者
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(); // 主线程等待 workerThread 完成
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 {
// 创建一个固定大小为 3 的线程池
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();

// 等待所有任务完成,最多等待 1 分钟
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):指一个线程对共享变量的修改,对其他线程是立即可见的。volatilesynchronizedfinal 都可以保证可见性。
  • 有序性 (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 包)提供了大量高级的并发构建块,极大地简化了多线程编程。除了上面提到的 ExecutorServiceCallableFutureLockConditionAtomic 类,还有:

  • 并发集合ConcurrentHashMap (线程安全的 HashMap)、CopyOnWriteArrayList (写时复制列表)、BlockingQueue (阻塞队列,用于生产者-消费者模式)。
  • 同步器
    • CountDownLatch (倒计时门闩):允许一个或多个线程等待其他线程完成一组操作。
    • CyclicBarrier (循环屏障):允许多个线程相互等待,直到所有线程都到达一个公共屏障点。
    • Semaphore (信号量):控制同时访问特定资源的线程数量。
    • Exchanger (交换器):允许两个线程在某个点交换对象。

九、总结

Java 多线程编程是现代高性能、高并发应用开发的核心。它通过允许程序同时执行多个任务来充分利用硬件资源,提高系统的响应性和吞吐量。然而,多线程也带来了复杂的同步和并发问题。Java 提供了从底层 synchronizedvolatilejava.util.concurrent 包中丰富的高级工具,帮助开发者有效地管理线程、同步共享资源、协调线程间协作。深入理解线程的生命周期、同步机制(synchronizedLockAtomic)、线程间协作(wait/notifyCondition)以及 Java 内存模型 (JMM) 是编写健壮、高效并发程序的关键。正确选择和使用这些工具,能够构建出稳定且性能优异的并发应用。