文章目录
01、JMM:Java内存模型
什么是JMM?
JMM : Java内存模型,不存在的东西,一种约定
关于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 操作这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
Volatile 是 Java 虚拟机提供轻量级的同步机制
- 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吃饭(
单线程
),老板、前台、服务员怎么安排都可以,预期结果与实际结果就是一致的。 - 如果你预定的时间点是吃饭高峰期,很多人来吃饭(
多线程
),这个时候为了餐厅效益,无论是前台还是服务员或者是老板都会对你的位置进行重排序,在你没有来的时候,会安排其他人到你预定的位置吃饭,如果其他人在你的位置吃饭,这个时候你来吃饭,那么实际结果和预期结果就不一样了,这个时候餐厅应该做出相应的赔偿,为了避免这种赔偿问题,老板就想到了一个方案,做个牌子放在客人预定的桌子上。 - 当前台或者是服务员或者是老板看到餐桌上放的这个牌子,就知道这个位置不能再调动了,其中这个放在餐桌上的牌子就是特殊类型的
内存屏障
了。
- 假设要去A餐厅(处理器)吃饭,A餐厅有前台B(
-
指令重排分为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)
- 分析:
我们使用普通的方式创建两个实例,确定是单例的
我们使用反射创建两个实例,发现报错了,不能使用反射创建实例
注意:枚举类默认有一个有参构造器,不是无参构造器。