代码的性能是最重要的。然而,在当今复杂的多线程移动应用世界里,我们常常会为保证内存数据的一致性而牺牲一些性能。线程竞争条件的设计和调试是一件非常耗时,且容易令人沮丧的工作,所以线程被锁定太长时间的情况并不少见。幸运的是,现在有一些简单的模式可以使锁定变得更有效率,从而避免对性能产生不必要的影响。
首先,让我们先预览一下只有简单 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编辑发布,转载请注明出处,谢谢。