Java高并发--volatile使用及实现原理

1概述

针对sychronized,我们知道它是一个重量级的锁,而我们现在要讨论的volatile就是一个轻量级的,不会引起线程切换。在我们讨论volatile的原理之前,我们先来看看Java多线程的一些概念。

2Java多线程的可见性、原子性、有序性

(1)可见性

可见性是指多个线程的情况下,一个线程改变了一个变量的值,而另外的线程能够马上看见这个值得改变。为什么能实现这种功能呢?因为在线程改变值得时候,并没有将这个值得改变先写入工作内存,而是直接写入了主内存,而其他线程读取值得时候也是直接从主内存里面读取,因此这个值能够实现可见性。那么在多线程的代码中如何来实现可见性呢?我们使用关键字volatile来实现可见性。同时还可以使用synchronized和final关键字来实现可见性。volatile关键字实现可见性,可以使变量的改变直接针对主内存,而不是工作内存。而synchronized关键字则是针对一个变量的使用 进行加锁,当变量使用结束,需要释放锁的时候,就将变量的改变刷新到主内存,以方便其他线程能够看见。

(2)原子性

所谓原子性就是一个操作不能够被再次拆分成多个操作,我们就说这个操作具有原子性。比如a = 1,这个操作不能够再次被拆分,我们就说这个操作是具有原子性。而针对a++,这个操作可以被拆分成a=a+1,这个操作就不具有原子性。如果一个操作不是原子性的操作,那么在多线程的时候,它就不是线程安全的。

非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。(由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。)

(3)有序性

有序性是指程序执行的顺序按照代码的先后顺序进行。针对程序的有序性,在线程内部观察线程,永远是有序的。而在其他线程观察本线程,会发现所有线程都是无序的。针对前半句话是指线程内表现为串行语句,而针对后半句话是指指令重排序。

那么在多线程要如何实现线程的有序性呢?可以直接加关键字volatile或者synchronized关键字来保证有序性。

下面我们简单介绍下指令重排序。

在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

  1. 编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

  2. 处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

指令重排序对单线程没有什么影响,它不会影响程序的运行结果,但是会影响多线程的正确性。既然指令重排序会影响到多线程执行的正确性,那么我们就需要禁止重排序。

那么JVM是如何禁止重排序的呢?这个问题稍后回答。

我们先看另一个原则happens-before。

happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推到出来,那么他们就不能保证有序性,可以随意进行重排序。

happen-before规则如下:

  • 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。

  • 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

  • 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

  • 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

  • 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

  • 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

我们着重看第三点volatile规则:对volatile变量的写操作 happen-before 后续的读操作。

为了实现volatile内存语义,JMM会重排序,其规则如下:

对happen-before原则有了稍微的了解,我们再来回答这个问题JVM是如何禁止重排序的?

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。

lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。下图是完成上述规则所需要的内存屏障:

3Volatile原理

下面我们首先来看一段代码

 

public class VolatileTest {
    public static boolean ready;
    public static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                System.out.println("run");
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    private static class WriterThread extends Thread {
        @Override
        public void run() {
            ready = true;
            number = 42;
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        new WriterThread().start();
    }
}

多次执行上面的代码我们就发现如下的执行结果:

run
run
run
run
run
42

这说明,写线程对共享数据的改变并没有立刻让读线程看见,这也就是并没有实现线程的可见性。要解决这个问题,我们只需要在我们的共享变量上添加volatile关键字即可。
接下来我们说说volatile的实现原理。使用volatile的时候,数据将会直接被写入主内存,而不会缓存在工作内存,因此针对volatile的关键字修饰的变量的操作,所有线程都是可以看见的,并且这个关键字强调了这个变量是共享的,因此编译器并不会对这个变量进行重排序,同时这个变量的使用不需要对关键字进行加锁,从而不会造成线程的堵塞。

 


 

上面这张图体现了点对非volatile变量操作的时候的内存过程,我们可以看出,如果两个线程位于不同的cpu的时候,他们就会将主内存的数据拷贝到各自的cpu缓存中,从而这很容易造成线程的不安全性。

总结下,我们使用volatile的好处就两点,使变量具有可见性,禁止指令重排序。

参考:程序员dd公纵号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值