小伙子,来谈恋爱啊?单例模式都不懂,谈个鬼哦
什么是单例模式
单例模式,顾明思议就是一个类只有一个实例,并且这个类负责创造自己的对象,同时确保只有单个对象被创建。这个类提供访问其唯一的对象的方式,我们在使用的时候可以直接调用其方法获取到,而不需要去实例化。
首先,来说一下单例模式的核心思想,构造方法私有,且需要一个静态方法用于获取对象实例。
//单例模式核心思想,构造器私有! private SingleModel(){ //编写相关逻辑 } //静态方法,获取对象实例 public static SingleModel getInstance(){ return 对象实例; }
饿汉式
听名字就知道,这是勤劳的汉子。在类已加载的时候,实例就帮你创建好了,不管你用没用到,先创建好再说。好处是线程是安全的,调用效率高。坏处是造成内存大量浪费。
/** * @Author youjp * @Description //TODO= 饿汉式单例: * @Date 2020-07-09$ 18:23$ * @throw **/ public class Hungry { private byte[] data1 = new byte[10240]; private byte[] data2 = new byte[10240]; private byte[] data3 = new byte[10240]; private static Hungry hungry=new Hungry(); //构造方法私有化 private Hungry(){ } public static Hungry getInstance(){ return hungry; } public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+":"+Hungry.getInstance()); },String.valueOf(i)).start(); } } }
懒汉式
懒汉式就是比较“”懒“”的创建方式了,单例对象言辞加载。在类加载的时候只加载了类的实例变量,而没有一开始就创建实例。只有在去使用方法用的时候,才会去检测是否创建了实例,如果有则返回,没有则新建。
/** * @Author youjp * @Description //TODO= 懒汉式,使用时才会去创建实例。线程不安全 * @Date 20200709$ 18:38$ * @throw **/ public class LazymanDemo { private static LazymanDemo lazymanDemo; private LazymanDemo(){ } private static LazymanDemo getInstance(){ if (lazymanDemo==null){ lazymanDemo =new LazymanDemo(); } return lazymanDemo; } public static void main(String[] args) { for (int i = 0; i <10; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName() + ":" + LazymanDemo.getInstance()); },String.valueOf(i)+"A").start(); } } }
与饿汉式的区别就是,懒汉式类加载时较快,访问对象时较慢,是线程不安全的。在大多数场景下,都使用懒汉式,因为饿汉式有一个坏处就是会造成内存空间的浪费。那么我们就需要去解决懒汉式线程不安全的问题。
主要需要解决两个问题”
- 线程的并发 问题:使用synchronized加锁即可
- JVM的指令重排问题:使用volatile关键字防止指令重排
懒汉式 DCL:双重检测锁
双检锁,又叫双重检测锁。它在懒汉式的基础上,使用了synchronized去解决懒汉式线程不安全的问题。加锁我们可以直接加在getInstance方法()上,但是这样子的静态方法锁整个临界区比较大,比较耗费资源,所以使用同步代码块。
/** * @Author youjp * @Description //TODO= 懒汉式DCL.双重检测锁 * @Date 2020-0709$ 19:11$ * @throw **/ public class LazymanDclDemo { private static LazymanDclDemo dclDemo; private LazymanDclDemo(){} public static LazymanDclDemo getInstance(){ if (dclDemo==null){ synchronized (LazymanDclDemo.class){ if (dclDemo==null){ dclDemo =new LazymanDclDemo(); } } } return dclDemo; } }
为什么要判空两次呢?
其实就是用了同步代码块,你必须要保证临界区完成一整套不可缺少的操作。最开始的判空是确认是否需要进入临界区。假如有2个线程都停在了第一个判空处,其中一个线程获得锁进去不判空直接new,那么它完成操作释放锁之后对于第二个等待锁的线程而言,它获得一释放的锁之后也是进去直接new,很显然,这一点都不符合临界区的设计。
懒汉式 DCL+volatile
指令重排的问题: dclDemo =new LazymanDclDemo();
这一条语句,并不是一个原子性操作,可能会存在指令重排的问题。
- java虚拟机在类加载以后,会分配内存给对象,在内存中开辟一段地址空间;
Singleton var = new Singleton();
- 对象的初始化;
var = init();
- 将分配好对象的地址指向dclDemo变量
dclDemo= var;
设想一下,如果发生了指令重排(1-3-2)操作,获得到单例的线程可能拿到了一个空对象,后续操作会有影响!因此需要引入volatile对变量进行修饰。
最后我们得到的完整代码:
/** * @Author youjp * @Description //TODO= 懒汉式DCL+Volatile.三重检测锁 * @Date 2020-0709$ 19:11$ * @throw **/ public class LazymanDclDemo { private volatile static LazymanDclDemo dclDemo; private LazymanDclDemo(){} public static LazymanDclDemo getInstance(){ if (dclDemo==null){ synchronized (LazymanDclDemo.class){ if (dclDemo==null){ dclDemo =new LazymanDclDemo(); } } } return dclDemo; } }
虽然,这样解决了懒汉式线程安全的问题,但其实也不是绝对安全的,是可以通过反射破坏的。这个我们后面讲解。
静态内部类
package com.single.demo; /** * @Author youjp * @Description //TODO= 静态内部类实现单例模式 ,多线程下相对是安全的 * 缺陷: 是可以通过反射破坏的,不安全 * @Date 2020-07-10$ 10:34$ * @throw **/ public class StaticInnerClass { private StaticInnerClass(){ System.out.println(Thread.currentThread().getName() + ":create StaticInnerClass"); } private static class InnerClass{ private final static StaticInnerClass inner=new StaticInnerClass(); } public static StaticInnerClass getInstance(){ return InnerClass.inner; } public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(()->{ StaticInnerClass.getInstance(); }).start(); } } }
静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
枚举
枚举示例:
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
完整的枚举单例
/** * @Author youjp * @Description //TODO= 使用枚举实现的单例模式 * @Date 2020-07-10$ 11:19$ * @throw **/ public class EnumSingleDemo { private EnumSingleDemo(){ System.out.println(Thread.currentThread().getName() + ":create EnumSingleDemo"); } //定义一个枚举类 static enum EnumSingleton{ INSTANCE; private EnumSingleDemo singleDemo; private EnumSingleton(){ //私有化枚举的构造函数 singleDemo=new EnumSingleDemo(); } //对外暴露一个获取EnumSingleDemo对象的静态方法 public EnumSingleDemo getInstance(){ return singleDemo; } } public static EnumSingleDemo getInstance(){ return EnumSingleton.INSTANCE.getInstance(); } public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(()->{ System.out.println(EnumSingleDemo.getInstance()); }).start(); } EnumSingleDemo demo1=EnumSingleDemo.getInstance(); EnumSingleDemo demo2=EnumSingleDemo.getInstance(); System.out.println(demo1==demo2); } }
以上介绍了饿汉式、懒汉式、懒汉式DCL双检锁、懒汉式DCL+volatile、静态类部类实现的单例模式。各有各自的特点。但都不是是绝对安全的。最安全的是通过枚举类实现的方式可以防止反射破坏。
反射破坏
前面实现了懒汉式DCL,静态类部类、以及枚举实现的单例模式。为什么说枚举是最安全的呢,我们使用反射去依次破坏实验。
尝试破坏懒汉式DCL+Volatile
package com.single.demo; import java.lang.reflect.Constructor; import java.lang.reflect.Field; /** * @Author youjp * @Description //TODO= 懒汉式DCL+Volatile.三重检测锁 * @Date 2020-0709$ 19:11$ * @throw **/ public class LazymanDclDemo { private volatile static LazymanDclDemo dclDemo; private static boolean flag = false; private LazymanDclDemo(){ synchronized (LazymanDclDemo.class){ if(flag == false){ flag = true; }else{ System.out.println(Thread.currentThread().getName() + ":不要试图用反射机制破坏单例模式"); } } } public static LazymanDclDemo getInstance(){ if (dclDemo==null){ synchronized (LazymanDclDemo.class){ if (dclDemo==null){ dclDemo =new LazymanDclDemo(); } } } return dclDemo; } public static void main(String[] args) { try { reflectBreakSingle(); } catch (Exception e) { e.printStackTrace(); } } /** * 使用反射破坏单例 */ public static void reflectBreakSingle() throws Exception { //使用反射破坏 Constructor<LazymanDclDemo> constructor= LazymanDclDemo.class.getDeclaredConstructor(null); constructor.setAccessible(true); //无视类私有方法,可以获取到访问私有变量的权限 //只要你的代码被别人看到,别人就能拿到属性 Field flag = LazymanDclDemo.class.getDeclaredField("flag"); flag.setAccessible(true);//无视这个属性 LazymanDclDemo lazyman1=constructor.newInstance(); //调用无参构造函数 //创建了一个对象后,将此对象的flag属性设置为false,又可以继续创建新的对象。 flag.set(lazyman1,false); System.out.println(lazyman1.flag); LazymanDclDemo lazyman2 = constructor.newInstance(); flag.set(lazyman2,true); System.out.println(lazyman2.flag); System.out.println(lazyman1); System.out.println(lazyman2); System.out.println(LazymanDclDemo.getInstance()); } }
运行结果
可以看到,通过反射获取到对象构造函数来创建对象获取到的hashcode值和通过编写懒汉式DCL单例获取到的hashcode值是不一样的。且以后我可以任意改造,还能多次创建不同的实例了,单例已经不安全了。
破坏静态内部类
与上面破坏的方式类似,我们只要通过通过类对象获取构造器,就可以进行反射破坏
package com.single.demo; import java.lang.reflect.Constructor; import java.lang.reflect.Field; /** * @Author youjp * @Description //TODO= 静态内部类实现单例模式 ,多线程下相对是安全的 * 缺陷: 是可以通过反射破坏的,不安全 * @Date 2020-07-10$ 10:34$ * @throw **/ public class StaticInnerClass { private StaticInnerClass(){ } private static class InnerClass{ private final static StaticInnerClass inner=new StaticInnerClass(); } public static StaticInnerClass getInstance(){ return InnerClass.inner; } public static void main(String[] args) { try { reflectBreakSingle(); } catch (Exception e) { e.printStackTrace(); } } /** * 使用反射破坏单例 */ public static void reflectBreakSingle() throws Exception { //通过类对象获取构造器 Constructor<StaticInnerClass> constructor= StaticInnerClass.class.getDeclaredConstructor(null); constructor.setAccessible(true); //无视类私有方法,可以获取到访问私有变量的权限 StaticInnerClass lazyman1=constructor.newInstance(); //调用无参构造函数 StaticInnerClass lazyman2 = constructor.newInstance(); System.out.println(lazyman1); System.out.println(lazyman2); System.out.println(StaticInnerClass.getInstance()); } }
可以看到静态方式编写的也是不安全的。那么有没有什么,能够防止反射序列化破坏呢?答案是有的。
尝试破坏枚举类
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; /** * @Author youjp * @Description //TODO= 使用枚举实现的单例模式 * @Date 2020-07-10$ 11:19$ * @throw **/ public class EnumSingleDemo { int i=0; private EnumSingleDemo(){ System.out.println("EnumSingleDemo被初始化 " + ++i + " 次"); } //定义一个枚举类 static enum EnumSingleton{ INSTANCE; private EnumSingleDemo singleDemo; private EnumSingleton(){ //私有化枚举的构造函数 singleDemo=new EnumSingleDemo(); } //对外暴露一个获取EnumSingleDemo对象的静态方法 public EnumSingleDemo getInstance(){ return singleDemo; } } public static EnumSingleDemo getInstance(){ return EnumSingleton.INSTANCE.getInstance(); } public static void main(String[] args) { try { reflectBreakSingle(); } catch (Exception e) { e.printStackTrace(); } } /** * 使用反射破坏单例 */ public static void reflectBreakSingle() { try { EnumSingleDemo lazyman=EnumSingleton.INSTANCE.getInstance(); //通过类对象获取构造器 Constructor<EnumSingleton> constructor= EnumSingleton.class.getDeclaredConstructor(null); constructor.setAccessible(true); //无视类私有方法,可以获取到访问私有变量的权限 EnumSingleDemo lazyman1=constructor.newInstance().getInstance(); //调用无参构造函数 EnumSingleDemo lazyman2 = constructor.newInstance().getInstance(); System.out.println(lazyman); System.out.println(lazyman1); System.out.println(lazyman2); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
运行结果:
注意报错,这里提示这个枚举类里,没有一个空参的构造方法。诶呀,这就显得很奇怪了,我明明有写一个空参的构造方法呀,为啥会说没有。。。
//定义一个枚举类 static enum EnumSingleton{ INSTANCE; private EnumSingleDemo singleDemo; private EnumSingleton(){ //私有化枚举的构造函数 singleDemo=new EnumSingleDemo(); } //对外暴露一个获取EnumSingleDemo对象的静态方法 public EnumSingleDemo getInstance(){ return singleDemo; } }
我们反编译去看看究竟是怎么回事?首先通过IDEA找到字节码文件
右键show in Explorer找到class文件所在路径
然后可以看到:
然后打开命令控制台,使用javap命令,反编译查看源码
javap -p ***.class
为啥反编译出来的是无惨构造器,那之前还说没找到无参构造方法呢。我都要自闭了
肯定是有一个是错误的,有可能是jdk反编译器骗了我们。
jad反编译
使用jad编译器(下载)。再去反编译看看。
运行命令后,帮我们生成了一个EnumSingleDemo.java文件
打开看看
哦,天哪,
这枚举类居然没有无参构造方法,只有有参构造方法,。
我们将我们的反射破坏单例方法进行修改,使用有参方法获取反射构造器,如下:
Constructor<EnumSingleton> constructor= EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
完整代码如下:
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; /** * @Author youjp * @Description //TODO= 使用枚举实现的单例模式 * @Date 2020-07-10$ 11:19$ * @throw **/ public class EnumSingleDemo { int i=0; private EnumSingleDemo(){ System.out.println("EnumSingleDemo被初始化 " + ++i + " 次"); } //定义一个枚举类 static enum EnumSingleton{ INSTANCE; private EnumSingleDemo singleDemo; private EnumSingleton(){ //私有化枚举的构造函数 singleDemo=new EnumSingleDemo(); } //对外暴露一个获取EnumSingleDemo对象的静态方法 public EnumSingleDemo getInstance(){ return singleDemo; } } public static EnumSingleDemo getInstance(){ return EnumSingleton.INSTANCE.getInstance(); } public static void main(String[] args) { try { reflectBreakSingle(); } catch (Exception e) { e.printStackTrace(); } } /** * 使用反射破坏单例 */ public static void reflectBreakSingle() { try { EnumSingleDemo lazyman=EnumSingleton.INSTANCE.getInstance(); //通过类对象获取构造器 Constructor<EnumSingleton> constructor= EnumSingleton.class.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); //无视类私有方法,可以获取到访问私有变量的权限 EnumSingleDemo lazyman1=constructor.newInstance().getInstance(); //调用无参构造函数 EnumSingleDemo lazyman2 = constructor.newInstance().getInstance(); System.out.println(lazyman); System.out.println(lazyman1); System.out.println(lazyman2); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
再次运行
试图使用反射破坏单例,结果报错了又。所谓魔高一尺道高一丈,查看一下源码
这里告诉我们如果这个类型是一个枚举类型,那么告诉我们不能使用反射破坏枚举。
现在我们才知道,反射确实不能破坏枚举的单例.
总结
共介绍了5类单例模式的写法,饿汉式、懒汉式、懒汉式DCL、静态内部类、枚举类。不建议使用懒汉式,简单的阔以使用饿汉式。涉及到反序列化创建对象时阔以使用枚举方式,这种方式最安全。如果考虑到延迟加载 的话,阔以采用静态内部类Holder的模式。如果对业务需求有特殊要求的时候阔以采用双检查锁的单例。
有兴趣的老爷,可以关注我的公众号【一起收破烂】,回复【006】获取2021最新java面试资料以及简历模型120套哦~