0%

java | synchronized 优化 偏向锁

轻量级锁在没有竞争的时候(就自己这个线程),每次冲突依然需要执行 CAS 操作。

Java6 引入偏向锁进行优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的表示没有竞争,就不用重新 CAS。以后只要不发生竞争,这个对象就归线程所有。

偏向锁的使用场景是,当多线程中,同步代码块只有一个线程操作的时候,才会考虑用这个。

根据 难搞的偏向锁终于被 Java 移除了 表明 JDK 15 之前,偏向锁默认是 enabled,从 15 开始,默认就是 disabled,除非显示的通过 UseBiasedLocking 开启。

偏向状态

来自于

1
2
3
4
5
6
7
8
9
10
11
12
13
|-----------------------------------------------|-------------------|
| Mark Word(32 bits) | State |
|-------------------------------------------------------------------|
| hashcode: 25 |age: 4 | biased_lock:0| 01| Normal |
|-------------------------------------------------------------------|
| thread:23 |epoch:2 |age: 4 | biased_lock:1| 01| Biased |
|-------------------------------------------------------------------|
| ptr_to_lock_record: 30 | 00| Lightweight Locked|
|-------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:30| 10| Heavyweight Locked|
|-------------------------------------------------------------------|
| | 11| Marker for GC |
|-------------------------------------------------------------------|

一个对象创建时

  • 如果开启了偏向锁「默认开启」,那么,对象创建后,mark word 值为 0x05 即最后 3 位是 101,这时他的 threadepochage 都是 0
    • 虽然,默认开启,但是,启动后是延迟开启,所以,输出的值是 0x01
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么,对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcodeage都是 0,第一次用 hashcode 才会赋值

查看偏向锁

需要借助一个第 3 方的包。

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

就用上面的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.redisc;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

@Data
class User {
}

@Slf4j(topic = "c.Test")
public class Test {
public static void main(String[] args) throws Exception {
User user1 = new User();
log.debug(ClassLayout.parseInstance(user1).toPrintable());

TimeUnit.SECONDS.sleep(5);
log.debug(ClassLayout.parseInstance(user1).toPrintable());

}
}

根据 Java利用 ClassLayout 查看对象头 可知,里面的数据应该是倒叙「不过我也没对应起来」。

输出,以 java8 作为编译器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
11:54:18.311 [main] DEBUG c.Test - com.redisc.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 对应输出 0x01
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 22 ef 00 f8 (00100010 11101111 00000000 11111000) (-134156510)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

11:54:23.320 [main] DEBUG c.Test - com.redisc.User 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) 22 ef 00 f8 (00100010 11101111 00000000 11111000) (-134156510)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

VM 中加上 -XX:BiasedLockingStartupDelay=0 输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
11:51:25.708 [main] DEBUG c.Test - com.redisc.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 对应输出 0x05
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 22 ef 00 f8 (00100010 11101111 00000000 11111000) (-134156510)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

11:51:30.713 [main] DEBUG c.Test - com.redisc.User 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) 22 ef 00 f8 (00100010 11101111 00000000 11111000) (-134156510)
12 4 (loss due to the next object alignment)

总结一下,很多课程说加上延迟后,就从 0x01 变成了 0x05 了,但是,我实际情况不是这样的。其他的倒是一样。

禁用和启用偏向锁 VM 参数

  • -XX:-UseBiasedLocking 可以禁用偏向锁
  • -XX:+UseBiasedLocking 可以启用偏向锁

Hashcode

Monitor 的 Hashcode 只有用的时候才会产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.redisc;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

@Data
class User {
}

@Slf4j(topic = "c.Test")
public class Test {
public static void main(String[] args) throws Exception {
User user1 = new User();
user1.hashCode(); // 只有使用的时候才会出现。
log.debug(ClassLayout.parseInstance(user1).toPrintable());

}
}

输出

1
2
3
4
5
6
12:25:14.995 [main] DEBUG c.Test - com.redisc.User 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) 22 ef 00 f8 (00100010 11101111 00000000 11111000) (-134156510)
12 4 (loss due to the next object alignment)

我们在看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
|-----------------------------------------------|-------------------|
| Mark Word(32 bits) | State |
|-------------------------------------------------------------------|
| hashcode: 25 |age: 4 | biased_lock:0| 01| Normal |
|-------------------------------------------------------------------|
| thread:23 |epoch:2 |age: 4 | biased_lock:1| 01| Biased |
|-------------------------------------------------------------------|
| ptr_to_lock_record: 30 | 00| Lightweight Locked|
|-------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:30| 10| Heavyweight Locked|
|-------------------------------------------------------------------|
| | 11| Marker for GC |
|-------------------------------------------------------------------|

可以看到 state = Biased 的时候,根本没有空存储 hashcode,所以,当你使用 hashcode 的时候,即便你的状态是 Biased 也要转为 Normal 才能存储 Hashcode 进而使用 Hashcode

撤销偏向锁或者偏向状态

一共有两种情况

  • 使用 HashCode
  • 当有其他线程使用偏向锁的时候,会讲偏向锁升级为轻量级锁

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程 T1 的对象仍有机会偏向 T2,重偏向会重置偏向对象的 Thread ID

当撤销偏向锁超过 20 次之后,JVM 会认为撤销频率过大,于是给这些对象加锁时重新偏向至加锁线程。

如果撤销锁阈值超过 40 次,那么,整个类所有的对象都会变成不可偏向的,新建的对象也是不可偏向的。

这个建议看满一航老师的讲解,这里不再多叙述了。

请我喝杯咖啡吧~