设计模式之单例模式(五种模式)


单例模式: 只有一个实例,并且只负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。单例模式属于创建型模式。常用单例模式有饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式。


四大原则:

  • 构造函数是私有的

  • 以静态方法或者枚举返回实例

  • 确保实例只有一个,尤其是多线程环境

  • 确保反序列换时不会重新构建对象



饿汉式: 饿汉式在类被初始化时就已经在内存中创建了对象,也就是不管你有没有用到,都先建好了。没有线程安全的问题,因为用到了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 效果时,可以考虑静态内部类的实现方式;若涉及到反序列化创建对象时,也可以尝试使用枚举方式。



参考博客:
深入理解单例模式:静态内部类单例原理
单例模式的五种写法

猜你喜欢

转载自blog.csdn.net/weixin_44668898/article/details/109242627