synchronized加锁流程 从偏向锁到重量级锁

  1. 加锁流程图

  2. 对象头介绍
    Java对象的内存布局分为三个部分:对象头、实例数据和对其填充。其中对象头又分为两部分使用64bits:用于存储对象自身运行时的数据——Mark Word(32bits)和类型指针,即指向它的类型元数据的指针——Klass Word(32bits)。使用synchronized进行对象锁的同步控制需要用到对象头中的Mark Word,从偏向锁膨胀到重量级锁的过程中,每一步都涉及了Mark Word的改变。初始(没有成为锁的时候)Mark Word会使用25bits记录hashcode(实际生成hashcode是在Object::hashCode()),使用4bits记录分代年龄,使用1bit记录偏向锁标志(初始为0)和2bits记录锁标志。

  3. Monitor
    Monitor是在使用对象锁的时候,Java虚拟机为对象生成的,在Java代码层面不可见,也无法直接操作。并且只有锁膨胀到重量级锁的时候才会使用到。

  4. 锁膨胀过程
    1、偏向锁:Java默认是开启偏向锁的,偏向锁的存在意义是:如果只有一个线程会使用到该锁,那么就不需要频繁修改Mark Word和使用Monitor对象了,提升了同步的效率。当初次使用对象锁的时候,对象锁的MarkWord会将原本记录hashcode的位置变为记录持锁线程(23bits)和epoch(2bits,在批量重定向和批量撤销的时候使用)。
    synchronized加锁流程 从偏向锁到重量级锁_第1张图片
    2、轻量级锁:当有线程想获取已经被别的线程持有偏向锁时(此时没有竞争,是交错使用的,如果不是交错使用,而是竞争使用的话会膨胀为重量级锁),该锁对象将会从偏向锁膨胀为轻量级锁。升级为轻量级锁的锁对象的MarkWord会再一次被改变,原先的Mark Work记录的分代年龄、偏向锁标志、偏向锁占用的25bit都会被清空,变为持锁线程的锁记录(lock record)地址。lock record是在线程加锁的时候生成的,该对象记录了自身地址+00(轻量级锁标记)和锁对象的引用。
    synchronized加锁流程 从偏向锁到重量级锁_第2张图片
        2.1、初次持有轻量级锁:线程生成的Lock Record对象会尝试将自己的地址+00跟对象锁的Mark Word进行交换,如果交换成功,将Lock Record的锁对象引用指向对象锁,此时初次加锁成功。如果交换失败并且对象锁的持有线程不为自身的话,会进入重量级锁膨胀的过程。这里的交换都是CAS
        2.2、轻量级锁重入:已经持有对象锁的线程想要再次获取同一个锁对象,线程栈中会再生成一个Lock Record对象,该对象同样会尝试和对象锁中的Mark Word进行CAS交换,此次交换肯定会失败,然后会检测对象锁的持有线程,持有线程如果是自身的话,那么Lock Record的锁对象引用会指向对象锁,Lock Record对象继续保存,成为重入计数。
    3、重量级锁:当轻量级锁产生竞争时(同时出现多个线程争夺一个对象锁),轻量级锁将会膨胀为重量级锁。过程如下:
        3.1、尝试添加轻量级锁时,和Mark Word 进行CAS交换时失败了,同时当前线程不是持锁线程。那么将会为对象锁申请Monitor锁,同时对象锁的Mark Word会指向该Monitor对象。同时Monitor对象会将其Owner设置为原持锁线程,并且将加锁失败的线程放入Monitor的EntryList。
    synchronized加锁流程 从偏向锁到重量级锁_第3张图片
    synchronized加锁流程 从偏向锁到重量级锁_第4张图片
        3.2持有轻量级锁的线程在轻量级锁膨胀为重量级锁之后的解锁过程:轻量级锁持有者代码执行完毕,将锁记录通过cas与锁对象的Mark Word进行交换的时候,监测到锁对象过的Mark Word的锁状态不为00(即轻量级锁)时,cas失败,此时进入重量级锁解锁过程。即按照Mark Word中存储的Monitor对象的地址,寻找到Monitor对象过,设置Monitor对象的Owner为null,并唤醒EntryList中的BLOCKED的线程。

  5. 重量级锁加锁流程
    1、尝试获取重量级锁的众多线程中某一个幸运儿会夺取到重量级锁,该线程在后面称为持锁线程。锁竞争成功后,Monitor对象中的Owner会指向持锁线程,持锁线程运行同步代码。而其未竞争成功的线程将会进行一段时间的自旋操作,该操作是JVM对锁竞争的优化,自旋操作不断尝试获取对象锁,如果在这段时间内,持锁线程完成同步代码快,释放了锁,那么自旋中的某个线程将会获取该锁,从而避免进入BLOCKED状态,避免了自身的线程上下文切换(该操作在单核cpu下没有优化效果)。如果自旋的过程中还是没法竞争到锁的话,那么这些线程将会进入EntryList中等待锁,线程状态变为BLOCKED。
    2、当持锁线程执行完同步代码,会将Monitor对象的Owner置为null,同时唤醒那些在EntryList中的阻塞线程,开始重新竞争锁。
    3、持锁线程可能还因为调用了LockSupport.park()、thread.join、wait等方法进入WaitSet中,线程状态变为WAITING/TIMED_WAITING,此时也会将Owner置null,然后唤醒EntryList中的线程,重新竞争锁。

你可能感兴趣的:(多线程学习笔记,多线程,java,jvm,并发编程)