【翻译】高效的double-checked线程锁

      代码的性能是最重要的。然而,在当今复杂的多线程移动应用世界里,我们常常会为保证内存数据的一致性而牺牲一些性能。线程竞争条件的设计和调试是一件非常耗时,且容易令人沮丧的工作,所以线程被锁定太长时间的情况并不少见。幸运的是,现在有一些简单的模式可以使锁定变得更有效率,从而避免对性能产生不必要的影响。

      首先,让我们先预览一下只有简单 setter 代码的基类:

public class Foo {
    private Map<string, string=""> data;

    public Foo() {
        data = new HashMap<string, string="">();
    }

    public void setData(String key, String value) {
        data.put(key, value);
    }
}

       在上面的代码里,每当我们实例化一个Foo对象的同时也在实例化一个HashMap对象,而无论该HashMap是否会被使用。一般情况下,在一个强劲配置的服务器上,前面实例化的开销相对较低。但是相对一个只放在你口袋里,并且整天运行在一块电池上的设备,那样子的开销将会上升得非常快。为了提升效率,我们采用懒加载策略来重写上面的代码。

public class Foo {
    private Map<string, string=""> data;

    public Foo() { }

    public void setData(String key, String value) {
        if (data == null)
            data = new HashMap<string, string="">();

        data.put(key, value);
    }
}

       现在Foo的构造器实质上是空的构造器,而且我们只有在调用setData方法时,才会产生实例化HashMap对象的开销。在这点上,我们快速实现了只有在绝对需要的时候才去使用内存。然而这种实现的方式并非是线程安全的。线程安全是非常重要的,自从Android对线程操作的要求达到了一个根本的层面(你不可以在UI线程上执行IO阻塞的操作)。

       在上面的例子中,有两个地方需要特别注意的。第一,我们需要线程安全的数据结构。使用 ConcurrentHashMap取代HashMap可以简单地解决这点。第二,稍稍复杂一点,我们需要为属性 data 的实例化,设置更微妙的竞争条件。有这样子的可能性,两个线程同时判断属性data是否为null,并尝试为其实例化。更糟糕的是,其中一个线程会对其实例化的map置入一个对象,但该map实例将会丢失,如果两一个线程也实例化了自己的map对象。为了避免这种竞争情况,我们可以为此加上 synchronized 代码块:

public class Foo {
    private Map<string, string=""> data;

    public Foo() { }

    public void setData(String key, String value) {
        synchronized (this) {
            if (data == null)
                data = new ConcurrentHashMap<string, string="">();
        }

        data.put(key, value);
    }
}

       这就确保了同一时间里,只有一个线程进行null检查,并在需要的时候对属性data进行实例化。好像到这里,问题都迎刃而解了,然而,或许你还会记得我们的关注点是在于性能的优化,很不幸,synchronized 代码块的开销往往比较大。在这种情况下,在同一时间里,只有一个线程可以高效地访问方法setData。幸好,我们还有另一个方法可以使用:double-checked locking。在维基百科上有一篇优秀的文章对 double-checked locking 作了详尽的介绍。在我们的例子里,运用该方法后的代码如下:

public class Foo {
    private volatile Map<string, string=""> data;

    public Foo() { }

    public void setData(String key, String value) {
        if (data == null) {
            synchronized (this) {
                if (data == null)
                    data = new ConcurrentHashMap<string, string="">();
            }
        }

        data.put(key, value);
    }
}

       在上面的代码中,有两个非常重要的改变。其一,为属性data添加了volatile声明。这将指示编译器,最终是Dalvik VM,确保数据的读写操作按预读顺序(译者注:happened-before order)执行。换句话说,写数据操作,总是发生在读数据操作之前(没有这个关键字的声明,编译器或JIT优化或会使它们顺序逆转)。其次,我们在synchronized 代码块外面再添加了一层null检查。这就确保一旦属性data已被实例化,我们将不会再执行 synchronized 的代码块。然而,如果属性data确实为null,我们将 synchronize 对象,然后双重检查属性data是否为null,以确保在两次检查之间,没有其他线程没有执行属性data的实例化。如果没有这个竞争条件,代码将继续执行,继而跳出这个 synchronized 代码块。

       到这里,细心而聪明的读者或许会注意到,上述方法在Java1.5之前并不是总是可靠的。Dalvik VM也是有相似的历史,使用该方法前,请从 这里 检查一下。在这里更推荐大家阅览 这篇 描述如何在Android处理内存一致性问题的优秀指南。

 

 

       本文由zhiweiofli编辑发布,转载请注明出处,谢谢。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值