彻底玩转单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,构造器私有是其重要的特点之一。
主要有饿汉模式,DCL懒汉式,内部类,枚举等实现方式!
1、饿汉式
//饿汉式单例
class Hungry{
//可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
2、DCL懒汉式
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+" ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式原子性操作的懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan(); /*不是一个原子性操作 指令可能重排 123,132此时lazyMan还没有完成构造 volatile*/
/*
1、类的加载,jvm方法区是否有类信息;
2、分配内存空间(堆);
3、初始化零值,将分配的空间初始化;
4、设置对象头(对象结构);
5、执行构造方法(程序员写的)
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
优点
- 实现了懒加载;
- 线程安全
- 锁粒度较细,只有第一次初始化的时候会走到synchronized部分
缺点 - 实现起来相对复杂,对于volatile的理解会比较的难
- 存在构造方式如果未设置private而导致反射实例化破坏单例的风险
3、静态内部类式
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
优点
- 实现了懒加载;
- 线程安全
- 没有锁,性能较好
缺点
- 实现及理解起来相对复杂
- 存在构造方式如果未设置private而导致反射实例化破坏单例的风险
4、反射破坏单例
静态内部类也会不安全,因为java中有个叫反射的东西,反射可以破坏单例。
破坏如下(同样用懒汉式的例子):
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance(); //此时对象已经创建
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //无视构造器的私有化
LazyMan instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
现在的程序已经比较健壮了,有了三层检测锁:1、synchronized (LazyMan.class) 2、原子性操作(避免原子重排) 3、synchronized (LazyMan.class),但是报错的地方是类的构造器里异常处理,若想绕过它也是可以的,因为在主方法里我们是先 LazyMan instance = LazyMan.getInstance(); 获得了实例对象,所以可以都直接通过反射来获取实例对象,同样可以破坏单例。
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance(); //绕过此时象的创建
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //无视构造器的私有化
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
此时单例又被破坏了,难道一个程序能疯狂被反射破坏吗,程序员也有他的办法,我们可以用红绿灯法则,设置一个标签位,当一个实例被初始化了,私有静态标签就改变,接下来再初始化一个就报错了。
private static boolean lisiqiang = false;
private LazyMan(){
synchronized (LazyMan.class){
if (lisiqiang == false){
lisiqiang = true;
}else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
是不是绝对现在的程序坚不可摧了?错,黑客可不是闹着完的,既然设定了一个私有的标签位,那我也可以把你的标签位也给破坏了
public static void main(String[] args) throws Exception {
Field lisiqiang = LazyMan.class.getDeclaredField("lisiqiang");
lisiqiang.setAccessible(true);// 去掉私有化
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //无视构造器的私有化
LazyMan instance1 = declaredConstructor.newInstance(); //这个可以正常实例化,但下一个实例将会被标签拦截,接下来我们把破坏了的标签重新赋值
lisiqiang.set(instance1,false); // 将第一个实例化标签产生的变化再改回来,此时就可以再实例一个对象了,单例又被破坏了
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
道高一尺魔高一丈,java的反射将java程序全部都破坏,不行 总要出一个制裁反射的,于是再JDK1.5中添加枚举,官方规定枚举不能被反射。那我偏要试试,枚举我也破了他的!!!
这个是枚举的定义方法,我不能得知构造器是无参还是有参的,那我们看编译后的源码再来进行修改
package single;
public enum EnumSingle {
INSTANCE; //这个是枚举的定义方法,我不能得知构造器是无参还是有参的,那我们看编译后的源码
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance1 = declaredConstructor.newInstance();
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
}
很显然是错误的,毕竟JDK自带的还是很健壮的,我们可以看看我们的java代码编译后的程序是否是我们从target看到的是无参构造的,进入当前文件夹的cmd,输入javap -p EnumSingle.class 查看反编译源码,发现也是无参的构造器,欸?都在骗我们,那我们找一个更加强大的反编译工具 jad.exe , 再次进入控制台输入 jad.exe -s java EnumSingle.class, 会产生一个java文件,查看,发现是一个有参的构造器,那我们再次修改一下反射工具。
果然这才是我们想要的结果,得出结论,枚举单例确实不能被反射破坏!!!