动态高并发时为什么推荐ReentrantLock而不是Synchronized?

前言碎语

    SynchronizedReentrantLock 大家应该都不陌生了,作为java中最常用的本地锁,最初版本中 ReentrantLock 的性能是远远强于 Synchronized 的,后续java在一次次的版本迭代中
对 Synchronized 进行了大量的优化,直到 jdk1.6 之后,两种锁的性能已经相差无几,甚至 Synchronized 的自动释放锁会更好用。

   在面试时被问到 Synchronized 和 ReentrantLock 的使用选择时,很多朋友都脱口而出的说用 Synchronized ,甚至在我面试的时候问面试者,也很少有人能够答出所以然来,moon 想说,这可不一定,只对标题感兴趣的同学可以直接划到最后,我可不是标题党~

Synchronized使用

   在 java 代码中 synchronized 的使用是非常简单的

  • 1.直接贴在方法上
  • 2.贴在代码块儿上


    image.png

程序运行期间,Synchronized那一块儿代码发生么什么?

   来看一张图

image.png

   在多线程运行过程中,线程会去先抢对象的监视器,这个监视器是对象独有的,其实就相当于一把钥匙,抢到了,那你就获得了当前代码块儿的执行权。

   其他没有抢到的线程会进入队列(SynchronizedQueue)当中等待,等待当前线程执行完后,释放锁.

   最后当前线程执行完毕后通知出队然后继续重复当前过程.

   从 jvm 的角度来看 monitorenter 和 monitorexit 指令代表着代码的执行与结束

SynchronizedQueue:

   SynchronizedQueue 是一个比较特殊的队列,它没有存储功能,它的功能就是维护一组线程,其中每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其 实没有任何一个元素,或者说容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用 peek 操作,因为只有移除元素时才有元素。

举个例子:

   喝酒的时候,先把酒倒入酒盅,然后再倒入酒杯,这就是正常的队列

   喝酒的时候,把酒直接倒入酒杯,这就是 SynchronizedQueue

   这个例子应该很清晰易懂了,它的好处就是可以直接传递,省去了一个第三方传递的过程。

聊聊细节,锁升级的过程

   在 jdk1.6 以前,Synchronized 是一个重量级锁,还是先贴一张图

image.png

   这就是为什么说,Synchronized 是一个重量级锁的原因,因为每一次锁的资源都是直接和 cpu 去申请的,而 cpu 的锁数量是固定的,当 cpu 锁资源使用完后还会进行锁等待,这是一个非常耗时的操作。

   但是在jdk1.6,针对代码层面进行了大量的优化,也就是我们常说的锁升级的过程

image.png

这就是一个锁升级的过程,我们简单的说说:

  • 无锁:对象一开始就是无锁状态。
  • 偏向锁:相当于给对象贴了一个标签(将自己的线程 id 存入对象头中),下次我再进来时,发现标签是我的,我就可以继续使用了。
  • 自旋锁:想象一下有一个厕所,里面有一个人在,你很想上但是只有一个坑位,所以你只能徘徊等待,等那个人出来以后,你就可以使用了
    这个自旋是使用 cas 来保证原子性的,关于 cas 我这里就不再赘述了。
  • 重量级锁:直接向 cpu 去申请申请锁,其他的线程都进入队列中等待。

锁升级是什么时候发生的?

  • 偏向锁:一个线程获取锁时会由无锁升级为偏向锁
  • 自旋锁:当产生线程竞争时由偏向锁升级为自旋锁,想象一下 while(true) ;
  • 重量级锁:当线程竞争到达一定数量或超过一定时间时,晋升为重量级锁

锁的信息是记录在哪里的?

image.png

   这张图是对象头中 markword 的数据结构,锁的信息就是在这里存放的,很清楚的表明了锁在升级的时候锁信息的变动,其实就是通过二进制的数值,来对对象进行一个标记,每个数值代表一种状态

既然synchronized有锁升级那么有锁降级吗?

   这个问题和我们的题目就有很大的关联了。

   在 HotSpot 虚拟机中是有锁降级的,但是仅仅只发生在 STW 的时候,只有垃圾回收线程能够观测到它,也就是说,在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。

   所以题目的答案,你懂了吗?哈哈,我们接着往下走。

ReentrantLock的使用

image.png

   ReentrantLock 的使用也是非常简单的,与 Synchronized 的不同就是需要自己去手动释放锁,为了保证一定释放,所以通常都是和 try~finally 配合使用的。

ReentrantLock的原理

   ReentrantLock 意为可重入锁,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是使用 AQS 去实现的。

   ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。

  • 公平模式下等待线程入队列后会严格按照队列顺序去执行
  • 非公平模式下等待线程入队列后有可能会出现插队情况
image.png

   这就是ReentrantLock的结构图,我们看这张图其实是很简单的,因为主要的实现都交给AQS去做了,我们下面着重聊一下AQS。

AQS

   AQS(AbstractQueuedSynchronizer): AQS 可以理解为就是一个可以实现锁的框架

   简单的流程理解

   公平锁:

image.png
  • 第一步:获取状态的 state 的值。
    • 如果 state=0 即代表锁没有被其它线程占用,执行第二步。
    • 如果 state!=0 则代表锁正在被其它线程占用,执行第三步。
  • 第二步:判断队列中是否有线程在排队等待。
    • 如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。
    • 如果存在就入队。
  • 第三步:判断锁的所有者是不是当前线程。
    • 如果是则更新状态 state 的值。
    • 如果不是,线程进入队列排队等待。

   非公平锁:

截屏2020-12-11 下午1.52.43.png
  • 第一步:获取状态的 state 的值。

    • 如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。
    • 如果不为0或者设置失败,代表锁被占用进行下一步。
  • 此时获取 state 的值,

    • 如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己
    • 如果不是0,则查看线程持有者是不是自己
      • 如果是,则给state+1,获取锁
      • 如果不是,则进入队列等待

   读完以上的部分相信你对AQS已经有了一个比较清楚的概念了,所以我们来聊聊小细节。

   AQS使用state同步状态(0代表无锁,1代表有),并暴露出 getState 、 setState 以及 compareAndSet 操作来读取和更新这个状态,使得仅当同步状态拥有一个期望值的时候,才会被原子地设置成新值。

   当有线程获取锁失败后,AQS是通过一个双向的同步队列来完成同步状态的管理,就被添加到队列末尾。

image.png

   这是定义头尾节点的代码,我们可以先使用 volatile 去修饰的,就是保证让其他线程可见,AQS 实际上就是修改头尾两个节点来完成入队和出队操作的。

   AQS 在锁的获取时,并不一定只有一个线程才能持有这个锁,所以此时有了独占模式和共享模式的区别,我们本篇文章中的 ReentrantLock 使用的就是独占模式,在多线程的情况下只会有一个线程获取锁。

image.png

   独占模式的流程是比较简单的,就根据state是否为0来判断是否有线程已经获得了锁,没有就阻塞,有就继续执行后续代码逻辑。

image.png

   共享模式的流程根据state是否大于0来判断是否有线程已经获得了锁,如果不大于0,就阻塞,如果大于0,通过CAS的原子操作来自减state的值,然后继续执行后续代码逻辑。

ReentrantLock和Synchronized的区别

  • 其实ReentrantLock和Synchronized最核心的区别就在于Synchronized适合于并发竞争低的情况,因为Synchronized的锁升级如果最终升级为重量级锁在使用的过程中是没有办法消除的,意味着每次都要和cpu去请求锁资源,而ReentrantLock主要是提供了阻塞的能力,通过在高并发下线程的挂起,来减少竞争,提高并发能力,所以我们文章标题的答案,也就显而易见了。

  • synchronized是一个关键字,是由jvm层面去实现的,而ReentrantLock是由java api去实现的。

  • synchronized是隐式锁,可以自动释放锁,ReentrantLock是显式锁,需要手动释放锁

  • ReentrantLock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。

  • ReentrantLock可以获取锁状态,而synchronized不能。

说说标题的答案

   其实题目的答案就在上一栏目的第一条,也是核心的区别,synchronized升级为重量级锁后无法在正常情况下完成降级,而ReentrantLock是通过阻塞来提高性能的,在设计模式上就体现出了对多线程情况的支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容