1、单例模式是一种常用的设计模式,定义该对象的类只能允许一个实例存在。
为什么需要单例模式?在有的时候,系统需要全局唯一的对象,例如系统时间、某些统一配置数据,
只有单例模式才能正常的管理和应用这些数据;
2、常见的创建单例模式的写法,先私有构造器,然后提供 public static getInstance()方法 返回该对象实例,
例如java.util.Calendar 中的日期时间类:
Calendar now = Calendar.getInstance();
还有Runtime类的 getRunTime()方法(这里命名就不是getInstance()方法了):
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
3、那么哪些单例模式才是线程安全的呢?
在现在的大多多线程并发环境中,我们需要考虑到线程安全的获取单例模式做法;
先来看一下饿汉模式Singleton.getInstance(),其实上面的Runtime.getRunTime()也是饿汉模式,这种是线程安全的,哪怕有多个线程同时执行Singleton.getInstance()时,其实在调用这个方法之前,发生并发之前,JVM虚拟机已经为我们创建好了这个单例对象,类在整个生命周期中只会被加载一次,所以是线程安全的
(类加载初始化的时候,先执行静态变量,这一句:private final static Singleton INSTANCE = new Singleton();
再执行静态代码块如果有静态代码块,然后再执行构造方法创建对象,如果想了解类初始化时的执行顺序,可以看看这篇博客:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
4、饿汉模式的写法保证了线程安全,但是这样也有他的缺点,如果这个对象从头到尾一直没有用到,那么就会一直占用内存空间,引起不必要的浪费;
那么有没有延迟加载方式的单例模式呢?如下懒汉模式:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
5、上面写法是线程不安全的,所以有时候说加上synchronized 关键字,保证了线程安全,但是效率很低,目前每获取一次都要同步等待,所以不推荐:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
6、于是又诞生了一种双重检查模式,这种方式是线程安全的吗?
public class Singleton {
private static Singleton singleton;// 第二行
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); //第八行
}
}
}
return singleton;
}
}
(第八行这一代码 instance=new Singleton())创建了一个对象。这一 行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实 发生的,2和3之间重排序之后的执行时序如 下。
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
所以这样,在多个线程访问情况下,可能造成创建了不只一个对象,达不到线程安全的效果;
解决方案:在第二行代码上加一个 volatile 关键字, private static volatile Singleton singleton,达到禁止指令重排序,线程安全效果;这一点再具体的分析,在读《Java并发编程的艺术》一书里面,讲的很清楚,推荐看一下:
《Java并发编程的艺术》 读书笔记 之 Java内存模型(八)双重检查锁定与延迟初始化
7、除了用双重模式实现延迟加载单例模式以外,还有一种,静态内部类的方式实现:
public class Singleton {
private Singleton() {
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
采用了类装载的机制来保证初始化实例时只有一个线程,类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;
8、还有另外一种新方式,来实现单例模式,枚举类:
在读《Effective Java》的时候,提到有一种单例模式:
public class Singleton {
private Singleton(){ }
//定义一个静态枚举类
static enum SingletonEnum{
//创建一个枚举对象,该对象为单例
INSTANCE;
private Singleton instance;
private SingletonEnum(){
instance=new Singleton();
}
public Singleton getInstnceEnum(){
return instance;
}
}
//对外的静态方法,该方法获取单例对象
public static Singleton getInstance(){
return SingletonEnum.INSTANCE.getInstnceEnum();
}
}
原因有下二:
1、反射的时候可以创建多个单例对象:
《Effective java》中只简单的提了几句话:
享有特权的客户端可以借助AccessibleObject.setAccessible方法,
通过反射机制调用私有构造器。
如果需要抵御这种攻击,可以修改构造器,
让它在被要求创建第二个实例的时候抛出异常。
2、序列化反序列化的时候,也会创建多出来的对象:
如果避免这种情况创建多个对象,需要在声明中加上
" implements Serializable" ,
并且把实例域声明为transient变量,
此外还需要提供一个readResolve()方法
看起来有点和静态内部类实现单例的方式相似,但是用这种方式,可以防止反序列化和反射的时重新创建新的对象,所以作者也说这是最佳实现单例的方式:
书中原文截图如下: