在本文中,我们将介绍在RxJava中创建Singleton对象的一些技术。 最重要的是,我们将学习Java中的双重检查锁定 。
Java中的Singleton模式是一种创新模式。 随着时间的流逝,人们开始关注Singleton模式的使用和实现。 这是由于单例的实现和使用方式存在一些非常根本的问题所致。
Java中的单例模式具有多种功能,例如:
- 确保只有一个类实例存在于JVM中。
- 提供对类实例的全局访问。
- 防止直接创建类实例的私有构造函数。
- 最适合用于日志记录,线程池,缓存等…
使用Java创建Singleton模式的三种基本方法。 我将列出所有这些内容,并告诉您单例模式是如何随着时间演变的,以及为什么双重检查锁定是当前最好的方法。
这是Java中Singleton模式的基本实现。
class Example{ private Example mExample = null ; public Example getInstance (){ if (mExample == null ) mExample = new Example (); return mExample; } // rest of the code... }
注意:构造函数在所有实现中都是私有的。
此代码将在多线程上下文中失败。 多个线程可以调用getInstance()方法并最终创建Singleton的多个实例。 这是不希望的行为。 Singleton的基本属性是,JVM中应该只有该类的单个实例。
优点:
- 易于阅读。
- 在单线程应用程序中可以正常工作。
缺点:
- 在多线程上下文中将失败。
- 多个线程可以创建此类的多个实例。
- 将无法达到Singletons的目的。
一些聪明的人想到了创建单例的优雅解决方案。 我们使用synced关键字来防止线程同时访问getInstance()方法。
class Example{ private Example mExample = null ; public synchronized Example getInstance (){ if (mExample == null ) mExample = new Example (); return mExample; } // rest of the code... }
通过使用synced关键字,我们是JVM,一次只能让一个字段访问此方法。 这解决了多线程上下文的问题。
如果您看一下上面的代码,您会注意到我们已经使整个方法同步。 每个访问该方法的线程都将首先获取一个锁。
同步或获取锁是一种昂贵的方法。 确实会降低应用程序的性能。 如果您想进一步了解同步的性能开销,那么这个SO答案将是一个好的开始。
即使所有线程都获得了锁定,它也只是需要锁定的第一个线程。 初始化对象后,空检查足以在线程之间维护单个实例。
优点:
- 确实很好地处理了多线程环境。
- 容易明白。
缺点:
- 每当线程尝试访问该方法时,获取不必要的锁定。
- 锁定确实非常昂贵,并且许多线程都想获得一个锁定,这会导致严重的性能开销。
在先前的方法中,我们将整个方法同步为线程安全的。 但是同步不仅适用于方法。 我们也可以创建同步块。
在此方法中,我们将创建一个同步块而不是整个方法。
class Example{ private Example mExample = null ; public Example getInstance (){ if (mExample == null ){ synchronized (Example. class ){ if (mExample == null ) mExample = new Example (); } } return mExample; } // rest of the code... }
这是步骤顺序:
- 第一个线程调用getInstance()方法。
- 它检查实例是否为空(对于第一个线程,它为)。
- 然后,它获取一个锁。
- 检查该字段是否仍然为空?
- 如果是,它将创建该类的新实例并初始化该字段。 最后,返回实例。
- 其余线程不需要获取锁定,因为字段已经初始化,因此降低了同步命中率!
注意同步块之前和之后的多个空检查。 因此,名称为double check lock 。
优点:
- 在多线程环境中工作。
- 比同步方法具有更好的性能。
- 只有第一个线程需要获取锁。
- 以上方法中最好的。
缺点:
- 一开始,双重null检查可能会造成混淆。
- 不行!!
是的,上述方法存在一个细微问题。 它并不总是有效。
问题在于,编译器对程序的感觉与人眼的感觉截然不同。 根据我们的逻辑,首先,应创建Example类的实例,然后将其分配给mExample字段。
但是不能保证此操作顺序。 编译器可以自由地对语句进行重新排序,只要它不影响最终结果即可。
因此,例如,您可能最终会将部分初始化的对象分配给mExample字段。 然后其他线程将对象视为非空。 这导致线程使用部分初始化的对象,这可能导致崩溃 !
如今,编译器对您的代码进行了某些优化,使他们可以自由地对语句进行重新排序。 当编译器内联构造函数调用时,可能会发生重新排序。
Doug Lea写了一篇有关基于编译器的重新排序的详细文章。
Paul Jakubik发现了使用双重检查锁定无法正常工作的示例。
如果上述所有方法都容易失败,那么我们还剩下什么?
在J2SE 5.0中,Java的内存模型发生了很大变化。 volatile关键字现在可以解决上述问题。
Java平台不允许将易失字段的读取或写入与之前的任何读取或写入重新排序。
class Example{ private volatile Example mExample = null ; public Example getInstance (){ if (mExample == null ){ synchronized (Example. class ){ if (mExample == null ) mExample = new Example (); } } return mExample; } // rest of the code... }
当心:仅在JDK 5及更高版本中有效。 对于android开发人员,您最好选择使用Java 7及更高版本的Java。
希望您觉得本文有用。 如果您愿意,请在下面的评论部分中告诉我,我很乐意写更多这样的概念文章。
翻译自: https://www.javacodegeeks.com/2019/09/double-check-locking-java.html