Java中仅单线程更新不可变类引用、多线程读取是否需要加锁?
package com.blah; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import org.springframework.stereotype.Component; import lombok.Value; @Component public class AdminCredentialsProvider { @Value public class AuthInfo { String authToken; String xsrfToken; } private AuthInfo adminAuthInfo = null; private final ScheduledExecutorService adminAuthInfoRefresher = Executors.newSingleThreadScheduledExecutor(); //This will be called by multiple threads public AuthInfo getSiteAdminCredentials() { return new AuthInfo(adminAuthInfo.getAuthToken(), adminAuthInfo.getXsrfToken()); } @PostConstruct public void init() { adminAuthInfo = loginAsSiteAdmin(); Runnable runnable = () -> { //Do I need to protect this assignment with a lock at all adminAuthInfo = loginAsSiteAdmin(); }; adminAuthInfoRefresher.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.HOURS); } private AuthInfo loginAsSiteAdmin() { //call rest endpoint to login return new AuthInfo("mockToken", "mockXsrf"); } }
结论
这个场景不需要加锁,但现有代码存在可见性缺陷,仅需做轻量调整即可满足并发安全要求,无需引入ReentrantLock、ReadWriteLock这类重量同步工具。
详细分析
- 前置条件已经规避了大部分并发风险
- AuthInfo是Lombok
@Value注解修饰的不可变类,所有字段默认带final修饰,实例初始化后属性不会变更,天然线程安全。 adminAuthInfo的引用赋值操作只有两个执行节点:Spring初始化Bean时的主线程赋值、后续单线程调度器的定时更新,不存在多线程写竞争,而Java的引用赋值本身就是原子操作,不会出现半赋值的中间状态。
现有代码的唯一问题是可见性保障缺失
普通成员变量的修改没有happen-before规则兜底,调用getSiteAdminCredentials的业务线程可能迟迟感知不到adminAuthInfo的更新,长期读取到旧的凭证信息。最优修复方案(二选一即可,都不需要加锁)
- 给
adminAuthInfo加volatile修饰:volatile的写操作对后续所有读操作可见,刚好匹配单线程写、多线程读的场景,没有额外性能损耗。 - 把
adminAuthInfo替换为AtomicReference<AuthInfo>:本质和volatile原理一致,原子的get/set操作也能完全满足需求。
- 加锁属于过度设计
锁的核心作用是解决多线程竞争下的原子性保障问题,这个场景没有多线程写竞争,仅有的写操作本身就是原子的,加锁只会带来不必要的性能开销,没有实际意义。
额外优化点
当前getSiteAdminCredentials方法中不需要重新new AuthInfo对象,既然AuthInfo本身不可变,直接返回adminAuthInfo引用即可,减少不必要的对象创建。
内容的提问来源于stack exchange,提问作者arahant
相关产品推荐
相关产品推荐

