一,概念

1.1)什么是单例模式:

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责自己创建自己的对象,同时确保永远只有这一个对象被创建

这个类提供了一种外界访问其唯一对象的方式,外界可以直接访问自己创建好的自身对象 ,不能实例化该类的对象

1.2)单例模式的意图:

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

二,单例模式的特性:


1、单例就是该类只能返回⼀个实例,即,单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给外界提供访问这一实例的唯一方法。

三,单例代码的特点:

单例代码所具备的特点:

1、私有化的构造函数

2、私有的静态全局变量,最终指向自己创建的自身对象

3、供外界访问自己创建的自身对象的、公有的静态方法

四,关键代码:

判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
构造函数是私有的。

五,代码实现——5种方式


首先,我们将创建一个 SingleObject类。

SingleObject 类有它的私有构造函数、以及自身的一个静态全局实例。

SingleObject 类提供了一个静态方法,供外界获取它的这个静态全局实例。

5.1)懒汉式,线程不安全

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

特点:这种方式 lazy loading懒加载 很明显。

缺陷:在多线程环境下不能正常工作!懒汉式存在致命的问题:当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

  1. public class Singleton {
  2. //私有的静态全局变量
  3. private static Singleton instance;
  4. //私有的构造方法
  5. private Singleton (){}
  6. //供外界访问获取类对象的函数:
  7. public static Singleton getInstance() {
  8. if (instance == null) {
  9. instance = new Singleton();
  10. }
  11. return instance;
  12. }
  13. }


自己编写:

  1. public class SingleObject01 {
  2. //私有的构造函数:
  3. private SingleObject01() {
  4. }
  5. //提供外界访问的全局变量:
  6. private static SingleObject01 singleObject01;
  7. //供外界访问的方法:
  8. public static SingleObject01 getInstance() {
  9. if (singleObject01 == null) {
  10. singleObject01= new SingleObject01();
  11. }
  12. return singleObject01;
  13. }
  14. /**
  15. * 分析:
  16. * 这种懒汉式的严格来讲,在多线程访问的情况下,都不是单例模式了,
  17. * 因为如果有多个线程同时访问,那么得到的将是多个SingleObject01对象。
  18. */
  19. }

5.1)懒汉式—加锁优化,线程安全

描述:这种方式具备很好的 lazy loading懒加载效果,能够在多线程中很好的工作,但是效率很低,99% 情况下不需要同步

缺点:虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法,会严重影响效率

  1. public class Singleton {
  2. //私有的静态全局变量
  3. private static Singleton instance;
  4. //私有的构造函数
  5. private Singleton (){}
  6. //加锁之后的供外界访问获取类对象的方法
  7. public static synchronized Singleton getInstance() {
  8. if (instance == null) {
  9. instance = new Singleton();
  10. }
  11. return instance;
  12. }
  13. }

自己编写:

  1. public class SingleObject02 {
  2. //私有构造函数:
  3. private SingleObject02() { }
  4. //私有的静态全局变量:
  5. private static SingleObject02 singleObject02;
  6. //供外界访问上边全局变量的方法:
  7. public static synchronized SingleObject02 getInstance() {
  8. if (singleObject02 == null) {
  9. singleObject02 = new SingleObject02();
  10. }
  11. return singleObject02;
  12. }
  13. }

注意:
但是同步操作的代码其实只在第一次调用时才被需要用到,因为只有在第一次的时候才创建了单例实例对象时。

对于第二次、第三次等以后的调用,直接返回第一次的时候实例化好的对象即可。这就引出了双重检验锁

5.2)双重检验锁

是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双重检验锁机制,安全且在多线程情况下能保持高性能。

  1. public class Singleton {
  2. //私有的静态全局变量
  3. private static Singleton singleton;
  4. //私有的构造函数
  5. private Singleton (){}
  6. //供外界访问获取类对象的方法:
  7. public static Singleton getSingleton() {
  8. //双重检验锁:
  9. if (singleton == null) {
  10. //锁住的范围是当前这个单例类:
  11. synchronized (Singleton.class) {
  12. if (singleton == null) {
  13. singleton = new Singleton();
  14. }
  15. }
  16. }
  17. //如果是第二次、第三次之后的访问,单例对象singleton已经在第一次被创建好了,所以直接返回该实例对象:
  18. return singleton;
  19. }
  20. }

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,

事实上在 JVM 中这句话大概做了下面 3 件事情:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),
所以线程二会直接返回 instance,然后使用,就会报错。

解决办法就是:我们只需要将 instance 变量声明成 volatile 就可以了。

使用 volatile 的主要原因是其一个特性:禁止指令重排序优化。

  1. public class Singleton {
  2. //使用volatile关键字修饰的私有的静态全局变量
  3. private volatile static Singleton singleton;
  4. //私有的构造函数
  5. private Singleton (){}
  6. //供外界访问获取类对象的静态方法
  7. public static Singleton getSingleton() {
  8. //双重检验锁:
  9. if (singleton == null) {
  10. synchronized (Singleton.class) {
  11. if (singleton == null) {
  12. singleton = new Singleton();
  13. }
  14. }
  15. }
  16. return singleton;
  17. }
  18. }

自己编写:

  1. public class SingleObject04 {
  2. //私有的构造函数:
  3. private SingleObject04() {}
  4. //私有的静态全局变量:
  5. private static volatile SingleObject04 singleObject04;
  6. //供外界获取该类对象的方法:
  7. public static SingleObject04 getInstance() {
  8. if (singleObject04 == null) {
  9. synchronized (SingleObject04.class) {
  10. if (singleObject04 == null) {
  11. singleObject04 = new SingleObject04();
  12. }else{
  13. return singleObject04;
  14. }
  15. }
  16. }
  17. return singleObject04;
  18. }
  19. }

5.3)饿汉式


描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

基于 classloader类加载机制 避免了多线程的同步问题,不过,类对象在类加载时就实例化,虽然导致类加载的原因有很多种,
在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类加载,
如果有其他的方式导致了类加载、从而初始化了 类对象instance ,显然没有达到 lazy loading懒加载的效果。

  1. public class Singleton {
  2. //私有的静态全局变量,指向类加载时就实例化好的类对象;
  3. private static Singleton instance = new Singleton();
  4. //私有的构造函数
  5. private Singleton (){}
  6. //供外界访问获取类对象的静态方法:
  7. public static Singleton getInstance() {
  8. return instance;
  9. }
  10. }

自己编写:

  1. public class SingleObject03 {
  2. //私有构造函数
  3. private SingleObject03() {}
  4. //私有的静态全局变量,饿汉式--直接提前实例化好
  5. private static SingleObject03 singleObject03 = new SingleObject03();
  6. //供外界访问的方法:
  7. public static SingleObject03 getInstance() {
  8. return singleObject03;
  9. }
  10. }


·

5.4)静态内部类

是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。这种方法也是《Effective Java》上所推荐的

静态内部类写法的优势:

  1. 这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,又因为外部类加载时、并不需要立即加载内部类因此该写法是懒汉式的、懒加载的;
  2. 同时访问实例对象的时候,不需要进行同步措施,还能保证线程安全问题,没有性能缺陷

    PS:什么是懒加载:一开始不会创建实例对象、只有当访问该实例对象的时候才会去创建

示例代码:

  1. public class Singleton {
  2. //私有的构造函数:
  3. private Singleton (){}
  4. //静态内部类:
  5. private static class SingletonHolder {
  6. //供外界访问的静态常量-实例对象:
  7. private static final Singleton INSTANCE = new Singleton();
  8. }
  9. //供外界访问获取类对象的方法:
  10. public static Singleton getInstance() {
  11. return SingletonHolder.INSTANCE;
  12. }
  13. }


自己编写:

  1. public class SingleObject05 {
  2. //私有的构造函数:
  3. private SingleObject05() {
  4. }
  5. //将静态全局变量放到静态内部类里边:
  6. private static class SingleObject05Inner{
  7. private static final SingleObject05 SINGLE_OBJECT_05=new SingleObject05();
  8. }
  9. //供外界访问的方法:
  10. public static SingleObject05 getInstance() {
  11. return SingleObject05Inner.SINGLE_OBJECT_05;
  12. }
  13. }


·

为什么静态内部类能够实现单例模式,且保证了线程安全?

我们刚才讲到,静态内部类的优点:

静态内部类写法的优势:

  1. 这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,又因为外部类加载时、并不需要立即加载内部类因此该写法是懒汉式的、懒加载的;
  2. 同时访问实例对象的时候,不需要进行同步措施,还能保证线程安全问题,没有性能缺陷

    PS:什么是懒加载:一开始不会创建实例对象、只有当访问该实例对象的时候才会去创建

关于懒加载的优势:
外部类加载时并不需要立即加载内部类,内部类不被加载则不会去创建实例对象,故而可以节省内存。
具体来说当单例类第一次被加载时,并不需要去加载静态内部类,
只有当getInstance()方法第一次被调用时,才会导致JVM去加载该单例类的静态内部类
这种方式,不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例对象的实例化。

为什么可以保证线程安全?

详情参见:

·

5.5)枚举

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

  1. public enum Singleton {
  2. INSTANCE;
  3. public void whateverMethod() {}
  4. }


·

六,建议

一般情况下,不建议使用第 1 种非线程安全的懒汉式单例和第 2 种线程安全的懒汉方式,建议使用第 3 种饿汉方式。

只有在明确必须要实现 lazy loading 懒加载效果时,才使用第四种双重检验锁方式第 5 种静态内部类方式。

一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

PS:什么是 lazy loading 懒加载?

懒加载其实就是延时加载,即当对象需要用到的时候再去加载。

什么叫做需要用到的时候?比如说一个对象被创建出来就需要一笔内存开支,如果接下来就没有其他的操作,那可以认为这个对象创建得“过早”了。

好处:内存占用小。