目录
- 为什么需要多线程?
- Java 线程的核心概念
- 创建和启动线程的三种方式
- 线程的生命周期(状态)
- 线程同步
synchronized关键字volatile关键字Lock接口(ReentrantLock)
- 线程间通信
wait(),notify(),notifyAll()join()方法
- 线程池
- 为什么需要线程池?
Executor框架ThreadPoolExecutor详解
- 高级并发工具
CountDownLatch,CyclicBarrier,SemaphoreConcurrentHashMapThreadLocal
- 总结与最佳实践
为什么需要多线程?
多线程的主要目的是为了提高程序的执行效率和改善用户体验。

- 提高 CPU 利用率:在单核 CPU 中,多线程通过“时间片轮转”的方式,让 CPU 在多个线程之间快速切换,当一个线程因为 I/O 操作(如读写文件、网络请求)而阻塞时,CPU 可以立即切换到另一个线程,从而避免了 CPU 空闲。
- 提高程序响应速度:在图形用户界面程序中,可以将耗时操作(如下载文件、复杂计算)放到后台线程执行,避免阻塞主线程(UI 线程),保持界面的流畅和可交互性。
- 简化编程模型:对于某些需要同时处理多个任务的场景(如 Web 服务器同时处理多个客户端请求),多线程可以将一个复杂的问题分解为多个相对独立的子任务,每个子任务由一个线程处理,使程序结构更清晰。
Java 线程的核心概念
- 进程:是操作系统进行资源分配和调度的基本单位,一个进程可以包含一个或多个线程。
- 线程:是进程中的一个执行流,是 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);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 4. 启动线程 (注意:不是调用 run() 方法)
thread1.start();
thread2.start();
}
}
实现 Runnable 接口(推荐)
这种方式更灵活,因为 Java 可以实现多个接口。Runnable 只是一个任务的定义,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.currentThread().getName() + " is running, i = " + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 3. 创建 Runnable 实例
MyRunnable myRunnable = new MyRunnable();
// 4. 创建 Thread 对象,并将 Runnable 实例作为参数传入
Thread thread1 = new Thread(myRunnable, "Thread-A");
Thread thread2 = new Thread(myRunnable, "Thread-B");
// 5. 启动线程
thread1.start();
thread2.start();
}
}
实现 Callable 接口(推荐,功能更强大)
Callable 是 Java 5 引入的,与 Runnable 相比,它有两大优势:

- 可以返回结果:
call()方法可以返回一个值。 - 可以抛出异常:
call()方法可以声明抛出异常。
Callable 通常需要配合 FutureTask 或 ExecutorService 来使用。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 定义一个类实现 Callable 接口
// 泛型指定了 call() 方法的返回类型
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
// 模拟耗时操作
Thread.sleep(1000);
return sum; // 返回计算结果
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
// 2. 创建 Callable 实例
MyCallable myCallable = new MyCallable();
// 3. 创建 FutureTask 对象,并将 Callable 实例作为参数传入
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 4. 创建 Thread 对象,并将 FutureTask 实例作为参数传入
Thread thread = new Thread(futureTask);
// 5. 启动线程
thread.start();
// 6. 主线程可以继续做其他事情...
System.out.println("Main thread is doing other work...");
// 7. 获取 Callable 的执行结果(此方法会阻塞,直到任务完成)
Integer result = futureTask.get();
System.out.println("The result from Callable is: " + result);
}
}
线程的生命周期(状态)
Java 线程在其生命周期中会经历以下几种状态:
- 新建:当使用
new关键字创建一个线程对象时,它处于新建状态。 - 就绪:当调用
start()方法后,线程进入就绪状态,此时线程已经准备好,等待 CPU 调度。 - 运行:当线程获得 CPU 时间片后,进入运行状态,开始执行
run()方法中的代码。 - 阻塞:线程在运行过程中,因为某些原因(如调用了
sleep(),wait(), 或等待 I/O 完成等)暂时让出 CPU,暂停执行,阻塞状态结束后,线程通常会回到就绪状态,等待再次被调度。 - 死亡:线程的
run()方法执行完毕,或者因为异常而终止,线程就进入死亡状态,不能再被启动。
可以使用 Thread.getState() 方法来获取线程的当前状态。
线程同步
当多个线程同时访问和操作同一个共享资源时,可能会导致数据不一致的问题,这种现象称为“线程安全问题”。
synchronized 关键字
synchronized 可以确保在同一时刻,只有一个线程可以执行被其修饰的代码块或方法。
-
同步方法:
public synchronized void add() { count++; }锁住的是当前对象实例(
this)。 -
同步代码块(更灵活,推荐):
public void add() { synchronized (this) { // 锁住的是 this 对象 count++; } }可以指定任意对象作为锁,从而实现更精细的控制。
volatile 关键字
volatile 关键字可以保证变量的可见性和禁止指令重排序。
- 可见性:当一个线程修改了一个
volatile变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。 - 局限性:
volatile不具备原子性。volatile int count = 0; count++;这条操作在 JVM 中不是原子的,它包含“读取-修改-写入”三个步骤,在多线程下仍然不安全。
Lock 接口(ReentrantLock)
ReentrantLock 是 java.util.concurrent.locks 包下的一个类,它提供了比 synchronized 更强大的功能。
- 可中断的锁获取:
lockInterruptibly()允许在等待锁的过程中响应中断。 - 公平锁:可以创建公平锁,线程按照请求的顺序获取锁。
- 锁超时:
tryLock(long time, TimeUnit unit)可以尝试获取锁,在指定时间内获取不到就放弃。 - 条件变量:
Condition对象可以更灵活地实现线程间的等待/通知机制。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
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;
}
}
线程间通信
wait(), notify(), notifyAll()
这三个方法必须在同步代码块或同步方法中使用,并且由锁对象(synchronized 锁定的对象)来调用。
wait():让当前线程等待,并释放锁。notify():随机唤醒一个正在等待该锁的线程。notifyAll():唤醒所有正在等待该锁的线程,它们会竞争锁。
经典的生产者-消费者模型就是使用它们实现的。
join() 方法
join() 方法的作用是等待该线程执行完毕,调用 thread.join() 的线程会阻塞,直到 thread 线程结束。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Child thread finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("Main thread is waiting for child thread...");
thread.join(); // 主线程在此等待
System.out.println("Main thread finished.");
}
}
线程池
频繁地创建和销毁线程是非常消耗资源的,线程池可以预先创建一组线程,任务提交时,线程池中的线程负责执行任务,任务完成后线程不会被销毁,而是返回线程池中等待下一个任务,从而避免了线程创建和销毁的开销。
Executor 框架
Java 提供了 Executor 框架来管理线程池。
Executors.newFixedThreadPool(int n):创建一个固定大小的线程池。Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程池大小超过处理任务所需要的线程,那么回收部分空闲线程;当任务数增加时,则能添加新线程。Executors.newSingleThreadExecutor():创建一个单线程的线程池,它只会用唯一的工作线程来执行任务。Executors.newScheduledThreadPool(int corePoolSize):创建一个大小无限的线程池,它支持定时以及周期性执行任务。
示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个固定大小为3的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
ThreadPoolExecutor 详解
Executors 工具类创建的线程池底层都是 ThreadPoolExecutor,为了更好地控制线程池,推荐直接使用 ThreadPoolExecutor。
其核心构造参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略
高级并发工具
CountDownLatch:一个同步工具,允许一个或多个线程等待其他一组线程完成操作后再执行,它像一个倒计时器。CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点,它可以循环使用。Semaphore:一个计数信号量,用于控制同时访问某个特定资源的线程数量。ConcurrentHashMap:线程安全的HashMap,提供了高效的并发访问。ThreadLocal:为每个使用该变量的线程提供一个独立的变量副本,从而隔离了多个线程的数据。
总结与最佳实践
- 优先使用
Executor框架:避免手动管理线程,优先使用线程池。 - 避免使用
Executors创建线程池:Executors提供的工厂方法创建的线程池存在风险(如newCachedThreadPool可能导致 OOM),推荐使用ThreadPoolExecutor构造函数来创建。 - 谨慎使用
synchronized:在简单场景下使用它,但在复杂场景下,ReentrantLock提供了更好的灵活性和控制力。 - 最小化同步范围:尽量只同步必要的代码块,而不是整个方法,以提高并发性能。
- 优先使用
volatile而不是synchronized:如果只是为了保证变量的可见性,volatile是一个轻量级的选择。 - 注意处理线程中断:在长时间运行的任务中,应该定期检查线程的中断状态(
Thread.currentThread().isInterrupted()),并在被中断时优雅地退出。 - 优先使用并发集合:如
ConcurrentHashMap,CopyOnWriteArrayList等,而不是对普通集合进行同步包装。 - 避免死锁:避免在多个线程中以不同的顺序获取多个锁。
希望这份详细的梳理能帮助你全面理解 Java 多线程!
