前言碎语
Synchronized 和 ReentrantLock 你们应该都不陌生了,做为java中最经常使用的本地锁,最第一版本中 ReentrantLock 的性能是远远强于 Synchronized 的,后续java在一次次的版本迭代中 对 Synchronized 进行了大量的优化,直到 jdk1.6 以后,两种锁的性能已经相差无几,甚至 Synchronized 的自动释放锁会更好用。java
在面试时被问到 Synchronized 和 ReentrantLock 的使用选择时,不少朋友都脱口而出的说用 Synchronized ,甚至在我面试的时候问面试者,也不多有人可以答出因此然来,moon 想说,这可不必定,只对标题感兴趣的同窗能够直接划到最后,我可不是标题党~面试
Synchronized使用
在 java 代码中 synchronized 的使用是很是简单的设计模式
- 1.直接贴在方法上
- 2.贴在代码块儿上
程序运行期间,Synchronized那一起代码发生么什么?
来看一张图api
在多线程运行过程当中,线程会去先抢对象的监视器,这个监视器是对象独有的,其实就至关于一把钥匙,抢到了,那你就得到了当前代码块儿的执行权。数据结构
其余没有抢到的线程会进入队列(SynchronizedQueue)当中等待,等待当前线程执行完后,释放锁.多线程
最后当前线程执行完毕后通知出队而后继续重复当前过程.并发
从 jvm 的角度来看 monitorenter 和 monitorexit 指令表明着代码的执行与结束。框架
SynchronizedQueue:
SynchronizedQueue 是一个比较特殊的队列,它没有存储功能,它的功能就是维护一组线程,其中每一个插入操做必须等待另外一个线程的移除操做,一样任何一个移除操做都等待另外一个线程的插入操做。所以此队列内部其 实没有任何一个元素,或者说容量是0,严格说并非一种容器。因为队列没有容量,所以不能调用 peek 操做,由于只有移除元素时才有元素。jvm
举个例子:
喝酒的时候,先把酒倒入酒盅,而后再倒入酒杯,这就是正常的队列。高并发
喝酒的时候,把酒直接倒入酒杯,这就是 SynchronizedQueue 。
这个例子应该很清晰易懂了,它的好处就是能够直接传递,省去了一个第三方传递的过程。
聊聊细节,锁升级的过程
在 jdk1.6 之前,Synchronized 是一个重量级锁,仍是先贴一张图
这就是为何说,Synchronized 是一个重量级锁的缘由,由于每一次锁的资源都是直接和 cpu 去申请的,而 cpu 的锁数量是固定的,当 cpu 锁资源使用完后还会进行锁等待,这是一个很是耗时的操做。
可是在jdk1.6,针对代码层面进行了大量的优化,也就是咱们常说的锁升级的过程。
这就是一个锁升级的过程,咱们简单的说说:
- 无锁:对象一开始就是无锁状态。
- 偏向锁:至关于给对象贴了一个标签(将本身的线程 id 存入对象头中),下次我再进来时,发现标签是个人,我就能够继续使用了。
- 自旋锁:想象一下有一个厕所,里面有一我的在,你很想上可是只有一个坑位,因此你只能徘徊等待,等那我的出来之后,你就可使用了。 这个自旋是使用 cas 来保证原子性的,关于 cas 我这里就再也不赘述了。
- 重量级锁:直接向 cpu 去申请申请锁,其余的线程都进入队列中等待。
锁升级是何时发生的?
- 偏向锁:一个线程获取锁时会由无锁升级为偏向锁
- 自旋锁:当产生线程竞争时由偏向锁升级为自旋锁,想象一下 while(true) ;
- 重量级锁:当线程竞争到达必定数量或超过必定时间时,晋升为重量级锁
锁的信息是记录在哪里的?
这张图是对象头中 markword 的数据结构,锁的信息就是在这里存放的,很清楚的代表了锁在升级的时候锁信息的变更,其实就是经过二进制的数值,来对对象进行一个标记,每一个数值表明一种状态。
既然synchronized有锁升级那么有锁降级吗?
这个问题和咱们的题目就有很大的关联了。
在 HotSpot 虚拟机中是有锁降级的,可是仅仅只发生在 STW 的时候,只有垃圾回收线程可以观测到它,也就是说,在咱们正常使用的过程当中是不会发生锁降级的,只有在 GC 的时候才会降级。
因此题目的答案,你懂了吗?哈哈,咱们接着往下走。
ReentrantLock的使用
ReentrantLock 的使用也是很是简单的,与 Synchronized 的不一样就是须要本身去手动释放锁,为了保证必定释放,因此一般都是和 try~finally 配合使用的。
ReentrantLock的原理
ReentrantLock 意为可重入锁,提及 ReentrantLock 就不得不说 AQS ,由于其底层就是使用 AQS 去实现的。
ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。
- 公平模式下等待线程入队列后会严格按照队列顺序去执行
- 非公平模式下等待线程入队列后有可能会出现插队状况
这就是ReentrantLock的结构图,咱们看这张图实际上是很简单的,由于主要的实现都交给AQS去作了,咱们下面着重聊一下AQS。
AQS
AQS(AbstractQueuedSynchronizer): AQS 能够理解为就是一个能够实现锁的框架。
简单的流程理解:
公平锁:
- 第一步:获取状态的 state 的值。
- 若是 state=0 即表明锁没有被其它线程占用,执行第二步。
- 若是 state!=0 则表明锁正在被其它线程占用,执行第三步。
- 第二步:判断队列中是否有线程在排队等待。
- 若是不存在则直接将锁的全部者设置成当前线程,且更新状态 state 。
- 若是存在就入队。
- 第三步:判断锁的全部者是否是当前线程。
- 若是是则更新状态 state 的值。
- 若是不是,线程进入队列排队等待。
非公平锁:
-
第一步:获取状态的 state 的值。
- 若是 state=0 即表明锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操做用 CAS 完成。
- 若是不为0或者设置失败,表明锁被占用进行下一步。
-
此时获取 state 的值,
- 若是是0,表明恰好线程释放了锁,此时将锁的持有者设为本身
- 若是不是0,则查看线程持有者是否是本身
- 若是是,则给state+1,获取锁
- 若是不是,则进入队列等待
读完以上的部分相信你对AQS已经有了一个比较清楚的概念了,因此咱们来聊聊小细节。
AQS使用state同步状态(0表明无锁,1表明有),并暴露出 getState 、 setState 以及 compareAndSet 操做来读取和更新这个状态,使得仅当同步状态拥有一个指望值的时候,才会被原子地设置成新值。
当有线程获取锁失败后,AQS是经过一个双向的同步队列来完成同步状态的管理,就被添加到队列末尾。
这是定义头尾节点的代码,咱们能够先使用 volatile 去修饰的,就是保证让其余线程可见,AQS 实际上就是修改头尾两个节点来完成入队和出队操做的。
AQS 在锁的获取时,并不必定只有一个线程才能持有这个锁,因此此时有了独占模式和共享模式的区别,咱们本篇文章中的 ReentrantLock 使用的就是独占模式,在多线程的状况下只会有一个线程获取锁。
独占模式的流程是比较简单的,就根据state是否为0来判断是否有线程已经得到了锁,没有就阻塞,有就继续执行后续代码逻辑。
共享模式的流程根据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是经过阻塞来提升性能的,在设计模式上就体现出了对多线程状况的支持。