单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式。
如何实现一个单例:
常见的单例设计模式,有如下五种写法,在编写单例代码的时候要注意以下几点:
1、构造器需要私有化
2、暴露一个公共的获取单例对象的接口
3、是否支持懒加载(延迟加载)
4、是否线程安全
1、饿汉式
饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。从名字中我们也可以看出这一点。具体的代码实现如下所示:
public class EagerSingleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
大多数文章觉得饿汉式不能支持懒加载,即使不使用也会浪费资源,一方面是内存资源,一方面会增加初始化的开销。
1、现代计算机不缺这一个对象的内存。
2、如果一个实例初始化的过程复杂那更加应该放在启动时处理,避免卡顿或者构造问题发生在运行时。满足 fail-fast 的设计原则。
2、懒汉式
有饿汉式,对应地,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载,具体的代码实现如下所示:
public class LazySingleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
以上的写法本质上是有问题,当面对大量并发请求时,其实是无法保证其单例的特点的,很有可能会有超过一个线程同时执行了new Singleton();
解决方案:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
以上的写法可以保证jvm中有且仅有一个单例实例存在,但是方法上加锁会极大的降低获取单例对象的并发度。同一时间只有一个线程可以获取单例对象,为了解决以上的方案则有了第三种写法。
3、双重检查锁
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式:
在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:
public class DclSingleton {
// volatile如果不加可能会出现半初始化的对象
// 现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序),为了兼容性我们加上
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4、静态内部类
我们再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?我们先来看它的代码实现。
public class InnerSingleton {
/** 私有化构造器 */
private Singleton() {
}
/** 对外提供公共的访问方法 */
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/** 写一个静态内部类,里面实例化外部类 */
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
SingletonHolder 是一个静态内部类,当外部类 Singleton被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
5、枚举
最后,我们介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示:
这是一个最简单的实现,因为枚举类中,每一个枚举项本身就是一个单例的:
public enum EnumSingleton {
INSTANCE;
}
更通用的写法如下:
public class EnumSingleton {
private Singleton(){
}
public static enum SingletonEnum {
EnumSingleton;
private EnumSingleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public EnumSingleton getInstance(){
return instance;
}
}
}
还可以将单例项作为枚举的成员变量:
public enum GlobalCounter {
INSTANCE;
private AtomicLong atomicLong = new AtomicLong(0);
public long getNumber() {
return atomicLong.incrementAndGet();
}
}
这种写法是Head-first中推荐的写法,他除了可以和其他的方式一样实现单例,他还能有效的防止反射入侵。
6、反射入侵
想要阻止其他人构造实例仅仅私有化构造器还是不够的,因为还可以使用反射获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,通过下边的方式解决:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){
if(singleton != null)
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
此时方法如下:
@Test
public void testReflect() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<DclSingleton> clazz = DclSingleton.class;
Constructor<DclSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
boolean flag = DclSingleton.getInstance() == constructor.newInstance();
log.info("flag -> {}",flag);
}
结果如下:
#7、序列化与反序列化安全
到止的单例依然是有漏洞的,如下代码:
@Test
public void testSerialize() throws IllegalAccessException, NoSuchMethodException, IOException, ClassNotFoundException {
// 获取单例并序列化
Singleton singleton = Singleton.getInstance();
FileOutputStream fout = new FileOutputStream("D://singleton.txt");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(singleton);
// 将实例反序列化出来
FileInputStream fin = new FileInputStream("D://singleton.txt");
ObjectInputStream in = new ObjectInputStream(fin);
Object o = in.readObject();
log.info("他们是同一个实例吗?{}",o == singleton);
}
我们废了九牛二虎之力还是没能阻止他返回false,结果如下:
readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在:
public class Singleton implements Serializable {
// 省略其他的内容
public static Singleton getInstance() {
}
// 需要加这么一个方法
public Object readResolve(){
return singleton;
}
}