单例模式与破解单例模式

单例模式与破解单例模式,第1张

单例模式与破解单例模式

目前据我了解的单例模式有如下几种:

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会加锁执行方法。所以,保证在静态代码块赋值和静态变量直接显式赋值是线程安全的。但是,假设该类实现了Serializable接口,那么有被序列化破解的风险。还有可以使用反射获取到其私有化构造器,直接通过反射去创建对象。所以,反射和序列化均可以破解该单例模式。

基于懒汉式的单例模式

懒汉式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对象
        Class clazz = 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对象
        Class clazz = 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对象
        Class clazz = 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对象
        Class clazz = 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的> T valueOf(Class enumType,String name)

那么,你说返回的是不是相同的枚举类对象。关于枚举用序列化和反序列为什么不能被破解的源码分析,我就不写了。感兴趣的,可以自行看源码,debug源码研究。跟上面我的流程差不多的。底层实现原理,我也跟你讲的明明白白了。今天,就搞到这里吧。伟子哥的小迷弟写了一下午,写的好累,存手敲2w+字,外加源码调试,有点累了。

此次分析单例模式和破解单例模式,还有从源码角度分析单例破解解决方案。觉得很硬核,记得点赞喔。后续会出更多硬核的文章!!! 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5686242.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存