Singleton指只能被实例化一次的类(就是我们经常说的单例),通常用于表示无状态的对象,如函数,或本质上唯一的组件。

将一个类设计为Singleton会使其客户端测试变的十分困难,因为Singleton要求构造器私有化,也就不能继承,所以,我们无法创建一个新实例,用于其实现的模拟测试。

有三种常见的实现单例的方式:

第一种:用一个final字段作为这个公有静态的成员【公有常量】

public class Elvis {
    // 用一个常量表示唯一实例
	public static final Elvis INSTANCE=new Elvis();
    
    // 构造器私有
	private Elvis() {}
    	
}

在这个例子中,私有构造器只被调用了一次,用来初始化共有的静态final字段Elvis.INSTANCE。因为构造器是私有的,你不可能在这个类的外部new一个实例,所以,一旦Elvis类被初始化,Elvis.INSTANCE实例全局唯一。这种方法有点:API可以清楚地表明该类是个Singleton,公有常量,永远包含同一个对象引用。

但通过类反射机制就可以越权!!

越权的客户可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有的构造器。

我们可以模拟反射攻击来构造一个新的实例:

public static void main(String[] args) throws Exception{    
    Constructor<Elvis> ctor =Elvis.class.getDeclaredConstructor();
    // 这里重新设置访问标识,绕开访问权限的限制,完全可以通过私有构造器创建实例
    // 与AccessibleObject.setAccessible(constructors, true);等效
    ctor.setAccessible(true);
    Elvis fake=ctor.newInstance();
}

反射机制绕开访问权限检测,无论是何种构造器,都可以创建实例,这就违反了单例原则。那么,如何控制呢?

如果需要防御这类攻击,可以修改构造器,使其在被要求创建第二个实例时抛出异常。

我们完善下这个例子,使其可以防御类反射攻击:

public class Elvis {
	public static final Elvis INSTANCE=new Elvis();
	private Elvis() {
		if(INSTANCE!=null) {
			throw new IllegalStateException("Elvis instance already exists");
		}
	}
}

如果想让它支持序列化,仅在其声明上加implements Serializable是不够的。为保证其满足Singleton的性质,还需要用transient来声明其所有实例字段,并提供一个readResove方法。

这里有几层含义:

(1)为什么只加 implements Serializable不行?

public class Elvis implements Serializable {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() {}
    public static Elvis getInstance() { return INSTANCE;}
}

列化机制有一个致命特性:反序列化时,会通过反射创建一个新的实例,即使构造器是private,这样就使得单例被破坏

Java 序列化机制的工作方式是:读取字节流–>分配一个新–>对象填充字段(包括 staticfinal以外的字段–>不调用你定义的构造器

所以,private Elvis()完全不会被调用,JVM 直接“造”出一个新对象。

(2)为什么要用 transient

原因只有一个:防止反序列化时,字段被“错误地恢复”。如一个对象引用了过期数据或不一致状态等等。

(3)为什么要用readResolve

反序列化完成后,JVM 会调用 readResolve()用它的返回值替换刚刚创建的对象

反序列化生成的新对象
    ↓
readResolve()
    ↓
返回原来的 INSTANCE

✅ 正确的单例 + 序列化写法

public class Elvis implements Serializable {

    private static final Elvis INSTANCE = new Elvis();

    private Elvis() {
         if(INSTANCE!=null) {
			throw new IllegalStateException("Elvis instance already exists");
		}
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    // 关键方法,必需是private修饰符、返回值必需是Object类型,也不能是static的,这都是规定
    private Object readResolve() {
        return INSTANCE;
    }
}

第二种:提供一个静态工厂方法作为公有的成员【静态方法】

public class Elvis {
    // final字段私有化了
	private static final Elvis INSTANCE=new Elvis();
    
	private Elvis() {
		if(INSTANCE!=null) {
			throw new IllegalStateException("Elvis instance already exists");
		}       
    }
    
    // 静态工厂方法
	public static Elvis getInstance(){return INSTANCE;}
}

这里,对Elvis.getInstance方法的所有调用都会返回同一个对象引用,不会有其他Elvis实例被创建出来。

这种方式有以下优点:

优点一:通过静态方法返回实例,具有足够灵活性

它提供了足够的灵活性,即使以后我们不再想将这个类设计为Singleton,也不需要修改其API。

假设我们决定Elvis不能再是单例了,每次调用都应该返回一个新的实例,那么,我们只需要把 return INSTANCE;改成 return new Elvis();即可。客户端所调用Elvis.getInstance()的代码完全不需要做任何修改,它们甚至感知不到背后的实现已经变了。这就是“不需要修改其API”的含义————公共方法签名没变,但行为变了

工厂方法现在返回的是唯一实例,但它很容易被修改,比如为每个调用它的线程返回一个唯一的实例。

举例:线程单例(Thread-local Singleton

假设你的应用是多线程的,你希望在每个线程内部,Elvis只有一个实例,但不同的线程要有自己独立的实例(这通常用于保存线程上下文信息)。使用静态工厂方法,我们可以利用 ThreadLocal来实现:

(1)基础版

静态工厂方法不做任何控制,直接返回,这与第一种方法的区别就是在final静态成员变量与调用者之间加了一个getInstance方法。因此,我们可以在getInstance方法体中做很多事情,其灵活性集中体现在此处。

public class Elvis{
    // 使用 ThreadLocal 来保存每个线程的实例
    private static final ThreadLocal<Elvis> threadLocalInstance=ThreadLocal.withInitial(Elvis::new);
    
    private Elvis(){}
    
    // API 完全没有变,但行为变成了“每线程单例”
    public static Elvis getInstance(){return threadLocalInstance.get();}
}

(2)升级版

基础版中,我们发现反射攻击的风险依然存在。我们现在想升级一下,使其能够防御这种风险。

public class Elvis {
	// 创建了一个存有线程变量的容器
	private static final ThreadLocal<Elvis> threadLocalInstance=new ThreadLocal<>();	
    // 创建了一个初始化原子变量开关
	private static final AtomicBoolean initialized=new AtomicBoolean(false);
    
    // 类加载时,进行第一次初始化
	static {
		threadLocalInstance.set(new Elvis());
	}
    
	// 私有构造器进行控制,实现全局Singleton
	private Elvis() {
		if(!initialized.compareAndSet(false, true)) {
			throw new IllegalStateException("Elvis instance already exists");
		}	
	}
	
	public static Elvis getInstance(){return threadLocalInstance.get();}
}

类加载时,完成Elvis类的第一次实例化,initializedfalse-->ture,以后无论哪个线程、不管是否反射compareAndSet均失败,都会抛出构造异常。

请注意,有的人认为static块比较麻烦会去掉,同时,静态常量threadLocalInstance这样声明:

private static final ThreadLocal<Elvis> threadLocalInstance=ThreadLocal.withInitial(Elvis::new);

**这样做是个伪防御,千万不要!!**因为Elvis::new属于懒加载模式,在类加载的时候并不会创建实例,更准确地说,ThreadLocal.withInitial(Elvis::new)只是注册了一个Supplier,真正的 new Elvis()要等到threadLocalInstance.get(),即withInitial具有延迟性。所以,这个防御逻辑只能阻止“第二次创建”,但无法阻止“反射比ThreadLocal更早创建实例”,就会出现一种BUG————反射成功,正常代码反而炸。

(3)高级版

升级版实现的是一个全局单例,我们要的是线程单例,还要改造下代码:

public class Elvis {
 	private static final ThreadLocal<Elvis> threadLocalInstance=new ThreadLocal<>();
	// 防御反射:同一线程只能创建一个
	private Elvis() {
		if(threadLocalInstance.get()!=null) {
			throw new IllegalStateException("Elvis instance already exists");
		}
		threadLocalInstance.set(this);
	}
    
	public static Elvis getInstance(){
		Elvis elvis=threadLocalInstance.get();
		if(elvis==null) {
			elvis=new Elvis();// 构造器里 set
		}
		return elvis;
	}   
}

这里防御反射,指的是同一线程内只能创建一个实例,这是由ThreadLocal语义决定的,不是缺陷。

ThreadLocal 的防御逻辑,只在同一个线程内有效;不同线程之间,ThreadLocal 是完全隔离的,所以反射在不同线程创建实例,防御是拦不住的。

**总结:**通常我们说Singleton指的是JVM级别唯一实例,但 ThreadLocal 单例的真实语义是:线程级别唯一实例

单例类型 作用范围 能否防跨线程反射
enum 单例 JVM 全局
静态变量单例 JVM 全局 ✅(加构造器校验)
ThreadLocal 单例 单个线程
Web 请求 Scope 请求级别

注意:静态工厂方法同公有常量一样也是有序列化问题的。

优点二:可以根据需要升级为泛型Singleton工厂

前面,我们知道静态工厂实现Singleton,简单来说,就是静态方法返回唯一实例。那么,泛型Singleton工厂就是将任意类型Singleton缓存起来,并由静态方法提供返回接口。

(1)经典工厂

public class SingleFactory {
    // 实例缓存
	private static final Map<Class<?>,Object> INSTANCES=new ConcurrentHashMap<>();
    
	private SingleFactory() {}
    // 泛型Singleton方法
	public static <T> T getInstance(Class<T> clazz) {		
		return clazz.cast(INSTANCES.computeIfAbsent(
					clazz, 
					k->{
						try {
							return k.getDeclaredConstructor().newInstance();// T类应有公开的无参构造方法
						} catch (Exception e) {
							throw new RuntimeException("Construct failed");
						}
					}
				));	
	}
}

测试示例:

Elvis e1 = SingletonFactory.getInstance(Elvis.class);
Elvis e2 = SingletonFactory.getInstance(Elvis.class);

System.out.println(e1 == e2); // true

(2)静态Singleton工厂容易升级泛型Singleton工厂

我们说第一种公有静态常量方式,很难改,主要是因为getInstance方法进行扩展,而静态工厂方法就很容易,只需将getInstance方法扩展为泛型方法即可,使用者调用无感。

下面,就让我们将ThreadLocal单例,升级为泛型Singleton工厂吧!

public class SingleFactory{
    private static final ThreadLocal<Map<Class<?>,Object>> THREAD_INSTANCES=ThreadLocal.withInitial(ConcurrentHashMap::new);
    
    private SingleFactory(){}
    
	public static<T> T getInstance2(Class<T> clazz) {
		Map<Class<?>,Object> map=THREAD_INSTANCES.get();
		return clazz.cast(map.computeIfAbsent(
				clazz, 
				k->{
					try {
						return k.getDeclaredConstructor().newInstance();
					} catch (Exception e) {
						throw new RuntimeException("Construct failed");
					}
				}));
	}
}

优点三:方法引用可以作为Lambda表达式的简写,用作Supplier

Elvis::getInstance就是一个Supplier<Elvis>Supplier是函数式接口,被称为生产者,而Elvis::getInstance就是这个接口的一个具体实现实例。比如:

ThreadLocal threadLocal=ThreadLocal.withInitial(Elvis::getInstance);

第三种:单元素枚举类型

public enum Elvis{INSTANCE}

用枚举实现Singleton是首选方式,简洁、自带序列化机制、能防止各种序列化或反射攻击。但这种方式也只能进行枚举之间的继承。

更多推荐