ReentrantLock锁优化和synchronized锁膨胀的共同点

背景

concurrent包下的Lock和jdk原生的synchronized经常被拿来作比较,通常会被问到两者的区别与优劣,本文不会讨论锁具体实现细节(比如轻量级锁具体修改了哪个地方的第几个位),而是基于两者对锁的宏观优化原理讨论一下彼此的共同点

synchronized的锁膨胀过程

在jdkx(忘记是哪个版本)之前,synchronized 是直接调用系统函数来阻塞线程(如linux下的pthread_mutex_lock),效率较低。后面受到了Doug Lea刺激之后,才对其做了优化,即将上锁的过程分为偏向锁、轻量级锁、重量级锁:

  • 一般情况下,锁的初始状态是可偏向状态(即无锁并且支持偏向锁)。当有个线程A尝试获取锁时,会通过一次CAS操作将当前线程的标志赋值给锁对象,一旦获取锁成功,则由可偏向状态升级为偏向锁
  • 后续线程A不断申请这把锁,只需要判断线程A与锁的持有线程是否一致即可。也就是说偏向锁只需要一次CAS操作就能搞定。
  • 此时如果有另一个线程B尝试上锁,则需要通过CAS修改偏向锁标志位为无锁状态,然后修改锁标志位为轻量级锁,如果成功则升级为轻量级锁,轻量级锁在使用完毕后还必须通过CAS撤销锁的状态,恢复为无锁。即每次轻量级锁从上锁到用完锁,都必须经历两次CAS操作,理想情况下,多个线程交替执行,没有锁的竞争,则每次获取的锁都是轻量级锁。
  • 如果此时有多个线程在同时竞争锁,那么轻量级锁会被升级会重量级锁,系统函数将会被调用来阻塞线程。

因此,总的来说,偏向锁只需要一次CAS操作,轻量级锁每次上锁都需要两次CAS,重量级锁则每次都需要调用系统函数来阻塞线程。因此开销是逐渐变大的。

ReentrantLock 的上锁过程

ReentrantLock是在synchronized还没有优化之前开发出来的,巧妙的使用了AQS同步队列来作为锁的辅助工具,上锁过程如下:

  • 线程A期望获取锁,先判断锁的state是否为0,是则表示当前锁没有被占用,尝试用CAS将state设置为1,并将当前占用锁的线程改为线程A。后续线程A再次尝试加锁时,只需要判断占用锁的线程和线程A是否一致,是的话则直接将state加1即可。即如果只有一个线程获取锁,那么就只需要一次CAS(与偏向锁完全可以对上)
  • 第二个线程B获取锁,先判断锁的state是否为0,如果此时线程A已经用完了锁,则线程B用CAS将state设置为1。理想情况下AB线程交替获取锁,每次获取锁只需要用到一次CAS操作。(与轻量级锁一致)
  • 如果线程B获取锁的时候发现state不为0,则会通过CAS初始化等待队列,将当前线程包装成一个node写入队列尾部,但是并不会立即阻塞,而是会进入一个死循环,先判断一下当前线程节点的prev是否是head,是则表示他是下一个正在等待的人,立刻尝试获取一次锁,获取锁失败了,再根据waitStatus判断需不需要阻塞(这里一共有两次尝试机会,即调用两次CAS,具体请自己看代码,这里不细说),几次尝试都不成功,才会调用LockSupport.parkNanos阻塞当前线程(一旦阻塞,就表示升级成了重量级锁)。

总结两者的共同点

  • 从上述的两个上锁过程描述,发现两者的上锁过程基本能对应上,甚至可以说是一毛一样,只不过一个是在c++层实现,一个在java层面实现。原则上其实都是为了减少在没有锁竞争的情况下,获取锁的开销。考虑到时间先后顺序,基本可以确认jdk是在借(chao)鉴(xi)Doug Lea,并且为了不让人发现,还整了偏向锁、轻量级锁和重量级锁的概念。。。。
  • 而细节的处理上,reentrantLock给了竞争线程更多的机会,并没有立即去阻塞,策略更加乐观,比synchronized更适合竞争较少的场景(如果竞争激烈的话,多余的CAS尝试反而是一种浪费)。

你可能感兴趣的:(ReentrantLock锁优化和synchronized锁膨胀的共同点)