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

什么是单例模式

在实际开发中为了节约系统资源,有时候需要确保系统中某个实例只有唯一一个,而这种模式就是我们常见的单例模式。而单例模式一般有三个要点:

  • 单例类只会有一个实例
  • 实例由自身创建
  • 它需要自身向整个系统提供这个实例

代码实现

要实现一个实例模式相对其他模式来说比较简单。例如现在我们系统需要创建一个唯一的文件管理器(FileManager):

public class FileManager {
    private static FileManager instance;

    public FileManager() {
    }

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


    public static void main(String[] args) {
        FileManager fileManager1 = FileManager.getInstance();
        FileManager fileManager2 = FileManager.getInstance();
        System.out.println(fileManager1 == fileManager2);
    }
}

通过上面的代码我们很简单的就实现了单例模式,从运行结果来看可以发现fileManager1fileManager2就是同一个实例。

线程安全

上面的代码看似没什么问题,但实际上问题还是很大。在单线程的情况下上面代码不会有什么问题,但是在多线程的情况下就不一定了。在多线的情况下,很可能出现在在判断instance == null多个线程同时判断为true,这将导致多个线程去进入到instance = new FileManager()中,从而导致单例模式失败。那么该如何解决这种情况呢?

使用synchronized修饰
    public static synchronized FileManager getInstanceWithSyncLock(){
        if (instance == null){
            instance = new FileManager();
        }
        return instance;
    }

通过添加同步锁保证线程安全这是最简单的方式,但这种方式的问题在于性能会有所损耗,在实际开发中并不是最好的解决方案。

双重检查锁定(Double-checked Locking)

之前使用同步方法来解决多线程下单例线程安全问题,但是出于性能原因并不被推荐。其实同步方法的问题在于每次去获取实例时都需要加同步锁,即使实例已经创建成功了,还是需要使用同步锁使的性能上会差一些。而我们使用双重检查锁定的方式便能很好的解决这个问题,修改代码如下:

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

不过需要注意的一点是,instance实例我们需要使用volatile来修饰避免指令排序带来的负面影响。使用双重锁能解决在实例被创建后我们无锁获取单例,同时也能保证线程的安全,相对之前的方式来说在性能上得到提升。

饿汉式单例

上面我们所实现的单例模式通常我们叫懒汉式单例,它的特点是只有我们获取单例时才会触发单例的实例化,如果我们一直不调用则系统不会去创建实例。而这种懒汉式单例带来的负面影响就是存在线程安全问题,需要使用同步锁去解决安全问题。而与懒汉式单例相反的则是饿汉式,它的特点是不管你用不用它都已经创建了。

public class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

从代码实现可以看出,这种方式不存在线程安全的问题,同时代码实现也很简单。但这种方式存在的缺点是即使你整个系统都没有任何人获取实例,这个实例也是会被创建的,相对懒汉式单例来说饿汉式会相对更占用系统资源。

Initialization Demand Holder

之前介绍的懒汉式和饿汉式单例他们各有优缺点,那有没有一种方式继能实现无锁同时又能实现延迟加载呢?答案是有点,而这种方式我们通常称之为Initialization Demand Holder (IoDH)。下面直接看代码看他是如何实现的,代码如下:

public class IoDHSingleton {

    private IoDHSingleton(){}

    private static class HolderClass{
        private static final IoDHSingleton instance  = new IoDHSingleton();
    }

    public static IoDHSingleton getInstance(){
        return HolderClass.instance;
    }

    public static void main(String[] args) {
        IoDHSingleton instance1 = IoDHSingleton.getInstance();
        IoDHSingleton instance2 = IoDHSingleton.getInstance();
        System.out.println(instance1 == instance2);
    }
}

通过代码可以发现,该方式在单例类的内部增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance方法返回给外部使用。由于静态单例对象并没有作为IoDHSingleton的成员变量而实例化,因此在类加载时并不会进行实例化对象的操作实现了懒加载的效果。当第一次调用getInstance()方法时将加载内部类HolderClass,此时则会创建HolderClass中的成员变量instance,而此时的线程安全则是由虚拟机来保证的。它确保了成员变量只能初始化一次并且整个过程也不需要同步锁来保证线程的安全。但是这种方式对编程语言有要求,在Java里我们可以这么设计,但是对于其他语言来说就不一定了。

总结

单例模式算是设计模式中最好理解的设计模式了,并且这种模式在实际开发中使用频率也特别高。而单例模式的核心就是提供唯一实例供整个系统使用,只要能保证系统中获取到的实例都是同一个实例我们就可以称它是单例模式。

本文示例代码地址:https://gitee.com/zengchao_workspace/design-pattern

原文始发于微信公众号(一只菜鸟程序员):设计模式(三)-单例模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72897.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!