java:基于volatile和Thread Local Storage的双重检查锁定(Double-checked locking)实现延迟初始化

总在同一个地方栽坑里是不可原谅的,以本文做个记录,以防下次再犯。
下面这段很简单的基于双重检查锁定(Double-checked locking)实现的延迟初始化(Lazy initialization)代码,还是让spotbugs找出了问题(感谢spotbugs)。
原因很简单,这种模式在java下无效,因为filedNames 变量不是线程可见的,具体原因涉及到java内存模型,网上已经有文章很深入的介绍,参见本文末尾的参考资料4

	private List<String> filedNames = null;
	public List<String> getFieldNames() {
	    // Double-checked locking
		if(null == filedNames){
			synchronized(this){
				if(null == filedNames){
					filedNames = doGetFieldNames();		
				}
			}
		}
		return filedNames;
	}

#解决方案1

把 fieldName 声明为volatile型,其他代码不变。
注意: 这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)。关于volatile关键字的含义参见参考资料。

	/** JDK5 required */
	private volatile List<String> filedNames = null;
	public List<String> getFieldNames() {
	    // Double-checked locking
		if(null == filedNames){
			synchronized(this){
				if(null == filedNames){
					filedNames = doGetFieldNames();		
				}
			}
		}
		return filedNames;
	}

#解决方案2
基于线程本地存储TLS(Thread Local Storage)实现双重检查锁定,这个方法也是 [《The “Double-Checked Locking is Broken” Declaration》][1]论文中提出的方案之一。
这个方案不需要volatile关键字支持当然也就没有JDK5以上版本的要求,但是略复杂,多了一个ThreadLocal变量,并且分成了两个方法。

	private volatile List<String> filedNames = null;
	/** 如果perThreadInstance.get() 返回一个非null值,说明 filedNames 已经被初始化过了 */
	@SuppressWarnings("rawtypes")
	private final ThreadLocal perThreadInstance = new ThreadLocal();
	public List<String> getFieldNames() {
		this.assertJavaBean();
		if(null == perThreadInstance.get()){
			initFieldNames();
		}
		return filedNames;
	}
	/** 同步初始化filedNames变量 */
	@SuppressWarnings({ "unchecked" })
	private void initFieldNames() {
		synchronized(this){
			if(null == filedNames){
				filedNames = doGetFieldNames();		
			}
		}
		// 给perThreadInstance设置一个非null值
		perThreadInstance.set(perThreadInstance);
	}

#通用化封装
说话仅仅一个延迟初始化就整出这么多问题,搞出这么多代码,虽然问题解决了,但对于我这个懒人来说实在太复杂了,如果项目中还有多个地方要用到延迟初始化,每个都要这么写代码实在是一件非常痛苦的事儿。
既然原理搞明白了,那么把这两种延迟初始化的解决方案用用泛型类封装一下不就可以复用了么?

于是我很快把上面的代码做了封装,顶层是接口类ILazyInitVariable,只有一个接口方法get(),接下来是个中间BaseLazyVar抽象类定义一个doGet()方法用于具体的初始化,BaseTls和BaseVolatile分别是基于前述方案2和方案1的具体实现类(也是抽象类,具体doGet()方法还是需要子类来完成)
这里写图片描述

完整代码如下

ILazyInitVariable.java

接口定义 ILazyInitVariable.java,中间抽象类BaseLazyVar也在其中

package gu.simplemq;

/**
 * 延迟初始化(Lazy initialization)变量封装接口
 * @author guyadong
 *
 * @param <T> 延迟变量类型
 */
public interface ILazyInitVariable<T> {
	public static abstract class BaseLazyVar<T> implements ILazyInitVariable<T>{
		/**
		 * 返回 T 实例
		 * @return
		 */
		abstract protected T doGet() ;
	}
	/**
	 * 返回延迟初始化的 T 实例 
	 * @return
	 */
	public T get();
}

##BaseVolatile.java

package gu.simplemq;

/**
 * 基于volatile的双重检查锁定实现{@link ILazyInitVariable}的抽象类<br>
 * 要求 JDK5 以上版本 
 * @author guyadong
 *
 * @param <T> variable type
 */
public abstract class BaseVolatile<T> extends ILazyInitVariable.BaseLazyVar<T>{
	private volatile T var = null;
	public BaseVolatile() {
	}
	@Override
	public T get() {
		// Double-checked locking
		if(null == var){
			synchronized(this){
				if(null == var){
					var = doGet();
				}
			}
		}
		return var;
	}
}

##BaseTls.java

package gu.simplemq;

/**
 * 基于Thread Local Storage的双重检查锁定实现{@link ILazyInitVariable}的抽象类<br>
 * @author guyadong
 *
 * @param <T> variable type
 */
public abstract class BaseTls<T> extends ILazyInitVariable.BaseLazyVar<T> {
	/**
	 * If perThreadInstance.get() returns a non-null value, this thread has done
	 * synchronization needed to see initialization of helper
	 */
	@SuppressWarnings("rawtypes")
	private final ThreadLocal perThreadInstance = new ThreadLocal();
	private T var = null;

	public BaseTls() {
	}

	@Override
	public T get() {
		if (null == perThreadInstance.get()) {
			initFieldNames();
		}
		return var;
	}

	@SuppressWarnings({ "unchecked" })
	private void initFieldNames() {
		synchronized (this) {
			if (null == var) {
				var = doGet();
			}
		}
		// Any non-null value would do as the argument here
		perThreadInstance.set(perThreadInstance);
	}
}

##使用示例
有了通用化封装,以用BaseVolatile为例

本文最开始的延迟初始化代码就很简单了:

	// filedNames  定义为ILazyInitVariable接口实例,并用BaseVolatile类实例化
	private final ILazyInitVariable<List<String>>filedNames = new BaseVolatile<List<String>>(){
		@Override
		protected List<String> doGet() {
		    // 调用初始化方法
			return doGetFieldNames();
		}};
	public List<String> getFieldNames() {
		return filedNames.get();
	}

#参考资料

  1. [《The “Double-Checked Locking is Broken” Declaration》][1]
  2. [《Lazy initialization》][2]
  3. [《Double-checked locking》][3]
  4. [《双重检查锁定与延迟初始化》][4]
  5. [《双重检查锁定失败可能性——参照《The “Double-Checked Locking is Broken” Declaration》》][5]
  6. [《Java中Volatile关键字详解》][6]
    [1]:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
    [2]:https://en.wikipedia.org/wiki/Lazy_initialization
    [3]:https://en.wikipedia.org/wiki/Double-checked_locking
    [4]:http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
    [5]:http://freish.iteye.com/blog/1008304
    [6]:https://www.cnblogs.com/zhengbin/p/5654805.html
<think>我们之前已经介绍了双重检查锁定在单例模式中的应用。现在我们来探讨双重检查锁定适用的场景以及具体的例子。 双重检查锁定Double-Checked Locking)主要用于减少同步的开销,尤其是在初始化某个资源(如创建单例对象)时。它适用于以下场景: 1. 需要延迟初始化(Lazy Initialization)且初始化成本较高的资源。 2. 需要线程安全地初始化共享资源,但后续访问不需要同步。 3. 希望降低同步带来的性能开销(避免每次访问都进行同步)。 下面我们举一个具体的例子:缓存系统的初始化。 假设我们有一个缓存管理器(CacheManager),它负责加载配置并初始化缓存。初始化过程比较耗时,所以我们希望在第一次使用时才初始化(懒加载),并且要保证多线程环境下只初始化一次。 使用双重检查锁定实现: </think>### 双重检查锁定Double-Checked Locking)的应用场景与实例 双重检查锁定主要用于解决**高并发环境下资源初始化的线程安全问题**,特别适合以下场景: #### ✅ 典型应用场景 1. **延迟初始化(Lazy Initialization)** -初始化成本昂贵(如数据库连接、大型对象) - 且可能不会立即使用时,避免不必要的资源消耗 2. **单例模式实现** - 需要全局唯一实例的场景 - 实例初始化开销大需延迟创建 3. **资源池管理** - 线程池/连接池的初始化 - 缓存系统的启动 4. **配置加载** - 全局配置文件的加载解析 - 避免多线程重复加载 #### 📝 实际代码示例:数据库连接管理器 ```java import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DatabaseConnectionManager { // volatile保证可见性禁止指令重排序 private static volatile Connection instance; // 私有构造器防止外部实例化 private DatabaseConnectionManager() {} public static Connection getConnection() { // 第一次检查:避免不必要的同步 if (instance == null) { synchronized (DatabaseConnectionManager.class) { // 第二次检查:防止重复创建 if (instance == null) { try { // 创建高成本数据库连接 String url = "jdbc:mysql://localhost:3306/mydb"; String user = "root"; String password = "password"; instance = DriverManager.getConnection(url, user, password); System.out.println("Database connection established"); } catch (SQLException e) { throw new RuntimeException("Failed to create DB connection", e); } } } } return instance; } } ``` #### 🌟 场景优势分析 1. **性能优化** ```java // 99%的调用直接返回实例,避免同步锁开销 if (instance != null) return instance; ``` 2. **线程安全** ```java synchronized (...) { // 保证只有一个线程执行初始化 if (instance == null) { // 二次检查防重复创建 instance = new ... // 安全创建 } } ``` 3. **资源节约** ```java // 避免每次调用都创建连接 // 只有当真正需要时才初始化 ``` 4. **延迟加载** ```java // 系统启动时不立即初始化 // 首次调用getConnection()时才创建 ``` #### ⚠ 使用注意事项 1. **必须使用`volatile`**(JDK 5+) 防止指令重排序导致返回未完全初始化的对象 2. **初始化逻辑不可抛异常** 静态实例初始化失败会导致后续调用永远失败 3. **不适合简单对象** 轻量级对象可直接用静态内部类方式: ```java public class Singleton { private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值