单例模式学习
注:学习内容来自刘伟老师的技术博客《史上最全的设计模式》。这个系列的博客深入浅出,引人入胜,值得学习。
1. 什么是单例模式?
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
2. 如何实现只能创建一个对象?
- 在类的外部不能通过new来实例化对象——实现方式是给构造函数加private关键字。
- 在类的内部只能实例化一次——在实例化前进行判断,如果为null才可以实例化。
3. 如何获得类内部的唯一实例化对象?
由于不能在外部实例化对象,所以不能通过对象的实例来调用类的方法,而只能通过类名来调用类的静态方法。又由于静态方法中只能使用静态的成员变量,因此我们在类中定义一个静态的引用指向该实例对象。如下为饿汉模式:
class Singleton { //静态成员变量 private static Singleton instance = null; //私有的构造方法 private Singleton() { } //获取单例对象 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉模式:
class Singleton { //静态成员变量 private static final Singleton instance = new Singleton(); //私有的构造方法 private Singleton() { } //获取单例对象 public static Singleton getInstance() { return instance; } }
4. 单例模式的不同实现方式?
- 饿汉子模式——在类加载的时候就实例化
- 懒汉子模式——在第一次调用getInstance方法的时候进行实例化
- 两种方法的比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
5. 懒汉模式的线程锁定方式
- getInstance()方法前面增加关键字synchronized进行线程锁
-
synchronized public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。
- 我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:
-
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }
-
问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下:
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
-
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
6. 一种更好的单例实现方式
- 饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
-
class Singleton { //私有的构造方法 private Singleton() { } //持有单例对象的静态内部类 private static class HoldClass { public static final Singleton instance = new Singleton(); } //获取单例对象 public static Singleton getInstance() { return HoldClass.instance; } }
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。