设计模式之单例模式

本文详细介绍了设计模式中的单例模式,包括其概念、结构、饿汉式和懒汉式的实现方式,以及如何解决多线程环境下可能出现的问题。同时,讨论了序列化和反射如何破坏单例,并提供了相应的解决方案。最后,指出枚举是实现线程安全单例的最佳实践。
摘要由CSDN通过智能技术生成

设计模式之单例模式

1. 概括

如果你希望一个类从始至终,都只能创建一个全局对象,那么应该使用单例模式

2. 结构

  • 写一个类,提供一个获取自己实例的方法
  • 私有化构造方法

3. 实现

单例模式有饿汉式和懒汉式两种

3.1 饿汉式

  • 是指在类加载时就去创建单例对象
  • java有两种方式,成员变量声明式、静态代码块式
  • 成员变量声明式
public class Singleton {
    // 构造私有化
    private Singleton() {
    }
    // 单例对象
    private static Singleton instance = new Singleton();
    // 提供获取方法
    public static Singleton getInstance() {
        return instance;
    }
}
  • 静态代码块式
public class Singleton {
    // 构造私有化
    private Singleton() {
    }

    // 静态代码块
    private static Singleton instance;
    static {
        instance = new Singleton();
    }

    // 提供获取方法
    public static Singleton getInstance() {
        return instance;
    }
}

3.2 懒汉式

  • 只有在获取对象时,对象才会被创建
public class Singleton {
    // 构造私有化
    private Singleton() {
    }
    
    private static Singleton instance;

    public static Singleton getInstance() {
        // 如果是第一次获取实例,才会创建
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
3.2.1 注意

这样会有一个问题,如果有多线程条件下,线程1经过if判断后,在创建对象之前,线程2抢到了线程,那么线程1和2就会创建两个不同的对象,这样就破坏了单例模式

3.2.2 问题解决
  1. 同步锁
    • 在获取对象实例的方法上synchronized关键字
    • 但是这样,程序的效率就会变低,因为线程问题只会在第一次创建实例时才会出现问题,而synchronized关键字每次都会加锁
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
  1. 双重检查锁(同步代码块)
    • 双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, 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;
}
  1. 静态内部类式(推荐
    • JVM在加载类过程中,不会加载静态内部类,只有内部类的属性和方法被调用时才会被加载,利用这个特性,把要实例的对象在静态内部类中声明,用static final修饰,保证被实例一次
public class Singleton {

    // 构造私有化
    private Singleton() {
    }
	// 静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  1. 枚举
    • 枚举线程安全,并且只会装载一次写法简单
    • 枚举是饿汉式
public enum Singleton {
    INSTANCE;
}
public class Client {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println(instance1 == instance2); // true
    }
}

4. 单例破坏

  • 虽说私有的构造方法,但利用java的特性仍可以轻松破坏单例(枚举除外),
  • 一般有两种方式:序列化、反射

4.1 序列化

  • 破坏演示:反序列化两次
public class Client {
    public static void main(String[] args) throws Exception{
        Singleton instance = Singleton.getInstance();
//        序列化
//        writeObject(instance);
		// 反序列化两次
        Singleton singleton1 = readObject();
        Singleton singleton2 = readObject();
        System.out.println(singleton1 == singleton2); // false
    }

    public static void writeObject(Singleton singleton) throws Exception{
        ObjectOutputStream outputStream
                = new ObjectOutputStream(new FileOutputStream("C:\\Users\\admin\\Desktop\\新建文件夹\\aaa"));
        // 序列化
        outputStream.writeObject(singleton);
        outputStream.flush();
        outputStream.close();
    }

    public static Singleton readObject() throws Exception {
        ObjectInputStream inputStream
                = new ObjectInputStream(new FileInputStream("C:\\Users\\admin\\Desktop\\新建文件夹\\aaa"));
        return (Singleton) inputStream.readObject();
    }
}
  • 解决:在单例类中提供一个Object readResolve()的方法,返回单例对象,这和jdk源码有关
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;
    }

    /**
     * 解决 序列化 破坏单例
     * @return
     */
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

4.2 反射

  • 破坏演示:反射构造方法,用构造对象创建两次实例
public class Client {
    public static void main(String[] args) throws Exception{
    	// 获取单例类的class对象
        Class<Singleton> clazz = Singleton.class;
        // 获取构造器
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
		// 关闭安全检查
        constructor.setAccessible(true); // 暴力反射
        // 创建两次对象
        Singleton singleton1 = constructor.newInstance();
        Singleton singleton2 = constructor.newInstance();
        System.out.println(singleton1 == singleton2); // false
    }
}
  • 解决:在构造创建对象时,如果不是第一次创建对象,报一个异常,停止程序运行
public class Singleton implements Serializable {

    private static boolean flag = false;
    // 构造私有化
    private Singleton() {
    	// 第一次是false
        if (flag) {
            throw new RuntimeException("不可创建多实例");
        }
        // 至为true,防止第二次进入
        flag = true;
    }
	// 省略....
}

4.3 绝杀,无解

但是、但是、但是:反射仍然是可以反射private static boolean flag

public class Client {
    public static void main(String[] args) throws Exception{
        Class<Singleton> clazz = Singleton.class;
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true); // 暴力反射
		// 第一次创建对象
        Singleton singleton1 = constructor.newInstance();
        
		// 获取flag的Filed对象
        Field flag = clazz.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.setBoolean(singleton1, false); // 设为false
        
		// 第二次创建对象
        Singleton singleton2 = constructor.newInstance();
        System.out.println(singleton1 == singleton2); // false
    }
}

所以:反射破坏单例性解决办法:绝杀,无解!!!!!(除非使用枚举)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值