Synchronized详解-》 无锁、偏向锁、轻量级锁、重量级锁
问: 什么是偏向锁,偏向锁加锁流程是什么??
答: makeword : 64位二进制位
偏向锁:64位二进制位中:其中 1、 表示偏向线程id的bit位都是0 & 2、锁状态位: 表示偏向锁状态
这个时候线程直接在锁对象markword的高位内存储当前线程的内存地址
步骤:
1 还要向当前的线程栈添加一条锁记录,让锁记录中的锁标识指向当前锁对象
2 通过CAS设置锁对象的markword,存储当前的线程地址 ,,这一步必须使用CAS,因为可能会有多个线程抢占锁
问: 偏向锁没法做到线程间的互斥,为什么还要这把锁呢???
答: 其实咱们写代码的时候,很多情况下觉得某块逻辑存在并发安全问题,为了程序的健壮性会使用synchronized来确保这块逻辑,在并发环境下它不出现问题,但是 实际情况下往往不是这样的,绝大部分情况下使用Synchronized包裹的代码块,它实际运行环境根本就没有多线程场景,可能自始至终就只有一个线程运行这个代码块。这种情况特别特别多,
如果没有偏向锁的话,直接使用轻量级锁会有一定的性能损耗,
=>因为轻量级锁加锁或者解锁 它都需要一次CAS操作 偏向锁解锁时不需要修改对象头的markword ,就少一次CAS操作..
问: 某个线程获取到锁对象的偏向锁之后,执行完同步代码块后,会释放这个偏向锁吗?
答: 是这样的,退出同步代码块后,对应的指令是monitorexit,然后
jvm在处理monitorexit这个指令的时候,第一步当前线程栈内与 这个锁对象相关的锁记录全部拿到,并释放掉最后一条锁记录。
通过检查lock对象的markword ,, 如果当前锁对象是偏向锁状态就啥也不做, 也就是说,就算线程退出同步代码块,该锁对象仍然保留了偏向当前线程的偏向锁, .
问: 那这样作有什么好处呢?
偏向锁退出时,锁仍然保留偏向状态好处,下次这个线程再去获取这边锁时,成本会更低,仅仅只需要对比一下当前这把锁是否偏向自己,就完事了.如果偏向自己直接执行同步代码块,这个加锁的过程中不涉及到一条cas指令.
问:使用偏向锁是否就一定会提升性能呢?
答: 这个还真不一定,如果实际运行情况就像砸门刚才聊的那样,锁对象自始至终都只会被一个线程获取-释放,获取-释放,这个条件下,偏向锁还是非常有用途的 ,但是条件骚味苛刻一丁点,, 偏向锁就会从性能提升效能手变成累赘 ..
问:怎么说呀?这块?
答:比如说,某个锁对象,因为之前被线程A获取过,它的锁状态是偏向线程A ,那假设线程B 也要获取这个锁对象, 因为锁对象并不偏向线程B嘛 ,所以线程B会触发偏向锁到轻量级锁升级的逻辑,这个逻辑很检查到很多状态来保证锁定正确性,这块就比较麻烦,,,所以说..如果没有偏向锁的存在的话,这个升级过程是可以避免掉的..
面试官: en 这点确实是这样的, 偏向锁升级的流程,砸门骚等再聊吧. 咱们先聊一下轻量级锁吧??
问: 轻量级锁,可能很多同学不知道什么是轻量级锁.. 如果咱们直接聊偏向锁升级到轻量级锁的这个流程,估计大家都不太好接受
骚等再聊偏向锁升级的过程..
问: 什么是轻量级锁??
答: 轻量级锁仍然解决不了线程竞争问题,它并不能提供线程之间的互斥性。
实际应用中,使用synchronized包裹的代码,实际运行情况下出现并发的概率并不高,绝大部分情况下是线程A运行完 同步代码块 线程B 再运行同步代码 ,这些交替运行这块同步代码 实际情况下 ,不存在线程并行
在这种情况下,如果没有轻量级锁 ,带来的后果就是直接 提前创建了重量级锁 对象 monitor
创建重量级锁是特别耗费资源、性能的 ,创建重量级锁应该等到实在没有办法,确实有线程并行这种情况下才去创建。。
咱们的轻量级锁介于偏向锁和重量级锁之间,用于的场景就是我刚刚说的 “多个线程交替执行同步代码块”的逻辑
真实运行环境下不存在线程并行使用的 引入轻量级锁,可以避免过早的创建重量级锁,而创建重量级锁很浪费资源
确实是这样的,绝大部分情况下,同步代码块内的逻辑是不存在并发执行的运行时环境的,其实更多地情况还是有多个线程它交替着,
去走这个同步代码块!这之间她们没有出现并行,是这样的哈. 如果这种情况下,确实不应该把重量级锁搞出来。
问:轻量级锁与偏向锁最大的区别是什么呢
答: 偏向锁的假定条件是这个锁只有一个指定的线程去获取,轻量级锁的假定条件是多个线程交替去获取这把锁。。
偏向锁针对的是没有多线程竞争的情况,也就是说只有一个线程访问临界区。它是通过 CAS 操作将加锁线程的线程 ID 设置到锁对象的 mark work 中,如果操作成功则表示加锁成功,否则表示存在多个线程竞争,需要升级锁。
轻量级锁针对的是多个线程交替进入同步块的情况。它是通过 CAS 操作将指向加锁线程栈的指针放置在锁对象的 mark word 中,表示已加锁,解锁时再将 mark word 替换回去。
加锁和解锁过程中都有可能触发锁膨胀(升级)。
问: 轻量级锁加锁时的锁标志位是在怎么设置的?
通过将lock record的地址赋值给对象锁的markword 设置的.
轻量级锁的锁标志位为 00
,转换为数字后就是 4 的倍数,比如 0100
表示 4, 1000
表示 8
如果 LockRecord 的地址是均为 4 的倍数,那么设置完 LockRecord 地址后,mark word 的末两位值就是 00
了,就是说一个操作完成了两件事。
第一件事情: 将对象锁的markword替换成指向Lock Record的指针
第二件事情: 因为Lock Record的地址 均为4的倍数 ,即 能表示轻量级锁的锁标志位 为00 ,表面该对象锁位轻量级锁
如下是 LockRecord 的定义,它内部有一个 _displaced_header
用于在加锁时存储锁对象的 mark word (为什么要这么做,是因为当对象锁的对象头要释放轻量级锁时,需要把markword替换回去,所以需要备份对象头无锁状态下的mark word到lock record中)
问:轻量级锁加锁的逻辑
答:无锁-》轻量级锁的过程
加锁的字节码是monitorenter指令,jvm执行这个指令之前,
它第一件事还是向当前线程栈中插入一条锁记录,锁记录内的锁引用字段 (owner)保存锁对象地址
第二件事:是让当前锁对象生成一条无锁状态的markword值 ,该markword 学名叫做 displacedMarkword
并且让这个生成的无锁状态的displacedMarked值保存到当前这条锁记录的displaced hdr字段内
第三件事:
CAS 将对象头中的MarkWord 替换为指向锁记录的指针,如果CAS替换成功表示 当前线程已获得该锁 ,
如果当前对象的markword就是无锁状态,那么这一步肯定就会修改成功
修改成功之后,当前锁对象就从无锁状态转变为轻量级锁状态了。。。并且从当前锁的markword可以看出持锁线程就是当前线程。。。
问: 聊一聊锁重入的问题
那如果当前锁状态是轻量级锁状态,并且从所得markword可以看出持锁线程就是当前线程的话
那当前线程再次使用synchronized去锁定这把锁,从jvm角度去看,都做了什么事情呢?
嗯 这块你,
首先,jvm解析器收到的字节码指令,仍然是monitorentor指令,在解析执行这个指令之前呢,
还是会向当前线程栈内插入一条锁记录,并且锁记录内的锁引用字段仍然指向这个锁对象。
第二件事: 还是会让锁对象生成一条无锁状态的markword值叫做displacedMarfword.
并且让线程栈内的锁记录保存这个displacedMarkword值到所记录的displaced字段内
第三件事: 还是使用CAS的方式去设置当前锁的markword值 因为当前锁对象的markword值处于轻量级锁状态 所以CAS会失败。
因为它采用的是无锁状态的markword值进行CAS,这一步CAS失败之后,当前线程需要检查为什么失败。
首先要检查的就是锁重入的情况,如果锁对象的markword内表示锁持有者的bit位的值 指向当前线程空间的话,说明当前锁持有者就是当前线程
这是一次锁重入的操作。锁重入的话,咱们仅仅需要把刚刚在栈内插入的那条锁记录的displaced 字段置为空,就可以了,完事。
锁重入次数是靠线程栈内指向当前锁的“锁记录”数量来完成的。当前线程每重入一次锁,就会在这个线程栈内插一条关于这把锁的锁记录。
[注意: 上图中的owner就是本图中的obj]
问:轻量级锁释放锁的大概逻辑
释放锁的字节码是monitorexit ,jvm处理这条指令时候首先从当前线程栈中找到“最后一条”锁引用字段指向当前锁对象的锁记录。
并且将这条锁记录的锁字段设置位null(这一步就是释放锁记录),这一步就是释放锁记录.. 这一步不需要CAS ,因为线程栈内的数据,不存在多线程访问的场景(栈封闭特性)
这一步完事之后,再检查这条锁记录的displaced字段是否有值? 如果没有值的话,说明这条锁记录是锁重入是存放的。
锁重入只需要将这一条锁记录释放,对应的逻辑
就是锁记录中的锁引用字段设置为null ,这样就完成了一次锁退出.
问 : 刚才你说了锁被重入之后的一次锁退出,还不是锁得完全释放呢..那假如说当前这个线程,它正在最后一次退出这把锁,完全释放这把锁,这块逻辑,您再说一下呗.
答: 首先也是将当前线程栈内锁引用指向当前锁对象的这条锁记录的锁引用字段设置为null ps 释放锁记录
然后检查锁记录,因为轻量级锁或者重量锁第一次加锁时候,在线程栈内插入的锁记录它比较特殊.第一次加锁时,这条锁记录它保存了一个displacedMarked值,这个
Markword值表示的状态对应无锁状态,接下里当前线程需要把锁记录内的displaced的值通过CAS的方式设置到当前锁对象的markword中,这一步设置成功之后,就
相当于完全释放锁了.
如果失败的话,当前锁可能已经被升级到重量级锁,或者 现在正处于碰撞状态中了,需要走重量级锁退出的逻辑了..
问: 假设当前锁状态处于偏向锁状态,并且偏向线程A,也就是说,所锁的markwoed内 存储的就是线程A的线程 ID ,,这种情况下: 线程B 是如何获取这把锁的..
当前偏向锁偏向线程A
线程B首先向线程栈内插入一条锁记录,锁记录的引用字段指向当前锁对象,发现当前锁对象处于偏向锁,并且偏向不是自己
线程B会提交一个撤销偏向锁的任务,这个任务会提交到VM线程的任务队列中,VM线程在后台不停地处理任务队列内的任务,VM取一个任务之后
看当前任务是否需要在安全点状态下执行,如果是那就等待安全点。
简单说下安全点,当处于安全点的时候,JVM所有的线程都处于阻塞状态。只有VM线程处于运行状态,VM可以处理一些特殊任务,比如Full GC,撤销偏向锁,
撤销偏向锁就必须在安全点内执行,因为撤销的过程中会修改持有锁的线程的桢,所以必须在安全点内执行
撤销偏向锁首先要检查JVM中当前所有的存活线程,首先检查持有偏向锁的线程是否存活,
前面咱们聊过偏向锁退出时,说过了,偏向锁退出时,并不将锁对象的偏向状态给置回,就算偏向锁完全退出,它仍然保留偏向状态..
a:如果持有偏向锁的线程已经消亡,直接把锁对象markword修改为无锁状态
b:如果偏向线程还活着:
活着还分两种呢:
一种是 偏向线程 仍然处于同步代码块中的还活着
另一种情况是 偏向线程 已经跳出同步代码块了 ,但是 偏向锁 仍然保留了偏向信息 ... 这个和刚刚聊过的 和偏向线程已消亡的处理是一致的. 直接将锁对象的 markword设置为无锁状态即可
问; 如何确定是否偏向线程还处于同步代码块中?
答: (如果栈内有一条所记录的锁引用字段指向当前锁对象,那么就说明偏向线程仍然处于同步代码块以内... 否则,说明偏向线程已经跳出同步代码块了..)
如果线程还在同步代码块,这是就需要将偏向锁升级了 ,,偏向锁升级的过程,首先遍历线程栈,找到锁记录指向当前锁对象的第一条记录,修改这条记录的displaced字段的值为无锁状态的markword,锁完全释放会使用到这个值 ..
再然后修改锁对象的markword为轻量级锁状态,并且呢..保留这条锁记录的内存地址,其实就是持有锁的线程的内存空间的第一个位置,后面可以根据这个位置空间来判断是不是当前线程持锁 .. 轻量级锁的markword保存着线程的地址。
偏向锁升级到轻量级锁的过程咱们聊完了,我不知道大家或者 您 有没有 发现一个问题 就算偏向锁升级到轻量级锁之后 , 并不能解决咱们目前的问题 , 偏向线程仍然
在同步代码块中, 另一个线程它也想进入到同步代码块内,然后就导致锁升级, 升级到轻量级锁之后,咱们知道,轻量级锁的应用场景是什么呀?多个线程它们交替窒息
感,它们并没有在并行(同步代码块),在非并行的情况下,轻量级锁可以避免创建Monitor管程嘛 但是现在问题已经很明显了,原偏向线程仍然在同步代码块以内..并且
咱现在的这个线程也想要执行同步代码块内的程序 轻量级锁它也不好使了 这块怎么办呀?
首先会触发一次偏向锁升级轻量锁的逻辑触发,升级完事以后,外部线程会继续自旋检查.. 它会检查处当前锁对象处于轻量级锁状态... 再触发一次锁升级,再将轻量级锁升级为重量级锁,然后利用重量级锁提供的互斥性,来保证线程安全..
轻量级锁->重量级锁的膨胀过程
重量级锁到底是干嘛用的:
强制将并行的线程依次通过同步代码块的实现,其实重量级锁和AQS很像,都是遵循管程模型。
重量级锁ObjectMonitor内部的主要数据结构有哪些?
核心的有3个队列,其中两个是 等待队列 分别 是 竞争队列 和 EntryList队列
还有一个阻塞WaitList (ps: WaitSet) 另外还有一个很重要的是当前持锁的线程引用.. owner字段 那这三个队列都是什么用途呢?
嗯 竞争队列和EntryList队列 , 这两个队列它用来存放等待获取锁资源线程的
WaitList这个是用来处理持锁线程调用锁对象wait()方法使用的 也就说锁,持锁线程调用锁对象Wait() 方法之后, 会把自己封装成一个Waiter节点插入到 WaitList队列里 再然后会唤醒一个等待节点,再然后这个执行wait操作的线程 就会使用park操作将自己挂起(ps: 会释放锁)
直到其他线程通过使用notify 或者 notifyAll这类的操作 将它从WaitList队列移动到 竞争队列
竞争队列和 EntryList队列里面的线程 最终会被持锁线程释放锁时选择一个唤醒 去抢占锁
重量级锁算不算公平锁呢?
很明显,不算公平锁,因为持锁线程释放掉锁之后,去竞争队列和EntryList队列内唤醒下一个继承者之前,这个窗口期间,外部再来线程是可以尝试获取锁定的
获取重量级的过程:
重量级锁markword保存的是管程对象的内存地址和重量级锁的状态值10,通过重量级锁markword保存的管程对象地址,找到管程对象,线程去管程内取抢占锁
进入管程之后自旋几次尝试获取锁,其实就是通过CAS设置owner字段指向自己,如果CAS成功锁抢占成功
如果失败把当前线程封装成waiter节点插入到竞争队列,插入成功后把自己挂起,一直等到其他线程唤醒
大厂最爱问的问题: 锁膨胀成重量级锁的过程
https://zhuanlan.zhihu.com/p/435839659
哪些情况下会发生锁膨胀操作呢?
一共有3种情况:
1 有线程调用轻量级锁或者偏向锁对象的hashcode方法 因为锁对象处于轻量级或者偏向状态时,markword 是没办法存储hash值的 所以
这个时候会膨胀成重量级锁, 然后重量级锁
2 持锁的线程调用锁对象的wiat()方法会导致膨胀
因为锁对象处于其他状态时,是没有管程对象存在的 没有管程存在 就没有地方存放线程节点
3第三种情况: 当前锁状态是轻量级锁状态,然后其他线程竞争是,真正的并发产生了 这个时候会膨胀成重量级锁
说第三种
首先这个竞争线程 因为获取锁失败 会走上 锁膨胀的逻辑 在锁膨胀逻辑里,会判断出来当前锁是轻量级锁状态 接下来 这个线程会获取一个空闲的管程对象 然后通过cas 修改锁对象状态为膨胀中 如果cas失败 说明有其他线程正在膨胀或者已经 膨胀结束了 , 再次自旋获取就可以了
如果cas后操作成功 那么当前线程需要给这个锁做升级操作 主要是将管程对象的owner 设置为员轻量级锁持有线程 然后再将持锁线程的第一条所记录内存储的displacedMarkword保存到管程对象内 可能锁降级的时候会用到吧
把栈顶markword拷贝到重量级锁之后 ,再下一步 设置锁对象的markword为重量级锁 包括两个信息
1个是重量级锁对象内存位置 另一个是重量级锁状态 再之后 其他线程再去请求获取这把锁 就都会找到这个重量级锁对象 走管程内的逻辑了
完事
感谢这个demo,让我看懂了下面的图
我先通俗的再用团委老师讲一下锁升级 举个例子
- 无锁:比如社团有一间教室 上自习 大家都可以用 没有财产问题 就是无锁状态
- 偏向锁:后来社团添置了打印机投影仪之类的物品,不能再敞开着大门了,团委老师就安装了一把锁,但是社团教室只有 小韩 一个同学来上自习,团委老师就把钥匙给他保管,因为下次教室还是他用,用完了钥匙就不用还给团委老师了,这就是偏向锁,节省了资源不用来回跑团委,如果小韩同学再也不用教室钥匙了,或者小韩不用的时候 另一位同学用,就把钥匙给另一位同学就行。
- 轻量锁:后面小陈也想用这个自习室,但是自己没有钥匙,就去找团委老师要,团委老师把钥匙放出去没收回来,看来大家都想用这个钥匙,以后谁在用就往团委借钥匙吧。 轻量锁就比原来偏向锁麻烦些了 偏向锁只需要借一次钥匙,轻量级锁次次都要借钥匙还要还钥匙,轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
- 重量级锁:小李 小张 小红 也都都想用钥匙,一开始团委老师说你们先等等,一个一个来,下一节课再来团委看看有没有钥匙,小红来了十次都没借到钥匙,要把自己气死了,直接去找院长,院长听了团委老师的管理方式,气不打一出来,你把钥匙随便给学生,出了问题你负责吗?以后谁也别借教室钥匙了,整个学院的东西,谁要用都找我借,教室门以后不用锁了,我直接把学院大门锁上,谁要用直接找我要学院的钥匙,用完把学院的大门锁上。 (重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。)