在计算机领域,多线程是提升程序性能、充分利用多核处理器的重要手段。Java从诞生之初就内置了对多线程的支持,提供了丰富的API和并发工具。然而,多线程编程也伴随着线程安全、死锁、性能损耗等挑战。本文将系统性地介绍Java多线程的核心概念、同步机制、高级并发工具以及最佳实践,帮助你构建可靠的高并发应用。
一、线程的基本概念
1.1 进程与线程
-
进程:操作系统资源分配的基本单位,拥有独立的内存空间。
-
线程:CPU调度的基本单位,是进程内的一个执行流,共享进程的内存空间。
Java中的线程通过 java.lang.Thread 类表示。每个线程都有自己的程序计数器、栈和局部变量,但堆内存和方法区是共享的。
1.2 线程的生命周期
Java线程在运行过程中会经历以下状态(定义在 Thread.State 枚举中):
-
NEW :线程对象已创建,但尚未调用
start()。 -
RUNNABLE:可运行状态,可能正在执行或等待CPU调度。
-
BLOCKED:等待获取监视器锁(synchronized)而被阻塞。
-
WAITING :无限期等待另一个线程执行特定操作(如
wait()、join()无超时)。 -
TIMED_WAITING :有限时等待(如
sleep(long)、wait(long)、join(long))。 -
TERMINATED:线程执行完毕。
状态转换关系如下:
java
NEW -> RUNNABLE -> TERMINATED
RUNNABLE <-> BLOCKED / WAITING / TIMED_WAITING
二、创建线程的三种方式
2.1 继承Thread类
重写 run() 方法,然后创建子类对象并调用 start()。
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
// 使用
MyThread t = new MyThread();
t.start();
缺点:Java单继承,无法再继承其他类。
2.2 实现Runnable接口
实现 Runnable 接口,将其作为参数传递给 Thread 对象。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
优点:解耦任务与线程,更灵活,推荐使用。
2.3 实现Callable接口与Future
Callable 可以有返回值,并抛出异常。通过 FutureTask 包装后提交给线程执行,或配合线程池使用。
java
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 42;
}
}
// 使用
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get(); // 阻塞获取结果
三、线程同步机制
多线程访问共享资源时,需要保证原子性、可见性和有序性。Java提供了多种同步机制。
3.1 synchronized关键字
-
同步方法:锁是当前实例对象(实例方法)或Class对象(静态方法)。
-
同步代码块:可以指定任意对象作为锁。
java
public class Counter {
private int count = 0;
public synchronized void increment() { // 实例锁
count++;
}
public void decrement() {
synchronized (this) { // 等价于上面的方法锁
count--;
}
}
}
synchronized 保证了原子性和可见性,但在高竞争时可能引入性能问题。
3.2 volatile关键字
volatile 保证变量的可见性,禁止指令重排序,但不保证原子性。适用于单个变量的读写操作,如标志位。
java
volatile boolean flag = true;
3.3 Lock接口与ReentrantLock
java.util.concurrent.locks.Lock 提供了比 synchronized 更灵活的锁操作,如尝试锁、可中断锁、公平锁等。
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
ReentrantLock 支持公平锁(通过构造函数指定),并且可以配合 Condition 实现更灵活的等待/通知。
3.4 原子类
java.util.concurrent.atomic 包提供了原子操作类,如 AtomicInteger、AtomicReference 等,利用CAS(Compare-And-Swap)实现无锁并发。
java
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子自增
四、线程间通信
4.1 wait/notify/notifyAll
这些方法必须在 synchronized 块内调用,配合对象监视器使用。
java
synchronized (lock) {
while (condition) {
lock.wait(); // 释放锁,进入等待
}
// 条件满足,执行操作
lock.notifyAll(); // 唤醒等待线程
}
注意 :始终在循环中使用 wait(),防止虚假唤醒。
4.2 Condition
Condition 是 Lock 的等待/通知机制,可以创建多个等待队列,比 wait/notify 更精细。
java
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (conditionFlag) {
condition.await();
}
condition.signalAll();
} finally {
lock.unlock();
}
4.3 管道流(PipedInputStream/PipedOutputStream)
用于线程间的字节流或字符流通信,但实际使用较少。
五、高级并发工具(JUC)
java.util.concurrent 包提供了大量现成的并发组件,极大地简化了并发编程。
5.1 CountDownLatch
允许一个或多个线程等待其他线程完成操作。计数器递减到零时,等待线程被唤醒。
java
CountDownLatch latch = new CountDownLatch(3);
// 工作线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
}
latch.await(); // 主线程等待所有工作线程完成
5.2 CyclicBarrier
让一组线程到达一个屏障点时被阻塞,直到最后一个线程到达,所有线程才继续执行。可重用。
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程都到了,开始下一阶段");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 阶段1工作
barrier.await();
// 阶段2工作
}).start();
}
5.3 Semaphore
控制同时访问特定资源的线程数量,相当于许可证。
java
Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
semaphore.acquire(); // 获取许可,若不足则阻塞
try {
// 访问共享资源
} finally {
semaphore.release();
}
5.4 Exchanger
用于两个线程之间交换数据。
java
Exchanger<String> exchanger = new Exchanger<>();
// 线程A
String data = "A数据";
String result = exchanger.exchange(data);
// 线程B
String data = "B数据";
String result = exchanger.exchange(data);
5.5 CompletableFuture
Java 8引入的异步编程工具,支持函数式组合,可以轻松编排异步任务。
java
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println);
六、线程池(Executor框架)
频繁创建销毁线程开销巨大,线程池通过复用线程提升性能。ExecutorService 是线程池的主要接口。
6.1 创建线程池
可以通过 Executors 工厂方法创建预定义线程池,但更推荐直接使用 ThreadPoolExecutor 以精确控制参数。
java
// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 缓存线程池(无限大,空闲60秒回收)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 自定义线程池
ThreadPoolExecutor customPool = new ThreadPoolExecutor(
2, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
6.2 提交任务
-
execute(Runnable):无返回值。 -
submit(Runnable/Callable):返回Future,可获取结果或异常。
java
Future<Integer> future = pool.submit(() -> {
Thread.sleep(1000);
return 100;
});
Integer result = future.get(); // 阻塞
6.3 关闭线程池
-
shutdown():不再接受新任务,已提交任务继续执行。 -
shutdownNow():尝试中断正在执行的任务,并返回未开始的任务列表。
java
pool.shutdown();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
七、并发集合
JUC提供了线程安全的集合,避免手动同步:
-
ConcurrentHashMap:分段锁或CAS实现的高并发Map。
-
CopyOnWriteArrayList:写时复制,适合读多写少场景。
-
BlockingQueue :阻塞队列,用于生产者-消费者模式(如
ArrayBlockingQueue、LinkedBlockingQueue)。 -
ConcurrentLinkedQueue:非阻塞队列,基于CAS。
-
ConcurrentSkipListMap/Set:基于跳表的有序集合。
八、死锁与避免
8.1 死锁产生的四个必要条件
-
互斥条件:资源一次只能被一个线程占用。
-
持有并等待:线程持有至少一个资源,并等待其他资源。
-
非抢占:资源只能由持有者主动释放。
-
循环等待:线程间形成循环等待链。
8.2 避免死锁的策略
-
打破循环等待:统一资源获取顺序。
-
使用
tryLock超时:获取锁失败时释放已持有的锁。 -
使用
open call原则:调用外部方法时不持有锁。 -
使用更高层次的并发工具 (如
ConcurrentHashMap、CountDownLatch)替代手动锁。
九、线程安全与最佳实践
-
优先使用不可变对象:对象一旦创建,状态不可变,天然线程安全。
-
缩小同步范围:仅在必要的地方加锁,使用同步代码块代替同步方法。
-
使用并发工具而非
wait/notify:JUC工具更可靠、易用。 -
避免线程泄露:线程池使用后必须关闭。
-
善用
ThreadLocal:维护线程本地变量,避免共享。 -
充分测试并发代码:使用工具(如JMH、JCStress)进行压力测试和正确性验证。
十、总结
Java多线程编程从基础的 Thread 到高级的 CompletableFuture,提供了丰富的抽象和工具。掌握线程的生命周期、同步机制以及JUC中的并发组件,是构建高性能、高可用系统的基石。在实际开发中,应根据场景选择合适的并发策略,避免过度优化或忽视线程安全。并发编程虽复杂,但通过系统学习和实践,完全可以驾驭。