单例模式: 只有一个实例,并且只负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。单例模式属于创建型模式。常用单例模式有饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式。
四大原则:
-
构造函数是私有的
-
以静态方法或者枚举返回实例
-
确保实例只有一个,尤其是多线程环境
-
确保反序列换时不会重新构建对象
饿汉式: 饿汉式在类被初始化时就已经在内存中创建了对象,也就是不管你有没有用到,都先建好了。没有线程安全的问题,因为用到了static,只会被创建一次,但浪费内存空间。
//饿汉式单例
public class HungryMan {
//私有构造
private HungryMan() {}
private final static HungryMan instance = new HungryMan();
public static HungryMan getInstance() {
return instance;
}
}
懒汉式: 在方法被调用后才创建对象,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法,区别就是synchronized关键字。
//懒汉式
public class LazyMan {
private static LazyMan instance;
private LazyMan(){} //私有构造方法
public static LazyMan getInstance() {
if (instance == null) {
instance = new LazyMan();
}
return instance;
}
}
双重锁懒汉模式(Double Check Lock): 又称DCL懒汉式,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。
//双重锁的懒汉模式
public class DoubleCheck {
private static DoubleCheck instance;
private DoubleCheck(){}
public static DoubleCheck getInstance() {
if (instance == null) { //比如两个线程同时进行这步,此时都没有实例
synchronized (DoubleCheck.class) { //一个一个线程来,假如第一个线程先进
if (instance == null) { //第一个进程进来后创建了实例,第二个就不能进来了
instance = new DoubleCheck();
}
}
}
return instance;
}
}
由于 jvm 存在乱序执行功能,DCL 也会出现线程不安全的情况。具体分析如下:
在双重循环和枷锁内的 instance = new DoubleCheck(),这一步在 jvm 中其实分为三步执行:
1、在堆内存开辟内存空间(分配内存空间)
2、在堆内存中实例化SingleTon里面的各个参数(执行构造方法,初始化对象)
3、把对象指向堆内存空间(把这个对象指向这个空间)
由于 jvm 存在乱序执行功能,所以可能在 2 还没执行时就先执行了 3,如果此时再被切换到线程 B 上,由于执行了 3,instance 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的 DCL 失效问题。
不过在 JDK1.5 之后,官方也发现了这个问题,故而具体化了 volatile,即在 JDK1.6 及以后,只要定义为 private volatile static DoubleCheck instance; 就可解决 DCL 失效问题。volatile确保 instance 每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。
静态内部类模式: 静态内部类的优点是外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 instance ,故而不占内存。即当 Holder 第一次被加载时,并不需要去加载 InnerClass ,只有当 getInstance() 方法第一次被调用时,才会去初始化 instance ,第一次调用 getInstance() 方法会导致虚拟机加载InnerClass 类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
//静态内部类模式
public class Holder {
private Holder(){}
//静态内部类
private static class InnerClass {
private static final Holder instance = new Holder();
}
//公共方法调用
public static Holder getInstance() {
return InnerClass.instance;
}
}
枚举: 枚举是比较少见的一种实现方式,但是看上面的代码实现,却更简洁清晰。并且还自动支持序列化机制,绝对防止多次实例化。枚举在 java 中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。我们可直接以 Holder.INSTANCE 的方式调用。
//enum 本身也是一个类
public enum EnumSingle {
INSTANCE;
public void getInstance() {
}
}
总结: 一般情况下,懒汉式(包含线程不安全和线程安全两种方式)都比较少用;饿汉式和双检锁都可以使用,可根据具体情况自主选择;在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;若涉及到反序列化创建对象时,也可以尝试使用枚举方式。
参考博客:
深入理解单例模式:静态内部类单例原理
单例模式的五种写法