Java
代
码
在
编译
后会
变
成
Java
字
节码
,字
节码
被
类
加
载
器加
载
到
JVM
里,
JVM
执
行字
节
码
,最
终
需要
转
化
为汇编
指令在
CPU
上
执
行,
Java
中所使用的并
发
机制依
赖
于
JVM
的
实现
和
CPU
的指令。
volatile是轻量级的 synchronized
它在多
处
理器开
发
中保
证
了共享
变
量的
“
可
见
性
”
。可
见
性的意思是当一个
线
程
修改一个共享
变
量
时
,另外一个
线
程能
读
到
这
个修改的
值
。如果
volatile
变
量修
饰
符使用恰当
的
话
,它比
synchronized
的使用和
执
行成本更低,因
为
它不会引起线程上下文的切换和调
度。
汇编
指令来
查
看
对
volatile
进行写操作:0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
通
过查
IA-32
架构软
件开
发
者手册可知,
Lock
前
缀
的指令在多核
处
理器下会引
发
了两件事情
。
1)将当前处理器缓存行的数据写回到系统内存。
Lock
前
缀
指令
导
致在
执
行指令期
间
,声言处
理器的
LOCK#
信号。
----Intel486
和Pentium处
理器,在
锁
操作
时
,
总
是在
总线
上声言
LOCK#
信号。 锁总线开销比较大
----P6和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则
不会声言LOCK#信号。相反,它会锁定这块内存区
域的
缓
存并回写到内存,并使用
缓
存一致性机制来确保修改的原子性,此操作被称
为
“
缓存
锁定”
,
缓存一致性机制会阻止同时修改由两个以上处理器缓
存的内存区域数据。
(老一代处理器锁总线,新一代处理器通过锁定内存地址,并使用缓存一致性保证修改的原子性)
!锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
嗅探一个
处
理器来检测
其他
处
理器打算写内存地址,而
这
个地址当前
处
于共享状
态
,那么正在嗅探的
处
理
器将使它的
缓
存行无效,在下次
访问
相同内存地址
时
,
强
制
执
行
缓
存行填充。
(多核处理器中,一个处理器要改内存中的值,若“嗅探”到其他处理器打算(注意这个打算,是将要修改但还没改)写内存,则使他的缓存行无效,下次用的时候再强制填充缓存行 )
volatile 在jdk1.7的优化
著名的
Java
并
发编
程大
师
Doug lea
在
JDK 7
的并
发
包里新增一个
队
列集合
类
LinkedTransferQueue,它在使用
volatile
变
量
时
,用一种追加字
节
的方式来
优
化
队
列出
队
和入
队
的性能。
???追加字节是什么操作🤔
这
种方式看起来很神奇,但如果深入理解
处
理器架构就能理解其中的奥秘。让
我
们
先来看看
LinkedTransferQueue
这
个
类
,它使用一个内部
类类
型来定
义队
列的 头节点(
head
)和尾
节
点(
tail
),而
这
个内部
类
PaddedAtomicReference
相
对
于父
类
AtomicReference
只做了一件事情,就是将共享
变
量追加到
64
字
节
。
我
们
可以来
计
算下,一个
对 象的引用占4
个字
节
,它追加了
15
个
变
量(共占
60
个字
节
),再加上父
类
的
value
变
量,一共
64
个 字节
。
为
什么追加
64
字
节
能
够
提高并
发编
程的效率呢
?因
为对
于英特
尔
酷睿
i7
、酷睿、
Atom
和
NetBurst
,以及
Core Solo
和
Pentium M
处
理器的
L1
、
L2
或
L3
缓
存的高速
缓
存行是
64
个字
节宽
,不
支持部分填充
缓
存行,
这
意味着,如果
队
列的
头节
点和尾
节
点都不足
64
字
节
的
话
,
处
理器会将
它
们
都
读
到同一个高速
缓
存行中,在多
处
理器下每个
处
理器都会
缓
存同
样
的
头
、尾
节
点,当一
个
处
理器
试图
修改
头节
点
时
,会将整个
缓
存行
锁
定,那么在
缓
存一致性机制的作用下,会
导
致
其他
处
理器不能
访问
自己高速
缓
存中的尾
节
点,而
队
列的入
队
和出
队
操作
则
需要不停修改
头
节
点和尾
节
点,所以在多
处
理器的情况下将会
严
重影响到
队
列的入
队
和出
队
效率。
Doug lea
使
用追加到
64
字
节
的方式来填
满
高速
缓
冲区的
缓
存行,避免
头节
点和尾
节
点加
载
到同一个
缓
存
行,使
头
、尾
节
点在修改
时
不会互相
锁
定。
简单来说,cpu 三级缓存L1 L2 L3 缓存行 宽度为64字节,若队列的字节不足,也会在锁定整个缓存行。如果填满64个字节,这样队列的头部和尾部不会分到同一缓存行,这样在一个cpu修改头部时,其他cpu中的缓存行只锁定头部所在缓存行,尾部操作不受影响。(虽然还是不能彻底理解,但是大概意思懂了 大师的操作果然高)
但也不是所有使用volatile的情况都会追加字节
1.缓存行字节宽不是64位的处理器 eg.奔腾 P6系列
2. 共享变量不会被频繁地写。因为使用追加字节的方式需要处理器读取更多的字节到高速 缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。 不过这种追加字节的方式在Java 7下可能不生效,因为Java 7变得更加智慧,它会淘汰或重新排列无用字段,需要使用其他追加字节的方式。