目录
- 为什么需要多线程?
- Java 多线程核心概念
- 进程 vs. 线程
- 并行 vs. 并发
- 上下文切换
- 创建和启动线程
- 继承
Thread类 - 实现
Runnable接口 - 实现
Callable接口 (返回结果,可抛出异常) - 三种方法的对比与选择
- 继承
Thread类的核心方法- 线程的生命周期
- 控制线程的方法 (
start(),run(),sleep(),join(),yield(),interrupt())
- 线程同步
- 为什么需要同步?(可见性、原子性、有序性问题)
synchronized关键字volatile关键字Lock接口 (如ReentrantLock)
- 线程间通信
wait(),notify(),notifyAll()
- 线程池 (
ExecutorService)- 为什么需要线程池?
ExecutorService的使用Executors工厂类
- 现代异步编程:
CompletableFuture
为什么需要多线程?
在单线程程序中,代码是顺序执行的,如果一个任务非常耗时(如网络请求、文件读写、复杂计算),整个程序都会被阻塞,直到这个任务完成,无法响应用户的其他操作。

多线程的主要目的就是并发执行,让程序能够同时处理多个任务,从而:
- 提高 CPU 利用率:当一个线程 I/O 阻塞时,CPU 可以切换到其他就绪的线程去执行,而不是空闲等待。
- 提升程序响应速度:在 GUI 应用中,可以将耗时操作放在后台线程,避免界面卡死。
- 简化程序模型:对于一些可以分解为多个独立子任务的问题,用多线程来处理逻辑更清晰。
Java 多线程核心概念
进程 vs. 线程
- 进程:是操作系统进行资源分配和调度的基本单位,一个进程可以包含一个或多个线程,进程之间内存空间是独立的。
- 线程:是进程中的一个执行流,是 CPU 调度的基本单位,同一进程内的线程共享该进程的内存空间(堆内存和方法区),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
简单比喻:一个进程就像一个工厂(进程),工厂里有多个车间(线程),它们共享原材料(堆内存),但每个车间有自己的工作台(栈内存)。
并行 vs. 并发
- 并发:指两个或多个任务在同一时间间隔内宏观上同时运行,但在微观上是交替执行的,在单核 CPU 上,通过快速切换线程,让用户感觉它们在同时运行。
- 并行:指两个或多个任务在同一时间真正地同时运行,在多核 CPU 上,不同的任务可以分配到不同的核心上同时执行。
上下文切换
CPU 只能同时执行一个线程,当操作系统需要从当前线程切换到另一个线程时,需要保存当前线程的执行状态(如程序计数器、寄存器等),并加载另一个线程的状态,这个过程称为上下文切换,频繁的上下文切换会带来性能开销。
创建和启动线程
Java 中创建线程主要有三种方式。

继承 Thread 类
这是最简单的方式,但缺点是 Java 不支持多重继承,如果一个类已经继承了另一个类,就无法再继承 Thread。
// 1. 定义一个类继承 Thread 类
class MyThread extends Thread {
// 2. 重写 run() 方法,这是线程要执行的任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + Thread.currentThread().getName() + " is running, i = " + i);
try {
Thread.sleep(500); // 休眠 500 毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 4. 启动线程 (调用 start() 方法,而不是 run())
thread1.start();
thread2.start();
System.out.println("Main thread is running.");
}
}
关键点:
- 必须调用
start()方法来启动线程。start()会告诉 JVM 创建一个新的线程,并在这个新线程中调用run()方法。 - 如果直接调用
run(),它就只是一个普通的方法调用,不会创建新线程,会在当前线程中顺序执行。
实现 Runnable 接口
这是更常用、更灵活的方式,它将任务(run 方法)和线程(Thread 类)分离开。
// 1. 定义一个类实现 Runnable 接口
class MyRunnable implements Runnable {
// 2. 实现 run() 方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable Thread " + Thread.currentThread().getName() + " is running, i = " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 3. 创建 Runnable 实例
MyRunnable runnable = new MyRunnable();
// 4. 创建 Thread 对象,并将 Runnable 实例作为参数传入
Thread thread1 = new Thread(runnable, "Thread-A");
Thread thread2 = new Thread(runnable, "Thread-B");
// 5. 启动线程
thread1.start();
thread2.start();
System.out.println("Main thread is running.");
}
}
优点:

- 避免了单继承的限制。
- 将任务和线程解耦,多个线程可以共享同一个
Runnable实例。
实现 Callable 接口
Callable 是 Java 1.5 引入的,功能类似于 Runnable,但它更强大:
call()方法可以返回一个结果。call()方法可以抛出异常。
Callable 通常与 Future 和 FutureTask 配合使用,用于获取异步执行的结果。
import java.util.concurrent.*;
// 1. 定义一个类实现 Callable 接口
// 泛型指定 call() 方法的返回类型
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
System.out.println("Callable Thread " + Thread.currentThread().getName() + " is running, sum = " + sum);
Thread.sleep(500);
}
return sum; // 返回计算结果
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 2. 创建 Callable 实例
MyCallable callable = new MyCallable();
// 3. 创建 FutureTask 对象,将 Callable 实例传入
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 4. 创建 Thread 对象,将 FutureTask 对象作为参数传入
Thread thread = new Thread(futureTask, "Callable-Thread");
thread.start();
// 5. 主线程可以继续做其他事情...
System.out.println("Main thread is doing other work...");
// 6. 获取线程执行的结果 (这会阻塞主线程,直到子线程执行完毕)
Integer result = futureTask.get();
System.out.println("The result from Callable is: " + result);
}
}
三种方法的对比与选择
| 特性 | 继承 Thread 类 |
实现 Runnable 接口 |
实现 Callable 接口 |
|---|---|---|---|
| 优点 | 编写简单,this 即代表当前线程 |
解耦任务与线程,支持共享资源 | 可以返回结果,可以抛出异常 |
| 缺点 | 继承受限,任务与线程耦合 | 获取结果不方便 (需共享变量) | 代码稍复杂,需配合 Future 使用 |
| 适用场景 | 简单的、不需要共享任务实例的线程任务 | 最常用,推荐首选 | 需要获取异步执行结果的场景 |
| 返回值 | 无 | 无 | 有 (通过 Future.get() 获取) |
| 异常处理 | 在 run 方法内部用 try-catch |
在 run 方法内部用 try-catch |
可以直接在 call 方法中 throws |
选择建议:
- 优先选择
Runnable,因为它更灵活、更符合面向对象的设计原则。 - 如果需要从线程中获取返回值,使用
Callable。 - 除非有特殊且简单的需求,否则尽量避免直接继承
Thread。
Thread 类的核心方法
线程的生命周期
一个线程从创建到销毁,会经历以下状态(Java 5 引入 java.lang.Thread.State 枚举):
- NEW (新建):线程被创建,但尚未调用
start()方法。 - RUNNABLE (运行):线程调用了
start()方法,此时它可能正在运行,也可能在等待 CPU 时间片,在操作系统中,它处于“就绪”或“运行”状态。 - BLOCKED (阻塞):线程因为等待监视器锁(即进入
synchronized代码块或方法)而阻塞。 - WAITING (等待):线程进入等待状态,直到其他线程调用
notify()或notifyAll()才能唤醒,调用Object.wait()、Thread.join()。 - TIMED_WAITING (超时等待):和
WAITING类似,但它可以在指定时间后自动唤醒,调用Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)。 - TERMINATED (终止):线程执行完毕或因异常退出。
控制线程的方法
void start(): 启动线程,JVM 会调用该线程的run()方法。void run(): 线程的执行体,继承Thread时需要重写此方法。static void sleep(long millis): 让当前线程休眠指定的毫秒数,进入TIMED_WAITING状态,会抛出InterruptedException。void join(): 等待该线程终止。thread1.join()会让当前线程(如主线程)等待thread1执行完毕。static void yield(): 提示当前线程让出 CPU,给其他线程一个执行的机会,这只是一个“建议”,操作系统不一定会采纳。void interrupt(): 中断线程,它不会强制终止线程,而是设置一个中断状态,线程可以通过isInterrupted()或Thread.interrupted()检查状态,并决定如何响应(如优雅地结束循环)。boolean isAlive(): 测试线程是否处于活动状态(RUNNABLE,BLOCKED,WAITING,TIMED_WAITING)。
线程同步
当多个线程同时访问和修改共享数据时,可能会导致数据不一致、程序错误等问题,这就是线程安全问题。
为什么需要同步?
- 原子性:一个或多个操作要么全部执行成功,要么全部不执行。
i++不是原子操作,它包含“读取-修改-写入”三个步骤。 - 可见性:一个线程对共享变量的修改,对其他线程是可见的,由于 CPU 缓存的存在,一个线程修改的值可能不会立即写回主内存,导致其他线程看不到最新的值。
- 有序性:程序的执行顺序应该按照代码的先后顺序执行,编译器和处理器可能会为了优化而对指令进行重排序。
synchronized 关键字
synchronized 是 Java 内置的锁机制,可以保证原子性、可见性和有序性。
它有两种用法:
- 修饰实例方法:锁是当前对象实例(
this),同一时间只有一个线程能进入该对象的这个同步方法。public synchronized void increment() { count++; } - 修饰静态方法:锁是当前类的
Class对象,同一时间只有一个线程能进入该类的这个同步静态方法。public static synchronized void decrement() { count--; } - 修饰代码块:可以指定锁对象,灵活性更高。
public void increment() { synchronized (this) { // 锁是 this count++; } }
示例:不安全的计数器
class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
public int getCount() {
return count;
}
}
public class UnsafeDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// 预期结果是 2000,但实际结果通常小于 2000
System.out.println("Final count: " + counter.getCount());
}
}
示例:使用 synchronized 修复
class SafeCounter {
private int count = 0;
// 使用 synchronized 保证 increment 的原子性
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
// ... main 方法同上,但使用 SafeCounter ...
// 最终输出会是 2000
volatile 关键字
volatile 关键字比 synchronized 更轻量级,它主要用于保证可见性和禁止指令重排序。
- 保证可见性:当一个线程修改了
volatile变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。 - 不保证原子性:
volatile不能保证i++这样的复合操作是原子的。 - 禁止指令重排序:可以防止 JVM 和 CPU 为了优化而对代码进行重排序。
适用场景:一个线程写,多个线程读的共享变量,状态标志位。
class VolatileExample {
// volatile 保证了 flag 的可见性
private volatile boolean flag = false;
public void writer() {
flag = true; // 对 flag 的修改对其他线程立即可见
}
public void reader() {
while (!flag) {
// 等待 flag 变为 true
}
System.out.println("Flag is now true!");
}
}
Lock 接口 (java.util.concurrent.locks)
synchronized 是隐式锁,而 Lock 提供了更灵活的显式锁,最常用的是 ReentrantLock。
ReentrantLock vs synchronized:
- 功能:
ReentrantLock提供了更强大的功能,如可中断的锁获取、可轮询的锁获取、公平锁等。 - 使用方式:
synchronized自动释放锁,而Lock必须在finally块中手动调用unlock()来释放锁,否则会导致死锁。 - 性能:在竞争不激烈时,两者性能相当,在竞争激烈时,
ReentrantLock的性能可能更好。
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++; // 临界区代码
} finally {
lock.unlock(); // 在 finally 中确保锁一定会被释放
}
}
public int getCount() {
return count;
}
}
线程间通信
wait(), notify(), notifyAll() 是 Object 类的方法,它们用于实现线程间的等待/通知机制,通常配合 synchronized 使用。
void wait(): 让当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法,调用wait()的线程会释放锁。void notify(): 唤醒在此对象监视器上等待的单个线程(选择哪个线程是随机的)。void notifyAll(): 唤醒在此对象监视器上等待的所有线程。
经典示例:生产者-消费者模型
class SharedResource {
private int item = 0;
private boolean isProduced = false;
// 生产者
public synchronized void produce() throws InterruptedException {
// 如果已经有产品,就等待消费者消费
while (isProduced) {
wait();
}
item++;
System.out.println("Produced: " + item);
isProduced = true;
notifyAll(); // 唤醒可能在等待的消费者
}
// 消费者
public synchronized void consume() throws InterruptedException {
// 如果没有产品,就等待生产者生产
while (!isProduced) {
wait();
}
System.out.println("Consumed: " + item);
isProduced = false;
notifyAll(); // 唤醒可能在等待的生产者
}
}
class Producer implements Runnable {
private SharedResource resource;
public Producer(SharedResource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
resource.produce();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private SharedResource resource;
public Consumer(SharedResource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
resource.consume();
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
new Thread(new Producer(resource)).start();
new Thread(new Consumer(resource)).start();
}
}
注意:wait() 必须在 synchronized 代码块或方法中调用,否则会抛出 IllegalMonitorStateException,通常使用 while 循环来检查条件,而不是 if,以防虚假唤醒。
线程池 (ExecutorService)
频繁地创建和销毁线程是非常消耗资源的,线程池就是为了解决这个问题而设计的,它预先创建一组线程,当有任务需要执行时,就从线程池中取出一个线程来执行,任务执行完毕后,线程不会销毁,而是返回线程池中,等待下一个任务。
为什么需要线程池?
- 降低资源消耗:减少创建和销毁线程的开销。
- 提高响应速度:任务到达时,无需等待创建线程即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,线程池可以统一地分配、调优和管理。
ExecutorService 的使用
ExecutorService 是线程池的核心接口。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建一个固定大小的线程池 (2 个线程)
ExecutorService executor = Executors.newFixedThreadPool(2);
// 2. 提交任务到线程池
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2); // 模拟耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is finished.");
});
}
// 3. 关闭线程池
// shutdown() 会停止接受新任务,但会执行完队列中已有的任务
executor.shutdown();
// 检查线程池是否已经关闭
try {
// 等待所有任务完成,最多等待 1 小时
if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
System.out.println("Some tasks were not terminated.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished.");
}
}
Executors 工厂类
Executors 提供了多种创建线程池的便捷方法:
newFixedThreadPool(int n): 创建一个固定大小的线程池。newCachedThreadPool(): 创建一个可缓存的线程池,如果线程池大小超过处理任务所需的线程,就会回收空闲线程;如果需求增加,则会创建新线程。newSingleThreadExecutor(): 创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证任务按顺序执行。newScheduledThreadPool(int corePoolSize): 创建一个可以执行延迟任务或周期性任务的线程池。
注意:阿里巴巴 Java 开发手册中明确指出,不允许使用
Executors创建线程池,而是要通过ThreadPoolExecutor的方式创建,以避免资源耗尽的风险。Executors创建的线程池在极端情况下(如大量任务)可能会导致 OOM。
现代异步编程:CompletableFuture
CompletableFuture 是 Java 8 引入的一个强大的异步编程工具,它是对 Future 的增强。Future 提供了一种异步计算的方式,但它使用起来不够方便,无法对计算结果进行链式处理和组合。
CompletableFuture 的特点:
- 非阻塞:可以链式调用方法,对结果进行处理。
- 函数式编程:支持
Function,Consumer,Runnable等函数式接口。 - 组合能力:可以组合多个
CompletableFuture,实现复杂的异步流程。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建一个异步任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
});
// 2. 链式处理结果
CompletableFuture<String> resultFuture = future
.thenApply(s -> s + " World") // 上一步成功后,对结果进行处理
.thenApply(s -> s + "!");
// 3. 阻塞获取最终结果
System.out.println("Final result: " + resultFuture.get()); // 会阻塞直到结果完成
// 4. 组合多个 CompletableFuture
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
// 当 future1 和 future2 都完成后,将它们的结果相加
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (a, b) -> a + b);
System.out.println("Combined result: " + combinedFuture.get());
}
}
| 概念/技术 | 核心作用 | 关键点/用法 |
|---|---|---|
| Thread | 线程的底层实现 | start(), run(), sleep(), join() |
| Runnable | 定义任务,与线程解耦 | 实现 run() 方法,传递给 Thread |
| Callable/Future | 异步任务,可返回结果 | 实现 call() 方法,用 Future.get() 获取结果 |
| 同步 | 解决线程安全问题 | synchronized (隐式锁), ReentrantLock (显式锁), volatile (可见性) |
| 线程通信 | 线程间协作 | wait(), notify(), notifyAll() (配合 synchronized) |
| 线程池 | 复用线程,提高性能,管理线程 | ExecutorService, Executors (或 ThreadPoolExecutor) |
| 异步编程 | 现代、非阻塞的异步处理方式 | CompletableFuture (链式调用、组合) |
掌握 Java 多线程是一个循序渐进的过程,建议从 Runnable 和 synchronized 开始,逐步学习线程池和 CompletableFuture,并深刻理解线程同步和通信的原理,这样才能编写出高效、健壮的并发程序。
