Java线程执行getFactors中途挂起,缓存更新后操作对象的疑问
不可变类OneValueCache的线程安全疑问解答
核心结论
线程A恢复执行后,操作的是更新前的cache对象。
原因分析
当线程A调用cache.getFactors(...)时,第一步会把当前cache引用指向的对象实例(也就是更新前的OneValueCache实例)加载到自己的工作内存中。后续整个方法的执行过程,都是基于这个已经获取到的实例进行操作。
即使线程B在此时更新了TestImmutable类中持有的cache引用(指向一个新的OneValueCache实例),线程A工作内存里的引用副本不会被自动同步,所以它只会继续操作最初拿到的那个旧实例。
这也是不可变类线程安全的关键之一:一旦实例创建完成,内部状态就不会改变,不管哪个线程持有该实例,操作时都不会出现状态不一致的问题;而引用的更新是原子操作,新线程会直接拿到新的实例,旧线程则继续操作旧实例,两者互不干扰。
可复现场景的代码示例
通过CountDownLatch可以精准控制线程挂起和恢复的时机,复现你描述的场景:
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 不可变类OneValueCache class OneValueCache { private final Integer number; private final Integer[] factors; public OneValueCache(Integer number, Integer[] factors) { this.number = number; this.factors = factors.clone(); // 防御性拷贝保证不可变性 } public Integer[] getFactors(Integer number) { if (this.number == null || !this.number.equals(number)) { return null; } try { // 模拟方法执行中途暂停,等待唤醒信号 TestImmutable.latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } return factors.clone(); } } public class TestImmutable { public static CountDownLatch latch = new CountDownLatch(1); private volatile OneValueCache cache = new OneValueCache(10, new Integer[]{2, 5}); public void updateCache(int number, Integer[] factors) { cache = new OneValueCache(number, factors); System.out.println("线程B已更新cache为number=" + number); } public void queryFactors(int number) { System.out.println("线程A开始查询number=" + number); Integer[] factors = cache.getFactors(number); if (factors != null) { StringBuilder sb = new StringBuilder(); for (int factor : factors) { sb.append(factor).append(","); } System.out.println("线程A查询结果:" + number + "的因数是" + sb.deleteCharAt(sb.length()-1)); } else { System.out.println("线程A未找到number=" + number + "的因数"); } } public static void main(String[] args) throws InterruptedException { TestImmutable test = new TestImmutable(); ExecutorService executor = Executors.newFixedThreadPool(2); // 启动线程A执行查询 executor.submit(() -> test.queryFactors(10)); // 等待线程A进入getFactors的暂停点 Thread.sleep(1000); // 启动线程B更新cache executor.submit(() -> test.updateCache(20, new Integer[]{2, 4, 5})).get(); // 唤醒线程A继续执行 latch.countDown(); executor.shutdown(); } }
代码输出说明
运行后会输出:
线程A开始查询number=10 线程B已更新cache为number=20 线程A查询结果:10的因数是2,5
可以看到,线程A最终返回的是旧cache中10的因数,证明它操作的是更新前的实例。
内容的提问来源于stack exchange,提问作者Unicorn
相关产品推荐
相关产品推荐

