单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
说白了就是在内存中,该类只存在一个实体对象!主要解决类的频繁地创建与销毁!
类一旦加载,就把单例初始化完成。即在加载类时就创建实例,需要用的时候直接拿来用
public class HungryDemo {
// 1. 直接初始化对象,分配内存
private static HungryDemo demo = new HungryDemo();
// 单例模式都是私用构造:确保只有一个对象
private HungryDemo(){}
private static HungryDemo getInstance(){
return demo;
}
public static void main(String[] args) {
// 2.通过 getInstance() 方法获取该类的唯一实例对象
HungryDemo demo1 = HungryDemo.getInstance();
HungryDemo demo2 = HungryDemo.getInstance();
System.out.println(demo1);
System.out.println(demo2);
}
}
执行结果:实例对象是同一个!
总结:执行效率高,但浪费内存!因为类加载就初始化了实例对象,分配了内存空间,如果没有使用此类,就会造成空间的浪费
——因为饿汉式单例模式,会造成空间浪费,所以延伸了懒汉式单例模式
延时加载。比较懒,只有当调用getInstance的时候,才初始化这个单例。
public class LazyDemo {
private static LazyDemo demo ;
private LazyDemo(){
// 3. 打印当前线程,检查对象创建情况
System.out.println(Thread.currentThread().getName());
}
private static LazyDemo getInstance(){
// 1.判断对象是否为空,如果空,则进行第一次初始化
if(demo == null){
demo = new LazyDemo();
}
return demo;
}
public static void main(String[] args) {
// 2. 模拟10个线程同时调用getInstance获取单例对象
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyDemo.getInstance();
}).start();
}
}
}
执行结果:每次执行结果不一致,存在多个线程获取到不是同一个实例对象,因此,在多线程下,懒汉式的单例模式是线程不安全的!
要想解决懒汉式在多线程下保证线程安全,可以使用synchronized加锁:
// 1.在方法上加锁
private synchronized static LazyDemo getInstance(){
if(demo == null){
demo = new LazyDemo();
}
return demo;
}
执行结果:只有一个线程能够进入初始化操作方法,加锁后确保了多线程下保证线程安全
这样虽然解决了问题,但因为用到了synchronized
,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,即可以放在代码块中,从而降低锁的粒度。
如下:
private static LazyDemo getInstance(){
// 1. 第一次检查(非同步)
if(demo == null){
synchronized(LazyDemo.class){
// 2. 第二次检查(同步)
if(demo == null){
demo = new LazyDemo();
}
}
}
return demo;
}
执行出来的结果看着像是线程安全的,只有一个线程能够进入初始化对象的方法
——为什么要检查2次对象判空判断?
答:双重检查锁(DCL:Double Check Lock)。先判断对象是否已被初始化,再决定是否加锁。如果多个线程同时通过了第一次检查,其中线程A首先通过了第二次检查并实例化了对象,释放了锁,其他通过了第一次检查的线程获得锁后,因为有第二次的检查(线程A已初始化了对象使其不为null),故不会继续执行实例化!
▶思考:为什么上述我说执行结果看着是线程安全,双重检查就一定是线程安全的吗?
答:不一定!双重检查锁同时体现了同步中的独占性与可见性同等的重要性。上述代码中只展现出了独占性,在极端的情况下,双重检查锁还是存在非线程安全的,学习过并发编程的同学可能已经看出了猫腻了:
demo = new LazyDemo();
上述初始化实例这行代码,并非是一个原子操作,实际上它分为三步操作:
正常执行步骤是按照上述:123顺序执行,因为存在编译器优化导致指令重排序的现象,就会导致实际执行的顺序与编写代码的顺序不一致情况。
比如线程A先进来执行,经过指令重排执行的顺序为132,线程A执行完13步骤时(2还未执行),此时线程B拿到执行权,执行到第一次检查 if 对象判空时(注意:在线程A没执行完释放锁之前,线程B是无法进入到synchronized同步块中的),因线程A已完成内存的分配(未初始化),故线程B不满足进入if ,而是直接return返回了demo对象,线程B最终拿到的demo对象是没有进行初始化操作的,即空对象!
针对上述举例说明的问题,我们可以通过Java关键字volatile修饰,从而可以禁止指令重排现象!
// 增加volatile
private volatile static LazyDemo demo ;
注意:我在上篇文章中讲解 Java内存模型中详细介绍过了volatile,感兴趣的同学可翻阅!
注:同步块能够保证多个线程有序的(即同步)执行块中的代码,但并不能避免块中的代码发生重排序。
单例的实现,不仅是懒汉和饿汉式,还可以通过静态内部类来实现,如下:
public class HolderDemo {
private HolderDemo(){}
private static HolderDemo getInstance(){
return InnerClass.demo;
}
// 定义静态内部类,返回对象实体
private static class InnerClass{
private static HolderDemo demo = new HolderDemo();
}
}
单例模式虽然增加了DCL双重检查,能够避免多线程下单例模式的唯一性!但是学过反射的同学可能知道,反射也是可以破坏单例模式的!
代码示例:
public class LazyDemo {
private volatile static LazyDemo demo ;
// 私有无参构造
private LazyDemo(){
}
// DCL双重检查
private static LazyDemo getInstance(){
// 1. 第一次检查(非同步)
if(demo == null){
synchronized(LazyDemo.class){
// 2. 第二次检查(同步)
if(demo == null){
demo = new LazyDemo();
}
}
}
return demo;
}
// 反射破坏
public static void main(String[] args) throws Exception {
// 1.通过getInstance()方法获取
LazyDemo demo1 = LazyDemo.getInstance();
// 反射获取私有的无参构造函数
Constructor constructor = LazyDemo.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 跳过权限检查
// 2.通过反射调用构造函数初始化一个对象
LazyDemo demo2 = constructor.newInstance();
System.out.println(demo1);
System.out.println(demo2);
}
」
执行结果:一个通过反射调用构造函数生成的,一个是getInstance()提供的,产生2个不同实例!
结论:一旦类能够被反射获取,那么就可以对类进行随意操作!就无法保证单例模式的安全!即便加上了DCL双重检查,还是无法避免道高一尺魔高一丈!
——那么如何解决呢?
DCL中我们通过synchronized类锁的方式达到同步机制的,锁的是class对象,在内存中只有一个class,所以我们可以在构造函数中也加上锁!
private LazyDemo(){
// 加锁,判断对象是否为空,如果不为空,抛出异常!
synchronized(LazyDemo.class){
if(demo != null){
throw new RuntimeException("禁止使用反射破坏单例!");
}
}
}
执行结果:多次初始化实例将抛出异常,双重检测升级为三重检测,一定程度上避免反射破坏
代码示例:
public class LazyDemo {
private volatile static LazyDemo demo ;
// 私有构造
private LazyDemo(){
// 加锁,判断对象是否为空,如果不为空,抛出异常!
synchronized(LazyDemo.class){
if(demo != null){
throw new RuntimeException("禁止使用反射破坏单例!");
}
}
}
// DCL双重检查
private static LazyDemo getInstance(){
...
}
// 反射破坏
public static void main(String[] args) throws Exception {
// 1. 通过反射获取私有的无参构造函数
Constructor constructor = LazyDemo.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 跳过权限检查
// 2.通过反射调用构造函数初始化2个对象,demo1、demo2
LazyDemo demo1 = constructor.newInstance();
LazyDemo demo2 = constructor.newInstance();
System.out.println(demo1);
System.out.println(demo2);
}
」
执行结果:通过反射构造2个实例对象,结果实例对象不是同一个!也破坏了单例模式的唯一性!
解决:我们在构造函数中定义一个中间变量,来判断调用构造函数的次数 ,达到一次调用目的!
// 1.使用中间变量限制多次调用构造函数
private static boolean isTransfer = false;
private LazyDemo(){
synchronized(LazyDemo.class){
// false 说明是第一次调用
if(isTransfer == false){
isTransfer=true;
// true 说明是第二次调用(实例已经存在了),抛出异常
}else{
throw new RuntimeException("禁止使用反射破坏单例!");
}
}
}
执行结果:通过中间变量,也能达到构造函数限制被调用一次,只初始化一个实例的目的!
通过中间变量就能够完美解决反射对单例模式的破坏吗???? 你错了!私有的构造函数能够通过反射获取,难道私有的成员变量就不能通过反射获取吗!
代码如下:
public static void main(String[] args) throws Exception {
// 1. 通过反射获取私有的无参构造函数
Constructor constructor = LazyDemo.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 跳过权限检查
// 2.通过反射调用构造函数初始化2个对象
LazyDemo demo1 = constructor.newInstance();
// 已知demo1初始化执行了newInstance(),私有参数isTransfer 会由false变为true
// 3.通过反射获取私有成员变量 isTransfer
Field field = LazyDemo.class.getDeclaredField("isTransfer");
field.setAccessible(true);
// 我们把isTransfer参数的值,重新设置为false,这样demo2的初始化就能够成功!
field.set(demo1,false);
LazyDemo demo2 = constructor.newInstance();
System.out.println(demo1);
System.out.println(demo2);
}
执行结果:是的!你没有看错!初始化了2个实例对象,单例模式还是被破坏了!私有变量也可以通过反射获取,并且可以随意修改值
通过上述代码可以得知,反射初始化对象,实际上是调用了newInstance() 方法,来看下其源码:
从源码上分析,红框标出来的部分:不能通过反射破坏枚举类!
也就是说,如果你的类是一个枚举类,那么就无法通过反射获取此类,更无法破坏!
1、定义枚举类
public enum EnumDemo {
INSTANCE;
private static EnumDemo getInstance(){
return INSTANCE;
}
}
2、定义测试类
通过定义一个外部类,来测试枚举类是否能够通过反射获取到
class Test{
public static void main(String[] args) throws Exception {
// 1.通过枚举获取实体
EnumDemo demo1 = EnumDemo.INSTANCE;
// 通过反射获取私有的无参构造函数
Constructor constructor = EnumDemo.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
// 2.通过反射获取实体
EnumDemo demo2 = constructor.newInstance();
System.out.println(demo1);
System.out.println(demo2);
}
}
运行结果:提示没有找到此方法????
分析大法:
根据我们上述查看的 newInstance() 源码,如果通过反射获取枚举类,理应抛出:Cannot reflectively create enum objects 错误! 但是这里却报的是没有找到该构造方法
通过target/classes 文件夹下,查看生成的class文件,或者通过 javap -p xxx.class 查看:
结论:通过上述字节码文件可以看出,该类是有无参构造函数的 !! ??
字节码文件中显示的是有构造函数的,但是进行反射的时候,为什么还提示我们没有找到这个方法?接下来继续使用反编译查看java源码:
在线Java反编译:Java decompiler online
——使用简单,直接将 jar 包和 class 文件拖到页面即可。(注:这里使用的是 jad方式)
得到的反编译的.java源码如下:
哦吼!通过源码可看到EnumDemo继承了Enum枚举类,并且实现了一个有参数的构造函数,并不是class文件看到的 无参构造函数!!(看来万恶的根源在源头,可恶!!)
OK!我们已经知晓了构造函数是有参数的,那我们继续修改上述通过反射获取的构造函数中的代码,在方法里传入两个类型即可: getDeclaredConstructor(String.class,int.class)
// 通过反射获取私有的有参构造函数
Constructor constructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
此时我们再来看一下反射的结果:get ! 触发了无法通过反射获取枚举类的错误!
反射的存在也会单例模式的不安全!如果需要保证单例模式的唯一性,可以使用枚举类!