Java内存模型以及Volatile关键字深度剖析

Java内存模型

Java内存模型应该说成Java线程内存模型,同CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的屏蔽掉了底层计算机的区别。

如上图所示每一个线程对应一个工作内存,相当于cpu的高速缓存,从主内存中获取共享变量,并存贮一份共享变量副本,而每个线程的共享变量副本都是相对独立的,那么当我们写多线程程序的时候,当不了解Java线程内存模型的时候,并发一上来就会出现这种bug,我们来看下面这个程序。

    private static boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            System.out.println("======>wait data");
            while(!initFlag){
            }
            System.out.println("==============>success");
        }).start();

        Thread.sleep(2000);

        new Thread(()->{
            prepareData();
        }).start();
    }

    public static void prepareData(){
        System.out.println("==============>prepareData start");
        initFlag = true;
        System.out.println("==============>prepareData end");
    }

这个程序线程1是执行一个循环,当initFlag值不变的时候,会一直循环。线程2是执行修改这个initFlag这个值,那么理论上来说这个程序的数据结果应该是:

======>wait data
==============>prepareData start
==============>prepareData end
==============>success

实际上,并不是这样的,它会陷入一个死循环,success不会输出出来,按理说,线程一应该可以感受的到initFlag值的变化,实际上线程1一直读取的是,工作内存中的共享变量副本。

先了解一下JMM的原子操作:

read(读取):从主内存中读取数据

load(载入):将主内存中读取到的数据写入工作内存中

use(使用):从工作内存中读取数据来计算

assign(赋值):将计算好的值重新赋值到工作内存中

store(存储):将工作内存数据写入主内存中

write(写入):将store过去的变量赋值给主内存中的变量

lock(锁定):将主存变量加锁,标识为线程独占状态

unlock(解锁):将主存变量解锁,解锁后其他线程可以锁定该变量。

那么将上述程序结合JMM的原子操作分析如下图所示:

 

 

首先线程1先从主内存读取initFlag变量然后load到工作内存中后use--->!initFlag变量,此时工作内存中,线程2也从主内存中read-》initFlag变量然后load到工作内存中,use变量并assign到工作内存中,然后store将数据写入主内存中,然后再write到主内存中,此时线程1读取的还是工作内存中的值,导致值没有一致性,那么如何解决这个问题呢?

在initFlag变量加上volatile关键字:

private static volatile boolean initFlag = false;

众所周知volatile可以使变量缓存可见性,并且禁止指令重排,那么它底层是如何实现的呢?底层实现主要是通过汇编loca前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)

IA-32架构软件开发手册对lock指令解释:

1)、会将当前处理器缓存行的数据立即写回到系统内存

2)、这个写会内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

加了volatile关键字之后所对应JMM的原子操作操作如图所示:

比较所不同之处:在store到主内存的时候,因为MESI缓存一致性协议的存在,其他CPU里有一个总线嗅探机制,可以理解为监听,该内存地址的数据改变则置为无效,所以当再一次执行!initFlag指令时,会重新读取载入数据,则获取的时true,从而可以循环结束,输出success,而如果两个线程同时修改initFlag时,一个线程再修改时会有缓存行级锁,所以需要等待释放锁之后再进行后续操作,这样的话读取使保持了数据的一致性,又不会影响性能,比直接再主内存加总线锁要轻量级很多。

但是这样又会暴露其他问题,再看以下代码:

    private static volatile int num = 0;

    public static void increate(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[10];

        for(int i = 0 ; i<=threads.length;i++ ){
            threads[i] = new Thread(()->{
               for(int j = 0; j< 1000 ;j++){
                   increate();
               }
            });
            threads[i].start();
        }

        for(Thread t:threads){
            t.join();
        }

        System.out.println(num);
    }

上述程序理论上应该输出:10*1000=10000,实际上呢结果不一,可能是10000也是9888,这是为什么呢?按找上述描述加了volatile关键字有缓存行级锁,那么取值应该是对的,问题就出在这里,线程1在执行num++的赋值之后store的时候有一个过程,线程2在store之前也执行了++操作,然后再线程一store write之后,由于cpu嗅探机制所以线程二的数据失效重新获取,再操作一次++的时候就不是原子操作了,所以导致没有达到预期的值,如图所示:

那么可以再++方法上加锁保持操作的原子性,这里就不讨论加锁及其细节了。

并发编程三个概念:

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

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

3、有序性:即程序执行的顺序按照代码的先后顺序执行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值