杂谈

2018-12-20  本文已影响11人  简书徐小耳

锁 voliate,JVM主内存,JVM工作内存 线程 OS主内存 cpu cache(高速缓存) 处理器

os的内存是用于磁盘和cpu的交互(因为磁盘io的速度太慢,而cpu太快 所以增加了内存)
由于cpu的速度太快,内存也跟不上了 所以增加了了cpu cache(包含三层)

所以现在的整个过程是 处理器和高速缓存交互,不同的高速缓存通过一致性协议使得缓存一直
缓存一致性协议和主内存交互,主内存和磁盘之间 也包含pagecache

而JVM的内存模型屏蔽了这些硬件特性 让其内存模型 和上述的模型大致相似
即 每个线程都有自己的工作内存,具体的数据从主内存获取

对应的到实际的处理就是 线程是在处理器上面被执行,然后该处理器需要为该线程准备对应的数据和指令,如果之前的处理器的cpu cache 没有该线程需要的数据,则cpu cache 会重新从os 主内存获取数据
所以我们要避免经常的切换线程上下文,这会导致 处理器不断的准备新的上下文环境,同时可能导致cpu cache 失效 重新从主内存获取数据

下面讲到锁了 不管是synchronize 还是AQS的各类锁
我们一般是锁定A对象,然后在锁定期间更新一些对象B(当然也可以是A 我们为了举例子方便)的属性
java的语境定义
lock 是锁定主内存中的对象,标识为某个线程独享 会清空锁定对象在该线程中的工作副本,从新从主内存获取对象
unlock 则相反 同时unlock 需要把对锁定对象修改内容同步到主内存

那么 为什么 我们不加锁并发的对对象进行修改 会有问题
假如我们a,b线程同时获取到执行操作对象B 导致每个人有一个副本
当a先修改对象B 同步回主内存,那么线程b中的对象B副本依然有效,所以这个时候线程b对修改后的对象B的结果是不可见
从而导致错误

这个是时候volatile出现了
他有禁止指令重排序,和保证变量的可见性
禁止指令重排序:比如我们对象B是voliate 那么我们在线程a中先给B 修改 在使用 B 那么在这两行代码直接的所有其他代码 都不会被指令重排序

那么voliate是如何保证可见性,他只是在线程使用这个对象的时候重新从主内存获取最新副本
虽然保证可见性但是不能保证线程安全,假如不加锁,线程 b先使用B(此时从主内存获取最新对象),然后进行赋值操作,此时正好线程a对B进行了修改并同步到主内存
这就导致了线程a无法感悟到之后的对象B修改
voliate同时还保证了我们修改对象后的下一步就是讲修改值同步到主内存
synchronize 的可见性是通过unlock之前将修改的变量同步回到主内存之前
voliate的可见性是依赖每次使用变量都从主内存获取最新的值
final的可见性则是依赖于构造器一旦一旦被初始化完成,同时构造器没有把this引用传递出去,那么其他线程就能看到看该变量


内存屏障
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:

阻止屏障两侧的指令重排序;
强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效。
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

image.png image.png

happens-before (先于)

程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。(不解锁,无法加锁)
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。(对一个volatile变量的读,总是能看到【任意线程】对这个volatile变量最后的写入)
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。


而AQS的可见性则是根据voliate和happensbefore 原则
当一个线程获取锁对共享变量修改后 在释放锁的时候会写voliate 这时候在写voliate的state字段之前加了 屏障保证之前的普通写 可被其他变量正确获取到 而下一个线程获取锁之前要先读取voliate的state 这就导致其对上一个线程写入的共享变量可见

synchronized关键字的另一个作用就是保证了一个线程执行临界区中的代码时,所修改的变量值对于稍后执行该临界区的线程来说是可见的。这对于保证多线程代码的正确性来说非常重要。

前文我们讲解锁是如何保证可见性的时候提到了线程获得和释放锁时所分别执行的两个动作:刷新处理器缓存和冲刷处理器缓存。对于同一个锁所保护的共享数据而言,前一个动作保证了该锁的当前持有线程能够读取到前一个持有线程对这些数据所做的更新,后一个动作保证了该锁的持有线程对这些数据所做的更新对该锁的后续持有线程可见

java虚拟机会在 MonitorExit ( 释放锁 ) 对应的机器码指令之后插入一个存储屏障,这就保障了写线程在释放锁之前在临界区中对共享变量所做的更新对读线程的执行处理器来说是可同步的。相应地,Java 虚拟机会在 MonitorEnter ( 申请锁 ) 对应的机器码指令之后临界区开始之前的地方插入一个加载屏障,这使得读线程的执行处理器能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中

上一篇下一篇

猜你喜欢

热点阅读