多线程

发布于 2026-07-29 09:17 更新于 2026-07-29 09:17 3636 字 19 min read ... 访问量

本文系统介绍了多线程的基本概念、线程创建与管理、生命周期、线程安全与同步机制、死锁避免、线程协作模型以及线程池的使用。重点说明了线程与进程的区别、并发与并行的差异,详细讲解了synchronized、wait/notify等同步机制,强调了线程安全、死锁预防和线程池的合理配置,指出应避免使用sleep实现定时任务,推荐使用ScheduledExecutorService,并建议通过Future和awaitTermination等方法正确等待任务完成。

多线程

基本概念

进程

进程是正在运行的程序实例,拥有相对独立的内存空间和系统资源。操作系统负责调度多个进程在 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()
  • TERMINATEDrun() 正常结束,或者因未捕获异常而结束。

Java 状态中没有单独的 RUNNING 枚举,正在执行的线程也属于 RUNNABLE

image-001
image-001

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_PRIORITYThread.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()
所属类ObjectThread
是否要求持有监视器
是否释放监视器
唤醒方式通知、中断、超时超时或中断

生产者与消费者

下面使用容量受限的资源计数演示线程协作。

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 的主要构造参数如下:

  1. corePoolSize:核心线程数。

  2. maximumPoolSize:允许创建的最大线程数。

  3. keepAliveTime:非核心空闲线程的存活时间。

  4. unit:存活时间单位。

  5. workQueue:核心线程繁忙后保存待执行任务的队列。

  6. threadFactory:创建工作线程的工厂。

  7. handler:线程数达到上限且队列已满时的拒绝策略。

任务提交顺序

调用 execute() 提交任务时,主要流程如下:

  1. 当前工作线程少于核心线程数时,创建核心线程执行任务。

  2. 核心线程都在工作时,尝试把任务加入队列。

  3. 队列已满且线程数少于最大线程数时,创建非核心线程执行任务。

  4. 队列已满且线程数已经达到最大值时,执行拒绝策略。

  5. 非核心线程空闲超过存活时间后可以被回收。默认情况下核心线程不会因空闲被回收,除非启用核心线程超时。

拒绝策略

  • 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() 来等待线程池任务结束,应使用 FutureawaitTermination() 或其他明确的同步方式。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka