Java线程同步与内存模型详解
线程等待机制:join()方法
join()方法基础原理
在多线程编程中,join()
方法用于实现线程间的同步等待。当线程t1执行t2.join()
时,t1将进入阻塞状态,直到目标线程t2执行完毕。这种机制本质上是一种线程生命周期管理手段,确保关键线程完成工作后再继续后续流程。
Thread t2 = new Thread(() -> {// 子线程任务代码
});
t2.start();
t2.join(); // 主线程在此等待t2终止
System.out.println("t2线程已结束");
典型错误模式分析
未同步的线程问题常表现为执行顺序不可控。如以下错误示例:
public class JoinWrong {public static void main(String[] args) {Thread t1 = new Thread(() -> {for(int i=1; i<=5; i++) {System.out.println("Counter: " + i);Thread.sleep(1000);}});t1.start();System.out.println("We are done."); // 可能先于子线程输出}
}
该代码存在两个关键缺陷:
- 主线程与t1线程并行执行,输出顺序无法保证
- 未处理
InterruptedException
,可能导致意外中断
正确实现方式
通过join()
方法可确保线程顺序执行:
public class JoinRight {public static void main(String[] args) {Thread t1 = new Thread(() -> {// 模拟耗时操作for(int i=1; i<=5; i++) {System.out.println("Counter: " + i);Thread.sleep(1000);}});t1.start();try {t1.join(); // 主线程等待t1完成} catch (InterruptedException e) {e.printStackTrace();}System.out.println("We are done."); // 保证最后输出}
}
高级用法与注意事项
带超时的等待
join(long millis)
方法允许设置最大等待时间(毫秒):
t1.join(1000); // 最多等待1秒
当超时发生时:
- 若线程仍在运行,主线程将继续执行
- 需配合
isAlive()
检查线程状态
多线程等待策略
单个线程可依次等待多个线程完成:
t1.join();
t2.join();
t3.join();
// 所有线程完成后继续执行
特殊场景处理
- 未启动线程:对未启动线程调用
join()
会立即返回 - 已终止线程:对已终止线程调用
join()
会立即返回 - 自等待问题:线程调用自身
join()
会导致永久阻塞(需避免):
Thread.currentThread().join(); // 错误用法!
异常处理机制
join()
方法可能抛出InterruptedException
,表明等待过程被中断。健壮的代码应包含中断处理:
try {workerThread.join();
} catch (InterruptedException e) {// 1. 记录中断日志// 2. 恢复中断状态Thread.currentThread().interrupt();// 3. 执行清理操作
}
底层实现原理
从JVM角度看,join()
的实现依赖于wait/notify
机制:
- 调用
join()
时,JVM会在目标线程对象上执行wait()
- 当目标线程终止时,JVM自动调用
notifyAll()
- 等待线程被唤醒后继续执行
这种设计使得join()
成为构建线程间happens-before关系的重要工具,能保证被等待线程的所有操作结果对当前线程可见。
Java内存模型(JMM)核心概念
内存架构设计
Java内存模型采用主内存与线程工作内存分离的架构设计:
- 主内存:存储所有共享变量(实例字段、静态字段、数组元素)
- 工作内存:每个线程私有的存储空间(处理器缓存/寄存器),保存线程操作所需变量的副本
// 示例:两个线程共享主内存中的变量
class SharedData {static int counter = 0; // 存储于主内存
}Thread t1 = new Thread(() -> {int localCopy = SharedData.counter; // 从主内存读取到工作内存localCopy++;SharedData.counter = localCopy; // 写回主内存
});Thread t2 = new Thread(() -> {// 可能读取到未更新的值(可见性问题)System.out.println(SharedData.counter);
});
原子性规则
JMM保证以下操作的原子性:
- 除long/double外的基本类型读写(int, boolean等)
- volatile修饰的long/double变量
// 非原子操作示例
class AtomicityExample {long value; // 64位long类型在32位JVM上可能分两次写入void unsafeWrite(long v) {value = v; // 非原子操作}volatile long safeValue; // volatile保证原子性void safeWrite(long v) {safeValue = v; // 原子操作}
}
可见性机制
JMM通过以下规则保证内存可见性:
- volatile变量:
- 写操作立即刷新到主内存
- 读操作直接从主内存读取
- 同步块:
- 进入同步块时清空工作内存
- 退出同步块时刷新工作内存到主内存
- 线程终止:
- 线程结束时工作内存自动同步到主内存
class VisibilityDemo {// 无可见性保证boolean flag = false;// 保证可见性volatile boolean safeFlag = false;void writer() {flag = true; // 可能滞留工作内存safeFlag = true; // 立即写入主内存}void reader() {while(!safeFlag) { // 总是读取最新值// 自旋等待}System.out.println(flag); // 可能输出false}
}
指令重排序限制
JMM通过happens-before原则保证执行顺序:
- 程序顺序规则:线程内操作按代码顺序执行
- 锁规则:解锁操作先于后续加锁操作
- volatile规则:volatile写操作先于后续读操作
- 线程启动规则:线程start()先于该线程任何操作
- 线程终止规则:线程所有操作先于其终止检测
class ReorderingExample {int x = 0;boolean ready = false;void