在项目开发过程中,有些类我们只需要一个对象,比如配置文件类,工具类,线程池,缓存池和日志对象等。使用单例模式就是我为了保证应用中某一个对象的实例有且只有一个。
先放一个总结,后面一个个解释
总结:
单例模式实现 | 是否线程安全 | 是否懒加载 | 是否防止反射构建 |
---|---|---|---|
饿汉模式 | 是 | 否 | 否 |
懒汉模式(双重检测) | 是 | 是 | 否 |
静态内部类 | 是 | 是 | 否 |
枚举 | 是 | 否 | 是 |
饿汉模式:
// 为什么叫饿汉模式,因为比较饥渴,还没到getInstance就创建了那个单例
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:没有线程安全问题,懒汉式需要双重锁定解决可能的线程安全问题。
缺点:类一加载就实例化(见类加载顺序),提前占用系统资源。
懒汉模式
第一版:普通版
第一版已经有单例的思想,在new对象之前进行检测。但是在多线程情况下回导致多个对象,因为可能有多个线程同时通过INSTANCE!=null
这个检测,然后创建了多个对象。
// 为什么叫懒汉,到getInstance才创建
public class Singleton {
private static Singleton INSTANCE=null;
//必须声明私有构造函数,不可省略。不然会有默认的public构造函数
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE!=null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
第二版:双重检测(未加volatile)
第二版已经进行了双重检测来保证多线程安全和单例。
先关注锁和第二重检测
双重检测采用了锁机制,在第二重检测前,保证只有一个线程能获得锁。释放锁后已经创建好了对象,其他线程再获得锁,不能通过第二重检测,保证了多线程环境下的单例。
为什么还需要第一重检测呢?
这是为了降低锁的粒度提高性能,不然每次检测都要获得锁,并发性能无法保证。
但是它还有个问题,就是会返回一个未初始化完全的对象。
jvm为了对代码进行优化会对其进行指令重排,instance = new Singleton()
这行代码不是原子性的,正常的new对象会分为三步
1. 分配对象空间
2. 初始化对象
3. 把引用指向该对象
但是指令重排后,可能按照132这样的顺序执行,如果恰巧第3步执行完,还没有进行第2步的初始化,其他线程调用了getInstance,不能通过检测,直接到最后return instance,这个时候就有可能获得一个未初始化成功的对象。
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
第三版:双重检测(添加了volatile,禁止指令重排)
在第二版已经详细解释过了,这里只留下代码
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
第四版:
第四版采用了静态内部类,且必须是private防止外部访问静态内部类。
public class Singleton {
private static class LazyHolder {
//把声明语句放到了内部类中
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
注意:
1. 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
2. INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
饿汉模式和懒汉模式都可以通过反射构造对象来打破单例
枚举
枚举可以阻止反射获取枚举类的私有构造函数,唯一缺点不是懒加载,提前占用系统资源。
public enum SingletonEnum {
INSTANCE;
}
枚举的语法就是这么优雅,如果不懂的话,看我的其他博客,有关于枚举的介绍。