Java多线程三——偏向锁/轻量级锁/重量级锁详解

三、 偏向锁/轻量级锁/重量级锁原理

锁升级模型图:

57d13e9832f1435c87073e5aa656646f.jpeg

3.0 了解 Lock Record_偏向锁和轻量级锁使用

Lock Record 是线程私有的,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word。线程获取到偏向锁或轻量级锁时,在当前线程栈帧中插入一个 Lock Record 记录,表示一次加锁,重入多少次就加多少个Lock Record 记录。

Lock Record包含两个信息:

  • 锁对象的Mark Word信息 ;
  • owner(指向锁对象的内存地址)。

在偏向锁和轻量级中使用LockRecord进行加解锁 :

  • 偏向锁时 Lock Record 中的 Mark Word 为空,owner指向锁对象的内存地址 ;
  • 轻量级锁时Lock Record 保存锁对象的Mark Word信息,owner指向锁对象的内存地址,并将锁对象的Mark Word信息替换为获得锁线程的LockRecord地址 。

例(轻量级锁):

2b9303257e5c45b88a6c59b49accb907.png

3.1 偏向锁

偏向锁是指一段同步代码一直被一个线程所访问(不存在多线程竞争),那么该线程会自动获取锁,降低获取锁的代价。

3.1.1 引入偏向锁的目的

在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令。但在多线程竞争时,需要进行偏向锁撤销步骤,因此其撤销的开销必须小于节省下来的CAS开销,否则偏向锁并不能带来收益。

JDK 1.6中默认开启偏向锁,可以通过 -XX:-UseBiasedLocking 来禁用偏向锁。当新创建一个对象的时候,如果该对象所属的 class 没有关闭偏向锁模式(默认所有class的偏向模式都是是开启的),那新创建对象的Mark Word将是可偏向状态(状态码为101),此时 Mark Word 中的 thread id(参见上文偏向锁状态下的Mark Word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

需要注意的是,即使模式默认开启,出于性能(启动时间)的原因,在JVM启动后的的头4秒钟这个feature是被禁止的。这也意味着在此期间,方法区 InstanceKlass 中的 _prototype_header 会将它的 locked_bias 位设置为0,以禁止实例化的对象被偏向。4秒钟之后,所有的 _prototype_headerlocked_bias 位会被重设为1,如此新的对象就可以被偏向锁定了。

  • 当对象头的 locked_bias 为0时,此对象处于未锁定不可偏向的状态。
    • 在此状态下,如果有线程尝试获取此锁,会升级为轻量级锁。如果有多个线程尝试获取此锁,会升级为重量级锁。
  • 当对象头的 locked_bias 为1时,此对象处于以下三种状态:
    • 匿名偏向(Anonymously biased) :状态码101,ThreadId为NULL(0),意味着还没有线程偏向于这个锁对象。第一个试图获取该锁的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。这是允许偏向锁的类对象的初始状态。
    • 可重偏向(Rebiasable) :在此状态下,偏向锁的 epoch 字段是无效的(即与锁对象对应InstanceKlass 的 _prototype_header 的 epoch 值不匹配,下文详解)。下一个试图获取锁对象的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。在批量重偏向的操作中,未被持有的锁对象都被置于这个状态,以便允许被快速重偏向。
    • 已偏向(Biased) :在此状态下,Thread Id非空,为已经持有此锁的线程ID ,且 epoch 为有效值——意味着其他线程正在使用这个锁对象。

3.1.2 偏向锁加锁过程【OOP和klass详见上节】

  • 验证对象 Mark Word 的 locked_bias 位。
    • 如果是0,则该对象不可偏向,走轻量级锁逻辑;如果是1,继续下一步操作。
  • 验证对象所属 InstanceKlass _prototype_header locked_bias 位。
    • 确认 _prototype_header locked_bias位是否为0,如果是0,则该类所有对象全部不允许被偏向锁定,并且该类所有对象的 locked_bias 位都需要被重置,使用轻量级锁替换;如果是1,继续下一步操作。
  • 如果为可偏向状态,且 ThreadId 为null(匿名偏向),则使用 CAS 时将对象的 Mark Word 头设置为当前入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,则无需再通过CAS来加锁和解锁了,只需往当前线程的栈中添加一条 Mark Word 为空的Lock Record,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;在这种情况下,synchronized 关键字带来的性能开销基本可以忽略。
  • 如果为可偏向状态,且 Java 对象头中的 ThreadId 并未指向当前线程(其他线程如线程2要竞争锁对象,而偏向锁不会主动释放,因此存储的还是线程1的threadID),则表示有竞争。当到达全局安全点(safepoint,代表了一个状态,在该状态下所有线程都是暂停的,即stop the world)时,去查看偏向的线程是否还存活,如果获得偏向锁的线程存活则被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
    • 如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里。
    • 如果偏向的线程已经不存活或者不在同步块中,则检查对象是否可以重偏向(对象epoch是否等于InstanceClass的epoch):
      • 在允许重偏向(epoch不相等)的情况下,原所有者线程会触发解锁,将对象恢复成匿名偏向(状态码=101,线程ID=0)的状态,然后重新偏向至当前线程;
      • 如果不允许重偏向(epoch相等),则会触发偏向锁撤销,将对象设置为无锁状态(001),竞争者线程按轻量级锁的逻辑去获取锁。

3.1.3 偏向锁的CAS详解

如果Java对象头中的线程ID不为当前线程ID,则表示可能存在竞争,但是因为锁升级是不可逆的,所以不会直接把偏向锁升级为轻量级锁,此时虚拟机会做【挽回策略】避免不必要的升级:

  • 首先会先等待持有偏向锁的线程进入安全点savepoint(与gc时的安全点是一样的),暂停线程;
  • 虚拟机会检查持有线程锁的线程状态是否存活:
    • 如果存活遍历栈帧中的Lock Record,判断该线程是否在执行同步代码块中的代码,如果是那就没办法只能升级;
    • 如果线程状态不是存活的或者不在执行同步代码块中的代码将会判断是否开启重偏向(对象epoch是否等于InstanceClass的epoch)。如果成功执行了重偏向那么将会重新设置为偏向锁状态,将线程id更新为新的线程id;不可重偏向就升级为轻量级锁。

3.1.4 偏向锁到偏向锁

由于偏向锁线程1获取锁后,不会主动修改对象头,所以哪怕此线程1实际已消亡,之前加锁对象的对象头还是保持偏向锁状态。这个时候线程2想要进入同步方法,他会先去查看线程1是否还存活:

  1. 如果线程1已经消亡或者不在同步块,则检查对象是否可以重偏向(对象epoch是否等于InstanceClass的epoch):
    1. 在允许重偏向的情况下,原所有者线程会触发解锁,将对象恢复成匿名偏向(状态码=101,线程ID=0)的状态,然后重新偏向至当前线程。偏向锁->解锁->偏向锁
    2. 如果不允许重偏向,则会触发偏向锁撤销,将对象设置为无锁状态(001),竞争者线程按轻量级锁的逻辑去获取锁。偏向锁->锁撤销->轻量级锁
  2. 如果线程1未消亡且还在同步代码块,但是其栈帧信息中不再需要此持有这个锁对象,会进行一次偏向锁->锁撤销->轻量级锁的过程。

【是否在同步代码块】:栈帧中是否存在 Lock Object地址指向该锁对象的Lock Record。

3.1.5 偏向锁的批量重偏向

如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了线程T1,T1同步结束后,另一个线程T2也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作。

-XX:BiasedLockingBulkRebiasThreshold 偏向锁批量重偏向阈值,默认20

  • 当对象数量超过重偏向阈值时,Java会对超过的对象作批量重偏向线程T2,此时前20个对象是轻量锁, 后面的对象都是偏向锁,且偏向线程T2。(前20个偏向锁->轻量级锁的过程包含锁撤销操作,当撤销次数达到批量重偏向阈值20时,会进行批量重偏向)
  • 如果对象虽然被多个线程访问,但没有发生竞争,这时偏向了T1的对象仍然有机会重新偏向T2,重偏向会重置对象的ThreadID 。

【为什么有批量重偏向】:当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point(安全点)时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。这个过程是要消耗一定的成本的,所以如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

#测试代码
public class BulkBias {

    private static Thread t1, t2;

    public static void main(String[] args) throws InterruptedException {
        // 延时产生可偏向对象
        TimeUnit.SECONDS.sleep(5);

        List<B> objects = new ArrayList<>(); // 创建50个对象,锁状态为101,匿名偏向锁
        for (int i = 0; i < 50; i++) {
            objects.add(new B());
        }

        t1 = new Thread(() -> {
            for (int i = 0; i < objects.size(); i++) {
                synchronized (objects.get(i)) { // 50个对象全部偏向t1 101
                }
            }
            LockSupport.unpark(t2); //恢复某个线程的运行(park & unpark 可以先 unpark)
        });

        t2 = new Thread(() -> {
            LockSupport.park(); // 暂停当前线程(park & unpark 可以先 unpark)
            //这里面只循环了30次!!!
            for (int i = 0; i < 30; i++) {
                Object a = objects.get(i);
                synchronized (a) {
                    //分别打印第19次和第20次偏向锁重偏向结果
                    if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        // 第19次轻量级锁00,第20次偏向锁101,偏向t2
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
        });
        t1.start();
        t2.start();
        t2.join();

        System.out.println("打印list中第11个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(objects.get(10)).toPrintable())); // 01 无锁
        System.out.println("打印list中第26个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(objects.get(25)).toPrintable())); // 101 偏向t2
        System.out.println("打印list中第41个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(objects.get(40)).toPrintable())); // 101 偏向t1
    }
}

class B {
}

//运行结果
第19次偏向结果
com.morris.concurrent.syn.batch.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           40 f3 bc 1d (01000000 11110011 10111100 00011101) (498922304)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第20次偏向结果
com.morris.concurrent.syn.batch.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 69 6c 1e (00000101 01101001 01101100 00011110) (510421253)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

打印list中第11个对象的对象头:
com.morris.concurrent.syn.batch.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

打印list中第26个对象的对象头:
com.morris.concurrent.syn.batch.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 69 6c 1e (00000101 01101001 01101100 00011110) (510421253)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

打印list中第41个对象的对象头:
com.morris.concurrent.syn.batch.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 60 6c 1e (00000101 01100000 01101100 00011110) (510418949)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

运行结果分析:

  • 当一个线程t1运行结束后,所有的对象都偏向t1。
  • 线程t2只对前30个对象进行了同步,0-18的对象会由偏向锁(101)升级为轻量级锁(000)【对象和方法区InstanceKlass的epoch相等】,19-29的对象由于撤销次数达到20,触发批量重偏向,偏向线程t2。
  • t2结束后,0-18的对象由轻量级锁释放后变成了无锁,19-29的对象偏向t2,30-49的对象还是偏向t1。

【总结】:批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

3.1.6 偏向锁的批量撤销

批量撤销就是对重偏向的一个补充。如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了T1,T1同步结束后,另一个线程T2也将这些对象作为锁对象进行操作(锁撤销),会导致偏向锁重偏向的操作。当一个偏向锁如果撤销次数到达撤销阈值(默认40)的时候就认为这个对象设计的有问题;那么JVM会把这个类所有的对象都撤销偏向锁,并且新实例化的对象也是不可偏向的,直接走轻量级锁的逻辑。

-XX:BiasedLockingBulkRevokeThreshold 偏向锁批量撤销阈值,默认40。

-XX:BiasedLockingDecayTime 距上次批量重偏向25秒内,撤销计数达到40,就会发生批量撤销。每隔(>=)25秒,会重置在[20, 40)内的计数,这意味着可以发生多次批量重偏向。

偏向锁的撤销的场景:

  1. 被偏向的对象进行hashcode计算时,不管该对象有没有被锁定,都会触发偏向锁撤销,通过CAS将计算好的hashcode存入Mark Word中。
  2. 当前的对象是已偏向未锁定状态,即所有者线程已经退出同步代码块,此时若有其它的线程尝试获取偏向锁:
    1. 在允许重偏向(还已触发批量重偏向,epoch不相等)的情况下,原所有者线程会触发解锁,将对象恢复成匿名偏向(状态码=101,线程ID=0)的状态;
    2. 如果不允许重偏向(还未触发批量重偏向,epoch不相等),则会触发偏向锁撤销,将对象设置为未锁定且不可偏向的状态(001),竞争者线程按轻量级锁的逻辑去获取锁。
  3. 当前的对象是已偏向已锁定的状态,即所有者线程正在执行同步代码块,此时有其它的线程尝试获取偏向锁,由于所有者线程仍需要持有这把锁,此时产生了锁竞争,偏向锁不适合处理这种有竞争的场景,即会触发偏向锁撤销,原偏向锁持有者线程会升级为轻量级锁定状态,竞争者线程按轻量级锁的逻辑去获取锁(锁升级)

锁撤销解锁是两个不同的概念:

  • 撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为无锁状态(状态码=001);
  • 解锁是指退出同步块的过程,即移除最近的锁记录,将对象恢复成匿名偏向(状态码=101,线程ID=0)的状态。

批量撤销测试代码如下:

public class BulkBiasAndRevoke {

    private static Thread t1, t2, t3, t4;

    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5); // 等待偏向延迟时间到达

        List<L> list = new ArrayList<>();
        for (int i = 0; i < 80; i++) {
            list.add(new L());
        }

        t1 = new Thread(() -> {
            for (int i = 0; i < 60; i++) {
                L l = list.get(i);
                synchronized (l) {
                }
            }
            LockSupport.unpark(t2);
        }, "t1");

        t2 = new Thread(() -> {
            LockSupport.park();
            for (int i = 0; i < 60; i++) {
                L l = list.get(i);
                synchronized (l) {
                }
            }
        }, "t2");

        t3 = new Thread(() -> {
            LockSupport.park();
            System.out.println("t3");
            for (int i = 0; i < 60; i++) {
                L l = list.get(i);
                // 0-18 001
                // 19-59 101 偏向t2
                synchronized (l) {
                    // 0-59 00
                }
                // 0-59 001
            }
        }, "t3");
        t4 = new Thread(() -> {
            synchronized (list.get(65)) {
                System.out.println("t4 begin" + ClassLayout.parseInstance(list.get(65)).toPrintable()); // 101
                LockSupport.unpark(t3);
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t4 end" + ClassLayout.parseInstance(list.get(65)).toPrintable()); // 00
                System.out.println("t4 end" + ClassLayout.parseInstance(list.get(66)).toPrintable()); // 101

            }
        }, "t1");
        t4.start();
        t1.start();
        t2.start();
        t3.start();
        t3.join();
        t4.join();

        System.out.println(ClassLayout.parseInstance(new L()).toPrintable()); // 001
    }
}
class L {
}


//运行结果
t4 begincom.morris.concurrent.syn.batch.L object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 61 79 1e (00000101 01100001 01111001 00011110) (511271173)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t4 endcom.morris.concurrent.syn.batch.L object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           28 ef c2 1d (00101000 11101111 11000010 00011101) (499314472)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t4 endcom.morris.concurrent.syn.batch.L object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.morris.concurrent.syn.batch.L object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

运行结果解析:

  • t1执行完后,0-59的对象偏向t1。
  • t2执行完后,0-18的对象为无锁(偏向锁->轻量级锁->无锁),偏向锁撤销执行到19号对象,也就是第20个锁对象时,会触发批量重偏向(触发批量重偏向后InstanceClass的epoch+1,此时对象的epoch<InstanceClass的epoch),此时接下来的19-59重偏向t2(重偏向后epoch同步了)。
  • t3执行时,此时0~18已经处于无锁状态,只能加轻量级锁。19~38号对象则有所不同,这20个对象执行时会逐个执行偏向锁撤销,到第38号对象时刚好又执行了20次,此时总的撤销次数到达40次,于是触发批量撤销。批量撤销会将类的偏向标记关闭,之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。由于之前执行过批量重偏向了,所以这里会升级为轻量级锁。
  • t4休眠前对象65为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象的锁状态由偏向锁变为轻量级锁,而不在执行同步的对象的锁状态不会改变(如对象66)。

【总结】:批量重偏向和批量撤销是针对类的优化,和对象无关。偏向锁重偏向一次之后不可再次重偏向。当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利。

以上步骤是常规步骤,如果t1和t2后加上「sleep 30s」代码,事情就不一样了。

虚拟机的偏向锁实现里有两个很关键的东西:

  • -XX:BiasedLockingDecayTime 距上次批量重偏向25秒内,撤销计数达到40,就会发生批量撤销。每隔(>=)25秒,会重置在[20, 40)内的计数,这意味着可以发生多次批量重偏向。
  • revocation_count 撤销计数器,会记录偏向锁撤销的次数

BiasedLockingDecayTime是开启一次新的批量重偏向距离上一次批量重偏向之后的延迟时间,默认为25000ms,这就是上面讲到的「规定的时间」。revocation_count是撤销计数器,会记录偏向锁撤销的次数。也就是说,在执行一次批量重偏向之后,经过了较长的一段时间(>=BiasedLockingDecayTime)之后,撤销计数器才超过阈值,则会重置撤销计数器。而是否执行批量重偏向和批量撤销正是依赖于撤销计数器的,sleep之后计数器被清零,本次不执行批量撤销,因此后续也就有机会继续执行批量重偏向。

根据以上知识可知,等待一段时间后撤销计数器会清零,因此不会再执行批量撤销,而是变成再次执行批量重偏向。此时T3加锁的过程就和上面有所不同了,0~18号对象已经变为无锁,因此这部分只能加轻量级锁。关键是19~38号对象,从19号对象开始又会执行偏向锁撤销,到38号对象时刚好20次,这就绕回常规情况下T2执行时的场景了,T2执行时19号对象是不是从偏向T1变成了偏向T2?所以这里从38号对象开始往后的其他对象都会从T2重新偏向T3。

这里的特性用虚拟机里面的话讲叫做「启发式更新」,我理解这样做主要是出于性能上的考虑。假如偏向锁只是偶尔会发生轮流加锁的这种竞争,虚拟机是允许的,20次以内随便你怎么玩,可以一直帮你执行偏向锁撤销。如果25秒内撤销次数超过20次了,还友情提供一次批量重偏向。但是假如线程间竞争很多,频繁执行偏向锁撤销和批量重偏向则可能会比较损耗性能,因此「规定的时间」内连续撤销超过一定次数(默认40次)虚拟机就不让你偏向了,这就是批量撤销的意义所在。

3.1.7 重偏向与撤销原理

以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作(即膨胀,锁升级)时,该计数器 +1 ,当这个值达到重偏向阈值(默认20)时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。

引入一个概念——epoch(偏向时间戳),除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。对象中的epoch初始值为创建该对象时class 中的epoch的值。每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的),比如每次发生批量重偏向时(撤销计数器达到重偏向阈值),就将 class 中的epoch + 1,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于偏向锁加锁状态的锁对象,将其epoch字段改为新值。退出安全点后,当有线程需要尝试获取偏向锁时,如果发现当前对象的epoch值和class的epoch不相等,则说明该对象的偏向锁已经无效了,此时竞争线程可以尝试对此锁对象重新进行偏向操作,即直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

锁对象的 epoch 自增针对的是被当前存活的 thread 持有的偏向锁锁对象。

可以理解为是第几代偏向锁。 偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。 而当一类对象撤销的次数过多,比如有个Test 类的对象作为偏向锁,经常被撤销,次数超过重偏向阈值 (XX:BiasedLockingBulkRebiasThreshold设置,默认为 20 )就会把当代的偏向锁废弃,把class的 epoch +1。所以当class类对象和锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。

不过为保证正在执行的持有锁的线程不能因为这个而丢失了锁,偏向锁撤销需要所有线程处于安全点,然后遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其epoch字段改为新值。

当撤销次数超过撤销阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为40),则废弃此类的偏向功能,也就是说这个类都无法偏向了。

3.1.8 偏向锁解锁过程

当偏向锁被一个线程获取到时,会往所有者线程的栈中添加一条 Displaced Mark Word 为空的Lock Record,每重入一次就加一个Lock Record

25b7d7b837274561b89feaa293e03675.png

当有其他线程尝试获得锁时,根据遍历偏向线程的 Lock Record 来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条Lock Record 的 owner 字段(指向锁对象的指针)设置为NULL(删除该条记录) 如下图。需要注意的是,偏向锁的解锁步骤中并不会修改锁对象头中的thread id。

45dcce473f04408295f52f81bdb18017.png

3.2 轻量级锁

3.2.1 偏向锁升级为轻量级锁的情况

  • 当禁用偏向锁时,新创建的对象为普通状态(001),即使该对象被synchronized修饰,也不会变为偏向锁状态(biased_lock被设置为0),直接升级为轻量级锁,并且对象Mark Word的锁标志位变为轻量级锁(000)。
    • 使用-XX:+UseBiasedLocking 禁用偏向锁。
    • 当一个偏向锁如果撤销次数到达阈值40,会禁用该类的所有新实例对象使用偏向锁。
  • 在发现2个不同线程在竞争偏向锁时由偏向锁升级为轻量级锁。

轻量级锁也就是自旋锁,利用CAS尝试获取锁。如果你确定某个方法同一时间确实会有一堆线程访问,而且工作时间还挺长,那么建议直接用重量级锁,不要使用 synchronized,因为在CAS过程中,CPU是不会进行线程切换的,这就导致CAS失败的情况下他会浪费CPU的分片时间,都用来干这个事了。

3.2.2 偏向锁升级到轻量级锁的具体流程

偏向锁时,如果线程2需要进入同步方法,线程1还持有这个对象,那么就会进入偏向锁->轻量级锁的升级过程。但是因为锁升级是不可逆的,所以不会直接把偏向锁升级为轻量级锁,此时虚拟机会做【挽回策略】避免不必要的升级:当到达全局安全点(safepoint)时(在这个时间点上没有字节码正在执行),去查看偏向的线程是否还存活,根据存活情况执行对应逻辑,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

  • 如果偏向的线程存活且还在同步块中,则偏向锁->锁撤销->轻量级锁,升级具体过程如下:
    • 需要完成偏向锁的撤销(101 -> 001)并升级为轻量级锁(001 -> 000);
    • 并将锁对象的 MarkWord 设置到 偏向线程栈帧的 LockRecord 对象中的 MarkWord 中(原偏向锁已创建为MarkWord空的LockRecord);
    • 再通过 CAS 操作将对象头中存储的 Lock Record 指针指向 LockRecord 的地址,原偏向的线程继续拥有锁。
  • 如果偏向的线程已经不存活或者不在同步块中:
    • 则检查对象是否可以重偏向(对象epoch是否等于InstanceClass的epoch):
      • 在允许重偏向(epoch不相等)的情况下,原所有者线程会触发解锁,将对象恢复成匿名偏向(状态码=101,线程ID=0)的状态,然后重新偏向至当前线程;偏向锁->无锁->偏向锁
      • 如果不允许重偏向(epoch相等),则会触发偏向锁撤销,将对象设置为无锁状态(001),竞争者线程按轻量级锁的逻辑去获取锁。偏向锁->锁撤销->轻量级锁

3.2.3 轻量级锁加锁(加锁和解锁都是用CAS来交换所对象的Lock Record指针)

线程在执行同步块之前,JVM会先在需要加锁线程的栈帧中创建一个 Lock Record ,其包括一个用于存储锁对象头中的 Mark Word 以及一个指向锁对象的指针。然后使用 CAS 操作尝试把 锁对象头 Mark Word 中的 LockRecord 指针 更新为 指向加锁线程创建的 Lock Record 的指针:

  • 如果这个更新动作成功了,即代表表该线程拥有了这个对象的锁,直接执行同步代码块。(当前锁对象的 MarkWord 的结构会有所变化,不再是存着 hashcode 等信息,将会出现一个指向 LockRecord 的指针,指向锁记录。)
  • 如果更新动作失败就说明至少存在一条线程与当前线程竞争获取该对象的锁。
    • 情况一:其它线程已经持有了该对象的锁,表明此时发生了锁的竞争,当前线程会进行进行自旋等待,尝试通过CAS操作获取锁。如果自旋次数超过了一定的阈值(默认为10)...【轻量级锁升级为重量级锁的情形】,轻量级锁会膨胀为重量级锁。
    • 情况二:自己执行了synchronized的锁重入,那么再往线程栈帧中添加一条 Lock Record 作为重入的记数,且在此时新加的 Lock Record 中,对象的MarkWord为null(相当于被前一个抢了),如下图:7408514948d643b8aba304955b0fee72.png

【轻量级锁升级为重量级锁的情形】

  • 自旋失败:当线程尝试获取轻量级锁时,如果锁已经被其他线程持有,当前线程会进行一定次数的自旋(即忙等)尝试获取锁。如果自旋次数超过预设的阈值(JVM参数-XX:PreBlockSpin可以设置自旋次数),仍然无法获取锁,那么轻量级锁会升级为重量级锁。
  • 有其他线程在等待:如果在一个线程持有轻量级锁的情况下,有另一个线程尝试获取同一个锁并进入了等待状态(比如调用了Object.wait()),那么轻量级锁会升级为重量级锁,以便能够处理线程之间的等待和唤醒操作。
  • 同步块过长:如果同步块执行的时间过长,轻量级锁的自旋可能会导致过多的CPU资源浪费。在这种情况下,轻量级锁可能会升级为重量级锁,以减少不必要的自旋。
  • JVM动态决策:JVM可能会根据当前的系统状态和锁竞争的激烈程度动态地决定是否将轻量级锁升级为重量级锁。例如,如果JVM检测到系统中有大量的线程竞争同一个锁,它可能会选择升级为重量级锁以减少竞争带来的性能损失。

3.2.4 轻量级锁解锁(加锁和解锁都是用CAS来交换Lock Record)

释放时会检查锁对象 Mark Word 中的 Lock Record 指针是否指向自己(自己是否持有锁),然后使用 CAS 将对象头中的 Mark Word 替换成线程栈帧中 Lock Record 内的 Mark Word 。如果成功,则表示没有竞争发生,如果替换失败则升级为重量级锁。CAS 过程如下:

  • 遍历当前线程栈,找到所有 Lock Object 指针地址等于当前锁对象的 Lock Record 。
  • 如果 Lock Record 的 Mark Word 为 null,代表这是一次锁重入,将 Lock Object 指针地址设置为 null 后continue。
  • 如果 Lock Record 的 Mark Word 不为 null,则利用 CAS 指令将锁对象头的 Mark word 恢复成为 Lock Record 中的 Mark Word。如果恢复成功,则continue;否则膨胀为重量级锁(失败是因为锁已经膨胀,Mark Word 已被替换成其他标志)。

3.3 重量级锁

重量级锁其实是一种称呼,synchronized 就是一种重量级锁(ReentrantLock 也是重量级锁,它会先尝试CAS获取锁,获取不到则转重量级锁),它是通过内部一个叫做 Monitor Lock(监视器锁)来实现,而监视器锁本质上是依赖于系统的 Mutex Lock(互斥锁)来实现,当加锁的时候需要用用户态切换为核心态,这样的时间成本和性能开销非常高,因此这种依赖于操作系统 Mutex Lock 的锁称为重量级锁。在JDK1.6后,JVM为了提高锁的获取与释放效率对 synchronized 进行了优化,引入了偏向锁和轻量级锁,所以锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),会随着竞争的激烈而逐渐升级。

ec7c030562d7448ea31b5522f812eee7.png

Monitor 的本质是 Object Monitor(翻译叫监视器或者管程),其也有自己的队列,最终阻塞调用的还是 t -> park() 函数,但是 park() 依赖于底层操作系统的 Mutex Lock、condition 信号量、counter 计数器(和 LockSupport 的 park / unpark 相同),由于使用 Mutex Lock 和 cond_wait 都需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

每一个 JAVA 对象都会与一个监视器 Monitor 关联,Monitor 和对象一起创建、销毁。重量级锁的状态下,对象的 Mark Word 为指向一个堆中 Monitor 对象的指针。当一个 Monitor 被重量锁对象持有后,该对象将处于锁定状态。下图为重量级锁对象的 MarkWord :

164d510cd300404ba0b7fe49d56e533d.png

Monitor 监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块

  • 同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
  • 协作。监视器提供 Signal 机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送 Signal(信号) 去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。

【轻量级锁升级为重量级锁的情况】

  • 轻量级锁 CAS 替换失败到达一定次数(默认为10)后,轻量级锁升级为重量级锁(通过CAS修改锁标志位,但不修改持有ID)。当后序的线程尝试获取锁时,就将自己挂起,等待被唤醒。
  • 需要注意,如果线程2自旋期间,有线程3也需要访问同步方法,则立刻由轻量级锁膨胀为重量级锁。
  • 有线程调用偏向锁对象的 hashcode 方法,因为这个时候 MarkWord 是没有办法存储hash值的,所以需要膨胀到重量级锁。
  • 持锁的线程调用锁对象的 wait() 方法,因为锁对象处于偏向或者轻量级锁的状态下,是没有管程对象和等待队列的,无法保存线程节点。

3.3.1 Monitor

Monitor 译为「监视器」「管程」,是重量级锁实现原理,此类对象由操作系统提供,Java 对象与 Monitor 对象是一一对应的。如果使用 synchronized 给一个 Java 对象上了锁(重量级锁),该Java对象头中的 MarkWord 对应的指针就会指向一个唯一的 Monitor 对象

Monitor 中有三个重要变量实现加锁与等待锁操作,分别是 OwnerEntryList WaitSet

  • Owner :当某个线程拥有锁,那么 Owner 就会指向该线程,表示 只允许该线程执行代码块。
  • EntryList :链表结构,当线程没能抢到锁,那么该线程就会被加入到 EntryList 中。待拥有锁的线程执行完毕,就会根据JVM底层的算法机制,唤醒其中的一个线程并使之成为新的 Owner。
  • WaitSet :调用 wait() 方法的线程, 也就是说处于 wait 状态的线程 会封装成 ObjectWaiter 存在这个链表。

在HotSpot虚拟机中,Monitor 是由 ObjectMonitor 实现的,其源码是用c++来实现的,位于HotSpot虚拟机源码 ObjectMonitor.hpp 文件中(src/share/vm/runtime/objectMonitor.hpp)。主要数据结构如下:

ObjectMonitor() {
    _header       = NULL; // 对象头的MarkWord
    _count        = 0; // 线程获取该锁的次数
    _waiters      = 0; // 调用wait方法后等待的线程数
    _recursions   = 0; // 重入计数器(加锁线程的重入次数)
    _object       = NULL; // 关联的对象(和 _header 的对象相同,比如synchronized括号里的对象)
    _owner        = NULL; // 占用当前锁的线程
    _WaitSet      = NULL; // 调用wait方法后等待的ObjectWaiter链表
    _WaitSetLock  = 0 ;   // 操作WaitSet链表的锁
    _Responsible  = NULL ;
    _succ         = NULL ; // 当前线程释放锁后,下一个执行的线程
    _cxq          = NULL ; // 多线程竞争锁时的单向链表(cxq链表头节点)
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程节点会被加入该链表(EntryList链表头节点)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
 }

线程在任何时间最多出现在一个列表上:要么是 cxq ,要么在 EntryList ,要么在 waitSet 。

ObjectMonitor 其主要成员包括:

  • ContentionList/Cxq :同步阻塞(操作系统_阻塞),所有请求锁的线程首先被放在这个竞争队列中。
    • Cxq是一由Node及其next指针逻辑构成的单向链表,并不存在一个队列的数据结构。每次新加入Node会在 Cxq 的队头进行,通过CAS改变第一个节点的指针为新增节点,同时设置新增节点的next指向后续节点;从 Cxq 取得元素时,会从队尾获取。显然,Cxq结构是一个无锁结构。
    • 因为只有 Owner 线程才能从队尾取元素,即线程出列操作无竞争,当然也就避免了 CAS 的 ABA问题。
  • EntryList :同步阻塞(操作系统_阻塞),Cxq 中那些有资格成为候选竞争线程被移动到 EntryList 中。这个列表也是存放竞争锁失败的线程(即因等待锁而被阻塞的线程)。
    • Cxq 会被线程并发访问,为了降低对 Cxq 队尾的竞争而建立 EntryList。在 Owner 线程释放锁时,JVM会从 Cxq 中迁移线程到 EntryList ,并会指定 EntryList 中的某个线程(一般为Head)为 OnDeck 线程(Ready Thread)。EntryList 中的线程作为候选竞争线程而存在。
    • OnDeck(Ready Thread) :任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck。Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为「“竞争切换”」。OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中,考虑到公平性,OnDeck Thread在EntryList中的位置不发生变化(依然在队头)。在OnDeck Thread成为Owner的过程中,还有一个不公平的事情,就是后来的新抢锁线程可能直接通过CAS自旋成为Owner而抢到锁。
  • WaitSet :等待阻塞(操作系统_阻塞),已获取锁的线程(Owner) 调用 Object#wait() 方法后,则线程会加入到 WaitSet 中。直到某个时刻通过 Object.notify() 或者 Object.notifyAll() 唤醒线程后根据「唤醒策略」判断是放入 cxq 还是 EntryList ,默认放入 EntryList。
    • Policy == 0 :放入到 Entrylist 队列的排头位置
    • Policy == 1 :放入到 Entrylist 队列的末尾位置
    • Policy == 2 (默认策略):判断 Entrylist 是否为空,为空就放入Entrylist中,否则放入 cxq 队列排头位置
    • Policy==3 :判断 cxq 是否为空,如果为空,直接放入 cxq 头部,否则放入 cxq 队列末尾位置
  • _Owner :用来指向持有 monitor 的线程,它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识,等到线程释放锁后_ower又会被重置为NULL。
  • count : 用于记录线程获取锁的次数,成功获取到锁后count会加1,释放锁时count减1。
  • _recursions : 记录重入次数。

14df37a256cf44f2825fa818e41e1e8f.png

5ecf0c7f2c7a4e678a987e39e6736da5.png

在 Object 的监视器 (Monitor) 模型上,一个对象拥有一个 EntryList 和一个 WaitSet ,线程调用 Object#wait() 方法后,则线程会加入到 WaitSet 中。

对于JUC并发包里面使用 AQS 并继承 Lock 接口实现的锁(如 ReentrantLock)或者是同步组件,则拥有一个 EntryList 和多个 WaitSet 。一个 EntryList 指的是AQS中的同步队列,多个 WaitSet 指的是多个 ConditionObject 对象实例上的等待队列(Condition#await()方法)。

3.3.2 轻量级锁膨胀为重量级锁——ObjectSynchronizer::inflate 函数

1. JVM源码下载 http://openjdk.java.net/ Mercurial --> jdk8 --> hotspot --> zip

2. IDE(Clion)下载 https://www.jetbrains.com/ 

回顾之前的分析:

  • 偏向锁升级为轻量级锁时要修改Mark Word,使之指向 Lock Record;
  • 轻量级锁升级为重量级锁时也需要修改 Mark Word,使之指向 ObjectMonitor。

创建/获取 ObjectMonitor 对象的过程即是锁的膨胀过程

ObjectSynchronizer::inflate 函数描述了轻量级锁膨胀为重量级锁的过程(创建/获取 ObjectMonitor 对象的过程),源码如下:

#synchronizer.cpp
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  ...
  //死循环,直到获取到ObjectMonitor为止
  for (;;) {
      //取出Mark Word
      const markOop mark = object->mark() ;
      //如果是重量级锁
      if (mark->has_monitor()) {
          //是重量级锁,说明肯定已经有现成的ObjectMonitor,直接用就好了
          ObjectMonitor * inf = mark->monitor() ;
          return inf ;
      }

      //正在膨胀的时候
      if (mark == markOopDesc::INFLATING()) {
        //继续循环,需要等待膨胀完成
         continue ;
      }

      //如果当前是轻量级锁
      if (mark->has_locker()) {
          //分配ObjectMonitor对象
          ObjectMonitor * m = omAlloc (Self) ;
          //初始化一些参数
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class

          //尝试将Mark Word更改为膨胀状态,此时Mark Word 全是0 --------->(1)
          //可能会有多线程走到这,因此用CAS
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
          if (cmp != mark) {
             //修改失败,继续循环
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }

          //若是修改成功,则取出之前轻量级锁存储的Mark Word
          markOop dmw = mark->displaced_mark_helper() ;
          //将Mark Word 搬到ObjectMonitor的_header字段里
          m->set_header(dmw) ;

          //_owner指向Lock Record,也就是设置锁的持有者是Lock Record------->(2)
          m->set_owner(mark->locker());
          //指向对象头
          m->set_object(object);
          //将Mark Word 指向ObjectMonitor------->(3)
          object->release_set_mark(markOopDesc::encode(m));
          ...
          //成功,则返回ObjectMonitor 对象
          return m ;
      }

      //无锁状态
      ObjectMonitor * m = omAlloc (Self) ;
      //初始化一些参数
      m->Recycle();
      //直接记录mark
      m->set_header(mark);
      //_owner为空-------------------->(4)
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class

      //将Mark Word修改为指向ObjectMonitor的指针-------------------->(5)
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          ...
          //失败,则重新尝试
          continue ;
      }
      ...
      //成功,则返回ObjectMonitor 对象
      return m ;
  }
}

上述代码即为简化过的膨胀流程,标注了5个重点

  1. 如果当前锁是轻量级锁,说明有线程正在持有该锁,尝试CAS修改锁为膨胀状态。
  2. _owner不指向任何线程,指向的是Lock Reocrd,后续会有相应的判断。
  3. 轻量级锁时 MarkWord 存储着指向 Lock Record 的指针,而此时变为指向重量级锁的指针,也就是指向 ObjectMonitor 的指针。此处是单线程操作,因此可以直接设置。
  4. 如果当前锁是无锁状态,将_owner置空。
  5. CAS尝试将 Mark Word 指向ObjectMonitor。

69d11b815a4f445fbc124cc612443585.png

3.3.3 重量级锁加锁过程——ObjectMonitor::enter 函数

在调用 ObjectSynchronizer::inflate 函数完成膨胀,获取 ObjectMonitor 对象后,会调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp)函数完成重量级锁的加锁,源码如下:

3.3.3.1 ObjectMonitor::enter——初次尝试加锁

初次获取锁在 ObjectMonitor::enter 函数的实现:

#ObjectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
  //当前线程
  Thread * const Self = THREAD ;
  void * cur ;

  //尝试CAS修改_owner字段为当前线程,也就是尝试获取锁
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
    //修改成功,则获取了重量级锁
     return ;
  }

  //以下都是CAS失败后的处理
  //如果当前_owner值为当前线程,则认为是重入了该锁
  if (cur == Self) {
    //重入次数+1,成功获取了锁
     _recursions ++ ;
     return ;
  }

  //_owner值为Lock Record,说明当前线程是之前轻量级锁的持有者
  if (Self->is_lock_owned ((address)cur)) {
    //重入次数为1次
    _recursions = 1 ;
    //改为当前线程
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
  ...

  { 
    ...
    for (;;) {
      //没有获取到锁,则执行该函数
      EnterI (THREAD) ;
      ...
      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;
      jt->java_suspend_self();
    }
  }
}

由上可知,enter(xx) 函数主要做了如下事情:

  • 先CAS尝试修改 ObjectMonitor 的 _owner 字段,会有几种结果:
    • 1、锁没被其它线程占用,当前线程成功获取锁。
    • 2、锁被当前线程占用,当前线程重入该锁,获取锁成功,重入次数_recursions++。
    • 3、锁被 LockRecord 占用(轻量级锁),而LockRecord又属于当前线程,属于重入,重入次数_recursions为1。
    • 4、以上条件都不满足,调用 EnterI() 函数。

 8d198e993de74c5e888c9ff3a1e9c57c.png

3.3.3.2 ObjectMonitor::EnterI——再次尝试加锁

初次获取锁失败后,会走到下面的流程,也就是 ObjectMonitor::EnterI 函数的实现(位于:src/share/vm/runtime/ObjectMonitor.cpp):

#ObjectMonitor.cpp
void ATTR ObjectMonitor::EnterI (TRAPS) {
    //当前线程
    Thread * Self = THREAD ;
    //尝试加锁----------->(1)
    if (TryLock (Self) > 0) {
        return ;
    }
    //尝试自旋加锁----------->(2)
    if (TrySpin (Self) > 0) {
        return ;
    }
    //构造ObjectWaiter 节点
    ObjectWaiter node(Self) ;
    //挂起/唤醒线程重置参数
    Self->_ParkEvent->reset() ;
    //前驱节点为无效节点
    node._prev   = (ObjectWaiter *) 0xBAD ;
    //当前节点状态为CXQ,也就是说节点在_cxq队列里
    node.TState  = ObjectWaiter::TS_CXQ ;

    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        //将节点插入_cxq队列的头----------->(3)
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

        //尝试获取锁----------->(4)
        if (TryLock (Self) > 0) {
            return ;
        }
    }
    ...
    for (;;) {
        //再次尝试获取锁----------->(5)
        if (TryLock (Self) > 0) break ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }
        //挂起线程----------->(6)
        if (_Responsible == Self || (SyncFlags & 1)) {
            //挂起有超时时间
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
        } else {
            //挂起没有超时时间
            Self->_ParkEvent->park() ;
        }
        //唤醒后再次获取锁,成功则退出循环----------->(7)
        if (TryLock(Self) > 0) break ;
        //...还是一些自旋策略
    }
    //将节点从_cxq或_EntryList里移除----------->(8)
    UnlinkAfterAcquire (Self, &node) ;
    ...
    return ;
}

c87f1d1d1ab54c6d85f0b29953b80dec.png

ObjectMonitor::EnterI 函数主要做了如下事情:

  1. TryLock (Self) 尝试加锁(通过CAS将锁对象的 ObjectMonitor 内的 _owner 字段设置为当前线程);
  2. TrySpin (Self) 尝试自旋加锁(TrySpin里多次调用TryLock,次数是10次;源码里指出经验值20-100可能最佳);
  3. 如果仍然没有获取到锁,进入死循环,直到插入cxq队列成功或者获取了锁:
    1. 将该线程封装成一个 ObjectWaiter 对象 CAS 插入到 cxq(单向链表)队列的队首,修改 _cxq 指向当前节点,成功则break跳出循环。
    2. 插入队列失败,再次调用 TryLock (Self) 尝试获取锁,成功则直接return结束。
  4. 进入下一个死循环,先调用 TryLock (Self) 尝试获取锁;
  5. 若仍旧获取锁失败,则线程放弃获取锁的动作,调用 park() 将自己挂起,线程阻塞于此处,等待别的线程唤醒它。
  6. 当某个线程唤醒在(5)被挂起的线程后,被唤醒的线程立即调用 TryLock (Self) 再次尝试获取锁,如果还是失败了,则继续回到(4)的死循环。
  7. 获取锁成功后,因为前边已经加入到队列了,因此需要将节点从队列(cxq/EntryList)移除。

 977bc81d3123439eaf44989868e20d27.png

3.3.4 重量级锁释放过程

上面分析了加锁的过程,它有两种结果:

  1. 成功获取锁,那么可以执行临界区代码。
  2. 获取锁失败,挂起等待别人唤醒。

关于2思考一个问题:是谁唤醒了它,如何唤醒的?由下源码可知:当前占有锁的线程释放锁后会唤醒阻塞等待锁的线程

先来看看1,线程执行完临界区代码后需要释放锁,偏向锁和轻量级锁的释放上文已经分析:若是释放失败,则会走到重量级锁的释放流程。重量级锁的释放流程,也就是 ObjectMonitor::exit 函数的实现(位于:src/share/vm/runtime/ObjectMonitor.cpp):

#ObjectMonitor.cpp
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
   //释放锁的线程不一定是重量级锁的获得者-------->(1)
   if (THREAD != _owner) {
     if (THREAD->is_lock_owned((address) _owner)) {
       //释放锁的线程是轻量级锁的获得者,先占用锁
       _owner = THREAD ;
     } else {
       //异常情况
       return;
     }
   }

   if (_recursions != 0) {
      //是重入锁,简单标记后退出
     _recursions--;
     return ;
   }
   ...

   for (;;) {
      if (Knob_ExitPolicy == 0) {
         //默认走这里
         //释放锁,别的线程可以抢占了
         OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
         OrderAccess::storeload() ;                         // See if we need to wake a successor
         //如果没有线程在_cxq/_EntryList等待,则直接退出
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            TEVENT (Inflated exit - simple egress) ;
            return ;
         }
         //有线程在等待,再把之前释放的锁拿回来
         if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
            //若是失败,说明被人抢占了,直接退出
            return ;
         }
      } else {
         ...
      }

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;
      //此处省略代码
      //根据QMode不同,选不同的策略,主要是操作_cxq和_EntryList的方式不同
      //默认QMode=0

      w = _EntryList  ;
      if (w != NULL) {
         //_EntryList不为空,则释放锁---------(2)
          ExitEpilog (Self, w) ;
          return ;
      }

      //_EntryList 为空,则看_cxq有没有数据
      w = _cxq ;
      if (w == NULL) continue ;//没有继续循环

      for (;;) {
          //将_cxq头节点置空
          ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
          if (u == w) break ;
          w = u ;
      }
      if (QMode == 1) {
         ...
      } else {
         // QMode == 0 or QMode == 2
         //_EntryList指向_cxq
         _EntryList = w ;
         ObjectWaiter * q = NULL ;
         ObjectWaiter * p ;
         //该循环的目的是为了将_EntryList里的节点前驱连接起来---------(3)
         for (p = w ; p != NULL ; p = p->_next) {
             guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
             //改为ENTER状态
             p->TState = ObjectWaiter::TS_ENTER ;
             p->_prev = q ;
             q = p ;
         }
      }

      w = _EntryList  ;
      if (w != NULL) {
          //释放锁---------(4)
          ExitEpilog (Self, w) ;
          return ;
      }
   }
}

当线程释放锁时,会从 cxq 或 EntryList 中挑选一个线程唤醒(根据「唤醒策略」,默认从 EntryList 的 Head 取),被选中的线程叫做 Heir presumptive (即假定继承人),就是上图中的 OnDeck(Ready Thread),假定继承人被唤醒后会尝试获得锁,但 synchronized 是非公平的,所以假定继承人不一定能获得锁(这也是它叫"假定"继承人的原因)。

  • 如果 _owner 不是当前线程(判断当前是不是持有锁的线程):
    • 若当前线程是之前持有轻量级锁的线程(由轻量级锁膨胀后还没调用过 enter 方法,此时_owner会是指向 Lock Record 的指针),则_owner改为指向当前线程,然后继续执行后面代码。
    • 否则异常情况,即当前不是持有锁的线程,抛出异常。
  • 如果_recursions (重入计数器) -1 后还不为0(还是重入),则返回继续执行程序代码。
  • 如果_recursions (重入计数器) -1 后为0,则释放锁(即设置owner为null),这个时刻其他的线程能获取到锁。
  • 如果当前没有线程在_cxq/_EntryList等待,则直接退出,因为不需要唤醒其他线程;或者如果 succ不为null,代表当前已经有个"醒着的"继承人线程OnDeck,那当前线程不需要唤醒任何线程,直接退出。
  • 根据 QMode 的不同,会执行不同的「唤醒策略」(QMode默认为0)
    • QMode = 2且 cxq 非空:取 cxq 队列队首的 ObjectWaiter 对象,调用 ExitEpilog() 方法,然后立即返回。【ExitEpilog (Self, w) :释放锁,将_owner置空;然后唤醒节点里ObjectWaiter对象封装的线程】
    • QMode = 3且 cxq 非空:把 cxq 队列插入到 EntryList 的 尾部;继续往下执行。
    • QMode = 4且 cxq 非空:把 cxq 队列插入到 EntryList 的头部;继续往下执行。
    • QMode = 0(默认):暂时什么都不做,继续往下(QMode默认是0);只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行解锁流程:
      • 如果 EntryList 的首元素非空,就取出来调用 ExitEpilog() 方法,该方法会唤醒 ObjectWaiter 对象的线程(在 EnterI() 方法中调用 park() 挂起的线程),然后立即返回;
      • 如果 EntryList 的首元素为空,就将 cxq 的所有元素放入到 EntryList 中,然后再从 EntryList 中取出来队首元素执行 ExitEpilog 方法,然后立即返回;

Qmode=0(默认策略) 的判断逻辑就是先判断 Entrylist 是否为空:如果不为空,则取出第一个唤醒;如果为空再从 cxq 里面获取第一个唤醒

Object#notify 方法唤醒线程后,线程会从 waitSet 移动到 EntryList ,线程最开始获取失败锁时总是在 _cxq 中,后再往 EntryList 中移动,所以 notify() 唤醒的线程会比普通线程获取锁的线程前执行。

 9d7c12ea63644e03a21002ccb637af72.png

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
无状态偏向锁轻量级重量级都是Java中的机制,它们的实现方式和性能表现不同。 无状态:也称为自旋,当线程尝试获取时,如果已经被其他线程占用,该线程会一直自旋等待的释放,直到获取到为止。这种适用于的持有时间非常短的情况,因为长时间的自旋会浪费CPU资源。 偏向锁偏向锁是一种针对加操作的优化手段,它的目标是减少无竞争情况下的操作的性能消耗。当一个线程访问一个偏向锁时,它会将对象头中的标识位设置为偏向,并将线程ID记录在对象头中。之后,该线程再次请求时,无需再次竞争,直接获取即可。这种适用于只有一个线程访问对象的情况。 轻量级轻量级是一种针对多线程竞争情况下的优化手段,它的目标是减少线程阻塞的时间,提高程序的并发性能。当一个线程访问一个轻量级时,它会将对象头中的标识位设置为轻量级,并将对象的指针保存在线程的栈帧中。之后,其他线程再次请求时,会通过自旋的方式尝试获取,而不是阻塞等待。如果自旋失败,就会升级为重量级。这种适用于的竞争不是很激烈的情况。 重量级重量级是一种针对多线程竞争情况下的优化手段,它的目标是保证线程的正确性和程序的稳定性。当一个线程访问一个重量级时,它会进入阻塞状态,直到被释放。这种适用于的竞争非常激烈的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值