设计模式(6)-单例模式

单例模式(Singleton Pattern)确保一个类只有一个实例,并且提供一个全局的访问。

单例模式随处可见,比如线程池缓存对话框日志对象等,这些时候如果制造出多个实例,程序运行就会出现预期之外的情况。
这里可能有疑问,我用全局静态变量也能做到一个类只有一个实例,为什么要引入这样一个设计模式呢?原因其实很简单,全局静态变量会造成资源浪费:假设这个类非常消耗资源,程序在运行过程中,不是每一次都用到这个类,那就是极大的浪费。

类图

类图不是目的,仅仅帮助理解

单例模式

单例模式的类图很简单,只有一个类,有一个代表自己实例的instance变量,还有一个提供全局访问的静态方法getInstance()

以下的代码和思路是针对Java语言

单例类型

单例模式分为懒汉式和饿汉式,区别在于实例化单例对象的时机。

饿汉式

在饿汉式单例模式实现中,不管单例是否用到,都会实例化一个单例对象。典型的写法如下:

/**
 * 单例
 * Created by Carlton on 2016/11/21.
 */
class Singleton private constructor()
{
    companion object
    {
        private val instance = Singleton()
        fun instance() = instance
    }
}

因为Kotlin和Java在静态语法上的不一致,后面的代码都用Java来实现方便理解

/**
 * 单例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {

    }

    private static Singleton instance = new Singleton();
    public static Singleton instance()
    {
        return instance;
    }
}

饿汉式非常简单,也不会出现资源占用之外的其他问题,就不多说。

饱汉式、常规方法

饱汉式也就是常规实现方式比较复杂,原因是我们用到类实例的时候才会去实例化,这中间会出现各种各样的情况。

介绍了两种实现方式,接下来我们实现一个常规的单例模式:

/**
 * 单例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {

    }

    private static Singleton instance = null;
    public static Singleton instance()
    {
        if(instance == null)
        {
            // 1
            instance = new Singleton();
        }
        return instance;
    }
}

在客户端获取单例的时候,检查对象是否是null如果是,则实例化一个,如果不是则直接返回已有的对象,如果在单线程的情况下,确实如此,现在如果,两个或者两个以上的线程就有问题了:
– 如果两个线程都到1这个位置
– 那么现在的情况就是if (instance == null)判断的时候两个线程都通过了
– 这个时候instance会实例化两次,这两个线程拿到的不是同一个实例

怎么解决这个问题呢?不慌解决,先看看一下双重验证和volatile

双重检查和volatile

如果加上线程锁,好像问题就解决了,先看看加线程锁怎么写:

/**
 * 单例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {

    }

    private static Singleton instance = null;
    public static Singleton instance()
    {
        if(instance == null)
        {
            // 1
            synchronized (Singleton.class)
            {
                // 2
                if(instance == null)
                { // 3
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

现在看看多线程的情况程序会出现什么问题:
– 如果有两个线程都到了1
– 因为同步锁的原因,只有一个线程可以先进入到2
– 当第一个线程进入3实例化一个instance后,第二个线程进入判断的时候,就不会进入3
这就是双重验证,在C/C++中,这样做是没有问题的,但是:双重检查对Java语言编译器不成立!原因在于,Java编译器中,Singleton类的初始化与instance变量赋值的顺序不可预料,如果一个线程在没有同步化的条件下读取instance引用,并调用这个对象的方法的话,可能会发现对象的初始化过程还没有完成,从而造成崩溃。

可能有人会觉得volatile可以解决问题,修改变量申明:

private static volatile Singleton instance = null;

先看看volatile是什么?

volatile变量具有synchronized的可见性特性,但是不具备原子特性。这就是说线程能够自动发现volatile变量的最新值。volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。

通过这个描述知道volatile是一个轻量级的线程同步,之前出现的问题在于线程没有同步化的条件下读取instance,现在加上volatile问题就解决了。但是:JDK1.5之前,这样使用双重检查还是有问题。

Java中如何正确的实现单例模式

说了这么多,如何才能正确实现单例模式呢?
* 使用饿汉式
* JDK1.5以后使用带volatile修饰的双重检查
* 同步锁加到方法上:

/**
 * 单例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
        private static volatile Singleton instance = null;
        private Singleton()
        {
        }
        public static synchronized Singleton instance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
        return instance;
        }
}

多说几句,如果把同步锁加到方法上面,代表这个方法同一时间只有一个线程能够进入方法,这个时候后面的线程进入就会正常的直接返回instance实例。

总结

单例模式在思路上是很简单的模式,也就不提供例子,单例模式还有很多单例模式的变种,但是核心没变:一个类只有一个实例;这个实例由自己来实例化;单例模式没有提供公共的构造函数,所以其他类不能对其实例化。需要注意的是,这个模式的复杂点在于实现方式,如何才能保证在各种情况下只有一个类实例才是关键点。

设计模式笔记本 【传送门】

不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢

发表评论

电子邮件地址不会被公开。 必填项已用*标注