Java设计模式——单例设计模式

数字鎏金渡
• 阅读 1273

单例设计模式

一般某些的类只需要一个就够了,重复创建他们将花费大量资源时,可以使用单例模式。比如某些工厂类、工具类。

饿汉式

静态常量
 /**
  * @author objcfeng
  * @description 饿汉式——静态常量
  * @Step: 
  * 1.私有化构造器
  * 2.创建私有类实例
  * 3.公有方法返回类实例
  * @date 2020/10/13
  */
 public class HungryMan01 {
  private static final HungryMan01 instance=new HungryMan01();
  private HungryMan01() {
  }
  public static HungryMan01 getInstance(){
  return instance;
  }
 }

步骤:

  1. 私有化构造器阻止外部代码通过构造器构建类实例;
  2. 创建私有类实例的成员变量声明为静态常量
  3. 通过一个公有的静态方法getInstance提供类实例。

注意:提供类实例的方法一定是静态的,不然无法访问到这个方法。

静态代码块
 /**
  * @author objcfeng
  * @description 饿汉式——静态代码块
  * @date 2020/10/13
  */
 public class HungryMan02 {
  private static final HungryMan02 instance;
  private HungryMan02() {
  }
  static {
  instance=new HungryMan02();
  }
  public static HungryMan02 getInstance(){
  return instance;
  }
 }

步骤差不多,只是把创建私有类实例的过程放在静态代码块中执行罢了。

饿汉式都是线程安全的。

懒汉式

非线程安全的懒汉式
 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 @NotThreadSafe
 public class LazyMan {
  private static LazyMan instance=null;
 ​
  private LazyMan() {
  }
  public LazyMan static getInstance(){
  if (instance==null){
  instance=new LazyMan();
  }
  return instance;
  }
 }

懒汉式解决了饿汉式类加载时就创建了实例而不管实例是否被用到的问题。

但是这里的懒汉式是存在线程安全问题的,在并发环境下,当线程A执行到if (instance==null)结果为true,并进入if的执行代码块中,但在执行实例化操作前,CPU分配给了了线程B,线程B开始执行,判断if (instance==null)为true,并执行实例化操作生成类实例B。然后CPU重新分配给线程A,A已经通过判断了,所以又执行了一次实例化操作生成类实例A。这时便有两个类实例了。

代码验证:

 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 ​
 @NotThreadSafe
 public class LazyMan {
  private static LazyMan instance=null;
 ​
  private LazyMan() {
  }
  public static LazyMan getInstance() throws InterruptedException {
  if (instance==null){
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan();
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  LazyMan lazyMan1=LazyMan.getInstance();
  System.out.println(lazyMan1);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  LazyMan lazyMan2=LazyMan.getInstance();
  System.out.println(lazyMan2);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }
 ​

输出

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.LazyMan@43d83830
 创建型.懒汉式.LazyMan@6218fb2e

由之前的分析可以看出,懒汉式不是线程安全的关键有两点,一是在线程A执行完判断未执行实例化时就有线程B进入方法;二是当线程A重新获得CPU资源后没有再次判断if内的条件还是否为true;解决二者之一,就能解决懒汉式非线程安全的问题了。

首先来尝试解决二号问题,首先再次判断在if内再使用一次if肯定是不行的,原因还是会在通过判断的时候可能发生CPU资源的切换。我记得在解决虚假唤醒的时候曾将 if 改为while来再次执行判断,可以一试:

 @NotThreadSafe
 public class LazyMan02 {
  private static LazyMan02 instance=null;
 ​
  private LazyMan02() {
  }
  public static LazyMan02 getInstance() throws InterruptedException {
  while (instance==null){
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if(Thread.currentThread().getName().equals("A")){
  Thread.yield();//让线程变为就绪态
  }
  System.out.println(Thread.currentThread().getName()+"执行实例化");
  instance=new LazyMan02();
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  LazyMan02 lazyMan1= LazyMan02.getInstance();
  System.out.println(lazyMan1);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  LazyMan02 lazyMan2= LazyMan02.getInstance();
  System.out.println(lazyMan2);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }

输出

 A进入方法getInstance()...
 B进入方法getInstance()...
 B执行实例化
 A执行实例化
 创建型.懒汉式.LazyMan02@4cc3632
 创建型.懒汉式.LazyMan02@7245297c

发现不行,看来使用while再次进行判断只有在使用等待、唤醒(wait、notify)时才能起效果。

第一个问题解决起来就简单了,添加同步,在线程A没出来前限制其他线程进入就完事了。见下章。

线程安全的懒汉式
 @ThreadSafe
 public class LazyMan03 {
  private static LazyMan03 instance=null;
 ​
  private LazyMan03() {
  }
  public static synchronized LazyMan03 getInstance() throws InterruptedException {
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if (instance==null){
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan03();
  }
  return instance;
  }
 }

如上加同步(synchronized)就完了。

输出:

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.非线程安全.LazyMan03@4cc3632
 创建型.懒汉式.非线程安全.LazyMan03@4cc3632

可见两个类实例是一样的。

虽然解决起来简单但是缺点也很大,在并发大的时候,每个线程都要等进入getInstance()内的方法执行完后才有可能进入方法开始获取实例。性能很低。

双重检查
 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 ​
 @ThreadSafe
 public class LazyMan04 {
  private static volatile LazyMan04 instance=null;
 ​
  private LazyMan04() {
  }
  public static LazyMan04 getInstance() throws InterruptedException {
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if (instance==null){
  synchronized (LazyMan04.class){
  if (instance==null){
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan04();
  }
  }
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  DoubleCheck doubleCheck= DoubleCheck.getInstance();
  System.out.println(doubleCheck);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  DoubleCheck doubleCheck= DoubleCheck.getInstance();
  System.out.println(doubleCheck);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }

输出:

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.线程安全.LazyMan04@6218fb2e
 创建型.懒汉式.线程安全.LazyMan04@6218fb2e

双重检查,顾名思义就是使用了两次if判断,当类实例还未创建出来时,线程通过第一个判断进入同步代码块,进行第二次判断和创建类实例,保证了线程在判断后和执行创建前不会有其他线程进入代码块,保证了线程安全性。

另外,在类实例创建出来后,所有线程都不会再进入同步代码块,保证了效率。

为什么要使用volatile?

因为new不是原子操作。

 public class Test {
  public static void main(String[] args) {
  new Object();
  }
 }

使用javac命令编译.java文件为.class文件

javac -encoding UTF-8 Test.java

再使用javap命令反编译.class文件

 javap -c Test

 D:WorkSpaceJavaJava设计模式MDsrc创建型双重检查>javac -encoding UTF-8 Test.java
 ​
 D:WorkSpaceJavaJava设计模式MDsrc创建型双重检查>javap -c Test.class
 Compiled from "Test.java"
 public class 创建型.双重检查.Test {
  public 创建型.双重检查.Test();
  Code:
  0: aload_0
  1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  4: return
 ​
  public static void main(java.lang.String[]);
  Code:
  //创建对象实例分配内存
  0: new           #2                  // class java/lang/Object
  //复制栈顶地址,并将其压入栈顶
  3: dup
  //调用构造器方法,初始化对象
  4: invokespecial #1                  // Method java/lang/Object."<init>":()V
  7: pop
  8: return
 }
线程1线程2
t1分配内存
t2变量赋值
t3 判断对象是否为null
t4 由于对象不为null,访问该对象
t5初始化对象

如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。(参考自https://www.cnblogs.com/zhuifeng523/p/11360012.html

volatile 作用

正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。

  1. 保证可见性。使用 volatile定义的变量,将会保证对所有线程的可见性。
  2. 禁止指令重排序优化。

由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

静态内部类

 /**
  * @author objcfeng
  * @description 单例—静态内部类
  * @date 2020/10/16
  */
 public class StaticInnerClass {
  private StaticInnerClass() {
  }
 ​
  private static class Singleton{
  private static final StaticInnerClass INSTANCE=new StaticInnerClass();
  }
  public static StaticInnerClass getInstance(){
  return Singleton.INSTANCE;
  }
 }

实现步骤:

  1. 私有化构造器;
  2. 私有静态内部类创建外部类的静态常量实例;
  3. 外部类公有方法返回这个实例。

实现原理:外部类加载时不会立即加载内部类,内部类不加载就不会初始化INSTANCE,实现懒加载。只有当第一次调用getInstance()方法时,内部类才会加载。

缺点:由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。一般单例模式都在静态内部类与双重检查(DCL Double Check Lock)之间抉择。

枚举

 /**
  * @author objcfeng
  * @description 枚举实现单例
  * @date 2020/10/16
  */
 public class EnumSingleton {
  private EnumSingleton() {
  }
  enum E{
  INSTANCE;
  private EnumSingleton singleton;
  E() {
  singleton=new EnumSingleton();
  }
  }
  public static EnumSingleton getInstance(){
  return E.INSTANCE.singleton();
  }
 }

实现步骤:

  1. 私有化构造函数
  2. 建立只有一个枚举实例的枚举类,枚举类有外部类的实例变量声明,枚举的构造函数中初始化外部类实例。
  3. 外部类公有方法返回这个实例。

优点:线程安全、防止反序列化创建新的对象。

点赞
收藏
评论区
推荐文章
3A网络 3A网络
2年前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非
Wesley13 Wesley13
3年前
java设计模式1
1:单例模式简介  单例模式是一种常用的软件设计模式,它确保某个类只有一个实例,而且自行实例化并向整个系统提供唯一的实例。总而言之就是在系统中只会存在一个对象,其中的数据是共享的  特点:    单例类只能有一个实例,所以一般会用static进行修释。    单例类必须自己创建自己的唯一实例。也就是在类中要new一个自己。    单例类必
Wesley13 Wesley13
3年前
java 23种设计模式(五、单例模式)
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式的结构  单例模式的特点:单例类只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。  饿汉式单例类publicclassEagerSingleton
Wesley13 Wesley13
3年前
java中饿汉与懒汉的故事(单例设计模式)
java中的单例设计模式关于设计模式,这其实是单独存在的东西,它不属于java,但是在java中使用较多,所以今天我就给大家介绍下单例设计模式中的饿汉和懒汉这俩朴素的打工人。首先我先说明下单例设计模式是啥(如果不想了解,可以直接划下去看饿汉和懒汉):类的单例设计模式就是采用一定的方法保证在整个软件系统中,对某个类只能存在一
Java单例模式7种写法,你是不是有用错过?
单例大家都很清楚的知道作用,也不多说,主要是在不同的情况下注意事项;单例模式多种写法1、饿汉式可用//饿汉式languageprivatefinalstaticSingletonINSTANCEnewSingleton();privateSingleton(){}publicstaticSingleton
红烧土豆泥 红烧土豆泥
4年前
创建型模式之单例设计模式
什么是单例设计模式?顾名思义,只有一个实例。单例模式它主要是确保一个类只有一个实例,并且可以提供一个全局的访问点。废话少说,直接上干货了单例模式之饿汉式所谓饿汉式,顾名思义,“它很饿”。所以说,它一旦被加载进来,就会直接实例化一个对象。例如:languageclassSingleton{privatestaticfin
Wesley13 Wesley13
3年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
3年前
C#单例
单例模式:步骤:1.定义静态私有对象2.构造函数私有化3.定义一个静态的,返回值为该类型的方法,一般以Getinstance/getInit为方法名称单例模式有懒汉和饿汉,最好使用饿汉1.饿汉式先实例化publicclassSingleton{privatestati
Wesley13 Wesley13
3年前
PHP单例模式(精讲)
首先我们要明确单例模式这个概念,那么什么是单例模式呢?单例模式顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类我们称之为单例类。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例
Wesley13 Wesley13
3年前
(面试常问)4种单例设计模式的总结(内含代码以及分析)
单例设计模式:  单例模式,是一种常见的软件设计模式.在它的核心结构中只包含了一个被称为单例的特殊类.通过单例模式可以保证系统中只有该类的一个实例对象.优点:  实例控制:单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问的是唯一的实例   灵活性:因为类控制了实例化过程,所以类可以很灵活的更改实
Wesley13 Wesley13
3年前
23种设计模式(1):单例模式
定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。类型:创建类模式类图:!23种设计模式(1):单例模式第1张|快课网(http://static.oschina.net/uploads/img/201407/05200605_0dij.gif"23种设计模式(1):单例模式