饿汉式单例模式的缺点_写一个简单的单例模式

(30) 2024-08-24 17:01:01

文章目录

  • 单例模式
  • 饿汉式
  • 懒汉式
    • 方式一
    • 方式二 DCL(double check lock)+volatile关键字
      • volatile可以保证可见性,但不能保证原子性。可以避免指令重排现象的产生。
    • 以上就安全了吗?不,反射可以破坏!
      • 解决方案,再在getInstance中加一把锁,来解决
    • 以上反射的破坏方案解决了,就安全了吗?不,如果一开始不通过getInstance创建,就又不安全了。
      • 解决办法,非当前的对象-红绿灯
    • 上一步的保护就安全了吗?不,如果通过反编译,获得了那个红绿灯标志位,就能够破坏。
    • 那就上枚举解决!
      • 试图通过反射来破坏枚举
      • 枚举的真实的构造函数是什么?
      • 下面开始破坏!但是破坏失败。说明,反射确实无法破坏枚举的单例

单例模式

  • 目的:保证实例全局唯一

  • 构造器私有!!!

饿汉式

  • 一上来就加载!可能会浪费内存

    package com.tiko.single; public class Hungry { 
          private byte[] data1 = new byte[1024]; private byte[] data2 = new byte[1024]; private Hungry(){ 
          } private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ 
          return HUNGRY; } } 

懒汉式

方式一

package com.tiko.single; public class LazyMan { 
    private LazyMan(){ 
    System.out.println(Thread.currentThread().getName()); } 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(double check lock)+volatile关键字

volatile可以保证可见性,但不能保证原子性。可以避免指令重排现象的产生。

package com.tiko.single; public class LazyMan { 
    private LazyMan(){ 
    System.out.println(Thread.currentThread().getName()); } private static volatile LazyMan LAZYMAN; //双重检测锁模式,懒汉式单例,DCL懒汉式 public static LazyMan getInstance(){ 
    if (LAZYMAN==null){ 
    synchronized (LazyMan.class){ 
   //这里的同步锁 类名.class是什么意思? if (LAZYMAN == null){ 
    LAZYMAN = new LazyMan();//不是原子性操作 /** * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把这个对象指向这个操作 * 在这些操作中,存在指令重排,我们希望是123,可能执行的时候是132 * 所以为了避免指令重排,需要在对象LAZYMAN加上volatile关键字,来避免指令重排 * */ } } } return LAZYMAN; } //以上的代码,在单线程下是没有问题的,但是在多线程下是有问题,以下是测试 public static void main(String[] args) { 
    for (int i = 0; i < 10; i++) { 
    new Thread(() -> { 
    LazyMan.getInstance(); }).start(); } } } 

以上就安全了吗?不,反射可以破坏!

package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class LazyMan { 
    private LazyMan(){ 
    System.out.println(Thread.currentThread().getName()); } private static volatile LazyMan LAZYMAN; //双重检测锁模式,懒汉式单例,DCL懒汉式 public static LazyMan getInstance(){ 
    if (LAZYMAN==null){ 
    synchronized (LazyMan.class){ 
   //这里的同步锁 类名.class是什么意思? if (LAZYMAN == null){ 
    LAZYMAN = new LazyMan();//不是原子性操作 /** * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把这个对象指向这个操作 * 在这些操作中,存在指令重排,我们希望是123,可能执行的时候是132 * 所以为了避免指令重排,需要在对象LAZYMAN加上volatile关键字,来避免指令重排 * */ } } } return LAZYMAN; } //以上的代码,在单线程下是没有问题的,但是在多线程下是有问题,以下是测试 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    LazyMan instance1 = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 程序输出: main main com.tiko.single.LazyMan@1540e19d com.tiko.single.LazyMan@b6 很明显,结果不符合单例的要求! 

解决方案,再在getInstance中加一把锁,来解决

package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class LazyMan { 
    private LazyMan(){ 
    synchronized (LazyMan.class){ 
    if(LAZYMAN!=null){ 
    throw new RuntimeException("不要通过反射来破坏!"); } } System.out.println(Thread.currentThread().getName()); } private static volatile LazyMan LAZYMAN; //双重检测锁模式,懒汉式单例,DCL懒汉式 public static LazyMan getInstance(){ 
    if (LAZYMAN==null){ 
    synchronized (LazyMan.class){ 
   //这里的同步锁 类名.class是什么意思? if (LAZYMAN == null){ 
    LAZYMAN = new LazyMan();//不是原子性操作 /** * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把这个对象指向这个操作 * 在这些操作中,存在指令重排,我们希望是123,可能执行的时候是132 * 所以为了避免指令重排,需要在对象LAZYMAN加上volatile关键字,来避免指令重排 * */ } } } return LAZYMAN; } //以上的代码,在单线程下是没有问题的,但是在多线程下是有问题,以下是测试 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    LazyMan instance1 = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 结果输出: Exception in thread "main" java.lang.reflect.InvocationTargetException ... at com.tiko.single.LazyMan.main(LazyMan.java:47) Caused by: java.lang.RuntimeException: 不要通过反射来破坏! 

以上反射的破坏方案解决了,就安全了吗?不,如果一开始不通过getInstance创建,就又不安全了。

package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class LazyMan { 
    private LazyMan(){ 
    synchronized (LazyMan.class){ 
    if(LAZYMAN!=null){ 
    throw new RuntimeException("不要通过反射来破坏!"); } } System.out.println(Thread.currentThread().getName()); } private static volatile LazyMan LAZYMAN; //双重检测锁模式,懒汉式单例,DCL懒汉式 public static LazyMan getInstance(){ 
    if (LAZYMAN==null){ 
    synchronized (LazyMan.class){ 
   //这里的同步锁 类名.class是什么意思? if (LAZYMAN == null){ 
    LAZYMAN = new LazyMan();//不是原子性操作 /** * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把这个对象指向这个操作 * 在这些操作中,存在指令重排,我们希望是123,可能执行的时候是132 * 所以为了避免指令重排,需要在对象LAZYMAN加上volatile关键字,来避免指令重排 * */ } } } return LAZYMAN; } //以上的代码,在单线程下是没有问题的,但是在多线程下是有问题,以下是测试 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    // LazyMan instance1 = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance1 = declaredConstructor.newInstance(); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 程序输出: main main com.tiko.single.LazyMan@1540e19d com.tiko.single.LazyMan@b6 违背单例 

解决办法,非当前的对象-红绿灯

package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class LazyMan { 
    private static boolean unknownFlag = false; private LazyMan(){ 
    synchronized (LazyMan.class){ 
    if (unknownFlag == false){ 
    unknownFlag = true; }else { 
    throw new RuntimeException("不要通过反射来破坏!"); } } System.out.println(Thread.currentThread().getName()); } private static volatile LazyMan LAZYMAN; //双重检测锁模式,懒汉式单例,DCL懒汉式 public static LazyMan getInstance(){ 
    if (LAZYMAN==null){ 
    synchronized (LazyMan.class){ 
   //这里的同步锁 类名.class是什么意思? if (LAZYMAN == null){ 
    LAZYMAN = new LazyMan();//不是原子性操作 /** * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把这个对象指向这个操作 * 在这些操作中,存在指令重排,我们希望是123,可能执行的时候是132 * 所以为了避免指令重排,需要在对象LAZYMAN加上volatile关键字,来避免指令重排 * */ } } } return LAZYMAN; } //以上的代码,在单线程下是没有问题的,但是在多线程下是有问题,以下是测试 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    // LazyMan instance1 = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance1 = declaredConstructor.newInstance(); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 结果输出: at com.tiko.single.LazyMan.main(LazyMan.java:51) Caused by: java.lang.RuntimeException: 不要通过反射来破坏! 

上一步的保护就安全了吗?不,如果通过反编译,获得了那个红绿灯标志位,就能够破坏。

那就上枚举解决!

package com.tiko.single; public enum EnumSingle { 
    INSTANCE; public static EnumSingle getInstance() { 
    return INSTANCE; } } //测试一下 class Test{ 
    public static void main(String[] args) { 
    EnumSingle instance = EnumSingle.INSTANCE; EnumSingle instance1 = EnumSingle.getInstance(); EnumSingle instance2 = EnumSingle.getInstance(); System.out.println(instance); System.out.println(instance1); System.out.println(instance2); } } 结果输出: INSTANCE INSTANCE INSTANCE 符合单例要求 

试图通过反射来破坏枚举

package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum EnumSingle { 
    INSTANCE; public static EnumSingle getInstance() { 
    return INSTANCE; } } //测试一下 class Test{ 
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    EnumSingle instance1 = EnumSingle.INSTANCE; //后来验证,反射获取枚举类的无参构造是假的,被源码骗了 Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 结果输出: Exception in thread "main" java.lang.NoSuchMethodException: 

枚举的真实的构造函数是什么?

  • 反编译,cmd->javap -p EnumSingle.class

    • 发现并么有问题?那问题出现在哪里了呢?
  • jad工具:cmd->jad -sjava EnumSingle.class

    发现枚举类的构造函数如下:

    饿汉式单例模式的缺点_写一个简单的单例模式 (https://mushiming.com/)  第1张

​ 源码真得骗人了!!!

下面开始破坏!但是破坏失败。说明,反射确实无法破坏枚举的单例

 Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); 
package com.tiko.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum EnumSingle { 
    INSTANCE; public static EnumSingle getInstance() { 
    return INSTANCE; } } //测试一下 class Test{ 
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
    EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } } 结果输出: Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects 说明反射确实无法破坏枚举的单例 

参考自狂神说

THE END

发表回复