前言
单例相信各位都不陌生,从学习java开始就会经常接触到这个概念,首先先来回忆一下什么是单例:
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。
————来自百度百科。
简单来说,就是让你的对象全局只有一个,而且还是你想要别人获取到的那一个。
以下代码测试使用环境:
java版本:1.8
编译器:idea、eclipse
预热一下
那么怎么做一个单例呢?先抛开反射这种情况不说,简单的来一个小小的单例demo:
/** 一个简单的单例demo */
public class Singleton {
/** 唯一的单例 */
private static volatile Singleton singleton = null;
/** 构造私有化 */
private Singleton(){}
/** 获取实例的工厂方法 */
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
从上面的代码我们可以看到,
- 构造是私有的,外界无法直接通过
new
来创建一个新的对象实例 - 类内存在一个私有的、静态的、被
volatile
修饰的字段,作为唯一单例对象。 - 存在一个工厂方法,当唯一单例对象不存在则创建一个并保存,如果有则直接返回。同时还使用
synchronized
关键字来保证线程的安全。
上面这个就是一个很简单的、有线程安全的懒汉式单例。(如果不清楚什么是懒汉、饿汉式的话可以去百度下补补功课哦)
但是问题就来了:在这个单例中,构造和字段都是私有的没错,但是通过反射可以轻松的去破坏他们。那么,我要怎么才能做到绝对的安全呢?
步入正题
首先我们先来思考一下,单例,需要全局唯一,且不可以被篡改。
-
怎样才能够全局唯一?
如果局限在一个类中的话,那么使用static
静态字段来保存一个单例对象可以做到唯一,就像上面的那个示例一样。* -
怎样才能够不被篡改?
*有两种方法可以防止篡改。
第一,使用final
关键字对字段进行修饰。Java中的final
字段的作用就是使一个字段作为常量无法修改。第二,使用异常。当一个人想要创建一个新的对象的时候,你直接抛出一个异常,让他尝尝苦头。*
综上所述,根据上面的线索来制定一个计划吧。
当我的类一初始化,便使类中出现一个常量作为唯一的单例实例,然后在构造方法中抛出异常,直接打断想要创建新实例的人的想法。
哦~是个不错的计划呢。
好了,思想工作做好了,怎样才能利用上面的几点来实现一个安全的单例呢?
/**@ 一个反射安全的单例实例Forte */
public class Singleton {
/** 一个常量,作为单例的唯一实例Scarlet */
private static final Singleton singleton;
/** 随便再来一个字段 */
private final String name;
/** 静态代码块,会在类加载的时候执行,并且在这个类的一生中只会执行一次,利用这个特性来初始化静态常量 */
static {
singleton = new Singleton("这是一个名称");
}
/** 私有构造,就算是利用反射调用也只会迎接抛出的一个异常罢了 */
private Singleton(String name) {
if(singleton != null) {
throw new RuntimeException("这可是个单例啊!");
}
this.name = name;
}
/** 提供一个工厂方法来获取单例的实例对象 */
public static Singleton getInstance() {
return singleton;
}
/** 获取name */
public String getName() {
return name;
}
}
如上所示,就形成了一个不会被反射所破坏的单例。
首先,单例对象通过final字段保护,不会被反射暴力破坏,其次构造方法内部也会直接抛出异常,阻止创建实例。
更深一步,出个难题
比点到为止要再向前迈出一小步
什么?假如说你想要在代码运行期间修改单例的实例?
以上面的举例代码为准,假如说你想要在代码运行期间,出于某种原因去修改name
这个静态常量字符串,要怎么办才好?
首先,想要修改他的值,那么就不能使用final
关键字来修饰了。那么,想要能够修改name
这个字段,只有去掉singleton
字段的final
关键字。在你想要修改的时候先将其赋值为null
,然后再重新new
一个新的对象。
但是说到这里,问题出现了。当我去掉final
字段的一瞬间,岂不是偏离了这篇帖子的主题?没有了final
的保护,反射岂不是可以趁虚而入?
别着急,接下来我将会展示从反射手下保护字段和方法的另一个手段:
/**@ 一个反射安全的单例实例Forte */
public class Singleton {
/** 一个常量,作为单例的唯一实例Scarlet,不再使用final修饰 */
private static Singleton singleton;
/** 随便再来一个字段 */
private final String name;
/** 静态代码块,会在类加载的时候执行,并且在这个类的一生中只会执行一次,利用这个特性来初始化静态常量 */
static {
//反射保护,保护Singleton.class的'name'字段
Reflection.registerFieldsToFilter(Singleton.class, "name");
singleton = new Singleton("这是一个名称");
}
/** 私有构造,就算是利用反射调用也只会迎接抛出的一个异常罢了 */
private Singleton(String name) {
if(singleton != null) {
throw new RuntimeException("这可是个单例啊!");
}
this.name = name;
}
/** 提供一个工厂方法来获取单例的实例对象 */
public static Singleton getInstance() {
return singleton;
}
/** 对单例的重新赋值, 通过synchronized保证线程安全 */
public static synchronized Singleton reset(String name){
//赋值为null
singleton = null;
//重新赋值
singleton = new Singleton(name);
//获取新值
return getInstance();
}
/** 获取name */
public String getName() {
return name;
}
}
聪明的你已经发现了,去掉final后,我在静态代码块里增加了一段新的代码:
Reflection.registerFieldsToFilter(Singleton.class, "name");
在这里我想对不熟悉Reflection
这个类的人卖个关子,我会在我后续的帖子中来聊聊这个类。但是在现在,我只告诉你这段代码的最终结果:
他会将Singleton.class
类中的name
字段注册到反射过滤器,注册完成后在你使用反射获取类中字段、方法的时候便不会获取到你注册过后的字段或方法。于是便相当于从反射手中保护了这些字段和方法。
有兴趣的朋友可以试试通过反射去获取
System
类中的classLoader
这个字段
更难的难题
首先先说一下,这个标题下的难题大部分我已经无力解决了…如果有谁能够解决下面将会提到的问题中我没有给出解决方案的问题,欢迎评论或者偷偷告诉我~
问题如下:
-
如果我想要用懒汉式该怎么办呢?
如果想要用懒汉式的话,只能通过使用
Reflection
反射保护来实现了,final是无法后期赋值的。 -
利用枚举不也可以直接实现单例吗?
从 普通层面 上来讲的话,是可以的。但是从 我就是硬要杠你! 的角度来讲的话,通过反射去破坏枚举类型的单例的难易度,远远小于去绕过
Reflection
对反射字段、方法的过滤和修改final字段的值的难度(或者说繁琐度)。 -
第一个单例中,假如我使用Unsafe类和对class的深度反射拆解方法步骤去破坏final字段的值,该怎么办呢?
这个问题我暂时是没有解决方案的。既然你都祭出了通过底层原理强行修改final字段值的方法,我是没有什么保护措施的。
但是通过我个人的测试经验来讲,final
字段的值在被修改后(我通过debug模式确认了字段的值已经被修改),在程序的执行过程中依旧是你这个final
字段修改前的值。原因或许是因为字段内存地址的分配原则导致,也有可能是其他原因导致,这我不得而知,但是我知道的是当你通过底层代码修改了final
字段的之后,有很大可能在程序执行过程中已经是使用的他原本的值。 -
第二个单例中,假如我在Reflection注册字段之前去利用反射获取字段或者通过对class的深度反射拆解方法步骤来绕过
Reflection
对字段获取的过滤,该怎么办呢?这个我也没辙啦。这样通过绕过字段过滤或者在注册前就先提前拿到字段对象的行为我是暂时没有想到办法避免的,除非你把你的代码写进java源码中,做到jvm一启动,你的反射保护就被注册这样。
结束啦
辛辛苦苦写完了这篇帖子,如果你喜欢的话请点赞收藏投硬币 评个论,收个藏,关个注之类的~
以上帖子来源于个人经验,如要转载请注明来源出处。我一般喜欢用ForeScarlet
这个名字o( ̄▽ ̄)ブ
如果你有什么更好的方法、更加便捷的操作,可以从评论或者私信指教我;
如果你发现了我有什么地方说的不对,请指正我;
如果你只是看我不爽想骂我,请别骂的太难听。
感谢您百忙中阅读我的文章~