并发和JVM

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

2020-12-11  本文已影响0人  Java柱柱

前言碎语

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

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

Synchronized使用

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

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

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

来看一张图

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

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

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

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

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

SynchronizedQueue:

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

举个例子:

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

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

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

聊聊细节,锁升级的过程

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ReentrantLock的使用

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

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

ReentrantLock的原理

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

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

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

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

AQS

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

简单的流程理解:

公平锁:

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

非公平锁:

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

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

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

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

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

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

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

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

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

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

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

ReentrantLock和Synchronized的区别

说说标题的答案

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

来源:https://www.tuicool.com/articles/A36jUbf

上一篇 下一篇

猜你喜欢

热点阅读