PS:本文已收录到1.3 K+ Star 数的开源项目-《大厂面试指北》,如果想要了解更多,可以看一看,项目地址如下:

https://github.com/NotFound9/interviewGuide​github.com

摘要

synchronized锁的原理也是大厂面试中经常会涉及的问题,本文主要通过对以下问题进行分析讲解,来帮助大家理解synchronized锁的原理。

1.synchronized锁是什么?锁的对象是什么?

2.偏向锁,轻量级锁,重量级锁的执行流程是怎样的?

3.为什么说是轻量级,重量级锁是不公平的?

4.重量级锁为什么需要自旋操作?

5.什么时候会发生锁升级,锁降级?

6.偏向锁,轻量锁,重量锁的适用场景,优缺点是什么?

1.synchronized锁是什么?锁的对象是什么?

synchronized的英文意思就是同步的意思,就是可以让synchronized修饰的方法,代码块,每次只能有一个线程在执行,以此来实现数据的安全。

一般可以修饰同步代码块、实例方法、静态方法,加锁对象分别为同步代码块块括号内的对象、实例对象、类。

在实现原理上,

  • synchronized修饰同步代码块,javac在编译时,在synchronized同步块的进入的指令前和退出的指令后,会分别生成对应的monitorenter和monitorexit指令进行对应,代表尝试获取锁和释放锁。 (为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。)
  • synchronized修饰方法,javac为方法的flags属性添加了一个ACCSYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACCSYNCHRONIZED修饰,则会先尝试获得锁。
  1. public class SyncTest {
  2. private Object lockObject = new Object();
  3. public void syncBlock(){
  4. //修饰代码块,加锁对象为lockObject
  5. synchronized (lockObject){
  6. System.out.println("hello block");
  7. }
  8. }
  9. //修饰实例方法,加锁对象为当前的实例对象
  10. public synchronized void syncMethod(){
  11. System.out.println("hello method");
  12. }
  13. //修饰静态方法,加锁对象为当前的类
  14. public static synchronized void staticSyncMethod(){
  15. System.out.println("hello method");
  16. }
  17. }

2.偏向锁,轻量级锁,重量级锁的执行流程是怎样的?

在JVM中,一个Java对象其实由对象头+实例数据+对齐填充三部分组成,而对象头主要包含Mark Word+指向对象所属的类的指针组成(如果是数组对象,还会包含长度)。像下图一样:

Mark Word:存储对象自身的运行时数据,例如hashCode,GC分代年龄,锁状态标志,线程持有的锁等等。在32位系统占4字节,在64位系统中占8字节,所以它能存储的数据量是有限的,所以主要通过设立是否偏向锁的标志位锁标志位用于区分其他位数存储的数据是什么,具体请看下图:

锁信息都是存在锁对象的Mark Word中的,当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中 LockRecord的指针;当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

这是网上找到的一个流程图,可以先看流程图,结合着文字来了解执行流程

偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

简单的来说,就是主要锁处于偏向锁状态时,会在Mark Word中存当前持有偏向锁的线程ID,如果获取锁的线程ID与它一致就说明是同一个线程,可以直接执行,不用像轻量级锁那样执行CAS操作来加锁和解锁。

偏向锁的加锁过程:

场景一:当锁对象第一次被线程获得锁的时候

线程发现是匿名偏向状态(也就是锁对象的Mark Word没有存储线程ID),则会用CAS指令,将 mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

场景二:当获取偏向锁的线程再次进入同步块时

发现锁对象存储的线程ID就是当前线程的ID,会往当前线程的栈中添加一条 DisplacedMarkWord为空的 LockRecord中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下, synchronized关键字带来的性能开销基本可以忽略。

场景二:当没有获得锁的线程进入同步块时

当没有获得锁的线程进入同步块时,发现当前是偏向锁状态,并且存储的是其他线程ID(也就是其他线程正在持有偏向锁),则会进入到撤销偏向锁的逻辑里,一般来说,会在 safepoint中去查看偏向的线程是否还存活

  • 如果线程存活且还在同步块中执行, 则将锁升级为轻量级锁,原偏向的线程继续拥有锁,只不过持有的是轻量级锁,继续执行代码块,执行完之后按照轻量级锁的解锁方式进行解锁,而其他线程则进行自旋,尝试获得轻量级锁。
  • 如果偏向的线程已经不存活或者不在同步块中, 则将对象头的 mark word改为无锁状态(unlocked)

由此可见,偏向锁升级的时机为:当一个线程获得了偏向锁,在执行时,只要有另一个线程尝试获得偏向锁,并且当前持有偏向锁的线程还在同步块中执行,则该偏向锁就会升级成轻量级锁。

偏向锁的解锁过程

因此偏向锁的解锁很简单,其仅仅将线程的栈中的最近一条 lockrecordobj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改锁对象Mark Word中的thread id,简单的说就是锁对象处于偏向锁时,Mark Word中的thread id 可能是正在执行同步块的线程的id,也可能是上次执行完已经释放偏向锁的thread id,主要是为了上次持有偏向锁的这个线程在下次执行同步块时,判断Mark Word中的thread id相同就可以直接执行,而不用通过CAS操作去将自己的thread id设置到锁对象Mark Word中。 这是偏向锁执行的大概流程:

轻量级锁

重量级锁依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的,而在大部分时候可能并没有多线程竞争,只是这段时间是线程A执行同步块,另外一段时间是线程B来执行同步块,仅仅是多线程交替执行,并不是同时执行,也没有竞争,如果采用重量级锁效率比较低。以及在重量级锁中,没有获得锁的线程会阻塞,获得锁之后线程会被唤醒,阻塞和唤醒的操作是比较耗时间的,如果同步块的代码执行比较快,等待锁的线程可以进行先进行自旋操作(就是不释放CPU,执行一些空指令或者是几次for循环),等待获取锁,这样效率比较高。所以轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再升级为重量级锁。

轻量级锁的加锁过程

JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word里面。

然后线程尝试用CAS操作将锁的Mark Word替换为自己线程栈中拷贝的锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋:不断尝试去获取锁,一般用循环来实现。

自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。

JDK采用了适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

轻量级锁的释放流程

在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。 轻量级锁的加锁解锁流程图:

重量级锁

当多个线程同时请求某个重量级锁时,重量级锁会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列,我也不知道为什么网上的文章都叫它队列,其实这个队列是先进后出的,更像是栈,就是当Entry List为空时,Owner线程会直接从Contention List的队列尾部(后加入的线程中)取一个线程,让它成为OnDeck线程去竞争锁。(主要是刚来获取重量级锁的线程是回进行自旋操作来获取锁,获取不到才会进从Contention List,所以OnDeck线程主要与刚进来还在自旋,还没有进入到Contention List的线程竞争)

Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List,主要是为了减少对Contention List的并发访问,因为既会添加新线程到队尾,也会从队尾取线程。

Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set。

OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck。

Owner:获得锁的线程称为Owner 。 !Owner:释放锁的线程

重量级锁执行流程:

流程图如下:

步骤1是线程在进入Contention List时阻塞等待之前,程会先尝试自旋使用CAS操作获取锁,如果获取不到就进入Contention List队列的尾部。

步骤2是Owner线程在解锁时,如果Entry List为空,那么会先将Contention List中队列尾部的部分线程移动到Entry List

步骤3是Owner线程在解锁时,如果Entry List不为空,从Entry List中取一个线程,让它成为OnDeck线程,Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,JVM中这种选择行为称为 “竞争切换”。(主要是与还没有进入到Contention List,还在自旋获取重量级锁的线程竞争)

步骤4就是OnDeck线程获取到锁,成为Owner线程进行执行。

步骤5就是Owner线程调用锁对象的wait()方法进行等待,会移动到Wait Set中,并且会释放CPU资源,也同时释放锁,

步骤6.就是当其他线程调用锁对象的notify()方法,之前调用wait方法等待的这个线程才会从Wait Set移动到Entry List,等待获取锁。

3.为什么说是轻量级,重量级锁是不公平的?

偏向锁由于不涉及到多个线程竞争,所以谈不上公平不公平,轻量级锁获取锁的方式是多个线程进行自旋操作,然后使用用CAS操作将锁的Mark Word替换为指向自己线程栈中拷贝的锁记录的指针,所以谁能获得锁就看运气,不看先后顺序。重量级锁不公平主要在于刚进入到重量级的锁的线程不会直接进入Contention List队列,而是自旋去获取锁,所以后进来的线程也有一定的几率先获得到锁,所以是不公平的。

4.重量级锁为什么需要自旋操作?

因为那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthreadmutexlock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。如果同步块中代码比较少,执行比较快的话,后进来的线程先自旋获取锁,先执行,而不进入阻塞状态,减少额外的开销,可以提高系统吞吐量。

5.什么时候会发生锁升级,锁降级?

偏向锁升级为轻量级锁: 就是有不同的线程竞争锁时。具体来看就是当一个线程发现当前锁状态是偏向锁,然后锁对象存储的Thread id是其他线程的id,并且去Thread id对应的线程栈查询到的lock record的obj字段不为null(代表当前持有偏向锁的线程还在执行同步块)。那么该偏向锁就会升级成轻量级锁。

轻量级锁升级为重量级锁: 就是在轻量级锁中,没有获取到锁的线程进行自旋,自旋到一定次数还没有获取到锁就会进行锁升级,因为自旋也是占用CPU的,长时间自旋也是很耗性能的。 锁降级 因为如果没有多线程竞争,还是使用重量级锁会造成额外的开销,所以当JVM进入SafePoint安全点(可以简单的认为安全点就是所有用户线程都停止的,只有JVM垃圾回收线程可以执行)的时候,会检查是否有闲置的Monitor,然后试图进行降级。

6.偏向锁,轻量锁,重量锁的适用场景,优缺点是什么?

篇幅有限,下面是各种锁的优缺点,来自《并发编程的艺术》:

锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度非常快。重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。

参考链接:

https://github.com/farmerjohngit/myblog/issues/12 http://redspider.group:4000/article/02/9.html https://blog.csdn.net/bohu83/article/details/51141836 https://blog.csdn.net/Dev_Hugh/article/details/106577862

原创不易,最后自荐一下自己做的原创技术公众号:大厂面试

"大厂面试"这个公众号是一个帮助程序员进入BAT等互联网大厂的原创技术号。这里不仅有面试题分享、大厂内推,还有Java,Python,PHP,前端技术分享,涵盖多线程、JVM、Spring、MySQL、Redis、微服务等,及阿里,头条,百度,美团等大厂面试经验!

  • 【大厂面试01期】高并发场景下,如何保证缓存与数据库一致性?
  • 【大厂面试02期】Redis过期key是怎么样清理的?
  • 【大厂面试03期】MySQL是怎么解决幻读问题的?
  • 【大厂面试04期】讲讲一条MySQL更新语句是怎么执行的?
  • 【大厂面试05期】说一说你对MySQL中锁的理解?
  • 【大厂面试06期】谈一谈你对Redis持久化的理解?
  • 【大厂面试07期】说一说你对synchronized锁的理解?

另外我本人在Github做了一个开源学习指南的项目-《大厂面试指北》,目前在Github已经获得1.4 K+的Star了

https://github.com/NotFound9/interviewGuide​github.com

关注公众号还可以领取《大厂面试指北》PDF版

存储过程没有执行完后没有释放锁_【大厂面试07期】说一说你对synchronized锁的理解?...相关推荐

  1. 存储过程没有执行完后没有释放锁_面试必问---synchronized实现原理及锁升级过程你懂吗?...

    synchronized实现原理及锁升级过程 前言: synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差 ...

  2. notify()唤醒线程,不会立即释放锁对象,需要等到当前同步代码块都执行完后才能释放锁对象

    notify()唤醒线程,不会立即释放锁对象,需要等到当前同步代码块都执行完后才能释放锁对象 public class Test3 {public static void main(String[] ...

  3. 理解c函数执行完后,释放内存

    在C语言中,函数仅仅是一个小的功能块,函数执行完后(也就是函数功能完成),会根据调用约定来释放,该执行函数所占用的内存资源.这里的资源就是执行该函数所占用的栈内存.当函数执行完后仅仅就是释放该函数所用 ...

  4. 线程执行完之后会释放吗_java多线程并发:CAS+AQS+HashMap+volatile+ThreadLocal,乐分享...

    CyclicBarrier.CountDownLatch.Semaphore 的用法 CountDownLatch(线程计数器 ) CountDownLatch 类位于 java.util.concu ...

  5. vue在一个方法执行完后执行另一个方法

    vue在一个方法执行完后执行另一个方法 用Promise来实现. Promise是ES6的新特性,用于处理异步操作逻辑,用过给Promise添加then和catch函数,处理成功和失败的情况 ES7中 ...

  6. c#中等待某个线程执行完后再执行某个线程

    在方法的外部申请一个这样的变量 CountdownEvent latch = new CountdownEvent(3); 比如现在执行的是方法a public void a() { int si=0 ...

  7. java new一个线程执行完后会自动销毁吗_Java基础总结,超级全的面试题

    1. static关键字是什么意思?Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?是否可以在 static 环境中访问非static 变量? stat ...

  8. selenium 执行完后自动退出浏览器窗口问题解决

    开发工具,比如pycharm有自动内存回收机制,代码执行完会自动回收内存,右上角执行或ctrl+shift+f10执行都会. 解决方法: 右键后选择Run file in python console ...

  9. 怎么保证在多线程任务都执行完后再执行某一动作 CountDownLatch

    在开发过程当中,我们使用了多线程来异步执行多任务,但是我们想在这些任务执行完成后,才接着再执行的话,就需要使用到CountDownLatch这个类了. 首先我们需要知道被执行任务的个数,比如这里是有2 ...

最新文章

  1. 关于 OpenIdConnect 认证启用 HTTPS 回调 RedirectUri 不生效问题
  2. (DT系列五)Linux kernel 是怎么将 devicetree中的内容生成plateform_device【转】
  3. 图像处理中消除相机透镜畸变和视角变换
  4. 程序员之路──如何学习C语言并精通C语言
  5. 预告丨大型出海知识盛宴,邀您一起 enjoy !
  6. Silverlight Expression[转]
  7. java aqs详解_Java AQS底层原理解析
  8. 【渝粤题库】国家开放大学2021春1377理工英语3题目
  9. mock模拟接口测试 vue_vue+mock.js实现前后端分离
  10. python 安装容易吗,Python安装的步骤操作其实是件很容易的事
  11. Android 解决qq分享后返回程序出现的Bug
  12. android状态栏自定义,如何自定义Android状态栏颜色
  13. 手机 物理分辨率 逻辑分辨率
  14. 虚幻airsim1:下载安装
  15. css选择器有哪些?
  16. Computer Vision的尴尬---by林达华
  17. MTK6737平台匹配设备节点的方法
  18. 关闭ADOX.Catalog创建Access的链接,避免ldb锁定
  19. k8s健康检查(七)
  20. linux实训心得体会范文

热门文章

  1. 网络流 (网络流问题汇总)
  2. Docker折腾手记-安装
  3. 删除win10开始菜单中程序的目录
  4. linux中断申请之request_threaded_irq【转】
  5. JPA Annotation注解
  6. Exchange Server 2010核心服务器角色介绍
  7. Java EE企业系统性能问题的原因和解决建议
  8. 博客导读(09.3.21)
  9. UPS改造及终端流量监控系统
  10. 还亲力亲为的蜡笔小新