首页 > 其他分享 >解锁设计模式的神秘面纱:编写无懈可击的代码之单例设计模式

解锁设计模式的神秘面纱:编写无懈可击的代码之单例设计模式

时间:2023-11-09 23:33:35浏览次数:44  
标签:test2 解锁 private instance 实例 线程 单例 设计模式 加载


前言

单例设计模式是23种设计模式中最常用的设计模式之一,无论是三方类库还是日常开发几乎都有单例设

计模式的影子。单例设计模式提供了一种在多线程情况下保证实例唯一性的解决方案。单例设计模式虽然简单,但是实现方案却非常多,大体上有以下7种最常见的方式。

饿汉模式

所谓饿汉式,就是不管你用不用这个对象,都先把这个对象进行创建出来,这样子在使用的时候就可以保证是单例。

特点

  • 线程安全性 在加载的时候已经被实例化,所以只有这一次,线程安全的
  • 懒加载 没有延迟加载,好长时间不使用,影响性能

示例:

// 没有延迟加载,好长时间不使用,影响性能
public class test1 {
  /**
   * 直接初始化对象
   * */
  private static final test1 INSTANCE = new test1();
  /**
   * 不允许外界进行new对象
   **/
  private test1() {
  }
  /**
   * 放行唯一方法 获取对象
   * @return
   */
  public static test1 getInstance() {
    return INSTANCE;
  }
}

总结: 这种方案实现起来最简单,当test1被加载后,就会立即创建instance,因此该方法可以保证百分百的单例,instance不可能被实例化两次。但是这种做instance可能被加载后很长一段时间才会被使用,就意味着instance开辟的内存占用时间更多。

注意:

如果一个类中成员属性比较少,且占用内存资源不多,那么就可以使用饿汉式。如果一个类中都是比较重的资源,这种方式就比较不妥

懒汉模式

所谓懒汉式就是在使用时再去创建,可以理解成懒加载。

示例:

public class test2 {
  private static test2 instance;
  private test2() {
    System.out.println("类被实例化了");
  }
  public static test2 getInstance() {
    if (instance == null) {
      instance = new test2();
    }
    return instance;
  }
}

总结: 当instance为null时,getInstance会首先去new一个实例,那之后再将实例返回。

注意: 但是这种实现方式会存在线程安全问题,多个线程同时获取将会出现不同的对象实例,破坏了单例的原则。

懒汉模式+同步方法

为了解决懒汉式线程安全问题,我们可以加上同步方法

特点

  • 直接在方法上进行加锁
  • 锁的力度太大. 性能不是太好
  • synchronized 退化到了串行执行

示例:

public class test2 {
  private static test2 instance;
  private test2() {
    System.out.println("类被实例化了");
  }
  public static synchronized test2 getInstance() {
    if (instance == null) {
      instance = new test2();
    }
    return instance;
  }
}

总结: 这种做法就保证了懒加载又能够百分百保证instance是单例的,但是synchronized关键字天生的排他性导致该方法性能过低。

双重检查锁

Double-Check-Locking是一种比较聪明的做法,我们其实只需要在instance为null时,保证线程的同步性,让只有一个线程去创建对象即可,而其他线程依然是直接使用,而当instance已经有实例之后,我们并不需要线程同步操作,直接并行读即可,这里我们再给类里面加上两个属性.

特点

  • 保证了线程安全
  • 如果实例中存在多个成员属性. 由于在代码执行过程当中,会对代码进行重排,,重排后, 可能导致别一个线程获取对象时初始化属性不正确的情况
  • 加volatile
  • 创建对象步骤
  • memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
  • memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 //注意,此时对象还没有被初始化! ctorInstance(memory); //2:初始化对象
  • 重排问题

示例:

public class test2 {
  private static test2 instance;
  private Object o1;
  private Object o2;
  private test2() {
    o1=new Object();
    o2=new Object();
    System.out.println("类被实例化了");
        }
  public static test2 getInstance() {
    // 为null时,进入同步代码块,同时避免了每次都需要进入同步代码块
    if (instance == null) {
      // 只有一个线程能够获取到锁
      synchronized (test2.class) {
        // 如果为Null在创建
        if (instance == null) {
          instance = new test2();
                    }
                }
          }
    return instance;
      }
}

总结: 当两个线程发现 instance == null 时,只有一个线程有资格进入同步代码块,完成对instance的初始化,随后的线程再次进入同步代码块之后,因为 instance == null 不成立,就不会再次创建,这是未加载情况下并行的场景,而instance加载完成后,再有线程进入getInstance方法后,就直接返回

instance,不会进入到同步代码块,从而提高性能。

注意: 这种做法看似完美和巧妙,既满足懒加载,又保证instance的唯一性,但是这种方式实际上是会出现空指针异常的。

解析空指针异常的问题:

在test2构造方法中,我们会初始化 o1 和 o2两个资源,还有Single自身,而这三者实际上并无前后关系的约束,那么极有可能JVM会对其进行重排序,导致先实例化test2,再实例化o1和o2,这样在使用test2时,可能会因为o1和o2没有实例化完毕,导致空指针异常。

解锁设计模式的神秘面纱:编写无懈可击的代码之单例设计模式_实例化

双重检查锁+volatile

解决上面的方法其实很简单,给instance加上一个volatile关键字即可,这样就防止了重排序导致的程序异常。

private volatile static test2 instance;

内部类(Holder)方式

holder方式借助了类加载的特点,我们直接看代码。

public class test3 {
  private test3() {
    System.out.println("类被实例化了");
  }
  /**
   * 使用内部类方式不会主动加载,只有主类被使用的时候才会进行加载
     * 第一次使用到的时候才去执行  只执行一次
   */
  private static class Holder {
    private static test3 instance = new test3();
  }
  /**
   * 提供外界进行调用
   * @return
   */
  public static test3 getInstance() {
    return Holder.instance;
  }
}

特点

  • 它结合了饿汉模式 安全性,也结合了懒汉模式懒加载。不会使用synchronized 所以性能也有所保证
  • 声明类的时候,成员变量中不声明实例变量,而放到内部静态类中
  • 不存在线程安全问题
  • 懒加载的
  • 反序列化问题 // 该方法在反序列化时会被调用 protected Object readResolve() throws ObjectStreamException { System.out.println("调用了readResolve方法!"); return Hoder.instance; }

总结: 我们发现,在test3中并没有instance,而是将其放到了静态内部类中,使用饿汉式进行加载。但是实际上这并不是饿汉式。因为静态内部类不会主动加载,只有主类被使用时才会加载,这也就保证了程序运行时并不会直接创建一个instance而浪费内存,当我们主动引用Holder时,才会创建instance实例,从而保证了懒加载。

枚举方式

枚举的方式实现单例模式是《Effective Java》作者力推的方式,枚举类型不允许被继承,同样是线程安全的并且只能被初始化一次。但是使用枚举类型不能懒加载,比如下面的代码,一旦使用到里面的静态方法,INSTANCE就会立即被实例化。

特点

  • 不存在线程安全
  • 没有懒加载

示例:

public enum test4 {
  INSTANCE;
  test4() {
    System.out.println("类被实例化了");
  }
  public static test4 getInstance() {
      return INSTANCE;
  }
}

源码

  • Runtime类
  • Mybatis ErrorContext
  • 类加载器

本期结束咱们下次再见

标签:test2,解锁,private,instance,实例,线程,单例,设计模式,加载
From: https://blog.51cto.com/u_14725510/8286313

相关文章

  • java的设计模式之抽象工厂模式
    抽象工厂模式(AvstractFactory)在某些情况下,需要创建一系列相关或相互依赖的对象,这些对象属于一组相关的产品族。同时,系统需要保证这些产品族之间的一致性。如果直接在代码中创建这些对象,会使得代码与具体产品的细节紧密耦合,不利于后续的扩展和维护。抽象工厂模式提供了一个接口,用于......
  • 每日总结之设计模式:组合模式
    一、什么是组合模式组合模式也成为整体部分模式,是一种结构型设计模式。它将对象组合成树形的层次结构,用来表示“整体-部分”的关系。通过组合模式,我们可以使用相同的方式处理单个对象和多个对象组合。二、角色组成组件(Component):定义组合模式中所有对象共有的方法和属性。叶......
  • C++跨DLL内存所有权问题探幽(一)DLL提供的全局单例模式
    最近在开发的时候,特别是遇到关于跨DLL申请对象、指针、内存等问题的时候遇到了这么一个问题。问题跨DLL能不能调用到DLL中提供的单例?问题比较简单,就是我现在有一个进程A,有DLLBDLLC,这两个DLL都依赖DLLD的单例,这个时候如果A调用了DLLB和DLLC,那么B和C能否正确引用到这个指......
  • 常用设计模式大全
    1.设计模式设计模式(Designpattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件......
  • "单"身贵族爱好者必备:新手也能看懂的单例模式入门指南
    大家好,欢迎来到程序视点!前言在前面三期里,我们详细讲解了工厂模式下的三种分类:简单工厂模式(SimpleFactory)工厂方法模式(FactoryMethod)抽象工厂模式(AbstractFactory)其中,简单工厂模式不是我们23中设计模式之气,其他两个设计模式都属于创建型模式的设计方式。今天,我们要接着讲......
  • 解锁表单新操作!JVS低代码表单自定义按钮功能全解析
    在普通的表单设计中,虽然自带的【提交】、【重置】、【取消】按钮可以满足基本操作需求,但在面对更多复杂的业务场景时,这些按钮的显示控制就显得有些力不从心。为了更好地满足用户在表单操作过程中的个性化需求,JVS低代码推出了表单自定义按钮功能。这项功能不仅可以更灵活地操作表单......
  • 设计模式-原型模式
    原型模式是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创......
  • Java的设计模式之工厂模式
    在软件设计中,我们经常遇到需要创建不同类型对象的情况,但是如果直接在代码中实例化对象,会使代码紧密耦合在一起,难以维护和拓展,此外,如果对象的创建方式需要变化,那么就需要再整个代码中进行大量的修改。工厂模式旨在解决这个问题。工厂模式提供了一个创建对象的接口,但是将具体的对象创......
  • 前端常用设计模式
    什么是设计模式?​设计模式(Designpattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,设计模式并不是一种固定的公式,而是一种思想,是一种解决问题的思路;使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可维护性。设计模式不区分编......
  • js给元素添加id,动态方式,举个简单例子
    在JavaScript中,如果你想动态地给一个已经存在的元素添加一个id属性,你可以通过获取那个元素的引用,然后设置它的id属性来实现。这里是一个简单的例子,它展示了如何给一个div元素动态添加一个id://假设我们有一个这样的HTML元素://<div>这是一个div</div> //首先,获取那个要添加i......