线程详解(理论)
一、进程和线程
在Java中,进程(Process)和线程(Thread)是多任务编程的核心概念,它们既有联系也有本质区别。
1.1 定义
- 进程是操作系统中资源分配的基本单位。它是一个正在运行的程序的实例,包括程序代码、数据、堆栈、寄存器状态以及进程控制块等信息。
- 线程是进程中的一个执行单元,是进程内部的最小调度单位。一个进程可以包含一个或多个线程,这些线程共享进程的资源,但各自有自己的执行路径和栈。
1.2 关系
包含关系:
- 一个进程可以包含多个线程,所有线程共享进程的资源(如内存、文件句柄等)。
执行单元:
- 进程是操作系统分配资源(CPU、内存)的基本单位。
- 线程是CPU调度的最小单位,是进程内的具体执行任务的分支。
协作方式:
- 进程间需要IPC(Inter-Process Communication)通信(如管道、Socket)。
- 线程间可直接通过共享内存通信(但需同步机制避免数据竞争)。
1.3 核心区别

比喻理解:
-
进程像一个独立的工厂:
每个工厂有自己的车间(内存)、工人(线程)、原料(资源),工厂之间通过物流(IPC)协作。 -
线程像工厂内的工人:
工人共享工厂的资源(车间、仓库),协同完成生产任务,但需要避免争夺工具(同步问题)。
1.4 引入线程的原因
- 并发执行:
线程允许一个进程中同时执行多个任务。通过在同一个进程内创建多个线程,可以实现并发处理,从而提高程序的效率和响应速度。例如,一个程序可以在一个线程中处理用户界面响应,在另一个线程中进行后台数据处理。 - 资源共享:
线程共享进程的资源(如内存空间和文件描述符),这使得线程间的通信和数据共享比进程间更加高效和简单。线程之间可以轻松共享数据和资源,而不需要像进程间那样复杂的通信机制(如管道或消息队列) - 减少开销:
创建和管理线程比创建和管理进程的开销要小得多。线程的创建和销毁成本较低,因为线程共享同一进程的地址空间和资源,不需要像进程那样进行内存映射和管理。 - 提高响应性:
在多线程程序中,某个线程的任务(如I/O操作)不会阻塞其他线程的执行。这意味着即使一个线程正在等待某些操作完成,其他线程仍然可以继续运行,从而保持程序的响应性。 - 更好的资源利用:
在多核处理器上,多线程程序可以将多个线程分配到不同的处理器核心上,从而提高程序的并行处理能力。这种并行执行能够更有效地利用计算资源,提升性能。
1.5 应用场景
适合用进程的场景
-
需要高隔离性(如浏览器每个标签页作为独立进程)。
-
调用外部程序(如Java中执行Shell脚本或启动其他应用)。
-
分布式系统中跨机器的任务协作(通过Socket通信)。
适合用线程的场景
-
高并发任务(如Web服务器处理大量请求)。
-
需要共享数据的任务(如多线程计算并汇总结果)。
-
异步任务(如后台下载文件时不阻塞UI主线程)。
1.6 启动代码示例
- 启动进程(执行外部命令)
/**
* 通过Runtime启动进程
* @throws IOException
* @throws InterruptedException
*/
@RequestMapping("/processByRuntime")
public void processByRuntime() throws IOException, InterruptedException {
Runtime run = Runtime.getRuntime();
//打开记事本,相当于在命令提示符中输入notepad打开记事本
run.exec("notepad");
}
/**
* 通过ProcessBuilder启动进程
*
* @throws IOException
* @throws InterruptedException
*/
@RequestMapping("/processByProcessBuilder")
public void processByProcessBuilder() throws IOException, InterruptedException {
//打开记事本
ProcessBuilder pBuilder = new ProcessBuilder("notepad");
pBuilder.start();
}
- 启动线程
@RequestMapping("/threadTest")
public void threadTest(){
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务
executor.submit(() -> {
System.out.println("Thread 1: " + Thread.currentThread().getName());
});
executor.submit(() -> {
System.out.println("Thread 2: " + Thread.currentThread().getName());
});
// 关闭线程池
executor.shutdown();
}
1.7 常见误区
-
误区1:线程一定比进程快?
→ 不一定!线程切换也有开销,高并发时可能因锁竞争反而降低性能。 -
误区2:多线程不需要考虑资源隔离?
→ 需要!线程共享内存,若未同步会导致数据不一致(如经典的i++非原子操作问题)。 -
误区3:进程间无法共享数据?
→ 可以通过共享内存(如MemoryMappedFile)或中间件(如Redis)实现。
二、线程的生命周期
Java 线程的生命周期主要包括以下几个状态:
- 新建(New):当创建了 Thread 类的实例,但还未调用 start() 方法时,线程处于新建状态。
- 就绪(Runnable):调用 start() 方法后,线程进入就绪状态,等待系统调度。
- 运行(Running):当线程被系统选中并开始执行 run() 或 call() 方法时,线程处于运行状态。
- 阻塞(Blocked):线程可能因为等待锁、等待 I/O 操作完成、调用 wait() 方法等而进入阻塞状态,暂时停止执行。
- 等待(Waiting):线程调用 wait()、join() 或 LockSupport.park() 等方法会进入等待状态,直到其他线程通知或中断。
- 超时等待(Timed Waiting):线程调用 sleep()、wait(long)、join(long) 或 LockSupport.parkNanos() 等方法,在等待一段时间后自动唤醒。
- 终止(Terminated):线程执行完 run() 或 call() 方法,或者出现异常而终止。
三、线程的创建和启动
3.1 继承Thread类
- 定义一个类 MyThread 继承 Thread 类,并重写 run() 方法,将线程要执行的代码放在 run() 方法中。
- 创建 MyThread 实例,并调用 start() 方法启动线程,调用 start() 会使线程进入就绪状态,等待系统调度执行,而不是直接调用 run() 方法,直接调用 run() 方法只是普通的方法调用,不会启动新线程。
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
System.out.println("---------------------Thread is running---------------------");
}
}
@RequestMapping("/threadByThread")
public void threadByThread(){
MyThread myThread = new MyThread();
myThread.start();//启动线程
}
3.2 实现 Runnable 接口
- 定义一个类 MyRunnable 实现 Runnable 接口,并重写 run() 方法。
- 创建 MyRunnable 的实例,将其作为参数传递给 Thread 类的构造函数,然后调用 start() 方法启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
System.out.println("----------------ThreadByRunnable is running----------------");
}
}
@RequestMapping("/ThreadByRunnable")
public void ThreadByRunnable(){
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();//启动线程
}
3.3 使用 Callable 和 Future(带返回值的线程)
- 定义一个类 MyCallable 实现 Callable 接口,重写 call() 方法,该方法可以有返回值,并且可以抛出异常。
- 创建 FutureTask 对象,将 MyCallable 实例作为参数传递给 FutureTask 的构造函数。
- 创建 Thread 对象,将 FutureTask 作为参数传递给 Thread 的构造函数并启动线程。
- 使用 futureTask.get() 方法获取线程执行的结果,该方法会阻塞直到线程执行完成并返回结果,如果线程未完成,调用线程会等待。
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// TODO: 线程执行的代码
return 100;
}
}
@RequestMapping("/ThreadByCallable")
public void ThreadByCallable() throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
// 获取线程的返回值
Integer result = futureTask.get();
System.out.println("ThreadByCallable result: " + result);
}
四、线程的同步
在Java多线程编程中,线程同步是确保多个线程安全访问共享资源的核心机制。
4.1 为什么要同步?
当多个线程同时修改共享数据时,可能导致数据不一致或竞态条件(Race Condition)。
示例:两个线程同时执行 i++(非原子操作):
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 实际分为三步:读取 → 修改 → 写回
}
}
问题:若线程A和B同时读取 count=5,各自加1后写回 6,最终结果应为 7,但实际得到 6。
4.2 同步机制
4.2.1 synchronized 关键字
作用:确保同一时刻只有一个线程执行同步代码。
使用方式:
(1)同步方法(锁是当前对象实例)
synchronized 修饰方法时,同一时间只有一个线程可以执行该方法,保证了方法的同步。
// 同步方法
public synchronized void increment() {
count++;
}
// 静态同步方法(锁是类的Class对象)
public static synchronized void staticMethod() {
// ...
}
(2)同步代码块(可指定锁对象)
this:表示当前对象实例,即调用这段代码的对象的引用。这个对象实例将作为同步块的锁对象(monitor lock),线程需要获取 当前对象实例的锁 才能执行同步块内的代码,确保同一时刻只有一个线程能进入该同步代码块。
互斥范围:所有使用 this 作为锁的同步块或同步方法(如 synchronized void method())共享同一把锁。
同一对象的多个线程会竞争该锁,但不同对象的锁是独立的。
public void increment() {
synchronized (this) {
count++;
}
}
this 与其他锁对象的对比:

关键注意事项:
(1)不要锁住可变对象:
// 错误!StringBuffer 内容可变,可能导致锁失效
synchronized (stringBuffer) {
stringBuffer.append("test");
}
正确做法:使用一个 final 不可变对象 作为锁:
private final Object lock = new Object();
synchronized (lock) {
// ...
}
(2)避免锁泄露:
确保同步块内的代码不会长时间阻塞(如死循环或等待I/O)。
(3)区分实例锁和类锁:
实例锁(this)和类锁(类名.class)是两把不同的锁,互不影响。
(4)为什么 synchronized (this) 和 synchronized 方法 是等价的?
非静态同步方法(如 public synchronized void method())隐式使用 this 作为锁,因此与 synchronized (this) 效果相同。
4.2.2 Lock锁
在Java并发编程中,Lock接口(尤其是其实现类ReentrantLock)提供了比传统synchronized更灵活、更强大的线程同步机制。
(1)使用 ReentrantLock 类,在进入代码块前调用 lock() 方法加锁,在代码块执行完后在 finally 块中调用 unlock() 方法释放锁,确保锁的释放,避免死锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 确保释放锁
}
}
(2)Lock 接口的核心方法

(3)可重入性
同一线程可重复获取同一把锁,避免死锁。
Lock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
methodB(); // 可重入,不会阻塞
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
}
(4)公平性
- 公平锁:线程按请求顺序获取锁(构造参数true)。
- 非公平锁:允许插队,默认模式(更高吞吐量)。
// 创建公平锁(按顺序分配锁)
Lock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
Lock nonFairLock = new ReentrantLock();
(5)可中断与超时机制
避免无限等待:通过tryLock和lockInterruptibly防止线程永久阻塞。
Lock lock = new ReentrantLock();
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 尝试3秒内获取锁
try {
// 成功获取锁后的操作
} finally {
lock.unlock();
}
} else {
// 超时后的处理逻辑(如记录日志、重试等)
}
(6) Lock vs synchronized

4.2.3 volatile 关键字
(1)作用
保证可见性:确保变量的修改对所有线程立即可见。
- 普通变量可能被线程缓存(在CPU寄存器或本地内存),而volatile变量直接读写主内存。
禁止指令重排序:防止JVM和处理器对指令进行重排序优化。
(2)适用场景
-
状态标志:如 volatile boolean isRunning,用于控制线程启停。
-
单次写入的安全发布:确保对象构造完成后对其他线程可见(需配合final)。
-
双重检查锁定(Double-Checked Locking):优化单例模式(需volatile禁止重排序)。
(3)代码示例
public class VolatileExample {
private volatile boolean flag = false;
public void start() {
new Thread(() -> {
while (!flag) { // 及时感知 flag 变化
// 等待
}
System.out.println("Thread started");
}).start();
}
public void setFlag(boolean value) {
flag = value; // 修改后立即对所有线程可见
}
}
(4)注意事项
-
不保证原子性:volatile无法解决复合操作(如i++)的线程安全问题。
-
替代方案:简单计数器场景优先使用原子类(如AtomicInteger)。
五、线程间通信
wait()、notifyAll() 与 notify()
核心机制:
-
wait():释放当前对象的锁,使线程进入等待状态,直到被notify()唤醒。
-
notify():随机唤醒一个在该对象上等待的线程。
-
notifyAll():唤醒所有在该对象上等待的线程。
使用规则:
-
必须在同步块中调用:wait()和notify()必须持有对象的锁(即位于synchronized块内)。
-
循环检查条件:使用while而非if检查条件,防止虚假唤醒(Spurious Wakeup)。
-
唤醒后重新竞争锁:被唤醒的线程需重新获取锁才能继续执行。
代码示例:
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final Object lock = new Object();
private volatile boolean isRunning = true; // 控制线程启停
public void produce() throws InterruptedException {
while (isRunning) {
synchronized (lock) {
while (queue.size() == MAX_SIZE) {
lock.wait(); // 等待队列不满
}
queue.add(1);
System.out.println("Produced, size: " + queue.size());
lock.notifyAll(); // 唤醒消费者
}
Thread.sleep(100); // 模拟生产耗时
}
}
public void consume() throws InterruptedException {
while (isRunning) {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 等待队列非空
}
queue.poll();
System.out.println("Consumed, size: " + queue.size());
lock.notifyAll(); // 唤醒生产者
}
Thread.sleep(200); // 模拟消费耗时
}
}
public void stop() {
isRunning = false; // 通过 volatile 变量安全终止线程
}
}
六、线程池
6.1 为什么需要线程池
- 减少线程创建/销毁开销:线程的创建和销毁成本高昂,线程池通过复用线程降低开销。
- 控制并发资源:避免无限制创建线程导致内存耗尽或CPU过载。
- 统一管理任务:通过队列缓冲任务,拒绝策略处理过载,增强系统的健壮性。
- 适用于高并发任务处理、异步编程、定时任务调度等。
使用建议:
- 优先手动创建 ThreadPoolExecutor,避免 Executors 的潜在问题。
- 根据任务类型合理设置线程数和队列容量。
- 始终为线程池定义有意义的名称和拒绝策略,方便问题排查。
6.2 线程池核心类:ThreadPoolExecutor
Java 线程池的核心实现类是 java.util.concurrent.ThreadPoolExecutor,其构造函数包含 7 个关键参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻线程,即使空闲也不会被回收,除非设置)
int maximumPoolSize, // 最大线程数(临时线程 + 核心线程)
long keepAliveTime, // 临时线程空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(自定义线程创建)
RejectedExecutionHandler handler // 拒绝策略
)
workQueue队列类型:
- LinkedBlockingQueue 无界队列(默认容量 Integer.MAX_VALUE) 任务量稳定,避免突发流量导致OOM
- ArrayBlockingQueue 有界队列(需指定固定容量) 控制任务积压,防止资源耗尽
- SynchronousQueue 不存储任务,直接移交线程执行 高吞吐场景,要求快速响应
- PriorityBlockingQueue 优先级队列(任务按优先级排序) 任务有优先级区分
handler拒绝策略:
当线程池和队列均满时,处理新提交任务的策略:
- AbortPolicy(默认) 抛出 RejectedExecutionException 异常
- CallerRunsPolicy 由提交任务的线程直接执行该任务
- DiscardPolicy 静默丢弃新任务,不抛异常
- DiscardOldestPolicy 丢弃队列中最旧的任务,然后重新提交新任务
6.3 线程池工作流程
当提交一个新任务时,线程池的处理逻辑如下:
-
核心线程未满 → 创建新线程执行任务。
-
核心线程已满 → 任务放入任务队列等待。
-
队列已满且线程数未达最大值 → 创建临时线程执行任务。
-
队列已满且线程数已达最大值 → 触发拒绝策略。
6.4 常见线程池类型
6.4.1 FixedThreadPool(固定大小线程池)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);// 核心线程=最大线程
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
System.out.println("Thread is running");
});
}
executorService.shutdown();
}
}
Executors.newFixedThreadPool(5) 创建一个固定大小为 5 的线程池。
executorService.execute() 方法将任务提交给线程池执行。
executorService.shutdown() 方法关闭线程池。
-
特点:线程数固定,使用无界队列(LinkedBlockingQueue)。
-
风险:任务堆积可能导致OOM。
-
适用场景:任务量已知且稳定。
6.4.2 CachedThreadPool(缓存线程池)
ExecutorService executor = Executors.newCachedThreadPool();
// 核心线程=0,最大线程=Integer.MAX_VALUE,存活时间60秒,使用SynchronousQueue
-
特点:线程数弹性伸缩,适合短时异步任务。
-
风险:无限制创建线程可能导致资源耗尽。
-
适用场景:高频短任务,如HTTP请求处理。
6.4.3 SingleThreadExecutor(单线程池)
ExecutorService executor = Executors.newSingleThreadExecutor();
// 核心线程=最大线程=1,使用无界队列
-
特点:所有任务按顺序执行,保证无并发问题。
-
适用场景:需顺序执行任务的场景(如日志写入)。
6.4.4 ScheduledThreadPool(定时任务线程池)
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
executor.scheduleAtFixedRate(() -> {
System.out.println("Task runs every 2 seconds");
}, 0, 2, TimeUnit.SECONDS);
-
特点:支持定时或周期性任务。
-
适用场景:心跳检测、定时数据同步。
6.5 监控线程池状态
通过 ThreadPoolExecutor 提供的方法监控运行状态:
int activeThreads = executor.getActiveCount(); // 活跃线程数
long completedTasks = executor.getCompletedTaskCount(); // 已完成任务数
int queueSize = executor.getQueue().size(); // 队列中等待任务数
6.6 自定义线程池
// 手动创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
5, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // 有界队列(容量100)
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由提交者执行
);
// 提交任务
executor.execute(() -> {
System.out.println("Task executed by " + Thread.currentThread().getName());
});
// 关闭线程池(平滑关闭)
executor.shutdown();
方法 作用
- shutdown() 平滑关闭:停止接受新任务,等待已提交任务执行完成。
- shutdownNow() 强制关闭:尝试中断所有线程,返回未执行的任务列表。
- isShutdown() 返回线程池是否已关闭。
- isTerminated() 返回所有任务是否已完成。
七、线程的调度和优先级
7.1 线程优先级
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
public class ThreadPriorityExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
}
}
setPriority() 方法可以设置线程的优先级,范围从 1(最低)到 10(最高),但优先级只是一个建议,实际的调度由操作系统决定。
7.2 线程调度
Java 线程的调度由操作系统负责,操作系统根据线程的状态和优先级等因素来调度线程的执行,开发人员可以通过 yield() 方法让当前线程让出 CPU 资源,让其他线程有机会执行,但不保证一定有效。
八、线程的中断
class MyThread extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 线程执行的代码
}
System.out.println("Thread interrupted");
}
}
public class ThreadInterruptExample {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
interrupt() 方法用于中断线程,调用该方法会设置线程的中断标志。
线程可以通过 isInterrupted() 方法检查中断标志,或者在阻塞操作中抛出 InterruptedException 来响应中断。

浙公网安备 33010602011771号