【杂记】有序性和可见性

1.CPU物理缓存结构

按照数据读取顺序和与CPU内核结合的紧密程度,CPU高速缓存有L1和L2高速缓存(即一级高速缓存和二级缓存高速),部分高端CPU还具有L3高速缓存(即三级高速缓存)。每一级高速缓存中所存储的数据都是下一级高速缓存的一部分,越靠近CPU的高速缓存读取越快,容量也越小。所以L1高速缓存容量很小,但存取速度最快,并且紧靠着使用它的CPU内核。L2容量大一些,存取速度也慢一些,并且仍然只能被一个单独的CPU核使用。L3在现代多核CPU中更普遍,容量更大、读取速度更慢些,能被同一个CPU芯片板上的所有CPU内核共享。最后,系统还拥有一块主存(即主内存),由系统中的所有CPU共享。拥有L3高速缓存的CPU,CPU存取数据的命中率可达95%,也就是说只有不到5%的数据需要从主存中去存取

2.CPU高速缓存进行数据读取的优势

(1)写缓冲区可以保证指令流水线持续运行,可以避免由于CPU停顿下来等待向内存写入数据而产生的延迟。
(2)通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。

3.并发的三大问题

(1).原子性问题,所谓的原子操作,就是"不可中断的一个或一系列操作",是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换。
(2).可见性问题,一个线程对共享变量的修改,另一个线程能够立刻可见,我们称改共享变量具备内存可见性,JMM的概念,将所有的变量都存在公共主存中,当线程使用变量时会把主存中的变量复制到自己的工作空间(或者叫做私有内存)中,线程对变量的读写操作,是自己工作内存中的变量副本,当两个线程同时操作一个共享变量事,就会出现可见性问题。使用java关键字volatile修饰共享变量,就可以解决其问题。为什么Java局部变量、方法参数不存在内存可见性问题?在Java中,所有的局部变量、方法定义参数都不会在线程之间共享,所以也就不会有内存可见性问题。所有的Object实例、Class实例和数组元素都存储在JVM堆内存中,堆内存在线程之间共享,所以存在可见性问题。
(3).有序性问题,所谓的程序的有序性,是指程序按照代码的先后顺序执行。如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。其中出现有序行问题的原因之一是:指令的重排序问题,一般来说,CPU为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序同代码中的先后顺序一致,但是它会保证程序最终的执行结果和代码顺序执行的结果是一致的。

4.如何解决内存可见性的问题

CPU提供了两种解决办法:
(1).总线锁,前端总线(也叫CPU总线)是所有CPU与芯片组连接的主干道,负责CPU与外界所有部件的通信,包括高速缓存、内存、北桥,其控制总线向各个部件发送控制信号,通过地址总线发送地址信号指定其要访问的部件,通过数据总线实现双向传输。在CPU内核1要执行i++操作的时候,将在总线上发出一个LOCK#信号锁住缓存(具体来说是变量所在的缓存行),这样其他CPU内核就不能操作缓存了,从而阻塞其他CPU内核,使CPU内核1可以独享此共享内存。总线锁的缺陷是:某一个CPU访问主存时,总线锁把CPU和主存的通信给锁住了,其他CPU不能操作其他主存地址的数据,使得效率低下,开销较大。总线锁的粒度太大了,最好的方法就是控制锁的保护粒度,只需要保证被多个CPU缓存的同一份数据一致即可。
(2).缓存锁,缓存一致性机制就是当某CPU对高速缓存中的数据进行操作之后,通知其他CPU放弃存储在它们内部的缓存数据,或者从主存中重新读取。

5.valatile作用

valatile关键字可以保证共享变量的主存可见性,也就是说将共享变量的改动值立即刷新回主存。在正常情况下,系统操作并不会校验共享变量的缓存一致性,只有当变量用的valatile关键字修饰了,改变量所得在缓存行才被要求进行缓存一致性的校验,不同线程对valatile变量的值具有内存可见性,即一个线程修改了某个volatile变量的值,该值对其他线程立即可见(使用volatile修饰的变量在变量值发生改变时,会立刻同步到主存,并使其他线程的变量副本失效。),并且禁止进行重排序(用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现,编译器级别是通过下面的规则实现的。)。volatile能保证数据的可见性,但volatile不能完全保证数据的原子
性,

6.java内存模型

JMM定义了一组规则和规范,改规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的,实际上JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值是在与解决可见性和有序性;另一个价值在与屏蔽各种硬件和操作系统的访问差异,保证java程序在各个平台下对内存的访问最终都是一致的。其中规定如下:
(1).所有的变量存储在主存中。
(2).每一个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
(3).不同线程之间无法访问直接访问彼此工作内存中的变量,要想访问只能通过主存来传递。

7.JMM的8中操作

Read(读取)—>Load(载入)—>Use(使用)—>Assign(赋值)—>Store(存储)—>Write(写入)—>Lock(锁定)—>Unlock(解锁)
必须满足的条件:
(1)不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。不允许read和load、store和write操作之一单独出现,意味着有read就有load,不能读取了变量值而不予加载到工作内存中;有store就有write,也不能存储了变量值而不写到主存中。
(2)不允许一个线程丢弃它最近的assign操作,也就是说当线程使用assign操作对私有内存的变量副本进行变更时,它必须使用write操作将其同步到主存中。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主存中。
(4)一个新的变量只能从主存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use和store操作之前,必须先执行assign和load操作。
(5)一个变量在同一个时刻只允许一个线程对其执行lock操作,但lock操作可以被同一个个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主存(执行store和write操作)。

8.JMM如何解决有序性问题

JMM提供了自己的内存屏障(读/写屏障)指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序(不是所有的编译器重排序都要禁止),并且定义了自己的规章:Happens-Before(先行发生规则),确保只要两个java语句之间存在Happens-Before关系,JMM尽量确保这两个java语句之间的内存可见性和指令有序性。

9.Happens-Before(先行发生规则)

(1)程序顺序执行规则(as-if-serial规则)在同一个线程中,有依赖关系的操作按照先后顺序,前一个操作必须先行发生于后一个操作(Happens-Before)。换句话说,单个线程中的代码顺序无论怎么重排序,对于结果来说是不变的。(一个线程内,按照代码顺序,书写在前面的操作先行发生(Happens-Before)于书写在后面的操作。)
(2)volatile变量规则对volatile(修饰的)变量的写操作必须先行发生于对volatile变量的读操作。(对一个volatile变量的写先行发生(Happens-Before)于任意后续对这个volatile变量的读。)
(3)传递性规则如果A操作先于B操作,而B操作又先行发生于C操作,那么A操作先行发生于C操作。(如果A操作先行发生于B操作,且B操作先行发生于C操作,那么A操作先行发生于C操作。)
(4)监视锁规则(Monitor Lock Rule)对一个监视锁的解锁操作先行发生于后续对这个监视锁的加锁操作。(对一个锁的unlock操作先行发生于后面对同一个锁的lock操作,即无论在单线程还是多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续执行lock操作。)
(5)start规则对线程的start操作先行于这个线程内部的其他任何操作。具体来说,如果线程A执行B.start()启动线程B,那么线程A的B.start()操作先行发生于线程B中的任意操作。(如果线程A执行ThreadB.start()操作启动线程B,那么线程A的ThreadB.start()操作先行发生于线程B中的任意操作。反过来说,如果主线程A启动子线程B后,线程B能看到线程A在启动操作前的任何操作。)
(6)join规则如果线程A执行了B.join()操作并成功返回,那么线程B中的任意操作先行发生于线程A所执行的ThreadB.join()操作。(如果线程A执行threadB.join()操作并成功返回,那么线程B中的任意操作先行发生于线程A的ThreadB.join()操作。join()规则和start()规则刚好相反,线程A等待子线程B完成后,当前线程B的赋值操作,线程A都能够看到。)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值