设计模式--单例模式(反射)

单例模式

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

注意:

  1. 单例模式只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

介绍

意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决: 一个全局使用的类频繁地创建与销毁。

何时使用: 当您想控制实例数目,节省系统资源的时候。

如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码: 构造函数是私有的。

应用实例:

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

4、JDK源码中,Runtime(饿汉模式),Currency(序列化)

优点:

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

缺点:

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

1. 懒汉模式 线程不安全

1、线程安全问题

2、double check 加锁优化

3、编译器(JIT), CPU有可能对指令进行重排序,导致使用到尚未初始化的实例, 可以添加volatile关键词进行修饰。对于volatile修饰的字段,可以防止指令重排序(字节码)

实例

/**
 * @author hz
 * @date 2020/8/2 15:22
 */

// 单例模式

public class LazySingletonTest {
    public static void main(String[] args) {
        new Thread( () -> {
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(instance);
        }).start();
        new Thread( () -> {
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(instance);
        }).start();
    }
}

class LazySingleton{
    private static LazySingleton instance;
    private LazySingleton() {};

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
// 运行结果
singleton.LazySingleton@70b2a7f5
singleton.LazySingleton@4965b721

在多线程下,两次拿到的实例对象不是同一个。解决办法:在获取实例方法上添加关键词:synchronized

public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

字节码层面


从字节码的层面来说:
    instance = new LazySingleton(); 的执行顺序
	JIT CPU
	1. 分配空间
    2. 初始化
    3. 引用赋值   2,3 在单线程下可颠倒
        
多线程模式下:可能会出现空指针异常
解决办法:
       添加关键词 volatile 防止重排序
       private volatile static LazySingleton instance;

2. 饿汉模式

优点: 没有加锁,执行效率会提高。

缺点: 类加载时就初始化,容易产生垃圾对象,浪费内存。

类加载的初始化阶段完成了实例的初始化。 本质上就是借助JVM类加载机,制保证实例的唯一性。

类加载过程:
1. 加载二进制数据到内存中,生成对应的Class数据结构;
2. 连接:a.验证 b.准备(给类的静态成员变量赋默认值) c. 解析
3. 初始化:给类的静态变量赋初值

只有在真正使用对应的类时,才会触发初始化,如(当前类是启动类即main函数所在类,直接进行new操作,访问静态属性,访问静态方法,用反射访问类,初始化一个子类等)

public class HangrySingletonTest {
    public static void main(String[] args) {
        HangrySingleton instance = HangrySingleton.getInstance();
        HangrySingleton instance1 = HangrySingleton.getInstance();
        System.out.println(instance == instance1);  // true
    }
}

// 饿汉模式
class HangrySingleton{
    private static HangrySingleton instance = new HangrySingleton();
    private HangrySingleton() {};

    public static HangrySingleton getInstance() {
        return instance;
    }
}

3. 静态内部类

1、本质上是利用类的加载机制来保证线程安全

2、只有在实际使用的时候,才会触发类的初始化,所以也是一种懒加载形式

public class InnerClassSingletonTest {
    public static void main(String[] args) {
        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
        System.out.println(instance == instance1);  // true

        new Thread(() -> {
            InnerClassSingleton instance3 = InnerClassSingleton.getInstance();
            System.out.println(instance3);
        }).start();

        new Thread(() -> {
            InnerClassSingleton instance4 = InnerClassSingleton.getInstance();
            System.out.println(instance4);
        }).start();

    }
}

// 静态内部类  基于JVM的类加载机制,懒加载
class InnerClassSingleton{
    private static class InnerClassHolder {
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }

    private  InnerClassSingleton() {}

    // 只有当调用这个方法时,并返回值,才会导致静态内部类的初始化
    public static InnerClassSingleton getInstance() {
        return InnerClassHolder.instance;
    }
}

4. 反射攻击实例

以上方式创建单例对象时,通过反射创建出来的对象不是单例。

/**
 * @author hz
 * @date 2020/8/2 16:46
 */

// 通过反射创建实例,测试
public class ReflectionTest {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor();
        // 设置访问权限,使用private修饰的也会被获取
        constructor.setAccessible(true);
        // 通过反射获取到的实例
        InnerClassSingleton innerClassSingleton = constructor.newInstance();

        // 通过静态方法获取到的实例
        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton == instance);  // false
    }
}

如何预防反射攻击?

在使用饿汉模式和静态内部类的方式,可以通过以下方式实现反射攻击的防护,懒汉模式不能使用这种方式。

	private InnerClassSingleton() {
        if (InnerClassHolder.instance != null) {
            throw new RuntimeException("单例对象不允许多个实例对象");
        }
    }

5.枚举

1、天然不支持反射创建对应的实例,且有自己的反序列化机制

2、利用类加载机制保证线程安全

在通过反射获取实例时InnerClassSingleton innerClassSingleton = constructor.newInstance();

由JDK的源码可以看出是通过枚举的方式实现

	public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, clazz, modifiers);
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

描述: 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过反射攻击(reflection attack) 来调用私有构造方法。

// 单例模式  枚举
public enum  EnumSingleton {
    INSTANCE;

    public void print() {
        System.out.println(this.hashCode());
    }
}

class EnumTest {
    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        System.out.println(instance == instance1);  // true
    }
}

通过反射的方式创建,是不支持的

// 测试enum能否被反射实例化
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton instance = constructor.newInstance("INSTANCE", 0);
    }
//Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

反序列化机制

1、实现接口Serializable

2、通过流的方式序列化到文件

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
        oos.writeObject(instance); 
        oos.close();

3、从磁盘拿出数据

// 从磁盘读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton o = (InnerClassSingleton) ois.readObject();

通过反序列化的机制创建对象,他不会调用我们自己的构造函数。而是使用自己的序列化机制,直接从数据流中获取数据。

// 测试 反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
    InnerClassSingleton instance = InnerClassSingleton.getInstance();

    // 序列化到文件
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
    oos.writeObject(instance);
    oos.close();

    // 从磁盘读出
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
    InnerClassSingleton o = (InnerClassSingleton) ois.readObject();

    System.out.println(o == instance);  // false
}

解决方案

查看Serializable文档说明,添加方法readResolve

* Classes that need to designate a replacement when an instance of it
 * is read from the stream should implement this special method with the
 * exact signature.
 *
 * <PRE>
 * ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 * </PRE><p>

添加版本号

若不添加版本号,序列化时JVM会根据字段名,方法名生成一个版本号存到序列化文件中,下次反序列化时,会根据class生成系列号进行比对,如果一致,可以进行反序列化,不一致,说明文件修改过。

static final long serialVersionUID = 42L;
// 替换序列化机制
Object readResolve() throws ObjectStreamException {
    return InnerClassHolder.instance;
}

完整

public class InnerClassSingletonTest {
    // 测试 反序列化
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InnerClassSingleton instance = InnerClassSingleton.getInstance();

        // 序列化到文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
        oos.writeObject(instance);
        oos.close();

        // 从磁盘读出
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
        InnerClassSingleton o = (InnerClassSingleton) ois.readObject();

        System.out.println(o == instance);  // false
    }
}

// 静态内部类  基于JVM的类加载机制,懒加载
class InnerClassSingleton implements Serializable {
    
    static final long serialVersionUID = 42L;

    private static class InnerClassHolder {
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }

    private InnerClassSingleton() {
        if (InnerClassHolder.instance != null) {
            throw new RuntimeException("单例对象不允许多个实例对象");
        }
    }

    // 只有当调用这个方法时,并返回值,才会导致静态内部类的初始化
    public static InnerClassSingleton getInstance() {
        return InnerClassHolder.instance;
    }

    // 替换序列化机制
    Object readResolve() throws ObjectStreamException {
        return InnerClassHolder.instance;
    }
}

枚举序列化

// 序列化测试
public static void main(String[] args) throws IOException, ClassNotFoundException {
    EnumSingleton instance = EnumSingleton.INSTANCE;

    // 序列化到文件
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("enumSingleton"));
    oos.writeObject(instance);
    oos.close();

    // 从磁盘读出
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enumSingleton"));
    EnumSingleton o = (EnumSingleton) ois.readObject();

    System.out.println(o == instance);  // false
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值