多线程
基本概念
进程
进程是正在运行的程序实例,拥有相对独立的内存空间和系统资源。操作系统负责调度多个进程在 CPU 上执行。
线程
线程是进程中的执行单元。一个进程可以包含多个线程,这些线程共享进程中的堆内存和部分资源,但每个线程都有自己的程序计数器、虚拟机栈等线程私有数据。
多个线程在单核 CPU 上通常交替执行,在多核 CPU 上可能真正并行执行。线程的具体调度由操作系统和 JVM 共同配合完成,程序不能依赖固定执行顺序。
并发与并行
- 并发表示多个任务在一段时间内交替推进。
- 并行表示多个任务在同一时刻分别由不同处理器核心执行。
创建线程
继承 Thread 类
定义 Thread 子类并重写 run(),再调用 start() 启动线程。
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + " 执行第 " + i + " 次任务");
}
}
}public class Main {
public static void main(String[] args) {
Thread thread1 = new MyThread("A");
Thread thread2 = new MyThread("B");
Thread thread3 = new MyThread("C");
thread1.start();
thread2.start();
thread3.start();
}
}直接调用 run() 只是普通方法调用,不会创建新的执行线程。一个 Thread 对象只能成功调用一次 start()。
实现 Runnable 接口
把任务与线程对象分离是更常用的方式。任务类实现 Runnable,再把任务对象传给 Thread。
public class TicketTask implements Runnable {
private final String stationName;
public TicketTask(String stationName) {
this.stationName = stationName;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(
stationName + ",窗口 "
+ Thread.currentThread().getName()
+ " 执行第 " + i + " 次任务"
);
}
}
}public class Main {
public static void main(String[] args) {
Runnable task1 = new TicketTask("哈西站");
Runnable task2 = new TicketTask("哈尔滨站");
new Thread(task1, "A").start();
new Thread(task1, "B").start();
new Thread(task2, "C").start();
}
}使用 Runnable 的优点如下:
- 任务类仍然可以继承其他类。
- 同一个任务对象可以交给多个线程执行,便于共享任务状态。
- 任务与线程生命周期分离,更容易交给线程池管理。
共享任务对象时也会共享其中的可变字段,因此必须考虑线程安全。
线程生命周期
Java Thread.State 定义了六种线程状态:
NEW:线程对象已创建,但尚未调用start()。RUNNABLE:线程正在运行,或者已经具备运行条件并等待 CPU 调度。BLOCKED:等待进入某个synchronized监视器。WAITING:无限期等待其他线程执行特定操作,例如无超时的wait()或join()。TIMED_WAITING:在指定时间内等待,例如sleep()、带超时的wait()或join()。TERMINATED:run()正常结束,或者因未捕获异常而结束。
Java 状态中没有单独的 RUNNING 枚举,正在执行的线程也属于 RUNNABLE。

Thread 常用方法
currentThread()
返回当前正在执行代码的线程对象。
System.out.println(Thread.currentThread().getName());
sleep()
让当前线程至少休眠指定时间,进入 TIMED_WAITING。实际恢复时间可能晚于指定时间。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
收到中断时,sleep() 会抛出 InterruptedException 并清除中断标记。无法继续处理时,通常应恢复中断状态,而不是只打印异常。
定时任务不应通过无限循环加长时间 sleep() 粗略实现,推荐使用 ScheduledExecutorService。
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
() -> System.out.println("执行备份任务"),
0,
24,
TimeUnit.HOURS
);
如果要求每天固定在当地时间执行,还需要根据当前时间计算首次延迟,并考虑时区和夏令时。
yield()
Thread.yield() 向调度器提示当前线程愿意让出执行机会,但调度器可以忽略该提示。它不能用于实现可靠的线程协作。
join()
当前线程调用另一个线程的 join() 后,会等待目标线程结束。
public class Main {
public static void main(String[] args) {
Runnable task = () -> System.out.println(
Thread.currentThread().getName() + " 完成任务"
);
Thread thread1 = new Thread(task, "A");
Thread thread2 = new Thread(task, "B");
Thread thread3 = new Thread(task, "C");
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("所有任务执行结束");
}
}守护线程
调用 setDaemon(true) 可以在线程启动前把它设置为守护线程。
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 提供后台服务
}
});
worker.setDaemon(true);
worker.start();
当 JVM 中只剩守护线程时,JVM 可以直接退出。守护线程不是专门绑定并守护某一个普通线程,也不能依赖它执行必须完成的保存或清理工作。
线程优先级
setPriority() 设置线程优先级提示,范围为 Thread.MIN_PRIORITY 到 Thread.MAX_PRIORITY。不同操作系统和 JVM 对优先级的处理不同,不能用它保证执行顺序或公平性。
线程安全与竞态条件
多个线程同时读写共享可变数据时,如果操作缺少正确同步,结果可能取决于不可预测的执行顺序,这称为竞态条件。
例如,money += 1000 并不是不可分割的单一步骤,它通常包含读取、计算和写回。
synchronized 关键字
synchronized 使用对象监视器实现互斥和可见性。进入同步区域前,线程必须先获得对应监视器;退出同步区域时释放监视器。
同步实例方法
同步实例方法锁定当前对象,也就是 this。
public class Account {
private int money = 20_000;
public synchronized void deposit(int amount) {
money += amount;
System.out.println("存款后余额:" + money);
}
public synchronized void withdraw(int amount) {
if (money < amount) {
throw new IllegalStateException("余额不足");
}
money -= amount;
System.out.println("取款后余额:" + money);
}
}只有锁定同一个对象的同步代码之间才会互斥。如果两个线程调用的是两个不同 Account 对象的方法,它们使用的不是同一把锁。
同步代码块
同步代码块可以缩小锁的范围,并明确指定锁对象。
public void deposit(int amount) {
synchronized (this) {
money += amount;
System.out.println("存款后余额:" + money);
}
}
锁对象应是所有相关线程都能访问到的稳定对象,不能随意更换。
同步静态方法
同步静态方法锁定当前类对应的 Class 对象。
public static synchronized void updateGlobalCount() {
count++;
}
它与下面的写法使用同一把锁:
synchronized (Account.class) {
count++;
}
实例同步方法与静态同步方法使用不同监视器,默认不会互相阻塞。
sleep() 与锁
线程在同步区域中调用 sleep() 时不会释放已经持有的监视器,因此不应在持锁期间进行不必要的长时间等待或耗时 IO。
synchronized 的性能
现代 JVM 会对锁进行多种优化,具体实现策略会随 JDK 版本变化。不能把 synchronized 一概视为固定的“重量级锁”,也不应依赖某个版本的偏向锁、轻量级锁等内部阶段编写业务逻辑。
CAS 是一种基于比较并交换的原子更新思想,AtomicInteger 等原子类会使用它或等价机制实现部分无锁操作。CAS 不能替代所有锁,复杂复合状态仍需要合适的同步方案。
死锁
死锁是多个线程互相等待对方持有的资源,导致所有相关线程都无法继续执行。
public class DeadLockTask implements Runnable {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
private final boolean firstA;
public DeadLockTask(boolean firstA) {
this.firstA = firstA;
}
@Override
public void run() {
if (firstA) {
synchronized (LOCK_A) {
synchronized (LOCK_B) {
System.out.println("A -> B");
}
}
} else {
synchronized (LOCK_B) {
synchronized (LOCK_A) {
System.out.println("B -> A");
}
}
}
}
}避免死锁
- 所有线程按照统一顺序获取多把锁。
- 减少锁的嵌套和持有时间。
- 不在持锁时执行外部调用、网络 IO 或不可控阻塞操作。
- 使用
Lock.tryLock()配合超时,在获取失败时释放已经持有的锁并重试。 - 使用线程转储、监控工具和
ThreadMXBean检测死锁。
普通 synchronized 锁等待不能通过中断直接取消,因此“发现死锁后中断线程”不一定能解除死锁。
wait()、notify() 和 notifyAll()
这三个方法定义在 Object 类中,用于基于同一个对象监视器进行线程协作。
使用规则
- 调用前必须持有该对象的监视器,否则抛出
IllegalMonitorStateException。 wait()让当前线程进入等待状态,并释放该对象监视器。notify()唤醒在该监视器等待集合中的一个线程,具体线程不确定。notifyAll()唤醒所有在该监视器上等待的线程。- 被唤醒的线程还必须重新竞争监视器,获得后才能从
wait()返回。
条件判断应使用 while,因为线程可能被错误唤醒,或者条件在重新获得锁前再次改变。
wait() 与 sleep() 的区别
| 对比项 | wait() | sleep() |
|---|---|---|
| 所属类 | Object | Thread |
| 是否要求持有监视器 | 是 | 否 |
| 是否释放监视器 | 是 | 否 |
| 唤醒方式 | 通知、中断、超时 | 超时或中断 |
生产者与消费者
下面使用容量受限的资源计数演示线程协作。
public class Resource {
private final int capacity = 20;
private int count;
public synchronized void produce() {
while (count == capacity) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
count++;
System.out.println("生产一个,当前数量:" + count);
notifyAll();
}
public synchronized void consume() {
while (count == 0) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
count--;
System.out.println("消费一个,当前数量:" + count);
notifyAll();
}
}Resource resource = new Resource();
Runnable producer = () -> {
for (int i = 0; i < 100; i++) {
resource.produce();
}
};
Runnable consumer = () -> {
for (int i = 0; i < 100; i++) {
resource.consume();
}
};
new Thread(producer, "生产者 1").start();
new Thread(producer, "生产者 2").start();
new Thread(consumer, "消费者 1").start();
new Thread(consumer, "消费者 2").start();真实项目中,生产者消费者模型通常优先使用 BlockingQueue,它已经封装了等待、通知和容量控制。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.put("task");
String task = queue.take();
线程池
频繁创建和销毁线程会消耗系统资源。线程池复用工作线程、限制并发数量,并统一管理任务队列和拒绝策略。
少量、生命周期明确的线程可以直接创建;需要持续处理大量短任务的服务器程序通常使用线程池。
ThreadPoolExecutor
核心参数
ThreadPoolExecutor 的主要构造参数如下:
-
corePoolSize:核心线程数。 -
maximumPoolSize:允许创建的最大线程数。 -
keepAliveTime:非核心空闲线程的存活时间。 -
unit:存活时间单位。 -
workQueue:核心线程繁忙后保存待执行任务的队列。 -
threadFactory:创建工作线程的工厂。 -
handler:线程数达到上限且队列已满时的拒绝策略。
任务提交顺序
调用 execute() 提交任务时,主要流程如下:
-
当前工作线程少于核心线程数时,创建核心线程执行任务。
-
核心线程都在工作时,尝试把任务加入队列。
-
队列已满且线程数少于最大线程数时,创建非核心线程执行任务。
-
队列已满且线程数已经达到最大值时,执行拒绝策略。
-
非核心线程空闲超过存活时间后可以被回收。默认情况下核心线程不会因空闲被回收,除非启用核心线程超时。
拒绝策略
AbortPolicy:抛出RejectedExecutionException。CallerRunsPolicy:由提交任务的调用线程执行任务,用于提供反压。DiscardOldestPolicy:丢弃队列中等待最久的任务,再尝试提交当前任务。DiscardPolicy:直接丢弃当前任务。
丢弃任务可能造成业务数据丢失,必须根据任务重要性选择策略并记录监控信息。
自定义线程池示例
public class TestTask implements Runnable {
private final int number;
public TestTask(int number) {
this.number = number;
}
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(
Thread.currentThread().getName()
+ " 执行任务 " + number
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
5,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 10; i++) {
executor.execute(new TestTask(i));
}
executor.shutdown();shutdown() 不再接收新任务,但会继续执行已经提交的任务。shutdownNow() 会尝试中断正在执行的任务,并返回尚未开始的队列任务,但不能保证任务立即停止。
Executors 提供的常用线程池
Executors 提供便捷工厂方法,但部分方法使用无界队列或近似无界线程数。服务器程序应了解其内部参数,并根据负载显式配置线程池。
FixedThreadPool
创建固定数量工作线程,通常使用无界队列保存多余任务。
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(new TestTask(i));
}
executor.shutdown();
任务提交速度长期高于处理速度时,无界队列可能持续增长并占用大量内存。
CachedThreadPool
根据任务量创建和复用线程,空闲线程通常保留约 60 秒。最大线程数非常大,适合执行时间短、提交量可控的任务,不适合没有并发上限的外部请求。
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new TestTask(1));
executor.shutdown();
ScheduledThreadPool
用于延迟任务和周期任务。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(
new TestTask(1),
5,
TimeUnit.SECONDS
);
executor.shutdown();
周期任务可以使用 scheduleAtFixedRate() 或 scheduleWithFixedDelay(),二者对任务执行时间的处理方式不同。
SingleThreadExecutor
始终使用单个工作线程按队列顺序执行任务。工作线程异常结束时,线程池会创建替代线程继续执行后续任务。
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.execute(new TestTask(i));
}
executor.shutdown();
它适合要求任务串行执行的场景,但无界队列同样可能积压任务。
WorkStealingPool
newWorkStealingPool() 基于 ForkJoinPool,工作线程可以窃取其他队列中的任务,适合能够拆分、彼此相对独立的计算任务。
ExecutorService executor = Executors.newWorkStealingPool(5);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(executor.submit(new TestTask(i)));
}
for (Future<?> future : futures) {
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (ExecutionException e) {
e.getCause().printStackTrace();
}
}
executor.shutdown();任务执行顺序不固定。不要通过让主线程长时间 sleep() 来等待线程池任务结束,应使用 Future、awaitTermination() 或其他明确的同步方式。
喜欢的话,留下你的评论吧~