深入理解 synchronized 原理

news2024/11/13 22:41:25

目录

一. 前言

二. Java对象的内存布局

2.1. 对象头

2.2. Mark Word

2.3. Class Metadata Pointer

2.4. Length

三. 偏向锁

3.1. 偏向锁的工作流程

3.2. 偏向失效

3.2.1. 误区一

3.3. 偏向撤销

3.3.1. 误区一

3.4. 偏向撤销的底层实现

3.5. HashCode与偏向撤销

3.6. 批量撤销是什么

3.7. 批量重偏向是什么

3.8. epoch 的作用

四. 轻量级锁

4.1. 轻量级锁的工作流程

4.2. 轻量级锁是否会自旋?

五. 重量级锁

5.1. Java线程模型(HotSpot实现)

5.2. 上下文切换

5.3. 自旋锁

5.4. 自适应自旋锁

5.5. ObjectMonitor对象

5.5. 重量级锁的加锁过程(ObjectMonitor::enter)

5.6. 重量级锁的解锁过程(ObjectMonitor::exit)

5.7. 重量级锁的_waitSet源码

5.8. 锁消除

5.9. 锁粗化

六. 字节码层面解读synchronized

6.1. 同步方法

6.2. class 文件结构

6.3. 同步代码块

6.4. 异常表(exception_table)

6.5. 字节码指令 monitorenter与monitorexit


一. 前言

    synchronized 是JDK为解决同步问题设计的一种锁,synchronized 保证被其修饰的方法或者代码块在任何时候都只能有一个线程访问。但在JDK1.5之前它的效率十分低下,属于重量级的锁。在JDK1.5后对 synchronized 在JVM层面进行了优化,即在JVM层面就将锁的功能实现,而不是依赖操作系统去实现,这里就省去了操作系统内核态和用户态的频繁切换,引入了自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

    关于 synchronized 的深度理解的文章全网不多,并且很多网上广为流传的图和对 synchronized的理解都有很多的错误,所以写了这篇文章,深入解读一下 synchronized,并修正一些书本和网上对 synchronized 的错误理解,希望对各位小伙伴的学习有所帮助。

二. Java对象的内存布局

    在JVM中,对象在内存中的布局分为三块区域:对象头实例数据对齐补全。synchronized 用的锁是存在Java对象头里的。见下图(注:下图采用64位内存结构):

2.1. 对象头

HotSpot 有两种对象头:
数组类型:如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头;
非数组类型:如果对象是非数组类型,则用2字宽存储对象头。

对象头由三部分组成:
Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
Class Metadata Pointer:类型指针指向它的类元数据的指针。
Length:记录数组长度。如果对象头为数组类型,则有此项。

2.2. Mark Word

源码中的注释(源码位置:src\share\vm\oops\markOop.hpp):

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

注:上图 从左到右是 高位 --> 低位,下文我们会JOL进行代码测试,它从左到右是 低位 --> 高位进行打印的与上图刚好相反,要注意一下。

Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特。这部分是实现轻量级锁和偏向锁的关键。

看到这里我们可以带以下几个疑问继续往下学习:
1. 当锁膨胀为轻量级锁和重量级锁时,记录对象的hashcode和分代年龄的数据去哪了?
2. 偏向锁的 thread ID、epoch 和 hashcode 的位置是冲突的呀?
3. 如果 Mark Word 既要记录偏向线程的信息也要记录 hashcode 时怎么办?
4. 偏向锁的 epoch 又是干嘛的呢?

2.3. Class Metadata Pointer

    这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的位大小,即32位的JVM为32位,64位的JVM为64位。

    如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。所以会默认开启指针压缩(如不开启 class pointer 将占用8字节),我们可以通过下面的命令查看:java -XX:+PrintCommandLineFlags -version

上图中:-XX:+UseCompressedClassPointers开启了类型指针压缩;-XX:+UseCompressedOops开启了普通对象指针压缩。

补充: 什么叫普通对象指针压缩?比如对象A中有一个对象B的引用, 这个引用就是一个指针。

2.4. Length

    如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。

64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

三. 偏向锁

    偏向锁也是 JDK 6 中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步语句,进一步提高程序的运行性能。

    “锁”如其名,偏向锁是一个偏心的锁,它会偏向于第一个获得它得线程。如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

3.1. 偏向锁的工作流程

注:看到这张图你可能有些疑惑,似乎和你在其他地方或某些书本上看到的偏向锁的流程有些许不同,别急我们慢慢往下来看。 

3.2. 偏向失效

    偏向锁不一定一直有效,虚拟机开启偏向锁的启动参数为:XX:+UseBiasedLocking,JDK6之后HotSpot会默认开启偏向锁。但这个偏向锁的开启是存在延迟的,大概的延迟时间在4秒左右,当然也可以通过参数 -XX:BiasedLockingStartupDelay=0 将延迟改为0,但并不建议。

// 我们来看下面这段代码

// 为了方便在程序中看到java对象内存布局,我们可以在maven中添加jol-core依赖
// <dependency>
// 	<groupId>org.openjdk.jol</groupId>
//	<artifactId>jol-core</artifactId>
//	<version>0.9</version>
// </dependency>

public static void test01(){
	Object o = new Object();
	System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印 mark word
	synchronized (o){
		System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印 mark word
	}
}

正如我们所料,当我们执行这段代码的时候我们的偏向锁并未生效,而是直接生成了轻量级锁。 

我们使线程睡眠5秒再次测试看看:

public static void test02() throws InterruptedException {
	Thread.sleep(5000); // 或开启-XX:BiasedLockingStartupDelay=0
	Object o = new Object();
	System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印markword
	synchronized (o){
		System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印markword
	}
}

我们可以发现,在虚拟机成功开启偏向锁之后但未进入同步代码块之前偏向状态已经被设置为1了,但此时并未设置Thread ID,当进入同步代码块之后Thread ID才会被真正设置。 

那么问题来了?偏向状态到底是在什么时间被设置的呢?是被线程设置?还是在初始化时被设置的呢?我们用下面这段代码再次验证一下:

public static void test03() throws InterruptedException {
	Object o1 = new Object(); // 我们先初始化o1
	Thread.sleep(5000); // 等待偏向锁开启
    System.out.println("----------------- o1 ------------------");
	System.out.println(ClassLayout.parseInstance(o1).toPrintable()); // 此时打印 o1 的对象头我们看到偏向状态为0
	synchronized (o1){
		System.out.println(ClassLayout.parseInstance(o1).toPrintable()); // 果然此时为轻量级锁
	}
	new Thread(() -> {
		System.out.println(ClassLayout.parseInstance(o1).toPrintable()); // 我们用另一个线程再次打印下 o1发现他的偏向状态仍然为0,并没有被重新设置为1
        System.out.println("----------------- o2 ------------------");
		Object o2 = new Object(); // 重新初始化对象 o2
		System.out.println(ClassLayout.parseInstance(o2).toPrintable()); // 查看 o2 的对象头偏向状态为 1
		synchronized (o2){
			System.out.println(ClassLayout.parseInstance(o2).toPrintable()); // 我们发现此时为偏向锁
		}
	}).start();
}

由此我们可以得出结论,当虚拟机开启偏向锁成功之后,被初始化的对象会开启对象的可偏向状态(也有一种说法是第一个获取对象的线程将可偏向状态设置为1的,我没有验证过。如果有大佬验证过此处欢迎指出改正),当线程进入同步代码块时可根据该对象的可偏向状态决定启动偏向锁/轻量级锁。

3.2.1. 误区一

《深入理解java虚拟机》在描述偏向锁时是这么写的:

由上面代码可以看出这样的描述并不准确吧。我们明显看出可偏向标志是在初始化时被设置的,并不是和Thread ID同时被线程所设置。 

这句话应该也误导了一些程序员,比如江湖上广为流传的一张图(我截取了片段):

我们测试的代码可以明显看出这个逻辑是错误的

首先可偏向状态是由初始化对象时就被设置的,并不是由线程去设置,其次可偏向状态为0时会直接采用轻量级锁而不是再尝试去修改Thread ID(因为竞争线程无法知道偏向线程此时的状态)。

3.3. 偏向撤销

偏向撤销顾名思义,就是将对象中的可偏向状态从1设置为0。

撤销偏向在什么时间发生呢?由上文的图中我们可以看出,当线程通过CAS操作替换Thread ID失败时和检查Mark Word对象中的Thread ID并不是本线程时,会执行偏向撤销(当然并不仅这两种情况,后边我们会讲到其他情况导致的偏向撤销)。

讲到这里可能有些小伙伴会质疑了,因为上文中的描述也和《Java并发编程的艺术》书中描述的并不一样,我们来看看这本书是如何描述的。然后针对这样的分歧点进行测试。

3.3.1. 误区一

下文取自《Java并发编程的艺术》:

 下面我们来用一段代码测试下:

public static void test03() throws InterruptedException {
	Thread.sleep(5000); // 或开启-XX:BiasedLockingStartupDelay=0
	Object o = new Object();
	System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 偏向状态正常开启
	synchronized (o){
		// 偏向锁执行
		System.out.println(ClassLayout.parseInstance(o).toPrintable());
	}
	// 代码块已退出
	System.out.println("代码块已退出");
	// 再次打印对象 o 的 markword 可以看出对象依然是偏向状态 Thread ID被设置为主线程
	System.out.println(ClassLayout.parseInstance(o).toPrintable());
	// 开启子线程
	new Thread(()->{
		synchronized (o){
			System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 我们可以看到偏向被撤销
		}
	}).start();
	Thread.sleep(1000);
	// 我们再次查看对象 o 的mark word 偏向被撤销(无锁状态)
	System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

很明显书上又错了。并且错的很离谱。这里同样又误导了很大一群程序员,我再次拿出江湖上流传很广的一张图: 

其实仔细想想就能明白,偏向锁存储Thread ID的位置是在Mark Word中,Mark Word在对象头中,而第二个线程进来时,它除了能看到Mark Word中的Thread ID外,它如何判断持有偏向锁线程的状态呢?(这条线程在同步代码块中?还是已经退出了同步代码块?)既然无法知道它的状态,仅仅通过CAS去修改Thread ID即便是成功了,但这又凭什么等于是竞争锁成功了呢?这仅仅能证明在修改Thread ID的过程中只有他一个人,仅此而已。所以当一个线程检查到Thread ID并不是自己时,他会直接进行偏向撤销而不是再次尝试替换自己的Thread ID到Mark Word中。 

3.4. 偏向撤销的底层实现

    在并发的环境中,如果想撤销偏向状态,就必须知道被写入Mark Word中的偏向线程的状态以及线程的精确信息。所以此时竞争者会向JVM提交一个STW(stop the word 时间静止)的请求,在偏向线程到达 safepoint 时来获取它的精确状态。如果偏向线程此时还处于同步代码块中,JVM 会将Mark Word的信息转移到偏向线程的栈帧的lock record中(官方命名为displaced mark word)(这里如果听不明白没关系,下面的轻量级锁我们会详细讲解这个过程,这里理解它为将偏向锁膨胀为轻量级锁即可),如果偏向锁不在同步代码块中,则将偏向状态设置为 0 并改为无锁状态。这个过程是不会引起整体的STW的

3.5. HashCode与偏向撤销

    上文中我在讲Mark Word时留下一个小问题,细心的小伙伴应该也能注意到,Mark Word中存储HashCode与Thread ID、epoch的位置是冲突的。那当需要存储HashCode时,偏向锁的Thread ID怎么办呢?

    在Java中如果一个对象计算了HashCode,那就应该一直保持该值不变(当然你也可以重写Object的hashcode()方法),这个值是强制保持不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的HashCode永远不会发生改变。因此当对象被计算过HashCode之后,他的偏向状态将被撤销,并且再也无法进入偏向状态。

我们再次用一段代码来验证:

public static void test04() throws InterruptedException {
	Thread.sleep(5000);
	Object o = new Object();
	System.out.println(o.hashCode());
	System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印 mark word
	synchronized (o){
		System.out.println(ClassLayout.parseInstance(o).toPrintable()); //打印 mark word
	}
}

而当对象正在处于偏向状态锁状态时,又收到需要计算HashCode的请求时,他的偏向状态会被立即撤销,并且锁会直接跳过轻量级锁膨胀为重量级锁。

public static void test05(){
	Thread.sleep(5000);
	Object o = new Object();
	System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 打印 mark word
	synchronized (o){
		System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 第一次查看 mark word
		System.out.println(o.hashCode());
		System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 在调 hashCode 之后再次查看 mark word
	}
}

以上说的HashCode的计算都来自Object::hashCode()或者System::identityHashCode(Objcet)方法,如果重写了hashCode()方法,计算HashCode时不会产生以上的问题。 

3.6. 批量撤销是什么

    此时请思考一个问题?当只有一个线程反复进入同步代码块时,偏向锁确实为我们带来了诸多好处(性能开销低到可以忽略不计)。但当其他线程尝试获取锁时,就需要等到safepoint时才能将偏向锁撤销为无锁或升级到轻量级锁/重量级锁。这个过程是要消耗一定的成本的。试想一下,如果该场景本身就存在多线程的竞争,那偏向锁的存在并不能提升性能,反而会使性能下降。

    如果你还不明白我再举一个例子:比如一个100次的循环中线程A持有了类N的100个锁对象,此时他们全部偏向线程A,但紧接着此时线程B、线程C、线程D再次走进这个循环...... 如果没有批量撤销机制的话,类N的100个锁对象将会被连续撤销100次。

    一个类的对象在一个时间内被撤销了很多次,JVM会认为这个程序的设计是有问题的,JVM会直接将这个类所有的对象都撤销偏向,新实例化的对象也不可以偏向。

3.7. 批量重偏向是什么

    我们再思考第二个问题?对象虽然被多线程访问了,但并没有竞争。原偏向线程为T1,但后面均由T2线程访问,但因为原线程已经被T1偏向,所以T2线程在使用synchronized时不得不使用轻量级锁。当这样的撤销次数也打到一定阈值时,JVM会认为自己偏向错了,此时会将锁重新偏向回T2。(批量重偏向并不会将已经升级的轻量级锁或重量级锁对象降级,而是新的偏向对象不再会因为Thread ID不同而被撤销偏向状态)。

3.8. epoch 的作用

    首先引入一个概念 epoch,我们可以把它理解为一个时间戳,或者偏向锁的版本。epoch 会存储在可偏向对象的Mark Word中,以及类的class信息中也会保存一个epoch的值。

    在新的线程请求MarkWord时,会判断epoch位是否与该对象所属类的class的epoch匹配。如果不匹配,则表明偏向已过期,需要重新偏向。这种情况,偏向线程可以简单地使用原子CAS指令重新偏向于这个锁对象。

为了更深刻的理解,我们现在来解读一下源码:

// 批量重偏向/批量撤销的触发条件:

static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {
  markOop mark = o->mark();
  // 如果不是偏向模式直接返回
  if (!mark->has_bias_pattern()) {
    return HR_NOT_BIASED;
  }
 
  // 获取锁对象的类元数据
  Klass* k = o->klass();
  // 当前时间
  jlong cur_time = os::javaTimeMillis();
  // 该类上一次批量重偏向的时间
  jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
  // 该类单个偏向撤销的计数
  int revocation_count = k->biased_lock_revocation_count();

  // 按默认参数来说:
  // 如果撤销计数大于等于 20,且小于 40,
  // 且距上次批量撤销的时间大于等于 25 秒,就会重置计数。
  if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
      (revocation_count <  BiasedLockingBulkRevokeThreshold) &&
      (last_bulk_revocation_time != 0) &&
      (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
    // This is the first revocation we've seen in a while of an
    // object of this type since the last time we performed a bulk
    // rebiasing operation. The application is allocating objects in
    // bulk which are biased toward a thread and then handing them
    // off to another thread. We can cope with this allocation
    // pattern via the bulk rebiasing mechanism so we reset the
    // klass's revocation count rather than allow it to increase
    // monotonically. If we see the need to perform another bulk
    // rebias operation later, we will, and if subsequently we see
    // many more revocation operations in a short period of time we
    // will completely disable biasing for this type.
    k->set_biased_lock_revocation_count(0);
    revocation_count = 0;
  }

  if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
    // 自增计数
    revocation_count = k->atomic_incr_biased_lock_revocation_count();
  }
  // 此时,如果达到批量撤销阈值,则进行批量撤销。
  if (revocation_count == BiasedLockingBulkRevokeThreshold) {
    return HR_BULK_REVOKE;
  }
  // 此时,如果达到批量重偏向阈值,则进行批量重偏向。
  if (revocation_count == BiasedLockingBulkRebiasThreshold) {
    return HR_BULK_REBIAS;
  }
  // 否则,仅进行单个对象的撤销偏向
  return HR_SINGLE_REVOKE;
}

简单得出结论
对于一个类来说,如果距离上次批量重偏向25秒内,
单次偏向撤销次数(revocation_count )到达20次,就会进行批量重偏向。
单次偏向撤销次数(revocation_count)达到40次,则会进行批量撤销。

参数:
-XX:BiasedLockingDecayTime=25000
-XX:BiasedLockingBulkRebiasThreshold=20
-XX:BiasedLockingBulkRevokeThreshold=40

// 批量重偏向执行逻辑:

static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
                                                                   bool bulk_rebias,
                                                                   bool attempt_rebias_of_object,
                                                                   JavaThread* requesting_thread) {
  assert(SafepointSynchronize::is_at_safepoint(), "must be done at safepoint");

  if (TraceBiasedLocking) {
    tty->print_cr("* Beginning bulk revocation (kind == %s) because of object "
                  INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                  (bulk_rebias ? "rebias" : "revoke"),
                  (void *) o, (intptr_t) o->mark(), o->klass()->external_name());
  }

  jlong cur_time = os::javaTimeMillis();//当前时间
  //将这次重偏向时间写入类元数据,作为下次触发批量重偏向或批量撤销的启发条件之一
  o->klass()->set_last_biased_lock_bulk_revocation_time(cur_time);


  Klass* k_o = o->klass();
  Klass* klass = k_o;

  if (bulk_rebias) { //todo 批量重偏向
    // Use the epoch in the klass of the object to implicitly revoke
    // all biases of objects of this data type and force them to be
    // reacquired. However, we also need to walk the stacks of all
    // threads and update the headers of lightweight locked objects
    // with biases to have the current epoch.

    // If the prototype header doesn't have the bias pattern, don't
    // try to update the epoch -- assume another VM operation came in
    // and reset the header to the unbiased state, which will
    // implicitly cause all existing biases to be revoked
    //todo 类开启偏向模式 才能批量重偏向 如果不是偏向模式则不能进入一下代码块
    if (klass->prototype_header()->has_bias_pattern()) {
      int prev_epoch = klass->prototype_header()->bias_epoch();
      //todo 自增类的 epoch
      klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
      // 获取类自增后的 epoch
      int cur_epoch = klass->prototype_header()->bias_epoch();

      // Now walk all threads' stacks and adjust epochs of any biased
      // and locked objects of this data type we encounter
      // 遍历所有线程
      for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
        GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
        //遍历所有线程的锁记录
        for (int i = 0; i < cached_monitor_info->length(); i++) {
          MonitorInfo* mon_info = cached_monitor_info->at(i);
          oop owner = mon_info->owner();
          markOop mark = owner->mark();
          //找到所有当前类的偏向锁对象
          if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
            // We might have encountered this object already in the case of recursive locking
            assert(mark->bias_epoch() == prev_epoch || mark->bias_epoch() == cur_epoch, "error in bias epoch adjustment");
            //更新该类的偏向锁对象的 epoch 与 类的epoch 保持一致
            owner->set_mark(mark->set_bias_epoch(cur_epoch));
          }
        }
      }
    }

    // At this point we're done. All we have to do is potentially
    // adjust the header of the given object to revoke its bias.
    // 这一步调用撤销偏向的方法,在这里将对象设置为匿名偏向状态(101)
    revoke_bias(o, attempt_rebias_of_object && klass->prototype_header()->has_bias_pattern(), true, requesting_thread);
  }

  // .....................略
 
  BiasedLocking::Condition status_code = BiasedLocking::BIAS_REVOKED;
    //todo 如果满足条件,则直接将锁重偏向于当前线程
  if (attempt_rebias_of_object &&
      o->mark()->has_bias_pattern() &&
      klass->prototype_header()->has_bias_pattern()) {
    markOop new_mark = markOopDesc::encode(requesting_thread, o->mark()->age(),
                                           klass->prototype_header()->bias_epoch());
    //将新的 mark word set进去
    o->set_mark(new_mark);
    status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED;
    if (TraceBiasedLocking) {
      tty->print_cr("  Rebiased object toward thread " INTPTR_FORMAT, (intptr_t) requesting_thread);
    }
  }

  assert(!o->mark()->has_bias_pattern() ||
         (attempt_rebias_of_object && (o->mark()->biased_locker() == requesting_thread)),
         "bug in bulk bias revocation");

  return status_code;
}

得出结论:
1. 当满足偏向重偏向条件(revocation_count == BiasedLockingBulkRebiasThreshold)并且class开启了偏向模式时(klass->prototype_header()->has_bias_pattern())会进行批量重偏向,此时会自增 ”类“中的epoch,然后遍历所有线程,并在这些线程中找到所有持有该锁对象的线程(由注释可看出是指轻量级锁),并将这些“对象”中的epoch修改为与“类”中的epoch一致。
2. 然后调用 revoke_bias方法(撤销偏向)关键参数 allow_rebias(是否允许重偏向被设置为true)。在这个方法中会将对象重新设置成匿名偏向状态。最后如果满足条件,将对象重新偏向给新的线程(Rebiased object toward thread)。

我们再来展示revoke_bias(撤销偏向)的部分源码:

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
  // .....................略
  uint age = mark->age();
  //构建一个偏向模式的 mark word (101) 
  markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
  //构建一个无锁模式的 mark word (001)
  markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
  // .....................略
  // 如果线程已经不存活
  if (!thread_is_alive) {
	// 如果可重偏向则将对象设置为匿名偏向状态
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } 
	// 否则设置为无锁状态
	else {
      obj->set_mark(unbiased_prototype);
    }
    if (TraceBiasedLocking && (Verbose || !is_bulk)) {
      tty->print_cr("  Revoked bias of object biased toward dead thread");
    }
    return BiasedLocking::BIAS_REVOKED;
  }

  // .....................略
  if (highest_lock != NULL) {
    // Fix up highest lock to contain displaced header and point
    // object at it
    highest_lock->set_displaced_header(unbiased_prototype);
    // Reset object header to point to displaced mark
    obj->set_mark(markOopDesc::encode(highest_lock));
    assert(!obj->mark()->has_bias_pattern(), "illegal mark state: stack lock used bias bit");
    if (TraceBiasedLocking && (Verbose || !is_bulk)) {
      tty->print_cr("  Revoked bias of currently-locked object");
    }
  } 
  else {
    if (TraceBiasedLocking && (Verbose || !is_bulk)) {
      tty->print_cr("  Revoked bias of currently-unlocked object");
    }
	//偏向锁的所有者(线程)没有正在持有锁
    if (allow_rebias) {
	  //设置为匿名偏向状态
      obj->set_mark(biased_prototype);
    } else {
	  //否则设置为无锁状态
      // Store the unlocked value into the object's header.
      obj->set_mark(unbiased_prototype);
    }
  }

  return BiasedLocking::BIAS_REVOKED;
} 

注:这里的逻辑比较晦涩,如有误,欢迎指正。

// 批量撤销执行逻辑:

// 本来只要撤销 o 这一个对象的偏向锁
// 但是在这次单个对象的偏向撤销中,触发了批量重偏向或批量撤销
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
                                                                   bool bulk_rebias,
                                                                   bool attempt_rebias_of_object,
                                                                   JavaThread* requesting_thread) {
  ...
  jlong cur_time = os::javaTimeMillis();
  o->klass()->set_last_biased_lock_bulk_revocation_time(cur_time);


  Klass* k_o = o->klass();
  Klass* klass = k_o;

  if (bulk_rebias) {
    // 批量重偏向的逻辑
    ...
  } else {
    ...
    // 批量撤销的逻辑
    // 首先,禁用 类元数据 里的可偏向属性
    // markOopDesc::prototype() 返回的是一个关闭偏向模式的 prototype
    klass->set_prototype_header(markOopDesc::prototype());

    // 其次,遍历所有线程的栈,撤销该类正在被持有的偏向锁为轻量级锁
    for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
      GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
      for (int i = 0; i < cached_monitor_info->length(); i++) {
        MonitorInfo* mon_info = cached_monitor_info->at(i);
        oop owner = mon_info->owner();
        markOop mark = owner->mark();
        if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
          // 具体撤销,还是通过调用之前说的 revoke_bias() 方法做的
          // 我们可以看到第二个参数 allow_rebias 被设置为false了(代表不可重偏向)
          revoke_bias(owner, false, true, requesting_thread);
        }
      }
    }

    // 当前锁对象可能未被任何线程持有
    // 所以这里单独进行撤销,以确保完成调用方的撤销语义
    revoke_bias(o, false, true, requesting_thread);
  }

  ......

}

得出结论:批量撤销的逻辑相对简单的多
对于批量撤销时,正在被线程持有的偏向锁通过 safepoint 遍历所有的 Java 线程栈,将偏向锁升级为轻量级锁,并将未被线程正在持有的偏向锁,直接将锁对象的可偏向状态设置为 0 ,禁用偏向状态。

四. 轻量级锁

4.1. 轻量级锁的工作流程

    由上图我们可以看出,在进入同步代码块时,如果对象没有被锁定(锁标志位 01),虚拟机将会在当前线程的栈帧中开辟一个空间 Lock Record(锁记录),用来存储当前Mark Word的拷贝(我们在介绍Mark Word结构时,曾经留下一个问题?那就是轻量级锁中的HashCode等信息去哪了?没错,它现在被存在了栈帧中)被拷贝的Mark Word 官方称为 Displaced Mark Word。

    随后使用CAS操作尝试把对象的Mark Word更新指向Lock Record的指针,如果这个动作成功了,更新锁标志位为“00”并进入同步代码块,失败则查看Mark Word中的指针是不是已经指向了自己,如果不是,则代表存在竞争,此时膨胀为重量级锁。

4.2. 轻量级锁是否会自旋?

    有些小伙伴看到这里是不是有些疑问?因为《Java 并发编程的艺术》一书与我上文中的描述并不一样,我们来引用它的原文看看:

 

我再次拿出江湖上流传很广的一张图:

 有疑问怎么办?我们先来看看原始论文是怎么说的:

https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

翻译一下:
当一个对象通过 monitorenter 字节码被轻量锁定时,锁记录就会隐式或显式地在线程(正在执行锁获取操作的线程)的栈上分配。锁记录(Lock Record)保存了对象 mark word 的原始值,还包含了用于标识被锁对象所需的元数据(元数据就是描述数据的数据)。在获取锁的过程中,将 mark word 复制到锁记录中(这个 mark word 副本叫 displaced mark word),并执行 CAS 操作尝试使对象 mark word 指向锁记录。如果 CAS 成功了,当前线程就拥有了这个锁。如果失败了,因为其它线程获得了锁,则锁膨胀(膨胀过程中,OS 互斥锁和条件变量会与该对象关联)。在锁膨胀的过程中,对象 mark word 用 CAS 更新,以指向包含互斥锁和条件变量指针的数据结构。 

铁证如山了吧。还没完我们继续再看看源码:

写到这里真的有些感慨。并不是不允许工具书有错,但有这么多错(并且很多错误是非常低级的,稍加验证就会发现是错的),因为这些错误误导了这么多的人真的有些说不过去,作为一本工具书还是应该更严谨的好。 

五. 重量级锁

    开讲重量级锁之前,请各位小伙伴思考这样几个问题?重量级锁为什么称之为重量级锁呢?为什么它比轻量级锁或者偏向锁来的更“重”呢?到底“重”在哪里?

5.1. Java线程模型(HotSpot实现)

    HotSpot 目前采用的是内核线程实现,也就是我们常说的 1:1 实现(常见的其他实现方式还有 1:N 和 M:N)。在这种模式下所有的用户态(User Mode)线程都是基于内核态(Kernel Mode)线程实现的。比如各种线程的创建、析构、同步,都需要通过OS(操作系统)在内核态环境调用。而这种调用的代价是比较高的,需要用户态和内核态中来回切换。(细心的同学会发现我们的Thread类,他的关键方法基本都被 native 修饰)。

    HotSpot 中的每一个Java线程都是直接映射到操作系统(OS)的内核线程中来实现的,而且中间没有额外的间接结构,所以HotSpot不会干涉线程的调度(当然是可以设置线程优先级给操作系统提供调度建议的)。所以冻结或者唤醒线程、该给线程分配多少时间片、线程应该去哪个处理器核心去执行等等,都是由操作系统完成的。

5.2. 上下文切换

    如果可运行数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU,这个过程将导致一次上下文切换。(保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文)。

    上下文切换需要一定的开销。但上下文切换的开销并不是只包含JVM和操作系统的开销,当一个新的线程被切换进来时,它所需要的数据可能不在当前处理器的缓存中,因此上下文切换将导致一些缓存的缺失,因而线程在首次调度运行时会更加缓慢。这就是为什么调度器会为每个可运行的线程分配一个最小执行时间(时间片),即使有许多其他线程正在等待执行(他将上下文切换的开销分摊到更多不会中断的执行时间上,从而提高整体的吞吐量,当然这会适当牺牲一定的响应性)。

    当线程由于等待某个发生竞争的锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去。如果线程频繁地发生阻塞,那么它们将无法使用完整的时间片。

:上下文切换的实际开销会随着平台的不同而变化,从经验上判断:大多数通用的处理器中,上下文切换的开销相当于5000 - 10000个时钟周期(几微秒吧)。

    看到这里你应该明白,如果想要阻塞或唤醒一条线程,则需要操作系统来帮助完成(synchronized 需要通过向内核申请互斥量(Mutex)来保证互斥同步)。尤其在JDK1.6以前的版本,被互斥的线程会被直接挂起,在这个过程中将包含两次额外的上下文切换,以及所有必要的操作系统操作和缓存操作(被阻塞的线程在其执行时间片还未用完之前就被交换出去,而随后当要获取的锁或者其他资源可用时,又再次被切换回来【当锁被释放时必须要告诉操作系统回复运行阻塞的线程】)。

所以在 synchronized 还没有引入一系列锁优化的时候,他的性能可想而知是很差的。

5.3. 自旋锁

    小伙伴们看了上文的描述是不是会觉得锁一旦膨胀到重量级之后性能就会变得很差?那在JDK1.6之后重量级锁还有哪些优化呢?我们这里要讲的自旋锁便是一种对重量级锁的优化。(我猜《Java并发编程的艺术》的作者一定是把它错以为是轻量级锁在CAS操作时的简单自旋了,实际上它并不是)。

    虚拟机的开发团队研究发现,大多数的应用上共享数据锁状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。与其直接挂起,不如让它“等一会儿”但不放弃处理器的执行时间(分配给线程的时间片)(只要这个自旋的开销小于一次挂起和唤醒,那么它就是值得的)。那如何让线程等待呢?我们只需要让线程自旋一会儿即可,这项技术就是所谓的自旋锁。

    自旋并不能代替阻塞,它只适合锁被短暂的占有的情况下。(CAS如果开启自旋与此处提到的自旋锁在这方面是类似的,他们都仅仅当锁被短暂占有或无竞争时才更具有优势,一但竞争十分激烈,不断自旋的开销将远大于将线程直接挂起的开销)。

    自旋锁在 JDK 1.4.2 中就已经引入,只不过是默认关闭的。可使用-XX:+UseSpinning参数开启,在以前的版本中自旋的次数默认为10次(一般认为10次自旋的损耗大概刚好小于挂起的开销)。可使用参数-XX:PreBlockSpin来自行修改。

    在JDK1.6之后默认开启,并对自旋锁进行了进一步的优化,引入了自适应自旋

5.4. 自适应自旋锁

    自适应自旋,顾名思义,自旋的次数不再是一个固定的值了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。

    如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很可能成功,进而允许自旋等待相对更长的时间。

    而如果这个锁很少成功获得锁,那么以后要获取这个锁时将可能直接省略掉自旋的过程(此时的自旋反而会增加synchronized的开销,所以就没有自旋的必要了)。

5.5. ObjectMonitor对象

下面就是就是我们今天的重头戏,也是synchronized的精华ObjectMonitor。认识它之前我们回顾下我们Mark Word的知识?

这个所谓的互斥量(重量级)指的便是底层的ObjectMonitor对象,但这个对象的作用并不仅仅只是用来存储原Mark Word的信息。除此之外还包含了大量优化操作,以及对synchronized实现互斥同步的底层操作,它是整个synchronized的核心。

首先我们来看看他的构造器:

// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
//构造器
ObjectMonitor() {
	_header = NULL; //mark word 
	_count = 0; 
	_waiters = 0; //等待线程数
	_recursions = 0; // 递归;线程的重入次数,典型的System.out.println
	_object = NULL; //  对应synchronized (object)对应里面的object
	_owner = NULL; // 标识拥有该monitor的线程
	_WaitSet = NULL; // 因为调用object.wait()方法而被阻塞的线程会被放在该队列中
	_WaitSetLock = 0 ;
	_Responsible = NULL;
	_succ = NULL;
	_cxq = NULL; // 竞争队列,所有请求锁的线程首先会被放在这个队列中
	FreeNext = NULL;
	_EntryList = NULL; // 阻塞;第二轮竞争锁仍然没有抢到的线程(在exit之后扔没有竞争到的线程将有可能会被同步至此)
	_SpinFreq = 0;
	_SpinClock = 0;
	OwnerIsThread = 0;
}

为了方便后边的学习我先来简单介绍下构造器中的三个队列:
_cxq队列:单向链表,所有请求锁的线程首先会被放在这个队列中,_cxq是一个临界资源,JVM通过CAS原子指令来将ObjectWaiter写入队列的头部。_cxq队列是个后进先出的栈(stack)。(_cxq队列中的线程在执行动作时会挣扎一下看看能否获得到锁(反正时间片没有用完,那就尝试一下获取锁),并不是直接就挂起)。
_entryList队列:双向链表,当_cxq队列不为空时,Owener会在unlock时根据不同的策略(QMode),将_cxq中的数据移动到_entryList中,并指定_entryList列表头部的第一个线程为OnDeck线程。(记住这里十分重要)。
_WaitSet队列:因为调用wait方法而被阻塞的线程会放在该队列中。

5.5. 重量级锁的加锁过程(ObjectMonitor::enter)

光看图不过瘾,我们接着看看源码:

以上这两段代码是初次尝试获取锁,以及判断是否为重入。如果没有获取锁,进入我们的关键方法EnterⅠ

关于TryLock的源码:

总结:
根据以上的源码我们可看出重量级锁在将线程挂起之前会不停的“挣扎”。几乎每一步都在尝试通过CAS获取锁,这样做是为了在拥有时间片时尽量尝试获取锁,避免挂起造成的性能损耗。(上文中的上下文切换已经说的非常清楚了)。

5.6. 重量级锁的解锁过程(ObjectMonitor::exit)

这部分看起来有些复杂,我们先看张流程图来梳理思路:

我们来看看源码:

总结:
exit的逻辑看似复杂,其实并不复杂,只要梳理清楚这几种策略就能很好的理解它了。

5.7. 重量级锁的_waitSet源码

wait的逻辑:

这块十分简单,就不展示了源码各位小伙伴可以自己去翻,大概就是包装当前线程ObjectWait,状态会设置为TS_WAIT,然后将它插入到_waitSet中(一个双向链表)。

notify的逻辑:

5.8. 锁消除

    锁消除是指虚拟机即时编译器在运行时编译器在运行时检测到某段需要同步的代码块根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略。

    锁消除的主要判定依据来源于逃逸分析,逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就不会有竞争,对这个变量实施的同步措施也就可以安全的消除。

逃逸分析并不是100%准确。
    HotSpot在1.6之后才开始支持逃逸分析,至今这项技术还未完全成熟,不成熟的主要原因是逃逸分析的计算成本非常高,甚至无法保证逃逸分析的性能收益会高于他的消耗。(现如今Java语言在服务逐渐小型化的大趋势中已经显得略有笨重,主要的劣势来自于即时编译、提前编译,这种大压力的算法正是即时编译的弱项)。

所以目前虚拟机采用的是并不那么精准,但时间压力相对较小的算法来处理逃逸分析。

5.9. 锁粗化

    试想一下,如果有一系列的操作都对同一个对象反复加锁和解锁,甚至在一个循环体中,那此时即使没有线程竞争,频繁的进行互斥操作也会导致不必要的性能开销。

    如果虚拟机探测到这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围粗化(扩展)到整个操作序列外部。

六. 字节码层面解读synchronized

    同步方法与同步代码在字节码层面块略有不同,我们先来看看同步方法(底层的实现逻辑他们都是一样的)。

6.1. 同步方法

// 测试代码:

public class Test6 {
    public synchronized void sync() {
        // 已这段代码为例
    }
}

我们首先使用 javap -v 命令来看看它反编译后的样子:

Last modified 2023-11-20; size 367 bytes
  MD5 checksum 498295a538617ae7ecfff526124e4ccf
  Compiled from "Test6.java"
public class com.lm.synchonized.Test6
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // com/lm/synchonized/Test6
   #3 = Class              #16            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/lm/synchonized/Test6;
  #11 = Utf8               sync
  #12 = Utf8               SourceFile
  #13 = Utf8               Test11.java
  #14 = NameAndType        #4:#5          // "<init>":()V
  #15 = Utf8               com/lm/synchonized/Test6
  #16 = Utf8               java/lang/Object
{
  public com.lm.synchonized.Test6();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lm/synchonized/Test6;

  public synchronized void sync();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 同步方法的访问标识
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/lm/synchonized/Test6;
}
SourceFile: "Test6.java"

我们可以看到这样一个flage:ACC_SYNCHRONIZED,没错这个标识便代表这个方法为同步方法。

6.2. class 文件结构

    class文件是一组以字节为基础的二进制流,它的各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部是程序运行的必要数据。(class文件自JDK1.2之后就定义好了,并且在后来的十几个大版本小版本中基本没有什么大的变化,它是java技术的基础与基石)。

class文件中只有两种数据类型:
1. 无符号数据: 以u1、u2、u4、u8(字节数)代表无符号数,它的主要作用是来描述数字、索引引用、数量值或者按照UTF-8构成字符串值。
2. 表:由多个无符号数或者其他表组成的复合数据类型,为了区别所有表的命名习惯性以_info结尾。

而我们的 ACC_SYNCHRONIZED 实际上是属于 方法表 中的 access_flags (u2)(作用是为方法定义访问标识)除了方法表还有字段表、属性表等等,而 ACC_SYNCHRONIZED 是仅属于方法表的访问标识。

方法表结构:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

方法表的其他访问标识:

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为 public
ACC_PRIVATE0x0002方法是否为 private
ACC_PROTECTED0x0004方法是否为 protected
ACC_STATIC0x0008方法是否为 static
ACC_FINAL0x0010方法是否为 final
ACC_SYCHRONIZED0x0020方法是否为 sychcronized
ACC_BRIDGE0x0040方法是不是由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为 native
ACC_ABSTRACT0x0400方法是否为 abstract
ACC_STRICT0x0800方法是否为 strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

方法表访问标识在class文件中真正的位置:

以下是我对class文件结构的简单整理如果对这块有兴趣的小伙伴可以参照这张表自己研究,在这里就不展开说了 。

同步方法的总结:
上面我们稍微拓展了一些额外的知识,现在我们重新回来总结下这个方法访问标识ACC_SYNCHRONIZED:
1. JVM会解析方法的访问标识,判断方法是不是同步的,其实在底层是去检查了方法的ACC_SYNCHRONIZED标记位是否被设置为1。如果是同步方法,执行线程会先尝试获取锁。在同步方法完成以后,不管是否正常返回(或异常),都会释放锁
2. 同步方法在实例方法上持有的锁对象为 this,在静态方法上持有的锁对象为 class。 

6.3. 同步代码块

// 测试代码

public class Test7 {
    public void test01() {
        synchronized (this) {
            //已这段代码为例
        }
    }
}

我们反编译一下这段代码并截取这段来看一下:

public void test01();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0		//将this入栈(操作数栈,以下均简称栈)
         1: dup			//复制栈顶元素(this的引用)
         2: astore_1		//将栈顶元素存储到局部变量表变量槽 1中(this)
         3: monitorenter	//将栈顶元素(此时就是this)作为锁,开始同步
         4: aload_1		//又从局部变量Slow 1的元素入栈(this)
         5: monitorexit        	//退出同步
         6: goto          14	//如果方法正常结束跳转到14
         9: astore_2		//这一步就是异常路径,见异常表(将异常对象存储到局部变量表变量槽 2中)
        10: aload_1		//和第四步同将局部变量Slow 1的元素入栈(this)(因为上边此时出现了异常)
        11: monitorexit		//退出同步
        12: aload_2		//将局部变量Slow 2的元素(异常对象)入栈
        13: athrow		//把异常对象抛给方法的调用者
        14: return		//方法正常返回
      Exception table:          //以下为异常表
         from    to  target type
             4     6     9   any
             9    12     9   any

我们可以看到方法的访问标识此时只有ACC_PUBLIC,也就是说这个方法并没有被虚拟机标记为同步方法,而是采用了字节码指令:monitorenter 、monitorexit,并且monitorexit 还出现了两次。这是为什么呢?其实两次monitorexit只执行了一次,其中一个monitorexit指令是在程序发生异常时去保证它的可以安全释放锁。那什么是异常表?我们下面再简单了解下异常表。

6.4. 异常表(exception_table)

    他的名字被称为表?难道它和方法表一样都属于表么?没错,异常表和方法表一样都属于表。但在隶属关系上异常表是方法表中Code属性的一部分(且不是必须存在的)。

异常表的属性表结构:

解释下:如果字节码从第start_pc行(这里的行指的是字节码相对于方法体开始的偏移量)到第end_pc行之间(不含end_pc)出现异常,则跳转到handler_pc行进行处理,当catchtype的值为0时,代表任意情况下都需要跳转到handler_pc 处进行处理。 

看到这里我们似乎有些明白了上边异常表的执行逻辑了,如还不清楚请看下图:

6.5. 字节码指令 monitorenter与monitorexit

    当线程执行到monitorenter指令时,会尝试获取栈顶对象对应监视器(monitor)的所有权,如果此时monitor没有其他线程占有,当前线程成功获取锁,monitor计数器设置为1。如果当前线程已经持有了monitor的所有权,monitorenter也会顺利执行(重入),monitor计数器加1。如果其他线程拥有了monitor的所有权,当前线程会阻塞,直到monitor计数器变为0。

    当线程执行monitorexit时,会将monitor计数器减1,计数器值等于0时,释放锁,其他线程可以尝试去获取monitor的所有权。

参考Chapter 6. The Java Virtual Machine Instruction Set 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1230570.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Kettle] 获取系统信息

系统信息是指Kettle系统环境的信息&#xff0c;包括了计算机系统的日期、星期等时间类型信息&#xff0c;计算机名称、IP地址等设备信息&#xff0c;Kettle系统转换过程中的信息等 需求&#xff1a;为方便读取计算机上的本月最后一天的交易数据文件&#xff0c;需要通过获取系…

词向量笔记

一、词向量的世界排名比赛MTEB MTEB: Massive Text Embedding Benchmark 二、词向量的维数 词向量的维数d1一般取20~500之间

不标年份的葡萄酒质量好吗?

我们在葡萄酒标上经常看到生产年份&#xff0c;也就是指全部葡萄采摘的年份。旧世界葡萄酒产国认为葡萄酒年份对他们的影响较大&#xff0c;而新世界葡萄酒&#xff0c;年份的意义就稍微小些。甚至有一部分葡萄酒酒标上没有年份。在酒标上没有标注年份的葡萄酒&#xff0c;被称…

ELK企业级日志分析平台

目录 一、elasticsearch 1、集群部署 2、cerebro部署 3、elasticsearch-head插件部署 4、elasticsearch集群角色分类 二、logstash 1、部署 2、elasticsearch输出插件 3、file输入插件 4、file输出插件 5、syslog 插件 6、多行过滤插件 7、grok过滤 三、kibana数…

网络工程师-HCIA网课视频学习

这里是速成的&#xff0c;只积累下&#xff0c;自己未曾学习到的东西。通过书本补充知识点。 视频&#xff1a;hcia17-链路聚合_哔哩哔哩_bilibili hcia16-路由高级特性&#xff1a; hcia17-链路聚合&#xff1a; 由于如果根据视频来学习的话&#xff0c;感觉视频的总结并不…

什么牌子的猫罐头好?性价比高的猫罐头推荐

很多铲屎官遇到过这样的问题&#xff01;他们购买了各种高质量的猫罐头&#xff0c;但猫咪却不吃或者吃了一段时间后出现软便等身体不适的情况。 作为一个担任了6年铲屎官的人&#xff0c;我对猫咪的日常饮食有着一定的了解。我给我家的猫咪尝试过很多种罐头。在这里&#xff0…

行情分析 - - 加密货币市场大盘走势(11.20)

大饼还是之前的策略&#xff0c;37000附近入场空单合适&#xff0c;目前4小时级别即将形成死叉&#xff0c;入场空单收益较大。 空单策略&#xff1a;入场37000附近 止盈34000-33000 止损39000 以太按照预期回调&#xff0c;笔者已经在1920打了止盈&#xff0c;目前没有入场&a…

HIS系统源码,云HIS源码,二级医院信息管理系统源码,预约挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生工作站、护士工作站

云HIS系统是一款满足基层医院各类业务需要的健康云产品。该产品能帮助基层医院完成日常各类业务&#xff0c;提供病患预约挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生工作站和护士工作站等一系列常规功能&#xff0c;还能与公卫、PACS等各类外部系统融合…

高效案例检索工具,Alpha案例库智慧检索成为律师检索工具首选

“工欲善其事&#xff0c;必先利其器。”当今&#xff0c;律界同仁需要权衡的问题早已不是“要不要”使用法律科技&#xff0c;而是如何高质量、高效率地使用法律科技工具。在业内人士看来&#xff0c;随着人工智能技术的不断发展&#xff0c;法律行业科技化将成为不可逆转的趋…

KaiwuDB 监控组件及辅助 SQL 调优介绍

一、介绍 KaiwuDB 具备完善的行为数据采集功能&#xff0c;此功能要求 KaiwuDB 数据库系统 C/E/T 端不同进程的不同维度的指标采集功能十分完善&#xff1b;在不同进程完成指标采集后&#xff0c;会通过 Opentelemetry 和 Collector 将指标存入 Prometheus&#xff0c;以便查找…

这两个让你直呼卧槽的软件,超级实用

不知道大家有没有碰到这种情况呢&#xff1f;在手机上解压文件解压不了&#xff0c;还得去电脑上下载之后解压&#xff0c;特别麻烦 为了解决这一问题&#xff0c;所以今天给大家准备 两款 解压缩 神器&#xff0c;让大家解的越来越熟练。 解压精灵 解压精灵这是一款解压缩并且…

docker打包chatpdf(自写)

docker打包上传 docker build -t kitelff/chatpdf:v0.1 .##修改镜像名字 docker tag c2c1a0eb4e08 kitelff/chatpdf:v0.1## push docker push kitelff/chatpdf:v0.1上传文件&#xff0c;测试效果

Django 入门学习总结3

1、创建数据库 打开mysite/settings.py文件&#xff0c;可以看到我们使用Python中已包含的默认的数据库SQLite&#xff0c;也可以使用其他的数据库&#xff0c;如Oracle、Mysql等。里面也包含时区、语言等设置信息。 在使用数据库和表之前&#xff0c;输入下面的命令&#xf…

windows下rust调试运行环境部署

1&#xff0c;rust编译环境安装 在联网环境下&#xff0c;建议使用rustup-init.exe程序安装&#xff08;本文使用的改模式) 选择1“默认"进行安装&#xff0c;默认安装x86_64-pc-windows-msvc 在安装完成后&#xff0c;后续为了配置gbd调试&#xff0c;也安装上x86_64-pc-…

第四代智能井盖传感器,实时守护井盖位安全

城市管理中井盖的安全问题始终是一个不容忽视的方面。传统的巡检方式不仅效率低下&#xff0c;无法实现实时监测&#xff0c;而且很难准确掌握井盖的异动状态。因此智能井盖传感器的应用具有重要意义。这种智能传感器可以帮助政府实时掌握井盖的状态&#xff0c;一旦发现异常情…

#gStore-weekly | gBuilder功能详解之表单录入

gBuilder除了可以提供结构化数据映射以及非结构化数据抽取两种构建知识图谱的方式以外&#xff0c;还提供了表单录入的方式来构建知识图谱的数据&#xff0c;用户只需要根据设计好的schema将实体、属性以及关系通过填写表单的形式录入&#xff0c;再通过一键生成NT文件即可获得…

【Redis】渐进式遍历数据库管理

文章目录 渐进式遍历scan 数据库管理切换数据库清除数据库 获取当前数据库key的个数 渐进式遍历 Redis使⽤scan命令进⾏渐进式遍历键&#xff0c;进⽽解决直接使⽤keys获取键时能出现的阻塞问题。每次scan命令的时间复杂度是O(1)&#xff0c;但是要完整地完成所有键的遍历&…

万户OA upload任意文件上传漏洞复现

0x01 产品简介 万户OA ezoffice是万户网络协同办公产品多年来一直将主要精力致力于中高端市场的一款OA协同办公软件产品&#xff0c;统一的基础管理平台&#xff0c;实现用户数据统一管理、权限统一分配、身份统一认证。统一规划门户网站群和协同办公平台&#xff0c;将外网信息…

pikach靶场暴力破解

pikach靶场暴力破解 文章目录 pikach靶场暴力破解安装pikach靶场暴力破解第一关第二关第三关第四关 安装pikach靶场 进入github下载pikach的源码 不是linux推荐下载压缩包 下载完成后放入phpstudy中进行解压放入www网站根目录下 在数据库中新建数据库为pikachu create data…

【C++】标准模板库 STL 简介

&#x1f9d1;‍&#x1f393;个人主页&#xff1a;简 料 &#x1f3c6;所属专栏&#xff1a;C &#x1f3c6;个人社区&#xff1a;越努力越幸运社区 &#x1f3c6;简 介&#xff1a;简料简料&#xff0c;简单有料~在校大学生一枚&#xff0c;专注C/C/GO的干货分…