
当一个线程正在执行 cache.getFactors() 时,即使另一线程将 cache 引用更新为新对象,原调用仍继续在原始对象上完成,不会切换到新对象——因为方法调用的目标对象在调用开始时就已确定。
当一个线程正在执行 `cache.getfactors()` 时,即使另一线程将 `cache` 引用更新为新对象,原调用仍继续在**原始对象上完成**,不会切换到新对象——因为方法调用的目标对象在调用开始时就已确定。
在 Java 中,方法调用绑定的是对象实例,而非字段引用本身。以 cache.getFactors(i, sec) 为例:该表达式首先读取 cache 字段的当前值(即某个 OneValueCache 实例的引用),然后将该引用压栈,并跳转至该实例所属类的 getFactors 方法字节码执行。这一过程是原子性的——一旦调用指令触发,目标对象(如 O₁)即被锁定为本次调用的接收者(receiver),后续无论 cache 字段被其他线程如何重新赋值(例如指向新创建的 O₂),都完全不影响正在进行中的 getFactors 执行上下文。
这一点可通过以下简化示例验证:
class Counter {
final int value;
Counter(int v) { this.value = v; }
int getAndSleep() {
try { Thread.sleep(100); } catch (InterruptedException e) {}
return value; // 始终返回构造时设定的 value
}
}
public class RaceDemo {
static volatile Counter counter = new Counter(1);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("Thread-1 calling getAndSleep()...");
int result = counter.getAndSleep(); // 此刻 counter 仍指向 Counter(1)
System.out.println("Thread-1 got: " + result);
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(10); } catch (InterruptedException e) {}
counter = new Counter(999); // 在 t1 调用中途修改引用
System.out.println("Thread-2 updated counter to Counter(999)");
});
t1.start(); t2.start();
t1.join(); t2.join();
}
}
运行输出通常为:
Thread-1 calling getAndSleep()... Thread-2 updated counter to Counter(999) Thread-1 got: 1
这清晰表明:t1 中 counter.getAndSleep() 的执行始终作用于最初那个 Counter(1) 实例,其 value 字段值不会因 counter 引用被重置而改变。
⚠️ 关键注意事项:
- ✅ 安全性前提:上述行为成立的前提是 OneValueCache 是真正不可变的(final 字段 + 无修改状态的方法),否则即使目标对象不变,多线程并发读写其内部状态仍可能导致数据不一致;
- ❌ 非原子性陷阱:虽然单个方法调用目标固定,但若业务逻辑依赖多次读取同一字段(如先 if (cache != null) 再 cache.getFactors()),则可能因中间引用变更引发 NullPointerException 或逻辑错误——此时需用 synchronized、Lock 或 AtomicReference 等机制保障临界区一致性;
- ? volatile 的作用边界:volatile 仅保证 cache 引用的可见性与有序性,确保新线程能立即看到最新引用值,但它不提供对方法调用过程的保护,也不影响已启动调用的执行目标。
总结而言,Java 的方法调用模型天然具备“调用快照”特性:每个方法调用在启动瞬间就捕获了接收者对象的身份。这是理解并发中对象生命周期与引用语义的基础,也是合理设计无锁缓存(如 OneValueCache)和线程安全类的重要依据。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











