synchronized面试题总结
1 偏向锁
当使用synchronized锁定某一个对象的时候,如果该对象处于匿名偏向状态,就是markword的64位二进制表示偏向线程的bit位都是0,并且锁状态是偏向状态。
此时该线程直接在这个锁对象的markword高位内存储 当前线程的内存地址,当然了这部分是CAS完成的
如果CAS成功,这把锁就偏向当前的线程了。
1 还要向当前的线程栈添加一条锁记录,让锁记录中的锁标识指向当前锁对象
2 通过CAS设置锁对象的markword,存储当前的线程地址
2 偏向锁没法做到线程间的互斥,为什么还要这把锁呢
有时候开发者认为某段代码存在竞争,其实可能实际跑起来并不存在竞争,很可能只有一个线程在执行
而且偏向锁比轻量级锁损耗更小,因为它在释放锁的时候不需要CAS
3 偏向锁的释放
jvm在处理monitorexit这个指令的时候,第一步当前线程栈内与 这个锁对象相关的锁记录全部拿到,并释放掉最后一条锁记录。偏向锁对象仍然保持着偏向状态
4 这么做有什么好处呢
为了下次获取锁更快,下次获取锁只需要对比线程地址就行了,不用再次CAS了
5 轻量级锁
轻量级锁仍然解决不了线程竞争问题,它并不能提供线程之间的互斥性。轻量级锁对于线程交替运行的情况有很好的性能
6 轻量级锁的加锁过程
1 从无锁到轻量级锁
在当前线程的线程栈插入一条锁记录,锁记录内锁引用字段保存锁对象的地址,让当前的锁对象生成一条无锁状态的markword值,命名是displacedMarkWord
把这个displacedMarkWord保存到锁记录的displaced字段内,注意是无锁状态的markword。为啥呢?锁释放还得写回去啊,不先保存无锁状态释放的怎么回写呢?
通过CAS的方式设置当前锁对象的markword值,修改为 当前线程持有轻量级锁状态,如果当前对象是无锁状态就能修改成功
7 轻量级锁重入
在收到monitorenter的时候,向当前线程栈内插入一条锁记录,锁记录的obj字段保存该锁对象的内存地址。
还会再生成一条无锁状态的markword,同时保存到锁记录的displaced字段内。
还是会通过CAS的方式设置锁对象的markword,所以CAS会失败,然后当前线程会检查失败原因,如果当前锁对象保存的线程地址就是当前线程的话,这就是重入。如果是重入就把刚刚插入的锁记录的displaced字段置为空,流程结束
8 轻量级锁的释放
monitorexit,从当前线程的栈中找到最后一条锁的obj字段指向当前锁对象的锁记录,然后把这条lockrecord设置为null,然后检查他的displaced字段是否有值,如果为null,那说明说明这个lockrecord是重入的释放放入的。释放的时候就是把锁记录的obj字段设置为null,这样就完成了退出。
如果是最后一个recordlock释放的话,释放的过程也是吧obj设置为null,然后检查锁记录,如果第一次那么这个lockrecord的displacedMarkWord保存的是无锁状态的markword,此时就要把这个无锁状态的markword通过CAS方式再放回到对象的markword中。
9 偏向锁的升级
当前偏向锁偏向线程A
线程B首先向线程栈内插入一条锁记录,锁记录的引用字段指向当前锁对象,发现当前锁对象处于偏向锁,并且偏向不是自己
线程B会提交一个撤销偏向锁的任务,这个任务会提交到VM线程的任务队列中,VM线程在后台不停地处理任务队列内的任务,VM取一个任务之后
看当前任务是否需要在安全点状态下执行,如果是那就等待安全点。
简单说下安全点,当处于安全点的时候,JVM所有的线程都处于阻塞状态。只有VM线程处于运行状态,VM可以处理一些特殊任务,比如Full GC,撤销偏向锁,
撤销偏向锁就必须在安全点内执行,因为撤销的过程中会修改持有锁的线程的桢,所以必须在安全点内执行
撤销偏向锁首先要检查JVM中当前所有的存活线程,首先检查持有偏向锁的线程是否存活,
如果持有偏向锁的线程已经消亡,直接把锁对象markword修改为无锁状态
如果偏向线程还活着,首先要确定偏向线程是否还在执行同步代码块,这个通过锁记录就能知道了。
如果已经不在同步代码块了,那就直接把锁对象的markword设置为无锁状态
如果线程还在同步代码块,此时就是锁升级的过程,首先遍历线程栈,找到锁记录指向锁对象的第一条记录,修改这条记录的displaced字段的值为无锁状态的markword,然后修改锁对象的markword为轻量级锁状态,保留这条锁记录的内存地址,其实就是持有锁的线程的内存空间的第一个位置,轻量级锁的markword保存着
线程的地址。
10 什么是重量级锁
重量级锁和AQS很像,都是遵循管程模型。其在JVM中的类型是ObjectMonitor,它数据结构的核心是三个队列
CQX队列和EntryList队列,还有waitList(相当于是AQS的条件队列)
还有一个很重要的当前锁的持有线程owner字段。
cqx和EntryList是保存竞争线程排队的,waitList用来处理调用了wait()方法后从竞争锁队列转移到等待队列
11 重量级获取锁过程
重量级锁markword保存的是管程对象的内存地址和重量级锁的状态值10,通过重量级锁markword保存的管程对象地址,找到管程对象,线程去管程内取抢占锁
进入管程之后自旋几次尝试获取锁,其实就是通过CAS设置owner字段指向自己,如果CAS成功锁抢占成功
如果失败把当前线程封装成waiter节点插入到竞争队列,插入成功后把自己挂起,一直等到其他线程唤醒
12 重量级锁的释放
首先把ObjectMonitor的 owner = null, 检查竞争队列和EntryList是否有等待的线程,如果有则根据唤醒策略去唤醒一个线程。
QMode == 1 : 将cxq中的元素转移到EntryList,并反转顺序
QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;
QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
QMode = 0:暂时什么都不做,继续往下看;
13 锁膨胀的过程
锁膨胀的三种情况
1 当调用锁对象的hashcode方法的时候,因为偏向锁和轻量级锁都没法存储hash值,只有重量级锁可以
2 当调用wait方法的时候,因为只有重量级锁才有管程,才能把这个线程包装成Wait放到WaitSet中
3 轻量级锁经过竞争膨胀
这里只讨论轻量级锁竞争导致的膨胀
假设现在有A正在持有轻量级锁,此时B去竞争,此时B会执行锁膨胀逻辑。锁膨胀逻辑里面线程B会获取一个空闲的管程对象,通过CAS修改锁对象为膨胀中状态,如果CAS失败说明正在膨胀或者膨胀已经结束,那就再次自旋。如果CAS成功了,说明当前线程设置锁状态成功,那么就由它来进行锁升级,将管程对象的owner字段设置为原轻量级锁持有线程,再将持有线程的第一条锁记录内存储的displacedMarkWord保存到管程对象里。再下一步,设置锁对象的markword为重量级锁,包含两个信息,一个是重量级锁也就是ObjectMonitor的内存位置,另一个是改变锁状态。