设计模式学习笔记2:单例模式

jupiter
2021-12-13 / 0 评论 / 615 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2021年12月13日,已超过865天没有更新,若内容或图片失效,请留言反馈。

1.介绍

单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。这种类型的设计模式属于创建型模式

单例模式具有典型的三个特点

  • 只有一个实例。
  • 自我实例化。
  • 提供全局访问点。

应用实例:

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

2.几种实现方式

2.1 懒汉式,线程不安全

是否 Lazy 初始化:
是否多线程安全:
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){}
    // 2.先定义单例对象但不实例化
    private static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

2.2 懒汉式,线程安全

是否 Lazy 初始化:
是否多线程安全:
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){}
    // 2.先定义单例对象但不实例化
    private static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public static synchronized LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

2.3 饿汉式

是否 Lazy 初始化:
是否多线程安全:
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

// 饿汉式单例模式
public class Hungry {
    // 1.私有化构造函数
    private Hungry(){}
    // 2.事先创建好对象
    private  final static Hungry HUNGRY = new Hungry();
    // 3.当需要调用的单例对象的时候通过暴露的公有的方法进行调用
    public static Hungry getInstance(){
        return  HUNGRY;
    }
}

2.4 双检锁/双重校验锁(DCL,即 double-checked locking)/DCL懒汉式单例

是否 Lazy 初始化:
是否多线程安全:
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){}
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

为什么要加volatile

lazyMan = new LazyMan(); // 不是一个原子性操作

创建对象的过程不是一个原子操作,分为以下3步:

  • 1.分配内存空间
  • 2.执行构造方法,初始化对象
  • 3.把这个对象指向这个空间

在执行的过程中可能会发生指令重排的现象,即我们真实想要的执行顺序是1->2->3。但是可能真实的执行顺序是1->3->2
则当线程A以1->3->2的顺序执行到3时,线程B也调用了返回对象实例的方法,则给线程B可能拿到未创建完成的对象进行调用从而导致出错。

2.5 登记式/静态内部类

是否 Lazy 初始化:
是否多线程安全:
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

2.6 枚举

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

3.反射下的单例模式

3.1 V1:在单例类已经实例化了对象的条件下使用反射破坏单例

3.1.1 示例

package single;

import java.lang.reflect.Constructor;

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){}
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazyMan instance1 = LazyMan.getInstance();
        // 获取空参构造器
        Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 将空参构造器的Accessible设置为true
        declareConstructor.setAccessible(true);
        // 调用空参构造器实例化对象
        LazyMan instance2 = declareConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}
single.LazyMan@1b6d3586
single.LazyMan@4554617c

3.1.2 防止措施

  • 通过在构造函数中加入一个同步代码块进行一次是否已经生成了该单例类对象的判断
package single;

import java.lang.reflect.Constructor;

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan!=null){
                throw  new RuntimeException("不要试图使用反射破坏单例模式");
            }
        }

    }
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazyMan instance1 = LazyMan.getInstance();
        // 获取空参构造器
        Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 将空参构造器的Accessible设置为true
        declareConstructor.setAccessible(true);
        // 调用空参构造器实例化对象
        LazyMan instance2 = declareConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at single.LazyMan.main(LazyMan.java:38)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏单例模式
    at single.LazyMan.<init>(LazyMan.java:11)
    ... 5 more

3.2 V2:V1解决方案破解

3.2.1 示例

package single;

import java.lang.reflect.Constructor;

// 懒汉式单例
public class LazyMan {
    // 1.私有化构造函数
    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan!=null){
                throw  new RuntimeException("不要试图使用反射破坏单例模式");
            }
        }

    }
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        // 获取空参构造器
        Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 将空参构造器的Accessible设置为true
        declareConstructor.setAccessible(true);
        // 调用空参构造器实例化对象
        LazyMan instance1 = declareConstructor.newInstance();
        LazyMan instance2 = declareConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

3.2.2 防止措施--"红绿灯策略'',设置标志位

package single;

import java.lang.reflect.Constructor;

// 懒汉式单例
public class LazyMan {
    private static boolean flag = false;

    // 1.私有化构造函数
    private LazyMan(){
        synchronized (LazyMan.class){
            if(flag == false){
                flag = true;
            }else{
                throw  new RuntimeException("不要试图使用反射破坏单例模式");
            }
        }

    }
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        // 获取空参构造器
        Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 将空参构造器的Accessible设置为true
        declareConstructor.setAccessible(true);
        // 调用空参构造器实例化对象
        LazyMan instance1 = declareConstructor.newInstance();
        LazyMan instance2 = declareConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

3.3 V3:V2解决方案破解

3.3.1 示例

  • 对字节码进行反编译获取到标志位,然后使用反射破解"红路灯"标志位
package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

// 懒汉式单例
public class LazyMan {
    private static boolean flag = false;

    // 1.私有化构造函数
    private LazyMan(){
        synchronized (LazyMan.class){
            if(flag == false){
                flag = true;
            }else{
                throw  new RuntimeException("不要试图使用反射破坏单例模式");
            }
        }

    }
    // 2.先定义单例对象但不实例化
    private volatile static LazyMan lazyMan;
    // 3.当发起调用的时候判断对象是否实例化,如果未实例化,则实例化再返回,否则直接返回
    public  static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例  DCL懒汉式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        // 获取空参构造器
        Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 将空参构造器的Accessible设置为true
        declareConstructor.setAccessible(true);
        // 调用空参构造器实例化对象
        LazyMan instance1 = declareConstructor.newInstance();

        // 获取"红绿灯标志位"
        Field flag = LazyMan.class.getDeclaredField("flag");
        // 重置为"绿灯"
        flag.set(instance1,false);

        LazyMan instance2 = declareConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}
single.LazyMan@4554617c
single.LazyMan@74a14482

3.3.2 防止措施-使用枚举Enum实现单例模式

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

public class Test{
    public  static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);

    }
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at single.Test.main(EnumSingle.java:20)

参考资料

  1. 【狂神说Java】单例模式-23种设计模式系列
  2. 单例模式
  3. Java中的双重检查锁(double checked locking)
  4. Java设计模式(一)之单例模式
0

评论 (0)

打卡
取消