前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单例模式的迭代式优化过程

单例模式的迭代式优化过程

作者头像
用户7506105
发布2021-08-09 15:17:24
2780
发布2021-08-09 15:17:24
举报
文章被收录于专栏:碎片学习录碎片学习录

在软件设计架构中,单例模式是最为常用的一种设计模式,所谓单例模式是指在创建某一个类的对象实例时该系统中有且仅有该类的一个实例,从而可以合理解决实例化对象的性能开销、资源分配等问题。从实现角度看,单例模式主要分为两种,一般称为饿汉式单例模式和懒汉式单例模式,以下逐一介绍

饿汉式单例模式

它是最简单实现单例模式的一种方式,所谓饿汉式是指它在类初始化时就会完成相关单例对象的创建(不会受任何不同条件的影响,即都会创建),可以想象以下在什么场景用什么方法可以在类初始化时就执行,熟悉java基础的一定可以想到是通过静态代码的形式进行创建。已经确定静态代码这一基本思路,则可以有两种实现方式,第一种是静态代码块方式,第二种是静态内部类方式

静态代码块实现

代码语言:javascript
复制
// 实现序列化接口是因为单例对象可能用于网络传输
public class HungrySingleton implements Serializable {
    // final关键字表示该对象不可更改,且该对象静态加载
    private static final HungrySingleton instance;

    // 静态代码块,在初始化HungrySingleton类时即执行
    static {
        instance = new HungrySingleton();
    }

    // 确保构造器私有,因为任何类在不写该方法时都会默认有一个公有的构造方法,从而可以直接 new HungrySingleton(),肯定会对象hash值不同与单例模式相悖
    private HungrySingleton() {}

    // 获取单例对象,外部程序直接调HungrySingleton.getInstance()获取对象
    public static HungrySingleton getInstance() {
        return instance;
    }
}

静态内部类实现

代码语言:javascript
复制
public class HungrySingleton {

    // 类是静态的,访问内部成员用类名.成员名
    // 静态类由getInstance触发
    private static class InnerClass {
        private static HungrySingleton instance = new HungrySingleton();
    }

    // 确保构造器私有,原因和上面一样
    private HungrySingleton() {}

    // 获取单例对象
    public static HungrySingleton getInstance() {
        return InnerClass.instance;
    }

}

以上为饿汉式单例的实现方法,它的优点是不存在线程安全问题,因为你多线程去获取对象走的都是getInstance方法,它只会触发一次而并不自动销毁,它的唯一性由jvm虚拟机在类初始化创建时保证,但是缺点是如果对象创建比较耗资源比如hbase的Connection对象,则如果实例的单例对象不使用就会造成资源的浪费

懒汉式单例

区别于饿汉式单例,它的思想是在需要使用单例对象时才创建,如果对象存在则直接返回,如果对象不存在则创建后返回。最简单的懒汉式单例模式实现如下

代码语言:javascript
复制
public class LazySingleton {

    // 静态加载时为空对象
    private static LazySingleton instance = null;

    // 原因和上面一样
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            // 如此一来就保证在调用getInstance方法才创建这个单例
            instance = new LazySingleton();
        }
        return instance;
    }
}

如果你认为这样就完成了懒汉式单例模式的创建,那就还是经验太少了,这样的实现在单线程场景是没有问题的,但是多线程场景的话是有问题的,具体什么问题下面说,一般单例都是创建连接比如socket等,多线程会出现问题,从而导致一直在new这个连接的bug。为什么多线程会出现问题,也就是为什么这个实现是线程不安全的,是因为

代码语言:javascript
复制
if (instance == null) {
    instance = new LazySingleton();
}

这个创建代码是非原子性的(原子性是指一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,比如多个线程进入这个逻辑,则instance临界资源就会不断销毁不断创建),想要保证原子性,一种常见手段是加内部锁synchronized(外部锁在此场景比较重量,耗资源),即

代码语言:javascript
复制
public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

此时方法是线程安全的,但性能却会存在问题,因为内部锁修饰的是静态方法即锁住的是整个类对象,意味着所有想要获取该单例对象的线程都必须要等待内部锁的释放,通俗解释就是如果一个线程进入临界区代码块创建好了单例对象,而后面有几百个线程要获取这个对象,则synchronized此时互斥的是这几百个线程,造成了几百个线程都要等待,显然这会降低系统的吞吐量,所以进一步考虑双重检查锁(即增加一次判断)进行优化实现

代码语言:javascript
复制
public class LazySingleton {

    // 使用volatile来禁止创建对象指令的重排序
    // 
    private static volatile LazySingleton instance = null;

    private LazySingleton() {
    }

    // 双重锁检查
    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

先简单回顾下对象创建的过程

  • 给对象分配堆内存空间;
  • 调用对象的构造器方法,并执行初始化操作(即完成静态飞马逻辑);
  • 将变量指向相应的内存地址(引用 类似是C++的指针)

假设单例对象已经被一个线程进入临界区创建成功,则此时instance一定不为null,所以所有线程都会直接跳出if,直接获得instance,而不会进入synchronized同步代码块,减小了锁的粒度,但是当该对象没创建好时,此时进入synchronized同步代码块仍然需要用if再判断一下以保证这个instance的创建操作是原子性的,是因为为null所以才能进行创建的

instance 需要使用 volatile 关键修饰,用于禁止对象在创建过程中出现指令重排序,这里的指令重排指的是创建对象的那三个过程,即第2、3步可能会发生指令重排序,在多线程下会出现线程不安全问题(单线程不会)

代码语言:javascript
复制
// 2. 由于线程1已经将变量指向内存地址,所以其他线程判断instance不为空,进而直接获取,但instance可能尚未初始化完成
if (instance == null) { 
    synchronized (LazySingleton.class) {
        if (instance == null) {
            // 1. 假设线程1已经给对象分配了内存空间并将变量instance指向了相应的内存地址,但尚未初始化完成,即尚未完成一些静态代码逻辑
            instance = new LazySingleton();
        }
    }
}
return instance;

所以由于重排序的存在,其他线程可能拿到的是一个尚未初始化完成的instance,此时就可能会导致异常,所以需要禁止其出现指令重排序。

以上双重检查且内部锁机制可以保证内存安全问题,在一般的场景也完全够用,但是一个系统中还是要保证自己创建的单例是否会在调用中有意或无意地被破坏,这是需要思考的。

一般来说有两种破坏单例模式的方法较为常见,一是网络序列化攻击,二是反射攻击

序列化攻击

序列化攻击是指将实例出来的对象序列化写入文件流,然后再反序列化获取对象,则这两次的对象不一致(hashcode不一样),demo如下

代码语言:javascript
复制
public class SerializationDamage {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 采用双重检查锁获取单例
        HungrySingleton instance = HungrySingleton.getInstance();
        // 获取输出流
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("SingletonFile"));
        // 将对象写入输出流
        outputStream.writeObject(instance);
        // 根据文件读取输入流
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("SingletonFile")));
        // 从输入流里面读取对象
        HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();
        // 会打印出false
        System.out.println(instance == newInstance); 
    }
}

这里相当于这个序列化会破坏这个单例创建方式,而是克隆了一个实例而已,为此需要实现序列化接口Serializable的readResolve方法,即

代码语言:javascript
复制
public class HungrySingleton implements Serializable {
    private Object readResolve() {
        return instance;
    }
}

因为生成文件的输入流inputStream的readObject()方法源码是

代码语言:javascript
复制
  // readObject在内部最终调用的是readOrdinaryObject方法
private Object readOrdinaryObject(boolean unshared) throws IOException{
        //hasReadResolveMethod判断对应的对象中是否有readResolve方法
        if (obj != null && handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod()) 
        {
            // 则通过反射调用该方法来获取对应的单例对象
            Object rep = desc.invokeReadResolve(obj);
          
           handles.setObject(passHandle, obj = rep);
        }
        return obj;
    }

反射攻击

反射的含义是获取原对象的Class类,根据Class类的方法去反射创建对象,调用方法等,java中反射功能过于强大,连公共权限和私有权限都破坏的了,所以这种破坏是无法规避的,demo如下:

代码语言:javascript
复制
public class ReflectionDamage {
    public static void main(String[] args) throws Exception {
        // 获取单例对象的构造器方法
        Constructor<HungrySingleton> constructor = HungrySingleton.class.getDeclaredConstructor();
        // 获取私有构造器的访问权限,且设置为可以访问,不管你私有还是公有
        constructor.setAccessible(true);
        // newInstance新建实例
        HungrySingleton hungrySingleton = constructor.newInstance();
        // 各种单例模式方法获取的单例
        HungrySingleton instance = HungrySingleton.getInstance();
        //  打印出为false
        System.out.println(hungrySingleton == instance); 
    }
}

即便在创建单例对象时将构造器声明为私有,此时仍然可以通过反射修改权限来获取,此时单例模式就被破坏了。

当你使用饿汉式单例方法时,可以采用

代码语言:javascript
复制
public class HungrySingleton implements Serializable {

    private static final HungrySingleton instance;
    // 静态代码块实现饿汉式单例
    static {
        instance = new HungrySingleton();
    }

    // 在私有构造器中抛异常,因为饿汉式单例对象是创建好的一定不可能为null,所以任何调用私有构造器的地方都会抛出异常
    // 即getDeclaredConstructor()方法获取不了构造器
    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("单例模式禁止反射调用");
        }
    }
}

如果使用的是懒汉式单例,此时由于无法知道对象何时会被创建,并且反射功能可以获取到任意字段,方法,构造器的访问权限,所以此时是没有任何方法能够规避掉反射攻击的

那么问题来了,有没有既可以保证线程安全、又不耗资源且又能有效地防止序列化合反射攻击的单例模式方法呢,在java中可以通过枚举类实现

枚举类单例

通用枚举类

代码语言:javascript
复制
public enum EnumInstance {

    INSTANCE;

    // 类的属性
    private String field;

    // 属性的get方法
    public String getField() {
        return field;
    }
    // 属性的set方法
    public void setField(String field) {
        this.field = field;
    }

    // 返回此时枚举类型的单例
    // 没有出现new对象的逻辑,因为对象类型为EnumInstance,字段是field,实例是INSTANCE,通过枚举已经引用到对应的堆空间地址了
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

枚举类是线程安全的,可以记住,可以看反编译的代码,和饿汉式单例类似,枚举类的字段通过get/set进行赋值

两种攻击的demo

代码语言:javascript
复制
public class EnumInstanceTest {
    public static void main(String[] args) throws Exception {
        // 序列化攻击
        // 获取枚举单例
        EnumInstance instance = EnumInstance.getInstance();
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingletonFile"));
        outputStream.writeObject(instance);
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("EnumSingletonFile")));
        EnumInstance newInstance = (EnumInstance) inputStream.readObject();
        // 返回的是true
        System.out.println(instance == newInstance);
        // 反射攻击,Enum类中选为两个参数的构造器:Enum(String name, int age)
        Constructor<EnumInstance> constructor = EnumInstance.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        // newInstance会抛异常,java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        EnumInstance enumInstance = constructor.newInstance("eric", 12);
        System.out.println(instance == enumInstance);
    }
}

此时可以保证序列化攻击无效,反射攻击抛异常

不得不说的是,软件开发流程也是如此的迭代下去,不断根据条件更严格的场景更换策略或优化策略,所以以后的技术思想也会逐渐迭代化,这就要求明确每一步优化到底是为了解决什么问题!加油!!!

附:收藏的python实现单例模式的几种方式,面试有手写

代码语言:javascript
复制
# 使用__new__方法,利用python自省(相当于java反射)
class Singleton(object):
    def __new__(cls, *args, **kwargs):
        # 如果没有隐变量_instance就调用__new__一个,python自省
        if not hasattr(cls, "_instance"):
            orig = super(Singleton, cls)
            cls._instance = org.__new__(cls, *args, **kwargs)
        return cls._instance
# 继承该单例模式
class MyClass(Singleton):
    pass
# 使用时直接,不线程安全
my_class = MyClass()

# 装饰器版本
def singletonDecor(cls):
    # 存储对象实例的字典
    instance = {}
    def getInstance(*args, **kwargs):
        if cls not in instance.keys():
            instance[cls] = cls(*args, **kwargs)
    return getInstance

# 装饰器参数是元类
@singletonDecor
class MyClass():
    pass


# 线程安全的单例模式
from functools import wraps
# python中采用的是可重入锁
from threading import RLock

def singleton(cls):
    """线程安全的单例装饰器"""
    instances = {}
    locker = RLock()
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            with locker:
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyClass():
    pass

# 还有一种,但不算是单例模式,但思想可以借鉴一下
# 采用__dict__共享变量的方式
# 创建实例时把所有实例的__dict__指向同一个字典,这样它们具有相同的属性和方法
# 相当于是new了两个相同的属性方法的对象,但是对象id不一样
class Singleton(object):
    _state = {}
    def __new__(cls, *args, **kwargs):
        obj = super(Singleton, cls).__new__(cls, *args, **kwargs)
        obj.__dict__ = cls._state
        return obj

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 碎片学习录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 饿汉式单例模式
    • 静态代码块实现
      • 静态内部类实现
      • 懒汉式单例
        • 序列化攻击
          • 反射攻击
          • 枚举类单例
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档