在Java运行时中,有些较为庞大复杂的类,如果频繁的创建和销毁对象,会造成系统的性能下降。
为 了解决这个问题,我们可以应用单例模式。
什么是单例模式
单例模式,顾名思义,就是一个类只有一个实例。在Java中,单例模式有通常两种实现方式(在深挖之前我是这么觉得的):饿汉式和懒汉式。他们的实现思想可以从以下三点入手:
- 是否线程安全
- 是否懒加载
- 能否反射破坏
饿汉式单例模式
饿汉式单例模式,是在类加载时就创建单例对象,这种方式比较简单,但缺点也很明显,就是在类加载时就完成实例化,如果类中存在资源耗费的操作,则会造成资源浪费。
public class Hungry {
private Hungry(){}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例模式
懒汉式单例模式,是在第一次调用getInstance()方法时才创建单例对象,这种方式比较常用,但也存在线程安全问题。
public class Lazy {
private Lazy(){}
private static Lazy lazy;
public static Lazy getInstance(){
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
但在多线程情况下,以上代码会出现问题,因为在多线程环境下,可能会创建多个实例对象。为了解决这个问题,我们可以加锁,常见的是使用双重检查锁定。
双检锁(常用的)
public class Lazy {
private Lazy(){}
private static Lazy lazy;
public static Lazy getInstance(){
if(lazy == null){
synchronized (Lazy.class){//第一道检查
if(lazy == null){//第二道检查
lazy = new Lazy();
}
}
}
return lazy;
}
}
双检锁看似已经解决了线程安全问题,但其实还是有问题。 详情可参考happens-before原则
因为lazy = new Lazy()
并不是一个原子性操作,在指令层面,这个操作可以分为三步
- 分配内存空间
- 执行构造方法,初始化对象
- 把对象指向这个空间
而在真正执行时,虚拟机为了效率可能会对这三步操作进行重排,比如,***步骤2和步骤3可以交换顺序***。
所以灾难就发生了。
在多线程环境下,假设我有两个线程A和B,如果步骤2和步骤3交换顺序,在A执行完3还没来得及执行2的时候,
lazy已经不是null了,这时B恰好走到了if(lazy == null)
这一步,
此时B就会直接返回一个未初始化的对象,导致程序出错。
为了解决这个问题也不难,只需要在private static Lazy lazy
中加上volatile
关键字即可,它的主要作用是确保变量的可见性和防止指令重排序
双检锁唯一的缺点可能就是实现起来较为繁琐,那有没有既满足了懒加载,也满足了线程安全同时效率高且简洁的单例模式呢?
静态内部类
静态内部类在程序启动的时候不会加载,第一次被调用时才会加载
public class Holder {
private static class HolderInstance{
private static final Holder INSTANCE = new Holder();
}
private Holder() {}
public static Holder getInstance(){
return HolderInstance.INSTANCE;
}
}
但是以上的所有写法都是不安全的,都可以通过反射来破解!
反射破坏单例模式
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Lazy instance = Lazy.getInstance();
Constructor<Lazy> lazyConstructor = Lazy.class.getDeclaredConstructor(null);
lazyConstructor.setAccessible(true);
Lazy instance2 = lazyConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
利用反射来突破访问控制,创建一个私有构造函数的实例,最终运行的结果如下
可见“成功”突破了单例模式,创建了两个不同实例
但反射的本质上还是通过将无参构造器暴露出来,然后调用newInstance()方法来创建对象,这就使得单例模式被破坏了。
想要解决也可以,在无参构造中也加把锁就行了
private Lazy(){
synchronized (Lazy.class){
if(lazy != null){
throw new RuntimeException("不要通过反射破坏单例!")
}
}
}
问题看似又被解决了
但是道高一尺魔高一丈,如果我在反射中不使用getInstance()方法,而是直接使用构造器new一个对象,会怎么样呢?
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// Lazy instance = Lazy.getInstance(); 不使用getInstance()方法
Constructor<Lazy> lazyConstructor = Lazy.class.getDeclaredConstructor(null);
lazyConstructor.setAccessible(true);
Lazy instance2 = lazyConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
“锁”再次被攻破了
不要灰心,我们仍可以通过添加新字段,即再次“加锁”来解决,但是这样一层层下去,代码会越来越复杂。而再牢固的锁,也会被撬开。我们不妨从底层入手,看看到底怎样才能彻底根除反射破坏
反射的底层实现
反射的底层实现是通过调用java.lang.reflect.Constructor
类的newInstance()
方法来创建对象。
通过底层代码可以看出,枚举类型是无法通过反射创建对象的,因为枚举类型是编译时就已经确定了,所以在编译时就已经确定了构造器,而反射是在运行时才确定构造器的。
验证反射是否可以破坏枚举
public enum Single {
INSTANCE;
public Single getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Single s1 = Single.INSTANCE;
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Single s2 = declaredConstructor.newInstance();
System.out.println(s1 == s2);
}
}
运行一下,程序确实抛出异常,但却不是因为枚举无法通过反射创建对象的异常”Cannot reflectively create enum objects”,而是找不到无参构造,这又是为什么呢?
虚假的无参构造
class文件告诉我们,枚举中只存在一个无参构造器,而上面的运行结果却是找不到无参构造器。那么枚举的构造器到底是什么?
再往下一层
最后在Enum.java的源码中发现,枚举实际上是重写了父类的签名为(String name,int ordinal)的构造函数,而在编译时,编译器会自动添加一个无参构造器,所以枚举的构造器实际上是有参的。
再次修改代码,验证枚举的构造器是否可以被反射破坏
public enum Single {
INSTANCE;
public Single getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Single s1 = Single.INSTANCE;
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
Single s2 = declaredConstructor.newInstance();
System.out.println(s1 == s2);
}
}
如愿以偿
写于2024-09-19深夜,既时和华哥又聊了很多,深有感触
附:单例模式的应用场景
单例模式的应用场景有很多,比如:
- 数据库连接池
- 日志对象
- 线程池
- 应用配置对象
- 应用缓存对象
- 注册表对象
- 全局配置对象
- 应用上下文对象
- 全局状态对象