在Java中实现单例设计模式的有效方法是什么?
当前回答
实现单例有很多细微差别。夹持器图案不能在许多情况下使用。在使用volatile时,也应该使用局部变量。让我们从头开始,反复讨论这个问题。你会明白我的意思的。
第一次尝试可能看起来像这样:
public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
...
}
这里我们有一个MySingleton类,它有一个名为INSTANCE的私有静态成员和名为getInstance()的公共静态方法。第一次调用getInstance()时,INSTANCE成员为空。然后,流将进入创建条件,并创建MySingleton类的新实例。对getInstance()的后续调用将发现INSTANCE变量已设置,因此不会创建另一个MySingleton实例。这确保只有一个MySingleton实例在getInstance()的所有调用方之间共享。
但这种实现有一个问题。多线程应用程序在创建单个实例时具有竞争条件。如果多个执行线程同时(或前后)命中getInstance()方法,它们将分别将INSTANCE成员视为null。这将导致每个线程创建一个新的MySingleton实例,然后设置instance成员。
private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
这里我们在方法签名中使用了synchronized关键字来同步getInstance()方法。这肯定会修复我们的种族状况。线程现在将一次一个地阻塞并进入方法。但这也造成了性能问题。这个实现不仅同步了单个实例的创建;它同步对getInstance()的所有调用,包括读取。读取不需要同步,因为它们只需返回INSTANCE的值。由于读取将构成我们调用的大部分(记住,实例化只发生在第一次调用上),因此通过同步整个方法,我们将导致不必要的性能损失。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronize(MySingleton.class) {
INSTANCE = new MySingleton();
}
}
return INSTANCE;
}
在这里,我们将同步从方法签名移到了一个同步块,该块包装MySingleton实例的创建。但这能解决我们的问题吗?嗯,我们不再阻止读取,但我们也后退了一步。多个线程将同时或几乎同时命中getInstance()方法,它们都会将INSTANCE成员视为空。
然后,它们将到达同步块,在那里将获得锁并创建实例。当该线程退出块时,其他线程将争夺锁,每个线程将逐一通过块并创建我们类的新实例。所以我们回到了我们开始的地方。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
在这里,我们从区块内部发出另一张支票。如果INSTANCE成员已经设置,我们将跳过初始化。这称为双重检查锁定。
这解决了我们的多重实例化问题。但我们的解决方案再次提出了另一个挑战。其他线程可能无法“看到”INSTANCE成员已更新。这是因为Java如何优化内存操作。
线程将变量的原始值从主内存复制到CPU的缓存中。然后,对值的更改将写入该缓存并从中读取。这是Java的一个旨在优化性能的特性。但这给我们的单例实现带来了一个问题。第二个线程 — 由不同的CPU或内核使用不同的缓存进行处理 — 不会看到第一个所做的更改。这将导致第二个线程将INSTANCE成员视为空,从而强制创建单例的新实例。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
我们通过在INSTANCE成员的声明上使用volatile关键字来解决这个问题。这将告诉编译器始终读取和写入主内存,而不是CPU缓存。
但这种简单的改变是有代价的。因为我们绕过了CPU缓存,所以每次对易失性INSTANCE成员进行操作时,我们都会受到性能影响 — 我们做了四次。我们再次检查存在性(1和2),设置值(3),然后返回值(4)。有人可能会说,这条路径是边缘情况,因为我们只在方法的第一次调用期间创建实例。也许创作上的表现是可以容忍的。但即使是我们的主要用例reads,也会对volatile成员进行两次操作。一次检查是否存在,再次返回其值。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (result == null) {
synchronized(MySingleton.class) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = createInstance();
}
}
}
return result;
}
由于性能受到影响是由于直接对volatile成员进行操作,所以让我们将一个局部变量设置为volatile的值,并改为对局部变量进行操作。这将减少我们在易失性上操作的次数,从而收回一些损失的性能。注意,当我们进入同步块时,必须再次设置本地变量。这确保了它是最新的,以及在我们等待锁时发生的任何更改。
我最近写了一篇关于这个的文章。解构单身汉。您可以在那里找到有关这些示例和“持有者”模式示例的更多信息。还有一个真实的例子展示了双重检查的易失性方法。
其他回答
以下是三种不同的方法
枚举/***使用Java Enum的Singleton模式示例*/公共枚举EasySingleton{实例;}双重检查锁定/延迟加载/***带有双重检查锁定的单线模式示例*/公共类DoubleCheckedLockingSingleton{私有静态易失性DoubleCheckedLockingSingleton INSTANCE;private DoubleCheckedLockingSingleton(){}公共静态DoubleCheckedLockingSingleton getInstance(){如果(INSTANCE==null){synchronized(DoubleCheckedLockingSingleton.class){//双重检查Singleton实例如果(INSTANCE==null){INSTANCE=新的DoubleCheckedLockingSingleton();}}}返回INSTANCE;}}静态工厂方法/***带有静态工厂方法的Singleton模式示例*/公开课Singleton{//在类加载期间初始化private static final Singleton INSTANCE=new Singleton();//防止创建另一个“Singleton”实例私有Singleton(){}公共静态Singleton getSingleton(){返回INSTANCE;}}
如果您不需要延迟加载,那么只需尝试:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return Singleton.INSTANCE; }
protected Object clone() {
throw new CloneNotSupportedException();
}
}
如果您希望延迟加载并且希望单例是线程安全的,请尝试双重检查模式:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(null == instance) {
synchronized(Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
protected Object clone() {
throw new CloneNotSupportedException();
}
}
由于双重检查模式不能保证有效(由于编译器的一些问题,我对此一无所知),因此您也可以尝试同步整个getInstance方法或为所有单例创建注册表。
Java 5+中的线程安全:
class Foo {
private static volatile Bar bar = null;
public static Bar getBar() {
if (bar == null) {
synchronized(Foo.class) {
if (bar == null)
bar = new Bar();
}
}
return bar;
}
}
注意此处的挥发性修饰语。:)这很重要,因为如果没有它,JMM(Java内存模型)就不能保证其他线程看到其值的更改。同步不会处理这一点——它只序列化对该代码块的访问。
@Bno的回答详细介绍了Bill Pugh(FindBugs)推荐的方法,并且可以更好地进行论证。去阅读并投票支持他的答案。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if (INSTANCE != null)
throw new IllegalStateException(“Already instantiated...”);
}
public synchronized static Singleton getInstance() {
return INSTANCE;
}
}
由于我们在getInstance之前添加了Synchronized关键字,因此在两个线程同时调用getInstance的情况下,我们避免了竞争条件。
枚举单例
实现线程安全的单例的最简单方法是使用Enum:
public enum SingletonEnum {
INSTANCE;
public void doSomething(){
System.out.println("This is a singleton");
}
}
自从在Java 1.5中引入Enum以来,此代码一直有效
双重检查锁定
如果你想编写一个在多线程环境中工作的“经典”单例(从Java1.5开始),你应该使用这个。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这在1.5之前不是线程安全的,因为volatile关键字的实现不同。
早期加载单例(甚至在Java1.5之前就可以使用)
这个实现在加载类时实例化单例,并提供线程安全性。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
public void doSomething(){
System.out.println("This is a singleton");
}
}
推荐文章
- Eclipse调试器总是阻塞在ThreadPoolExecutor上,没有任何明显的异常,为什么?
- Java生成两个给定值之间的随机数
- 如何有效地从数组列表或字符串数组中删除所有空元素?
- 比较JUnit断言中的数组,简洁的内置方式?
- codestyle;把javadoc放在注释之前还是之后?
- 如何在Spring中定义List bean ?
- 将Set<T>转换为List<T>的最简洁的方法
- 在JavaScript中,什么相当于Java的Thread.sleep() ?
- 使用Java重命名文件
- URL从Java中的类路径加载资源
- .toArray(new MyClass[0]) or .toArray(new MyClass[myList.size()])?
- Hibernate中不同的保存方法之间有什么区别?
- Java 8流和数组操作
- Java Regex捕获组
- Openssl不被视为内部或外部命令