Can you really write singleton pattern - Java implementation

Source: Siege Lion Eating Orange
Link:  http://www.tekbroaden.com/singleton-java.html



The singleton pattern may be the pattern with the least code, but less does not necessarily mean simple. It takes a lot of brains to use the singleton pattern well and correctly. This article makes a summary of the common singleton pattern writing in Java. If there are any mistakes or omissions, I urge readers to correct them.

As the name suggests, the hungry

method creates an object instance the first time the class is referenced, regardless of whether it is actually needed. code show as below:

public class Singleton { 
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}



The advantage of this is that it is simple to write, but it is impossible to delay the creation of objects. But we often hope that objects can be loaded as late as possible to reduce the load, so we need the following lazy method: single-

threaded writing

method is the simplest, consisting of a private constructor and a public static factory method, In the factory method, the singleton is null judged, if it is null, a new one is taken out, and finally the singleton object is returned. This method can achieve lazy loading, but it has an Achilles heel: it is not thread-safe. If there are two threads calling the getSingleton() method at the same time, it is very likely that the object will be created repeatedly.

public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}



Consider the thread-safe way of writing

This way of writing considers thread safety, and uses synchronized to lock the null judgment of singleton and the new part. At the same time, the use of the volatile keyword on the singleton object is restricted to ensure its visibility to all threads, and instruction reordering optimization is prohibited. In this way, the singleton pattern can be semantically guaranteed to be thread-safe. Note that what is said here is semantically, there are still small pits in actual use, which will be written later.

public class Singleton {
private static volatile Singleton singleton = null;

private Singleton(){}

public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;

}



The writing method that takes into account thread safety and efficiency

Although the above writing method can run correctly, its efficiency is low and it cannot be practically applied. Because every time the getSingleton() method is called, it must be queued here in synchronized, and there are very few cases where new is needed. Therefore, a third way of writing was born:

public class Singleton {
private static volatile Singleton singleton = null;

private Singleton(){}

public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;

}



这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。



那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看volatile这个关键字。其实这个关键字有两层语义。第一层语义相信大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。

注意,前面反复提到“从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。

静态内部类法

那么,有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}

private Singleton(){}

public static Singleton getSingleton(){
return Holder.singleton;
}
}



但是,上面提到的所有实现方式都有两个共同的缺点:

  • 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
  • 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

枚举写法

当然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法:

public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}



使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。

总结

这篇文章发出去以后得到许多反馈,这让我受宠若惊,觉得应该再写一点小结。代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是jdk版本)下,自然有不同的最优解(或者说较优解)。
比如枚举,虽然Effective Java中推荐使用,但是在Android平台上却是不被推荐的。在这篇Android Training中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如双重检查锁法,不能在jdk1.5之前使用,而在Android平台上使用就比较放心了(一般Android都是jdk1.6以上了,不仅修正了volatile的语义问题,还加入了不少锁优化,使得多线程同步的开销降低不少)。

最后,不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
 
参考资料

《Effective Java(第二版)》
《深入理解Java虚拟机——JVM高级特性与最佳实践(第二版)》

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325810080&siteId=291194637