流程是跟着 死磕Synchronized底层实现 来学习和尝试去看了一些源码实现,源码解读还是建议读这位的系列文章。本文主要自己重新梳理一下流程。
名词解释
先解释一下一些词方便理解流程。
对象头:一个对象头存储着MarkWork和类型指针,数组对象还会存储数组长度
MarkWord:默认情况下存储着对象HashCode、分代年龄和锁的状态位和标志位
Epoch: 其本质是一个时间戳,代表了偏向锁的有效性,除了对象中的epoch,对象所属的类的class信息中也会保存epoch值。 JVM内部为每个类维护了一个偏向锁计数器会记录类的对象的所有重偏向操作。当达到阈值时,会对这个类的对象进行批量重偏向,对类的class信息保存的epoch值进行更新,然后扫描所有持有类的对象的线程栈,将更新的值赋给被锁定的对象,来保证线程安全。线程获取偏向锁的话,会去比较类class信息的epoch和MarkWork中的epoch不相同的话代表可以重偏向,将尝试偏向锁。
LockRecord: 包含对Mark Word的拷贝(Displaced Mark Word)和对锁对象的指针。是线程栈中锁记录。 当前处于轻量级锁状态下,Displaced Mark Word位置存储的是无锁状态的对象头,重入的话会是标记为null。
ObjectMonitor: 对象锁的监管者,ObjectMonitor对象中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),控制着一个对象阻塞队列和条件队列的入队和出队,ObjectMonitor对象中也有一个变量_owner,_owner指向持有ObjectMonitor对象的线程(就是当前正在执行的线程)
偏向锁:是针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一同个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其MarkWord中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchornized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,就可以直接进入到该方法中去。
轻量级锁:轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开。轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。
重量级锁:重量级锁依赖操作系统的MutexLock(互斥锁)来实现,会通过操作系统来进行阻塞和唤醒线程,阻塞和唤醒会从用户态转换到内核态。重量级锁是由ObjectMonitor来控制。
加锁流程
偏向锁加锁
偏向锁这里要注意,如果开启偏向锁优化是打开的,那么在对象创建的时候,Mark Word的是否偏向位置就是1,创建的时候就是一个匿名的偏向锁。
进入加锁流程后,如果当前锁对象是偏向的,则进入偏向锁的处理流程(白色填充),四种情况:
- 当前锁是偏向自己的就重入,计数加1,然后就可以直接退出了;
- 当前类的对象被批量锁重入,导致epoch的值不等于Class信息的epoch,然后使用CAS操作抢锁,成功就退出,没抢到线程的会去触发锁升级的流程。
- 当前类的对象可能由于多次偏向锁撤销,认为当前类的对象不适合偏向锁,这时就会尝试撤销当前偏向锁,然后进入非偏向锁(橘色填充)的执行流程。
- 上面三种是属于特殊情况,正常就是拿偏向自己的MarkWord CAS替换匿名偏向的MarkWord,也就是当前锁对象没有偏向任何一个线程,并且当前线程抢到了锁,就直接退出成功;当前有偏向线程,代表没抢到,当前线程就会去执行当前锁对象的锁升级流程。
偏向锁锁升级
进去锁升级流程后也会进行进一步判断
白色填充代表主流程,红色填充应该是不会走到,这里我也不明白为什么修改成匿名偏向,在主要流程上。
返回1代表要认为偏向锁已经撤销,会进入轻量级锁竞争流程。
返回3代表偏向锁已经撤销且锁重偏向成功,直接获取到锁了
CAS失败回直接进入撤销偏向锁,但这里撤销要等安全点
匿名偏向这里拿到 死磕Synchronized底层实现 的解释当调用锁对象的Object#hash或System.identityHashCode()方法会导致该对象的偏向锁或轻量级锁升级。这是因为在Java中一个对象的hashcode是在调用这两个方法时才生成的,如果是无锁状态则存放在mark word中,如果是重量级锁则存放在对应的monitor中,而偏向锁是没有地方能存放该信息的,所以必须升级。
类关闭偏向模式的话,置为无锁,这里接着走下去会去升级为轻量级锁。
撤销偏向锁
如果不满足上面三种情况或者没在上面的环境退出,就会进入偏向锁的撤销。
这里的返回1也是代表要认为偏向锁已经撤销,会进入轻量级锁竞争的流程。
这里除非偏向锁是自己线程的,否则会在安全点执行,会去判断偏向的线程是否存活,是否还拥有锁,如果不存活或者存活但不拥有锁,就会把markword置为无锁,方便发起撤销的线程升级轻量级锁,如果线程还存活,而且拥有锁,为了保证线程安全,会帮它直接升级成轻量级锁,然后执行偏向撤销的就会进入轻量级锁竞争的执行流程。
轻量级锁
如果当前不是偏向锁,则走橘色填充的流程,设置当前LockRecord为无锁的对象头代表当前线程准备好获取轻量级锁,这里用自己的LocckRecord地址CAS去抢占无锁的MarkWord,流程走到这,要么是Class关闭了偏向锁,要么当前不是偏向锁状态,Class关闭偏向锁的话尝试撤销偏向锁,没有竞争的话就会设置锁对象头为无锁,而轻量级锁释放的话,会把Displaced Mark Word(无锁MarkWord)设置回锁对象头,所以这里抢占的是无锁MarkWord,如果失败则代表有竞争存在,进行锁升级,成功的话,就代表当前线程获取到了轻量级锁。
锁升级为轻量级
轻量级锁释放后会拿LockRecord存储的无锁对象头替换锁对象的MarkWord,所以不重入的话就代表发送竞争了,要升级为重量级锁。
无锁状态下,CAS设置MarkWork为当前线程栈锁记录是要获取轻量级锁,获取失败代表发生竞争,要升级成重量级锁。
偏向锁锁升级和撤销偏向锁返回1的时候都会进入当前流程,来升级到轻量级锁。
重量级锁
重量级锁是由互斥量ObjectMonitor来控制的,所以会先去创建或者获得一个ObjectMonitor对象。
拿到ObjectMonitor后,存储的owner不是自己的话就会先尝试自旋去抢锁,超出一定次数后,就会把自己当前线程封装成一个Node节点CAS的方式进入CXQ队列,这里的CXQ也就是重量级锁的等待队列了,失败的话会尝试 获取一遍锁,再入队的自旋操作。成功后也会尝试获取一下锁,没抢到就会调用系统函数来阻塞。解锁后会再去尝试获取锁。
锁释放
Synchronized是一个非公平锁,所以可能不需要唤醒就已经有线程抢到了锁,如果需要唤醒的话会根据策略一个要被唤醒的线程,然后把ObjectMonitor的owner修改为null,然后唤醒选择的线程。