设计模式——单例模式

122 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

简述

属于创建型模式 , 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

优点

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例)。
  • 避免对资源的多重占用(比如写文件操作)。

缺点

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

使用场景:

  • 要求生成唯一序列号的环境;
  • 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数 器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式 (当然,也可以直接声明为static的方式)。

实现

一、饿汉式(静态属性赋初始化值)

  • 类加载到内存后,就实例化一个单例,JVM保证线程安全
  • 简单实用,推荐使用!
  • 唯一缺点:不管用到与否,类装载时就完成实例化

1653838342091.png

此处有细节!

  1. 私有静态实例,防止被引用
  2. 私有构造方法,防止被引用
  3. 只暴露 getInstance 方法
  4. 在实际项目中可能会习惯于申明public的属性和方法或者直接使用lombok,这样会导致单例的不完全,会有误用的隐患

二、饿汉式(静态代码块)

  • 和第一种方式区别不大

1653839055010.png

三、懒汉式(线程不安全版)

  • lazy loading
  • 也称懒汉式
  • 虽然达到了按需初始化的目的,但却带来线程不安全的问题

1653839351779.png

图中main函数为测试代码,测试一下,输出值为:

{
    "733781502":1,
    "827914427":1,
    "957478169":1,
    "845345740":4,
    "1028594625":1,
    "855985814":1,
    "722702912":75,
    "713625663":1,
    "71690323":1,
    "171789701":1,
    "1111541821":1,
    "865029649":1
}

可以看出并发100线程取创建单例的时候,会有线程安全问题,此次测试建了12个不同的对象出来,因此来了一种改良版的。

四、懒汉式(线程安全版)

  • lazy loading* 也称懒汉式
  • 虽然达到了按需初始化的目的,但却带来线程不安全的问题
  • 可以通过synchronized解决,但也带来效率下降

1653839326716.png

再来运行一下main'函数

{
    "733756802":100
}

这下就线程安全了,但是因为引入了synchronized会导致性能的一定下降

五、懒汉式(线程安全版双重检查版本)

1653839576513.png

这是一种公认的比较好的懒汉式写法,通过双重检查来提高效率

六、懒汉式(静态内部类方式)

  • 静态内部类方式
  • JVM保证单例
  • 加载外部类时不会加载内部类,这样可以实现懒加载
  • 看似是最完美的方法了,但是还是会存在反射攻击或者反序列化攻击( constructor.newInstance(); )

1653840148299.png

反射攻击:

    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();

序列化攻击:

    Singleton instance = Singleton.getInstance();
    byte[] serialize = SerializationUtils.serialize(instance);
    Singleton newInstance = SerializationUtils.deserialize(serialize);

七、枚举方式(完美)

在《Effective Java》书中有这样一句话:单元素的枚举类型已经成为实现Singleton的最佳方法。 因为枚举就是一个天然的单例,并且枚举类型通过反射都无法获取封装的私有变量,非常安全。

  • 枚举是线程安全的,而且任何情况下都是一个实例
  • 枚举不能实例化,不能被反射
  • 非懒加载,防反序列化

1653840316352.png

那这种是懒汉还是饿汉呢?

因为单例对象实例化放在了成员变量声明的时候,由于该枚举只有⼀个INSTANCE实例,所以instance只会存在⼀个实例

因此也是属于懒汉的!

其他

Spring的Bean

通过Spring管理的类,默认也都是单例的

这是我们平时一致用的,就不过多赘述了,那。。。什么时候用多例呢???

单例模式和多例模式说明:

  1. 单例模式和多例模式属于对象模式。
  2. 单例模式的对象在整个系统中只有一份,多例模式可以有多个实例。
  3. 它们都不对外提供构造方法,即构造方法都为私有。

如何产生单例多例:

在配置文件的bean中添加scope="prototype";

多例模式

使用场景

  • 防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象的状态改变导致了对象对另一个请求做了错误的处理;
  • 比如对sku(对象的字段偏多)的字段进行操作,在上下文引用过程中需要频繁使用sku的较多字段,使用多例模式会使代码更加优雅和扩展性更高

生产多例类

@Component
public abstract class LookupContext {
 
    @Lookup("skuContext")
    public abstract SkuContext skuContext();
}

SkuContext sku的上下文引用

@Slf4j
@Component
@Scope(SCOPE_PROTOTYPE)
public class SkuContext {
 
     // sku的属性组list
    List<Material> materialGroups;
 
    @Override
    public void wrapProcess(Sku sku) {
        this.materialGroups=sku.getMaterialGroups();
        log.info("属性:{}",materialGroups);
    }
}

SkuService sku的执行类

@Slf4j
public class SkuService {
 
    @Autowired
    private LookupContext lookupContext;
​
    void wrapProcess(Sku sku) {
        // 包装流程
        lookupContext.skuContext().wrapProcess(sku);
    }
}

总结

如果后续需要对sku做个性化差异,继承SkuContext的类实现差异化,然后将多例注入到LookupContext中就行了。

具体多例模式还有哪些合适的场景,容我在探究探究

参考

多例:

juejin.cn/post/699905…

blog.csdn.net/qq_23160151…