You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带参数的按需初始化持有者惯用法实现及并发问题咨询

问题1:按需初始化持有者模式传递参数的方案

传统的按需初始化持有者模式依赖静态内部类的延迟加载特性,JVM会保证静态内部类在首次被访问时才初始化,且这个过程是线程安全的,但它本身不支持直接传递构造参数。要适配带参构造的场景,有两种可行方案:

方案一:提前通过静态方法设置参数,再获取实例

这种方式要求先调用参数设置方法,确保在获取实例前参数已被正确初始化,后续所有getInstance调用都会使用这个参数创建实例:

public class Api {
    private String basePath1;
    private String basePath2;

    private StringBuilder temporalEndpoint;
    private StringBuilder spatialEndpoint;
    private StringBuilder fileUploadEndpoint;
    private StringBuilder fileDownloadEndpoint;
    private StringBuilder fileDeleteEndpoint;
    private StringBuilder listMetaDataEndpoint;
    private static final Logger LOG = LogManager.getLogger(Api.class);

    // 静态参数容器,必须在getInstance前设置
    private static volatile String initBasePath1;
    private static volatile String initBasePath2;

    private Api() {
        this.basePath1 = initBasePath1;
        this.basePath2 = initBasePath2;
        buildEndpoints();
    }

    // 先调用此方法设置初始化参数
    public static void setInitParams(String basePath1, String basePath2) {
        if (initBasePath1 != null || initBasePath2 != null) {
            throw new IllegalStateException("参数已初始化,不可重复设置");
        }
        synchronized (Api.class) {
            if (initBasePath1 != null || initBasePath2 != null) {
                throw new IllegalStateException("参数已初始化,不可重复设置");
            }
            initBasePath1 = basePath1;
            initBasePath2 = basePath2;
        }
    }

    // 持有者内部类
    private static class Holder {
        private static final Api INSTANCE = new Api();
    }

    public static Api getInstance() {
        if (initBasePath1 == null || initBasePath2 == null) {
            throw new IllegalStateException("请先调用setInitParams设置参数");
        }
        return Holder.INSTANCE;
    }

    public void buildEndpoints() {
        // 原方法实现不变
        temporalEndpoint = new StringBuilder(basePath1).append(API_TEMPORAL);
        spatialEndpoint = new StringBuilder(basePath1).append(API_SPATIAL);
        fileUploadEndpoint = new StringBuilder(basePath1).append(API_FILE_UPLOAD);
        fileDownloadEndpoint = new StringBuilder(basePath1).append(API_FILE_DOWNLOAD);
        fileDeleteEndpoint = new StringBuilder(basePath2).append(API_FILE_DELETE);
        listMetaDataEndpoint = new StringBuilder(basePath2).append(API_LIST_METADATA);
    }
}

注意:参数只能设置一次,且必须在第一次调用getInstance前完成,否则会抛出异常。

方案二:让持有者类持有参数和实例,同步控制初始化

如果需要在调用getInstance时传入参数,可通过同步块确保参数与实例初始化的原子性,同时检查后续调用的参数一致性:

public class Api {
    private String basePath1;
    private String basePath2;

    private StringBuilder temporalEndpoint;
    private StringBuilder spatialEndpoint;
    private StringBuilder fileUploadEndpoint;
    private StringBuilder fileDownloadEndpoint;
    private StringBuilder fileDeleteEndpoint;
    private StringBuilder listMetaDataEndpoint;
    private static final Logger LOG = LogManager.getLogger(Api.class);

    private Api(String basePath1, String basePath2) {
        this.basePath1 = basePath1;
        this.basePath2 = basePath2;
        buildEndpoints();
    }

    private static class Holder {
        private static Api INSTANCE;
        private static String BASE_PATH1;
        private static String BASE_PATH2;
    }

    public static Api getInstance(String basePath1, String basePath2) {
        if (Holder.INSTANCE == null) {
            synchronized (Holder.class) {
                if (Holder.INSTANCE == null) {
                    // 检查参数一致性,避免多次调用传入不同参数
                    if (Holder.BASE_PATH1 != null && !Holder.BASE_PATH1.equals(basePath1)
                        || Holder.BASE_PATH2 != null && !Holder.BASE_PATH2.equals(basePath2)) {
                        throw new IllegalArgumentException("传入参数与首次初始化参数不一致");
                    }
                    Holder.BASE_PATH1 = basePath1;
                    Holder.BASE_PATH2 = basePath2;
                    Holder.INSTANCE = new Api(basePath1, basePath2);
                }
            }
        }
        return Holder.INSTANCE;
    }

    public void buildEndpoints() {
        // 原方法实现不变
        temporalEndpoint = new StringBuilder(basePath1).append(API_TEMPORAL);
        spatialEndpoint = new StringBuilder(basePath1).append(API_SPATIAL);
        fileUploadEndpoint = new StringBuilder(basePath1).append(API_FILE_UPLOAD);
        fileDownloadEndpoint = new StringBuilder(basePath1).append(API_FILE_DOWNLOAD);
        fileDeleteEndpoint = new StringBuilder(basePath2).append(API_FILE_DELETE);
        listMetaDataEndpoint = new StringBuilder(basePath2).append(API_LIST_METADATA);
    }
}

这种方式下,第一次调用getInstance的参数会作为实例的初始化参数,后续调用必须传入相同参数,否则抛出异常。


问题2:并发问题分析

当前双重检查锁定(DCL)代码的并发安全性

你当前的代码中,apiInstance被声明为volatile,在Java 5及以上版本中是线程安全的。volatile关键字会禁止JVM对对象初始化的指令重排序,避免了其他线程看到半初始化的Api实例(比如basePath1还未赋值,但apiInstance已不为null的情况)。

但存在业务逻辑风险:如果多次调用getInstance传入不同的basePath1/basePath2,第一次初始化后,后续调用会直接返回已存在的实例,忽略后续传入的参数,可能导致实例使用的参数不符合预期。

改用按需初始化持有者模式后的并发安全性

  • 方案一:静态内部类Holder的初始化由JVM保证线程安全,setInitParams方法通过双重检查锁定和同步块确保参数只能被设置一次,整体线程安全。只要调用顺序正确(先设置参数,再获取实例),就不会有并发问题。
  • 方案二:通过同步块控制Holder.INSTANCE的初始化,同时检查参数一致性,确保只有第一次调用的参数生效,同步块保证了初始化过程的原子性,整体线程安全。

两种方案都依赖单例的核心特性——实例唯一,因此参数必须全局唯一,不能在运行时动态修改,否则会违背单例设计的初衷。


内容的提问来源于stack exchange,提问作者Shreelakshmi Joshi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 11:37:00