JUC并发编程学习(十八) -搞懂单例模式

简介: JUC并发编程学习(十八) -搞懂单例模式

小伙子,来谈恋爱啊?单例模式都不懂,谈个鬼哦

什么是单例模式

单例模式,顾明思议就是一个类只有一个实例,并且这个类负责创造自己的对象,同时确保只有单个对象被创建。这个类提供访问其唯一的对象的方式,我们在使用的时候可以直接调用其方法获取到,而不需要去实例化。

首先,来说一下单例模式的核心思想,构造方法私有,且需要一个静态方法用于获取对象实例。

 //单例模式核心思想,构造器私有!
    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();
        }
    }
}

20200401134307494.png

与饿汉式的区别就是,懒汉式类加载时较快,访问对象时较慢,是线程不安全的。在大多数场景下,都使用懒汉式,因为饿汉式有一个坏处就是会造成内存空间的浪费。那么我们就需要去解决懒汉式线程不安全的问题。

主要需要解决两个问题

  1. 线程的并发 问题:使用synchronized加锁即可
  2. 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();


这一条语句,并不是一个原子性操作,可能会存在指令重排的问题。

  1. java虚拟机在类加载以后,会分配内存给对象,在内存中开辟一段地址空间;
Singleton var = new Singleton();
  1. 对象的初始化;
var = init();
  1. 将分配好对象的地址指向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());
    }
}

运行结果

20200401134307494.png

可以看到,通过反射获取到对象构造函数来创建对象获取到的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());
    }
}

20200401134307494.png

可以看到静态方式编写的也是不安全的。那么有没有什么,能够防止反射序列化破坏呢?答案是有的。

尝试破坏枚举类

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();
        }
    }
}

运行结果:

20200401134307494.png

注意报错,这里提示这个枚举类里,没有一个空参的构造方法。诶呀,这就显得很奇怪了,我明明有写一个空参的构造方法呀,为啥会说没有。。。

  //定义一个枚举类
    static enum  EnumSingleton{
        INSTANCE;
        private EnumSingleDemo singleDemo;
        private EnumSingleton(){ //私有化枚举的构造函数
          singleDemo=new EnumSingleDemo();
        }
        //对外暴露一个获取EnumSingleDemo对象的静态方法
        public EnumSingleDemo getInstance(){
            return singleDemo;
        }
    }

我们反编译去看看究竟是怎么回事?首先通过IDEA找到字节码文件

20200401134307494.png

右键show in Explorer找到class文件所在路径

20200401134307494.png

然后可以看到:

20200401134307494.png

然后打开命令控制台,使用javap命令,反编译查看源码

javap -p ***.class

为啥反编译出来的是无惨构造器,那之前还说没找到无参构造方法呢。我都要自闭了

肯定是有一个是错误的,有可能是jdk反编译器骗了我们。

jad反编译

使用jad编译器(下载)。再去反编译看看。

20200401134307494.png

运行命令后,帮我们生成了一个EnumSingleDemo.java文件

20200401134307494.png

打开看看

20200401134307494.png

哦,天哪,

这枚举类居然没有无参构造方法,只有有参构造方法,。

我们将我们的反射破坏单例方法进行修改,使用有参方法获取反射构造器,如下:

 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();
        }
    }
}

再次运行

20200401134307494.png

试图使用反射破坏单例,结果报错了又。所谓魔高一尺道高一丈,查看一下源码

20200401134307494.png

这里告诉我们如果这个类型是一个枚举类型,那么告诉我们不能使用反射破坏枚举。

现在我们才知道,反射确实不能破坏枚举的单例.


总结


共介绍了5类单例模式的写法,饿汉式、懒汉式、懒汉式DCL、静态内部类、枚举类。不建议使用懒汉式,简单的阔以使用饿汉式。涉及到反序列化创建对象时阔以使用枚举方式,这种方式最安全。如果考虑到延迟加载 的话,阔以采用静态内部类Holder的模式。如果对业务需求有特殊要求的时候阔以采用双检查锁的单例。


有兴趣的老爷,可以关注我的公众号【一起收破烂】,回复【006】获取2021最新java面试资料以及简历模型120套哦~


相关文章
|
3月前
|
Java
【多线程面试题二十五】、说说你对AQS的理解
这篇文章阐述了对Java中的AbstractQueuedSynchronizer(AQS)的理解,AQS是一个用于构建锁和其他同步组件的框架,它通过维护同步状态和FIFO等待队列,以及线程的阻塞与唤醒机制,来实现同步器的高效管理,并且可以通过实现特定的方法来自定义同步组件的行为。
【多线程面试题二十五】、说说你对AQS的理解
|
3月前
|
Java
【多线程面试题十六】、谈谈ReentrantLock的实现原理
这篇文章解释了`ReentrantLock`的实现原理,它基于Java中的`AbstractQueuedSynchronizer`(AQS)构建,通过重写AQS的`tryAcquire`和`tryRelease`方法来实现锁的获取与释放,并详细描述了AQS内部的同步队列和条件队列以及独占模式的工作原理。
【多线程面试题十六】、谈谈ReentrantLock的实现原理
|
3月前
|
Java 程序员 容器
【多线程面试题二十四】、 说说你对JUC的了解
这篇文章介绍了Java并发包java.util.concurrent(简称JUC),它是JSR 166规范的实现,提供了并发编程所需的基础组件,包括原子更新类、锁与条件变量、线程池、阻塞队列、并发容器和同步器等多种工具。
|
6月前
|
安全 算法 Java
多线程知识点总结
多线程知识点总结
66 3
|
算法 Ubuntu C++
[总结] C++ 知识点 《四》多线程相关
[总结] C++ 知识点 《四》多线程相关
|
安全 Java
92. 你说你精通Java并发,那给我讲讲JUC吧(二)
92. 你说你精通Java并发,那给我讲讲JUC吧(二)
91 1
|
安全 Java 容器
92. 你说你精通Java并发,那给我讲讲JUC吧(一)
92. 你说你精通Java并发,那给我讲讲JUC吧(一)
170 0
92. 你说你精通Java并发,那给我讲讲JUC吧(一)
|
缓存 Java 编译器
JUC并发编程学习(十七) -5分钟搞懂volatile
JUC并发编程学习(十七) -5分钟搞懂volatile
JUC并发编程学习(十七) -5分钟搞懂volatile
|
SpringCloudAlibaba 安全 前端开发
JUC系列(一) 多线程基础复习
问:如何学习JUC? 答: 源码 + Java帮助文档 面试高频, juc 其实就是 Java.util 包下的线程分类的工具
JUC系列(一) 多线程基础复习
下一篇
无影云桌面