详解设计模式之单例模式

1.单例模式概述

单例设计模式所解决的问题就是:保证类的对象在内存中唯一,即一个类只有一个对象实例

单例模式的结构

  • 单例类。只能创建一个实例的类,对象的创建
  • 访问类。使用单例类,对象的使用

单例模式特点

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例

单例模式的优缺点

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失

单例模式实现思路

  • 为了保证一个类只有一个对象实例,所以不允许其他程序使用new创建单例类的对象,所以私有化单例类的构造函数
  • 单例类必须自己创建自己的唯一实例,可以通过new在单例类中创建一个单例类对象
  • 由于单例类需要给其他对象提供单例类的实例,所以对外需要提供一个方法,能够让其他程序获取该对象,所以定义一个共有的方法将创建的对象返回

单例模式的类图

  • Singleton为单例类
  • instance :Singleton是类中创建的唯一单例类对象
  • Singleton():私有的构造方法,防止其他程序通过new创建对象,打破单例类的唯一实例的特性
  • getInstance():Singleton : 一个公有的,返回值是一个单例类对象的方法,供其他程序使用来创建单例类实例

单例模式类图


2.单例模式的实现

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建

    • 饿汉式是典型的空间换时间当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要在判断,节省了运行时间,当然如果创建好的对象一直没有使用的话,就会造成内存的浪费
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

    • 懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间
    • 即在调用getInstance()方法时实例才被创建,常见的实现方法就是在getInstance()方法中进行new实例化

2.1饿汉式(静态变量)

单例类

public class Singleton {

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

    //2,在本类中创建本类对象(私有静态变量)
    private static final Singleton instance = new Singleton();

    //3,提供一个公共的访问方式,让外界获取该对象(静态方式)
    public static Singleton getInstance() {
        return instance;
    }
}
  • 为什么getInstance()方法和类实例对象是静态的?

    因为不能使用new来创建对象,但是又希望调用类中的方法,所以方法必须是静态的,并且由于静态方法只能调用静态成员,所以类实例对象也是静态的。

  • 为什么对象的访问修饰符是private?

    如果访问修饰符是public,那么就可以通过Singleton.instance的方式获取到单例类对象,造成了不可控

  • final修饰对象的原因?

    由于单例模式的核心是个类只有一个对象实例,所以可以将其看为一个常量

测试类

public class Test {
    public static void main(String[] args) {
        // 创建Singletion类的对象
        // 通过类名调用类方法创建对象
        Singleton instance = Singleton.getInstance();

        Singleton instance1 = Singleton.getInstance();

        //判断获取到的两个是否是同一个对象,是同一个对象
        // == 比较的是对象在内存中的地址值
        System.out.println(instance == instance1);
    }
}


2.2饿汉式(静态代码块)

单例类

/**
 * 恶汉式
 * 在静态代码块中创建该类对象
 */
public class Singleton {

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

    // 在成员位置声明该类的对象
    private static Singleton instance;
	
    // 静态代码块,创建该类对象
    static {
        instance = new Singleton();
    }

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

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

静态代码块:执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即static修饰的数据成员


2.3懒汉式(线程不安全)

/**
 * 懒汉式
 * 线程不安全
 */
public class Singleton {

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

    //声明Singleton类型的变量instance
    private static Singleton instance; //只是声明一个该类型的变量,并没有进行赋值

    //对外提供访问方式
    public static Singleton getInstance() {
        //判断instance是否为null,如果为null,说明还没有创建Singleton类的对象
        //如果没有,创建一个并返回,如果有,直接返回
        if(instance == null) {
            //线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
            instance = new Singleton();
        }
        return instance;
    }
}

​ 该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载(lazy loading)的效果。

​ 但是如果是多线程环境,会出现线程安全问题。如果在多线程下,一个线程进入了if (instance== null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。解决方式:使用同步锁synchronized


2.4懒汉式(线程安全)

/**
 * 懒汉式
 * 线程安全
 */
public class Singleton {

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

    //声明Singleton类型的变量instance
    private static Singleton instance; //只是声明一个该类型的变量,并没有进行赋值

    //对外提供访问方式(使用synchronized关键字,保证每次只有一个线程执行该方法)
    public static synchronized Singleton getInstance() {
        //判断instance是否为null,如果为null,说明还没有创建Singleton类的对象
        //如果没有,创建一个并返回,如果有,直接返回
        if(instance == null) {
            //线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
            instance = new Singleton();
        }
        return instance;
    }
}

​ 该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低,因为每次调用此方法都需要先判断锁。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。


2.5懒汉式(双重检查锁)

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

​ 第一次检查是为了验证是否创建对象(提高效率),第二次检查是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象

/**
 * 双重检查方式
 */
public class Singleton { 

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

    private static Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

注意

对于两次instance的是否为空的判断解释:

1.为何在synchronized外面的判断?
为了提高性能!如果去掉这次的判断那么在运行的时候就会直接的运行synchronized,所以这会使每个getInstance()都会得到一个静态内部锁,这样的话锁的获得以及释放的开销(包括上下文切换,内存同步等)都不可避免,降低了效率。所以在synchronized前面再加一次判断是否为空,则会大大降低synchronized块的执行次数。

2.为何在synchronized内部还要执行一次呢?
因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。第一次判断是为了验证是否创建对象,第二次判断是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象

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

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


双重检查锁的改进

/**
 * 双重检查方式
 */
public class Singleton { 

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

    // 使用volatile关键字,保证有序
    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

2.6懒汉式(静态内部类)

​ 静态内部类单例模式中实例由内部类创建,由于 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 类的唯一性。


2.7饿汉式(枚举方式)

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

单例类

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

测试类

public class Test {
    public static void main(String[] args) {
        // 获取类对象
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
		
        // 判断对象是否是同一个
        System.out.println(instance == instance1);
    }
}

3.单例模式的破坏与解决

3.1单例模式的破坏

枚举方式除外,可以通过序列化和反射破坏单例模式,使得上述定义的单例类创建多个对象

  • 反射

单例类

以双重检查方式构建单例模式

/**
 * 双重检查方式
 */
public class Singleton { 

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

    private static Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

测试类

public class Test {
    public static void main(String[] args) throws Exception {
        //1,获取Singleton的字节码对象 
        Class clazz = Singleton.class;
        
        //2,获取无参构造方法对象
        Constructor cons = clazz.getDeclaredConstructor();
        
        //3,取消访问检查(因为无参方法是private的)
        cons.setAccessible(true);
        
        //4,利用反射创建Singleton对象
        Singleton s1 = (Singleton) cons.newInstance();
        Singleton s2 = (Singleton) cons.newInstance();

        //如果返回的是true,说明并没有破坏单例模式,如果是false,说明破坏了单例模式
        System.out.println(s1 == s2); 
    }
}

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


  • 序列化反序列化

单例类

以静态内部类的方式实现单例模式,并实现实现序列化

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;
    }
}

测试类

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,表明序列化和反序列化已经破坏了单例设计模式。


3.2解决单例模式的破坏

反射方式破解单例的解决方法

单例类

当通过反射方式调用构造方法进行创建对象时,判断对象是否是第一次创建,如果不是第一次创建就抛出异常

public class Singleton {

    private static boolean flag = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class) {

            //判断flag的值是否是true,如果是true,说明非第一次访问,直接抛一个异常,如果是false的话,说明第一次访问
            if (flag) {
                throw new RuntimeException("不能创建多个对象");
            }
            //将flag的值设置为true
            flag = true;
        }
    }

    //定义一个静态内部类
    private static class SingletonHolder {
        //在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供公共的访问方式
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

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

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

单例类

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;
    }
    
    /**
     * 下面是为了解决序列化反序列化破解单例模式
     * 添加readResolve() 方法
     */
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

readResolve()方法源码解析

在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;
}

4单例模式的应用

Runtime类使用的就是单例模式

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
}

从上面源代码中可以看出Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的。

5.参考资料:

  • https://www.cnblogs.com/xuwendong/p/9633985.html#_label2
  • https://blog.csdn.net/weixin_39940206/article/details/89366157
  • https://www.bilibili.com/video/BV1Np4y1z7BU
  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值