1. 线程基础与创建方式
在Java并发编程中,线程是最基本的执行单元。理解线程的创建方式及其特性是掌握并发编程的基础。Java提供了四种主要的线程创建方式,每种方式都有其适用场景和优缺点。
1.1 继承Thread类
继承Thread类是最直接的线程创建方式。开发者需要创建一个继承自Thread的子类,并重写run()方法来实现线程的具体逻辑。
java复制class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread创建线程");
}
}
// 使用方式
new MyThread().start();
优点:
- 实现简单直观,适合快速原型开发
- 可以直接通过this访问线程对象本身
缺点:
- Java的单继承机制限制了类的扩展性
- 业务逻辑与线程控制耦合在一起,违反了单一职责原则
- 无法获取线程执行结果
实际开发中,这种方式已经较少使用,主要因为继承带来的扩展性限制。但在一些简单场景或教学示例中仍可见到。
1.2 实现Runnable接口
实现Runnable接口是更推荐的线程创建方式。这种方式将线程控制与业务逻辑分离,提高了代码的灵活性。
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable创建线程");
}
}
// 使用方式
new Thread(new MyRunnable()).start();
优点:
- 避免了单继承的限制,可以继承其他类
- 实现了线程控制与业务逻辑的解耦
- 多个线程可以共享同一个Runnable实例
缺点:
- 仍然无法直接获取执行结果
- 需要借助Thread类来启动线程
实际应用场景:
- 需要多个线程执行相同任务的场景
- 需要与其他类继承体系配合的情况
- 线程池任务提交的基本形式
1.3 实现Callable接口
Callable接口是对Runnable的增强,它允许线程返回计算结果,并且可以抛出异常。
java复制class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable返回值";
}
}
// 使用方式
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
System.out.println(futureTask.get()); // 获取返回值
优点:
- 可以获取线程执行结果
- 能够抛出受检异常
- 同样避免了单继承限制
缺点:
- 使用稍复杂,需要配合FutureTask
- get()方法会阻塞调用线程直到结果就绪
性能考虑:
FutureTask的get()方法会阻塞调用线程,在需要异步获取结果的场景中,可以考虑使用CompletableFuture等更高级的API。
1.4 使用线程池
线程池是生产环境中最推荐的线程管理方式。它通过复用线程来减少创建和销毁的开销,并提供更强大的任务管理能力。
java复制ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new MyRunnable());
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get());
executor.shutdown();
优点:
- 线程复用降低资源消耗
- 提供任务队列和拒绝策略等管理能力
- 支持批量提交任务
- 可以获取执行结果
缺点:
- 需要合理配置线程池参数
- 必须记得关闭线程池
线程池配置建议:
- 核心线程数:CPU密集型任务建议设置为CPU核心数+1
- 最大线程数:IO密集型任务可以设置较大值
- 队列选择:根据任务特性选择有界或无界队列
- 拒绝策略:根据业务需求选择合适的策略
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程状态与生命周期
理解线程的状态转换对于调试并发问题和设计高效的多线程程序至关重要。Java线程在其生命周期中会经历多种状态。
2.1 线程的六大状态
Java线程的状态由Thread.State枚举定义,共包含六种状态:
| 状态名称 | 描述 |
|---|---|
| NEW | 线程已创建但未启动 |
| RUNNABLE | 线程正在JVM中执行或等待操作系统资源 |
| BLOCKED | 线程因等待监视器锁而阻塞 |
| WAITING | 线程无限期等待其他线程执行特定操作 |
| TIMED_WAITING | 线程在指定时间内等待 |
| TERMINATED | 线程已完成执行 |
状态转换图:
mermaid复制graph TD
A[NEW] -->|start()| B[RUNNABLE]
B -->|synchronized竞争失败| C[BLOCKED]
C -->|获取锁| B
B -->|wait()| D[WAITING]
B -->|sleep()/wait(timeout)/join(timeout)| E[TIMED_WAITING]
D -->|notify()/notifyAll()| B
E -->|超时或notify()/notifyAll()| B
B -->|run()完成| F[TERMINATED]
2.2 关键状态转换详解
NEW → RUNNABLE:
- 触发条件:调用线程的start()方法
- 注意:start()只能调用一次,重复调用会抛出IllegalThreadStateException
RUNNABLE → BLOCKED:
- 场景:线程尝试进入synchronized代码块但锁被其他线程持有
- 特点:这是唯一由锁竞争直接导致的状态转换
RUNNABLE → WAITING:
- 触发方式:
- Object.wait()
- Thread.join()
- LockSupport.park()
- 特点:需要其他线程显式唤醒
RUNNABLE → TIMED_WAITING:
- 触发方式:
- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos()/parkUntil()
- 特点:会自动超时唤醒
WAITING/TIMED_WAITING → RUNNABLE:
- 唤醒方式:
- 对应的notify()/notifyAll()
- 中断interrupt()
- 超时(仅TIMED_WAITING)
2.3 线程优先级与守护线程
线程优先级
- 范围:1(MIN_PRIORITY)到10(MAX_PRIORITY),默认5(NORM_PRIORITY)
- 特点:
- 只是提示性,不保证严格执行
- 不同平台实现可能不同
- 极端优先级可能导致线程饥饿
java复制Thread thread = new Thread(() -> {});
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
守护线程
- 特点:
- 为其他线程提供服务
- JVM在所有用户线程结束后会退出,不管守护线程状态
- 必须在线程启动前设置
java复制Thread daemon = new Thread(() -> {
while(true) {
// 后台任务
}
});
daemon.setDaemon(true);
daemon.start();
使用注意事项:
- 不要用守护线程执行关键业务逻辑
- 守护线程中创建的子线程默认也是守护线程
- 守护线程的finally块不一定能执行
3. synchronized深度解析
synchronized是Java中最基本的同步机制,理解其实现原理对于编写高效并发代码至关重要。
3.1 对象监视器(Monitor)机制
每个Java对象都与一个监视器(Monitor)相关联,这是synchronized实现同步的基础。
对象内存布局:
- 对象头(Mark Word):存储哈希码、GC年龄、锁状态等信息
- 实例数据:对象字段的实际值
- 对齐填充:保证对象大小是8字节的倍数
Monitor关键字段:
- _owner:持有锁的线程
- _EntryList:等待获取锁的线程队列
- _WaitSet:调用wait()后等待的线程队列
- _count:锁的重入次数
锁获取流程:
- 线程执行monitorenter指令
- 检查_owner是否为空
- 为空:设置_owner为当前线程,_count=1
- 不为空但为当前线程:_count++
- 不为空且非当前线程:进入_EntryList等待
- 获取锁成功,执行同步代码
锁释放流程:
- 线程执行monitorexit指令
- _count--
- 如果_count==0,清空_owner
- 唤醒_EntryList中的等待线程
3.2 锁升级过程
JDK1.6引入了锁升级优化,根据竞争情况动态调整锁级别。
锁级别:
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
升级流程:
-
偏向锁:
- 场景:单线程重复获取锁
- 原理:在Mark Word中记录线程ID
- 优点:无竞争时开销极小
- 撤销:当第二个线程尝试获取锁时
-
轻量级锁:
- 场景:多线程交替获取锁
- 原理:通过CAS操作获取锁
- 特点:采用自旋方式避免线程阻塞
- 升级:自旋超过阈值或第三个线程竞争时
-
重量级锁:
- 场景:多线程激烈竞争
- 原理:使用操作系统互斥量
- 特点:线程会进入阻塞状态
- 开销:用户态/内核态切换成本高
锁升级触发条件:
| 当前状态 | 触发条件 | 新状态 |
|---|---|---|
| 无锁 | 第一个线程获取锁 | 偏向锁 |
| 偏向锁 | 第二个线程竞争 | 轻量级锁或重量级锁 |
| 轻量级锁 | 自旋失败或多线程竞争 | 重量级锁 |
3.3 JVM锁优化
除了锁升级,JVM还提供了其他锁优化技术。
锁消除:
- 原理:通过逃逸分析确定对象不会逃逸出当前线程
- 场景:方法内局部对象同步
- 效果:完全消除同步开销
java复制public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // 局部变量
sb.append(a).append(b);
return sb.toString();
}
锁粗化:
- 原理:将连续的锁操作合并
- 场景:循环内同步操作
- 效果:减少锁获取/释放次数
java复制// 优化前
for(int i=0; i<100; i++) {
synchronized(lock) {
// 操作
}
}
// 优化后
synchronized(lock) {
for(int i=0; i<100; i++) {
// 操作
}
}
自旋锁优化:
- 原理:竞争锁失败时不立即阻塞,而是自旋重试
- 适用:锁持有时间短的场景
- 实现:自适应自旋(根据历史成功率调整自旋次数)
4. volatile原理与应用
volatile是轻量级的同步机制,它保证了变量的可见性和有序性。
4.1 内存可见性实现
问题背景:
- CPU多级缓存导致内存可见性问题
- 线程可能读取到过期的缓存值
volatile解决方案:
- 写操作:强制刷新到主内存(写屏障)
- 读操作:强制从主内存读取(读屏障)
java复制volatile boolean flag = false;
// 线程A
flag = true; // 写屏障,立即刷新到主内存
// 线程B
while(!flag) { // 读屏障,每次从主内存读取
// 等待
}
内存屏障类型:
- LoadLoad屏障:禁止读操作重排序
- StoreStore屏障:禁止写操作重排序
- LoadStore屏障:禁止读后写重排序
- StoreLoad屏障:禁止写后读重排序
4.2 禁止指令重排序
问题背景:
- 编译器和处理器会进行指令重排序优化
- 可能导致多线程下的逻辑错误
经典案例:双重检查锁定(DCL)
java复制public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不加volatile的问题:
对象初始化可能被重排序为:
- 分配内存空间
- 设置引用指向内存
- 初始化对象
导致其他线程可能获取到未完全初始化的对象。
4.3 volatile的局限性
无法保证原子性:
- 复合操作(如i++)仍然需要同步
- 原因:i++包含读-改-写三个操作
解决方案:
- 使用synchronized
- 使用原子类(AtomicInteger等)
- 使用Lock
java复制// 使用原子类
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
// 使用Lock
Lock lock = new ReentrantLock();
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
适用场景:
- 状态标志位(如开关控制)
- 一次性安全发布
- 独立观察(定期更新值)
- 开销较低的读-写锁策略
5. 并发编程实践建议
5.1 线程创建选择
- 简单任务:Runnable
- 需要返回值:Callable
- 生产环境:线程池
- 避免直接继承Thread
5.2 锁使用建议
- 优先使用synchronized
- 高竞争场景考虑Lock
- 读写分离场景用ReadWriteLock
- 尽量减小同步块范围
- 避免嵌套锁
5.3 volatile使用场景
- 状态标志位
- 双重检查锁定
- 独立观察变量
- 内存屏障需求
5.4 性能优化方向
- 减少锁竞争
- 减小锁粒度
- 使用无锁数据结构
- 合理使用线程池
- 避免过早优化
并发调试技巧:
- 使用Thread Dump分析死锁
- 使用jstack工具
- 使用VisualVM等可视化工具
- 编写确定性测试用例
- 考虑使用并发测试工具(如JCStress)
