一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
Java并发机制的底层实现原理
volatile
定义
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
即:若一个字段被声明成volatile,把呢Java线程内存模型确保所有线程看到这个变量的值是一致的。
为了提高处理速度,处理器不直接和内存进行通信,而是将系统内存的数据读取到内部缓存(L1/L2或其他)后再进行操作,但操作完不知何时会写回内存。
如果对声明了volatile的共享变量进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令。Lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回操作会是得在其他CPU里缓存了该内存地址的数据无效;
这也就是volatile的两条实现原则:
- Lock前缀指令会引起处理器缓存回写到内存;
- 一个处理器的缓存回写到内存,会导致其他处理器的缓存无效;
优化
优化方式
JDK7里面新增了LinkedTransferQueue通过内部类PaddedAtomicReference定义了头节点(head)和尾节点(tail),该内部类相较于父类AtomicReference只做了一件事情:将共享变量追加到64字节。
这样做的目的是避免头节点和尾节点被同时读到一个高速缓存行中,使得头、尾节点在修改的时候不会互相锁定,否则的话,当一个处理器修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其它处理器不能访问自己高速缓存中的尾节点,所以在多处理器的情况下会严重影响到队列的入队和出队效率。
volatile变量都应追加到64字节?
不必,以下两种方式除外
- 缓存行非64字节宽的处理器
- 共享变量不会被频繁地写
synchronized的实现原理和应用
sychronized实现同步的基础:Java中的每一个对象都可以作为锁,表现为
- 对于普通同步方法,锁是当前实例对象;
- 对于静态同步方法,锁是当前类的Class对象;
- 对于同步代码块,锁是Synchronized括号里配置的对象;
Java对象头
锁的升级与对比
JDK6中,锁一共有四种状态,由低到高依次为:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
锁可以升级,但是不能降级
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比,仅存在纳秒级的差距 | 如果线程之间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于自由一个线程访问同步块的场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间;同步块执行速度非常快 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量;同步块执行时间较长 |
原子操作的实现原理
使用总线锁保证原子性
总线锁
使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
使用缓存锁保证原子性
缓存锁定
是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言Lock#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。
以下两种情况除外
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器回调用总线锁定;
- 处理器不支持缓存锁定;
Java如何实现原子操作
通过锁和循环CAS的方式实现原子操作
自旋CAS实现的基本思路:循环进行CAS操作,一直到成功为止。
三个问题
-
ABA问题
解决方法:Java5开始,
AtomicStampedReference来解决,该类的compareAndSet会依次检查当前应用是否等于预期应用、当前标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值 -
循环时间开销大
-
只能保证一个共享变量的原子操作
解决方法:Java5开始,提供了
AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里面来进行CAS操作
一些CPU术语
| 术语 | 英文 | 描述 |
|---|---|---|
| 内存屏障 | memory barriers | 一组处理器命令,用于实现对内存操作的顺序限制 |
| 缓冲行 | cache line | CPU高速缓存中可以分配的最小存储单位。处理器天蝎缓存行时会加载整个缓存行,现代CPU需要执行几百次CPU命令。 |
| 原子操作 | atomic operations | 不可中断的一个或者一系列操作 |
| 缓冲行填充 | cache line fill | 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存(L1\L2\L3的或者所有) |
| 缓存命中 | cache hit | 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器会从缓存中读取操作数,而不是从内存中读取 |
| 写命中 | write hit | 当处理器将操作数写回到一个内存缓存的区域时,它首先回检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称作写命中 |
| 写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
| 比较并交换 | Compare And Swap | CAS操作涉及以下操作(内存中的原数据V,旧的预期值A,需要修改的新值B):比较A与V是否相等(比较);如果相等,即没有发生变化,将B写入V(交换),如果不相等,即发生了变化,则不交换;返回操作是否成功 |
| CPU流水线 | CPU pipeline | CPU流水线的工作方式是将一条指令分成几步分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度 |
| 内存顺序冲突 | Memory order violation | 一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现内存顺序冲突时,CPU必须清空流水线 |
Java内存模型
基础
顺序一致性
同步原语
sychronized
volatile
final
设计
Java并发编程基础
待补充
Java中的锁
待补充
Java并发容器和框架
待补充
Java中的13个原子操作类
待补充
Java中的并发工具集
待补充
Java中的线程池
待补充
Executor框架
待补充
Java并发编程实践
待补充