美文网首页
单例设计模式最优解(性能、并发、反射、序列化)

单例设计模式最优解(性能、并发、反射、序列化)

作者: 古都旧城 | 来源:发表于2018-12-22 14:14 被阅读8次

所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。

作用

  • 避免对象的多次创建,节约资源

单例设计模式的关键点

  • 私有构造函数
  • 提供方法返回单例对象
  • 保证多线程情况下单例依然唯一
  • 确保反序列化的时候也不会重新构建对象

上面的关键点也非必须都要坚持的点,还是需要具体场景具体分析吧。

1、最简单的实现(恶汉)

public class Singleton{
    private static final Singleton singleton = new Singleton();
    
    public static Singleton getInstance(){
        return singleton;
    }
    
    private Singleton(){
    
    }
}

2、性能优化--lazy loaded(懒汉)

  • 上面的代码虽然简单,但是有一个问题----无论这个类是否被使用,都会创建一个instance对象,并且这个类还不一定会被使用,那么这个创建过程就是无用的,怎么办呢?

为了解决这个问题,我们想到的新的解决方案:

public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
  • 代码的变化有俩处----首先,把 instance 设置为 null ,知道第一次使用的时候判是否为 null 来创建对象。因为创建对象不在声明处,所以那个 final 的修饰必须去掉。

  • 我们来想象一下这个过程。要使用 SingletonClass ,调用 getInstance()方法,第一次的时候发现instance时null,然后就创建一个对象,返回出去;第二次再使用的时候,因为这个instance事static的,共享一个对象变量的,所以instance的值已经不是null了,因此不会再创建对象,直接将其返回。

  • 这个过程就称为lazy loaded ,也就是迟加载-----直到使用的时候才经行加载。

3、同步(应对多线程问题)

  • 上面的代码很清楚,也很简单。单线程下,这段代码没什么问题,可是如果是多线程呢,麻烦就来了,我们来分析一下:

  • 线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!

  • 解决的办法也很简单,那就是加锁:

public class SingletonClass{
    private static SingletonClass instance = null;
    public synchronized static SingletonClass getInstance(){
        if(instance == null){
            instance = new SingletonClass();
        }
        return instance;
    }
    private SingletonClass(){
    
    } 
}
  • 只要getInstance()加上同步锁,,一个线程必须等待另外一个线程创建完后才能使用这个方法,这就保证了单利的唯一性。

4、又是性能

  • 上面的代码又是很清楚也很简单的,然而,往往简单的东西不够理想。这段代码毫无疑问存在性能的问题----synchronized修饰的同步块可是要比一般的代码慢上几倍的!如果存在很多次的getInstance()调用,那性能问题就不得不考虑了?!!!

  • 让我们来分析一下,究竟是整个方法都必须加锁,还是紧紧其中某一句加锁就足够了?我们为什么要加锁呢?分析一下lazy loaded的那种情形的原因,原因就是检测null的操作和创建对象的操作分离了,导致出现只有加同步锁才能单利的唯一性。

  • 如果这俩个操作能够原子的进行,那么单利就已经保证了。于是,我们开始修改代码:

public class SingletonClass{
    private static SingletonClass instance = null;
    public static SingletonClass getInstance(){
        synchronized(SingletonClass.class){
            if(instance == null){
            instance = new SingletonClass();
            }
        }
        return instance;
    }
    private SingletonClass(){
    
    } 
}
  • 首先去掉 getInstance() 的操作,然后把同步锁加载到if语句上。但是,这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要经行同步,性能的问题还是存在。如果............我们事先判断一下是不是为null在去同步呢?
public class SingletonClass{
    private static SingletonClass instance = null;
    public static SingletonClass getInstance(){
        if(instance == null){
            synchronized(SingletonClass.class){
                if(instance == null){
                instance = new SingletonClass();
                }
            }
        }    
        return instance;
    }
    private SingletonClass(){
    
    } 
}
  • 还有问题吗?首先判断instance是不是为null,如果为null在去进行同步,如果不为null,则直接返回instance对象。

  • 这就是double---checked----locking 设计实现单利模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。

5、从源头检查

  • 下面我们开始说编译原理。所谓编译,就是把源代码”翻译“成目标代码----大多是是指机器代码----的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。

  • 要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。

  • 下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

  • 下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!

举例:
创建一个对象 new Object() 看似一句话,但是实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
1、跟实例分配内存;
2、调用类的构造函数,初始化成员字段;
3、将instance 对象指向分配的内存空间(此时instance就不是null了);
1、2、3的顺序可能不一致,所以可能会出错。

6. 解决方案

了这么多,难道单例没有办法在Java中实现吗?其实不然!

  • 在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义,用volatile修饰instance之后,能够保证instance对象每次都是从主内存读取的。

说明:Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。这同样也导致了volatile也会有一些性能问题,不过影响还是非常小的。

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if(instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 

}

这种方法的缺点

  • 这只是JDK1.5之后的Java的解决方案,之前版本volatile还没有被赋予这个语义功能,所以这个方案不适用于之前的老版本。
  • 某些情况下还是会出现失效问题,在《java并发编程实践》一书中谈到了这个问题,是不赞成这种用法的。

其实,还有另外的一种解决方案,并不会受到Java版本的影响:

静态内部类方案(Effiective Java推荐的方式)

public class SingletonClass { 
    
  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 
    
}
  • 在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。

  • 由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。

到这里一般就够用了,只不过如果考虑一些奇葩情况当然依然是有一些问题存在的。
1、可以反序列化创建对象。
2、可以反射调用私有构造函数创建对象。

枚举单例设计模式(最安全最简单的方式)

public enum  SingleInstance {
    INSTANCE;
    public void test(){
        System.out.println(INSTANCE.name());
    }
}

调用

public class MainTest {
    public static void main(String[] args) {
        SingleInstance.INSTANCE.test();
    }

}
  • 写法简单是枚举单例最大的优点,枚举在java中其实编译后生成的也是一个java类,枚举不仅能够有字段,也可以有自己的方法,最重要的是枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例,在上面几种单例创建模式实现中,在反序列的情况下他们会出现重新创建对象

  • 我们知道序列化可以将一个单例对象写到磁盘,然后在读取回来,从而有效的获取了一个实例,即使构造函数是私有的,反序列化操作依然可以通过特殊途径去创建一个类的新的实例。

  • 反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象反序列化。

  • 上面几种示例如果要杜绝单例对象在反序列被重新创建的情况,就必须加入readResolve()函数,就是在这个方法里面直接将单例对象返回(复写此方法,直接返回单例对象即可),而不是新创建一个对象。(对于枚举则不会存在这个问题,枚举反序列化也不会创建新的实例【为什么:参见:枚举为什么是最好的单例,以及序列化反序列化等操作】)

  • 另外枚举也可以避免反射创建对象(具体参见上方文章)

更多的还有容器类的单例设计管理模式,适合管理很多单例类。

容器的单例设计模式:

/**
 * 单例管理类
 */
public class SingletonManger {
    private static Map<String,Object> objectMap = new HashMap<>();

    /**
     * 私有化管理类,防止创建多个
     */
    private SingletonManger(){
        
    }

    /**
     * 插入单例类
     * @param key
     * @param instance
     */
    public static void registerService(String key,Object instance){
        if(!objectMap.containsKey(key)){
           objectMap.put(key,instance);
        }
    }

    /**
     * 获取单例类
     * @param key
     * @return
     */
    public static Object getSigleInstance(String key){
        return objectMap.get(key);
    }
}

相关文章

  • 单例设计模式最优解(性能、并发、反射、序列化)

    所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。 作用 避免对象的多次创建,节约资源 单例设计模...

  • 设计模式-单例模式【实现、序列化、反射】

    设计模式-单例模式【实现、序列化、反射】 [toc] 1. 实现 单例模式的实现有很多种,分类方式也不一而足,比如...

  • 单例模式Java篇

    单例设计模式- 饿汉式 单例设计模式 - 懒汉式 单例设计模式 - 懒汉式 - 多线程并发 单例设计模式 - 懒汉...

  • 单例模式

    由于性能问题,优化,采用双重检查锁 懒汉式内部类单例 枚举类从JDK层面就保证不能被序列化和反射所破坏单例模式 枚...

  • Java 单例模式有哪些实现方式

    实现方式线程安全并发性能好可以懒加载反射/序列化/反序列化安全饿汉模式YYNN懒汉模式(不加锁)NYYN懒汉模式(...

  • Android读文笔记

    单例总结 - 腾讯Bugly Enum枚举才是最好的单例实现方式,解决了懒加载、反序列化、反射、克隆、性能等问题。...

  • java程序的性能优化(二)

    善用设计模式 单例模式:各类写法,反序列化破坏单例 代理模式:jdk接口代理,asm代理,cglib,javass...

  • 并行模式与算法

    java高并发程序设计 - 网易云课堂 一、单例模式 高并发情况下的单例模式 public class Stati...

  • 避免单例模式被反序列化和反射创建出新的实例

    双重检测锁的单例模式 使用反射获取实例测试 使用反序列化获取实例测试

  • 枚举实现单例模式

    枚举实现单例模式 前面我们说到序列化和反序列化以及反射对单例都是有破坏的,下面我们介绍一种更加优雅的实现,也是ef...

网友评论

      本文标题:单例设计模式最优解(性能、并发、反射、序列化)

      本文链接:https://www.haomeiwen.com/subject/ppeikqtx.html