3-线程共享volatile

前言

volatile:常于多个线程读,一个线程写的情况,可保证线程可见性,但不保证原子性。和sync区别: sync即可保证原子性也可保证可见性
当变量设置成volatile后,如果volatile值修改,那么会立即把这个值回写到主存,其它线程在读这个值的时候会把工作内存的副本值 置为无效,从主内存上取值

一.内存模型的相关概念

cpu从内存读取数据的速度跟不上cpu执行的速度,所以cpu中有了高速缓存。当程序在运行时,会将运算需要的数据从主存复制一份到cpu的高速缓存,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,
当运算结束之后,再将高速缓存中的数据刷新到主存当中。在单线程中是没有问题,但到多线程环境下可能就会出现缓存不一致性的问题.
如何解决缓存不一致性问题,通常来说有以下2种解决方法:

1> 通过总线加#LOCK方式
早期cpu是通过这种方式实现的,但是这个做会导致效率低下.因为cpu和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。
2> 通过缓存一致性协议
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

二.并发编程中的三个概念

并发编程中通常会遇到3个问题:原子性问题,可见性问题,有序性问题

1.原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

3.有序性

即程序执行的顺序按照代码的先后顺序执行。
处理器为了提高程序运行效率,可能会对输入代码进行优化,会对指令重排序,它不会保证执行是按我们编写的顺序执行的,但会保证程序最终结果和代码执结果是一致的.
重排序不会影响单个线程内程序执行的结果,但多线程不一定不影响.比如我们常用的通过变量来控制上下文件加载完成的例子

 //线程1:
 context = loadContext();   //语句1
 inited = true;             //语句2
 //线程2:
 while(!inited ){
    sleep()
 }
 doSomethingwithconfig(context);

上面代码语句1和语句2没有依赖性,因些可能会被重排序,如果发生重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
所以将inited定义成volatile类型,会保证vlatile变量之前的语句不会重排序到在volatile语句行之后执行,而之后的语句不会被重排序到volatile之前执行.

三.java内存模型

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

i  = 10;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。
那么Java语言 本身对 原子性、可见性以及有序性是如何保证的

1.原子性

java中对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

  x = 10;         //语句1  原子的
  y = x;         //语句2   非原子的,包括2个操作:先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
  x++;           //语句3   非原子的
  x = x + 1;     //语句4   非原子的

注:只有简单的读取和赋值是原子操作,而且必须是将数字赋给变量是原子,变量赋值变量不是原子.

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

2.可见性

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

3.有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
1>通过volatile关键字来保证一定的“有序性”.
2>synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
3>java内存happen-before规则,无需任何同步器协助就已经存在,可以在编码中直接使用
程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。

3.1、happen-before原则

一段代码在单线程中执行的结果是有序的。注意是执行结果,因为jvm会对指令重排序,但并不影响执行结果,这个规则只在单线程中有效,多线程不一定.

管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。

无论是在多线程还是单线程下,一个锁处于被锁定状态,那必须先执行unlock操作,后面才能lock

volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定可见于读操作.

线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

A操作可见于B操作,B操作可见于C操作 ,那么A操作可见于C操作

happen-before主要是为了阐述多线程之间的可见性,如果一个操作的执行结果对另一个操作可见,那么这2个操作间必须存在happens-before关系.
happen-before是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,解决在并发环境下两操作之间是否可能存在冲突的所有问题。

四.深入剖析volatile关键字

1.volatile关键字的两层含义

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。

2.volatile不能保证对变量操作的原子性

3.volatile能保证有序性

在前面提到volatile关键字能禁止指令重排序
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

4.volatile的原理和实现机制

如何保证可见性和禁止指令重排序的?
观察加入volatile关键字时所生成的汇编代码发现,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能::
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主内存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

五.volatile使用场景

(1)对变量的写操作不依赖于当前值
(2)该变量没有包含在具有其他变量的不变式中

1.状态标记量

     volatile boolean flag = false;
     while(!flag){
         doSomething();
     }
     public void setFlag() {
         flag = true;
     }

2、double check(单例模式)

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

这里为什么要使用volatile修饰instance?主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情:
(1)给instance分配内存
(2)调用Singleton的构造函数来初始化成员变量
(3)将instance对象指向分配的内存空间(执行完这步instance就为非null了)。
但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用时就会报错

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值