1. 线程基础概念解析
1.1 线程与进程的本质区别
在操作系统层面,进程和线程是两种不同的资源分配和执行单位。进程是操作系统进行资源分配的基本单位,每个进程都有独立的地址空间、数据栈和其他系统资源。而线程是CPU调度的基本单位,属于进程内的执行流,共享进程的资源空间。
举个生活中的例子:我们可以把进程想象成一个工厂,而线程就是工厂里的工人。工厂(进程)提供了厂房、原材料等公共资源,工人(线程)们共享这些资源,各自完成不同的生产任务。这种设计带来了几个关键特性:
- 创建开销:启动新进程需要分配独立的内存空间等资源,而创建线程只需少量栈空间,开销小得多
- 通信成本:进程间通信(IPC)需要特殊机制(如管道、消息队列),而线程间可直接读写共享内存
- 稳定性影响:一个进程崩溃不会影响其他进程,但一个线程崩溃可能导致整个进程终止
实际开发中需要注意:多线程共享变量虽然方便通信,但也带来了线程安全问题,必须通过同步机制来保护共享数据。
1.2 并行与并发的技术实现
现代计算机系统中,并行和并发是两个常被混淆但本质不同的概念:
- 并发(Concurrency):指系统具有处理多个任务的能力,这些任务在时间上是重叠的。单核CPU通过时间片轮转实现并发,宏观上看似"同时"执行多个线程,微观上是快速切换
- 并行(Parallelism):指系统真正同时执行多个任务,需要多核CPU或分布式系统的硬件支持
性能优化时的一个常见误区:在单核环境下盲目增加线程数期望提高性能。实际上,线程切换(Context Switch)本身就有开销,线程数超过CPU核心数后性能反而可能下降。根据经验公式:
code复制最佳线程数 = CPU核心数 * (1 + 等待时间/计算时间)
对于I/O密集型任务(如网络请求),由于等待时间长,可以适当增加线程数;而对于CPU密集型任务,线程数最好等于或略多于CPU核心数。
2. Java线程创建方式详解
2.1 继承Thread类的实现方式
这是最基本的线程创建方式,适合简单的线程任务:
java复制class MyThread extends Thread {
@Override
public void run() {
// 线程执行逻辑
System.out.println("Thread running: " + getName());
}
}
// 使用方式
MyThread thread = new MyThread();
thread.start();
这种方式的局限性很明显:Java是单继承的,继承了Thread后就不能再继承其他类。在实际项目中,更推荐使用实现接口的方式。
2.2 实现Runnable接口的标准做法
Runnable是函数式接口,只有一个run()方法,更适合面向接口编程:
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running: " +
Thread.currentThread().getName());
}
}
// 使用方式
Thread thread = new Thread(new MyRunnable());
thread.start();
从Java 8开始,可以利用lambda表达式进一步简化:
java复制new Thread(() -> {
System.out.println("Lambda thread running");
}).start();
2.3 Callable与Future的配合使用
当需要获取线程执行结果时,Callable比Runnable更合适:
java复制class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "Task completed";
}
}
// 使用方式
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 阻塞获取结果
executor.shutdown();
Callable的优势在于:
- 可以返回执行结果
- 可以抛出受检异常
- 配合Future可以实现超时控制:
future.get(500, TimeUnit.MILLISECONDS)
2.4 线程池的最佳实践
直接创建线程的代价较高,生产环境推荐使用线程池:
java复制// 创建固定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// 提交任务
pool.execute(() -> {
System.out.println("Task running in pool");
});
// 优雅关闭
pool.shutdown();
Java线程池的核心参数:
| 参数 | 说明 | 设置建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU密集型:N+1,I/O密集型:2N |
| maximumPoolSize | 最大线程数 | 根据任务特性调整 |
| keepAliveTime | 空闲线程存活时间 | 60s左右 |
| workQueue | 任务队列 | 根据并发量选择队列类型 |
| threadFactory | 线程工厂 | 自定义线程命名等 |
| handler | 拒绝策略 | 根据业务需求选择 |
实际项目中最常见的坑:使用无界队列(LinkedBlockingQueue)导致内存溢出。高并发场景建议使用有界队列并设置合理的拒绝策略。
3. 线程状态管理与控制
3.1 六种状态的完整生命周期
Java线程的生命周期包括以下状态:
- NEW:新建状态,尚未调用start()
- RUNNABLE:可运行状态,包括就绪(Ready)和运行中(Running)
- BLOCKED:阻塞状态,等待获取监视器锁
- WAITING:无限期等待,需其他线程显式唤醒
- TIMED_WAITING:限期等待,超时后自动唤醒
- TERMINATED:终止状态,线程执行完毕
状态转换的典型场景:
- 调用Object.wait():RUNNABLE → WAITING
- notify()/notifyAll():WAITING → BLOCKED
- Thread.sleep():RUNNABLE → TIMED_WAITING
- 等待I/O操作:RUNNABLE → (系统级阻塞)
3.2 线程顺序执行的实现方案
保证线程执行顺序的几种方法:
- join()方法:让父线程等待子线程结束
java复制Thread t1 = new Thread(() -> System.out.println("T1"));
Thread t2 = new Thread(() -> {
try {
t1.join();
System.out.println("T2");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
t2.join();
System.out.println("T3");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t3.start();
- 单线程池:利用FIFO特性
java复制ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("T1"));
executor.execute(() -> System.out.println("T2"));
executor.execute(() -> System.out.println("T3"));
executor.shutdown();
- CountDownLatch/Phaser等同步工具(适合更复杂的场景)
3.3 wait/notify机制的正确使用
生产者-消费者模型的经典实现:
java复制class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
public Buffer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 缓冲区满,等待
}
queue.offer(item);
notifyAll(); // 通知消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 缓冲区空,等待
}
int item = queue.poll();
notifyAll(); // 通知生产者
return item;
}
}
关键注意事项:
- 必须在同步代码块中使用wait/notify
- 判断条件要用while而不是if(防止虚假唤醒)
- 优先使用notifyAll()而非notify()(避免信号丢失)
4. 线程中断与资源清理
4.1 优雅停止线程的三种方式
- 标志位法(推荐)
java复制class SafeStopThread implements Runnable {
private volatile boolean stopped = false;
@Override
public void run() {
while (!stopped) {
// 执行任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Thread stopped safely");
}
public void stop() {
this.stopped = true;
}
}
- 中断机制
java复制Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
break;
}
}
});
thread.start();
// ...
thread.interrupt();
- 通过Future取消(线程池场景)
java复制Future<?> future = executor.submit(task);
// ...
future.cancel(true); // mayInterruptIfRunning=true
4.2 线程池的资源回收
正确关闭线程池的步骤:
- 调用shutdown():停止接收新任务,已提交任务继续执行
- 等待一段时间后调用shutdownNow():尝试中断正在执行的任务
- 使用awaitTermination()等待所有任务完成
java复制executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
5. 线程同步高级技巧
5.1 synchronized的优化使用
JDK6后对synchronized做了重大优化,了解这些机制有助于写出更高效的代码:
-
锁升级过程:
- 无锁 → 偏向锁(单线程访问)
- 偏向锁 → 轻量级锁(少量竞争)
- 轻量级锁 → 重量级锁(激烈竞争)
-
锁粗化:将连续的加锁解锁合并为一次
-
锁消除:JVM检测到不可能存在共享数据竞争时,会消除锁
最佳实践:
- 减小同步代码块范围
- 对不同功能使用不同的锁(细化锁粒度)
- 读写分离场景使用ReadWriteLock
5.2 volatile关键字的语义
volatile保证了变量的可见性和有序性(禁止指令重排序),但不保证原子性。典型应用场景:
- 状态标志位
java复制volatile boolean running = true;
public void stop() {
running = false;
}
- 单例模式的双重检查锁定
java复制class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
5.3 ThreadLocal的内存泄漏防范
ThreadLocal使用不当会导致内存泄漏:
java复制// 正确使用方式
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
threadLocal.set("value");
// 使用值
} finally {
threadLocal.remove(); // 必须清理
}
根本原因:
- ThreadLocalMap的Entry是弱引用Key,但Value是强引用
- 线程池场景下线程会复用,如果不remove(),Value会一直存在
解决方案:
- 使用后必须调用remove()
- 考虑使用static final修饰ThreadLocal实例
- 继承InheritableThreadLocal实现线程间值传递要谨慎
6. 并发工具类实战
6.1 CountDownLatch的应用场景
典型应用:并行任务完成后汇总结果
java复制CountDownLatch latch = new CountDownLatch(3);
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
try {
// 执行任务
Thread.sleep(1000);
} finally {
latch.countDown();
}
});
}
latch.await(5, TimeUnit.SECONDS); // 等待所有任务完成
System.out.println("All tasks completed");
executor.shutdown();
6.2 CyclicBarrier的循环使用
适合分阶段任务,比如多轮比赛:
java复制CyclicBarrier barrier = new CyclicBarrier(5, () ->
System.out.println("All players ready for next round"));
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int round = 1; round <= 3; round++) {
try {
Thread.sleep((long)(Math.random() * 1000));
System.out.println(Thread.currentThread().getName() +
" ready for round " + round);
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
6.3 CompletableFuture的异步编排
Java8提供的强大异步编程工具:
java复制CompletableFuture.supplyAsync(() -> {
// 异步任务1
return "Result1";
}).thenApplyAsync(result1 -> {
// 依赖任务1的结果
return result1 + " processed";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
// 并行任务2
return "Result2";
}), (r1, r2) -> r1 + " & " + r2)
.exceptionally(ex -> {
// 异常处理
return "Recovered";
}).thenAccept(System.out::println);
优势:
- 链式调用,编排复杂异步流程
- 内置线程池管理
- 丰富的组合方法(thenCombine、thenCompose等)
7. 线程安全设计模式
7.1 不可变对象模式
最简单的线程安全方案:让对象创建后不可修改
java复制public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
// 只有getter方法
public String getName() { return name; }
public int getAge() { return age; }
}
实现要点:
- 类声明为final
- 所有字段final
- 不提供setter方法
- 如果字段是引用类型,确保其也是不可变的或防御性拷贝
7.2 线程局部存储模式
为每个线程创建独立的对象实例:
java复制public class ThreadLocalFormatter {
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return formatter.get().format(date);
}
}
适用场景:
- 线程不安全的工具类(如SimpleDateFormat)
- 需要保存线程上下文信息(如用户会话)
- 避免在方法中频繁创建对象
7.3 写时复制(Copy-On-Write)模式
读多写少场景的高效解决方案:
java复制public class CopyOnWriteList<E> {
private volatile List<E> list = new ArrayList<>();
public void add(E element) {
synchronized (this) {
List<E> newList = new ArrayList<>(list);
newList.add(element);
list = newList;
}
}
public E get(int index) {
return list.get(index); // 无需加锁
}
}
Java标准库实现:
- CopyOnWriteArrayList
- CopyOnWriteArraySet
特点:
- 读操作完全不加锁,性能极高
- 写操作加锁并复制整个数据结构
- 适合读多写少且数据量不大的场景
8. 性能调优与问题排查
8.1 线程池参数优化实践
线上环境线程池配置建议:
- CPU密集型任务:
java复制int coreSize = Runtime.getRuntime().availableProcessors();
ExecutorService executor = new ThreadPoolExecutor(
coreSize,
coreSize * 2,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
- I/O密集型任务(如Web服务):
java复制ExecutorService executor = new ThreadPoolExecutor(
50, // 考虑QPS和平均响应时间
200,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactoryBuilder().setNameFormat("io-pool-%d").build(),
new ThreadPoolExecutor.AbortPolicy()
);
监控关键指标:
- 活跃线程数
- 任务队列大小
- 拒绝任务数
- 任务执行时间
8.2 死锁检测与解决
典型死锁场景:
java复制Object lock1 = new Object();
Object lock2 = new Object();
new Thread(() -> {
synchronized (lock1) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) {
System.out.println("Thread1 got both locks");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) {
System.out.println("Thread2 got both locks");
}
}
}).start();
诊断工具:
- jstack:查看线程堆栈和锁持有情况
- JConsole/VisualVM:图形化监控
- Arthas:在线诊断工具
预防措施:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 减少同步代码块范围
8.3 线程转储分析技巧
通过jstack获取线程转储后,重点关注:
- 死锁信息:
code复制Found one Java-level deadlock:
...
- 阻塞线程状态:
code复制"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f48740f7000 nid=0x5e1e waiting for monitor entry [0x00007f486b7f6000]
java.lang.Thread.State: BLOCKED (on object monitor)
- CPU高的线程:
code复制"Thread-0" #11 prio=5 os_prio=0 tid=0x00007f48740f5000 nid=0x5e1d runnable [0x00007f486b8f7000]
java.lang.Thread.State: RUNNABLE
分析工具推荐:
- fastthread.io(在线分析)
- IBM Thread and Monitor Dump Analyzer
- VisualVM的线程转储分析插件
9. Java内存模型深入
9.1 happens-before规则详解
Java内存模型定义的8条基本规则:
- 程序顺序规则:线程内代码按书写顺序执行
- 锁规则:解锁操作先于后续的加锁操作
- volatile规则:volatile写先于后续读
- 线程启动规则:start()先于线程内任何操作
- 线程终止规则:线程内操作先于终止检测
- 中断规则:interrupt()调用先于检测到中断
- 终结器规则:对象构造先于finalize()
- 传递性:A先于B,B先于C,则A先于C
这些规则确保了多线程环境下的可见性和有序性。
9.2 双重检查锁定问题根源
看似正确的单例实现其实有隐患:
java复制class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题出在这里!
}
}
}
return instance;
}
}
问题在于new Singleton()不是原子操作,可能发生指令重排序:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
如果2和3重排序,其他线程可能拿到未初始化的对象。解决方案:
- 使用volatile修饰instance
- 改用静态内部类方式
- 使用枚举实现单例(最安全)
9.3 final字段的内存语义
final字段的特殊处理:
- 构造函数内对final字段的写入,与后续读取该引用之间建立happens-before关系
- 可以保证引用所指对象的初始化状态对所有线程可见
- 引用本身不可变,但对象内容可能仍需要同步
正确使用示例:
java复制class FinalFieldExample {
final int x;
int y;
public FinalFieldExample() {
x = 42; // 正确构造
y = 1; // 普通写入
}
}
10. 并发编程最佳实践
10.1 锁使用的黄金法则
- 只在必要的时候加锁,减小同步范围
- 永远不要在同步块中调用外部方法(容易造成死锁)
- 按固定顺序获取多个锁
- 使用tryLock()替代无限制等待
- 考虑使用读写锁提升并发度
10.2 避免常见并发陷阱
- 伪共享问题:看似不相关的变量因位于同一缓存行导致性能下降
java复制// 解决:使用填充或@Contended注解
class Data {
@sun.misc.Contended
volatile long value1;
volatile long value2;
}
- 线程池任务堆积:使用有界队列并设置合理的拒绝策略
- ThreadLocal内存泄漏:务必在finally块中remove()
- 并发修改异常:使用并发集合或加锁保护
10.3 性能优化检查清单
-
减少锁竞争:
- 缩小同步块范围
- 降低锁粒度
- 使用读写锁
- 考虑无锁数据结构
-
合理配置线程池:
- 根据任务类型选择队列
- 设置合适的线程数
- 添加监控指标
-
利用硬件特性:
- CPU缓存行对齐
- 避免伪共享
- 考虑NUMA架构影响
-
选择合适工具:
- 简单互斥:synchronized
- 复杂同步:ReentrantLock
- 原子操作:AtomicXXX
- 并发集合:ConcurrentHashMap
在实际项目中,我通常会先使用简单的synchronized实现功能,再通过性能测试找出热点,逐步优化为更精细的并发控制方案。记住过早优化是万恶之源,但完全不考虑并发问题更是灾难的开始。