Java JUC并发之单例模式的巧妙用法

十八、单例模式

程序员必会!!!

1、饿汉式

//  饿汉式单例
public class Hungry {

    // 在饿汉式单例下 这些资源一起全部加载进来
    // 会造成空间浪费
    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];
    private byte[] data4 = new byte[1024];

    private Hungry() { // 构造器私有

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }
}

饿汉式与懒汉式的本质区别:

  • 饿汉式是 线程安全 的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变;懒汉式 如果在创建实例对象时不加上synchronized,则会导致对对象的访问不是 线程安全 的

  • 从实现方式来讲:懒汉式是延时加载,只有在需要的时候才创建对象;

    饿汉式在虚拟机启动的时候就会创建,饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例、所以如果是一个 工厂模式 ,缓存了很多实例、那么就得考虑效率问题,因为这个类一加载,则把所有实例不管用不用一块创建。

2、懒汉式

双重检查锁(Double Checked Locking)

在单线程情况下,实现单例模式是安全的,但是如果考虑多线程,就可能会出现问题,导致出现多个LazyMan的实例!

原因:考虑可能有两个线程同时调用getInstance(),LazyMan就会被实例化两次 并且被不同对象持有,完全违背单例模式的初衷。

解决方法一 加锁

// 懒汉式单例-加锁
public class LazyMan {

    private LazyMan() {
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance() {
         synchronized (LazyMan.class){
             if (lazyMan == null) {
                 lazyMan = new LazyMan();   
                }
            }
        }
        return lazyMan;
    }
}

加锁虽然能解决问题,但是因为用到了synchronized,会导致很大的性能开销,并且每次初始化时都会加锁,性能浪费。

解决方法二、双重检查锁(有缺陷)

先判断对象是否已被初始化,再决定要不要加锁

// 懒汉式单例-加锁
public class LazyMan {

    private LazyMan() {
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance() {
        if(lazyMan == null){
             synchronized (LazyMan.class){
             if (lazyMan == null) {
                 lazyMan = new LazyMan();   
                }
            }
        }
        }
        return lazyMan;
    }
}

这样写的话,运行顺序就会变成:

  1. 检查变量是否被初始化(先不去获得锁),如果已被初始化则立即返回
  2. 获得锁
  3. 再次检查变量是否已被初始化,如果还没被初始化,就初始化一个对象

执行双重检查是因为:如果多个线程同时通过了第一次检查,并且其中一个线程首先通过第二次检查并实例化了对象,其余通过了第一次检查的线程就不会再去实例化对象。

双重检查的好处:除了初始化的时候会加锁,后续的所有调用都会避免枷锁,直接返回解决了性能消耗的问题

DCL 懒汉式仍然存在缺陷!

lazyMan = new LazyMan();

实例化上述对象的过程并不是原子性操作,它可以分为三步(编译器层面):

  1. 分配内存空间
  2. 初始化对象,执行构造方法
  3. 将对象指向刚才分配的内存空间

但是底层的编译器为了性能,可能会对这三步操作进行重排序(指令重排)

执行顺序可能会变成 1-3-2

假设A线程先通过第一次判断,获得锁,并且A线程调用顺序为1-3-2,执行完3之后,B线程刚好检查到 lazyMan 不为空,便返回一个未初始化完成的对象,于是产生了两个实例!

完整的DLC懒汉式 => 对lazyMan 加上 volatile 关键字 禁止指令重排

package com.liu.single;

// 懒汉式单例
public class LazyMan {

    private LazyMan() {

        System.out.println(Thread.currentThread().getName() + "LazyMan ok");

    }

    private volatile static LazyMan lazyMan; // volatile 禁止指令重排


    public static LazyMan getInstance() {

        // 双重检测锁模式   懒汉式单例 DCL (Double Check Locking)
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null) {
                    lazyMan = new LazyMan(); // new 关键字 非原子性操作 可能会出现指令重排
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        // 多线程并发

        // 不安全
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

小结 : 单例模式下使用volatile,可以禁止指令重排!

DLC + volatile 才是完整的懒汉式单例

静态内部类的懒汉式

package com.liu.single;

// 使用静态内部类

public class Holder {

    private Holder() {

    }

    private static Holder getInstance() {
        return InnerClass.HOLDER;
    }


    public static class InnerClass{

        private static final Holder HOLDER = new Holder();


    }
}

以上的单例都不安全 = > 因为有反射的存在,可以轻松破坏单例的安全性

package com.liu.single;

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

// 懒汉式单例
public class LazyMan02 {

    private static boolean liu = false; // 关键字可以加密 防止反射破坏


    private LazyMan02() {

        synchronized (LazyMan02.class) {
            if (liu == false) {
                liu = true;
            }else {
                throw new RuntimeException("不要试图用反射破坏代码!");
            }
        }
        //System.out.println(Thread.currentThread().getName() + "LazyMan ok");

    }

    private volatile static LazyMan02 lazyMan; // volatile 禁止指令重排


    public static LazyMan02 getInstance() {

        // 双重检测锁模式   懒汉式单例 DCL (Double Check Locking)
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null) {
                    lazyMan = new LazyMan02(); // new 关键字 非原子性操作 可能会出现指令重排
                }
            }
        }
        return lazyMan;
    }

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

        // 反射方法 :getDeclaredFiekd() 获得该类中声明的所有字段
        Field liu = LazyMan02.class.getDeclaredField("liu");
        // 反射 => 可以破坏单例
        //LazyMan02 instance = LazyMan02.getInstance();

        Constructor<LazyMan02> declaredConstructor = LazyMan02.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        LazyMan02 instance = declaredConstructor.newInstance();

        liu.set(instance, false); // 反射可以破坏单例的安全性

        LazyMan02 instance02 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance02);
    }
}


使用枚举类可以防止反射的破坏

package com.liu.single;

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

/**
 * Enum 枚举
 * 本身也是一个类
 */
public enum EnumSingle {

    INSTANCE;

    private EnumSingle() {
    }
    public EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        EnumSingle instance = EnumSingle.INSTANCE;

        //java.lang.NoSuchMethodException:
         Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);

        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

通过IDEA查看枚举类底层源码 => 无参构造器,使用反射之后,发现报的错误并不是我们预想的结果

Java JUC并发之单例模式的巧妙用法

使用无参构造的Enum:报错 => java.lang.NoSuchMethodException

预期报错: java.lang.IllegalArgumentException: Cannot reflectively create enum objects

Java反编译工具Jad 下载 :https://www.jianshu.com/p/5d8736d9a32a

使用命令javap -p EnumSingle.class进行编译:

可以看到编译之后的.java文件里面也是使用无参构造!

Java JUC并发之单例模式的巧妙用法

枚举类型的最终反编译源码如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingle.java

package com.liu.single;

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

public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/liu/single/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i); // 证实枚举类使用的是有参构造,参数分别是String和int
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static void main(String args[])
        throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException
    {
        EnumSingle instance = INSTANCE;
        Constructor declaredConstructor = com/liu/single/EnumSingle.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = (EnumSingle)declaredConstructor.newInstance(new Object[0]);
        System.out.println(instance);
        System.out.println(instance2);
    }

    private static EnumSingle[] $values()
    {
        return (new EnumSingle[] {
            INSTANCE
        });
    }

    public static final EnumSingle INSTANCE = new EnumSingle("INSTANCE", 0);
    private static final EnumSingle $VALUES[] = $values();

}

将Enum类中通过反射传入两个参数作为构造器的参数 : String.class int.class

package com.liu.single;

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

public enum EnumSingle {

    INSTANCE;

    private EnumSingle() {

    }

    public EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        EnumSingle instance = EnumSingle.INSTANCE;

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); // 使用String.class 和 int.class作为构造器的参数

        declaredConstructor.setAccessible(true);

        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

其报错结果为:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

即:无法通过反射来创建枚举类的实例 => 单例模式下使用枚举enum可以有效阻止反射,保证单例模式的安全性不被破坏!

上一篇:JUC学习


下一篇:Juc 概述