Java多线程编程-(3)-从一个错误的双重校验锁代码谈一下volatile关键字

在Java多线程编程-(2)中提及到了一段使用Synchronized关键字实现的单利模式–双重校验锁,代码如下:

慧眼的小伙伴,已经发现了其中的问题,并给了及时的回复:

这也是我今天准备和大家一起学习的内容。上述的代码是错误的写法,之所以是错误的,这是因为:指令重排优化,可能会导致初始化单利对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。

例如:线程A在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有完成初始化操作。线程B来调用newInstance()方法,得到的就是为初始化完全的单例对象,这就会导致系统出现异常行为。

为了解决上述的问题,可以使用<span style="font-size: 14px">volatile</span>关键字进行修饰instance字段。volatile关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证instance字段被初始化时,单例对象已经被完全初始化。

最终代码如下:

那么问题来了,为什么volatile关键字可以实现禁止指令的重排序优化以及什么是指令重排序优化哪?

在Java内存模型中我们都是围绕着原子性、有序性和可见性进行讨论的。为了确保线程间的原子性、有序性和可见性,Java中使用了一些特殊的关键字申明或者是特殊的操作来告诉虚拟机,在这个地方,要注意一下,不能随意变动优化目标指令。关键字volatile就是其中之一。

指令重排序

是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度(比如:将多条指定并行执行或者是调整指令的执行顺序)。编译器、处理器也遵循这样一个目标。注意是单线程。可显而知,多线程的情况下指令重排序就会给程序员带来问题。

最重要的一个问题就是程序执行的顺序可能会被调整,另一个问题是对修改的属性无法及时的通知其他线程,已达到所有线程操作该属性的可见性。

根据编译器的优化规则,如果不使用volatile关键字对变量进行修饰的,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的想爱你城中,看到变量修改顺序都会是反的。一旦使用volatile关键字进行修饰的话,虚拟机就会特别小心的处理这种情况。

因此,如何正确的使用双重校验锁,以及为什么使用关键字volatile这里我们应该很清楚了。

上述也提到了volatile关键字的另一个作用就是:变量在多个线程之间可见。

volatile可见性

首先我们先看一下段代码:

执行结果:

可以看出 在单线程的情况下,程序会一直执行下去,即一直执行while循环,导致程序不能正常执行下边的代码。解决的方法可以使用多线程。多线程示例代码如下:

执行结果如下:

可以看出使用多线程的技术实现,但是有一个问题就是在一些平台上执行的时候会出现死锁的情况,解决的方法就是使用volatile关键字。即变量用volatile关键字修饰。

volatile关键字的作用就是强制从公共堆栈中取得变量的值,而不是线程私有的数据栈中取得变量的值。

volatile与synchronized的区别

1、关键字volatile是线程同步的轻量级实现,性能比synchronized要好,并且volatile只能修于变量,而synchronized可以修饰方法,代码块等。

2、多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。

3、volatile可以保证数据的可见性,但不可以保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。

4、volatile解决 的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。

28