前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式二十四章经之单例设计模式

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

作者头像
我就是马云飞
发布2018-06-22 17:31:56
4210
发布2018-06-22 17:31:56
举报

概述

单例模式是应用最广的设计模式之一。也可能是很多初级工程师唯一会使用的设计模式。从字面意思,单例模式就是单例对象的类必须保证只有一个实例的存在,而且自行实例化并向整个系统提供这个实例。例如,创建一个对象需要消耗太多的资源,如要访问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。避免引发不必要的内存泄漏问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我就是马云飞 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 懒汉单例模式
    • 线程不安全
      • 线程安全
      • DCL单例模式
      • 饿汉单例模式
      • 静态内部类单例模式
      • 枚举单例模式
      • 使用容器实现单例模式
      • 总结
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档