欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦

程序员文章站 2022-04-13 12:05:36
...

什么是单例模式?
  单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式(如下面例子中的LazyMan01.getInstance),可以直接访问,不需要实例化该类的对象。

注意:

1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

意图在于保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决了一个全局使用的类频繁地创建与销毁的问题。

  面试要是说起单例模式,那肯定就会提到饿汉式和懒汉式.笔试手写的两种单例模式看代码:

package com.qiu.single;
//饿汉式单例
public class Hungry {
    //因为下面new的对象都是static静态的,所以饿汉式可能会浪费空间
    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(){//getInstance获取实例
        return hungry;
    }
}

因为new的对象是静态的会和类一起加载,所以用到数组的时候就会浪费空间.
所以就出来了一个懒汉模式
代码:

package com.qiu.single;

public class LazyMen01 {
    private  LazyMen01(){
        //私有的构造器
        
    }
    private static LazyMen01 lazyMen;
    
    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
        if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
            lazyMen=new LazyMen01();
        }
        return lazyMen;
    }
}

但是这个代码是有问题的,单线程下是可以的,但是如果在并发测试下呢?在多线程下呢?
实例代码:

package com.qiu.single;

import com.qiu.LazyMen;

public class LazyMen01 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
            LazyMen01.getInstance();
            }).start();
        }

    }
    private  LazyMen01(){
        //私有的构造器
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
        if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
            lazyMen=new LazyMen01();
        }
        return lazyMen;
    }

}

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦居然有五个线程!
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦而且每次的执行结果都不一样.所以我们应该要再想办法.这个时候就想到了要加一把锁但是存在在加锁之前就被线程拿到了,所以我们要做两次判断,加锁判断一次.先判断lazyMan01是否为空,如果为空,那就加锁.
相关代码:

package com.qiu.single;

import com.qiu.LazyMen;

public class LazyMen01 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
            LazyMen01.getInstance();
            }).start();
        }

    }
    private  LazyMen01(){
        //私有的构造器
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦

这种就叫双重检测锁模式的懒汉式单例(DCL懒汉式)
  但是:
这种代码还是有问题的,这里就要谈到一个关键词叫Volatile是不是很熟悉?
看下这几行代码:

 if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }

**lazyMen=new LazyMen01();**这行代码你觉得有没有问题呢?
答案是有的,因为这种new对象不是一个原子性操作.(关于原子性操作大家可以看我前面的博客有专门的解释过.这里不做深入探讨)
  我们先说下new对象的一个底层具体执行过程:
1.分配内存空间
2.执行构造方法
3.把这个对象指向这个空间

但是这样会发生一个指令重排的现象比如说:
我们期望的是执行123
但是他由于不具有原子性操作有可能执行顺序为132.假设多线程下A线程没有问题
但是B线程来了,A线程已经指向这个对象空间了,但是B还是认为lazyMen不为空了,
则B线程会直接走return lazyMen
此时这个lazyMan还没有完成构造,导致这个空间是虚无的,
所以必须要加上Volatile形成一个完整的双重检测锁:

private volatile static LazyMen01 lazyMen;

这样就能保证他不会被指令重排了.
这是面试时,必须要说到的点.
有能力的还能有更骚的操作哦.
比如说:静态内部类实现:
代码:

package com.qiu.single;
//静态内部类
public class Holder {
    private Holder(){

    }
    public static Holder getInstance(){
        return InnerClass.holder;
    }
    public  static class InnerClass{
        private static final Holder holder = new Holder();
    }
}

但是:
这样还是不安全的:JAVA中有一种类叫做反射只要有反射存在任何代码都会变得不安全.任何关键字都是纸老虎!!
就拿上面最完整的DCL单例来说
代码:

package com.qiu.single;

import com.qiu.LazyMen;

public class LazyMen01 {
    public class LazyMen01 {
    public static void main(String[] args) {
        LazyMen01 instance = LazyMen01.getInstance();
        LazyMen01 instance = LazyMen01.getInstance();
        
    }
    private  LazyMen01(){
        //私有的构造器
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

    LazyMen01 instance = LazyMen01.getInstance();
    LazyMen01 instance = LazyMen01.getInstance();

正常情况下上面两个实例是相等的.然后我们用反射来破坏这个单例

package com.qiu.single;

import com.qiu.LazyMen;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class LazyMen01 {
    public static void main(String[] args) throws Exception {
        LazyMen01 instance1 = LazyMen01.getInstance();
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题

        Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        LazyMen01 instance2 = declaredConstructor.newInstance();
        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
    private  LazyMen01(){
        //私有的构造器
        System.out.println(Thread.currentThread().getName()+":ok");
    }
    private volatile static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦按照单例的说法,他们两个应该是一样的才对,但是反射做到了破坏了单例.
然后我们就要想我们能不能去解决这种破坏呢?
思路.类只有一个,如果我们在类上加一把锁呢?

代码:

package com.qiu.single;

import com.qiu.LazyMen;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class LazyMen01 {
    public static void main(String[] args) throws Exception {
        LazyMen01 instance1 = LazyMen01.getInstance();
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题

        Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        LazyMen01 instance2 = declaredConstructor.newInstance();
        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
    private  LazyMen01(){//私有的构造器
        synchronized (LazyMen.class){
            if (lazyMen!=null){
                throw new RuntimeException("不要试图使用反射来破坏异常!");
            }
        }
        System.out.println(Thread.currentThread().getName()+":ok");
    }
    private volatile static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

由于反射是从无参构造器走的,所以我们在私有构造器上面加了一把锁.如果lazyMan不等于null,那说明已经创建了实例了.这时候抛出异常.这样就形成了三重检测了,避免了某一种反射的破坏.

synchronized (LazyMen01.class){
            if (lazyMen!=null){
                throw new RuntimeException("不要试图使用反射来破坏异常!");
            }
        }

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦
貌似这种反射破坏解决了,但是人总是聪明的**,魔高一尺道高一丈**.
假设两个对象都是用反射来new的呢?

package com.qiu.single;

import com.qiu.LazyMen;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class LazyMen01 {
    public static void main(String[] args) throws Exception {
        //LazyMen01 instance1 = LazyMen01.getInstance();
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题

        Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        LazyMen01 instance1 = declaredConstructor.newInstance();
        LazyMen01 instance2 = declaredConstructor.newInstance();

        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
    private  LazyMen01(){//私有的构造器
        synchronized (LazyMen01.class){
            if (lazyMen!=null){
                throw new RuntimeException("不要试图使用反射来破坏异常!");
            }
        }
        System.out.println(Thread.currentThread().getName()+":ok");
    }
    private volatile static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

 LazyMen01 instance1 = declaredConstructor.newInstance();
        LazyMen01 instance2 = declaredConstructor.newInstance();

        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());

就是说两个对象都是用反射new出来的,那么结果怎么样了呢?
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦
居然还是出来了两个实例.这样单例模式又被破坏了
再解决:
在任何情况下,可以通过非当前类对象.设置一个信号值,比如说现在定义一个变量,将变量名字设置的十分复杂.

private static  boolean qiuzhikang =false;

意思就是在synchronized同步的时候加一个判断,如果最开始时变量qiuzhikang为false.那么执行判断后,变量qiuzhikang就变为了true.
否则就抛出异常.

package com.qiu.single;

import com.qiu.LazyMen;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class LazyMen01 {
    private static boolean qiuzhikang = false;

    public static void main(String[] args) throws Exception {
        //LazyMen01 instance1 = LazyMen01.getInstance();
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题

        Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        LazyMen01 instance1 = declaredConstructor.newInstance();
        LazyMen01 instance2 = declaredConstructor.newInstance();

        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }

    private LazyMen01() {//私有的构造器
        synchronized (LazyMen01.class) {
            if (qiuzhikang == false) {
                qiuzhikang = true;
            } else
                throw new RuntimeException("不要试图使用反射来破坏异常!");
        }
        System.out.println(Thread.currentThread().getName() + ":ok");
    }
    
    private volatile static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

改变的代码:

 private LazyMen01() {//私有的构造器
        synchronized (LazyMen01.class) {
            if (qiuzhikang == false) {
                qiuzhikang = true;
            } else
                throw new RuntimeException("不要试图使用反射来破坏异常!");
        }

来看看结果:
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦确实是解决了问题.
如果说别有用心的人不通过反编译的情况下,他是找不到我们设置的这个关键字的.如果关键字经过加密处理就更安全了.
但是:
再厉害的加密也会有人解密!
方法:改变你的变量值
假设我找到了你设置的隐藏的变量.找到之后,拿到变量就可以搞破坏了,具体实现过程看代码.

package com.qiu.single;

import com.qiu.LazyMen;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class LazyMen01 {
    private static boolean qiuzhikang = false;

    public static void main(String[] args) throws Exception {
        //LazyMen01 instance1 = LazyMen01.getInstance();
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
        Field qiuzhikang = LazyMen01.class.getDeclaredField("qiuzhikang");
        qiuzhikang.setAccessible(true);
        Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        LazyMen01 instance1 = declaredConstructor.newInstance();
        qiuzhikang.set(instance1,false);
        LazyMen01 instance2 = declaredConstructor.newInstance();
        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }

    private LazyMen01() {//私有的构造器
        synchronized (LazyMen01.class) {
            if (qiuzhikang == false) {
                qiuzhikang = true;
            } else
                throw new RuntimeException("不要试图使用反射来破坏异常!");
        }
        System.out.println(Thread.currentThread().getName() + ":ok");
    }

    private volatile static LazyMen01 lazyMen;

    private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的

        if (lazyMen==null){
            synchronized (LazyMen01.class){
                if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
                    lazyMen=new LazyMen01();
                }
            }
        }
        return lazyMen;
    }

}

改变的代码:

 Field qiuzhikang = LazyMen01.class.getDeclaredField("qiuzhikang");
        qiuzhikang.setAccessible(true);
  qiuzhikang.set(instance1,false);

就是将信号量重新改成false.这样单例又又又被破坏了.

道高一尺,魔高一丈

那到底怎么解决反射呢?
现在我们只能通过源码了,点newinstance进入;
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦枚举是JDK1.5出来的,自带单例模式.
代码:

package com.qiu.single;

//enum是一个什么?本身也就是一个Class类
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class  test{
    public static void main(String[] args) {
        EnumSingle instance1= EnumSingle.INSTANCE;
        EnumSingle instance2= EnumSingle.INSTANCE;

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

源码说反射是不能破坏枚举的.那我们来尝试一下.
首先分析源码里面是有参构造还是无参构造.
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦

package com.qiu.single;

import java.lang.reflect.Constructor;

//enum是一个什么?本身也就是一个Class类
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class  test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1= EnumSingle.INSTANCE;


        System.out.println(instance1);
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        
        //NoSuchMethodException: com.qiu.single.EnumSingle.<init>()
        System.out.println(instance2);
    }
}

一番操作之后发现:

NoSuchMethodException: com.qiu.single.EnumSingle.<init>()

彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦但是这与我们源码中的不同.

throw new IllegalArgumentException("Cannot reflectively create enum objects");

探究失败!!!
不服气!再来!!Idea是个大骗子.
通过CMD指令javap -p EnumSingle.class
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦

还是有空参构造器
这是时候我们就要找到一个更专业的工具!jad
将EnumSingle.class文件反编译成java文件.
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦
转换完成.
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦枚举类型的最终反编译.
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦
这里用的不是无参构造,而是一个有参构造器!!!
好的,知道有参之后,改代码.

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);

改成了:

 Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);

看结果:
彻底玩转java单例模式(深究原理)万恶之源的反射有用?面试可以玩转面试官哦
确实证明了反射不能破坏枚举的单例.