锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后 自己进入 Monitor 的 EntryList BLOKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null, 唤醒 EntryList 中 BLOCKED 线程。
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候获取到了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
线程1 (core 1 上) | 对象 Mark | 线程 2 (core 2 上) |
---|---|---|
- | hashcode age bias 01 | - |
访问同步块,获取 monitor | 10 (重量级锁)重量锁指针 | - |
成功(加锁) | 10 (重量级锁)重量锁指针 | - |
执行同步块 | 10 (重量级锁)重量锁指针 | - |
执行同步块 | 10 (重量级锁)重量锁指针 | 访问同步块,获取 monitor |
执行同步块 | 10 (重量级锁)重量锁指针 | 自旋重试 |
执行完毕 | 10 (重量级锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01 (无锁) | 自旋重试 |
- | 10 (重量级锁)重量锁指针 | 成功(加锁) |
- | 10 (重量级锁)重量锁指针 | 执行同步块 |
- | … | … |
- 自旋重试就是不断循环 获取锁,直到循环次数完毕,或者获取到锁
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多旋几次,反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
// 自旋锁实现
public class SpLock implements Lock {
// 使用 Thread 可以保留更多信息, 作为锁使用
private AtomicReference<Thread> cas = new AtomicReference<>();
@Override
public void lock() {
Thread current = Thread.currentThread();
// 不支持锁重入
while (!cas.compareAndSet(null, current)) {
// do
}
}
@Override
public void unlock() {
Thread currentThread = Thread.currentThread();
cas.compareAndSet(currentThread, null);
}
}
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到 对象的 Mark Word头,之后发现线程 ID 是自己的就表示没有竞争,不用重新 CAS (不用锁重入, Lock Record 只有一条记录)。以后只要不发生竞争,这个对象就归该线程所有。
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized ( obj ) {
// 同步块 C
}
}
偏向状态
回忆对象头格式
|-------------------------------------------------------------|-------------------------|
| Mark Word (64 bit) | State |
|-------------------------------------------------------------|-------------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_block:0 | 01 | Normal |
|-------------------------------------------------------------|-------------------------|
| thread:23 | epoch:2 | unused:1 | age:4 | biased_block:1 | 01 | Biased |
|-------------------------------------------------------------|-------------------------|
| ptr_to_lock_record:62 | 00 | LightWeight Locked |
|-------------------------------------------------------------|-------------------------|
| ptr_to_heavyweight_monitor:62 | 10 | HeavyWeight Locked |
|-------------------------------------------------------------|-------------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------------|-------------------------|
一个对象创建时:
-
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age都为 0
-
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟
-
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位 为 001,这时它的 hashcode、age都为 0,第一次用到 hashcode 时才会赋值。
1 )测试延迟特性
2 )测试偏向锁
class Dog {}
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
new Thread(() -> {
System.out.println("synchronized 前");
System.out.println(classLayout.toPrintable());
synchronized (d) {
System.out.println("synchronized 中");
System.out.println(classLayout.toPrintable());
}
System.out.println("synchronized 后");
System.out.println(classLayout.toPrintable());
}, "t1").start();
}
输出:
synchronized 前
mythread.jol.Dog 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 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
synchronized 中
mythread.jol.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 d8 8c 1a (00000101 11011000 10001100 00011010) (445437957)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
synchronized 后
mythread.jol.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 d8 8c 1a (00000101 11011000 10001100 00011010) (445437957)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
前 8 个字节是 Mark Word , 后 8个字节是 Klass 指针
System.out.println(ByteOrder.nativeOrder()); // LITTLE_ENDIAN, 说明数据是 小端序
-
小端序:数据的高位字节存放在地址的高端 低位字节存放在地址低端
-
大端序:数据的高位字节存放在地址的低端 低位字节存放在地址高端 比如一个整形 0x1234567
1 是 高位数据,7是 低位数据。按照小端序 01 放在内存地址的高位。大端序反之。
Little Endian
0x100 | 0x101 | 0x102 | 0x103 | ||
---|---|---|---|---|---|
67 | 45 | 23 | 01 |
Big Endian
0x100 | 0x101 | 0x102 | 0x103 | ||
---|---|---|---|---|---|
01 | 23 | 45 | 67 |
synchronized 前: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
synchronized 中: 00000000 00000000 00000000 00000000 00011010 11010010 00011000 00000101
synchronized 后: 00000000 00000000 00000000 00000000 00011010 11010010 00011000 00000101
处于偏向锁的对象解锁后,线程 id 仍存储与对象头中
3)测试禁用
在上面测试代码运行时添加 VM 参数 -xx:-UseBiasedLocking
禁用偏向锁
输出
synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
synchronized 中 00000000 00000000 00000000 00000000 00011011 11001101 11110011 10101000
synchronized 后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
- 测试 hashCode
- 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
撤销 - 调用对象 hashCode
调用了对象的 hashCode , 但偏向锁的对象 MarkWord 中 存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
hashCode: 21685669
synchronized 前 ... 001
synchronized 中 ... 10
synchronized 后 ... 000
撤销 - 其他线程使用对象
- 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
// 设置 偏向锁 不延迟生效,否则会使用轻量级锁
private static void test2() {
Dog d = new Dog();
new Thread(() -> {
synchronized (d) {
System.out.println(Thread.currentThread().getName() + "->" + ClassLayout.parseInstance(d).toPrintable());
}
synchronized (Test2.class) {
Test2.class.notify();
}
}, "t1").start();
new Thread(() -> {
synchronized (Test2.class) {
try {
Test2.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "->" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
System.out.println(Thread.currentThread().getName() + "->" + ClassLayout.parseInstance(d).toPrintable());
}
System.out.println(Thread.currentThread().getName() + "->" + ClassLayout.parseInstance(d).toPrintable());
}, "t2").start();
}
t1->mythread.jol.Dog object internals:...101
t2->mythread.jol.Dog object internals:...100
t2->mythread.jol.Dog object internals:...00
t2->mythread.jol.Dog object internals:...001
t1->mythread.jol.Dog object internals:...101
t2->mythread.jol.Dog object internals:...100
t2->mythread.jol.Dog object internals:...00
t2->mythread.jol.Dog object internals:...001