单例模式

一、定义

单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点。

 

二、单例模式结构图

单例模式中设计的角色:

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。 

三要素:

  • 私有的构造方法。
  • 指向自己实例的私有静态引用。
  • 以自己实例为返回值的静态的共有方法。

 

三、单线程环境下的两种经典实现

立即加载:在类加载初始化的时候就主动创建实例;

延迟加载:等到真正使用的时候才去创建,不用时不去主动创建。

在单线程环境下,单例模式有两种经典的实现:一种是饿汉式单例(立即加载),一种是懒汉式单例(延迟加载)

 

饿汉式单例(线程安全)

// 饿汉式单例
public class Singleton1 {
 
    // 指向自己实例的私有静态引用,主动创建
    private static Singleton1 singleton1 = new Singleton1();
 
    // 私有的构造方法
    private Singleton1(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton1 getSingleton1(){
        return singleton1;
    }
}

类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

懒汉式单例(线程不安全)

// 懒汉式单例
public class Singleton2 {
 
    // 指向自己实例的私有静态引用
    private static Singleton2 singleton2;
 
    // 私有的构造方法
    private Singleton2(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton2 getSingleton2(){
        // 被动创建,在真正需要使用时才去创建
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

四、单例模式的优缺点

单例模式优点:

  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

单例模式缺点:

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

单例模式适用场景:

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

 

五、多线程环境下的单例模式

实现线程安全的懒汉式单例的几种正确姿势

1)、同步延迟加载 — synchronized方法

// 线程安全的懒汉式单例
public class Singleton2 {
 
    private static Singleton2 singleton2;
 
    private Singleton2(){}
 
    // 使用 synchronized 修饰,临界资源的同步互斥访问
    public static synchronized Singleton2 getSingleton2(){
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}
/* Output(完全一致): 
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
 *///:~

这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。

 2)、同步延迟加载 — synchronized块

// 线程安全的懒汉式单例
public class Singleton2 {
 
    private static Singleton2 singleton2;
 
    private Singleton2(){}
 
 
    public static Singleton2 getSingleton2(){
        synchronized(Singleton2.class){  // 使用 synchronized 块,临界资源的同步互斥访问
            if (singleton2 == null) { 
                singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }
}
/* Output(完全一致): 
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
        16993205
 *///:~

这种实现方式的运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高。

3)、同步延迟加载 — 使用内部类实现延迟加载

// 线程安全的懒汉式单例
public class Singleton5 {
 
    // 私有内部类,按需加载,用时加载,也就是延迟加载
    private static class Holder {
        private static Singleton5 singleton5 = new Singleton5();
    }
 
    private Singleton5() {
 
    }
 
    public static Singleton5 getSingleton5() {
        return Holder.singleton5;
    }
}
/* Output(完全一致): 
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
        482535999
 *///:~

这种方式也是一种效率比较高的做法。

 

六、单例模式与双重检查

使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率。

// 线程安全的懒汉式单例
public class Singleton3 {
 
    //使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
    private static volatile Singleton3 singleton3;
 
    private Singleton3() {
    }
 
    public static Singleton3 getSingleton3() {
        // Double-Check idiom
        if (singleton3 == null) {
            synchronized (Singleton3.class) {       // 1
                // 只需在第一次创建实例时才同步
                if (singleton3 == null) {       // 2
                    singleton3 = new Singleton3();      // 3
                }
            }
        }
        return singleton3;
    }
}
/* Output(完全一致): 
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
        1104499981
 *///:~

为了在保证单例的前提下提高运行效率,我们需要对 singleton3 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:
   
  必须使用volatile关键字修饰单例引用。

 

那么,如果上述的实现没有使用 volatile 修饰 singleton3,会导致什么情形发生呢? 为解释该问题,我们分两步来阐述:

(1)、当我们写了 new 操作,JVM 到底会发生什么?

  首先,我们要明白的是: new Singleton3() 是一个非原子操作。代码行singleton3 = new Singleton3(); 的执行过程可以形象地用如下3行伪代码来表示:

memory = allocate();        //1:分配对象的内存空间
ctorInstance(memory);       //2:初始化对象
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址

但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:

memory = allocate();        //1:分配对象的内存空间
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址
ctorInstance(memory);       //2:初始化对象

(2)、重排序情景再现 
   
  了解 new 操作是非原子的并且可能发生重排序这一事实后,我们回过头看使用 Double-Check idiom 的同步延迟加载的实现:

  我们需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 singleton3 来引用此对象。这行代码存在的问题是,在 Singleton 构造函数体执行之前,变量 singleton3 可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。下面是程序可能的一组执行步骤:

  1、线程 1 进入 getSingleton3() 方法; 
  2、由于 singleton3 为 null,线程 1 在 //1 处进入 synchronized 块; 
  3、同样由于 singleton3 为 null,线程 1 直接前进到 //3 处,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的; 
  4、线程 1 被线程 2 预占; 
  5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象; 
  6、线程 2 被线程 1 预占。 
  7、线程 1 通过运行 Singleton3 对象的构造函数来完成对该对象的初始化。

  显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。

 

五、单例模式与ThreadLocal

借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为 线程局部范围内的操作 。这里的 ThreadLocal 也只是用作标识而已,用来标识每个线程是否已访问过:如果访问过,则不再需要走同步块,这样就提高了一定的效率。对应的代码清单如下:

// 线程安全的懒汉式单例
public class Singleton4 {
 
    // ThreadLocal 线程局部变量
    private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<Singleton4>();
    private static Singleton4 singleton4 = null;    // 不需要是
 
    private Singleton4(){}
 
    public static Singleton4 getSingleton4(){
        if (threadLocal.get() == null) {        // 第一次检查:该线程是否第一次访问
            createSingleton4();
        }
        return singleton4;
    }
 
    public static void createSingleton4(){
        synchronized (Singleton4.class) {
            if (singleton4 == null) {          // 第二次检查:该单例是否被创建
                singleton4 = new Singleton4();   // 只执行一次
            }
        }
        threadLocal.set(singleton4);      // 将单例放入当前线程的局部变量中 
    }
}
/* Output(完全一致): 
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
*///:~

借助于 ThreadLocal,我们也可以实现线程安全的懒汉式单例。但与直接双重检查模式使用,使用ThreadLocal的实现在效率上还不如双重检查锁定。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值