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

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

作用

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

单例设计模式的关键点

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

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

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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容