欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java 设计模式之单例模式

程序员文章站 2022-05-04 17:10:42
...

在《Head First 设计模式》一书中,将单例模式称作单件模式。这里为了适应大环境,把它称之为大家更熟悉的单例模式。

一、了解单例模式

1.1 什么是单例模式

单例模式确保一个类只有一个实例,并提供一个安全访问点。

我们把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。想要获取单例实例,通过单例类是唯一的途径。单例类提供对这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。

1.2 单例模式 UML 图解
Java 设计模式之单例模式

1.3 单例模式应用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。比如线程池、缓存、日志对象等。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。
  • 以及要求只有一个对象的场景。

二、单例模式具体应用

2.1 经典的单例模式实现

采用经典单例模式实现代码有一个特点:如果我们不需要这个实例 (调用 getInstance() 方法),它就永远不会产生。因此这种方式也被称为“延迟实例化”(lazy instantiaze)。也被大家称为“懒汉式”。

单例类 Singleton

package com.jas.singleton;

public class Singleton {
    // 用静态变量来记录 Singleton 类的唯一实例
    private static Singleton uniqueInstance;

    /**
     * 把构造器声明为私有的,只有自己 Singleton 内部才可以调用构造器
     */
    private Singleton(){}

    /**
     * getInstance() 方法来实例化对象
     * 
     * @return Singleton 的实例对象
     */
    public static Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }

        return uniqueInstance;
    }
}

测试类

package com.jas.singleton;

public class SingletonTest {
    public static void main(String[] args) {

        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        System.out.println(singleton1 == singleton2);
    }
}

     /**
     * 输出
     * true
     */

这虽然是经典的单例模式,但是这样做却存在着一个严重的问题:当多个线程同时访问 getInstance() 方法时,会产生线程安全问题,可能导致产生的实例可能会有多个,这样就违反了单例的原则。

2.2 处理多线程

存在线程安全问题,我们的第一反应可能是加同步锁。就像下面这样,这样做是可以解决线程安全问题,但是却降低了性能。因为只有在第一次执行该方法的时候,才真正需要同步。之后再调用此方法,同步反而会成为一种累赘。

    /**
     * 通过 synchronized 关键字,来保证不会同时有两个线程进入该方法
     * 
     * @return Singleton 的实例对象
     */
    public synchronized static Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }

        return uniqueInstance;
    }

2.3 改善多线程问题

为了符合大多数 Java 程序,很明显地,我们需要确保单例模式能在多线程的情况下正常工作。但是同步的做法会击垮其性能,所以提供以下几种方法来解决问题。

(1) 直接同步

直接同步虽然会降低性能,但是如果你的程序可以承受 getInstance() 造成的额外代价,同步确实是一种既简单又有效的方法。但是你必须知道,同步一个方法,可能会使程序的执行效率下降几十倍。因此,如果你需要频繁使用单例对象,那么你就要重新考虑设计了。

(2) “急切”创建实例

如果应用程序总是创建并使用单例创建的对象,或者在创建和运行时方面的负担不太严重,你可以急切 (early) 创建此对象。这种方式也被大家称为“恶汉式”。就像下面这样

package com.jas.singleton;

public class Singleton {
    //在静态初始化器中创建对象,用来保证线程安全
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

利用上面这种做法,我们依赖 JVM 在加载这个类时马山创建此唯一的实例。JVM 保证在任何时候任何线程访问 getInstance() 方法之前,一定会先创建此实例。这样一来就可以解决多线程之间的安全问题。

(3)双重检验加锁

利用双重检验加锁 (double-checked locking),首先检查实例是否已经被创建了,如果未创建,“才”开始同步。这样一来,只有第一次会同步,这样做正是我们想要的。

package com.jas.singleton;

public class Singleton {
    //volatile 关键字用来保证内存可见性,使多线程正确处理 uniqueInstance 对象
    private static volatile Singleton uniqueInstance;

    private Singleton(){}

    public static Singleton getInstance(){
        //使用这种方式,只有第一次才会彻底访问并执这里的代码
        if(uniqueInstance == null){     //检查实例,如果不存在进入同步区
            synchronized (Singleton.class){
                if(uniqueInstance == null){     //进入同步区后,再检查一次。如果为 null,才开始创建实例
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

如果你性能是你关心的重点,那么这种方式会帮你大大减少访问 getInstance() 时的时间消耗。需要在注意的是:这种双重检验加锁的方式并不适用于 1.4 及之前更早的版本。

三、单例模式总结

3.1 优缺点总结

优点

  • 实例控制:单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  • 灵活性:因为类控制了实例化过程,所以类可以灵活更改实例化过程。

缺点

  • 开销:虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例。
  • 可能的开发混淆:使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用 new 关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
  • 对象生存期:不能解决删除单个对象的问题。在提供内存管理的语言,只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。

3.2 部分知识总结

  • 单例模式确保程序中一个类最多只有一个实例。单例模式也提供访问这个实例的全局点。
  • 如果你使用多个类加载器,可能导致单例模式失效,从而产生多个实例。类加载器可参考博文:虚拟机类加载机制
  • 确定性能和资源上的限制,我们应当选择合适的方案来实现单例模式。

PS:点击了解更多设计模式 http://blog.csdn.net/codejas/article/details/79236013

四、参考文献

《Head First 设计模式》
https://www.cnblogs.com/tufujie/p/5614682.html