Java单例模式一文通

简介: Java单例模式一文通

在程序开发中我们往往会涉及到设计模式,那么什么是设计模式呢?官方正式的定义是一套被反复使用经过分类编目,且多数人知晓的代码设计经验总结。简单的说设计模式是软件开发人员在软件开发过程中面临问题时所做出的解决方案。常用的设计模式有23中,因为篇幅有限在本篇文章中我之讲解23中设计模式中最经典的模式:单例模式。


零、什么是单例模式

单例模式是创建型模式的一种,它主要提供了创建类对象的最优方式。在单例模式下每个类负责创建自己的对象,并且要保证每个类只创建一个对象,也就是说每个类只能提供唯一的访问对象的方式。


单例模式的出现是为了解决全局类被频繁的创建和销毁造成的性能开销,以及避免对资源的多重占用。单例模式虽然解决了这些问题但是它存在几个问题,首先在单例模式下没有接口无法继承,其次它还与单一职责原则相互冲突,并且单例模式下的类只关注内部实现逻辑,不关注外部如何实例化。


Java 中实现单例模式的方式有六种,分别是饿汉模式、懒汉模式、加锁懒汉模式、双重判定加锁懒汉模式、内部静态类实现懒汉模式以及枚举懒汉模式。下面我分别对这六种实现单例模式的方法进行一一讲解。


一、饿汉模式

所谓饿汉模式就是在类被加载时就会实例化该类的一个对象,它就像是一个很饥饿人要迫不及待的吃东西一样。以饿汉模式编写的单例模式需要注意如下两点,首先类的构造函数必须定义为 private,这是为防止该类不会被其他类实例化,其次类必须提供了静态实例,并通过静态方法返回给调用方。下面我们就根据这两点来编写饿汉模式的单例模式。

//饿汉模式
public class HungryMode {
  //1.定义私有构造函数
  private HungryMode() {
  }
  //2.定义静态实例并返回给调用方
  private static HungryMode hungryMode=new HungryMode();
  public static HungryMode getHungryMode() {
    return hungryMode;
  }
}

饿汉模式的代码很简单,按照前面所说的两个注意点进行编写代码即可。这种模式可以快速且简单的创建一个线程安全的单例对象,之所以说它是线程安全的,是因为它只在类加载时才会初始化,在类初始化后的生命周期中将不会再次进行创建类的实例。因此这种模式特别适合在多线程情况下使用,因为它不会多个线程中创建多个实例,避免了多线程同步的问题。但是万物不是只有优点没有缺点的,这种模式最大的缺点就是不管你是否用到这个类,这个类都会在初始化的时候被实例化,这样会造成性能的轻微损耗。饿汉模式一般用于占用内存小并且在初始化时就会被用到的时候。


二、懒汉模式

什么是懒汉模式呢?懒汉模式就是只在需要对象时才会生成单例对象,它就像一个很懒的人只有在你叫他的时候他才会动一动。和饿汉模式一样,懒汉模式也有两点需要注意的,首先类的构造函数必须定义为 private,其次类必须提供静态实例对象且不进行初始化,并通过静态方法返回给调用方,在编写返回实例的静态方法时我们需要判断实例对象是否为空,如果为空则进行实例化反之则直接放回实例化对象。下面我们就来看以下代码如何实现懒汉模式。

//懒汉模式
public class LazyMode {
  //1.定义私有构造函数
  private LazyMode() {
  }
  //2.静态实例对象且不进行初始化
  private static LazyMode lazyMode;
  //3.编写静态方法,返回实例对象
  public static LazyMode getLazyMode() {
    if(lazyMode==null)
      lazyMode=new LazyMode();
    return lazyMode;
  }
}

懒汉模式规避了饿汉模式的缺点,只有在我们需要用到类的时候才会去实例化它,并且通过饿汉模式类中的静态方法(本例中的getLazyMode),基本上规避了重复创建类对象的问题。到这里就需要注意了我所说的是基本上规避,而不是完全规避,我为什么这么说呢?这是因为懒汉模式并没有考虑在多线程下当类的实例对象没有被生成的时候很有可能存在多个线程同时进入 getLazyMode 方法,并同时生成实例对象的问题。因此我们说在懒汉模式下实现的单例模式是线程不安全的。那么这个问题怎么解决呢?这时我们就可以使用加锁懒汉模式,我们来看一下代码如何实现。

//加锁懒汉模式
public class LockSluggerMode {
  //1.定义私有构造函数
  private LockSluggerMode() {
  }
  //2.静态实例对象且不进行初始化
  private static LockSluggerMode lockSluggerMode;
  //3.编写静态方法,返回实例对象
  public static LockSluggerMode getLazyMode() {
    synchronized(LockSluggerMode.class) {
      if(lockSluggerMode==null) {
        lockSluggerMode=new LockSluggerMode();
      }
    }
    return lockSluggerMode;
  }
}

在上面的代码中我们增加了同步锁,这样就避免了前面所说的问题。加锁懒汉模式和懒汉模式的相同点都是在第一次需要时,类的实例才会被创建,再次调用将不会重新创建新的实例对象,而是直接返回之前创建的实例对象。这两种模式都适用于单例类的使用次数少,但消耗资源较多的时候。但是加锁懒汉模式因为涉及到了锁,因此与懒汉模式相比多了一些额外的资源消耗。


三、双重判定加锁懒汉模式

双重判定加锁懒汉模式在 Java 面试中会被经常问到,但是很少有人能够正确的写出双重判定加锁懒汉模式的代码,甚至很少有人会说出来这种模式的问题,以及在 JDK1.5版本中是如何修正这个问题的。针对这几个问题我在这一小节中进行一一讲解。


双重判定加锁懒汉模式的实现其实是创建线程安全单例模式的老方法,当单例的实例被创建时它会用单个锁进行性能优化,但是因为这个方法实现起来很复杂,因此在 JDK1.4 中实现总是失败。在 JDK1.5 没有修正这个问题前,为什么还需要这个模式呢?这时因为在加锁懒汉模式中虽然解决了线程并发的问题,又实现了延迟加载,但是它存在性能问题。这是因为使用 synchronized 的同步方法执行速度会比普通方法慢得多,如果多次调用获取实例的方法时积累的性能损耗就会很大,因此就出现了双重判定加锁懒汉模式。我们先来看一下具体的代码实现。

public class DoubleJudgementLockSluggerMode {
  private static DoubleJudgementLockSluggerMode doubleJudgementLockSluggerMode;
  private DoubleJudgementLockSluggerMode() {
  }
  public static DoubleJudgementLockSluggerMode getInstance() {
    if(doubleJudgementLockSluggerMode==null) {
      synchronized (DoubleJudgementLockSluggerMode.class) {
        if(doubleJudgementLockSluggerMode==null) {
          doubleJudgementLockSluggerMode=new DoubleJudgementLockSluggerMode();
        }
      }
    }
    return doubleJudgementLockSluggerMode;
  }
}

在上述代码中我们在同步代码块外层多加了一个 doubleJudgementLockSluggerMode 是否为空的判断,因此在大部分情况下调用 getInstance 方法都不会执行同步代码块,而是直接返回已经实例化的对象,进而提高了代码的性能。下面我们考虑一个问题,如果程序中存在线程一和线程二,当线程一执行了外层的判断语句它发现实例对象没有创建,然而这个时候线程二也执行到了外层判断语句,它同样发现实例对象没有创建,然后这两个线程依次执行同步代码块中的内容,分别创建了连个实例对象,对于单例模式来说这种情况我们必须避免,因此我么们在同步代码块中增加了 if(doubleJudgementLockSluggerMode==null) 判断语句来解决这个问题。


到目前为止虽然实现了延迟加载和线程并发问题,同时也解决了执行效率问题但是在 JDK1.5 之前还存一些问题。首先在 Java 中存在指令重排优化,这个功能会在不改变原有语义的情况下调整指令顺序来让程序运行的更快。但是在 JVM 中并没有规定优化哪些内容,所以 JVM 可以随意的进行指令重排优化。这样就引出了一个问题,因为指令重排优化的存在会导致初始化 DoubleJudgementLockSluggerMode 和将对象地址付给 doubleJudgementLockSluggerMode 的顺序发生改变。如果在创建单例对象时,在构造函数被调用之前就已经给当前对象分配了内存并且还将对象的字段赋予了默认值,那么此时如果将分配的内存地址赋予 doubleJudgementLockSluggerMode 字段,并有一个线程来调用 getInstance 方法,由于该对象有可能尚未初始化,因此程序就会报错。但是在 JDK1.5 中修正了这个问题,我们只需要利用 volatile 关键字来禁止指令重排优化来避免上述问题。增加 volatile 关键字后,代码如下:

public class DoubleJudgementLockSluggerMode {
  private static volatile DoubleJudgementLockSluggerMode doubleJudgementLockSluggerMode;
  private DoubleJudgementLockSluggerMode() {
  }
  public static DoubleJudgementLockSluggerMode getInstance() {
    if(doubleJudgementLockSluggerMode==null) {
      synchronized (DoubleJudgementLockSluggerMode.class) {
        if(doubleJudgementLockSluggerMode==null) {
          doubleJudgementLockSluggerMode=new DoubleJudgementLockSluggerMode();
        }
      }
    }
    return doubleJudgementLockSluggerMode;
  }
}

四、内部静态类懒汉模式

双重判定加锁懒汉模式实现起来不仅复杂,在 JDK1.4 及其以下版本上还存在指令重排优化的问题,那么有没有既解决了线程安全的问题又可以实现懒加载的单例模式的实现方法呢?答案是有的,我们可以利用静态内部类来实现。我们先来看一下代码如何实现。

// 内部静态类懒汉模式
public class StaticInnerClass {
  private StaticInnerClass() {}
  //定义内部静态来
  private static class StaticInnerClassHolder{
    //在内部静态类中实例化 StaticInnerClass
    public static StaticInnerClass staticInnerClass =new StaticInnerClass();
  }
  public static StaticInnerClass getStaticInnerClass() {
    return StaticInnerClassHolder.staticInnerClass;
  }
}

这种方式和饿汉模式一样都是利用了类加载机制,因此不存在对线程并发的问题,同时只要不适用内部类 JVM 就不会去创建单例对象,进而实现了与懒汉模式一样的延迟加载。但是这种方式会导致最终生成的 class 文件变大,程序体积变大。


五、枚举懒汉模式

枚举懒汉模式在开发中并不常用,一般来说如果你编写的类既要支持序列化和反射,又要支持单例模式的话可以使用枚举懒汉模式,但是因为使用了枚举因此会造成内存占用过大的问题。下面我们来看以下代码,然后根据代码来详细讲解枚举懒汉模式。

class EnumMode {
  //more code
}
public enum ModeEnum{
  INSTAMCE;
  private EnumMode enumMode;
  private ModeEnum() {
    enumMode=new EnumMode();
  }
  public EnumMode getEnumMode() {
    return enumMode;
  }
}

在上述代码中如果要获取 EnumMode 实例对象我们必须这样调用 ModeEnum.INSTAMCE.getEnumMode()。那么枚举懒汉模式实现的原理是什么呢?首先在枚举中明确了构造方法并设置为私有,当我们访问枚举实例的时候会执行构造方法,同时每个枚举实例是 static final 类型,因此只能被实例化一次。只有在构造方法被调用时单例才会被实例化。


六、总结

这篇文章讲解了 Java 中单例模式的实现方式,这些实现方式中常用的是饿汉模式和双重判定加锁懒汉模式这两种,其他方式我们也需要掌握。最后我总结一下实现单例模式各种方式的线程安全问题。

image.png

目录
相关文章
|
11月前
|
设计模式 安全 Java
Java编程中的单例模式深入剖析
【10月更文挑战第21天】在Java的世界里,单例模式是设计模式中一个常见而又强大的存在。它确保了一个类只有一个实例,并提供一个全局访问点。本文将深入探讨如何正确实现单例模式,包括常见的实现方式、优缺点分析以及最佳实践,同时也会通过实际代码示例来加深理解。无论你是Java新手还是资深开发者,这篇文章都将为你提供宝贵的见解和技巧。
177 65
|
10月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
140 4
|
10月前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
315 2
|
10月前
|
设计模式 安全 Java
Java编程中的单例模式深入解析
【10月更文挑战第31天】在编程世界中,设计模式就像是建筑中的蓝图,它们定义了解决常见问题的最佳实践。本文将通过浅显易懂的语言带你深入了解Java中广泛应用的单例模式,并展示如何实现它。
|
设计模式 安全 Java
Java 编程中的设计模式:单例模式的深度解析
【9月更文挑战第22天】在Java的世界里,单例模式就像是一位老练的舞者,轻盈地穿梭在对象创建的舞台上。它确保了一个类仅有一个实例,并提供全局访问点。这不仅仅是代码优雅的体现,更是资源管理的高手。我们将一起探索单例模式的奥秘,从基础实现到高级应用,再到它与现代Java版本的舞蹈,让我们揭开单例模式的面纱,一探究竟。
105 11
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
设计模式 Java 安全
Java设计模式-单例模式(2)
Java设计模式-单例模式(2)
|
10月前
|
设计模式 SQL 安全
Java编程中的单例模式深入解析
【10月更文挑战第24天】在软件工程中,单例模式是设计模式的一种,它确保一个类只有一个实例,并提供一个全局访问点。本文将探讨如何在Java中使用单例模式,并分析其优缺点以及适用场景。
82 0
|
11月前
|
设计模式 SQL 安全
【编程进阶知识】Java单例模式深度解析:饿汉式与懒汉式实现技巧
本文深入解析了Java单例模式中的饿汉式和懒汉式实现方法,包括它们的特点、实现代码和适用场景。通过静态常量、枚举类、静态代码块等方式实现饿汉式,通过非线程安全、同步方法、同步代码块、双重检查锁定和静态内部类等方式实现懒汉式。文章还对比了各种实现方式的优缺点,帮助读者在实际项目中做出更好的设计决策。
299 0
|
设计模式 安全 Java
Java 单例模式,背后有着何种不为人知的秘密?开启探索之旅,寻找答案!
【8月更文挑战第30天】单例模式确保一个类只有一个实例并提供全局访问点,适用于需全局共享的宝贵资源如数据库连接池、日志记录器等。Java中有多种单例模式实现,包括饿汉式、懒汉式、同步方法和双重检查锁定。饿汉式在类加载时创建实例,懒汉式则在首次调用时创建,后者在多线程环境下需使用同步机制保证线程安全。单例模式有助于提高代码的可维护性和扩展性,应根据需求选择合适实现方式。
77 1