synchronized
synchronized 的使用方式
synchronized 关键字给代码或者方法上锁时,都有显示或者隐藏的上锁对象。当一个线程试图访问同步代码块时,它首先必须得到锁,而退出或抛出异常时必须释放锁。
- 给普通方法加锁时,上锁的对象是 this;
- 给静态方法加锁时,锁的是 class 对象;
- 给代码块加锁,可以指定一个具体的对象作为锁。
monitor 锁
在Java中,实现线程同步的一种基本且直观的方式是通过synchronized关键字。当我们将synchronized应用于代码块或方法时,实际上是在使用一种称为monitor锁(或内置锁)的机制来确保任何时刻只有一个线程能够访问被保护的资源。这一机制的核心在于,每个Java对象都可以充当这样的锁,从而实现对共享资源的独占访问。
在更深层次上,Java虚拟机(JVM)通过监视器锁(monitor)来支持synchronized语义。每当线程请求访问一个synchronized代码块时,它实际上是在请求一个特定对象的监视器。如果请求成功,线程将进入代码块并执行,同时其他线程则会被阻止,直到当前线程释放监视器锁。
在Java虚拟机(JVM)中,为了提高线程同步的效率,引入了多种锁机制,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。JVM 会根据使用情况,对 synchronized 的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁 -> 轻量级锁 -> 重量级锁。锁只能升级,不能降级,所以一旦升级为重量级锁,就只能依靠操作系统进行调度。在竞争程度较低的场景下,Synchronized 可以提供较高的性能。在JVM对Synchronized进行优化后,如使用偏向锁、轻量级锁等,能使其在无竞争和轻度竞争情况下避免重量级锁使用操作系统互斥量带来的性能消耗。
Synchronized 属于 JVM 的内置锁,Synchronized 方法或代码块在编译后,会在字节码层面有一对 monitorenter 和 monitorexit 指令,分别表示获取锁和释放锁。
当一个线程试图获取某个对象的监视器(也叫做监控锁或同步锁)时,它会执行 monitorenter 指令。这个指令会把对象引用加载到操作数栈中,然后尝试获取这个对象所指向的对象的锁。如果获取成功,那么这个线程将成为该对象的所有者,其他线程必须等待锁被释放才能获取。当线程退出同步代码块或调用 wait() 方法时,monitorexit 指令负责释放锁。
所谓锁的升级、降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
获取和释放 monitor 锁的时机
- 获取锁:当线程试图进入由synchronized修饰的代码块或方法时,它必须先尝试获取该代码块或方法所关联对象的monitor锁。如果锁当前未被其他线程持有,则该线程将成功获取锁并继续执行;否则,线程将被阻塞,直到锁可用。
- 释放锁:线程在完成synchronized代码块的执行后,无论其是通过正常执行流程结束还是因抛出异常而提前退出,都会自动释放之前获取的monitor锁。这种自动管理锁的机制简化了多线程编程中的资源管理,减少了死锁的风险。
- 最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行
我们首先来看一个 synchronized 修饰方法的代码的例子:
arduino
public synchronized void method() {
method body
}
我们看到 method() 方法是被 synchronized 修饰的,为了方便理解其背后的原理,我们把上面这段代码改写为下面这种等价形式的伪代码。
csharp
public void method() {
this.intrinsicLock.lock();
try{
method body
}
finally {
this.intrinsicLock.unlock();
}
}
在这种写法中,进入 method 方法后,立刻添加内置锁,并且用 try 代码块把方法保护起来,最后用 finally 释放这把锁,这里的 intrinsicLock 就是 monitor 锁。经过这样的伪代码展开之后,相信你对 synchronized 的理解就更加清晰了。
用 javap 命令查看反汇编的结果
JVM 实现 synchronized 方法和 synchronized 代码块的细节是不一样的,下面我们就分别来看一下两者的实现。
同步代码块
首先我们来看下同步代码块的实现,如代码所示。
csharp
public class SynTest {
public void synBlock() {
synchronized (this) {
System.out.println("eva");
}
}
}
在 SynTest 类中的 synBlock 方法,包含一个同步代码块,synchronized 代码块中有一行代码打印了 eva 字符串,下面我们来通过命令看下 synchronized 关键字到底做了什么事情:首先用 cd 命令切换到 SynTest.java 类所在的路径,然后执行 javac SynTest.java,于是就会产生一个名为 SynTest.class 的字节码文件,然后我们执行 javap -verbose SynTest.class,就可以看到对应的反汇编内容。
关键信息如下:
yaml
public void synBlock();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #13 // String eva
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
从里面可以看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 指令。
这里有一个 monitorenter,却有两个 monitorexit 指令的原因是,JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁
可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,我们来具体看一下 monitorenter 和 monitorexit 的含义:
monitorenter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
- 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
- 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
- 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。
同步方法
从上面可以看出,同步代码块是使用 monitorenter 和 monitorexit 指令实现的。而对于 synchronized 方法,并不是依靠 monitorenter 和 monitorexit 指令实现的,被 javap 反汇编后可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。
同步方法的代码如下所示。
csharp
import java.util.concurrent.CountDownLatch;
public class SynTest {
public Integer lock;
public SynTest(Integer lock) {
this.lock = lock;
}
public synchronized void synMethod() {
lock = lock + 1;
}
public Integer getLock() {
return lock;
}
}
class Client {
public static void main(String[] args) {
SynTest synTest = new SynTest(1);
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++){
new Thread(synTest::synMethod).start();
countDownLatch.countDown();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行结束");
System.out.println(synTest.getLock());
}
}
对应的反汇编指令如下所示。
yaml
public synchronized void synMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #7 // Field lock:Ljava/lang/Integer;
5: invokevirtual #13 // Method java/lang/Integer.intValue:()I
8: iconst_1
9: iadd
10: invokestatic #19 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: putfield #7 // Field lock:Ljava/lang/Integer;
16: return
LineNumberTable:
line 13: 0
line 14: 16
可以看出,被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞。
这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。我们可以通过下面这张图,来看一下 monitor 的原理。
注意了,下面是面试题目高发地。比如,你能描述一下 monitor 锁的实现原理吗?
如上图所示,我们可以把运行时的对象锁抽象地分成三部分。其中,EntrySet 和 WaitSet 是两个队列,中间虚线部分是当前持有锁的线程,我们可以想象一下线程的执行过程。
当第一个线程到来时,发现并没有线程持有对象锁,它会直接成为活动线程,进入 RUNNING 状态。
接着又来了三个线程,要争抢对象锁。此时,这三个线程发现锁已经被占用了,就先进入 EntrySet 缓存起来,进入 BLOCKED 状态。此时,从 jstack 命令,可以看到他们展示的信息都是 waiting for monitor entry。
less
"http-nio-8084-exec-120" #143 daemon prio=5 os_prio=31 cpu=122.86ms elapsed=317.88s tid=0x00007fedd8381000 nid=0x1af03 waiting for monitor entry [0x00007000150e1000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.io.BufferedInputStream.read(java.base@13.0.1/BufferedInputStream.java:263)
- waiting to lock <0x0000000782e1b590> (a java.io.BufferedInputStream)
at org.apache.commons.httpclient.HttpParser.readRawLine(HttpParser.java:78)
at org.apache.commons.httpclient.HttpParser.readLine(HttpParser.java:106)
at org.apache.commons.httpclient.HttpConnection.readLine(HttpConnection.java:1116)
at org.apache.commons.httpclient.HttpMethodBase.readStatusLine(HttpMethodBase.java:1973)
at org.apache.commons.httpclient.HttpMethodBase.readResponse(HttpMethodBase.java:1735)
处于活动状态的线程,执行完毕退出了;或者由于某种原因执行了 wait 方法,释放了对象锁,进入了 WaitSet 队列,这就是在调用 wait 之前,需要先获得对象锁的原因。
就像下面的代码:
csharp
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
此时,jstack 显示的线程状态是 WAITING 状态,而原因是 in Object.wait()。
less
"wait-demo" #12 prio=5 os_prio=31 cpu=0.14ms elapsed=12.58s tid=0x00007fb66609e000 nid=0x6103 in Object.wait() [0x000070000f2bd000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@13.0.1/Native Method)
- waiting on <0x0000000787b48300> (a java.lang.Object)
at java.lang.Object.wait(java.base@13.0.1/Object.java:326)
at WaitDemo.lambda$main$0(WaitDemo.java:7)
- locked <0x0000000787b48300> (a java.lang.Object)
at WaitDemo$$Lambda$14/0x0000000800b44840.run(Unknown Source)
at java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
发生了这两种情况,都会造成对象锁的释放,进而导致 EntrySet 里的线程重新争抢对象锁,成功抢到锁的线程成为活动线程,这是一个循环的过程。那 WaitSet 中的线程是如何再次被激活的呢?接下来,在某个地方,执行了锁的 notify 或者 notifyAll 命令,会造成 WaitSet 中的线程,转移到 EntrySet 中,重新进行锁的争夺。如此周而复始,线程就可按顺序排队执行。
Synchronized中的分级锁
JVM 会根据使用情况,对 synchronized 的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁 -> 轻量级锁 -> 重量级锁。
Java 中的锁主要分为三种:偏向锁、轻量级锁和重量级锁。这三种锁在不同的场景下具有不同的性能特点。
- 偏向锁:无竞争的情况下,只有一个线程进入临界区,采用偏向锁
- 轻量级锁:多个线程可以交替进入临界区,采用轻量级锁
- 重量级锁:多线程同时进入临界区,交给操作系统互斥量来处理
重量级锁
重量级锁是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合 posix 接口的操作系统(如 macOS 和绝大部分的 Linux),上述操作是通过 pthread 的互斥锁(mutex)来实现的。此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。
为了尽量避免昂贵的线程阻塞、唤醒操作,Java 虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。
与线程阻塞相比,自旋状态可能会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。
我们可以用等红绿灯作为例子。Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如说我们在 synchronized 代码块里只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更加合适。
然而,对于 Java 虚拟机来说,它并不能看到红灯的剩余时间,也就没办法根据等待时间的长短来选择自旋还是阻塞。Java 虚拟机给出的方案是自适应自旋,根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)。
就我们的例子来说,如果之前不熄火等到了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等到绿灯,那么这次不熄火的时间就短一点。
自旋状态还带来另外一个副作用,那便是不公平的锁机制。处于阻塞状态的线程,并没有办法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
轻量级锁
轻量级锁是JDK 6时加入的新型锁机制,它名字中的"轻量级"是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为"重量级"锁。不过,需要强调一点,轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁的工作过程:在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为"01"状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方为这份拷贝加了一个Displaced前缀,即Displaced Mark Word),这时候线程堆栈与对象头的状态如图13-3所示。
然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为"00",表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如图13-4所示。
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为"10",此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过CAS操作来进行的,如果对象的 Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁能提升程序同步性能的依据是"对于绝大部分的锁,在整个同步周期内都是不存在竞争的"这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
轻量级锁适用于线程交替执行同步块的场景,Java 虚拟机也存在着类似的情形:多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。
偏向锁
如果说轻量级锁针对的情况很乐观,那么接下来的偏向锁针对的情况则更加乐观:从始至终只有一个线程请求某一把锁。
这就好比你在私家庄园里装了个红绿灯,并且庄园里只有你在开车。偏向锁的做法便是在红绿灯处识别来车的车牌号。如果匹配到你的车牌号,那么直接亮绿灯。
具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中,并且将标记字段的最后三位设置为 101。
在接下来的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁对象标记字段中:
- 最后三位是否为 101,
- 是否包含当前线程的地址,
- 以及 epoch 值是否和锁对象的类的 epoch 值相同。
如果都满足,那么当前线程持有该偏向锁,可以直接返回。
这里的 epoch 值是一个什么概念呢?
在每个类中维护一个 epoch 值,你可以理解为第几代偏向锁。当设置偏向锁时,Java 虚拟机需要将该 epoch 值复制到锁对象的标记字段中。在宣布某个类的偏向锁失效时,Java 虚拟机实则将该类的 epoch 值加 1,表示之前那一代的偏向锁已经失效。而新设置的偏向锁则需要复制新的 epoch 值。为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java 虚拟机需要遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。该操作需要所有线程处于安全点状态。
如果总撤销数超过另一个阈值(对应 Java 虚拟机参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。此时,Java 虚拟机会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置轻量级锁。
可是多线程环境,也不可能只是同一个线程一直获取这个锁,其他线程也是要干活的,如果出现多个线程竞争的情况,也就有了偏向锁升级的过程
偏向锁,轻量锁,它俩都不会调用系统互斥量(Mutex Lock),只是为了提升性能,多出的两种锁的状态,这样可以在不同场景下采取最合适的策略,所以可以总结性的说:
- 偏向锁:无竞争的情况下,只有一个线程进入临界区,采用偏向锁
- 轻量级锁:多个线程可以交替进入临界区,采用轻量级锁
- 重量级锁:多线程同时进入临界区,交给操作系统互斥量来处理
弃用偏向锁
为啥要弃用偏向锁,因为维护成本有些高了,来看openJDK JEP 374: Deprecate and Disable Biased Locking,相信你看上面的文字说明也深有体会。一句话解释就是维护成本太高。偏向锁给 JVM 增加了巨大的复杂性,只有少数非常有经验的程序员才能理解整个过程,维护成本很高,大大阻碍了开发新特性的进程。
JAVA对象头
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
也就是说 JAVA对象 = 对象头 + 实例数据 + 对象填充。
其中,对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。
32位虚拟机对象头
普通对象的对象头包含2部分,第一部分被称为Mark Word,第二部分为类型指针。如果对象为数组,除了普通对象的两部分外对象头还包含数组长度。
32位虚拟机普通对象的对象头
css
|-----------------------------------------------------------|
| Object Header (64 bits) |
|---------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|---------------------------------|-------------------------|
32位虚拟机数组对象的对象头
scss
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
32位虚拟机对象头详情如下
vbnet
|--------------------------------------------------------------------------------------------------------------|
| Object Header(64bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word(32bits) | Klass Word(32bits) | State |
|--------------------------------------------------------------------------------------------------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | OOP to metadata object | Nomal |
|--------------------------------------------------------------------------------------------------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object | Biased |
|--------------------------------------------------------------------------------------------------------------|
| ptr_to_lock_record:30 | 00 | OOP to metadata object | Lightweight Locked |
|--------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:30 | 10 | OOP to metadata object | Heavyweight Locked |
|--------------------------------------------------------------------------------------------------------------|
| | 11 | OOP to metadata object | Marked for GC |
|--------------------------------------------------------------------------------------------------------------|
64位虚拟机对象头
vbnet
|--------------------------------------------------------------------------------------------------------------|
| Object Header(128bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word(64bits) | Klass Word(64bits) | State |
|--------------------------------------------------------------------------------------------------------------|
| unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object | Nomal |
|--------------------------------------------------------------------------------------------------------------|
| thread:54| epoch:2 |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object | Biased |
|--------------------------------------------------------------------------------------------------------------|
| ptr_to_lock_record:62 | 00 | OOP to metadata object | Lightweight Locked |
|--------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:62 | 10 | OOP to metadata object | Heavyweight Locked |
|--------------------------------------------------------------------------------------------------------------|
| | 11 | OOP to metadata object | Marked for GC |
|--------------------------------------------------------------------------------------------------------------|
对象头的总结
- lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了 lock标记。该标记的值不同,整个 Mark Word表示的含义不同。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock 和 biased_lock共同表示对象处于什么锁状态
- age:4位的 Java对象年龄。在GC中,如果对象在 Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行 GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold 选项最大值为15的原因。
- identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法 System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到线程 Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向锁的时间戳。
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor的指针。
对象头
Mark Word
Mark Word保存了对象运行时必要的信息,包括哈希码(HashCode)、GC分代年龄、偏向状态、锁状态标志、偏向线程ID、偏向时间戳等信息。通过类型指针,可以找到对象对应的类型信息。32位虚拟机和64位虚拟机的Mark Word长度分别为4字节和8字节。
不论是32位还是64位虚拟机的对象头部都使用了4比特记录分代年龄,每次GC时对象幸存年龄都会加1,因此对象在survivor区最多幸存15次,超过15次时,仍然有可达根的对象就会从survivor区被转移到老年代。可以通过-XX:MaxTenuringThreshold=15参数修改最大幸存年龄。
CMS垃圾回收器默认为6次。
分级锁中的Mark Word
- 初期锁对象刚创建时,还没有任何线程来竞争,对象的 Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
- 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时 Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图。
- 当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的 Mark Word就执行哪个线程的栈帧中的锁记录。如下图。
- 如果竞争的这个锁对象的线程超过两个线程,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象 Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图。
Klass pointer
相比32位对象头大小,64位对象头更大一些,64位虚拟机对象头的Mark Word和类型指针地址都是8字节。而通常情况,我们的程序不需要占用那么大的内存。因此虚拟机通过压缩指针功能,将对象头的类型指针进行压缩。而Mark Word由于运行时需要保存的头部信息会大于4字节,仍然使用8字节。若配置开启了-XX:+UseCompressedOops,虚拟机会将类型指针地址压缩为32位。若配置开启了-XX:+UseCompressedClassPointers,则会压缩klass对象的地址为32位。
需要注意的是,当地址经过压缩后,寻址范围不可避免的会降低。对于64位CPU,由于目前内存一般到不了2^64,因此大多数64位CPU的地址总线实际会小于64位,比如48位。
开启-XX:+UseCompressedOops,默认也会开启-XX:+UseCompressedClassPointers。关闭-XX:+UseCompressedOops,默认也会关闭-XX:+UseCompressedClassPointers。
如果开启-XX:+UseCompressedOops,但是关闭-XX:+UseCompressedClassPointers,启动虚拟机的时候会提示"Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops"。
通过JOL查看分级锁的MarkWord
scss
相关方法:
1.使用jol计算对象的大小(单位为字节):
ClassLayout.parseInstance(obj).instanceSize()
2.使用jol查看对象的内存布局:
ClassLayout.parseInstance(obj).toPrintable()
xml
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
什么是大/小端模式
大端模式
大端模式(Big-Endian)又称大端字节序,由于在网络传输中一般使用的是大端模式,所以也叫网络字节序。
在大端模式中,将高位字节放在低位地址,低位字节放在高位地址。
举个例子,数值 0x12345678,其中 0x12 这一端是高位字节,0x78 这一端是低位字节。
该数值的存储顺序是这样的:
大端模式符合我们阅读和书写的方式,都是从左到右的。比如 12345678,我们只需要按照从左到右的顺序进行阅读和书写就是大端模式的存储顺序了。
小端模式
小端模式(Little-Endian)又称小端字节序,由于大多数计算机内部处理使用的是小端模式,所以也叫主机序。
在小端模式中,将高位字节放在高位地址,低位字节放在低位地址。
小端模式比较符合我们人类的思维模式,大的放大的那一边,小的放小的那一边。但是在计算机中存储的顺序与我们看到的顺序是相反的。
开启指针压缩可以减少对象的内存使用。
在关闭指针压缩时,String、Integer等字段由于是引用类型,因此分别占8个字节;
而开启指针压缩之后,这两个字段只分别占用4个字节。
因此,开启指针压缩,理论上来讲,大约能节省接近百分之五十的内存。(如果对象属性都是引用类型的话)
jdk8及以后版本已经默认开启指针压缩,无需配置。
- 开启(-XX:+UseCompressedOops) 可以压缩指针。
- 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针。
无锁
typescript
public class ClockTest {
public static void main(String[] args) {
Object o = new Object();
System.out.println("无锁--------------------------");
System.gc();
String binary = Integer.toBinaryString(o.hashCode());
System.out.println(binary);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
python
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 17 c6 d3 (00001001 00010111 11000110 11010011) (1)
4 4 (object header) 66 00 00 00 (01100110 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
由于内存按小端模式分布,因此显示的内容是反着的。上面实际对象头内容为 00 00 00 66 d3 c6 17 09。
00000000 00000000 00000000 0 1100110 11010011 11000110 00010111 00001001
上面我们用到的 JOL 版本为 0.10, 带领大家快速了解一下位具体值,接下来我们就要用 0.16 版本查看输出结果,因为这个版本给了我们更友好的说明。
偏向锁
它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
偏向锁的锁状态和未锁状态一样都是01,当对象处于偏向状态时,偏向标记为1;当对象处于未偏向时,偏向标记为0。
无锁状态升级为偏向锁的条件:
- 对象可偏向,对象未加锁时,执行CAS更新对象头部线程偏向线程ID为当前线程成功。
- 对象可偏向,对象已加锁,但偏向线程ID为空,执行CAS更新对象头部线程偏向线程ID为当前线程成功。
- 对象可偏向,对象已加锁,且偏向线程ID等于当前线程ID。
- 对象可偏向,对象已加锁,且偏向线程ID不为空且不等于当前线程ID,执行CAS更新对象头部线程偏向线程ID为当前线程成功。
csharp
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(("进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
python
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) //不可偏向
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000700005f5d9c0 (thin lock: 0x0000700005f5d9c0)// 表示轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
看到这个结果,你应该是有疑问的,JDK 1.6 之后默认是开启偏向锁的,为什么初始化的代码是无锁状态,进入同步块产生竞争就绕过偏向锁直接变成轻量级锁了呢?
虽然默认开启了偏向锁,但是开启有延迟,大概 4s。原因是 JVM 内部的代码有很多地方用到了synchronized,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗,所以就有了延迟策略
虚拟机启动时,会根据-XX:BiasedLockingStartupDelay配置延迟启动偏向,在JDK1.8中,默认为4秒。有需要时可以通过-XX:BiasedLockingStartupDelay=0关闭延时偏向。但是不建议这么做。我们可以通过Thread.sleep(5000);
csharp
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
// 显式触发一次gc
System.gc();
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(("进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
xian
}
python
未进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000000000000d (biasable; age: 1) //一种匿名偏向状态,是对象初始化中,JVM 帮我们做的,可偏向状态,虽然后三位位101,但是线程id=0
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007f7e6e00900d (biased: 0x0000001fdf9b8024; epoch: 0; age: 1) //已偏向状态,最后三位位101,且线程id不为空。
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
这样的结果是符合我们预期的,但是结果中的 biasable 状态,在 MarkWord 表格中并不存在,其实这是一种匿名偏向状态,是对象初始化中,JVM 帮我们做的
这样当有线程进入同步块:
- 可偏向状态:直接就 CAS 替换 ThreadID,如果成功,就可以获取偏向锁了
- 不可偏向状态:就会变成轻量级锁
偏向锁机制概述
偏向锁是Java HotSpot虚拟机在JDK 1.6中引入的一种锁优化机制。它的基本原理是,当一个线程第一次访问一个同步块时,如果此时没有其他线程竞争该锁,那么这个锁就会偏向于这个线程,以后只要这个线程再次访问这个同步块,就不需要再进行锁的竞争,可以直接进入同步代码块执行,大大提高了锁的性能。
可偏向状态(Biased State)
当一个线程首次尝试获取某个对象的锁时,如果该对象没有被其他线程锁定过,那么JVM可能会决定使用偏向锁。这意味着锁的标记字段(mark word)会被更新以包含线程ID,并且锁标志位会被设置为表示偏向锁的状态(通常是01)。一旦一个对象被偏向于某个线程,只要同一个线程再次试图获取该对象的锁,它就可以直接访问对象而无需额外的同步开销。
不可偏向状态
如果一个已经被偏向于特定线程的对象被另一个线程尝试锁定,那么偏向锁就会失效,对象会变成不可偏向状态。此时,JVM会撤销偏向,并将对象的锁升级到下一个更重的级别,通常是轻量级锁(如果条件允许的话)。这种状态意味着任何线程现在都需要通过更复杂的同步机制(如轻量级锁或重量级锁)来获取对象的锁。
偏向锁的设计是为了减少无竞争情况下的同步开销。但是,如果多个线程频繁地竞争同一个对象的锁,偏向锁可能会成为性能瓶颈,因为它会导致锁升级,增加了额外的同步成本。
如何到达不可偏向状态
- 对象创建初期:当一个对象刚被创建时,它处于不可偏向状态,因为没有任何线程曾经访问过它。
- 锁竞争:如果有多个线程同时尝试获取同一个对象的锁,那么偏向锁会失效,对象会从偏向锁状态转变为轻量级锁或重量级锁,进入不可偏向状态。
- 锁撤销:当持有偏向锁的线程退出同步块或终止时,偏向锁需要被撤销,使对象回到不可偏向状态。撤销偏向锁时,JVM会检查是否有其他线程曾经竞争过该锁,如果有的话,对象会升级到更高层次的锁。
- 显式禁用:可以通过JVM参数显式禁用偏向锁。例如,使用-XX:-UseBiasedLocking参数可以完全禁用偏向锁机制。
不可偏向状态的作用
不可偏向状态确保了锁的公平性和一致性。当多个线程试图获取同一个锁时,偏向锁会失效,所有线程都将有机会公平地竞争锁资源。此外,当偏向锁被撤销时,对象回到不可偏向状态,可以避免潜在的线程饥饿问题。
实现细节
在Mark Word中,有一组位用于表示锁的状态。在不可偏向状态时,这些位会设置为特定的值,表明对象目前没有偏向任何线程,也没有线程持有该锁。当线程尝试获取锁时,它会检查Mark Word中的这些位,以确定是否可以偏向于当前线程,或者是否需要进入更复杂的锁获取流程。
轻量级锁
轻量级锁的锁状态为00
升级为轻量级锁条件:
- 对象不可偏向,跳过偏向锁直接使用轻量级锁。
- 对象可偏向,但偏向加锁失败(存在线程竞争)。
- 对象获取调用hashCode后加锁。
- 对象已升级为重量级锁后,锁降级只能降级为轻量级锁,无法降级为偏向锁。
轻量级锁会在线程的栈帧中开辟一个锁记录区域,将当前对象的头部保存在锁记录区域中,将锁记录区域的地址保存到当前对象头部。
对象不可偏向直接升级到轻量锁
csharp
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.gc();
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(("进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
python
未进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1) //不可偏向
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000700002dc99c0 (thin lock: 0x0000700002dc99c0) //轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
对象可偏向,但偏向加锁失败(存在线程竞争)
scss
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
System.gc();
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(("进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
System.out.println(("thread1 进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
},"thread1");
thread1.start();
thread1.join();
System.out.println("主线程再次查看锁对象,MarkWord为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(("主线程再次进入同步块,MarkWord 为: "));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
yaml
未进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000000000000d (biasable; age: 1) //可偏向状态,后三位101,线程id=0,gc=1
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fbf6e00900d (biased: 0x0000001fefdb8024; epoch: 0; age: 1) //已偏向状态,最后三位101,且线程id不为空,gc=1
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread1 进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000bb5fa40 (thin lock: 0x000070000bb5fa40) //轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次查看锁对象,MarkWord为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1) // 无锁,gc age=1
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007000097c89b8 (thin lock: 0x00007000097c89b8) // 由于对象不可偏向,主线程再次进入同步块,自然就会用轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向后调用hashCode方法升级为轻量级锁
scss
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
System.gc();
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
synchronized (o) {
System.out.println(("进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
python
未进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000000000000d (biasable; age: 1) //可偏向状态,后三位101,线程id=0,gc=1
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000da7c9c0 (thin lock: 0x000070000da7c9c0) //轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
重量级锁
轻量级锁自循环一定次数后一致获取不到锁,则升级为重量级锁条件。自旋次数默认为10次,可以通过-XX:PreBlockSpin配置修改次数。
csharp
import org.openjdk.jol.info.ClassLayout;
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread.sleep(5000);
System.out.println("未进入同步块,MarkWord 为:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(("thread1 进入同步块,MarkWord 为:"));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}, "thread1");
thread1.start();
synchronized (o) {
Thread.sleep(1000);
System.out.println(("主线程再次进入同步块,MarkWord 为: "));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
thread1.join();
}
}
kotlin
未进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) //可偏向状态
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fed3801f75a (fat lock: 0x00007fed3801f75a) ////重量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread1 进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fed3801f75a (fat lock: 0x00007fed3801f75a) //重量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
重量级锁降级
当重量级锁解锁后就会进行锁降级,锁降级只能降级为轻量锁,无法再使用偏向锁。看好了哈,是解锁后....
scss
import org.openjdk.jol.info.ClassLayout;
public class ClockTest {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
final Object o = new Object();
Thread thread = new Thread() {
@Override
public void run() {
synchronized (o) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
};
thread.start();
synchronized (o) {
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
thread.join();
Thread.sleep(5000);
System.out.println("------------");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
yaml
Connected to the target VM, address: '127.0.0.1:61934', transport: 'socket'
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fabc18543fa (fat lock: 0x00007fabc18543fa) //重量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fabc18543fa (fat lock: 0x00007fabc18543fa) //重量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) //不可偏向状态
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007000084349b0 (thin lock: 0x00007000084349b0) //轻量级锁
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Disconnected from the target VM, address: '127.0.0.1:61934', transport: 'socket'
使用JMH 对Synchronized做基准测试
java
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class SynchronizedNormalBenchmark {
public static class Test {
public synchronized void doSth() {
}
public void doSthCommon() {
}
}
Test test = new Test();
@Benchmark
public void synchronizedJob() {
test.doSth();
}
@Benchmark
public void noSynchronizedJob() {
test.doSthCommon();
}
public static void main(String[] args) throws Exception{
Options opts = new OptionsBuilder()
.include(SynchronizedNormalBenchmark.class.getSimpleName())
.resultFormat(ResultFormatType.CSV)
.build();
new Runner(opts).run();
}
}
基准测试结果
bash
Benchmark Mode Cnt Score Error Units
SynchronizedNormalBenchmark.noSynchronizedJob avgt 10 0.538 ± 0.035 ns/op
SynchronizedNormalBenchmark.synchronizedJob avgt 10 17.093 ± 1.783 ns/op
通过Score可以发现有锁平均耗时是无锁的平均耗时的30多倍。