✅ 一、局部变量 vs 成员变量
| 特性 | 局部变量(方法内) | 成员变量(类字段) |
|---|---|---|
| 存储位置 | 栈(Stack) | 堆(Heap,随对象存在) |
| 默认值 | ❌ 没有,默认必须显式初始化 | ✅ 有(0, false, null 等) |
| 生命周期 | 方法调用期间 | 对象存在期间 |
| 线程安全性 | 若不逃逸 → ✅ 安全 若引用共享对象 → ❌ 可能不安全 |
默认 ❌ 不安全(单例 Bean 中多线程共享) |
📌 关键原则:局部变量本身是线程私有的,但它引用的对象可能被共享。
✅ 二、线程安全判断核心准则
🔐 一个方法是否线程安全,取决于是否存在「多个线程共享可变状态」。
- ✅ 安全:只操作局部创建
的对象,且对象不逃逸到方法外(如不赋给成员变量、不返回、不传给共享组件)。 - ❌ 不安全:操作了共享的可变状态(如实例变量、静态变量),且该状态非线程安全(如
ArrayList,HashMap)。
💡 “对象逃逸”是关键:即使变量是局部的,只要它指向的对象被多个线程访问,就可能不安全。
✅ 三、Lambda / 匿名内部类对局部变量的限制
规则:
被 lambda 或匿名内部类捕获的局部变量必须是 effectively final(实质上不可变)。
- ✅ 允许:
- 读取基本类型变量(如
int x = 5; System.out.println(x);) - 调用引用类型对象的方法(如
log.append(...))
- 读取基本类型变量(如
- ❌ 禁止:
- 修改基本类型变量(
x++) - 重新赋值引用变量(
log = new StringBuilder();)
- 修改基本类型变量(
为什么?
- 编译器会将局部变量的值(基本类型)或引用(对象地址)拷贝到内部类中;
- 如果原变量后续被修改,会导致拷贝值与原始值不一致,引发逻辑错误;
- 所以 Java 强制要求:被捕获的局部变量不能变。
⚠️ 注意:这个限制与是否用 lambda 无关,匿名内部类同样受此约束。
✅ 四、对象 vs 变量:关键区分
| 操作 | 修改的是? | 是否违反 effectively final? |
|---|---|---|
count++ |
局部变量本身的值(栈上) | ✅ 违反 |
log.append("...") |
堆上对象的内容 | ❌ 不违反(log 引用未变) |
log = new StringBuilder() |
局部引用变量的值(栈上地址) | ✅ 违反 |
🧠 记住:
- 变量 ≠ 对象
- 引用变量的值 = 地址
- 对象的内容 = 堆上的状态
✅ 五、如何实现“可变但安全”的异步操作?
当需要在 lambda/异步任务中修改状态时:
推荐方案:
使用线程安全的可变容器:
1
2AtomicInteger count = new AtomicInteger(0);
AtomicReference<StringBuilder> logRef = new AtomicReference<>(new StringBuilder());封装到自定义堆对象中(确保不被多线程共享):
1
2
3class Context { int count; StringBuilder log; }
Context ctx = new Context();
CompletableFuture.runAsync(() -> ctx.count++);
✅ 只要每个调用创建独立对象,且仅被一个异步任务使用 → 线程安全。
✅ 六、Spring Bean 与线程安全
- Spring 默认 Bean 是 单例(Singleton);
- 所有请求共享同一个实例;
- 因此:
- ❌ 避免在 Bean 中使用可变的实例变量存储请求级数据;
- ✅ 尽量让方法无状态(stateless),只用局部变量;
- ✅ 如需状态,使用
ThreadLocal、方法参数传递、或每次新建对象。