博客已迁移到CSDN《https://blog.csdn.net/qq_33375499》
对于一个软件系统中的某些类而言,只有一个实例是很重要的。单例模式(Singleton)是结构最简单的设计模式,它的核心结构中只包含一个被称为单例类的特殊类。单例模式是一种对象创建型模式。实现单例模式有3个要点:
- 某个类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供这个实例
单例模式(Singleton)定义:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
单例模式实现
对于单例模式(Singleton),在单例类的内部创建它的唯一实例,并通过静态方法getInstance()让客户端可以使同它的唯一实例;为了防止在外部对单例类实例化,将其构造函数的设置为private;在单例类内部定义一个Singleton类型的静态对象作为供外部共享访问的唯一实例。
在单例模式的实现过程中需要注意以下3点:
(1) 单例类的构造函数的访问权限为private
(2) 提供一个类型为自身的静态私有成员变量
(3) 提供一个公有的静态工厂方法。
一、饿汉式单例
饿汉式单例类中定义了一个静态变量,当类被加载时,静态变量就会被初始化,此时类的私用构造函数会被调用,单例类的唯一实例将被创建。代码:
public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton(){ } public static EagerSingleton getInstance(){ return instance; } }
二、懒汉式单例
懒汉式单例与饿汉式单例不同的是采用了延迟加载(Lazy Load)技术,即需要的时候再加载实例。代码:
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton(){ } /** * 使用synchronize关键字对方法加锁,确保任意时刻都只有一个线程可以执行该方法 * @return */ synchronized public static LazySingleton getInstance() { if (instance == null){ instance = new LazySingleton(); } return instance; } }
上述饿汉式单例类中,在getInstance()方法前面增加了synchronized关键字,以处理多线程同时访问的的问题,但是在多线程高并发访问的环境中将会导致系统性能大大降低。因此的对该代码进行改进,可以发现,我们在代码中无需对整个方法进行加锁,只需用synchronized对 instance = new LazySingLeton() 进行锁定即可。
(有关synchronized详解,可以参考: 《java 锁机制(synchronized 与 Lock)》)
代码如下:
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton(){ } public static LazySingleton getInstance() { if (instance == null){ synchronized (LazySingleton.class){ instance = new LazySingleton(); } } return instance; } }
以上代码即实现了单例模式,貌似又解决了多线程高并发的问题,然而事实并非如此,我们来分析一下。假如,线程A和线程B都在同一实际调用getInstance() 方法,此时instance对象为null,线程A、线程B同时进入if (instance == null) {} 代码块,假如A先获取锁,进行实例创建,当A执行完毕后,B获取锁,也会进行实例创建,导致产生了多个单例对象,违背了单例模式的设计原则。怎么解决呢?我们可以在synchronized代码块中再次进行 instance == null 的判断,这种方式称为双重检查锁定(Double-Check Locking)。代码如下:
public class LazySingleton { private static volatile LazySingleton instance = null; private LazySingleton(){ } public static LazySingleton getInstance() { if (instance == null){ synchronized (LazySingleton.class){ // 二重判断 if (instance == null){ instance = new LazySingleton(); } } } return instance; } }
注意:细心的小伙伴会在上面代码中发现,我们在定义类变量时多了一个volatile 关键字,被volatile关键字修饰的成员变量可以确保多个线程都能够正确处理。并且,volatile关键字会屏蔽Java虚拟机自动的代码优化,可能会导致系统的运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
volatile:用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排,且是内存可见的。
volatile特性:volatile具有可见性、有序性,不具备原子性。
注意:volatile不具备原子性,这是volatile与java中的synchronized、Lock最大的功能差异,这一点在面试中也是非常容易问到的点。
可见性、有序性、原子性:
1.原子性:指多个操作要么全部执行,要么不执行。
2.可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线其他能够立即读取到线程1修改后的值。
3.有序性:值程序执行时按照代码编写的先后顺序执行。
三、使用IoDH实现单例
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类采用synchronized来解决线程安全问题,在性能上会对系统有一定影响。可见,无论是饿汉式还是懒汉式都会存在一些问题。为了解决这些问题,在java语言中可以通过Initialization on Demand Holder(IoDH)技术类实现单例模式。
在IoDH中,需要在单例类中增加一个静态内部类(static class),在该内部类中创建单例对象,在将该单例对象通过getInstance() 方法返回给外部内使用,代码如下:
public class Singleton { private Singleton(){ } private static class IoDHSingleton { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return IoDHSingleton.instance; } }
运行机制:由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance() 时将加载内部类IoDHSingleton,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个static成员变量,由java虚拟机自动保证线程安全问题,确保该成员变量只能别初始化一次。
通过使用IoDH既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为java中最好的单例实现方式。
单例模式的缺点
在现在很多面向对象语言的运行环境中,很多都提供了垃圾自动回收技术,因此如果实例的共享对象长时间不被利用,系统会自动销毁并回收资源,下次利用又将重新实例化,这将导致共享的单例对象状态的丢失。