对于如何实现单例模式,已经有很多博客来说明这个事了,比如陈皓老师的深入浅出单实例Singleton设计模式,想要深入了解单例模式可以直接去看他们博客,这里自我讨论学习一下。
在陈浩老师的博客中提到了两个存在线程安全的单例模式实现方式,在《Java多线程编程核心技术》也提到了这些问题,如何正确地写出单利模式这篇博客也提到了这些方法。之所以给出这么多连接,是希望以后自己忘了的时候可以随时查查。
在这里,直接讨论几种正确的方法。
1、通常理解的饿汉模式(立即加载)
public class Singleton{
private volatile static Singleton singleton = new Singleton();
private Singleton(){} //私有构造函数
public static Singleton getInstance(){
return singleton;
}
}
这种方式的缺点在于 将类的创建委托给了类加载器,我们无法控制,比如我们需要在创建类以前需要干一些其他事情(比如某个配置文件,或是某个被其他类创建的资源),这些事情依赖其他类。我们希望在第一次调用getInstance()的时候才创建类。
因此对上面方法进行修改。老版《Effective Java》中推荐的做法是:
public class Singleton{
private static class singletonHolder{ //交由另一个私有函数来创建
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return singletonHolder.INSTANCE;
}
}
2、双重检查模式
public class Singleton{
private volatile static Singleton singleton = null;
private Singletion(){}
public static Singleton getInstance(){
if(singleton==null){ //第一个singleton==null判断
synchronized(Singleton.class){
if(singleton==null){ //第二个singleton==null判断
singleton = new Singleton();
}
}
}
return singleton;
}
}
如果去掉第一个判断,类似于在getInstance方法上加上synchronized关键字,每次获取一个实例都要获取锁,消耗太大。
如果去掉第二个判断,有可能两个线程同时通过了检查,从而同步地
创造出了两个实例。
使用 volatile 有两个功用:
1)这个变量不会在多个线程中存在复本,直接从内存读取。
2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
使用volatile 主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1)给 singleton 分配内存
2)调用 Singleton 的构造函数来初始化成员变量,形成实例
3)将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
另外synchronized(Singleton.class)
是获取Singleton的类锁(区别于“对象锁”),另一种获取类锁的方式是为static静态方法加上synchronized关键字。使用类锁后,所有被synchronized修饰的static方法(代码块)只能被同步执行。
另外类锁和对象锁之间并不冲突。
3、枚举单例模式
public enum Singleton{
INSTANCE;
int attr; //属性示例
Singleton(){ //默认私有的构造方法。
}
}
在使用枚举单例模式时,直接使用Singleton.INSTANCE
类访问。
关于使用枚举简单、而且是线程安全的等等,很少有人解释清楚,可以参考这个单例模式中为什么用枚举更好
在《Effective java》中认为,枚举天生就是不可变的,因此所有的域都应该是final的。它们可以是公有的,但最好是私有的。常量是线程安全的,不能改嘛。另外,在一开始,书中说“枚举类型是指由一组固定的常量组成合法值的类型”