单例模式与多线程

news2024/10/2 14:36:42

文章目录

  • 一、 简介
  • 二、详细介绍
    • 1. 立即加载/饿汉模式
    • 2. 延迟加载/懒汉模式
    • 3. 使用静态内置类实现单例模式
    • 4. 序列化和反序列化的单例模式
    • 5. 使用static代码块实现单例模式
    • 6. 使用enum枚举数据类型实现单例模式

一、 简介

在标准的23个设计模式中,单例模式在应用中是比较常用的。单多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成了在使用多线程的单例子模式时会出现一些意外。这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。

二、详细介绍

1. 立即加载/饿汉模式

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化。立即加载从中文的语境来看,是“着急”“急迫”的含义。所以也被称为“饿汉模式”。如下面的代码:

public class Main {
    public static void main(String[] args) throws InterruptedException {
      MyThread t1=new MyThread();
      MyThread t2=new MyThread();
      MyThread t3=new MyThread();

      t1.start();
      t2.start();
      t3.start();

    }
}
class  Myobject{
    //立即加载(静态变量会在类初始化时进行初始化)
    private  static  Myobject myobject=new Myobject();
    private Myobject(){

    }
    public static Myobject getInstance(){
        return myobject;
    }
}

class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(Myobject.getInstance().hashCode());
    }
}

在这里插入图片描述

由打印结果可以知道,hashcode是同一个值,说明对象是同一个对象,也就实现了立即加载型单例模式,现在我们分析一下它存在的问题,它的缺点就是不能有其它实例变量,如果我们在getInstance中加入其它实例变量,并赋值,由于代码没有被同步,所以容易出现非线程安全问题,导致变量值被覆盖,解决方法我们后续再谈。

2. 延迟加载/懒汉模式

延迟加载的定义与原理 延迟加载是开发过程中灵活获取对象的一种求值策略,该策略在定义目标对象时并不会立即计算实际对象值,而是在该对象后续被实际调用时才去求值,所以也被称为懒汉模式。更改上面代码:

public class Main {
    public static void main(String[] args) throws InterruptedException {
      MyThread t1=new MyThread();
      MyThread t2=new MyThread();
      MyThread t3=new MyThread();

      t1.start();
      t2.start();
      t3.start();

    }
}
class  Myobject{
    //立即加载(静态变量会在类初始化时进行初始化)
    private  static  Myobject myobject;
    private Myobject(){

    }
    public static Myobject getInstance(){
        if(myobject !=null)
        {}else
        {
          myobject=new Myobject();
        }
        return myobject;
    }
}

class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(Myobject.getInstance().hashCode());
    }
}

在这里插入图片描述

由结果可以知道,这种对象加载模式会在多线程中创建多个对象的实例的情况,与单例模式的初衷是背离的。那么如果解决该方案这种错误的单例模式的问题?下面给出了几种解决方案:

  • 声明synchronized关键字

即然多个线程可以同时进入getInstance()方法,我们只需要对getInstance()方法声明synchrozied关键字即可

synchronized  public static Myobject getInstance(){
        try{
            if(myobject !=null){
            }else{
            Thread.sleep(3000);
            myobject=new Myobject();
            }
        }catch(InterruptedException e)
        {
            e.printStackTrace();
        }
        return myobject;
    }

在这里插入图片描述
此方法在加入同步Synchronized关键字后得到相同实例的对象,但整个方法被上锁后,运行效率会非常低,下一个线程想要取得对象,必须等上一个线程释放完锁后,才可以执行。那换成同步代码块可以快速解决吗(对运行的代码块加上锁)?答案是一样的,不能快速解决,原因是同步代码块依然是获得的是对象锁,所以效率依然很低下。下面尝试使用针对某些重要代码进行单独的同步来测试一下:

 public static Myobject getInstance(){
       try{
           if(myobject!=null){

           }else{
               //模拟在创建对象之前做的一些准备工作
               Thread.sleep(3000);
               //使用synchronized(MyObject.class)
               //但还是有非线程安全问题
               //多次创建MyObject类的对象,结果并不是单例
               synchronized (Myobject.class){
                   myobject=new Myobject();
               }
           }
           
       }catch(InterruptedException e)
       {
           e.printStackTrace();
       }
       return myobject;
    }
}

在这里插入图片描述
此方法使同步synchronized语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率的确得到了提升,但遇到多线程情况还是无法得到一个实例对象。到底如何解决懒汉模式下的多线程情况呢,下面介绍使用DCL双检查锁机制:

class  Myobject{
    //立即加载(静态变量会在类初始化时进行初始化)
    private volatile static  Myobject myobject;
    private Myobject(){

    }
    public static Myobject getInstance(){
       try{
           if(myobject!=null){

           }else{
               //模拟在创建对象之前做的一些准备工作
               Thread.sleep(3000);
               synchronized (Myobject.class){
                   myobject=new Myobject();
               }
           }
           
       }catch(InterruptedException e)
       {
           e.printStackTrace();
       }
       return myobject;
    }
}

使用volatile修改变量myObject,使该变量在多个线程间可见,另外禁止myObject=new MyObject()代码重排序。myObject=new MyObject()代码包含3个步骤。

  • memory=allocate() :分配对象的内存空间
  • ctorInstance():初始化对象
  • myObject():设置instance指向刚分配的内存地址

JIT编译器有可能将这三个步骤重排序成。

  • memory=allocate():分配对象的内存空间
  • myObject=memory:设置instance指向刚分配的内存地址
  • ctorInstance(memory):初始化对象

这时,构造方法虽然还没有执行,但myObject对象已经具有内存地址,即值不是null。当访问myObject对象中的值是,是当前声明数据类型的默认值。运行结果可以发现,使用DCL双检查锁成功解决了懒汉模式下的多线程问题,DCL也是大多数线程结合单例模式使用的解决方案。

3. 使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非现场安全问题。我们还可以使用其他方法达到同样的效果。

public class  Myobject{
    //内部类方法
    private static class MyObjectHandler{
        private static Myobject myobject=new Myobject();
    }
    private Myobject(){

    }

    public static Myobject getInstance(){
        return MyObjectHandler.myobject;
    }
}

在这里插入图片描述
原理是,静态内部类定义阶段就已经初始化了,在静态内部类定义我的Myobject时,由于它被static修饰,所以在初始化MyObjectHandler类时,它也被初始化了,且只有一个副本,所以访问时也会只存在一个实例,这样就实现了单例模式。

4. 序列化和反序列化的单例模式

如果将单例对象进行序列化,使用默认的反序列化行为取出的对象是多例的。

//定义一个UserInfo对象
public class UserInfo {

}
//定义Myobject类
public class  Myobject implements Serializable{
    private static final long SerializableUID=888L;
    public static UserInfo userInfo=new UserInfo();
    private static Myobject myobject=new Myobject();
    private Myobject(){
        
    }
    publicstatic Myobject getInstance(){
        return myobject;
    }
}
//main函数
public class Main {
    public static void main(String[] args) throws InterruptedException {
       try{
          Myobject myobject=Myobject.getInstance();
          System.out.println("序列化-myobject="+myobject.hashCode()+"userinfo:"+myobject.userInfo.hashCode());
          FileOutputStream fosref=new FileOutputStream(new File("myObject-File.txt"));
          ObjectOutputStream oosRef=new ObjectOutputStream(fosref);
          oosRef.writeObject(myobject);
          oosRef.close();
          fosref.close();

       }catch(FileNotFoundException e)
       {
           e.printStackTrace();
       }catch(IOException e)
       {
           e.printStackTrace();
       }
       try{
           FileInputStream fisRef=new  FileInputStream(new File("myObject-File.txt"));
           ObjectInputStream iosRef=new ObjectInputStream(fisRef);
           Myobject myobject=(Myobject)iosRef.readObject();
           iosRef.close();
           fisRef.close();
           System.out.println("序列化-myObject="+myobject.hashCode()+"userinfo:"+myobject.userInfo.hashCode());

       }catch(FileNotFoundException e)
       {
           e.printStackTrace();
       }catch(IOException e)
       {
           e.printStackTrace();
       }catch(ClassNotFoundException e)
       {
           e.printStackTrace();
       }
    }
}

在这里插入图片描述
从打印结果可以看出,在反序列化时创建了新的MyObject对象,但UserInfo对象得到复用,因为hashcode是同一个。为了实现Myobject对象在内存中一直呈现单例效果,我们可以在反序列化中使用下面方法,对原有的Myobject对象进行复用:

public class  Myobject implements Serializable{
    private static final long SerializableUID=888L;
    public static UserInfo userInfo=new UserInfo();
    private static Myobject myobject=new Myobject();
    private Myobject(){
        
    }
    public static Myobject getInstance(){
        return myobject;
    }
    protected Object readResolve()throws ObjectStreamException{
        return Myobject.myobject;
    }
}

在这里插入图片描述
上面加入的方法的作用是在反序列化时不创建新的Myobject对象,而是复用JVM内存中原有的Myobject单例对象,即UserInfo对象被复用,这就实现了对Myobject序列化和反序列化保持单例性。那么为什么序列化和反序列化会破坏单例模式呢?序列化和反序列化可以破坏单例模式,因为在反序列化过程中,JVM会创建一个新的对象,而不是使用单例类中的静态实例。当一个单例类的实例被序列化并再次反序列化时,会导致创建新的对象,从而破坏了单例的特性。这种情况发生的原因是,当对象被序列化时,它的状态信息被保存到字节流中,而在反序列化时,JVM会根据字节流重新创建对象。由于单例类的构造函数是私有的,无法直接调用,因此JVM无法使用单例类的构造函数来创建实例。相反,它会调用默认的反序列化机制来创建一个新的对象,并忽略掉单例类中的任何特殊实现。为了解决这个问题,需要在单例类中添加一个特殊的方法readResolve()。该方法在反序列化时被调用,并返回原始的单例实例。通过在readResolve()方法中返回单例实例,可以确保在反序列化时仍然使用同一个对象,从而保持了单例的特性。总之,为了在序列化和反序列化过程中保持单例的特性,必须在单例类中实现readResolve()方法,并确保返回单例实例。这样可以防止通过反序列化操作破坏单例模式。

5. 使用static代码块实现单例模式

静态代码块中的代码在类加载的时候就会执行,所以我们可以应用静态代码块这个特性实现单例模式。

public class  Myobject{
   private static Myobject instance=null;
   private Myobject(){

   }
   static{
       instance =new Myobject();
   }
   public static Myobject getInstance(){
       return instance;
   }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1=new MyThread();
      MyThread t2=new MyThread();
      MyThread t3=new MyThread();

      t1.start();
      t2.start();
      t3.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(Myobject.getInstance().hashCode());
    }
}

在这里插入图片描述

6. 使用enum枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似。枚举(Enum)数据类型在Java中可以用于实现单例模式,其原理是利用枚举类型的特性保证了单例的实现。在使用枚举实现单例模式时,只需要定义一个包含单个枚举常量的枚举类。这个枚举常量就是单例的实例,而枚举类本身就是单例类。枚举类中的枚举常量在加载枚举类时就会被实例化,并且枚举常量的实例是线程安全的。这意味着无论是多线程环境下还是通过序列化和反序列化,枚举单例都能保持单例的特性。

public class  Myobject{
  
   public Myobject(){

   }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1=new MyThread();
      MyThread t2=new MyThread();
      MyThread t3=new MyThread();

      t1.start();
      t2.start();
      t3.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(Singleton.INSTANCE.getInstance().hashCode());
    }
}
 enum Singleton  {
    INSTANCE;
    
    private  Myobject myobject;
    
    private Singleton(){
        myobject=new Myobject(); 
    }

    public Myobject getInstance(){
        return  myobject;
    }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/544584.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

传染病学模型 | SIR 、SEIR传染病学模型

文章目录 SIR传染病学模型SEIR传染病学模型参考资料SIR传染病学模型 SIR模型是一种流行病学模型,用于描述传染病在人群中的传播过程。SIR模型将人群分为三个类别:易感者(Susceptible)、感染者(Infectious)和康复者(Recovered)。三个类别之间的转移可以用以下三个微分方…

二、IOC容器(1)

一、IOC操作Bean管理 1.什么是Bean管理? Spring创建对象Spring注入属性Bean管理是2个操作 2.Bean管理操作有两种方式 基于xml配置文件方式实现基于注解方式实现 二、IOC操作Bean管理(基于xml方式) 1.基于xml方式创建对象 使用bean标签&…

new与delete用法详解与底层原理,operator new与operator delete函数,定位new与内存泄漏介绍等

tips 其实进程运行起来或者说程序运行起来都是去执行函数,任务就是不断的去执行函数。C的入口就是main函数,然后在这个函数当中可能碰到程序某些调用其他函数的语句就去调用其他函数。在全局的区域可以去创建变量,定义函数,但就是…

数据结构-栈,队列

栈,队列 1 知识框架2 栈2.1 顺序栈2.2 链式栈 3 队列3.1 顺序队列3.2 循环队列3.3 链式队列 4 数组4.1 二维数组4.2 特殊数组的压缩存储 1 知识框架 2 栈 定义:只允许在一端进行插入或删除得到线性表 栈的数学性质:n个不同元素进栈&#xff…

phpWord使用模板填充数据:包含表格及嵌套表格(多个表格/循环表格)

参考文档 模板处理 基础使用 安装过程省略,首先加载模板: $templateProcessor new TemplateProcessor(ROOT_PATH . uploads/template/自动生成模板.docx); 完整保存流程 首先,要进行测试,起码能够写一个完整的demo,以下是我测…

实验三---面向对象分析与设计——UML用例图与活动图

一、实验目的: 掌握面向对象分析中用例建模的基本思想,学会识别参与者和用例,掌握UML用例图的绘制方法,学会编写用例说明;了解活动图的作用和组成元素,掌握UML活动图的绘制方法,学会使用活动图来…

秒懂算法 | KMP算法(Java描述)

Knuth-Morris-Pratt 算法(简称 KMP)是由高德纳(Donald Ervin Knuth)和沃恩普拉特在1974年构思,同年詹姆斯H莫里斯也独立地设计出该算法,最终三人于1977年联合发表。该算法较Brute-Force算法有较大改进&…

门电路OD门

漏极开路输出的门电路(OD门) 为了满足输出电平的变换,输出大负载电流,以及实现“线与”功能,将CMOS门电路的输出级做成漏极开路的形式,称为漏极开路输出的门电路,简称OD(Open&#x…

【JVM】1. JVM与Java体系结构

文章目录 1.1. 前言🍉1.2. 参考书目🍉1.3. Java及JVM简介🍉1.4. Java发展的重大事件🍉1.5. 虚拟机与Java虚拟机🍉1.6. JVM的整体结构🍉1.7. Java代码执行流程🍉1.8. JVM的架构模型🍉…

4. QT中的鼠标键盘事件 --- 鼠标拖拽案例

1. 说明 在QT的控件或者窗口当中,如果对于当前鼠标或者键盘的功能需要自己定义,可以重写父类当中对应虚函数,主要包括以下几个: //键盘按键按下 virtual void keyPressEvent(QKeyEvent *event); //键盘按键抬起 virtual void ke…

为什么C++这么复杂还不被淘汰?

C是一门广泛使用的编程语言,主要用于系统和应用程序的开发。尽管C具有一些复杂的语法和概念,但它仍然是编程界的重量级选手,在编程语言排行榜中一直位居前列。为什么C这么复杂还不被淘汰呢? C有以下优势 1、C具有高性能 C是一门编…

unity进阶学习笔记:photonServer测试

photonServer是由photon发布的一个网络框架,其封装了UDP和TCP通信机制让用户可以直接调用API实现网络游戏通信 1 photonServer下载安装 进入Photon官网的SDK选项,选择下载Server。目前Server版本已经更新到v5,这里我为了和教程保持一致下载…

Unittest接口测试生成报告和日志方法

HTML报告 直接把HTMLTestRunner.py放入工程目录即可报告脚本封装 #HTNL格式报告now datetime.datetime.now().strftime(%Y-%m-%d_%H_%M_%S)htmlreport reportpath "/" now r"result.html"print("测试报告生成地址:%s"% htmlre…

Revit干货 | 系统族、内建族、可载入族一次性搞清楚!

对于使用人数较多的revit软件,其中的许多概念与我们常用的CAD完全不同,以至于让许多工程师觉得revit软件有点高深莫测,不可琢磨,从而有了抗拒心理。 Revit软件中的重要概念: “族”是revit软件中的很重要也很基本的概念…

程序的各种段以及堆栈相关问题

C中一般有三种变量: 局部变量全局变量静态变量 C中一般有五个内存段: 代码段 也叫文本段,包含frequently executed code通常是只读的(未了避免程序被错误改写)代码段是不包含程序变量(如局部变量、全局变…

【数据结构】带头双向链表,真正的六边形战士

文章目录 概要整体架构流程小结 概要 *数据结构中的链表在实际开发中应用非常广泛,但写一个链表并不是一件简单的事情。链表有八种结构,对于刚入门的新手来说,往往会先经历单链表的折磨。 而今天我要讲的带头双向链表非常适合新手学习&#…

C++6.类和对象(下)

1.友元函数,一般现在类的最上面。 2.函数的参数中,如果不改变,尽量加上const修饰。 3.对于自定义类型,使用运算符进行操作基本上都要用运算符重载,但是有些运算符重载会默认自动生成。 4.初始化列表,是成…

【wpf】列表类,用相对源时,如何绑定到子项

前言 在之前的一篇文章 :《【wpf】深度解析,Binding是如何寻找数据源的》https://blog.csdn.net/songhuangong123/article/details/126195727#:~:text%E3%80%90wpf%E3%80%91%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90%EF%BC%8CBinding%E6%98%AF%E5%A6%82%E4…

读书|林曦:她把自己的生活,过成了无用但丰盈的美学

时代在以加速度的方式变化,让人难以从容。而当我们陷于横向的比较系统,权衡着卷、躺、润时,也有人在探寻另一条纵向的路——向古人学习,以传统美学关照和滋养当下生活。      立夏之际,水墨画家林曦的新作《无用之…

33岁跳槽无路,濒临绝望之际受贵人指点,成功上岸阿里(测试岗)

写在前面 马上过34岁生日了,和大家聊聊最近的情况,半年前还在迷茫该学什么,怎样才能走出现在的困境,半年后已经成功上岸阿里,感谢在这期间帮助我的每一个人 开始 30多岁工作的时候总是有种力不从心的感觉&#xff0…