带参数的按需初始化持有者惯用法实现及并发问题咨询
问题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
相关产品推荐
相关产品推荐

