✅ 一、局部变量 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. 使用线程安全的可变容器

    1
    2
    AtomicInteger count = new AtomicInteger(0);
    AtomicReference<StringBuilder> logRef = new AtomicReference<>(new StringBuilder());
  2. 封装到自定义堆对象中(确保不被多线程共享):

    1
    2
    3
    class Context { int count; StringBuilder log; }
    Context ctx = new Context();
    CompletableFuture.runAsync(() -> ctx.count++);

✅ 只要每个调用创建独立对象,且仅被一个异步任务使用 → 线程安全

✅ 六、Spring Bean 与线程安全

  • Spring 默认 Bean 是 单例(Singleton)
  • 所有请求共享同一个实例;
  • 因此:
    • ❌ 避免在 Bean 中使用可变的实例变量存储请求级数据;
    • ✅ 尽量让方法无状态(stateless),只用局部变量;
    • ✅ 如需状态,使用 ThreadLocal、方法参数传递、或每次新建对象。

© 2024 竹林听雨 使用 Stellar 创建
总访问 113 次 | 本页访问 26