区块链技术博客
www.b2bchain.cn

单例模式 你还只停留在volatile关键字上吗

这篇文章主要介绍了单例模式 你还只停留在volatile关键字上吗的讲解,通过具体代码实例进行19480 讲解,并且分析了单例模式 你还只停留在volatile关键字上吗的详细步骤与相关技巧,需要的朋友可以参考下https://www.b2bchain.cn/?p=19480

本文实例讲述了2、树莓派设置连接WiFi,开启VNC等等的讲解。分享给大家供大家参考文章查询地址https://www.b2bchain.cn/7039.html。具体如下:

前言:如果你在面试过程中,面试官问你对于设计模式中的单例模式你了解多少或是说请说一说你对单例模式的理解?你会怎么回答呢?可能一开始你就慌了,也可能你张嘴就说单例模式分为饿汉式和懒汉式两种,然后就是顿“解说”,到最后给面试官的直觉就是会点皮毛。那么在面试过程中怎么回答才能让面试官对你刮目相看呢!那么它来了。

文章目录

  • 单例模式的实现
    • 饿汉式
    • 懒汉式
  • 懒汉式的多种实现
    • 同步代码块
    • 双重锁定检查(DCL)
      • volatile的作用
        • 禁止指令重排
        • 保证内存可见性
        • 不保证原子性
    • 静态内部类
  • 枚举实现(饿汉式)
    • 枚举实现单例模式的优点
  • 解决反射破坏单例模式
    • 反射破坏单例模式示例
    • 解决方案示例
  • 单例对象的序列化
  • 单例对象的克隆

单例模式的实现

一个类的对象在内存中只存在一个,那么这个类就是一个单例类,我们称这种类的设计模式为单例设计模式。那么怎么保证一个类在内存中只存在一个对象呢,首先就需要通过私有构造方法来禁止外界使用new关键字创建单例类的对象。

饿汉式

饿汉式单例模式在单例类加载的时候就会创建该类的对象,如果我们后期没有到这个对象,那么就会造成内存的浪费。但是饿汉式可以保证线程安全。有可能其他人的写法是将对象的创建写在静态代码块里面,其实都一样,无论是在静态代码块,还是在变量声明时创建对象,jvm都会统一进行初始化,所以这里两种写法没有本质上的区别。

/**  * 先来看一下 饿汉式 单例模式的 代码实现  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 15:45  */ public class HungrySingleton {      private static HungrySingleton instance = new HungrySingleton();      /* 写法二 使用静态代码块 来创建 对象     private static HungrySingleton SINGLETON = null;      static {         SINGLETON = new HungrySingleton();     }     */      //私有构造方法     private HungrySingleton() { }      public static HungrySingleton getInstance(){         return instance;     } }  

懒汉式

既然饿汉式单例模式出现可能会造成内存浪费的问题,那么懒汉式单例模式就是可以解决这个问题,但是又会面临着其他的问题!先看一看懒汉式的代码实现。

/**  * 该种实现方式 虽然解决了 内存浪费的问题 但是性能极差  * 每次获取对象都需要去获取锁 判断是否拥有锁对象  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 16:12  */ public class LazySingleton {      private static LazySingleton instance;      //私有 构造 方法     private LazySingleton(){}      //使用同步方法 加锁     public static synchronized LazySingleton getInstance(){         if (instance != null) {             instance = new LazySingleton();         }         return instance;     } } 

懒汉式的多种实现

上一种懒汉式单例模式的代码实现会存在性能上的问题,因为每次获取单例对象都需要去获得锁,所有性能上还可以进一步优化,那么就新的实现就诞生了。

同步代码块

说明:这些写法和上面的同步方法没有本质的区别,并没有解决性能差的问题,这里是为了让读者看到代码一步步的演变而实现,在面试过程中,就不要和面试官说这种实现方式了,可以先说一下上述使用同步方法实现单例模式,然后在说一说下面即将要说的双重锁定检查实现的单例模式。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 16:32  */ public class LazySingleton2 {      private static LazySingleton2 instance;      //私有 构造 方法     private LazySingleton2(){}      public static LazySingleton2 getInstance(){         //使用同步代码块保证 对象创建的原子性         synchronized(LazySingleton2.class){             if (instance == null) {                 instance = new LazySingleton2();             }         }         return instance;     } }  

双重锁定检查(DCL)

DCL:Double Check Lock,看到DCL别觉得有多高级,即双重锁检查。下面来看一下代码实现,我相信你看到代码就能秒懂为啥叫DCL;但是为什么要使用DCL这是你需要思考的问题!DCL能否解决之前存在的性能问题呢?答案是肯定的,但是它还会产生新的问题,这里会存在一个面试题,就是说DCL实现的懒汉单例模式也是线程不安全的,以下代码也会产生线程安全问题,但是可以通过volatile关键字来解决这个问题,在不考虑反射、反序列化和克隆的情况下,使用volatile和DCL实现的懒汉式单例模式就是线程安全的。这里大家可能都存在一个误区,那就只有是DCL实现就是线程不安全的,这是一个坑,希望你不要被面试官所坑,当然前提是不考虑反射、反序列化和克隆。下面来看一下DCL实现。

 /**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 16:59  */ public class LazySingleton3 {    	//不适合 生产环境下使用 此处为了学习使用 生产环境中需要使用volatile 关键字     private static LazySingleton3 instance;      //2.添加 volatile进制 instance对象创建时的指令重排 从而保证程序的正确性     //private static volatile LazySingleton3 instance;      private LazySingleton3(){}      public static LazySingleton3 getInstance(){         if (instance == null) {             synchronized (LazySingleton2.class) {                 if (instance == null) {                     instance = new LazySingleton3();                 }             }         }         return instance;     } } 

volatile的作用

禁止指令重排

在DCL中出现了volatile关键字,如果没有volatile修饰instance变量,那么就会存在线程安全问题,而在此处是为保证instance对象创建时jvm指令的顺序执行。什么意思呢,instance = new LazySingleton3(); 这个操作并不是原子操作,可以理解成jvm会给它生成三个指令,
1.先给instance变量在栈(帧)中分配内存,此时instance不指向任何对象仍然是空。
2.调用LazySingleton3类的构造方法来创建对象,并且给该对象在堆内存中分配内存地址。
3.让栈中的instance对象指向第二条指令创建的对象。但是在对象创建时会发生指令重排,也就是说第二条指令和第三条指令执行顺序会发生改变,这样在多线程下就会存在对象的引用不为空,但是堆中并没有完成该对象的初始化,这样去使用该对象就会抛出异常,导致程序报错。下图是我之前写的一些代码,变量名称可能与上述的对不上,但是表达的是一个意思。如果你还是不理解这个问题话请看下面这个例子:
在我们上学的时候,都会交作业,按照正常的逻辑顺序,应该是先写好作业,然后再上交,然后老师再批阅,但是有些年轻的学生不讲“武德”,等到交作业时先交一本空的作业本或是其他的作业本,然后再老师批阅作业时一顿猛补,如果等到老师批阅到你的作业时你还没有补完去换回之前的那本作业,那么老师就会找你谈谈,如果你在老师批阅到你的作业之前换回来了,那么就不会出现问题。这就好比指令重排,写作业相当于调用类的构造方法初始化对象,交作业相当于让栈中的变量指向堆中创建对象分配的内存地址,老师批阅作业相当于使用对象,如果使用前没有初始化完成就会出错。因为这个问题是在多线程情况下才产生的,单线程是不会存在这种问题的,所以也叫线程安全问题。

单例模式 你还只停留在volatile关键字上吗
指令重排示例程序,复制代码即可运行。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/12 15:35  */ public class VolatileDemo {      static Integer a = 0;     static Integer b = 0;     static Integer x = 0;     static Integer y = 0;      public static void main(String[] args) throws InterruptedException {         for (int i = 0; i < Integer.MAX_VALUE; i++) {              Thread t1 = new Thread(new Runnable() {                 @Override                 public void run() {                     System.out.println(Thread.currentThread().getName());                     // 有可能发生重排,即 先执行 x = b,再执行 a = 1                     a = 1;                     x = b;                  }             });              Thread t2 = new Thread(new Runnable() {                 @Override                 public void run() {                     System.out.println(Thread.currentThread().getName());                     // 有可能发生重排,即先执行 y = a,再执行 b = 1;                     b = 1;                     y = a;                 }             });              t1.start();             t2.start();             t1.join();             t2.join();              /*              * 预期结果: [x=0 y=1, x=1 y=0, x=1 y=1]              * 实际结果中会存在【x=0 y=0】这种情况出现              */             System.out.println("第" + i + "次, x=" + x + ", y=" + y);             if (x == 0 && y == 0) {                 break;             }             a = b = x = y = 0;         }     } } 

保证内存可见性

在解释volatile的内存可见性前,先来看下面这个程序。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 19:00  */ public class VolatileVisibility extends Thread {      boolean flag = true;      int count;      @Override     public void run() {         while (flag){             count = 9;         }     }      public static void main(String[] args) throws InterruptedException {         VolatileVisibility t1 = new VolatileVisibility();         t1.start();         Thread.sleep(1000);         t1.flag = false;         System.out.println(t1.count);     } }  

单例模式 你还只停留在volatile关键字上吗
分析程序运行的预期结果,主线程开启t1线程后,休眠了1000毫秒,然后将flag设置为false,去停止t1线程的while循环,最后打印count,预期结果是控制台打印9,程序运行结束,但是从实际运行结果来看,程序并没有停止运行。这是为什么呢?
首先,t1线程在运行时会将flag和count加载到自己的工作内存中,并不是直接操作主内存(堆内存)中的变量,同样主线程也是如此,当主线程执行完t1.flag = false这个操作后,会将flag从主线程的栈中写入到主内存中,但还是它并不会去强制t1线程中的flag失效,所有当主线程执行完成后,t1线程仍然还在运行,此时我们看到的结果是程序并没有停止,这里补充一点,flag从主线程的栈中写入到主内存中并不能保证及时写入,可能会存在延迟,单是这里不是因为这个原因,而是因为while循环中的操作执行太快,导致t1线程一直在使用flag和count,来不及将count写入主内存,就又开始使用count,也来不及去主内存中读取flag,导致程序无法停止。

不保证原子性

先看下面一个程序,count++和sum++操作都不是原子操作,但是synchronize保证了sum++的原子性,当两条线程执行完成后,都能保证sum的值准确,但还是count则不行,因为volatile不能保证原子性。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/12 21:15  */ public class App {      private volatile int count;      private int sum;      public void incrementCount(){         count++;     }      public synchronized void incrementSum(){         sum++;     }      public int getCount() {         return count;     }      public int getSum() {         return sum;     }       public static void main(String[] args) throws InterruptedException {         App app = new App();         for (int i = 0; i < Integer.MAX_VALUE; i++) {             Thread t1 = new Thread(new Runnable() {                 @Override                 public void run() {                     for (int j = 0; j < 10; j++) {                         app.incrementCount();                         app.incrementSum();                     }                 }             });              Thread t2 = new Thread(new Runnable() {                 @Override                 public void run() {                     for (int j = 0; j < 10; j++) {                         app.incrementCount();                         app.incrementSum();                      }                 }             });              t1.start();             t2.start();             t1.join();             t2.join();              System.out.println("count: " + app.count);             System.out.println("sum: " + app.sum);              if (app.count % 10 != 0 || app.sum % 10 != 0) {                 System.out.println("预期结果不正确");                 break;             }         }     } } 

静态内部类

然后再回到懒汉式单例模式,既然上述的单例模式这么麻烦,要考虑这么多问题,那么有没有更简单的,那么它来了,使用静态内部类实现。

/**  * 需要从类的加载过程去思考  * 类的加载分为 加载、连接、初始化三个过程  * 连接又分为 验证、准备、解析 三个过程  * 如果不懂每个过程需要做什么事情可以去思考一下人生 再回来看这段  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 19:55  */ public class LazySingleton4 {      //私有构造     private LazySingleton4(){}      private static class LazyHolder {         private static final LazySingleton4 INSTANCE = new LazySingleton4();     }      //只有 第一次调用该方法 才会导致 内部类 的加载和初始化其成员变量     public static LazySingleton4 getInstance(){         return LazyHolder.INSTANCE;     } } 

枚举实现(饿汉式)

下面我们来看一种非常少见的单例模式,使用枚举实现,但是这种方式实现只能是饿汉单例模式,也是推荐大家使用的一种。具体实现如下,非常简单的代码。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 20:12  */ public enum EnumSingleton implements Serializable {      INSTANCE;      public void doTask(){         //TODO 任务方法         System.out.println("do something...");     }      public static void main(String[] args) throws Exception {         //枚举 对象 序列化 //        EnumSingleton instance = EnumSingleton.INSTANCE; //        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\Users\zbowen\Desktop\aa.txt")); //        oos.writeObject(instance);          //枚举 对象 的反序列化         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\Users\zbowen\Desktop\aa.txt"));         EnumSingleton obj = (EnumSingleton) ois.readObject();         ois.close();         System.out.println(EnumSingleton.INSTANCE == obj);      } 

枚举实现单例模式的优点

1、避免反射攻击破坏单例模式,因为反射是通过newInstance创建对象,这个操作会检查该类是否被ENUM修饰,如果是就会抛出异常,无法创建对象。
2、解决序列化破坏单例模式,枚举对象序列化后,反序列化得到的依然是原来的单例对象,不会破坏单例模式。
3、代码简单、编写起来非常方便。

解决反射破坏单例模式

反射破坏单例模式示例

以上实现方式除了使用枚举实现能够保证单例模式不被破坏,其他实现方式都会被反射所破坏。以DCL实现方式为例,认识反射破坏单例模式的原理。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 16:59  */ public class LazySingleton3 {      //添加 volatile进制 instance对象创建时的指令重排 从而保证程序的正确性     private static volatile LazySingleton3 instance;      private LazySingleton3(){ 		System.out.println("执行了构造方法");     }      public static LazySingleton3 getInstance(){         if (instance == null) {             synchronized (LazySingleton3.class) {                 if (instance == null) {                     instance = new LazySingleton3();                 }             }         }         return instance;     }      public static void main(String[] args) throws Exception {         //通过 调用getInstance() 获取单例对象         LazySingleton3 instance = LazySingleton3.getInstance();         //打印单例对象         System.out.println("instance:" + instance);          //通过反射获取 单例对象         Class<LazySingleton3> lazyClass = LazySingleton3.class;         Constructor<LazySingleton3> constructor = lazyClass.getDeclaredConstructor();         //暴力 执行私有构造器 不执行权限检查         constructor.setAccessible(true);         //通过 newInstance 获取反射出的对象         LazySingleton3 refInstance = constructor.newInstance();         //打印反射出的对象         System.out.println("refInstance:" + refInstance); 		//比较两个对象是否是同一个对象         System.out.println("instance == refInstance:" + (instance == refInstance));     } } 

运行结果

单例模式 你还只停留在volatile关键字上吗

解决方案示例

从上述运行结果来看,无论是通过反射还是new关键字创建对象都会执行构造方法。我个人是这么认为的,只要代码能够执行进入到构造方法里面,那就代表这个对象已经创建了,即已经给这个对象分配了内存地址,刚好有个特殊的引用this(但是this这个引用只对内可见)指向这个内存地址,这个对象的成员变量也都赋值了默认的初始值。例如 instance = new Simple()在构造方法没有执行完成的情况下,instance还是为null(不考虑指令重排),从构造方法的定义上来说,构造方法就是用来初始化类的属性的,这么理解也没有问题。
所以这么解决反射问题,在构造方法中判断单例对象是否已经被创建了,如果被创建了,instance 就不会为空,此时如果再执行构造方法就是反射操作执行的,直接抛出异常即可,如果instance为空,那么在对象没有被创建的情况下允许反射第一次帮我们创建单例对象,如果后面再调用就会抛出异常,但是以上操作必须要同步,避免调用者使用多线程反射攻击,即使用多条线程同时通过反射创建该类的对象,这样也破坏了单例模式。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 21:27  */ public class LazySingleton3Plus {      //添加 volatile进制 instance对象创建时的指令重排 从而保证程序的正确性     private static volatile LazySingleton3Plus instance;      private LazySingleton3Plus(){         //synchronized 会阻塞当前 线程 此处的线程锁可以 和 getInstance()中的锁不一致         synchronized (LazySingleton3Plus.class){             if (instance != null){                 throw new RuntimeException("非法操作:禁止使用反射攻击创建对象...");             }             instance = this;         }     }      public static LazySingleton3Plus getInstance(){         if (instance == null) {             synchronized (LazySingleton3Plus.class) {                 if (instance == null) {                     instance = new LazySingleton3Plus();                 }             }         }         return instance;     }       //用于接收 反射 出的对象     static LazySingleton3Plus instance1, instance2;      public static void main(String[] args) throws Exception {          for (int i = 0; i < Integer.MAX_VALUE; i++) {             //通过反射获取 单例对象             Class<LazySingleton3Plus> lazyClass = LazySingleton3Plus.class;              Thread t1 = new Thread(new Runnable() {                 @Override                 public void run() {                     //通过反射获取 单例对象                     Constructor<LazySingleton3Plus> constructor = null;                     try {                         constructor = lazyClass.getDeclaredConstructor();                         //暴力 执行私有构造器 不执行权限检查                         constructor.setAccessible(true);                         //通过 newInstance 获取反射出的对象                         instance1 = constructor.newInstance();                     } catch (Exception e) {                         e.printStackTrace();                     }                 }             });              //这里使用 了 lambda 表达式简写了 如果不懂请看 t1             Thread t2 = new Thread(() -> {                 try {                     Constructor<LazySingleton3Plus> constructor = null;                     constructor = lazyClass.getDeclaredConstructor();                     //暴力 执行私有构造器 不执行权限检查                     constructor.setAccessible(true);                     //通过 newInstance 获取反射出的对象                     instance2 = constructor.newInstance();                 } catch (Exception e) {                     e.printStackTrace();                 }             });              t1.start();             t2.start();             t1.join();             t2.join();              System.out.println("instance1:" + instance1);             System.out.println("instance2:" + instance2);              //最多只有 一个对象被反射创建             if (instance1 != null && instance2 != null) {                 System.out.println("单例模式被破坏");                 break;             }         }     } } 

运行结果:多线程下去反射对象,也只会得到一个对象但是这并不代表着内存中只存在一个该类的实例,只是对外界只会存在一个引用指向一个对象。其实每一条线程反射都会创建一个对象,只不过对外界可见的只有一个。

单例模式 你还只停留在volatile关键字上吗

单例对象的序列化

在JDK1.8中,对象的反序列化已经不会执行构造方法了。对象的反序列化是一个深拷贝操作,这里对序列化和反序列化不做过多的解释,不懂的可以思考一下人生再来。对于反序列化会破坏单例模式的原因有两点:

第一,反序列化会重新创建一个新的对象。

 /**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 21:27  */ public class SingletonSerializable implements Serializable {      //添加 volatile进制 instance对象创建时的指令重排 从而保证程序的正确性     private static volatile SingletonSerializable instance;      private SingletonSerializable(){         System.out.println("构造方法执行了");         //synchronized 会阻塞当前 线程 此处的线程锁可以 和 getInstance()中的锁不一致         synchronized (SingletonSerializable.class){             if (instance != null){                 throw new RuntimeException("非法操作:禁止使用反射攻击创建对象...");             }             instance = this;         }     }      public static SingletonSerializable getInstance(){         if (instance == null) {             synchronized (SingletonSerializable.class) {                 if (instance == null) {                     instance = new SingletonSerializable();                 }             }         }         return instance;     }       public static void main(String[] args) throws Exception {         //获取单例对象         SingletonSerializable instance = SingletonSerializable.getInstance();         //对 单例对象进行 序列化         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\Users\zbowen\Desktop\aa.txt"));         oos.writeObject(instance);          //控制程序 的执行 查看对象的反序列化会不会执行 构造方法         System.in.read();          //反序列化         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\Users\zbowen\Desktop\aa.txt"));         SingletonSerializable obj = (SingletonSerializable) ois.readObject();          System.out.println("instance:" + instance);         System.out.println("obj:" + obj);         System.out.println("instance == obj: " + (instance == obj));     }  } 

运行结果:第一次输出“构造方法执行了”是调用getInstance()方法执行的,从运行结果可以看出,反序列化不会执行构造方法(jdk1.8下),同时,反序列化会创建一个新的对象,破坏了单例模式,那么如何解决呢?
单例模式 你还只停留在volatile关键字上吗
解决方法:通过查看readObject()源码,发现源码中是调用invokeReadResolve来创建对象的,然后通过反射出Object类的对象,来获得目标对象,我们只需在代码中添加一个readResolve方法即可,注意这个方法的参数为空,返回值必须为Object类型,实现返回单例对象即可。

/**  * @author BW ZHANG  * @version 1.0  * @date 2020/11/13 22:19  */ public class SingletonSerializablePlus implements Serializable {      //添加 volatile进制 instance对象创建时的指令重排 从而保证程序的正确性     private static volatile SingletonSerializablePlus instance;      private SingletonSerializablePlus(){         //synchronized 会阻塞当前 线程 此处的线程锁可以 和 getInstance()中的锁不一致         synchronized (SingletonSerializable.class){             if (instance != null){                 throw new RuntimeException("非法操作:禁止使用反射攻击创建对象...");             }             instance = this;         }     }      public static SingletonSerializablePlus getInstance(){         if (instance == null) {             synchronized (SingletonSerializablePlus.class) {                 if (instance == null) {                     instance = new SingletonSerializablePlus();                 }             }         }         return instance;     }       //要求:返回值必须为Object类型 不能写SingletonSerializablePlus     //方法参数必须为空  权限修饰符没有限制 建议写成 private     private Object readResolve(){         return instance;     }      public static void main(String[] args) throws Exception {         //获取单例对象         SingletonSerializablePlus instance = SingletonSerializablePlus.getInstance();         //对 单例对象进行 序列化         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\Users\zbowen\Desktop\aa.txt"));         oos.writeObject(instance);          //反序列化         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\Users\zbowen\Desktop\aa.txt"));         SingletonSerializablePlus obj = (SingletonSerializablePlus) ois.readObject();          System.out.println("instance:" + instance);         System.out.println("obj:" + obj);         System.out.println("instance == obj : " + (instance == obj));     } } 

运行结果:这样反序列化破坏单例模式的第一个问题得到了解决。
单例模式 你还只停留在volatile关键字上吗
第二,如果我先对单例对象逐个进行序列化,这里逐个进行序列化的对象并不是同一个对象,而是先获取单例对象,进行序列化,然后程序运行结束,再获重新运行程序获取一个单例对象,然后序列化到另一个文件中,此时被序列化的这两个对象就不是同一个对象了,然后分别对着两个对象进行反序列化,判断这两个对象是否为同一个对象。

单例对象的克隆

本文转自互联网,侵权联系删除单例模式 你还只停留在volatile关键字上吗

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 单例模式 你还只停留在volatile关键字上吗
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们