「设计模式」- 教你手写单例模式

这是我参与8月更文挑战的第12天,活动详情查看: 8月更文挑战java

前言

单例模式(Singleton pattern):确保一个类只有一个实例,并提供该实例的全局访问点安全

本文主要分析单例模式常见的几种实现方式markdown


一. 类图

单例模式使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。多线程

私有构造函数保证了不能经过构造函数来建立对象实例,只能经过公有静态函数返回惟一的私有静态变量。函数


二. 实现方式

2.1 饿汉式

饿汉式在类加载的时候就进行实例化,这样作的好处是线程安全;但缺点也是有的,首先在加载的时候就进行实例化,万一这个类占用的资源很大,就会很是浪费资源,毕竟它不必定在何时被使用,但内存是一开始就被占用了。post

public class HungryManSingleton {
    private static HungryManSingleton hungryManSingleton = new HungryManSingleton();

    private HungryManSingleton() { }
    
    public static HungryManSingleton getInstance() {
        return hungryManSingleton;
    }
} 
复制代码

在main方法中验证饿汉式实现的单例模式spa

HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = HungryManSingleton.getInstance();
System.out.println("从饿汉单例获取的两个实例比较:" + instance1.equals(instance2));
复制代码

输出:线程


使用反射破坏饿汉式单例模式:code

//使用反射获取构造方法,再将构造方法的私有性破坏,而后用这个构造方法建立一个实例
Class<HungryManSingleton> singletonClass = HungryManSingleton.class;
Constructor<HungryManSingleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);

HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = declaredConstructor.newInstance();
System.out.println("与反射获取的实例比较:" + instance2.equals(instance1));
复制代码

输出:orm

能够看到,他们并非同一个对象,这意味着饿汉式单例模式被破坏了

事实上,使用反射后,不管是饿汉式、懒汉式、升级的双重校验锁机制、静态内部类机制,都是不安全的


2.2 懒汉式

在懒汉式的实现中,默认不会进行实例化,何时用到了,何时 New,从而节约资源

public class LazySingleton {
    private static LazySingleton lazySingleton;
    
    private LazySingleton() {
        System.out.println(Thread.currentThread().getName());
    }

    public static LazySingleton getInstance() {
        if (lazySingleton == null) lazySingleton = new LazySingleton();
        return lazySingleton;
    }
}
复制代码

可是这个实如今多线程的环境下是不安全的,试想如下,当 lazySingleton 为空时,试想一下,当lazySingleton 为空时,有多个线程同时经过了if (lazySingleton == null) 的判断,这样就会致使new 被执行了屡次,使用代码复现一下:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> LazySingleton.getInstance()).start();
    }
}
复制代码

控制台输出:

能够看到,实例化代码被执行了三次,为了解决线程安全的问题有两个方法:

  1. getInstance() 方法的层级上加关键字 synchronized
  2. 引入双重检测锁

3.3 双重校验锁

为了解决懒汉式线程不安全的问题,能够引入双重校验锁的机制,双重检验锁也是一种延迟加载,而且较好的解决了在确保线程安全的时候效率低下的问题

如下是代码实现:

public class DCLSingleton {
    private volatile static DCLSingleton dclSingleton;

    private DCLSingleton() { }

    public static DCLSingleton getInstance() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) dclSingleton = new DCLSingleton();
            }
        }
        return dclSingleton;
    }
}
复制代码

在这个实现中,对比一下懒汉式在方法上加锁,那么每次调用那个方法都要得到锁,释放锁,等待等待……而双重校验锁锁住了部分的代码。进入方法若是检查为空才进入同步代码块,这样很明显效率高了不少

3.3.1为何要双重校验

那在这里为何 dclSingleton == null 要判断两次,假设咱们先去掉第二次的判断。

若是两个线程一块儿调用 getInstance()方法,而且都经过了第一次的判断 dclSingleton == null,那么第一个线程获取了锁,而后进行了实例化后释放了锁,而后第二个线程会开始执行,而后立刻也进行了实例化,这就尴尬了。

因此加上第二次判断后,先进来的线程判断了一下,哦?为空,我建立一个,而后建立一个实例以后释放了锁,第二个线程进来以后,哎?已经有了,那我就不用建立了,而后释放了锁,开开心心的完成了单例模式。


3.3.2 为何要使用关键字volatile

对于 new 操做来讲,它不是一个原子性操做,他在底层大概发生了如下三件事:

  • 在堆中分配内存空间
  • 执行它的构造方法,初始化对象
  • 在栈中定义引用,再把这个对象指给堆中的实际对象

咱们指望它是按顺序发生的,可是因为Java的指令重排机制,可能在没有初始化对象时,就把栈中定义的引用指给堆中的空间,当第二个线程再进来的时候,第一次断定是否为空,他认为不为空,因而将尚未进行初始化的对象返回了;这就是为何要加上关键字volatile的缘由。


3.4 静态内部类实现

InnerClassSingleton类加载时,静态内部类 InnerClass没有被加载进内存。只有当调用 getInstance() 方法从而触发 InnerClass.INSTANCEInnerClass才会被加载,初始化实例 INSTANCE。

这种方式不只具备延迟初始化的好处,并且由虚拟机提供了对线程安全的支持。

public class InnerClassSingleton {
    private InnerClassSingleton() { }

    public static InnerClassSingleton getInstance() {
        return InnerClass.INSTANCE;
    }

    static class InnerClass {
        private static final InnerClassSingleton 
                INSTANCE = new InnerClassSingleton();
    }
}
复制代码

3.5 枚举

这是单例模式的最佳实践,它实现简单,而且在面对复杂的序列化或者反射攻击的时候,可以防止实例化屡次

外部调用直接使用 Singleton.INSTANCE,简单粗暴。

因为 Enum 实现了 Serializable 接口,因此不用考虑序列化的问题(其实序列化反序列化也能致使单例失败的,可是咱们这里不过多研究),而且加载的时候 JVM 能确保只加载一个实例,因此它是线程安全的,并且反射没法破解这种单例模式的实现

public enum Singleton {
    INSTANCE;
}
复制代码

总结

本文论述了单例模式常见的五种实现方式,在《Effect Java》中,做者极力推崇使用枚举类来实现单例模式,并认为这个实现是单例模式的最佳实践

感谢阅读,但愿本文对你有所帮助

相关文章
相关标签/搜索