Java多线程01 volatile

一, volatile

1. 内存不可见性

JMM(java内存模型) 是抽象的概念,描述多线程与内存间的通信,Java线程内存模型与CPU缓存模型类似,是标准化的,用于屏蔽底层的内存访问差异。

Java中,每个线程都有自己独立的工作内存,存储共享变量的副本,改变单个线程的变量副本不会影响到其他线程中的副本。

我们来看这样一份代码

public class voil {
    private static boolean flag=false;

    public static void refresh(){
        System.out.println("refreshing data...");
        flag=true;
        System.out.println("refreshion complete");
    }

    public static void loadData(){
        while(!flag);
        System.out.println("flag had been changed");
    }

    public static void main(String[]args){
        Thread th0 = new Thread(()->{loadData();},"ThreadA");
        Thread th1 = new Thread(()->{refresh();},"ThreadB");
        
        th0.start();
        try{
            Thread.sleep(300);
        }catch(InterruptedException e){
            e.printStackTrace();
        }

        th1.start();
    }
}

其执行结果如下

refreshing data...
refreshion complete
[]

在th1执行完毕后,th1只在其线程内部更改了flag的值,而并未更改主内存区的flag值,所以线程th0中继续死循环。

解决方法如下,下节我们将开始通过8大原子操作解释volatile。

    private static volatile boolean flag=false;

结果:

refreshing data...
refreshion complete
flag had been changed

2. 内存交互的8大原子操作

为了工作内存与主内存的交互,java内存模型定义了8类原子操作。

  1. lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  2. read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  3. load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  4. use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  5. assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  6. store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  7. write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
  8. unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

参考:

  1. JMM:内存模型以及8种原子操作

3. MESI缓存一致性协议

MESI协议的四种状态

  1. M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

  1. E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

  1. S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

  1. I: 无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。

状态跃迁图

在这里插入图片描述
在这里插入图片描述

第一节中volatile作用过程

线程0加载S类型的变量后,若要修改该变量,则线程0对工作内存中的变量加锁,同时向总线发送修改消息。同时线程1嗅探到该消息,并将该变量状态置为I(Invalid)。若线程1需要读该变量,则向总线发送消息,线程0读取到该消息,立刻将该值写回主内存并向线程1发送可读消息,并解锁工作内存变量。线程1读取主内存变量后,向线程0发送已读信号,线程0接收信号后将变量状态重新置为S(Share)。

volatile原理

JMM内存交互 volatile修饰的变量的read,load,use操作和assgin,store,write必须是连续的,即修改后必须立即同不回主存,使用时必须从主内存刷新,以此保证volatile变量的可见性。

底层实现 通过lock前缀指令,锁定 变量缓存行 区域并回写主存,又称为“缓存锁定”。

  • 缓存一致性协议会阻止两个以上的处理器同时修改主存的数据。
  • 单个处理器缓存回写到主存后会导致其他处理器的缓存无效。

参考:

  1. 【并发编程】MESI–CPU缓存一致性协议

4. 非原子性数据的缓存一致性

我们再来看这样一份代码,理论上,counter的输出值应该是1000*10=10^4

public class ex4 {
    private static volatile int counter=0;

    private static void compute(){
        counter++;
    } 

    public static void main(String[]args){
        for(int i=0;i<10;i++){
            String name = "Thread"+i;
            Thread t0 = new Thread(()->{
                for(int k=0;k<1000;k++)
                    compute();
            },name);
            t0.start();
        }

        try{
            Thread.sleep(3000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

可实际的输出情况为:

9990
PS C:\Users\w1738\Desktop\Classfied\JavaMultiThreadTest> 

原因

出现这种情况的根本原因是counter++操作本身不是原子的,它分为三步:将counter读入工作缓存;对其进行++;回写到主内存。这三步的JVM指令为:

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

注意最后一步是内存屏障(Memeory Barrier),该指令可以确保特定操作的执行顺序。对于volatile,Java内存模型将在写操作 插入写内存屏障,若是读操作,将在该操作 插入读内存屏障。这意味着voaltile可以保证一旦完成写入,任何访问该变量的线程将得到最新的值,而在写入前,保证所有该在之前的操作完成。

在线程1读入counter=0后被阻塞,随后线程2开始执行,并读入counter=0,自加,并更新主存中的counter=1。
线程1再次开始执行,此时因为之前还有自加和写入主存的操作没有完成,而内存屏障保证该操作必须在接下来优先执行。所以在线程1中自加counter=1,并写回主存。

总的来说

  1. volatile仅仅用来保证该变量对所有线程的可见性,不保证原子性。
  2. 不要将volatile用在GetAndOperate场合。

参考:

  1. 为什么volatile不能保证原子性?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值