轻量级锁和重量级锁
必备知识
了解轻量级锁,就必须先了解 CAS 指令
CAS 指令
概念
CAS (Compare-And-Swap
或者 Compare-And-Set
) 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题
其中大致包含三个主要操作
- 找到需要读取的内存指针(offset)
- 进行比较的期望值(expect)
- 需要写入的修改值(update)
其中第一个操作和第二个操作成功时才会执行执行第三个步骤,整个过程是一个无锁的过程,所以不排除有两个 CPU 同时执行了 CAS 操作同一个对象,就可能发生 CAS 带来的经典问题 ABA 问题,Java 并发包提供了 AtomicStampedReference
类,利用变量的版本号加 CAS 保证
在 Java 中,Unsafe 类是 CAS 面对 Java 程序员开放的功能,其底层调用的是各个系统的 cmpxchg
指令,
常见的工具类
java.util.concurrent.atomic
包下面的类,例如 AtomicBoolean
/ AtomicLong
缺点
- 在自旋失败的时候会一直占用 CPU 资源,如果经常长时间失败,会浪费很多资源
- CAS 操作的是一个共享变量,也就是说只能保证临界区一个变量的原子操作,不能保证整个临界区的原子操作
- ABA 问题
自旋锁
概念
通俗的说就是占用着 CPU 资源,不时地挂起线程,恢复线程
在竞争锁失败的时候不阻塞,而是自旋的同时去尝试竞争锁,直到达到一定的次数后,放弃自旋,阻塞
自旋锁是 JDK 1.4.2 中引入,需要使用 -XX:+UseSpinning
JVM 参数来开启,可以使用 -XX:PreBlockSpin=10
来指定自旋的此时,默认是 10 次,而在 JDK 1.6 及之后已经默认打开,并且自旋次数已经不能自己设定,而是由虚拟机对锁的监测来预测需要自旋的次数,通常自旋不会超过一个线程上下文切换的时间,不然就没有意义
优势
在锁竞争的场景下,如果同步代码块中业务处理时间很短,通过短暂的自旋就可以竞争到锁,那么使用自旋锁将减少由于线程切换导致的内核态与用户态直接切换带来的性能损耗
劣势
在这一系列操作中都需要占用 CPU 资源,如果在处理 CPU 密集型任务,就会得不偿失
单核处理器中,自旋的时候不是阻塞自己,拿到锁资源的线程永远无法执行
轻量级锁
发生场景
多个线程交替执行竞争锁资源的场景会升级为轻量级锁
原理
轻量级锁是利用 CAS 操作达到无锁情况下实现线程安全的目的
轻量级锁底层的操作
- 虚拟机栈中创建一个名为锁记录的空间(Lock Record),存储的是所对象当前的 Mark Word 的拷贝(Displaced Mark Word)
- 将对象头中的 Mark Word 复制到 Lock Record 中
- 拷贝成功后,利用 CAS 操作尝试将对象的 Mark Word 内容更新为 Lock Record 指针,并且将 Lock Record 中的 owner 指针指向 对象的 Mark Word
- 更新成功,那么线程就拥有了该对象的锁
- 更新失败,那么锁就要升级到重量级锁
存疑:在 CAS 尝试将对象的 Mark Word 内容更新为 Lock Record 指针操作中是否会存在自旋操作,个人猜测是有的
重量级锁
发生场景
多个线程同时竞争锁资源的场景会升级为重量级锁
原理
重量级锁是一个 OS 层面的锁,需要依赖 OS 的 Mutex Lock,Mark Word 记录的是 monitor 对象的的起始地址
具体实现可以参考文档