单例模式详解及使用反射破坏单例(每日很“刑”小技巧)

彻底玩转单例模式

单例模式(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文件,查看,发现是一个有参的构造器,那我们再次修改一下反射工具。
在这里插入图片描述
果然这才是我们想要的结果,得出结论,枚举单例确实不能被反射破坏!!!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值