java:延迟加载实现方案对比:双重检查锁定 vs 原子化条件更新

在多线程编程中,延迟加载是一种常见的优化策略,它可以推迟对象的创建直到首次使用时。这种方式尤其适用于创建成本较高的对象或者可能在整个程序生命周期中并未使用的对象。本文将深入探讨两种在 Java 中实现线程安全延迟加载的方案:经典的双重检查锁定(Double-Checked Locking)和现代的原子化条件更新(Atomic Conditional Update)。

场景示例

假设我们需要实现一个线程安全的 Redis 参数管理类,该类在首次访问时初始化参数(redisParam),并且只初始化一次:

java 复制代码
class RedisManagement {
    private final AtomicReference<ImmutableMap<MQParam,String>> redisParam = new AtomicReference<>();
    
    // 参数初始化相关方法
    private String getRedisURI() { /* ... */ }
    private String createCmdChannel() { /* ... */ }
    private String createLogMonitorChannel() { /* ... */ }
    private String createHeartbeatMonitorChannel() { /* ... */ }
}

方案一:双重检查锁定(Double-Checked Locking)

双重检查锁定是实现延迟初始化的经典模式,它通过两次检查来避免不必要的同步开销。

实现示例

java 复制代码
protected Map<MQParam,String> getRedisParameters(){
    // 第一次检查:避免不必要的同步
    if (redisParam.get() == null) {
        // 同步块确保线程安全
        synchronized (redisParam) {
            // 第二次检查:确保只初始化一次
            if (redisParam.get() == null) {
                Builder<MQParam, String> builder = ImmutableMap.<MQParam,String>builder()
                    .put(MQParam.REDIS_URI, getRedisURI())
                    .put(MQParam.CMD_CHANNEL, createCmdChannel())
                    .put(MQParam.LOG_MONITOR_CHANNEL, createLogMonitorChannel())
                    .put(MQParam.HB_MONITOR_CHANNEL, createHeartbeatMonitorChannel())
                    .put(MQParam.HB_INTERVAL, CONFIG.getInteger(HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_PERIOD).toString())
                    .put(MQParam.HB_EXPIRE, CONFIG.getInteger(HEARTBEAT_EXPIRE, DEFAULT_HEARTBEAT_EXPIRE).toString());
                
                if(!Strings.isNullOrEmpty(webredisURL)){
                    builder.put(MQParam.WEBREDIS_URL, webredisURL);
                }
                
                // 设置初始化后的值
                redisParam.set(builder.build());
                
                // 执行一次性副作用操作(如日志记录)
                GlobalConfig.logRedisParameters(
                    JedisPoolLazys.NAMED_POOLS.getDefaultPool().getParameters());
            }
        }
    }
    return redisParam.get();
}

优点

  1. 性能优化:对象初始化后,后续访问不再需要同步开销
  2. 意图明确:这是一种广为人知的设计模式,大多数 Java 开发者都能理解
  3. 精细控制:可以精确控制何时执行副作用操作(如日志记录)

缺点

  1. 代码冗长:需要显式的 null 检查和同步块
  2. 容易出错:实现时容易遗漏 volatile 关键字或其他细节
  3. 样板代码:存在大量重复的检查和同步代码

方案二:原子化条件更新(Atomic Conditional Update)

这是一种更现代化的方法,利用 AtomicReference 的原子操作来实现线程安全的延迟初始化。

实现示例

java 复制代码
protected Map<MQParam,String> getRedisParameters(){
    return redisParam.updateAndGet(v -> {
        // 只有在值为 null 时才执行初始化逻辑
        if (v == null) {
            Builder<MQParam, String> builder = ImmutableMap.<MQParam,String>builder()
                .put(MQParam.REDIS_URI, getRedisURI())
                .put(MQParam.CMD_CHANNEL, createCmdChannel())
                .put(MQParam.LOG_MONITOR_CHANNEL, createLogMonitorChannel())
                .put(MQParam.HB_MONITOR_CHANNEL, createHeartbeatMonitorChannel())
                .put(MQParam.HB_INTERVAL, CONFIG.getInteger(HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_PERIOD).toString())
                .put(MQParam.HB_EXPIRE, CONFIG.getInteger(HEARTBEAT_EXPIRE, DEFAULT_HEARTBEAT_EXPIRE).toString());
            
            if(!Strings.isNullOrEmpty(webredisURL)){
                builder.put(MQParam.WEBREDIS_URL, webredisURL);
            }
            
            // 执行一次性副作用操作(如日志记录)
            GlobalConfig.logRedisParameters(
                JedisPoolLazys.NAMED_POOLS.getDefaultPool().getParameters());
            
            // 返回新构建的对象
            return builder.build();
        }
        // 否则返回原有值
        return v;
    });
}

工作原理分析

根据 OpenJDK 的实现,AtomicReference.updateAndGet方法的核心逻辑如下:

java 复制代码
public final V updateAndGet(UnaryOperator<V> updateFunction) {
    V prev, next;
    do {
        prev = get();
        next = updateFunction.apply(prev);
    } while (!compareAndSet(prev, next));
    return next;
}

这个方法的工作原理是:

  1. 获取当前值(prev)
  2. 将当前值传递给 updateFunction 函数,获得新值(next)
  3. 使用 compare-and-swap(CAS)操作尝试用新值替换当前值
  4. 如果 CAS 操作失败(说明有其他线程同时修改了值),则重复以上步骤
  5. 最终返回更新后的值

在我们的延迟加载场景中,这意味着只有当当前值为 null 时,才会执行初始化逻辑并创建新对象。如果其他线程已经完成了初始化,那么 updateFunction 会直接返回已存在的值,而 compareAndSet 会发现值没有变化,从而不会进行任何更新。

优点

  1. 代码简洁:消除了显式的同步块和重复检查
  2. 原子性强:整个检查-初始化-设置过程在一个原子操作中完成
  3. 不易出错:不需要手动管理同步,降低了实现难度
  4. 现代化:符合函数式编程的思想

缺点

  1. 学习曲线:对于不熟悉函数式编程的开发者可能不够直观
  2. 细微差异:Lambda 表达式始终会被调用,尽管通常很快返回(现代 JVM 会优化这种情况)

性能对比

在大多数情况下,两种方案的性能差异可以忽略不计:

  • 双重检查锁定在对象初始化后完全没有额外开销
  • AtomicReference 方案每次调用都有极小的函数调用开销,但现代 JVM 能够很好地优化这种情况

如何选择?

选择双重检查锁定当你:

  1. 需要极致的性能优化
  2. 团队对经典设计模式更熟悉
  3. 需要精确控制副作用的执行时机

选择原子化条件更新当你:

  1. 希望代码更简洁、现代化
  2. 团队熟悉函数式编程概念
  3. 希望降低并发编程的复杂性

总结

两种方案都是实现线程安全延迟加载的有效方法。双重检查锁定是经典且经过验证的方式,而原子化条件更新则是更现代化的选择。选择哪种方案取决于团队的技术偏好、代码维护性要求以及具体的性能需求。

无论选择哪种方案,重要的是要理解其实现原理,并确保正确处理初始化过程中的副作用操作,比如日志记录或资源分配。

相关推荐
独自归家的兔2 小时前
千问通义plus - 代码解释器的使用
java·人工智能
嘟嘟w2 小时前
什么是UUID,怎么组成的?
java
通往曙光的路上2 小时前
认证--JSON
java
期待のcode2 小时前
springboot热部署
java·spring boot·后端
222you3 小时前
Spring框架的介绍和IoC入门
java·后端·spring
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于Java的人体骨骼健康知识普及系统为例,包含答辩的问题和答案
java·开发语言
喵手3 小时前
集合框架概述:让数据操作更高效、更灵活!
java·集合·集合框架
Java爱好狂.3 小时前
如何用JAVA技术设计一个高并发系统?
java·数据库·高并发·架构设计·java面试·java架构师·java八股文
sheji34163 小时前
【开题答辩全过程】以 基于JAVA的社团管理系统为例,包含答辩的问题和答案
java·开发语言