Java线程内存模型

之前没怎么弄明白volatile关键字,只知道它能够保证线程间数据的可见性,保证对float、double类型数据读写的原子性。
在这之前要先搞懂Java的线程内存模型,中午的时候网上找了下相关的文章,看文字属实吃力,还是B站看视频容易理解一点。BiliBili传送门

接下来大概梳理一下。

主内存、高速缓存

主内存通常意义上就是普通的内存条的空间,从主内存中读取数据比从硬盘中读取数据快的多。但是随着CPU性能的提升,CPU和内存之间的数据传输能力也会到一个瓶颈,为了增加CPU的处理速度,目前CPU通常会有自带缓存(高速缓存),从缓存中读取数据比从主内存中更快。在任务管理器中可以看到CPU的高速缓存大小:

这里的L1、L2、L3就是一级二级三级缓存。
如果计算机是多核的,那就会有多个高速缓存,整体内存架构如下图。

JMM(Java Memory Model) Java内存模型

JMM和CPU缓存模型类似,在多线程编程中,多个CPU分别处理不同的线程时,会在各自的缓存中创建主内存中共享变量的副本,之后对共享变量的操作实质上是对缓存中的变量的操作。
我们将缓存称为工作内存,因此线程间的工作内存是独立的,那么如何保证多线程的可见性呢?在某线程完成对某变量的更新的时候,会将工作内存中的新值写入主内存,然后其他线程从主内存将新值再读入(细节下面再讲)。

一个小问题

看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
    new Thread(()->{
        System.out.println("start");
        while (!initFlag){

        }
        System.out.println("end...");
    }).start();

    Thread.sleep(2000);

    new Thread(()->{
        System.out.println("prepare");
        initFlag = true;
        System.out.println("prepare End,now flag is " + initFlag);
    }).start();


}

线程1循环监听initFlag,直到为true输出end,线程2修改initFlag为true,预期结果是依次输出

  • start
  • prepare
  • prepare End,now flag is true
  • end…

然而结果是:

线程1一直在等待,我试了再开线程3读取主内存中的flag输出,结果为true,说明线程2确实把initFlag修改了,并且写回到了主内存中,但是线程1中的工作内存中的initFlag副本还是false,所以线程1仍在监听。

如何解决呢?

给该共享变量加上volatile关键字,表示该变量不稳定,可能会被多个线程修改读取。
细节看Next Part????

JMM原子操作

内存模型中,一个线程将共享变量从主内存中读取到工作内存中,需要有以下几步(按时间顺序):

  1. read(读取):从主内存中读取数据。
  2. load(载入):将主内存中的数据读取到工作内存中。
  3. use(使用):CPU从工作内存中读取数据来计算。
  4. assign(赋值):将计算好的新数据放入工作内存。
  5. store(存储):将计算好的新数据写入主内存(这个时候主内存中会同时有该共享变量的旧值和新值)。
  6. write(写入):将上一步送过去的新值覆盖旧值
  7. lock(锁定):将主内存变量加锁,标识为线程独占状态。
  8. unlock(解锁):解锁。

上述例子中,程序的数据传输流程如下:

因此,在没有使用volatile关键字的时候,线程1没有感知到主内存中变量的变化,那么应该如何实现主内存中数据更新到线程的工作内存中呢,或者说如何感知变化呢?

总线加锁

这是一种比较暴力的方式,就是在一个线程读取主内存中的共享变量的时候,CPU会在总线对这个数据进行加锁,直到使用完数据之后释放锁,然后其他线程才能处理。
这种方式感觉的synchronized关键字差不多,无非就是这个是针对数据加锁而synchronized是针对对象加锁。将对这个共享变量的操作串行化。

MESI缓存一致性协议

当某线程更改了共享变量,CPU会立即将数据写回主内存,然后其他CPU通过总线嗅探机制可以感知到数据的变化,在监测到主内存中的共享变量更新后,会使自己线程内共享内存中的变量副本失效。当处理器发现缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
当然,本文假定是多CPU分别执行一个线程(并行),也存在单核多线程(并发)的情况。

volatile基于MESI实现,当一个变量在使用了volatile关键字声明后,对该变量的所有写操作(写到工作内存)转换成汇编指令后,会使用汇编lock指令,这个指令会使当前缓存中的数据立即写回到主内存,并使其他线程的工作内存中的变量副本无效。
并且,在写回主内存的时候(即从storewrite)会在之前之后加上加锁解锁步骤:

在这个过程中,其他线程不能从主内存读写这个数据。所以volatile是一种轻量级锁。

再看CAS

回忆下前几天的CAS(Compare And Swap),现在知道了volatile可以保证可见性和有序性,但是不能保证原子性,因为各个线程之间存在共享变量的副本,假如现在有两个副本要对一个共享整形变量进行自增操作,那么两个CPU各自自增之后,再写主内存。
但是假如线程1先写入了新值,那么如上面所说,线程2中的值会失效,那么线程2中的自增操作后的新值就丢失了,因此即使变量使用了volatile,我们也只能保证该共享变量在线程间的可见性,而并不能保证线程安全。

那么在使用CAS后,区 别就是,线程2在写入的时候,会比较之前拿到的旧值和目前主内存中的值是否一致,如果一致,那么表示该变量没有被其他线程写过,如果不一致,表示已经被其他线程修改过,此时重新取新值,执行一样的计算,然后再写入,再比较,直至成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值