JUC-06-JMM,Volatile关键字,单例模式

01、JMM:Java内存模型

什么是JMM?

JMMJava内存模型,不存在的东西,一种约定

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回【主存】。
	- 例如:在一个进程中,有些变量是线程共享的,如果一个线程对这个变量进行修改,并不会直接在主存中
	- 修改,而是拷贝一份到线程自己的工作内存中,一旦这个线程解锁,就要把这个变量再刷新回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中。
3、加锁和解锁是同一把锁。

线程之间的变量是相互不可见的,如果两个线程同时修改一个变量的值,要使用Volatile关键字保证变量的【可见性】

内存交互操作有8种:线程间共享的主内存,和线程独享的工作内存

虚拟机实现必须保证每一个操作都是原子的不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

在这里插入图片描述

lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

不允许read和load、store和write操作之一单独出现,即使用了read必须load,使用了store必须write

不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

不允许一个线程将没有assign的数据从工作内存同步回主内存

一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量,就是对变量
实施use、store操作之前,必须经过assign和load操作

一个变量同一时间只有一个线程能对其进行lock,多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须
重新load或assign操作初始化变量的值

如果一个变量没有被lock,就不能对其进行unlock操作,也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存

在这里插入图片描述

02、Volatile关键字

上面出现的问题,怎么解决那,下面就轮到Volatile关键字出场了

请你谈谈你对 Volatile 的理解?

  • volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。JMM(Java内存模型)是围绕并发过程中如何处理可见性原子性有序性这3个特征建立起来的,而volatile可以保证其中的两个特性。

可见性:

  • 可见性是一种复杂的属性,通常我们无法确保执行读操作的线程能适时地看到其他线程写入的值,为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
  • 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到
  • 在 Java 中 volatile、synchronized 和 final 都可以实现可见性。
  • 可见性在 JVM 底层是基于内存屏障实现的,当对 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而 volatile 变量 JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,所以就不会有可见性问题。
  • 对 volatile 变量进行写操作时,会在写操作后加一条 store 屏障指令,将工作内存中的共享变量刷新回主内存;
  • 对 volatile 变量进行读操作时,会在写操作后加一条 load 屏障指令,从主内存中读取共享变量;

原子性:

  • 原子性指的是某个线程正在执行某个操作时,中间不可以被加塞或分割,要么整体成功,要么整体失败。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。
  • 再如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
  • Java的 concurrent 包下提供了一些原子类,AtomicInteger、AtomicLong、AtomicReference等。
  • 在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

  • Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含禁止指令重排序的语义。
  • synchronized 是由一个变量在同一个时刻只允许一条线程对其进行 lock 操作这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
VolatileJava 虚拟机提供轻量级的同步机制
- 1、保证可见性
- 2、不保证原子性
- 3、禁止指令重排

1、保证可见性

public class JMMDemo{
    private static int num = 0;
    // private volatile static int num = 0; // 保证线程之间变量的可见性
    public static void main(String[] args) {
        // 在主线程中开启一个测试线程, 测试线程先拿到num值,由于num的值为0,判断成立,陷入死循环
        new Thread(() -> {
            while (num == 0) {
                // 死循环
            }
            System.out.println("num的修改对Thread_A线程可见,死循环结束");
        }, "Thread_A").start();

        // 将主线程睡眠(main线程)
        try {
            // System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(2);
            // System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 主线程睡眠结束,拿到CPU执行时间,然后修改num的值,如果这个num的值对线程Thread_A可见,则会输出后面的日志信息
        // 通过运行结果可以看出,主线程执行结束后,num的值为1,Thread_A 线程依旧阻塞死循环。
        // 解决方法,使用volatile关键字修饰这个变量,让它在线程之间可见
        num = 1;
        System.out.println(num);
    }
}

2、不保证原子性

public class JMMDemo2 {
    // volatile 不保证原子性
    private volatile static int num = 0;

    public static void add() {
        // num++; 不保证原子性  真正的是两步操作:1. num+1;  2. num = num+1
        // 两步操作执行完才会将值刷新到内存,如果执行一半被其他线程占用,则两个线程就会对同一个值进行加一操作
        // 例如:线程A从内存中拿到num的值为66,执行完第一步(执行完第二部内存中的num的值才会改变),
        // 此时线程B拿到执行权,B从内存中拿到num的值也是66,然后对66进行加一操作,最后线程A和线程B就会对同一个数进行加一操作。就出现问题了。
        num++; 
    }

    public static void main(String[] args) {
        noAtomic();
    }

    public static void noAtomic() {
        // 理论上 num 结果应该为 20*1000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add(); // 调用静态方法
                }
            }).start();
        }
        while (Thread.activeCount() > 2) { // 查看存活线程个数,默认两个main gc在执行
            Thread.yield();// 线程礼让
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}
  • 运行结果
main 13380
  • 如何保证原子性
/**
 * 为了保证原子性操作:我们引入java.util.concurrent.atomic包,包中对于不同类型的变量对象不同的类
 * 对于整数:AtomicInteger类*/
public class JMMDemo03 {
    //保证可见性volatile,保证原子性AtomicInteger
    private volatile static AtomicInteger num=new AtomicInteger();

    //给这个静态方法加锁可以保证原子性:synchronized或者lock,
    //我们不使用锁,如何保证原子性?
    //保证原子性
    public static void add(){
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        atomic();
    }

    public static void atomic(){
        //理论上num的值为2万
        for(int i=1;i<=20;i++){
            new Thread(()->{
                for(int j=0;j<1000;j++){
                    add();//调用静态方法
                }
            }).start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }
}
  • 运行结果
main 20000

3、禁止指令重排

  • 什么是指令重排?

使用大白话来说吧:你写的代码在经过编译器编译之后,可能执行的顺序会发生改变,发生改变的原因就是指令重排了

  • 源代码-->编译器优化的重排-->指令并行也可能重排-->内存系统也会重排-->程序执行

  • 举个例子:预定餐桌

    • 假设要去A餐厅(处理器)吃饭,A餐厅有前台B(编译器)、服务员C(指令)以及老板D(内存系统),如果就只有你一个人去吃饭的时候,你给前台或者服务员或者老板说一声预定1号桌,半小时后过来。如果餐厅就你一个人去吃饭(单线程),没问题,别说等半个小时,就是等一个小时,1号桌还是你的。
    • 但是,如果现在是吃饭高峰期,很多人来吃饭,你给前台说了,前台忙着没有及时给服务员或者老板说,这个时候有个人来吃饭,刚好看到1号桌没人,老板或者服务员就让他就坐1号桌吃饭了,等你过来的时候,1号桌已经有人了,这个时候对于你来说,这个结果就不是你想要的了。
    • 如果从计算机执行指令角度来分析的话,你要到1号桌吃饭,这是预期结果,如果你预定的时间点不是吃饭高峰期或者没有人去餐厅A吃饭(单线程),老板、前台、服务员怎么安排都可以,预期结果与实际结果就是一致的。
    • 如果你预定的时间点是吃饭高峰期,很多人来吃饭(多线程),这个时候为了餐厅效益,无论是前台还是服务员或者是老板都会对你的位置进行重排序,在你没有来的时候,会安排其他人到你预定的位置吃饭,如果其他人在你的位置吃饭,这个时候你来吃饭,那么实际结果和预期结果就不一样了,这个时候餐厅应该做出相应的赔偿,为了避免这种赔偿问题,老板就想到了一个方案,做个牌子放在客人预定的桌子上。
    • 当前台或者是服务员或者是老板看到餐桌上放的这个牌子,就知道这个位置不能再调动了,其中这个放在餐桌上的牌子就是特殊类型的内存屏障了。
  • 指令重排分为3种:

    • 1:编译器优化重排:编译器的优化前提是在保证不改变单线程语义的情况下,对重新安排语句的执行顺序。

    • 2:指令并行重排:如果代码中某些语句之间不存在数据依赖,处理器可以改变语句对应机器指令的顺序

      int x=1;	//1
      int y=2;	//2
      x=x+5;		//3
      y=x+x;		//4
      执行顺序为期望为:1234
      如果为2134或者1324也可以:没有改变单线程的语义
      如果是4123就不行,因为y和x之间存在数据依赖
      
    • 3:内存系统的重排序:处理器和主内存之间还存在一二三级缓存,这些读写缓存的存在,使得程序的加载和存取操作,可能是乱序无章的。

  • 如在多线程的情况下,单例模式就不安全了。

为了解决这个问题,JMM允许编译器在生成指令顺序的时候,可以插入特定类型的内存屏障来禁止指令重排

  • 当一个变量使用volatile修饰的时候,volatile关键字就是内存屏障,当编译器在生成指令顺序的时候,发现了volatile,就直接忽略掉,不再重排序了
  • 内存屏障在单例模式中使用的挺多的

03、深入单例模式

  • 单例模式的构造器都是私有的,通过一个静态方法来获取类的实例,饿汉式这个实例已经创建好等待着被获取,懒汉式是什么时候获取什么时候创建

饿汉式单例模式

饿汉式特点:

  • 1.饿汉式在类加载的时候就实例化,并且创建单例对象
  • 2.饿汉式线程安全 (在线程还没出现之前就已经实例化了,因此饿汉式线程一定是安全的)
  • 3.饿汉式没有加任何的锁,因此执行效率比较高
  • 饿汉式在类加载的时候就初始化,不管你是否使用,它都实例化了,所以会占据空间,浪费内存
//饿汉式单例
public class HungryDemo {
    //无参构造
    private HungryDemo(){};
    /*
    * private 为了对外无法访问
    * static 为了在类加载的时候加载
    * final  为了只创建一次*/
	 
	 //不管用不用已经创建出对象了
    private static final HungryDemo HUNGRY_DEMO = new HungryDemo();

    //通过这个静态方法获取实例
    public static HungryDemo getInstance(){
        return HUNGRY_DEMO;
    }
}

懒汉式单例模式

懒汉式特点:

  • 1.懒汉式默认不会实例化,外部什么时候调用什么时候new
  • 2.懒汉式【线程不安全】( 因为懒汉式加载是在使用时才会去new 实例的,那么你去new的时候是一个动态的过程,是放到方法中实现的,如果这个时候有多个线程访问这个实例,这个时候实例还不存在,还在new,就会进入到方法中, 有多少线程就会new出多少个实例,一个方法只能return一个实例,那最终return出哪个呢?是不是会覆盖很多new的实例?

这种情况当然也可以解决,那就是加同步锁:synchronized,避免这种情况发生

  • 3.懒汉式一般使用都会加同步锁,效率比饿汉式差
  • 4.懒汉式什么时候需要什么时候实例化,相对来说不浪费内存
初级版本的懒汉式单例模式:
//懒汉式单例
public class LazyDemo {
    //构造方法
    private LazyDemo(){
        System.out.println(Thread.currentThread().getName()+"-OK!!!");//为了测试
    };
    //默认不会被实例化
    private  static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化:没加锁不安全
    public static LazyDemo getInstance_unsafe(){
        if (lazyDemo==null){
            lazyDemo=new LazyDemo();
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //我们使用循环创建多个实例然后打印出来看看,是不是单例模式的
        for(int i=0;i<10;i++){
            new Thread(()->{
                System.out.println(LazyDemo.getInstance_unsafe());
            }).start();
        }
    }
}
  • 运行结果
Thread-0-OK!!!
Thread-5-OK!!!
Thread-4-OK!!!
Thread-2-OK!!!
Thread-1-OK!!!
com.aismall.singleInstance.LazyDemo@49b7d06d
Thread-3-OK!!!
com.aismall.singleInstance.LazyDemo@7f608cfa
com.aismall.singleInstance.LazyDemo@49b7d06d
com.aismall.singleInstance.LazyDemo@378ea21c
com.aismall.singleInstance.LazyDemo@378ea21c
com.aismall.singleInstance.LazyDemo@1025efd2
com.aismall.singleInstance.LazyDemo@1025efd2
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@22ff7883
  • 分析:
从打印的结果可以看出,并不是一个实例,单例模式是失败的
双重加锁版本的懒汉式单例模式
//懒汉式单例
public class LazyDemo {
    //构造方法
    private LazyDemo(){
        System.out.println(Thread.currentThread().getName()+"-OK!!!");//为了测试
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private volatile static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();//不是一个原子性操作
                    /*
                     * 创建对象在内存中的步骤:
                     *   1.分配内存空间
                     *   2.执行构造方法,初始化对象
                     *   3.把这个对象执行这个空间
                     *
                     * 如果出现指令重排:
                     *   线程A进来,执行步骤为 1 3 2 在执行到3的时候线程B进来了,
                     *   线程B会观察第一个if语句(锁的是后面的语句),lazyDemo不为null,
                     *   但是此时的地址指向的内存中对象还没有成功(线程A创建的是一个null),
                     *   时候线程B提前执行,return语句,创建出一个对象,当线程A回来时继续执行
                     *   这时候就会出现来两个实例,单例模式被破坏
                     * 避免指令重排:
                     *   我们使用volatile关键字修饰这个lazyDemo变量,也就是第二重锁
                     */
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //我们使用循环创建多个实例然后打印出来看看,是不是单例模式的
        for(int i=0;i<10;i++){
            new Thread(()->{
                System.out.println(LazyDemo.getInstance_safe());
            }).start();
        }
    }
}
  • 运行结果
Thread-1-OK!!!
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
com.aismall.singleInstance.LazyDemo@384e8869
  • 分析
从上面的打印结果可以看出,单例模式成功了
如果去掉volatile关键字,是不是还是单例,可以去掉打印一下结果看看。

注意:在构建的时候指令重排的概率是非常小的,要演示很多次才可能出现。
防止简单暴力反射的懒汉式单例模式

使用暴力反射破坏单例

//懒汉式单例
public class LazyDemo {
    //构造方法
    private LazyDemo(){
        System.out.println(Thread.currentThread().getName()+"-OK!!!");//为了测试
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private/* volatile*/ static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //创建一个实例
        LazyDemo instance1=LazyDemo.getInstance_safe();

        //使用暴力反射创建一个实例:破坏单例模式
        Constructor<LazyDemo> declaredConstructor=LazyDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);

        //破坏初级版防止暴力反射,两个都使用反射创建,破坏单例
        LazyDemo instance2=declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
main-OK!!!
main-OK!!!
com.aismall.singleInstance.LazyDemo@74a14482
com.aismall.singleInstance.LazyDemo@1540e19d
  • 防止这种破坏:因为这种破坏要使用构造器,我们在构造器中加锁可以防止这种破坏
//懒汉式单例
public class LazyDemo {
    //构造方法
    private LazyDemo(){
        //初级版防止暴力反射,如果已经有这个类的实例,就拿到锁,进入判断。
        synchronized (LazyDemo.class){
        	//因为我们已经创建了lazyDemo这个实例,所以报错
            if(lazyDemo!=null) {
                throw new RuntimeException("不要试图使用反射来破坏抛出异常");
            }
        }
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private volatile static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //创建一个实例
        LazyDemo instance1=LazyDemo.getInstance_safe();

        //使用暴力反射创建一个实例:破坏单例模式
        Constructor<LazyDemo> declaredConstructor=LazyDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyDemo instance2=declaredConstructor.newInstance();
        
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果:使用暴力反射创建实例,我们抛出异常
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.aismall.singleInstance.LazyDemo.main(LazyDemo.java:55)
Caused by: java.lang.RuntimeException: 不要试图使用反射来破坏抛出异常
	at com.aismall.singleInstance.LazyDemo.<init>(LazyDemo.java:25)
	... 5 more

继续破坏:破坏初级版防止暴力反射,两个实例都使用反射创建,破坏单例

//懒汉式单例
public class LazyDemo {
    //构造方法
    private LazyDemo(){
        //初级版防止暴力反射,如果已经有这个类的实例,就拿到锁,进入判断。
        synchronized (LazyDemo.class){
            if(lazyDemo!=null) {
                throw new RuntimeException("不要试图使用反射来破坏抛出异常");
            }
        }
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private volatile static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //使用暴力反射创建一个实例:破坏单例模式
        Constructor<LazyDemo> declaredConstructor=LazyDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);

        //破坏初级版防止暴力反射,两个都使用反射创建,破坏单例
        LazyDemo instance1=declaredConstructor.newInstance();
        LazyDemo instance2=declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
com.aismall.singleInstance.LazyDemo@74a14482
com.aismall.singleInstance.LazyDemo@1540e19d
  • 分析
通过上面的运行结果,我们可以看出,单例模式被破坏了,为什么哪?
	- 因为两次都是使用暴力反射来创建的实例,没有使用getInstance_safe()方法,
	- 所以lazyDemo这个变量的值为null,虽然我们在构造器中拿到类锁,进入if语句进行判断,
	- 但是判断失败,所以不会抛出异常,单例模式失效
  • 防止这种破坏:使用一个判断标志位就可以解决这个问题
//懒汉式单例
public class LazyDemo {
    //使用一个标志位,来防止暴力反射
    private static boolean flag=true;
    //构造方法
    private LazyDemo(){
        //高级版本的防止暴力反射
        synchronized (LazyDemo.class) {
            //我们不判断lazyDemo,判断flag
            //标志位为true
            if (flag == true) {
                //设为false
                flag = false;
            } else {
                throw new RuntimeException("不要试图使用反射来破坏抛出异常");
            }
        }
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private volatile static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //使用暴力反射创建一个实例:破坏单例模式
        Constructor<LazyDemo> declaredConstructor=LazyDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);

        //破坏初级版防止暴力反射,两个都使用反射创建,破坏单例
        LazyDemo instance1=declaredConstructor.newInstance();
        LazyDemo instance2=declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.aismall.singleInstance.LazyDemo.main(LazyDemo.java:59)
Caused by: java.lang.RuntimeException: 不要试图使用反射来破坏抛出异常
	at com.aismall.singleInstance.LazyDemo.<init>(LazyDemo.java:31)
	... 5 more

继续破坏:这种方式是使用一个标志位来防止的,如果我们能拿到这个字段,就可以继续破坏,理论上这个字段是拿不到的,我们演示一下如果哪能拿到这个字段如何破坏

//懒汉式单例
public class LazyDemo {
    //使用一个标志位,来防止暴力反射
    private static boolean flag=true;
    //构造方法
    private LazyDemo(){
        //高级版本的防止暴力反射
        synchronized (LazyDemo.class) {
            //我们不判断lazyDemo,判断flag
            //标志位为true
            if (flag == true) {
                //设为false
                flag = false;
            } else {
                throw new RuntimeException("不要试图使用反射来破坏抛出异常");
            }
        }
    };

    //第二重加锁:默认不会被实例化,保证原子性,避免指令重排,加上volatile
    private volatile static LazyDemo lazyDemo=null;

    //什么时候调用什么时候实例化,这种方式比直接在静态方法上加锁效率高一点
    public static LazyDemo getInstance_safe(){
        //第一重锁:锁住这个实例
        if (lazyDemo==null){
            synchronized (LazyDemo.class) {
                if(lazyDemo==null) {
                    lazyDemo = new LazyDemo();
                }
            }
        }
        return lazyDemo;
    }

    public static void main(String[] args) throws Exception {
        //【假设】我们知道这关键字是谁(现实基本不可能)
        //获取这个LazyDemo的flag字段
        Field flag = LazyDemo.class.getDeclaredField("flag");
        // 在反射的时候能够访问这个私有变量:flag
        flag.setAccessible(true);

        //使用暴力反射创建一个实例:破坏单例模式
        Constructor<LazyDemo> declaredConstructor=LazyDemo.class.getDeclaredConstructor(null);

        //创建一个实例
        LazyDemo instance1=declaredConstructor.newInstance();

        // 我们在手动把这个值改为:flag=true
        flag.set(instance1,true);//这句代码注意位置
        //在创建一个实例
        LazyDemo instance2=declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
com.aismall.singleInstance.LazyDemo@1540e19d
com.aismall.singleInstance.LazyDemo@677327b6

枚举可以防止反射破坏单例

  • 我门进入这个方法newInstance()查看一下源码:
public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        	//不能使用反射创建枚举类
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
  • 我们测试一下:
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //通过方法进行创建
        EnumSingle instance1=EnumSingle.INSTANCE;
        EnumSingle instance2=EnumSingle.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);

        //通过反射进行创建:获取空参构造器
        //获取空参构造的时候报错:java.lang.NoSuchMethodException: com.aismall.singleInstance.EnumSingle.<init>()
        //Constructor<EnumSingle> declaredConstructor=EnumSingle.class.getDeclaredConstructor(null);
        Constructor<EnumSingle> declaredConstructor=EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        //能获取该类中的所有字段
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle1=declaredConstructor.newInstance();
        EnumSingle enumSingle2=declaredConstructor.newInstance();
        System.out.println(enumSingle1);
        System.out.println(enumSingle2);
    }
}
  • 运行结果:
INSTANCE
INSTANCE
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.aismall.singleInstance.EnumSingle.main(EnumSingle.java:27)
  • 分析:
我们使用普通的方式创建两个实例,确定是单例的
我们使用反射创建两个实例,发现报错了,不能使用反射创建实例

注意:枚举类默认有一个有参构造器,不是无参构造器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彤彤的小跟班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值