学习java多线程2——volatile关键字作用及其原理

并发编程中的三个概念

1、原子性

和数据库事务的原子性概念是一样的,一组操作要么全部成功要么全部失败,不可分割。

    x = 10;         //语句1

    y = x;         //语句2

    x++;           //语句3

    x = x + 1;     //语句4

    其实只有语句1是原子性操作,其他三个语句都不是原子性操作。语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存。

    Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2、可见性

这里是指线程共享变量的可见性,在这之前我们先来了解下线程内存工作模型。

学习java多线程2——volatile关键字作用及其原理_第1张图片

 共享内存:也就是我们的RAM内存,可以被多个线程共享

工作内存:也可以称为高速缓存Cache,每个线程对应

在线程对某一数据的操作过程中,是先将数据从共享内存中取出到线程自己的工作内存,然后对数据加以操作,最后将操作后的值更新到共享内存当中。

可见性导致的安全性问题,类似于数据库事务中的丢失更新。假设现在有A、B两个线程,要对int x =0进行加一操作,单个线程操作可以分为三步:

读取x的值到工作内存

对值进行+1

将x的新值更新到共享内存

假设现在A线程现在完成了第二步,这个时候A的工作线程中x=1,但是没有完成第三步,这个时候B线程读取了x的值0,并且进行了操作。当A和B线程执行完后,x的值为1,这就是不安全的。

对此,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3、有序性

在开发者看来,程序总是按照我们编写的“顺序”来执行。但是JVM会在不影响执行结果的前提下,对一些没有数据依赖的语句进行重排列,从而获得更高的执行效率。这种情况我们称为指令重排序

例如:int a=1;   int b=2;

这两条语句的执行前后顺序对执行结果不会有任何的影响。大多数情况下都是如此,但是在某些情况下,会出现一点小问题。

//线程1:

    inited = false;

    context = loadContext();   //语句1

    inited = true;             //语句2

    //线程2:

    while(!inited ){

     sleep()

    }

    doSomethingwithconfig(context);

由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

    从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

 

在Java里面,可以通过volatile关键字来保证一定的“有序性”。

    Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

    下面就来具体介绍下happens-before原则(先行发生原则):

  •     程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  •     锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作

  •     volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  •     传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  •     线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  •     线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  •     线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  •     对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

下面简单说一下我对先行发生原则的理解:

对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

volatile关键字的两层语义

    一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    第一层:

    1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:此处的修改是指将线程工作内存中的变量修改并写入主内存,如果没有写入主内存,对于其它线程仍是不可见,这也是不保证原子性的关键)

    2. 禁止进行指令重排序。

       

    第二层:

    1. 会强制将修改的值立即写入主存;

    2. 会使共享变量的缓存无效,只能去主存中读取数据

volatile的原理和实现机制

    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

    2)它会强制将对缓存的修改操作立即写入主存;

    3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile的作用:

    1、对于共享变量,每个线程对其的缓存失效

    2、能够保证每个线程对变量做到可见性,一个线程修改,其他线程都能看见(强制将修改值压入内存)

    3、禁止指令重排,但无法做到原子性,还要依赖其他锁机制

你可能感兴趣的:(java虚拟机)