一、线程与进程:操作系统资源管理的核心概念
在操作系统的资源管理体系中,进程与线程是两个关键概念。进程是操作系统分配资源的最小单位,每个进程拥有独立的内存空间、文件句柄等资源,进程间通过内存隔离保证安全性。而线程是 CPU 调度的最小单位,同一进程内的多个线程共享进程资源(如内存、全局变量),这使得线程间通信更高效,但也带来了共享资源竞争的问题。相较于进程,线程具有更轻量的特性,上下文切换成本更低,因此多线程编程成为提升 CPU 利用率的重要手段。
二、Java 线程的创建与生命周期管理
(一)线程创建的三种方式
- 继承 Thread 类
通过创建 Thread 子类并重写 run () 方法实现线程逻辑:
public class MyThread extends Thread {
@Override
public void run() {
// 业务逻辑
}
}
new MyThread().start(); // 启动线程
此方式简单直观,但受限于 Java 单继承机制,扩展性较差。
- 实现 Runnable 接口(推荐)
分离线程逻辑与线程对象,更符合面向接口编程原则:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 业务逻辑
}
}
new Thread(new MyRunnable()).start();
该方式避免了单继承限制,便于代码复用。
- 使用 Callable+Future(支持返回值与异常处理)
适用于需要异步计算结果的场景:
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
return 100; // 可返回计算结果
});
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞获取结果
(二)线程生命周期的六个阶段
- 新建(New):线程对象已创建,但尚未调用 start () 方法。
- 就绪(Runnable):调用 start () 后进入就绪队列,等待 CPU 调度。
- 运行(Running):获取 CPU 执行权,执行 run () 方法。
- 阻塞(Blocked/Waiting/Timed Waiting):
- Blocked:等待锁(如 synchronized 同步块)。
- Waiting:调用 Object.wait () 或 LockSupport.park (),需其他线程唤醒。
- Timed Waiting:调用 Thread.sleep (long) 等带超时参数的方法,超时后自动恢复。
- 终止(Terminated):run () 方法正常执行完毕或因异常退出。
三、线程同步与并发控制:解决共享资源问题
(一)共享资源的三大核心问题
- 原子性:操作不可中断(如 i++ 是非原子操作,需加锁保证)。
- 可见性:线程修改共享变量后,其他线程能否及时感知(volatile、synchronized、Lock 可保证)。
- 有序性:编译器或处理器优化可能导致指令重排序(volatile 通过内存屏障禁止重排序)。
(二)同步机制:锁的实现与对比
-
synchronized 关键字(JVM 内置锁)
- 作用范围:
- 实例方法:锁定当前实例对象。
- 静态方法:锁定当前类的 Class 对象。
- 代码块:锁定指定对象(如 synchronized (this))。
- 原理:基于 Monitor 对象实现,竞争激烈时会从偏向锁升级为重量级锁,性能开销较大。
- 作用范围:
-
ReentrantLock(JUC 显式锁)
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 加锁(支持响应中断、尝试加锁)
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须在finally中释放锁
}
特点:支持公平锁 / 非公平锁、条件变量(Condition)、可中断加锁,适用于复杂同步场景。
对比总结:
特性 | synchronized | ReentrantLock |
---|---|---|
锁获取方式 | 自动加解锁 | 手动加解锁 |
公平性 | 非公平 | 可设置公平性 |
条件变量 | 无 | 支持(Condition) |
锁升级机制 | 有(偏向锁→重量级) | 无 |
(三)线程间通信:协作与协调
- Object.wait/notify/notifyAll
需配合 synchronized 使用,实现线程间的等待 - 通知机制:
synchronized (obj) {
obj.wait(); // 释放锁,进入等待队列
obj.notify(); // 唤醒一个等待线程
}
- LockSupport.park/unpark
更高效的线程阻塞 / 唤醒工具,避免传统 wait/notify 的死锁隐患:
LockSupport.park(); // 阻塞当前线程
LockSupport.unpark(thread); // 唤醒指定线程
四、JUC 并发工具类:提升开发效率的利器
(一)并发容器:线程安全的数据结构
-
ConcurrentHashMap(JDK1.8+)
底层采用 “数组 + 链表 + 红黑树” 结构,通过 CAS 与分段锁(synchronized)实现线程安全,适用于高并发读多写少场景。 -
CopyOnWriteArrayList
写时复制机制(修改时创建新数组),读操作无锁,适合读多写少但不要求实时性的场景(如日志记录)。 -
BlockingQueue(阻塞队列)
如 ArrayBlockingQueue、LinkedBlockingQueue,支持线程安全的插入 / 取出操作,天然适配生产者 - 消费者模式。
(二)同步辅助类:控制线程协作
- CountDownLatch
允许一个或多个线程等待其他线程完成操作:
CountDownLatch latch = new CountDownLatch(3);
// 子线程完成任务后调用latch.countDown()
latch.await(); // 主线程等待计数归零
- CyclicBarrier
多个线程互相等待,全部到达屏障点后一起执行:
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障点");
});
barrier.await(); // 线程到达后等待其他线程
- Semaphore
控制同时访问资源的线程数量(如限流):
Semaphore semaphore = new Semaphore(2); // 允许2个线程同时访问
semaphore.acquire(); // 获取许可(阻塞)
try {
// 业务逻辑
} finally {
semaphore.release(); // 释放许可
}
五、线程池:资源管理的核心组件
(一)为什么使用线程池?
- 避免频繁创建 / 销毁线程的开销,重用线程。
- 控制线程数量,防止内存溢出(如大量并发请求)。
- 统一管理线程生命周期,支持定时任务、异步处理等场景。
(二)ThreadPoolExecutor 核心参数与工作流程
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(闲置时不销毁)
int maximumPoolSize, // 最大线程数(核心+临时线程)
long keepAliveTime, // 临时线程闲置存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
);
工作流程:
- 提交任务,若核心线程数未满,创建新线程执行任务。
- 若核心线程已满,任务进入队列(workQueue)。
- 若队列已满且线程数未达 maximumPoolSize,创建临时线程执行任务。
- 若线程数达上限且队列已满,触发拒绝策略。
(三)拒绝策略(RejectedExecutionHandler)
- AbortPolicy(默认):抛出 RejectedExecutionException,终止任务。
- CallerRunsPolicy:由提交任务的线程直接执行任务。
- DiscardPolicy:静默丢弃任务,无任何提示。
- DiscardOldestPolicy:丢弃队列中最老的任务,尝试提交新任务。
(四)常见线程池工厂方法(注意陷阱)
- Executors.newFixedThreadPool(int nThreads):固定大小线程池,使用无界队列(LinkedBlockingQueue),可能导致 OOM。
- Executors.newCachedThreadPool():可缓存线程池,线程数无上限,适用于短时间任务。
- Executors.newScheduledThreadPool(int corePoolSize):定时任务线程池,支持延迟执行和周期性任务。
最佳实践:优先使用 ThreadPoolExecutor 自定义参数,避免无界队列引发内存问题。
六、Java 内存模型(JMM)与原子操作
(一)JMM 核心架构
- 主内存(Main Memory):所有线程共享的内存,存储对象实例、静态变量等。
- 工作内存(Working Memory):每个线程的本地缓存,存储主内存变量的副本。
交互规则:线程对变量的操作必须先从主内存读取到工作内存,修改后写回主内存,这是可见性问题的根源。
(二)volatile 关键字:可见性与有序性的保证
- 作用:
- 强制线程从主内存读取变量,保证修改的可见性。
- 通过内存屏障禁止指令重排序,保证有序性。
- 局限性:不保证原子性(如 volatile int i; i++ 仍需加锁)。
(三)CAS(Compare-And-Swap):无锁原子操作
- 原理:基于硬件指令(如 CPU 的 cmpxchg),通过 “比较 - 交换” 实现原子更新。
- 步骤:
- 读取当前值(V)和预期值(A)。
- 若 V 等于 A,更新为新值(B);否则重试。
- 问题:
- ABA 问题:值从 A→B→A 时,CAS 误认为未修改(解决方案:AtomicStampedReference 添加版本号)。
- 循环开销:竞争激烈时重试次数多,性能下降。
- 应用场景:AtomicInteger、ConcurrentHashMap 的底层实现。
七、高级主题:线程安全设计与性能优化
(一)线程安全设计原则
- 不可变性(Immutable):如 String、Integer 等类,通过 final 关键字保证状态不可变,避免共享变量被修改。
- 线程封闭(Thread Local):使用 ThreadLocal 将变量与线程绑定(如数据库连接、用户上下文):
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("value"); // 仅当前线程可见
- 锁分段(Lock Stripping):如 ConcurrentHashMap 对不同分段(Segment)加锁,降低锁粒度,提升并发性能。
(二)性能优化与死锁预防
- 减少锁竞争:
- 缩小锁范围:同步块内避免无关操作。
- 使用细粒度锁:如分段锁、ReentrantReadWriteLock 读写锁。
- 死锁预防:
- 按顺序加锁:所有线程以相同顺序获取锁。
- 限时加锁:使用 tryLock (long time, TimeUnit unit) 避免永久阻塞。
- 诊断工具:
- jps:查看 Java 进程 ID。
- jstack <pid>:打印线程栈,检测死锁、阻塞。
- VisualVM:图形化监控线程状态、锁竞争。
(三)Fork/Join 框架:并行任务分解
适用于大任务可分解为小任务的场景(如归并排序、文件搜索)。核心类包括 ForkJoinTask(任务抽象类)和 ForkJoinPool(线程池)。
示例:计算 1+2+…+n
public class SumTask extends RecursiveTask<Integer> {
private int start, end;
private static final int THRESHOLD = 1000; // 阈值,小于该值直接计算
public SumTask(int start, int end) { this.start = start; this.end = end; }
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
return IntStream.rangeClosed(start, end).sum();
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(start, mid);
SumTask rightTask = new SumTask(mid+1, end);
leftTask.fork(); // 异步执行左任务
return rightTask.compute() + leftTask.join(); // 合并结果
}
}
}
(四)异步编程:CompletableFuture
简化异步任务的组合、回调和异常处理,支持链式调用与结果合并:
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser());
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> fetchOrder());
CompletableFuture<Void> combinedFuture = userFuture.thenCombine(orderFuture, (user, order) -> {
process(user, order); // 合并处理用户与订单信息
return null;
});
总结
Java 多线程编程是提升应用性能的关键技术,但也伴随着共享资源竞争、死锁等复杂问题。通过深入理解线程生命周期、同步机制、JUC 工具类及线程池原理,结合合理的设计原则与性能优化策略,开发者可以编写出高效、安全的并发程序。在实际开发中,需根据具体场景选择合适的并发工具,并借助诊断工具持续优化系统性能。