page contents

java单例模式为什么要加双重锁?为什么要加volatile?

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对...

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

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

实现

我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。

SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo,我们的演示类使用 SingleObject 类来获取 SingleObject 对象。

程序员:java单例模式,为什么要加双重锁?为什么要加volatile?

//final类不可继承

final public class Single {

//使用volatile修饰变量

private static Single single = null;

public static Single create() {

//第一次验校

if (single == null) {

//同步代码块(类锁)

synchronized (Single.class) {

//第一次验校

if (single == null) {

single = new Single();

}

}

}

return single;

}

/**

* 私有构造函数,外部访问不了

*/

private Single() {

}

}

这是一个典型的双重锁单例模式,在很多单例源码中经常可以看见。

1.为什么要进行第一次判空

我们知道单例模式只有第一次执行create()方法的时候才会走synchronized 中的代码,后面再次访问的时候直接返回single 对象。如果说我们没有第一次验校,每一个线程都要走synchronized 中的代码,而每一次线程都要去拿到同步锁才能执行。在多线程的情况下每一个线程要拿到single 对象都要排队等待同步锁释放。因此第一次验校作用就是为了提高程序的效率。

2.为什么要进行第二次判空

举个例子:假如现在没有第二次验校,线程A执行到第一次验校那里,它判断到single ==null。此时它的资源被线程B抢占了,B执行程序,进入同步代码块创建对象,然后释放同步锁,此时线程A又拿到了资源也拿到了同步锁,然后执行同步代码块,因为之前线程A它判断到single ==null,因此它会直接创建新的对象。所以就违反了我们设计的最终目的。

3.变量为什么要加volatile关键字

在上面例子中volatile保证代码指令不会被重排序,首先我们得先了解什么是volatile关键字与及它的特性。

volatile关键字的特性

volatile具有可见性、有序性,不具备原子性。

注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,这一点在面试中也是非常容易问到的点。

原子性:不可打断的操作,要么成功要么失败。例如基本数据类型的读写操作都是属于原子性,int a = 10这一过程就是原子性(即可以看成当一个线程执行int a = 10这句代码时其他线程是处于等待获取cpu资源的状态,因为线程并发是通过cpu调度交替执行,并不是真正的并行执行),但是像a++这样的运算操作就是非原子性,因为a++虚拟机要运行三个指令:读取a,a+1,a赋值,这个过程是允许打断的。

volatile具有可见性是指当一个线程对变量进行原子操作的时候,另外的线程能立即获取到最新的数据。

java如何实现可见性需要了解Java的内存模型:

程序员:java单例模式,为什么要加双重锁?为什么要加volatile?

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在。它描述了不同线程访问共享变量的规则。

我们程序所有的共享变量都是放在主内存中,而线程会从主内存拷贝它需要的共享变量到自己的工作内存。

线程之间变量的传递则需要靠主内存。

Java内存模型规定对共享变量的操作只能在自己工作内存进行,即我读一个共享变量时需要先从工作内存找。写的时候也需要修改工作内存的变量后再push到主内存。

在多线程并发的情况下会发生什么问题?

假如主内存中有个共享变量 int a = 10,线程A和线程B的工作内存都有这个变量的副本。假如线程A在自己工作内存中修改了a的值,int a = 20,此时线程A还未来得及push到主内存中线程B就已经读取了a的值,线程B读取到的值是10,因此就造成了数据的混乱。

而java的volatile保证了共享变量的可见性:volatile修饰的变量当线程在工作内存修改后会立马push到主内存中,同时会把工作内存的变量设置为禁止读取,因此访问这个变量的时候不能从自己的工作内存访问,必须要去主内存中取。

还是举上面的例子:变量a被volatile修饰 volatile int a = 10,线程A修改了a的值立马push到主存中,线程B访问的时候到主存访问就可以得到最新的值。

其次volatile还可以禁止虚拟机对指令进行重排序。指令重排作用是虚拟机在不影响单线程程序执行结果的前提下对指令重新排序,提高程序运行效率。但是重排序在多线程并发的情况下也是容易出现问题的。

在上诉单例模式中volatile保证了虚拟机执行字节码的时候指令不会重排序。

single = new Single() 在我们看来就是一句话操作而已,但在虚拟机看来它一共分为了几个指令操作:

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将引用指向对象的内存空间地址

虚拟机执行的时候不一定是按顺序123的执行,也有可能是132。这是虚拟机的重排序引起的,单线程情况下是没有什么bug的,最终都会创建出对象,只是先后顺序不同。

但是在上面例子中会出现这么一种情况:

假如线程A执行 single = new Single()虚拟机是按132排序执行,当执行到3的时候single 引用已经不为空。此时若线程B执行到第一次验校处(第一次验校不在同步代码中,因此所有线程随时都可以访问),它判断 single ==null 得到false,直接返回single对象。但是此时single对象还没初始化完成,因此很有可能就会发生bug。


  • 发表于 2020-02-13 14:46
  • 阅读 ( 673 )
  • 分类:Java开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1478 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章