设计模式之单例模式并发反射安全

前言

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式常见分为懒汉式和饿汉式,具体介绍参考菜鸟教程

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

一. 饿汉式

饿汉式的特点

  1. 初始化直接创建对象
  2. 如果创建大量不使用的对象就是资源的浪费
  3. 饿汉式不会出现线程问题
  4. 反射可能会出现非单例问题

如果不了解并发下懒汉式和饿汉式,直接饿汉式就可以了,不会出现并发问题

代码实现饿汉式

package com.concurrent.demo20Single;

/**
 * 饿汉式
 * 1. 直接创建对象
 * 2. 如果创建大量不使用的对象就是资源的浪费
 * 3. 饿汉式不会出现线程问题
 * 4. 反射可能会出现非单例问题
 * @author lane
 * @date 2021年05月29日 下午4:44
 */
public class SingleDemoHungry {
  // 这就是浪费空间的代码
/*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];*/

    public static SingleDemoHungry singleDemoHungry = new SingleDemoHungry();
  
    private SingleDemoHungry(){};
   
    public static SingleDemoHungry getInstance(){

        return singleDemoHungry;

    }
}

饿汉式测试

package com.concurrent.demo20Single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author lane
 * @date 2021年05月29日 下午4:48
 */
public class SingleTest {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {

        hungryTest();
        
    }

    //测试饿汉式
    public static void hungryTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //正常测试懒汉式单例
        SingleDemoHungry singleDemoHungry1  = SingleDemoHungry.getInstance();
        SingleDemoHungry singleDemoHungry2  = SingleDemoHungry.getInstance();
        System.out.println("饿汉式测试是否单例:"+(singleDemoHungry1==singleDemoHungry2));//true
        //反射测试懒汉
        Class<SingleDemoHungry> singleDemoHungryClass = SingleDemoHungry.class;
        Constructor<SingleDemoHungry> constructor = singleDemoHungryClass.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        SingleDemoHungry singleDemoHungry11 = constructor.newInstance();
        System.out.println("饿汉式测试反射是否单例:"+(singleDemoHungry11==singleDemoHungry1));//false

    }
}

//打印结果
饿汉式测试是否单例:true
饿汉式测试反射是否单例:false

结果表明了不使用反射情况下可以实现单例效果,但是反射创建对象的话无法保证单例

想要保证单例只需要修改下构造方法为如下即可

private SingleDemoHungry(){
        if (singleDemoHungry!=null){
            throw new RuntimeException("小老弟,没想到吧,防御一手");
        }

    }

image-20210529225006748

二. 懒汉式

懒汉式特点

  1. 需要的时候才创建
  2. 多线程会出现问题
  3. 资源不会像饿汉那样浪费
  4. 反射仍会出现问题需要防御两手

代码实现懒汉式

package com.concurrent.demo20Single;

/**
 * 饿汉式
 * 为了线程安全需要加volatile和synchronized
 * @author lane
 * @date 2021年05月29日 下午10:53
 */
public class LazyDemo {

    private static volatile LazyDemo lazyDemo ;

    private LazyDemo(){}
     /**
     *  lazyDemo = new LazyDemo();非原子性操作
     * 1. 分配内存空间
     * 2、执行构造方法,初始化对象
     * 3、把这个对象指向这个空间
     * 
     * 123 单线程正常 123执行
     * 132 指令重排 变成 132 执行
     * 多线程 A 指令重排执行到13的时候 ,多线程B进入第一个判断发现已经分配空间
     * 开始执行return 并继续操作,实际对象并没有初始化,出现问题
     * 所以需要加上volatile关键字来避免指令重排
     * DCL懒汉式 就是双重加锁检测
     */
    public static  LazyDemo getInstance(){

        if (lazyDemo==null){
        synchronized (LazyDemo.class) {
            if (lazyDemo ==null){
              //非原子性操作
            lazyDemo = new LazyDemo();}
        }
        }
        return  lazyDemo;

    }

}

对于添加volatile关键字的解读

private static volatile LazyDemo lazyDemo ;这行代码上为什么加上volatile的解读

lazyDemo = new LazyDemo();

这行代码并非原子性操作

  1. 分配内存空间

  2. 执行构造方法,初始化对象

  3. 把这个对象指向这个空间

假如在下面这种情况下会出现问题

  1. 123 单线程正常 123执行

  2. 132 指令重排 变成 132 执行

  3. 多线程 A 指令重排执行到13的时候 ,多线程B进入第一个判断是否为null 发现已经分配空间

  4. 开始执行return 并继续操作,实际对象并没有初始化,出现问题

需要加上volatile关键字来避免指令重排

Volatile 可以保证可见行和避免指令重排,但是不能保证原子性

懒汉式测试

package com.concurrent.demo20Single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author lane
 * @date 2021年05月29日 下午4:48
 */
public class SingleTest {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {

      //  hungryTest();
        LazyTest();
        
    }
    //懒汉式测试
    private static void LazyTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyDemo lazyDemo1 =LazyDemo.getInstance();
        LazyDemo lazyDemo2 =LazyDemo.getInstance();
        System.out.println("懒汉式测试是否单例:"+(lazyDemo1==lazyDemo2));//true
        //反射创建
        Class<LazyDemo> lazyDemoClass = LazyDemo.class;
        //反射获取无参构造器
        Constructor<LazyDemo> declaredConstructor = lazyDemoClass.getDeclaredConstructor(null);
       //允许访问私有构造 
        declaredConstructor.setAccessible(true);
        LazyDemo lazyDemo11 = declaredConstructor.newInstance();
        System.out.println("懒汉式测试反射是否单例:"+(lazyDemo1==lazyDemo11));//false

    }

}

测试结果

image-20210529231320778

发现懒汉式单例在反射依然不够安全,需要修改一下构造函数

 private LazyDemo(){
        if(lazyDemo!=null){
            throw new RuntimeException("懒汉式防御第一手");
        }
    }

测试结果

image-20210529231552595

反射再修改一下也会导致懒汉式再次变得不安全

        LazyDemo lazyDemo11 = declaredConstructor.newInstance();
        LazyDemo lazyDemo12 = declaredConstructor.newInstance();
        System.out.println("懒汉式测试反射是否单例:"+(lazyDemo11==lazyDemo12));//false

测试结果

image-20210529231843347

构造函数再修改下避免这种反射问题

   private static int num = 0;
    private LazyDemo(){

        if(lazyDemo!=null){
            throw new RuntimeException("懒汉式防御第一手");
        }

        if (num==0){
            num =1;
        }else {
            throw new RuntimeException("懒汉式防御第二手");

        }

    }

再次测试结果

image-20210529232344514

以上基本就可以保证单例模式了,如果攻击者知道我们定义的字段是num的话,每次修改一下num的初始值也是无法保证单例的。

 //除非知道字段否名称,否则无法再次破坏了
        Field num = lazyDemoClass.getDeclaredField("num");
         LazyDemo lazyDemo11 = declaredConstructor.newInstance();
         num.set(lazyDemo11,0);
        LazyDemo lazyDemo12 = declaredConstructor.newInstance();
         System.out.println("懒汉式测试反射是否单例:"+(lazyDemo11==lazyDemo12));//false

官方对于反射,有完全安全的方式就是枚举类

三. 静态内部类单例模式

package com.concurrent.demo20Single;

/**
 * 静态内部类创建单例模式
 * @author lane
 * @date 2021年05月30日 上午12:02
 */
public class InnerDemo {

    private InnerDemo(){
      // System.out.println(Thread.currentThread().getName()+"单例创建");
    }

    public static InnerDemo getInstance(){
        return InnerClass.innerDemo;
    }
  //静态内部类
    private static class InnerClass {
        private static final InnerDemo innerDemo = new InnerDemo();

    }
  
/* public static void main(String[] args) {
        //测试下线程是否安全的 //Thread-0单例创建
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                InnerDemo innerDemo = InnerDemo.getInstance();
            }).start();
        }

        InnerDemo innerDemo1 = InnerDemo.getInstance();
        InnerDemo innerDemo2 = InnerDemo.getInstance();
        System.out.println(innerDemo1 == innerDemo2);
    }*/


}

四. 枚举类单例模式

枚举类不会出现线程问题,反射也无法破坏枚举类

代码实现

package com.concurrent.demo21enum;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 枚举单例
 * @author lane
 * @date 2021年05月31日 下午6:11
 */
public enum  Single {
    INS;

    private Single (){}

    public static Single getInstance(){
        return INS;
    }

}

class EnumDemo {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Single instance1 = Single.getInstance();
        Single instance2 = Single.getInstance();
        System.out.println("枚举是否单例" +(instance1 == instance2));

        Class<Single> singleClass = Single.class;
        //因为枚举默认继承Enum类,必须两个参数的构造
//        singleClass.getDeclaredConstructor(null);
        Constructor<Single> declaredConstructor = singleClass.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        Single single1 = declaredConstructor.newInstance();
        Single single2 = declaredConstructor.newInstance();
        System.out.println("反射下看枚举单例能否被破坏:"+(single1==single2));


    }

}

测试结果

image-20210531183456467

源码介绍

image-20210530012253081

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值