2.1 volatile的应用
volatile是轻量级的synchronized,在保证了共享变量的“可见性”
可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
如果volatile修饰符使用恰当,会比synchronized的使用和执行成本更低,因为不会引起线程上下文的切换和调度。
1. volatile的定义与实现原理
CPU术语定义
术语 | 英文单词 | 术语描述 |
---|---|---|
内存屏障 | memory barriers | 是一组处理器指令,用于实现对内存操作的顺序限制 |
缓冲行 | cache line | cpu高数缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行,现代CPU需要执行几百次CPU指令 |
原子操作 | atomic operations | 不可中断的一个或一系列操作 |
缓存行填充 | cache line fill | 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高数缓存行到适当的缓存(L1,L2,L3的或所有) |
缓存命中 | cache hit | 如果进行高数缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取 |
写命中 | write hit | 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中 |
写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
volatile的两条实现原则
1. LOCK前缀指令会引起处理器缓存回写到内存
2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效
2.2 synchronized的实现原理和应用
java中的每个对象都可以作为锁,具体表现为以下3种形式:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchonized括号里配置的对象
2.2.2 锁的升级与对比
锁有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
锁可以升级但不能降级。意味着偏向锁升级成轻量级锁后不能降级为偏向锁,**目的是为了提高获得锁和释放锁的效率
1.偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
**
1)偏向锁的撤销
使用了一种等到竞争出现才释放的机制
当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(这个时间点上没有正在执行的字节码)。
它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
2) 关闭偏向锁
在java6和java7中是默认启用的,但是在应用程序启动几秒后才激活,
可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。
如果你确定应用程序里所有的锁通常情况处于竞争状态,
可以通过JVM参数关闭偏向锁:-XX:UseBiasedLocking=false,那么程序默认会进入轻量级锁状态
2.轻量级锁
1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
2)轻量级锁解锁
解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
3.锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自选会消耗CPU | 追求响应时间 同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |
2.3 原子操作的实现原理
处理器如何实现原子操作
1) 使用总线锁保证原子性
所谓总线锁是使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
2) 使用缓存锁保证原子性
同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在LOCK操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。
3) 两种情况下处理器不会使用缓存锁定
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定
- 有些处理器不支持缓存锁定
JAVA如何实现原子操作
1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。
自旋的基本思路就是循环进行CAS操作直到成功为止
2)CAS实现原子操作的三大问题
1)ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成B,又变成A,那么使用CAS进行检查是会发现它的值没有发生变化,但实际上却变化了。
解决思路:使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加1,那么A-B-A就会变成1A-2b-3a
2)循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
pause指令有两个作用:
第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零
第二,它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提供CPU的执行效率。
3)只能保证一个共享变量的原子操作
对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。