详解设计模式之六种单例模式

    技术2024-09-28  50

    我们只谈单例的实现,不结合实际用例出发

    第一种:静态常量饿汉式单例模式

    我们先来看看静态常量饿汉式单例模式的实现

    /** * 单例模式的实现 */ class Singleton { /** * 测试样例. */ private int i = 0; /** * 将初始化构造器(空)设置为私有化 * 一个类即使没有显示的声明一个空的初始化器,也会有一个初始化器, * 如果一个类没有显示的声明一个初始化器,那么JVM将会自动一个空的初始化器. */ private final static Singleton instance = new Singleton(); private Singleton() { } /** * 通过静态方法来获取该类唯一的实例. * * @return */ public static Singleton getInstance() { return instance; } /** * 接下来定义自己的方法处理自己的业务. */ public void say() { System.out.println(i); i++; } }

    以下代码是静态常量饿汉式单例模式的测试样例

    public class SingLeton_1 { public static void main(String[] args) { // 这样获取到的Singleton在内存中永远是唯一的. for (int i = 0; i < 5; i++) { // 这里new出了5个Singleton对象,但是在内存中只有一个Singleton实例存在. Singleton singleton = Singleton.getInstance(); singleton.say(); } } }

    下面是静态常量饿汉式单例模式样例截图

    我们来分析下静态常量模式下的单例有什么优缺点 优点:

    在类装载的时候完成实例化,避免了线程同步问题 缺点:

    在类装载的时候完成实例化,没有达到懒加载的效果,如果从开始就没有使用该实例,那么就就会造成内存浪费.

    第一种单例模式在使用的时候应该结合项目来综合考虑到底是否使用该单例模式.

    第二种:静态代码块懒汉式
    /** * 静态代码块懒汉式.单例模式的实现. * 将空的构造方法进行私有化 通过一个静态代码块在类装在的时候进行实例的初始化. */ class SingLe { /** * 测试用的属性. */ private int i = 0; /** * 将空的构造函数设置为私有. */ private SingLe() { } private static final SingLe instance; static { // 初始化唯一的Singleton_2的实例. instance = new SingLe(); } public static SingLe getInstance() { return instance; } // 测试用的方法. public void say() { System.out.println(i); i++; } }

    注测试类

    public class SingLeton_2 { public static void main(String[] args) { for (int i = 0; i < 5; i++) { // 测试样例: singleton_2获取了五次该对象的实例 // 但是在内存中该类的实例只存在一次. SingLe singleton_2 = SingLe.getInstance(); singleton_2.say(); } } }

    我们来看下静态代码块实现的单例模式的优缺点

    静态代码快实现的单例模式和静态常量实现的单例模式思路类似,都是在类装在的时候对该类的唯一实例进行初始化 我们来看下优缺点:

    优点: 在类装在的时候对该类唯一实例进行初始化,避免了一切线程同步问题缺点: 没有达到懒加载的效果,和静态常量类似,如果该类一直没有使用则会造成内存浪费.

    那么我们怎么对以上方法的不足进行改善呢? 如果要改善那么我们久应该将单例模式设置为懒加载的效果,只有在除此使用的时候才进行唯一实例的初始化。

    第三种:静态内部类实现懒加载

    这里使用了静态内部类的特性,这些特性在下面的代码注释中也有解释. 我们来看看下面的这一种方式,不过这种方式仍然和静态有关。

    /** * 解决没有实现懒加载的效果: 使用静态内部类来实现懒加载的效果,并且同时避免Java线程同步机制. */ class Single_3 { /** * 静态内部类: * 特点 * 1. 静态内部类不会在Single_7当前类加载的时候进行加载,而是在需要改类的时候采去类装载静态内部类 * 2. 类的静态属性只会在类第一次类加载的时候初始化,这样保证了线程安全性 * 3. 避免了线程安全,利用静态内部类来实现延迟加载,效率高 * 4. 所有的业务操作都在内部类中实现. 通过外部类的方法获取到内部类的实体进行调用. */ static class S { private static final S instance = new S(); // 测试用例. private int i = 0; // 这里需要将静态内部类的构造方法进行私有化吗? private S() { System.out.println("初始化了" + i); } public void say() { System.out.println(i); i++; } } public static S getInstance() { return S.instance; } }

    测试方法.

    public class SingLeton_3 { public static void main(String[] args) { for (int i = 0; i < 5; i++) { // 通过测试可以发现s对象在内存中只有一个实例,因为初始化构造器只执行了一次. Single_3.S s = Single_3.getInstance(); s.say(); } } }

    我们来看执行效果图. 静态内部类在综合上继承了以上两种单例模式的优点,而又弥补了以上两种单例模式的缺点。 静态内部类既保证了线程之间的同步,又实现了懒加载效果。

    第四种:懒汉式之线程不安全

    下面我们来看下懒汉式: 懒汉式其实就是实现了的懒加载. 实现代码:

    /** * 懒汉式的具体实现 */ class IdlerNoSecuritySingle { private static IdlerNoSecuritySingle instance = null; private static Object lock = new Object(); private static int i = 0; private IdlerNoSecuritySingle() { i = 0; } public void say() { System.out.println(i); i++; } public static IdlerNoSecuritySingle getInstance() { if (instance == null) { // 对初始化方法进行同步. // 但是这里有一个小问题就是: 加入有一个线程执行完上面的判断方法 instance为空进入到synchronized代码块之前. // CPU使用权给了另一个刚开始的线程,而这个线程也通过了instance为空的判断也进入了下面,并且此时这个线程是幸运的。 // CPU使用权一直没有被剥夺直到return. // 此时轮到第一个线程去获取lock锁,并且此时也获取到了lock锁,那么其将会再次new出一个对象并将其覆盖第一个线程new出来的这个对象 // 此时这个懒汉式单例模式就出现了线程同步问题. synchronized (lock) { instance = new IdlerNoSecuritySingle(); } } return instance; } }

    这里由于我们是测试线程安全问题,所以我这里使用了50个线程来测试出这个问题.

    public class IdlerNoSecurity { /** * 这里使用较多的线程来测试线程同步问题. * * @param args */ public static void main(String[] args) { for (int i = 0; i < 50; i++) { // 这里使用50个线程来测试. new Thread(() -> { IdlerNoSecuritySingle instance = IdlerNoSecuritySingle.getInstance(); instance.say(); }, "" + i).start(); } } }

    结果可想而知,正确的结果是输出0-49 这50个数来,但是实际上却不是这个理想的状态,我们来看下最后的截图. 可以说这种线程不安全的单例模式建议在单线程环境下使用,如果是多线程下则非常之不建议使用该中单例模式,虽然其实现了懒加载的效果.

    第五种:线程安全的懒汉式

    线程安全的懒汉式单例模式有两种写法 一是通过同步方法来实现并发情况下的安全 二是通过同步代码块来实现并发情况下的安全,也就是DCL

    第一种: 通过同步方法来实现线程安全的懒汉式. 实现代码:

    class IdlerSecuritySingleSynchronizedMethod { private int i = 0; private IdlerSecuritySingleSynchronizedMethod instance; private IdlerSecuritySingleSynchronizedMethod() { i = 0; } /** * 对GET方法进行加锁来实现线程安全的懒汉式. * <p> * 但是这个方法不适合高并发环境下,因为对方法加锁将会很严重的影响其性能. * * @return */ public synchronized IdlerSecuritySingleSynchronizedMethod getInstance() { if (instance == null) { instance = new IdlerSecuritySingleSynchronizedMethod(); } return instance; } }

    具体的测试用例这里就不一一给出了,因为这个是对方法进行加锁,那么这个类一定是单例的。如果觉得有疑问的话,建议亲自尝试一下. 下面我们来看看著名的DCL单例模式的实现:

    class IdlerSecuritySynchronized { private int i = 0; private Object lock = new Object(); private IdlerSecuritySynchronized instance; /** * 这个就是著名的DCL单例模式下的懒汉式. * * @return */ private IdlerSecuritySynchronized getInstance() { // 这里不会出现上面所述的线程不安全问题. 加入有多个线程同时竞争lock锁那也没有关系. // 因为即使多个线程先后获取到了这个lock锁,instance也只会初始化一次. if (instance == null) { synchronized (lock) { if (instance == null) { instance = new IdlerSecuritySynchronized(); } } } return instance; } }

    DCL单例模式既实现了线程同步问题,也实现了懒加载的效果。在单例模式可以用在多线程开发中.

    第六种:枚举实现单例模式
    enum EnumSingle{ /** * instance就是enum的一个实例. */ INSTANCE; public void say(){ System.out.println("OK!"); } /** * 可以在下面定义自己的业务实现. */ public void t1(){ System.out.println("自己的业务实现"); } }

    具体的测试用例在这里也就不去细说了,有一部分是因为枚举的特性,也有一部分是上面已经将了好多单例的测试样例。 到这里单例模式就已经详细的说完了,至于在实际开发中应该对应该具体的使用哪一种单例模式应该权衡利弊之后再做决断. 如果上文有哪里写的不对,请于下面留言指出,若看到将会第一时间修改.

    Processed: 0.012, SQL: 9