【设计模式】 单例设计模式 (二)

单例设计模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式使用场景

  • 业务系统全局只需要一个对象实例,比如发好奇,redis连接对象等
  • Spring IOC 容器中的 bean 默认都是单例
  • SpringBoot 中的 controller、service、dao层通过 @Autowire 的依赖注入对象默认都是单例的

单例模式分类

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

单例模式实现步骤

  • 私有化构造方法
  • 提供获取单例的方法

1 饿汉式

1.1 饿汉式-方式1(静态变量方法)

/**
 * @author xiaoxin
 * @date 2022-12-13
 * 饿汉式-方式1(静态变量方法)
 */
public class SingletonHungry {

    //私有构造方法,外界无法创建该对象
    private SingletonHungry() {

    }

    // 在成员位置创建该类的对象
    private static SingletonHungry instance = new SingletonHungry();

    // 对外提供静态方法获取该对象
    public static SingletonHungry getInstance() {
        return instance;
    }

    /**
     * 单例对象的方法
     */
    public void process() {
        System.out.println("方法实例化成功!");
    }
}

说明:

​ 该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

  • 优点:实现简单,不需要考虑线程安全问题
  • 缺点:不管有没有使用该对象实例,instance对象一直占用着这段内存

1.2 饿汉式-方式2(静态代码块方式)

/**
 * @author xiaoxin
 * @date 2022-12-13
 * 饿汉式-方式2(静态代码块)
 */
public class SingletonHungry {

    //私有构造方法,外界无法创建该对象
    private SingletonHungry() {

    }

    // 在成员位置创建该类的对象
    private static SingletonHungry instance;

    static {
        instance = new SingletonHungry();
    }

    // 对外提供静态方法获取该对象
    public static SingletonHungry getInstance() {
        return instance;
    }

    /**
     * 单例对象的方法
     */
    public void process() {
        System.out.println("方法实例化成功!");
    }
}

说明:

​ 该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

1.3 枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

/**
 * 枚举方式
 */
public enum Singleton {
    INSTANCE;
}

说明:

​ 枚举方式属于饿汉式方式。

2 懒汉式

懒汉式主要解决饿汉式中存在的内存浪费问题。但也产生了新问题

2.1 懒汉式-方式1(线程不安全)

/**
 * @author xiaoxin
 * @date 2022-12-13
 * 懒汉式:线程不安全的
 */
public class SingletonLazy {

    //私有构造方法,外界无法创建该对象
    private SingletonLazy() {

    }

    // 当需要用到该实例的时候在创建实例对象
    private static SingletonLazy instance;

    // 对外提供静态方法获取该对象
    // 当调用该方法时才会创建对象
    public static SingletonLazy getInstance() {
        // 实例为null的时候才创建
        if (instance == null) {
            /**
             * 线程安全问题:
             *      在多线程的情况下,此时线程1进来之后,CPU被线程2剥夺,线程2发现也进来了
             *      这就导致有多个线程同时进入到该if判断中,并且都去实例化对象
             *      这样就创建了不止一个实例对象
             */
            instance = new SingletonLazy();
        }
        return instance;
    }

    /**
     * 单例对象的方法
     */
    public void process() {
        System.out.println("方法实例化成功!");
    }
}

说明:

​ 从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

解决方案1:

2.2 懒汉式-方式2(线程安全)

// 对外提供静态方法获取该对象
// 当调用该方法时才会创建对象
public static synchronized SingletonLazy getInstance() {
    // 实例为null的时候才创建
    if (instance == null) {
        /**
             * 加了synchronized锁之后,只能有一个线程进入该方法
             * 此时线程就是安全的
             */
        instance = new SingletonLazy();
    }
    return instance;
}

说明:

​ 该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。所以我们只需要对实例化对象的操作进行加锁就能解决这个问题

2.3 懒汉式-方式3(双重检查锁)

再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

// 对外提供静态方法获取该对象
// 当调用该方法时才会创建对象
public static synchronized SingletonLazy getInstance() {
    //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
    if (instance == null) {
        synchronized (SingletonLazy.class) {
            /**
                 * 抢到锁之后再次判断是否为null
                 * 这里再次加一个判断的原因是:
                 *      有可能两个线程都同时进入了前一个if判断,第一个线程获取到锁后会实例化对象
                 *      然后释放锁,第二个线程获取到锁之后也会实例化对象,但是此时对象已经被实例化了
                 *      不需要再次实例化,所以为了解决这个一个问题,我们使用双重检查锁
                 *      再次检查是否需要实例化对象
                 */
            if (instance == null)
                instance = new SingletonLazy();
        }

    }
    return instance;
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

指令重排:首先编译器执行指令的时候会存在指令重排的情况,以便于提高指令的执行速度。就像是考试一样,肯定是先把会的题做完再做难的题目。

JVM中实例化对象内存模型流程如下:

  1. 分配空间给对象
  2. 在空间内创建对象
  3. 讲对象赋值给instance引用

假如出现如下顺序错乱的情况:

线程的执行顺序为:1 -> 3 -> 2, 那么这时候会把空指针赋值给instance(指令重排现象)

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

(由于我对JVM不够了解,以后学好了再回来详细解释)

public class SingletonLazy {

    //私有构造方法,外界无法创建该对象
    private SingletonLazy() {

    }

    // 当需要用到该实例的时候在创建实例对象
    // 添加 volatile 关键字,禁止实例化对象时,内存模型中出现指令重排现象
    private static volatile SingletonLazy instance;

    // 对外提供静态方法获取该对象
    // 当调用该方法时才会创建对象
    public static synchronized SingletonLazy getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                /**
                 * 抢到锁之后再次判断是否为null
                 * 这里再次加一个判断的原因是:
                 *      有可能两个线程都同时进入了前一个if判断,第一个线程获取到锁后会实例化对象
                 *      然后释放锁,第二个线程获取到锁之后也会实例化对象,但是此时对象已经被实例化了
                 *      不需要再次实例化,所以为了解决这个一个问题,我们使用双重检查锁
                 *      再次检查是否需要实例化对象
                 */
                if (instance == null)
                    instance = new SingletonLazy();
            }

        }
        return instance;
    }

    /**
     * 单例对象的方法
     */
    public void process() {
        System.out.println("方法实例化成功!");
    }
}

小结:

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

2.4 懒汉式-方式4(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的。,只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性(这里加载静态属性的时候会自动上锁)。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

/**
 * 静态内部类方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

说明:

​ 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始 INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

JVM对于类的加载会加类锁,所以多线程情况下也只会保证实例化阶段是一个线程在进行。所以指令重排序就无关紧要了。同时static修饰的资源保证了全局唯一

小结:

​ 静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

3.单例设计模式存在的问题

3.1 问题演示

破坏单例模式:

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

  • 序列化反序列化

    Singleton类:

    public class Singleton implements Serializable {
    
        //私有构造方法
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    Test类:

    public class Test {
        public static void main(String[] args) throws Exception {
            //往文件中写对象
            //writeObject2File();
            //从文件中读取对象
            Singleton s1 = readObjectFromFile();
            Singleton s2 = readObjectFromFile();
    
            //判断两个反序列化后的对象是否是同一个对象
            System.out.println(s1 == s2);
        }
    
        private static Singleton readObjectFromFile() throws Exception {
            //创建对象输入流对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
            //第一个读取Singleton对象
            Singleton instance = (Singleton) ois.readObject();
    
            return instance;
        }
    
        public static void writeObject2File() throws Exception {
            //获取Singleton类的对象
            Singleton instance = Singleton.getInstance();
            //创建对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
            //将instance对象写出到文件中
            oos.writeObject(instance);
        }
    }
    

    上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

  • 反射

    Singleton类:

    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
        
        private static volatile Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    
            if(instance != null) {
                return instance;
            }
    
            synchronized (Singleton.class) {
                if(instance != null) {
                    return instance;
                }
                instance = new Singleton();
                return instance;
            }
        }
    }
    

    Test类:

    public class Test {
        public static void main(String[] args) throws Exception {
            //获取Singleton类的字节码对象
            Class clazz = Singleton.class;
            //获取Singleton类的私有无参构造方法对象
            Constructor constructor = clazz.getDeclaredConstructor();
            //取消访问检查
            constructor.setAccessible(true);
    
            //创建Singleton类的对象s1
            Singleton s1 = (Singleton) constructor.newInstance();
            //创建Singleton类的对象s2
            Singleton s2 = (Singleton) constructor.newInstance();
    
            //判断通过反射创建的两个Singleton对象是否是同一个对象
            System.out.println(s1 == s2);
        }
    }
    

    上面代码运行结果是false,表明反射已经破坏了单例设计模式

注意:枚举方式不会出现这两个问题。

3.2 问题的解决

  • 序列化、反序列方式破坏单例模式的解决方法

    在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    Singleton类:

    public class Singleton implements Serializable {
    
        //私有构造方法
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
        
        /**
         * 下面是为了解决序列化反序列化破解单例模式
         */
        private Object readResolve() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    源码解析:

    ObjectInputStream类

    public final Object readObject() throws IOException, ClassNotFoundException{
        ...
        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);//重点查看readObject0方法
        .....
    }
        
    private Object readObject0(boolean unshared) throws IOException {
    	...
        try {
    		switch (tc) {
    			...
    			case TC_OBJECT:
    				return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
    			...
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }    
    }
        
    private Object readOrdinaryObject(boolean unshared) throws IOException {
    	...
    	//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
        obj = desc.isInstantiable() ? desc.newInstance() : null; 
        ...
        // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
        if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
        	// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
        	// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
        	Object rep = desc.invokeReadResolve(obj);
         	...
        }
        return obj;
    }
    
  • 反射方式破解单例的解决方法

    public class Singleton {
    
        //私有构造方法
        private Singleton() {
            /*
               反射破解单例模式需要添加的代码
            */
            if(instance != null) {
                throw new RuntimeException();
            }
        }
        
        private static volatile Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    
            if(instance != null) {
                return instance;
            }
    
            synchronized (Singleton.class) {
                if(instance != null) {
                    return instance;
                }
                instance = new Singleton();
                return instance;
            }
        }
    }
    

    说明:

    ​ 这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值