volatile 的应用
概述
volatile 是轻量级的 synchronized,如果使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程的上下文切换和调度问题。
volatile 的实现原理
我们知道,volatile 可以保证 “可见性”,那它是如何保证的呢?
对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在的缓存行数据会写到系统内存。但是,就算回写到内存,如果其他处理器缓存的值还是旧的,在执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改时,就会从系统内存从新读取这个数据。
简单可总结为以下两条:
- Lock 前缀的指令会引起处理器缓存回写到主内存
- 一个处理器的缓存回写到主内存会导致其他处理器的缓存失效
volatile 优化
JDK 7 的并发包中新增了一个队列集合类 LinkedTransferQueue,它在使用 volatile 变量时,用一种追加字节的方式来优化队列出队和入对的性能。
为什么追加字节能优化性能呢?
因为大多数处理器的高速缓存行是 64 字节,不支持部分填充缓存行,这意味,如果队列的头节点和尾节点都不足 64 字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点。而使用追加字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载在同一个缓存行,使得头节点、尾节点在修改时不会相互锁定。
那是不是在使用 volatile 变量的时候都应该追加到 64 字节呢?并不是,有以下两种情况不建议这种方式:
-
缓存行非 64 字节宽的处理器
-
共享变量不会被频繁的写
如果共享变量不被频繁写的话,锁的几率非常小,就没必要通过追加字节的方式来避免相互锁定。
synchronized 的实现原理与应用
Java 中的每一个对象都可以作为锁,具体表现为以下三种形式:
- 对于普通方法:锁是当前实例对象
- 对于静态方法:锁是当前类的 Class 对象
- 对于代码块:锁是代码块中配置的对象
当一个对象试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面存储什么信息呢?
Java 对象头
synchronized 用的锁是存在 Java 对象头里的。
内容 | 说明 |
---|---|
Mark Word | 存储对象的 hashCode、分代年龄和锁标记位 |
Class Metadata Address | 存储对象数据类型的指针 |
Array Length | 数组的长度(如果当前对象时数组) |
在运行期间,Mark Word 里存储的数据会随着锁标记位的变化而变化。
锁的升级与对比
Java SE 1.6 为了减少获取锁和释放锁带来的性能消耗,引入了 “ 偏向锁 ” 和 “ 轻量级锁 ” ,在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种策略目的是为了提高获取锁和释放锁的效率。
-
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈桢中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单的测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示当前线程已经获得了锁,如果测试失败就需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1( 表示当前是偏向锁 ),如果没有设置,则使用 CAS 竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
-
轻量级锁
线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获取锁,如果失败,表示其他线程竞争锁,当前线程遍尝试使用自旋来获取锁。
轻量级解锁时,会使用原子的 CAS 操作将 Mard Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。
锁的优缺点对比:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗 CPU | 追求响应时间,同步块执行速度非常快的场景 |
重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长的场景 |
原子操作的实现原理
处理器如何实现原子操作
- 通过总线锁
- 通过缓存锁定
Java 如何实现原子操作
- 锁
- 循环 CAS
网友评论