目前据我了解的单例模式有如下几种:
1.懒汉式
2.饿汉式
3.基于静态内部类实现
4.枚举
基于饿汉式实现单例模式
package com.wiggin.singleton; @SuppressWarnings("all") public class Singleton1 { private Singleton1() { } private static final Singleton1 INSTANCE = new Singleton1(); public static Singleton1 getInstance() { return INSTANCE; } }
package com.wiggin.singleton; @SuppressWarnings("all") public class Singleton6 { private Singleton6() { } private static final Singleton6 INSTANCE; static { INSTANCE = new Singleton6(); } public static Singleton6 getInstance() { return INSTANCE; } }
饿汉式单例模式是天生线程安全的。类加载过程的初始化阶段,jvm会加锁执行
基于懒汉式的单例模式
懒汉式v1.0
package com.wiggin.singleton; @SuppressWarnings("all") public class Singleton2 { private Singleton2() { } private static Singleton2 instance; public static Singleton2 getInstance() { if (instance == null) { //若instance为null,则创建对象并赋值给instance变量 instance = new Singleton2(); } return instance; } }
懒汉式v1.0是线程不安全的。若在多线程的环境下,可能会有多个线程进入getInstance()的if判断逻辑里面去。那么就会多次new 对象,并且返回给各自的线程。所以,有可能出现不同线程调用getInstance()方法获取到的instance不是同一个对象。
懒汉式v2.0
package com.wiggin.singleton; public class Singleton3 { private Singleton3() { } private static Singleton3 instance; public static synchronized Singleton3 getInstance() { if (instance == null) { instance = new Singleton3(); } return instance; } }
懒汉式v2.0在getInstance()方法上加上了synchronized关键字,保证执行getInstance()方法时串行执行的。那么,多次调用getInstance()主要目的是为了获取到instance,而不是去new 对象。new对象是第一个抢到synchronized锁的线程已经完成。那么,后面的线程仍然需要争抢synchronized锁,若抢不到就会进入阻塞状态。明明,后面的线程只需要读取instance,不存在线程安全问题。那么,这样做效率会不会太低了。那么就提出了双重检验锁(Double Check Lock),即后面的懒汉式v3.0
package com.wiggin.singleton; @SuppressWarnings("all") public class Singleton4 { private Singleton4() { } private static volatile Singleton4 instance; public static Singleton4 getInstance() { if (instance == null) { //若instance为null,才会进到同步代码块. synchronized (Singleton4.class) { if (instance == null) { //还需判断一次instance是否为null,因为高并发场景下,很可能有多个线程进入第一个if判断,然后同步等待synchronized锁 instance = new Singleton4(); } } } return instance; } }
懒汉式v3.0加入了双重检验锁,那么只有前期进入第一个if判断逻辑的几个线程会阻塞等待串行执行,等到instance被赋值之后,instance就不为null,那么就进不去第一个if判断逻辑,那么就不会去争抢synchronized锁,而是直接return,那样在高并发场景下,效率比懒汉式v2.0高出不少。
那么为什么还要加上volatile关键字呢?
volatile关键字的作用:1.保证可见性 2.防止指令重排序
可见性问题的提出(简单谈,跟懒汉式无关):Java 内存模型即JMM,提出工作内存和主内存两个概念。每个线程 *** 作的各自的工作内存。首先,会将主内存的数据加载到工作内存中,线程直接 *** 作的是工作内存,那么线程修改该变量也是直接在工作内存中修改,然后再同步回去主内存。那么,另一个线程 *** 作的工作内存中的副本,没有重新从主内存中重新读取新值,那么就存在可见性问题,jvm层面使用内存屏障解决的,汇编层面使用lock前缀指令解决的。从硬件角度分析,就是现在的cpu都是多核的,不同线程可能跑在不同的cpu核心上,每个cpu都有对应的高速缓存(主要是解决cpu和内存读写速度差距过大问题)。cpu从内存读数据,是先加载到cpu缓存中,最后寄存器再从cpu缓存中取数据给cpu执行。那么,修改数据也是先修改cpu缓存,然后再刷回内存。那么,如果其他cpu核心的缓存没有失效,那么是优先读取缓存中的数据的,即不能及时去读取内存中最新的值,就存在可见性问题。那么硬件层面就是通过MESI缓存一致性协议解决的。
上面扯远了,跟懒汉式没什么关系。如果感兴趣,可以参考并发编程相关书籍和博客。后期,也打算写并发编程相关的博客,有volatile,synchronized锁升级,并发工具类源码等...
回归正题,这里加volatile主要是为了解决重排序问题。
因为cpu和编译器(这里指的不是javac前端编译器,这里指的是后端编译器)优化,导致cpu所执行的指令顺序和原先不一致。这里就需要使用volatile关键字去保证有序性。jvm层面也是使用内存屏障保证,汇编层面是使用lock前缀指令保证的。
基于静态内部类实现单例模式
package com.wiggin.singleton; @SuppressWarnings("all") public class Singleton5 { private Singleton5() { } public static Singleton5 getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final Singleton5 INSTANCE = new Singleton5(); } }
基于枚举实现单例模式
package com.wiggin.singleton; public enum Singleton7 { INSTANCE; }
下面就谈到如何破解单例模式
破解方式:1.反射 2.序列化和反序列
反射破解单例模式
package com.wiggin.singleton; import java.io.Serializable; @SuppressWarnings("all") public class Singleton8 { private Singleton8() { } private static final Singleton8 INSTANCE = new Singleton8(); public static Singleton8 getInstance() { return INSTANCE; } }
反射暴力破解代码实现:
package com.wiggin.test; import com.wiggin.singleton.Singleton8; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @SuppressWarnings("all") public class Test1 { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { //获取到单例对象对应Class对象 Classclazz = Singleton8.class; //获取到构造器 Constructor constructor = clazz.getDeclaredConstructor(); //因为是private,需要设置权限 constructor.setAccessible(true); //创建对象 Singleton8 instance = constructor.newInstance(); //判断instance和Singleton1.getInstance()是否相等 System.out.println(instance == Singleton8.getInstance()); //false } }
运行结果:
序列化破解单例模式:
注意:序列化需要实现Serializable,若单例对象对应的类没有实现Serializable接口,那么也不需要考虑被序列化破解的问题。
package com.wiggin.singleton; import java.io.Serializable; @SuppressWarnings("all") public class Singleton8 implements Serializable { private Singleton8() { } private static final Singleton8 INSTANCE = new Singleton8(); public static Singleton8 getInstance() { return INSTANCE; } }
序列化破解单例模式的代码实现:
package com.wiggin.test; import com.wiggin.singleton.Singleton8; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @SuppressWarnings("all") public class Test2 { public static void main(String[] args) throws Exception { //1.将单例对象序列化并存入硬盘,注意Singleton1要实现Serializable接口 FileOutputStream fos = new FileOutputStream("f:\code\abc.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); Singleton8 instance = Singleton8.getInstance(); oos.writeObject(instance); oos.close(); fos.close(); //2.从硬盘反序列化对象到内存 FileInputStream fis = new FileInputStream("f:\code\abc.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton8 instance1 = (Singleton8) ois.readObject(); System.out.println(instance == instance1); //false } }
运行结果:
上面谈到的单例模式被破解,那么有没有对应的解决方案来防止被破解呢?
1.针对反射暴力破解的解决方案:
反射无非就是获取到私有化的空参构造器来创建对象,那么可以在该私有化的空参构造函数动点手脚。
防止被反射破解v1.0
package com.wiggin.singleton.update; @SuppressWarnings("all") public class Singleton8 { private static Singleton8 instance; private Singleton8() { if (instance != null) { //若instance不为null,那么就不给通过该构造器去new对象 throw new RuntimeException("已经创建好单例对象啦,不要重复创建!!!"); } } public static Singleton8 getInstance() { if (instance == null) { synchronized (Singleton8.class) { if (instance == null) { instance = new Singleton8(); } } } return instance; } }
若instance不为null时,调用该空参构造器直接抛出异常。看上去好像可以喔。那么试一下,反射能不能破解先。
反射暴力破解的代码实现:
package com.wiggin.test; import com.wiggin.singleton.Singleton8; import java.lang.reflect.Constructor; @SuppressWarnings("all") public class Test3 { public static void main(String[] args) throws Exception { //获取到单例对象对应Class对象 Classclazz = Singleton8.class; //获取到构造器 Constructor constructor = clazz.getDeclaredConstructor(); //因为是private,需要设置权限 constructor.setAccessible(true); //创建对象 Singleton8 instance = constructor.newInstance(); //判断instance和Singleton1.getInstance()是否相等 System.out.println(instance == Singleton8.getInstance()); //false } }
运行结果:
为什么还是false?
原来是这两行代码的问题。
在没有调用getInstance()前,一直用反射创建对象,instance一直为null,那么就可以一直创建成功。太可恶了!!!
那么有没有相关的解决方案呢?
那肯定是有的。那就是假如先调用反射创建对象,再调用getInstance(),那么可以直接用反射创建的对象给instance赋值,那么instance就有值了,之后用反射就不能创建对象了。
具体看下面的代码
防止反射暴力破解v2.0
package com.wiggin.singleton.update; @SuppressWarnings("all") public class Singleton9 { private Singleton9() { if (instance != null) { //若instance不为null,那么就不给通过该构造器去new对象 throw new RuntimeException("已经创建好单例对象啦,不要重复创建!!!"); } else { //instance为null,则直接用反射创建的对象提前给instance赋值即可解决 instance = this; } } private static Singleton9 instance; public static Singleton9 getInstance() { if (instance == null) { synchronized (Singleton9.class) { if (instance == null) { instance = new Singleton9(); } } } return instance; } }
那么,反射暴力破解问题解决了。那么,序列化和反序列该如何解决呢?
下面我们通过源码分析,分析如何解决序列化问题。
单例模式代码:
package com.wiggin.singleton.update; import java.io.Serializable; @SuppressWarnings("all") public class Singleton8 implements Serializable { private static Singleton8 instance; private Singleton8() { if (instance != null) { //若instance不为null,那么就不给通过该构造器去new对象 throw new RuntimeException("已经创建好单例对象啦,不要重复创建!!!"); } System.out.println("通过构造器创建对象"); } public static Singleton8 getInstance() { if (instance == null) { synchronized (Singleton8.class) { if (instance == null) { instance = new Singleton8(); } } } return instance; } }
序列化破解单例代码:
package com.wiggin.test; import com.wiggin.singleton.update.Singleton8; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @SuppressWarnings("all") public class Test4 { public static void main(String[] args) throws Exception { //1.将单例对象序列化并存入硬盘,注意Singleton1要实现Serializable接口 FileOutputStream fos = new FileOutputStream("f:\code\abc.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); Singleton8 instance = Singleton8.getInstance(); oos.writeObject(instance); oos.close(); fos.close(); //2.从硬盘反序列化对象到内存 FileInputStream fis = new FileInputStream("f:\code\abc.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton8 instance1 = (Singleton8) ois.readObject(); System.out.println(instance == instance1); //false } }
运行结果:
注意:如果序列化创建对象是走的是Singleton8空参构造方法的话,会打印通过构造器创建对象这一句话。第一句通过构造器创建对象是调用getInstance(),getInstance()通过new 对象产生的。所以说,序列化创建对象是不走Singleton8的空参构造器去创建对象。
下面我们就通过源码来搞清楚究竟是为什么?
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
第六步执行完之后,会回到第五步那个位置蓝色区域的哪一行代码
那么说,可能是调用父类Object的空参构造器去创建Singleton8的实例,没有走Singleton8的构造方法。那么,我们去给它换个父类,试试看是不是调到了父类的public的空参构造函数。
代码如下:
package com.wiggin.singleton.update; public class Father { public Father() { System.out.println("调用了Singleton8父类Father的空参构造函数"); } }
package com.wiggin.singleton.update; import java.io.Serializable; @SuppressWarnings("all") public class Singleton8 extends Father implements Serializable { private static Singleton8 instance; private Singleton8() { if (instance != null) { //若instance不为null,那么就不给通过该构造器去new对象 throw new RuntimeException("已经创建好单例对象啦,不要重复创建!!!"); } System.out.println("通过构造器创建对象"); } public static Singleton8 getInstance() { if (instance == null) { synchronized (Singleton8.class) { if (instance == null) { instance = new Singleton8(); } } } return instance; } }
再来debug调试一下
在Father类的空参构造函数中打个断点,等一下看一下它会不会走到这里来
再往下执行
看到ObjectInputStream的readOrdinaryObject(boolean unshared)方法,走到2217行
再点一下向下箭头的按钮就到2236行
再往下走就到达了ObjectInputStream的readObject0(Class> type, boolean unshared)的这行代码
点进去看
然后向下走就回到了
继续向下走
向下走
再向下走就main函数了
总结:反序列过程中,通过父类Father类的public 空参构造器创建了Singleton8的对象。其中,有一个过程会回调Singleton8的Object readResolve()方法(如果Singleton8有该方法的话)。
那个方法有什么用呢?
我们继续看到哪里的源码。
那么说,我们在Singleton8上面添加Object readResolve(),让该方法的返回值为单例对象即可解决。
Singleton8单例实现的代码:
package com.wiggin.singleton.update; import java.io.Serializable; @SuppressWarnings("all") public class Singleton8 extends Father implements Serializable { private static Singleton8 instance; private Singleton8() { if (instance != null) { //若instance不为null,那么就不给通过该构造器去new对象 throw new RuntimeException("已经创建好单例对象啦,不要重复创建!!!"); } System.out.println("通过构造器创建对象"); } public static Singleton8 getInstance() { if (instance == null) { synchronized (Singleton8.class) { if (instance == null) { instance = new Singleton8(); } } } return instance; } public Object readResolve() { return instance; } }
测试代码:
package com.wiggin.test; import com.wiggin.singleton.update.Singleton8; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @SuppressWarnings("all") public class Test4 { public static void main(String[] args) throws Exception { //1.将单例对象序列化并存入硬盘,注意Singleton1要实现Serializable接口 FileOutputStream fos = new FileOutputStream("f:\code\abc.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); Singleton8 instance = Singleton8.getInstance(); oos.writeObject(instance); oos.close(); fos.close(); //2.从硬盘反序列化对象到内存 FileInputStream fis = new FileInputStream("f:\code\abc.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton8 instance1 = (Singleton8) ois.readObject(); System.out.println(instance == instance1); //true } }
运行结果:
运行结果为true,证明了我的源码分析没有问题。nice!!!
为什么《Effective Java》中强烈推荐使用枚举呢?
下面我们试一下用反射和序列化来破解枚举。
枚举类:
package com.wiggin.singleton; public enum Singleton7 { INSTANCE; }
反射破解枚举代码:
package com.wiggin.test; import com.wiggin.singleton.Singleton7; import com.wiggin.singleton.Singleton8; import java.lang.reflect.Constructor; public class Test6 { public static void main(String[] args) throws Exception { //获取到单例对象对应Class对象 Classclazz = Singleton7.class; //获取到构造器 Constructor constructor = clazz.getDeclaredConstructor(); //因为是private,需要设置权限 constructor.setAccessible(true); //创建对象 Singleton7 instance = constructor.newInstance(); //判断instance和Singleton1.getInstance()是否相等 System.out.println(instance == Singleton7.INSTANCE); } }
运行结果:
枚举没有空参方法,报的错
那么我们去看看枚举类Singleton7反编译的代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: Singleton7.java package com.wiggin.singleton; public final class Singleton7 extends Enum { public static Singleton7[] values() { return (Singleton7[])$VALUES.clone(); } public static Singleton7 valueOf(String name) { return (Singleton7)Enum.valueOf(com/wiggin/singleton/Singleton7, name); } private Singleton7(String s, int i) { super(s, i); } public static final Singleton7 INSTANCE; private static final Singleton7 $VALUES[]; static { INSTANCE = new Singleton7("INSTANCE", 0); $VALUES = (new Singleton7[] { INSTANCE }); } }
枚举这里也是使用饿汉式喔。Singleton7的构造函数是private Singleton7(String s, int i)
修改后的反射破解代码:
package com.wiggin.test; import com.wiggin.singleton.Singleton7; import com.wiggin.singleton.Singleton8; import java.lang.reflect.Constructor; public class Test6 { public static void main(String[] args) throws Exception { //获取到单例对象对应Class对象 Classclazz = Singleton7.class; //获取到构造器 Singleton7的构造器是这样的 private Singleton7(String s, int i) Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class); //因为是private,需要设置权限 constructor.setAccessible(true); //创建对象 Singleton7 instance = constructor.newInstance(); //判断instance和Singleton1.getInstance()是否相等 System.out.println(instance == Singleton7.INSTANCE); } }
运行结果:
那么反射不中用,那么我们用序列化和反序列化搞点事情。
序列化和反序列化破解代码:
package com.wiggin.test; import com.wiggin.singleton.Singleton7; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @SuppressWarnings("all") public class Test7 { public static void main(String[] args) throws Exception { //1.将单例对象序列化并存入硬盘,注意Singleton1要实现Serializable接口 FileOutputStream fos = new FileOutputStream("f:\code\abc.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); Singleton7 instance = Singleton7.INSTANCE; oos.writeObject(instance); oos.close(); fos.close(); //2.从硬盘反序列化对象到内存 FileInputStream fis = new FileInputStream("f:\code\abc.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton7 instance1 = (Singleton7) ois.readObject(); System.out.println(instance == instance1); //true } }
运行结果:
序列化和反序列也不中用呀。所以,说枚举是最安全的,《Effective Java》最推荐的方式。
具体枚举为什么序列化也不行,是因为它底层调用了Enum的这个方法。代码截图如下:
然后我们平时常用的枚举类对象.valueOf(String name),是不是返回name对应相同的对象。
那么Singleton7的valueOf(String name)里面调用了父类Enum的
那么,你说返回的是不是相同的枚举类对象。关于枚举用序列化和反序列为什么不能被破解的源码分析,我就不写了。感兴趣的,可以自行看源码,debug源码研究。跟上面我的流程差不多的。底层实现原理,我也跟你讲的明明白白了。今天,就搞到这里吧。伟子哥的小迷弟写了一下午,写的好累,存手敲2w+字,外加源码调试,有点累了。
此次分析单例模式和破解单例模式,还有从源码角度分析单例破解解决方案。觉得很硬核,记得点赞喔。后续会出更多硬核的文章!!!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)