栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程安全分析 volatile,JMM内存模型 重排序

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程安全分析 volatile,JMM内存模型 重排序

初步认识 Volatile 缓存一致性协议

在了解volatile关键字之前,必须要了解一个东西,叫做缓存一致性协议。
线程是CPU的最小调度单位(废话)。但是很多操做设计内存读取。高速缓存应运而生。将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。这个操作很好的提升了性能,但是又引发了一个新的问题,一个很严重的问题。

有A B 两个线程。
内存中Int a = 10;
A线程 a = 20;
然后B线程读取a ;
B读取的还是10

为什么???
因为在多 CPU 环境下,每个线程可能会运行在不同的 CPU 内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题。
怎么办
为了解决缓存不一致的问题,在 CPU 层面做了很多事情,主要提供了两种解决办法

  1. 总线锁
  2. 缓存锁
总线锁和缓存锁

总线锁,顾名思义, 直接锁住总线,A现在改数据,直接锁住总线,此时B是没有办法去读的,必须等到A一顿操作完成,B才去读,这样安全了吧。那必须付出代价,大大降低了性能。利比如:A上厕所,直接把整个厕所门锁住了。其他人怎么上。 一样的, 其他线程没办法去操作或者读取其他内存地址的数据,开销特别大。那怎么办?只能控制锁的粒度了。A上厕所把自己的坑位上锁就行,其他人依然可以上别的坑位。

所以引入了缓存锁
它核心机制是基于缓存一致性协议来实现的。
为了达到数据访问的一致,需要各个处理器在访问缓存时遵守规则,不能不讲武德。在读写时根据协议来操作,常见的协议有MSI,MESI,MOSI 等。最常见的就是 MESI 协议。

MESI 协议

MESI 表示缓存行的四种状态,分别是

  1. M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
  2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU 缓存中,并且没有被修改
  3. S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致
  4. I(Invalid) 表示缓存已经失效

读
针对读而言,其实很简单,只要缓存处于失效状态,直接从主内存中去读就好了。

写
针对写也很简单,你不能直接写S状态的缓存,如果你要写S状态的,至少要把其他缓存设为无效,不然你一写,人家读的还是原来的,就坏了事了。如果是缓存已经是失效状态,也不能再写了,都失效了,还写个毛啊。

可见性问题

有人可能会说:这挺好的,不是解决了问题吗?为什么还要volatile关键字,这不是多此一举?

其实, 如果多个 cpu 同时缓存了相同的共享数据时,可能存在可见性问题。为什么A线程改了自己的本地缓存,这个时候,我A是要做很多操作的,例如:告诉其他线程,这个缓存无效了,其他缓存还要给我个回信,说:已阅。那我A是不是这段时间就啥也不干等着其他缓存给我回信呢?其实理论上来说是这样的,但是这样把A阻塞,岂不是浪费资源,我A还有很多其他的事情要干,不能就等你回复,万一,等个一万年还没等到呢?
所以,为了避免阻塞带来的资源浪费。在 cpu 中引入了 Store Bufferes。
A 只需要在写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 Invalidate 消息,然后继续去处理其他指令。
当B收到消息之后,把这个缓存写入了自己的 store bufferes 。
看着像没什么毛病。

但是,我A提交之后,什么时候回写内存是个问题,而B也有自己缓存,如果拿到值了。B也不会去内存读。

所以我们希望有个hero出现,能为我们做这么一件事。确定把A改的东西写到了内存中,然后B才能读,换言之,B读的时候,A已经将缓存写入了主内存,完成了缓存回写,其他线程闻到味道了,都来更新自己失效的缓存

重排序问题

我们有两个数据 a , b 这两个东西没有任何依赖关系,

int a = 3;
int b = 5;

现在,A线程把A改成了8,B改成了10,我Main过来可能只会读到一个数据的改变,甚至一个都读不到,也可能都读到。
为什么

一个都读不到

因为没有hero的出现。我MAIN线程中也有缓存,我直接读我自己的,我不知道缓存已经失效了。因为A还没有把自己的缓存写入内存,内存没动,我怎么知道A小子偷偷的改了.

读到一个b的改变

因为a.b两个数据没有任何依赖关系,所以我编译器很任性,我认为看A不顺眼,把B排到A的前面去了,B改变了。然后也更到了内存,其他线程闻着味,把自己的缓存同步了,A还没变,没来得及变,Main就来了,读了一把,所有只读到一个的改变了。

我们此时希望一个hero出现,同时帮我们做一件事情。在读之后,后面我读的一定是最新的数据;声明写屏障后,写屏障之前的代码都会同步到主存当中,保证之后的线程读到的都是最新的。

内存屏障

这个时候,硬件方面给我们提供了一个内存屏障的东西,这玩意分为读屏障,写屏障,全屏障。
插入读屏障之后,我A在读取之后读的一定是最新的。
声明写屏障后,写屏障之前的代码都会同步到主存当中,保证之后的线程读到的都是最新的。
如果有一个数 num = 0,在主内存中是0,num = 2的时候如果没有写屏障,那么可能只是写到缓存中,并没有回写到内存。后续有人来读,读到的是脏数据。
而且屏障可以保证代码有序性,读之后的不可能排到读之前,写之前的不会排到写之后

指令交错问题

如果A线程的读,跑到了B线程的写前面,那也就无能为力了。
指令排序不能跨越线程,只能给自己的线程里重排序。

JMM内存模型

JMM 全称是 Java Memory Model. 什么是 JMM 呢?导致可见性问题的根本原因是缓存以及重排序。而 JMM 实际上就是提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。

JMM 抽象模型分为主内存、工作内存;主内存是所有线程共享的,一般是实例对象、静态字段、数组对象等存储在堆内存中的变量。工作内存是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。

JMM 是如何解决可见性有序性问题的

简单来说,JMM 提供了一些禁用缓存以及进制重排序的方法,来解决可见性和有序性问题。这些方法大家都很熟悉:volatile、synchronized、final;

HappenBefore

JMM 中有哪些方法建立 happen-before 规则

  1. 一个线程中的每个操作,happens-before 于该线程中的任意后续操作; 可以简单认为是 as-if-serial。单个线程中的代码顺序不管怎么变,对于结果来说是不变的.
  2. volatile 变量规则,对于 volatile 修饰的变量的写的操作,一定 happen-before 后续对于 volatile 变量的读操作。
  3. 传递性规则,如果 1 happens-before 2; 3happens-before 4;
  4. start 规则,如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()操作 happens-before 线程 B 中的任意操作
  5. join 规则,如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程A 从 ThreadB.join()操作成功返回。
  6. 监视器锁的规则,对一个锁的解锁,happens-before 于随后对这个锁的加锁
结束语

volatile关键字已经不需要再啰嗦了,说了这么多。
volatile关键字做的事就是发一个#lock前缀的指令,作用是这样的:

1.LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

2.LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。

几道常见题

volatile 能使得一个非原子操作变成原子操作吗?为什么?

valatile不具备原子性,例如线程A将0改为1之后,此时内存线程A还没有将这个值写入主内存中,此时线程B缓存失效,去主内存读取,还是0。对于复合操作,volatile变量无法保障其原子性,如果要保证复合操作的原子性,就需要使用锁。并且,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用。

如果对一个数组修饰volatile,是否能够保证数组元素的修改对其他线程的可见?为什么?

不能,因为你volatile只能保证引用地址的可见性,而不能保证里面元素的可见性。

synchronized 和 volatile 的区别是什么?

(1) volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。

(2) volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。

(3) volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

JMM内存模型用来解决什么问题?

JMM主要解决的问题: 解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题

数据同步八大原子操作

(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态

(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量

(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作

(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

再见再见再见再见再见。。。。。。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/959025.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号