单例模式,你所应该知道的一切

单例模式是我们最常见的设计模式之一。由于设计模式在面向对象程序设计中起着举足轻重的作用,在面试中很多面试官都喜欢问一些与设计模式相关的问题。在常用设计模式中,Singleton是唯一一个能用短短几十行代码完整实现的模式。因此,写一个Singleton的类型是一个很常见的面试题。
关于单例模式,网上有不少博客文章。我也看了不少,一开始似懂非懂,后来在《剑指offer》也看到单例模式,但是不是用java写的。所以自己也想用java总结一下java实现的单例模式。

题目:如何实现Singleton模式

题目:设计一个类,我们只能生成该类的一个实例。


不好的解法一:只适用于单线程环境(懒汉式,线程不安全)

由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以防止被多次创建实例。
我们可以声明一个静态(static)的实例,在需要的时候创建该实例,也就是实现懒加载。
下面的实现就是基于这个思路:


/**
 * Created by Zheng548 on 2017/2/15 0015.
 * 只适用于单线程环境(懒汉式,线程不安全)
 * csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
 */
public class Singleton1 {

    private static Singleton1 instance = null;

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

上面代码在Singleton1的静态属性instance中,只有在instance为null的时候才创建一个实例一避免重复创建。同时我们把构造函数定义为私有函数,这样才能确保只创建一个实例。


不好的解法二:能在多线程中工作但效率不高(懒汉式,线程安全)

解法一中的代码在单线程中工作正常,但在多线程的情况下就有问题了。设想两个线程同时运行到判断instance是否为null的if语句时,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时Singleton就不能满足单例模式的要求了。
为了保证在多线程环境下我们还只能得到类的一个实例,需要加上一个同步锁。

/**
 * Created by Zheng548 on 2017/2/15 0015.
 * 能在多线程中工作但效率不高(懒汉式,线程安全)
 * csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
 */
public class Singleton2 {
    private static Singleton2 instance = null;

    private Singleton2() {

    }
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

可以看到,此种解法与第一种的区别仅仅在于后者在getInstance方法上加了synchronized 关键字(了解更多关于synchronized ,请点击我),来实现同步。


下面在看看解法二有没有解法一的问题。
我们还是假设有两个线程同时想要创建一个实例。synchronized 方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行synchronized方法。当第一个线程获得锁时,第二个线程只能等待。于是,第一个线程发现此时实例还没有创建,它就创建一个实例,然后,它释放所获得的锁。此时第二个线程可以获得类的锁,发现实例已经被创建,于是它就不会重复创建实例。
这样就保证了我们在多线程环境中也只能得到一个实例。
然而,问题来了
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。由于同步一个方法会降低100倍或更高的性能, 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。这就引出了双重检查锁定模式

可行的解法三:双重检查锁定模式

什么是双重检查锁定模式,请看维基百科的权威解释:
双重检查锁定模式
其实,虽然这个名词听起来很厉害的样子,我这样的菜鸟为之一震,然而,它并不可怕。
首先,我们考虑一下,为什么要引入“双重检查锁定模式”这样一个概念呢?一般而言,引入新的概念通常是为了解决某个问题,那么我没呢的问题是什么,还记得吗?
第二,我们的问题。解法二的同步很低效,一旦初始化完成,instance创建之后,同步就显得不再必要。
第三,如何避免上述的问题呢?
1. 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
2. 获取锁
3. 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
4. 否则,初始化并返回变量。

于是我们把解法二的代码做进一步的改进:

/**
 * Created by Zheng548 on 2017/2/15 0015.
 * 可行的解法三:双重检查锁定模式
 * csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
 */
public class Singleton3 {

    private static Singleton3 instance = null;

    private Singleton3() {
    }

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

推荐解法一:利用静态构造函数

/**
 * Created by Zheng548 on 2017/2/15 0015.
 * 推荐解法一:利用静态构造函数
 * csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
 */
public class Singleton4 {

    private static final Singleton4 instance = new Singleton4();

    private Singleton4() {
    }

    public static Singleton4 getInstance() {
        return instance;
    }
}

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。

推荐解法二:静态内部类 实现单例模式

加载一个类时,其内部类是否同时被加载?引申出单例模式的另一种实现方式

加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

package com.company;

/**
 * Created by Zheng548 on 2017/2/15 0015.
 *  推荐解法二:静态内部类 实现单例模式
 * csdn blog :http://my.csdn.net/Zheng548?locationNum=0&fps=1
 */
public class Singleton5 {

    private static class SingletonHolder {
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    private Singleton5() {
    }
    public static final Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;

代码下载

本文所有的代码

学习更多

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值