代码的性能是最重要的。然而,在当今复杂的多线程移动应用世界里,我们常常会为保证内存数据的一致性而牺牲一些性能。线程竞争条件的设计和调试是一件非常耗时,且容易令人沮丧的工作,所以线程被锁定太长时间的情况并不少见。幸运的是,现在有一些简单的模式可以使锁定变得更有效率,从而避免对性能产生不必要的影响。
首先,让我们先预览一下只有简单 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编辑发布,转载请注明出处,谢谢。
相关推荐
在介绍双检锁模式(Double-Checked Locking Pattern,DCLP)的C++实现中,Scott Meyers和Andrei Alexandrescu在其2004年的文章中指出,传统的单例模式实现并不具备线程安全性。单例模式是设计模式中经常被提及的一种...
总结来说,《C++ and the Perils of Double Checked Locking》揭示了在多线程编程中,为了优化而采用的双重检查锁定策略可能带来的隐患,并提供了相应的解决方案,提醒开发者在编写多线程代码时要充分理解和考虑C++...
为了解决单例模式的线程安全问题,一种流行的方法是采用双检查锁定模式(Double-Checked Locking Pattern,简称DCLP)。DCLP的目标是在初始化共享资源(如单例对象)时添加高效的线程安全性,通过在检查实例是否存在...
在这个双重检查锁定(Double-Checked Locking)的实现中,volatile关键字确保了即使在多线程环境下,也只有一个Singleton实例会被创建。synchronized保证了在多个线程同时访问时,只有一个线程能进入同步代码块,...
- **Singleton的双锁实现**:双锁(double-checked locking)模式用于确保单例的安全创建,但可能存在并发问题,因此在多线程环境下应谨慎使用。 ### Web和IIS - **应用程序池**、**WebApplication**和线程池的关系...
传统的双重检查锁定(Double-Checked Locking)和静态内部类(Initialization-on-Demand Holder Class)都是在保证线程安全的同时,避免不必要的同步开销。 2. **生产者-消费者模式**: 该模式用于线程间的协作,...
为了提高性能,人们提出了**双重检查锁定**(Double-checked locking)的方法。这种方法首先在不加锁的情况下检查`instance`是否为`null`,如果为`null`则进行同步操作: ```java public static Singleton ...
尽管如此,`volatile`在某些场景下,如单例模式的双重检查锁定(Double-Checked Locking)或者作为标志位时,可以有效提升性能,同时保证基本的可见性。 总结来说,Java中的锁和`volatile`关键字都是为了应对并发...
一种简单的线程安全Singleton实现方法是使用双重检查锁定(double-checked locking)技术: ```cpp class Singleton { private: static Singleton* instance; MutexLock mutex; Singleton() {} // 私有构造函数 ...
Java中可以使用双重检查锁定(Double-Checked Locking)或静态内部类等方式实现线程安全的单例。 3. **守护线程模式**:守护线程是一种特殊的线程,它会一直运行直到所有非守护线程结束。Java中,可以通过Thread....
- **线程安全的Singleton模式实现**:确保在多线程环境中Singleton对象的唯一性和安全性,通常采用双重检查锁定(Double-checked locking)机制。 #### 总结与展望 多线程服务器的编程模型对于提高网络应用的性能...
Java中可以使用双重检查锁定(Double-Checked Locking)或静态内部类等方式实现线程安全的单例。 5. **线程同步机制**:包括synchronized关键字、volatile变量、Lock接口(如ReentrantLock)等,它们用于保证共享...
例如,在双检锁(Double-Checked Locking)模式中,为了防止无谓的同步开销,程序员需要使用内存栅栏来确保在单例对象初始化时的正确性。如果没有适当的内存栅栏,编译器或硬件的优化可能会导致线程在对象未完全初始...
双重检查锁定(Double-Checked Locking)和静态内部类是两种常见的线程安全的单例实现方式。 5. **同步机制**:Java提供了多种同步机制,包括`synchronized`关键字、`Lock`接口(如`ReentrantLock`)、`Semaphore`...