1.依赖JVM
java代码编译为字节码,JVM执行字节码生成汇编指令,CPU执行汇编指令。
java并发机制依赖于JVM的实现和CPU指令。
2.Volatile实现原理
轻量级Sycronized,保证了共享变量的可见性
保证一个线程修改一个共享变量后,另一个线程总是能够读到这个修改后的变量值
2.1 相关CPU术语
- 内存屏障:一组处理器指令,用于限制对内存操作的顺序
- 缓冲行:cache line,缓存线,缓存中可以分配的最小存储单位
- 原子操作:不可中断的一个或一系列操作
- 缓冲行填充:处理器识别到从内存读取的操作数是可缓存的时,就读取整个缓存行填充到适当的缓存
- 缓存命中:缓存行填充的位置是下一次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存中
- 写命中:处理器将操作数回写时,会检查这个缓存的内存地址是否在缓存行中,存在则写回缓存而不是主存,称为命中
- 写缺失:一个有效缓存行被写入到不存在的内存区域
2.2 实现原理
处理器缓存(cpu cache line),系统内存,处理器操作的是工作内存的数据。
对声明volatile的变量进行写操作,JVM会向处理器发送一条lock前缀指令(lock addl),lock前缀指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使其他cpu中缓存了该地址内存的缓存行失效
缓存一致性协议:保证各个处理器的缓存是一致的
2.3 两条实现原则
缓存一致性机制:阻止同时修改两个以上处理器缓存的内存区域数据。
- LOCK指令前缀在执行期间会声言处理器的LOCK#信号,确保处理器能独占任何共享内存
- 有些处理器(Intel486等)是直接在总线上声言LOCK#信号的,锁总线,开销大——总线锁定
- 现代处理器,如果访问的内存区域的缓存在处理器内部,则会锁缓存行——缓存锁定
缓存失效:一个处理器缓存回写到系统内存会导致其他处理器缓存失效。
- MESI控制协议:维护内部缓存和其他处理器缓存的一致性
- Modified:修改
- Exclusive:独享
- Shared:共享
- Invalid:无效
- 许多处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器缓存的数据在总线上保持一致
- 嗅探一个处理器,其他处理器打算写内存地址,嗅探的处理器会使内存地址对应的缓存行失效,下一次访问相同地址(写的时候),会强制执行缓存行填充
2.4 Volatile使用优化
追加到64字节——LinkedTransferQueue,使用追加字节的方式来优化队列出队入队的性能
很多处理器缓存行为64字节宽,且不支持部分填充,将共享变量追加到64字节大小,可以避免队列头尾节点加载到同一缓存行中,使头尾节点不会互相锁定,可以单独进行操作。
两种情景下不该使用这种方式:
- 缓存行非64字节宽的处理器
- 共享变量不会被频繁地写的时候
这种追加方式在Java7中可能不生效,会淘汰和重新排列无用字段,需要使用其他追加字节的方式
3.Sycronized实现原理
Sycronized操作都很重量级,但是1.6版本对Sycronized进行了各种优化:
- 引入偏向锁和轻量级锁,减少获得锁和释放锁的性能消耗
- 锁的存储结构升级
三种形式的Sycronized锁:
- 普通同步方法,锁当前实例对象
- 静态同步方法,锁类的class对象
- 同步代码块,锁Sycronized()中配置的对象
3.1 实现原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块的同步
- 代码块同步是通过指令monitorenter和monitorexit实现的
- 方法同步则是另一种方式实现的,但可以使用这两个指令实现
monitorenter指令插入到同步代码块开始位置,monitorexit指令插入到结束位置;
JVM要保证以下两点:
- 每个monitorenter指令必须有对应的monitorexit指令对应
- 任何对象都有一个moniter与之关联。一个monitor被持有则会进入锁定状态。
执行到monitorenter指令会尝试获取对象所对应的monitor所有权
3.2 Java对象头
Synchronized的锁是存在Java对象头中的
如果对象是数组,用3个字来存储对象头,非数组对象用两个字存储对象头。
- 32位虚拟机:1字=4字节=32位(bit)
- 64位虚拟机:1字=8字节=64位(bit)
Java对象头长度:(2或3个字)
- Mark Word:标记字,Class Metadata Address:类元数据地址
- 数组会包含第三个字:Array length,非数组没有
- Array length数组长固定为32位,另外两个视JVM位数而定
- 锁数据存放在Mark Word中
32位JVM中Mark Word默认存储结构:(25412)
不同锁在32位JVM的Mark Word中的存放形式:
64位JVM中Mark Word默认存储结构:
3.3 锁升级优化(jdk1.6)
四种锁,级别从低到高:无锁,偏向锁,轻量级锁,重量级锁。
锁级别可以升但是不能降,级别越高越重。
偏向锁
CAS操作:Compare and Swap 比较并交换,常用来加锁和解锁。
锁对象的对象头Mark Word中有一位(bit)用来表示该对象锁是否是偏向锁(偏向锁标识)。
大多数情况下,锁不仅不存在竞争而且总是由同一线程多次获得,其中CAS的加解锁操作就花费了很大的代价。偏向锁可以通过相关数据结构减少CAS操作数量,提高应用性能。
偏向锁只有等竞争出现才释放锁。当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
偏向锁获取流程图:
偏向锁撤销流程图:
JVM相关参数:
-
关闭偏向锁启动延迟:
-XX:BiasedLockingStartupDelay=0
-
关闭偏向锁:
-XX:-UseBiasedLocking
轻量级锁(自旋锁)
存在线程竞争,但是竞争的线程不阻塞。升级为重量级锁后会阻塞,且不能降级。
-
轻量级锁加锁:(会将MarkWord复制一份再修改)
-
轻量级锁解锁:
不同锁的比较
-
偏向锁:适用于只有一个线程访问同步块的场景
加锁解锁消耗非常小,多个线程竞争会产生撤销偏向锁的额外开销
-
轻量级锁:追求响应时间,同步块执行速度非常快时使用
若同步块执行慢,自旋会一直消耗cpu
-
重量级锁:追求吞吐量,同步快执行时间长时使用
线程阻塞,响应时间慢
4.原子操作实现原理
原子操作,不可被分割的一个或一系列操作
4.1 相关术语
-
缓存行:缓存最小操作单位
-
CAS:比较并交换,需要输入两个数,一个旧值,一个新值,旧值没有发生变化才会进行交换成新值
-
cpu流水线:指令处理流水线,将指令分解为5-6步后由5-6个不同的功能单元分别执行
-
内存顺序冲突:多个cpu同时修改一个缓存行的不同部分引起其中一个cpu操作无效
出现内存顺序冲突必须清空流水线
4.2 实现原子操作
处理器会保证简单内存操作的原子性,总线锁和缓存锁保证复杂内存操作的原子性
总线锁定
处理器提供一个LOCK#信号,一个处理器在总线上输出此信号,其他处理器请求将被阻塞,该处理器独占共享内存。总线锁开销大,并且会导致其他处理器无法处理其他内存地址的数据。
缓存锁定
内存区域如果被缓存在缓存行中,且在锁操作期间被锁定,那么锁操作回写到内存时不会再总线上输出Lock#信号,而是修改内部的内存地址。其他处理器回写已被锁定的缓存行的数据时,会使缓存行失效。
两种情况下处理器不会使用缓存锁定
- 操作数不能被缓存,或者操作数跨多个缓存行,会使用总线锁定
- 有些处理器不支持缓存锁定,就算锁了缓存,也会调用总线锁
这两种情况可以通过Intel处理器提供的Lock操作前缀指令来解决,被这些指令操作的内存区域就会加锁:
- 位测试与修改:BTS,BTR,BTC
- 交换:XADD,CMPXCHG
- 操作数和逻辑指令:ADD,OR
4.3 Java实现原子操作
Java中可以通过锁和循环CAS操作实现原子操作
循环CAS操作实现
-
JVM中的CAS操作基于处理器的CMPXCHG指令实现
-
Java从1.5开始提供了一些原子类:AtomicBoolean,AtomicInteger等等,其中提供了CompareAndSet方法实现CAS操作
-
自旋CAS:循环CAS操作直到成功为止
循环CAS操作的三个问题
-
ABA问题:原值从A变B再变A会判定为没变化
AtomicStampedReference(jdk1.5)类的CompareAndSet方法可以解决这个问题
-
循环时间长开销大:CAS长时间不成功会浪费大量cpu资源
-
只能保证一个共享变量原子操作:多个共享变量可以使用锁,或者将多个共享变量合并为一个
jdk1.5提供了AtomicReference可以确保引用对象之间的原子性(可以将多个变量放入引用对象)
锁机制实现
JVM内部实现了多种锁:偏向锁,轻量级锁(自旋锁),互斥锁
除了偏向锁,其他锁都通过自旋CAS的方式来获取和释放锁