设计模式二十四章经之单例设计模式

概述

单例模式是应用最广的设计模式之一。也可能是很多初级工程师唯一会使用的设计模式。从字面意思,单例模式就是单例对象的类必须保证只有一个实例的存在,而且自行实例化并向整个系统提供这个实例。例如,创建一个对象需要消耗太多的资源,如要访问IO和数据库等资源,这时需要考虑单例模式。

懒汉单例模式

线程不安全

当提到单例模式的时候,我们第一反应就是如下代码:

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

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

这边我们发现在方法中加上了synchronized关键字,也就是将此方法设置为一个同步方法,这就是为了防止上面说到多线程而使用的情况。我们仔细看代码会发现,只要调用getInstance方法都会进行同步,这样会消耗不必要的资源。

懒汉模式的优点就是单例只有在使用时才会被实例化,在一定程度上解决了成本。缺点是第一次加载时需要进行实例化,时间上会有差。最大的问题就是上面说的每次调用都要进行同步,造成不必要的同步开销。

DCL单例模式

dcl即double checked locking,也就是双重锁校验。优点是既能够在需要时才初始化实例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

这段代码看似很完美,但是,它是有问题的,主要是因为singleton = new Singleton()他不是一个原子操作,这句代码在JVM中大约做了如下三件事:

1、给 singleton 分配内存

2、调用 Singleton 的构造函数来初始化成员变量

3、将singleton对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是上面的第二点和第三点的顺序是无法确定的。最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 singleton已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleton,然后使用,然后顺理成章地报错。这就是DCL失效问题,而且难以跟踪问题。

我们只需要将 instance 变量声明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。

DCL的缺点:第一次加载反应慢,也由于java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生的概率很小。

饿汉单例模式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到懒加载的效果。

静态内部类单例模式

前面说到DCL虽然在一定程度上解决了资源消耗,多余的同步,线程的安全等问题。但是在某些情况会出现失效问题。所以在《Effective Java》中推荐采用静态内部类的形式实现单例。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举单例模式

什么? 枚举,没错,就是枚举!

public class Singleton {  
   private static final Singleton INSTANCE = new Singleton();  
   public enum EasySingleton{
    INSTANCE;
    }   
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。最重要的是默认枚举类型实例的创建就是线程安全。但是并不建议这么写,一个是可能是很少有人知晓。另一个是枚举会占用大量的内存。

使用容器实现单例模式

除了比较常用的单例模式外,还有一种单例模式也值得推荐,就是使用容器单例模式。

public class SingletonManager {
    private static Map<String,Object> map=new HashMap<String, Object>();
    private SingletonManager(){}
    public static void registerService(String key,Object instance){
        if (!map.containsKey(key)){
            map.put(key,instance);
        }
    }
    public static Object getService(String key){
        return map.get(key);
    } 
}

在程序的初始化,将多个单例类型注入到一个统一管理的类中,使用时通过key来获取对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行操作。降低了用户的使用成本,也对用户影藏了具体的实现,降低了耦合度。

总结

单例模式是开发中最常见的设计模式,一般来说用的比较多的就是饿汉模式和DCL模式。最后来说下,单例的优缺点。

优点:

  • 单例只有一个实例,减少了内存开销以及系统的性能开销。
  • 可以避免对资源的多重占用。
  • 可以在系统设置全局的访问点,优化和共享资源访问。

缺点:

  • 单例没有接口,扩展复杂,如需扩展只能修改代码。
  • 单例模式的生命周期是应该是和应用一起的,所以在创建时,我们需要传入application的context而不是activity的context。避免引发不必要的内存泄漏问题。

原文发布于微信公众号 - 我就是马云飞(coding_ma)

原文发表时间:2018-05-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java Edge

单例模式(Singleton Pattern)百媚生1 动机2 定义结构分析优点缺点适用场景应用总结实现方式1、懒汉式(非线程安全)2、懒汉式(线程安全)4、双重检验锁模式(double checke

34910
来自专栏java工会

Java基础第二阶段知识点,招初级java的面试官都在问这些

JDK:是java开发的工具箱,包含jre,还包含将java文件编译为class文件的javac工具类(编译器),除此之外还包括java原生的API;包含J2S...

691
来自专栏古时的风筝

从实例出发,了解单例模式和静态块

什么是单例模式呢,单例模式(Singleton)又叫单态模式,它出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例...

630
来自专栏闻道于事

Java 集合补充

集合和数组不一样,数组元素可以是基本类型的值,也可以是对象(的引用变量),集合里只能保存对象(的引用变量)。

665
来自专栏Java编程

Java异常的深入研究与分析

本文是异常内容的集大成者,力求全面,深入的异常知识研究与分析。本文由金丝燕网独家撰写,参考众多网上资源,经过内容辨别取舍,文字格式校验等步骤编辑而成,以飨读者。...

3180
来自专栏JMCui

读书笔记 之《Thinking in Java》(对象、集合、异常)

一、前言:     本来想看完书再整理下自己的笔记的,可是书才看了一半发现笔记有点多,有点乱,就先整理一份吧,顺便复习下前面的知识,之后的再补上。     真的...

3588
来自专栏陈树义

如何唯一确定一个 Java 类?

今天偶然想起之前和朋友讨论过的一个问题:如何唯一确定一个 Java 类?我相信大多数朋友遇到这个问题的回答都是:类的全路径呗。但事实上,唯一确定一个 Java ...

1033
来自专栏JAVA高级架构

Java面试2018常考题目汇总(一)

一、JAVA基础篇-概念 1.简述你所知道的Linux: Linux起源于1991年,1995年流行起来的免费操作系统,目前, Linux是主流的服务器操作系统...

31810
来自专栏java一日一条

Java到底是不是一种纯面向对象语言?

Java——是否确实的 “纯面向对象”?让我们深入到Java的世界,试图来证实它。

391
来自专栏Java面试笔试题

两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?

不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方...

762

扫码关注云+社区