JUC并发编程(13)--- 彻底玩转单例模式
Posted 小样5411
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程(13)--- 彻底玩转单例模式相关的知识,希望对你有一定的参考价值。
前言
本文将一步步带你实现一个安全的单例(枚举)
单例模式
1、饿汉式单例
饿汉式就是一上来就加载,一上来就new这个类对象,对应内存空间也会一上来就分配,但是没有被使用,这样就浪费了空间,所以不用饿汉式
//饿汉式单例
public class Hungry {
//假设下面为4组内存资源,但一上来就加载,并没有使用,就浪费了空间
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();
private static Hungry getInstance(){
return hungry;
}
}
2、懒汉式单例
懒汉式就是等需要用的时候再加载,用的时候调用getInstance
//懒汉式单例
public class LazyMan {
///构造器私有
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
但上述单例在单线程下面是没问题的,但是多线程呢?多线程就有问题了,具体来看看并发下有什么问题
//懒汉式单例
public class LazyMan {
///构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
执行了三次,每次结果都不一样,而且不是单例的。这表明线程不安全
所以要保证线程安全就要上锁,也就出现了双重检测锁(DCL)
//懒汉式单例---双重检测锁
public class LazyMan {
///构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
这样就保证了单例,但是又出现一个问题,就是new LazyMan()底层不是原子性操作,它的操作分三步:1、分配内存空间 2、执行构造方法、初始化对象 3、将对象指向这个空间
由于执行时计算机会指令重排,所以可能不是按照123顺序执行,而是按照132顺序执行,这样的话如果有两个线程,线程A执行132,线程B再执行的时候由于对象指向了空间lazyMan非空,线程B就以为lazyMan不为空,直接return lazyMan,那么这就有问题了,这里return的lazyMan是没有new的,所以没有分配空间,是空的。所以我们要禁止指令重排,即加volatile关键字
双重检测锁+原子性操作(volatile)
3、静态内部类实现单例
//静态内部类实现
public class Holder {
private Holder(){
System.out.println(Thread.currentThread().getName()+"ok");
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private final static Holder holder = new Holder();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Holder.getInstance();
}).start();
}
}
}
这里可以不通过外部类调用,用内部类调用,这也可以实现单例
但是上面虽然保证线程安全,并且禁止了指令重排可能带来的危险,但是还是能破解,变得不安全,那就是用反射!!!
我们可以用反射破坏一下双重检测锁
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//懒汉式单例
public class LazyMan {
///构造器私有
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
LazyMan lazyMan = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
LazyMan lazyMan1 = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(lazyMan1);
}
}
发现两个就不一样了,那么反射就破坏了单例,但如何解决呢,也很好解决,只要在私有化构造再加一个锁,并判断是否lazy为空
package com.yx.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//懒汉式单例
public class LazyMan {
///构造器私有
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
LazyMan lazyMan = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
LazyMan lazyMan1 = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(lazyMan1);
}
}
但又出现问题,如果两个都用反射创建呢,根本就不通过构造
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//懒汉式单例
public class LazyMan {
///构造器私有
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//LazyMan lazyMan = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
又不安全了,单例被破坏了,那可以又相出一些判断方法进行解决,但是在反射面前都能破解,因为通过反射可以创建对象,获取类的方法和变量,并且对他们进行操作。
有没有什么方法能保证安全不被反射破坏呢?
有,枚举!!!
package com.yx.test;
import java.lang.reflect.Constructor;
public enum EnumSingle {
INSTANCE;
private EnumSingle(){
}
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.INSTANCE;
EnumSingle instance1 = EnumSingle.INSTANCE;
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);//反射获取空参构造
declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
EnumSingle lazyMan1 = declaredConstructor.newInstance();
EnumSingle lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
当我们获取创建的单例并用反射破坏时,会爆出异常
查看源码会发现,爆出的异常和源码中不一致,按道理应该爆不能用反射创建枚举
用jad.exe反编译EnumSingle.class
反编译生成对应的java文件
反编译后发现,是有参数的,并且可以看到枚举其实也是class类,只是继承了Enum类
再次运行,发现确实通过反射会爆出Cannot reflectively create enum objects
总结:保证单例安全就用枚举,枚举,枚举!!!
以上是关于JUC并发编程(13)--- 彻底玩转单例模式的主要内容,如果未能解决你的问题,请参考以下文章