单例模式1

单例模式

  • 单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

 

为什么要使用单例模式?

  • 在某些业务情况下,我们需要保证某个类的全局唯一性

    • 打印日志类。如果全局不唯一,日志文件成为竞争资源。可能被多个Looger类重写覆盖。就算是追加写,日志也会出现数据不一致的现象。
    • 程序配置类信息,我们大多数情况希望程序中只存在一份。
    • 全局ID生成器

 

如何实现一个单例模式?

  • 延迟加载 在用到的时候再去加载。
  • 创建过程的线程安全 表示可能会被其他线程通过 new 方式创建出来。
  • fail-fast原则 是有问题及早暴露。避免程序在运行中暴露错误。这样有利于提高系统的可用性。

 

1.饿汉式

 

  • 在类加载的时候,就会加载 instance 静态实例。
  • 优点:线程安全。并发度高。
  • 缺点:不支持延迟加载。

2. 懒汉式

 

  • 为了保证单例创建的线程安全。我们给 getInstance 方法加了一大把锁。
  • 优点:支持延迟加载。
  • 缺点:并发度低。如果量化一下并发度只有 1 。性能下降。

 

3.双重检查

  • 可以看见。我们只是将创建单例类的代码块使用 synchronized 锁起来了。在外一层提前判断了 synchronized 是不是被实例化。这样做的好处就是 被枷锁的代码只会执行一次保证了线程安全。且不会被多个线程竞争。保证了并发度。
  • 优点:并发度高。支持延迟加载。
  • 缺点:无

4.静态内部类

  • 在加载 IdGenerator 的时候,不会直接去实例化 instance 。而是调用 getInstance 方法时,才会去实例化。instance 的唯一性,创建过程的线程安全性,都由 JVM 来保证。
  • 优点:并发度高。支持延迟加载。相较于双重检查更简单。
  • 缺点:无

5.静态内部类

  • 利用java枚举本身的 特性。保证实例的线程安全性和实例唯一性。
  • 优点:并发度高。实现最简单。
  • 缺点:不支持延迟加载。

 

 

单例模式存在哪些问题?

  • 大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题

1.对OOP特性的支持不友好

  • 我们知道。OPP的四大特性是封装,抽象,继承,多态。**单例对抽象,继承,多态都支持得不好。
  • 单例模式违背了基于接口而非实现得设计原则,也违背了广义上得OPP抽象特性。

 

2.单例会隐藏类之间得依赖关系

  • 通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。
  • 单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

 

3.单例对代码的扩展性不友好

  • 如果在系统设计初期。没有考虑到一些业务的拓展。
  • 例如,在业务初期,我们会觉得系统中只会存在一份数据库连接池配置信息。但是随着业务的发展,有些sql执行非常缓慢。这样他就会阻塞其他sql的执行。那么我们就需要将快慢sql分开执行。所以就需要两份数据库连接池信息。

 

4.单例 对代码的可测试性不友好

  • 单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

 

5. 单例不支持有参数的构造函数

  • 单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。

    • 第一种解决思路就是:创建完实例之后,调用init()函数传递参数。需要注意的是,我们在使用这个单例的类的时候,要先调用init()方法。
    • 第二种解决思路是:将参数放到 getIntance方法中。但是这个方法有些问题,因为是单例的,所以在第二次设置的时候,就不会生效。
    • 第三种解决思路是:将参数放到另一个全局变量中。里面的值既可以直接定义 ,也可以通过配置文件加载得到。

6.有何替代解决方案

  • 为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

 

 

 

如何设计实现一个集群环境下的分布式单例模式?

1.如何理解单例模式中的唯一性?

  • 定义:一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
  • 范围:集群唯一 > 线程唯一 > 进程唯一
  • 小范围在大范围下不一定唯一。大范围在小范围下肯定唯一。

 

2.如何实现线程唯一的实例?

  • 在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。

 

3.如何实现集群环境下的单例

  • ,我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
  • 简化来说就是要找一个他们竞争资源来共享信息。

 

 

4.如何实现一个多例模式?

  • 多利是一个对象可以有多个实例。
  • 多利模式和工厂模式的区别在于工厂模式可以产生不同类的实例。而多利模式只能产生一个类的实例。
  • 实现的话使用一个HashMap来存储