java:基于volatile和Thread Local Storage的双重检查锁定实现延迟初始化

总在同一个地方栽坑里是不可原谅的,以本文做个记录,以防下次再犯。 下面这段很简单的基于双重检查锁定(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》论文中提出的方案之一。 这个方案不需要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》
  2. 《Lazy initialization》
  3. 《Double-checked locking》
  4. 《双重检查锁定与延迟初始化》
  5. 《双重检查锁定失败可能性——参照《The “Double-Checked Locking is Broken” Declaration》》
  6. 《Java中Volatile关键字详解》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java Edge

Netty 源码深度解析(九) - 编码概述1 抽象类 MessageToByteEncoder2 抽象类 MessageToMessageEncoder一个java对象最后是如何转变成字节流,写到s

编码器实现了ChannelOutboundHandler,并将出站数据从 一种格式转换为另一种格式,和我们方才学习的解码器的功能正好相反。Netty 提供...

1011
来自专栏程序你好

最近我遇到的10个Java面试问题

最近,我参加了一些java的面试。突然,我有了一个想法,我想和大家分享我的经历。我希望我能通过分享我最近几个月遇到的10个Java面试问题来帮助大家。

703
来自专栏博岩Java大讲堂

Java虚拟机--对象内存布局

3366
来自专栏Java学习网

Java面试题系列之基础部分(六)——每天学5个问题

Java基础部分学习的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io的语法,虚拟机方面的语法,这些都是最基...

2445
来自专栏java达人

方法参数过多怎么办

我们在编程或阅读前人的代码时,经常会看到多个参数的方法,有的甚至达到二十个,看得人眼花缭乱,不便于阅读和维护,而且参数很容易混淆,如两个参数类型同为short型...

2318
来自专栏blackheart的专栏

[程序设计语言]-[核心概念]-02:名字、作用域和约束(Bindings)

1.名字、约束时间(Binding Time) 在本篇博文开始前先介绍两个约定:第一个是“对象”,除非在介绍面向对象语言时,本系列中出现的对象均是指任何可以有名...

2148
来自专栏FD的专栏

写出形似QML的C++代码

我的第一个想法(居然?)是做个Embedded-DSL。不过C++又不是Ruby……随便搜了一下,发现了一篇文章,也只是利用了重载运算符和运算符优先级,看上去限...

542
来自专栏余林丰

Effective Java通俗理解(下)

第31条:用实例域代替序数   枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用...

2229
来自专栏java一日一条

最全面的 Android 编码规范指南

这份文档参考了 Google Java 编程风格规范和 Google 官方 Android 编码风格规范。该文档仅供参考,只要形成一个统一的风格,见量知其意就可...

994
来自专栏逆向技术

win32编程简介

  我们要编写windos程序.都离不开API. 也就是我们所说的win32程序. 所以学好win32是你能不能再windows下编写程序的基础.

1023

扫码关注云+社区