JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。在了解这些锁之前,要先了解一下对象头的结构。很多书中介绍过32位的对象头结构,但是对于64的说的很模糊,下面通过工具对64位的结构进行验证。
1. 查看对象结构和大小的工具jol-core:
在maven中添加依赖就可以使用了:
使用:
System.out.println(ClassLayout.parseInstance(user).toPrintable());
在64位jvm中对象头有12个字节,也就是96位,前8个字节被称为一个字宽(markword),后4个字节为一个指针,指向描述原始对象的布局和行为的另一个对象(元对象),被称作klass。
MarkWord | klass |
---|---|
64 | 16 |
下面是对象头的中的Mark Word布局,之后会进行详细的介绍:
锁状态/gc | Mark Word(64 bits) | 是否偏向 | 锁标志位 |
---|---|---|---|
无锁 | unused:25 hashcode:31 unused:1 age:4 | 0 | 01 |
偏向锁 | threadId:54 epoch:2 unused:1 age:4 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针:62 | - | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针:62 | - | 10 |
gc标记 | - | - | 11 |
2. 验证锁状态
2.1 验证无锁状态
可以两种方法得到无锁状态的对象:
(1)默认情况下偏向锁开启(jvm参数为:-XX:+UseBiasedLocking)时,只创建对象并打印对象头,这种情况下偏向锁还未激活。
(2)关闭偏向锁参数,设置jvm参数为:-XX:-UseBiasedLocking。
public class LockTest {
static ObjectLock lock;
public static void main(String[] args) throws Exception {
lock = new ObjectLock();
System.out.println("befre lock");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
class ObjectLock {
String name;
}
输出:
上图中表红的3位,分别为:
是否偏向 | 锁标志位 |
---|---|
0 | 01 |
可能有人感到奇怪,为什么是前3位呢?因为是小端存储,感兴趣的可以网上搜一下“大端存储与小端存储”。这里的是反着的。
通过实验可以看出,如果偏向锁没有开启或者开启了未激活时,新创建的对象是无锁状态。
无锁状态下还有31位的hashcode,但是上面的输出中那31位都是0,这是因为hashcode还没有计算,调用一下hashcode方法就可以看到了。
public class LockTest {
static ObjectLock lock;
public static void main(String[] args) throws Exception {
lock = new ObjectLock();
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
System.out.println("hashcode:: "+Integer.toBinaryString(lock.hashCode()));
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
class ObjectLock {
String name;
}
输出:
可以看到调用hashcode后,对应位置上和对象的hashcode是一样的。
2.2 验证偏向锁状态
是否启用偏向锁可以通过参数(UseBiasedLocking)控制,jdk1.6以后默认都是开启的,但是注意是在程序运行几秒之后才激活。所以,要看偏向锁状态必须保证开启偏向锁参数并且激活偏向锁。
public class LockTest {
static ObjectLock lock;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
lock = new ObjectLock();
System.out.println("befre lock");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
class ObjectLock {
String name;
}
输出:
上图中表红的3位,分别为:
是否偏向 | 锁标志位 |
---|---|
1 | 01 |
就是偏向锁状态。但是其他都为0,这是因为没有对对象进行加锁操作,下面我们进行加锁测试验证threadId+epoch。
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
lock = new ObjectLock();
synchronized (lock) {
// 第一次加锁
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
// 第二次加锁
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
输出:
# 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
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 70 80 c4 (00000101 01110000 10000000 11000100) (-998215675)
4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 70 80 c4 (00000101 01110000 10000000 11000100) (-998215675)
4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 70 80 c4 (00000101 01110000 10000000 11000100) (-998215675)
4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到3次输出是一样因为都是在同一个线程中,而且偏向锁没有明显的锁释放过程,只有锁的撤销。第一次加锁后把线程id写入对象头中,第二次进入同步块时只是验证threadid是否相同,发现相同直接进入。
总结:
(1)当代码进入同步快前,如果为可偏向状态(即对象的初始状态,没有偏向过任何线程,threadid为空),则尝试用 CAS 操作, 将自己的线程 ID 写入MarkWord。如果CAS成功则进入同步快执行代码;如果CAS失败则说明有其他的线程B获取了偏向锁,此时需要撤销 线程 B 获得的偏向锁,将 线程 B 持有的锁升级为轻量级锁。这个过程后面会进行详细介绍。
(2)当代码进入同步快前,如果为已经偏向状态(即上面代码中的过程,对象偏向过一个线程),则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID ,如果相等直接进入代码块,如果不相等撤销偏向锁。
3.偏向锁转为轻量级锁
3.1 偏向锁的撤销过程
其实偏向锁的撤销过程不是恢复到无锁状态,而是偏向锁升级到轻量级锁的过程。撤销偏向锁是发生在安全点(停止所有线程的执行),当到达安全点时通过Mark Word中记录的线程id找到对应的线程:
(1)在当前线程的栈桢(Stack Frame)中创建用于存储锁记录(lock record)的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
(2)然后线程尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向锁记录的指针。
如果成功,当前线程获得锁;如果失败,表示该对象已经被加锁了, 先进行自旋操作, 再次尝试 CAS 争抢, 如果仍未争抢到, 则进一步升级锁至重量级锁。
3.2 偏向锁升级为轻量级锁
其实偏向锁撤销的同时就是升级为轻量锁的过程,下面做一个简单的演示:
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
lock = new ObjectLock();
synchronized (lock) {
// 第一次加锁
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// Thread.sleep(5000);
Thread t1 = new Thread(() -> {
synchronized (lock) {
// 第二次加锁
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
});
t1.start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
输出:
# 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
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 18 00 e8 (00000101 00011000 00000000 11101000) (-402647035)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 18 00 e8 (00000101 00011000 00000000 11101000) (-402647035)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 50 18 f3 01 (01010000 00011000 11110011 00000001) (32708688)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock 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 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
4个输出,第一个是在主线程中加锁,第二个是释放锁后,第三个是新建一个线程加锁,第四个是新建线程释放锁后。
可以看出输出第一、二都是偏向锁,新建一个线程后因为是不同的线程,所以释放偏向锁并升级为轻量级锁,释放锁后恢复为无锁状态(因为锁是不能降级的)。
目前只有这种是轻量级状态的,其他的情况只要是存在竞争就会转化为重量级锁。其实,之前说过,“如果失败,表示该对象已经被加锁了, 先进行自旋操作, 再次尝试 CAS 争抢, 如果仍未争抢到, 则进一步升级锁至重量级锁。”,这个自旋的过程其实很短暂的,实验程序基本是跑不出来的,看到的就是直接到重量级锁了。
3.3 重量级锁
下面程序模拟了线程直接的竞争锁的情况,线程1先获取锁,然后在还没释放锁时线程2进行抢锁。
public static void main(String[] args) throws Exception {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Thread.sleep(5000);
lock = new ObjectLock();
Thread t1 = new Thread(() -> {
System.out.println(df.format(new Date()) + " Thread1 try lock ======> " + ClassLayout
.parseInstance(lock)
.toPrintable());
synchronized (lock) {
System.out.println(
df.format(new Date()) + " Thread1 lock ======> " + ClassLayout
.parseInstance(lock)
.toPrintable());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
df.format(new Date()) + " Thread1:: " + ClassLayout.parseInstance(lock)
.toPrintable());
}
});
Thread t2 = new Thread(() -> {
System.out.println(
df.format(new Date()) + " Thread2 try lock ======> " + ClassLayout
.parseInstance(lock)
.toPrintable());
synchronized (lock) {
System.out.println(
df.format(new Date()) + " Thread2 lock:: " + ClassLayout
.parseInstance(lock)
.toPrintable());
}
});
t1.start();
Thread.sleep(1000);
t2.start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
输出:
2020-09-04 10:51:16:184 Thread1 try lock ======> orderTest.ObjectLock 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 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2020-09-04 10:51:16:953 Thread1 lock ======> orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 c8 90 0e (00000101 11001000 10010000 00001110) (244369413)
4 4 (object header) b0 7f 00 00 (10110000 01111111 00000000 00000000) (32688)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2020-09-04 10:51:17:187 Thread2 try lock ======> orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 c8 90 0e (00000101 11001000 10010000 00001110) (244369413)
4 4 (object header) b0 7f 00 00 (10110000 01111111 00000000 00000000) (32688)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2020-09-04 10:51:19:956 Thread1:: orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5a 69 83 0c (01011010 01101001 10000011 00001100) (209938778)
4 4 (object header) b0 7f 00 00 (10110000 01111111 00000000 00000000) (32688)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2020-09-04 10:51:19:958 Thread2 lock:: orderTest.ObjectLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5a 69 83 0c (01011010 01101001 10000011 00001100) (209938778)
4 4 (object header) b0 7f 00 00 (10110000 01111111 00000000 00000000) (32688)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 java.lang.String ObjectLock.name null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
orderTest.ObjectLock 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 java.lang.String ObjectLock.name null
Instance size: 16 bytes
程序运行时间表:
10:51:16:184 | 10:51:16:953 | 10:51:17:187 | 10:51:19:956 | 10:51:19:958 | 10:51:19:958 |
---|---|---|---|---|---|
t1尝试获取lock 偏向锁 | t1获取到lock 偏向锁 | t2尝试获取lock 偏向锁 | t1由于t2的竞争升级为重量级锁 | t2获取到锁 重量级锁 | 锁释放后 无锁状态 |
3.4 总结
总结一下,其实无锁->偏向锁->轻量级锁->重量级锁的转化过程中没那么复杂,注意记住:
(1)只有一个线程获取锁时,就是偏向锁。
(2)多个线程时,不存在竞争(多个线程顺序执行),轻量级锁。
(3)多个线程存在竞争时重量级锁。
很多时候,偏向锁->轻量级锁->重量级锁的转化过程,基本你只能看见开头和结尾,因为轻量级锁的自旋被优化不会长时间持续进行的,所以你看到的就是偏向锁到重量级锁的过程。