接前篇 重学Java之单例模式–懒汉式_南国樗里疾的博客-CSDN博客
https://blog.csdn.net/weixin_44021334/article/details/114880800 ,
说到懒汉式多线程不安全,
改进办法是 双重锁模式,也就是 双重锁定(Double-Check Locking) ,简称 DCL 。
对外提供获取实例的方法加上锁,
package single;
public class DCLLazyMan {
private static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
System.out.println(Thread.currentThread().getName() + ", " + toString());
}
public synchronized static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
}
}
return dclLazyMan;
}
}
这样是可以实现了,但是每次调用 getInstance()
都受到同步锁的影响,效率低。
改进下,
双重锁代码示例,
package single;
public class DCLLazyMan {
private static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) { // 注释1
synchronized (DCLLazyMan.class){ //注释 2
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
}
}
}
return dclLazyMan;
}
public static void main(String[] args){
//多线程
for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}
}
}
注释1 :将 synchronized
关键字从方法声明中去除,把它加入到方法体当中,效果是和直接在方法上加 synchronized
是一样的。在实例还未创建时才会运行到注释2处 ,如果实例已创建,不会执行到 注释 2 处,不受同步锁影响,效率较好。
注释 2 :实例未创建,需要创建实例,加锁保证只有一个线程操作。
运行结果:
Thread-0, single.DCLLazyMan@63b4a181
2中创建实例的代码 dclLazyMan = new DCLLazyMan()
,不是原子性操作,
它包含3个步骤:
我们期望的执行顺序是 123 ,但是实际执行的情况可能是 132 ,涉及指令重排, CPU 是支持的,单线程情况下没问题。
多线程的就不行了,假设 A 线程执行到 13 ,B 线程执行,因为对象已经指向内存空间了,B 认为对象已存在,直接执行 return dclLazyMan
了,但是此时 DCLLazyMan
还没有完成构造,这就有问题了。
改进方法,添加 volatile
关键字,避免指令重排。
package single;
public class DCLLazyMan {
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
/**
* 不是原子性操作;
* 1.分配内存空间;
* 2.执行构造方法,初始化对象;
* 3.把对象指向这个内存空间。
*
* 涉及指令重排,
* 我们期望顺序是 123 ,但底层可能执行的是 132 ;
* 单线程是可以的。
* 多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
* */
}
}
}
return dclLazyMan;
}
}
这样就是一个比较好的饿汉式了。
添加了双重检测锁 + 原子性操作,基本就OK了,但是呢,道高一尺魔高一丈,反射可以破坏这种方式。
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class DCLLazyMan {
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
}
}
}
return dclLazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//多线程
/*for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}*/
DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();// 正常获取
//反射获取
Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//无视私有构造器
DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
System.out.println("dclLazyMan1 = " + dclLazyMan1);
System.out.println("dclLazyMan2 = " + dclLazyMan2);
// dclLazyMan1 和 dclLazyMan2 不一样,反射破坏了这种单例
}
}
运行结果,dclLazyMan1 和 dclLazyMan2 不一样,说明这种单例被破坏了。
dclLazyMan1 = single.DCLLazyMan@4554617c
dclLazyMan2 = single.DCLLazyMan@74a14482
破坏的顺序是,先正常获取实例,再反射创建实例。
针对这种破坏,改善方法是在构造函数中判断示例是否已创建,如果已创建,说明是有人想搞破坏,抛出异常。
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class DCLLazyMan {
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
synchronized (DCLLazyMan.class){
if (dclLazyMan != null){
throw new RuntimeException("禁止使用反射创建");
}
}
//System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
}
}
}
return dclLazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//多线程
/*for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}*/
DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
//反射
Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//无视私有构造器
DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
System.out.println("dclLazyMan1 = " + dclLazyMan1);
System.out.println("dclLazyMan2 = " + dclLazyMan2);
// dclLazyMan1 和 dclLazyMan2 不一样,反射破坏了这种单例
}
}
运行结果,可以看到是有效的。
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:408)
at single.DCLLazyMan.main(DCLLazyMan.java:59)
Caused by: java.lang.RuntimeException: 禁止使用反射创建
at single.DCLLazyMan.(DCLLazyMan.java:14)
... 5 more
4中反射的破坏的顺序是,先正常获取实例,再反射创建实例。
试试,不正常获取,直接用反射创建两个实例,
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class DCLLazyMan {
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
synchronized (DCLLazyMan.class){
if (dclLazyMan != null){
throw new RuntimeException("禁止使用反射创建");
}
}
//System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
}
}
}
return dclLazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//多线程
/*for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}*/
//DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
//反射
Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//无视私有构造器
DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象
//System.out.println("dclLazyMan1 = " + dclLazyMan1);
System.out.println("dclLazyMan2 = " + dclLazyMan2);
System.out.println("dclLazyMan3 = " + dclLazyMan3);
}
}
运行结果如下,好家伙,又被破坏了。
dclLazyMan2 = single.DCLLazyMan@4554617c
dclLazyMan3 = single.DCLLazyMan@74a14482
针对这种破坏,再改进下,在构造函数里添加标志位判断,
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class DCLLazyMan {
public static boolean enable = false;
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
synchronized (DCLLazyMan.class){
if (enable == false) {//首次创建
enable = true;
} else{
throw new RuntimeException("禁止使用反射创建");
}
}
//System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
/**
* 不是原子性操作;
* 1.分配内存空间;
* 2.执行构造方法,初始化对象;
* 3.把对象指向这个内存空间。
*
* 涉及指令重排,
* 我们期望顺序是 123 ,但底层可能执行的是 132 ;
* 单线程是可以的。
* 多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
* */
}
}
}
return dclLazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//多线程
/*for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}*/
//DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
//反射
Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//无视私有构造器
DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象
//System.out.println("dclLazyMan1 = " + dclLazyMan1);
System.out.println("dclLazyMan2 = " + dclLazyMan2);
System.out.println("dclLazyMan3 = " + dclLazyMan3);
// dclLazyMan1 和 dclLazyMan2 不一样,反射破坏了这种单例
}
}
运行结果如下,可以看到,阻止了这种反射情况。
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:408)
at single.DCLLazyMan.main(DCLLazyMan.java:64)
Caused by: java.lang.RuntimeException: 禁止使用反射创建
at single.DCLLazyMan.(DCLLazyMan.java:18)
... 5 more
但是呢,如果 enable
也被反射修改,关注这一段 myField.set(dclLazyMan2, false)
,
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class DCLLazyMan {
public static boolean enable = false;
// 使用 volatile 避免指令重排
private volatile static DCLLazyMan dclLazyMan;
private DCLLazyMan(){
synchronized (DCLLazyMan.class){
if (enable == false) {//首次创建
enable = true;
} else{
throw new RuntimeException("禁止使用反射创建");
}
}
//System.out.println(Thread.currentThread().getName() + ", " + toString());
}
// 双重检测锁加原子性操作
public static DCLLazyMan getInstance(){
if (dclLazyMan == null) {
synchronized (DCLLazyMan.class){
if (dclLazyMan == null) {
dclLazyMan = new DCLLazyMan();
/**
* 不是原子性操作;
* 1.分配内存空间;
* 2.执行构造方法,初始化对象;
* 3.把对象指向这个内存空间。
*
* 涉及指令重排,
* 我们期望顺序是 123 ,但底层可能执行的是 132 ;
* 单线程是可以的。
* 多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
* */
}
}
}
return dclLazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//多线程
/*for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLLazyMan.getInstance();
}
}).start();
}*/
//DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
//反射
Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//无视私有构造器
Field myField = DCLLazyMan.class.getDeclaredField("enable");
myField.setAccessible(true);
DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
myField.set(dclLazyMan2, false);
DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象
//System.out.println("dclLazyMan1 = " + dclLazyMan1);
System.out.println("dclLazyMan2 = " + dclLazyMan2);
System.out.println("dclLazyMan3 = " + dclLazyMan3);
// dclLazyMan1 和 dclLazyMan2 不一样,反射破坏了这种单例
}
}
运行结果,
dclLazyMan2 = single.DCLLazyMan@74a14482
dclLazyMan3 = single.DCLLazyMan@1540e19d
可以看到,这种保护手段就没用了,吐血、当场 go die 。
看下 Constructor.newInstance
方法,
@CallerSensitive
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"); // 注释 5
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
注释 5 :如果时枚举,抛出异常。so ,为了防止反射,用枚举吧。