深入理解《Effective Java》 之 条目3:利用私有构造器或枚举类型强化Singleton属性
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 序列化机制的工作方式是:读取字节流–>分配一个新–>对象填充字段(包括 static、final以外的字段–>不调用你定义的构造器
所以,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类的第一次实例化,initialized从false-->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是首选方式,简洁、自带序列化机制、能防止各种序列化或反射攻击。但这种方式也只能进行枚举之间的继承。
更多推荐

所有评论(0)