synchronized原理和锁膨胀过程
作者:互联网
对象头
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。
下面介绍对象头:
-
普通对象头:Mark Word + Kclass Word
-
数组对象头:Mark Word + Kclass Word + 数组长度
解释关键名称 -
Mark Word:存储运行时的数据,如hash、分代年龄age、是否为可偏向状态、锁标志位
-
Kclass Word(Klass Pointer):指向方法区的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类
64位系统
锁状态 | 56bit | 1bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|---|
54bit | 2bit | 是否偏向锁 | 锁标志位 | |||
无锁态 | 对象hashcode | 0 | 分代年龄 | 0 | 01 | |
偏向锁 | Thread Id | Epoch | 0 | 分代年龄 | 1 | 01 |
特殊的可偏向锁 | 空 | 1 | 01 | |||
轻量级锁 | 指向栈中锁的记录指针 | 00 | ||||
重量级锁 | 指向互斥量(Monitor初始物理地址)的指针 | 10 | ||||
GC | 空 | 11 |
对象头的参数说明
参数 | 说明 |
unused | 未使用位,可以说是预留位 |
identity_hashcode | 对象标识hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中 |
age | 分代年龄,固定4位 |
biased_lock | 是否开启偏向状态(0--关闭,1--开启),固定1位 |
lock | 锁标志位(01--无锁或者偏向锁,00--轻量级锁,10--重量级锁),固定2位 |
thread | 持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。 偏向锁才有,固定54位 |
epoch | 偏向时间戳。偏向锁才有,固定占2位 |
ptr_to_lock_record | 指向栈中锁记录的指针。轻量级锁才有,固定62位 |
ptr_to_heavyweight_monitor | 指向线程Monitor的指针。重量级锁才有,固定62位 |
偏向锁状态
- 匿名偏向(Anonymously biased)
在此状态下thread_ptr为NULL(0),意味着还没有线程偏向于这个锁对象。第一个试图获取该锁的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。这是允许偏向锁的类对象的初始状态。 - 可重偏向(Rebiasable)
在此状态下,偏向锁的epoch字段是无效的(与锁对象对应的klass的mark_prototype的epoch值不匹配)。下一个试图获取锁对象的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。在批量重偏向的操作中,未被持有的锁对象都被至于这个状态,以便允许被快速重偏向。 - 已偏向(Biased)
这种状态下,thread ptr非空,且epoch为有效值——意味着其他线程正在只有这个锁对象。 - 批量撤销
当class(类)偏向撤销超过40次,会触发批量撤销,禁用偏向 - 批量重偏向
当一个Class(类),被单独撤销偏向20次时,将会触发批量重偏向
轻量级锁获取锁的过程
获取锁过程设计到的知识点:
- Lock Record
线程的栈中开辟的锁记录空间 - owner
对象头的指针 - Displaced Mark Word
用于储存锁对象目前的Mark Word的拷贝(官方把这份拷贝加了个Displaced前缀,即Displaced Mark Word)。
-
在线程进入同步方法、同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为"01"状态,是否为偏向锁为"0"),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Recored)的空间,用于储存锁对象目前的Mark Word的拷贝(官方把这份拷贝加了个Displaced前缀,即Displaced Mark Word)。
-
将对象头的
Mark Word
拷贝到线程的锁记录(Lock Recored)中。 -
拷贝成功后,虚拟机将使用
CAS
操作(如果对象的Mark Word与当代女线程栈中的Displaced Mark Word一致则认为CAS成功)尝试将对象的Mark Word
更新为指向Lock Record
的指针。如果这个更新成功了,则执行步骤4
,否则执行步骤5
。 -
更新成功,这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位将转变为"00",即表示此对象处于轻量级锁的状态。。
-
更新失败,虚拟机首先会检查对象的
Mark Word
是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行,否则说明这个锁对象已经被其其它线程抢占了。进行自旋执行步骤3
,如果自旋结束仍然没有获得锁,轻量级锁就需要膨胀为重量级锁,锁标志位状态值变为"10",Mark Word中储存就是指向monitor
对象的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
释放锁的过程:
- 使用
CAS
操作将对象当前的Mark Word
和线程中复制的Displaced Mark Word
替换回来(依据Mark Word
中锁记录指针是否还指向本线程的锁记录),如果替换成功,则执行步骤2
,否则执行步骤3
。 - 如果替换成功,整个同步过程就完成了,恢复到无锁的状态(01)。
- 如果替换失败,说明有其他线程尝试获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
无锁分析
如果是jdk1.6及之后默认开启的话,答案1:JVM开启4s前创建的对象是无锁状态的,此时这些对象只可以升级为轻量级锁和重量级锁(不能转为偏向锁);答案2:4s之后创建的对象默认是无锁可偏向的,可以理解成特殊的无锁状态,这个特殊的无锁状态只可以转为偏向锁,并且只有获得了偏向锁后,进行撤销偏向锁或者直接升级为轻量级锁,后面还可以升级为重量级锁。
偏向锁
偏向锁作用:就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
获取锁的过程:
- 检查
Mark Word
是否为可偏向锁的状态,即是否偏向锁即为1即表示支持可偏向锁,否则为0表示不支持可偏向锁。 - 如果是可偏向锁,则检查
Mark Word
储存的线程ID
是否为当前线程ID
,如果是则执行同步块,否则执行步骤3
。 - 如果检查到
Mark Word
的ID
不是本线程的ID
,则通过CAS
操作去修改线程ID
修改成本线程的ID
,如果修改成功则执行同步代码块,否则执行步骤4
。 - 当拥有该锁的线程到达安全点之后,挂起这个线程,升级为轻量级锁。
锁释放的过程:
- 有其他线程来获取这个锁,偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
- 等待全局安全点(在这个是时间点上没有字节码正在执行)。
- 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态,否则设置为被锁定状态。如果锁对象处于无锁状态,则恢复到无锁状态(01),以允许其他线程竞争,如果锁对象处于锁定状态,则挂起持有偏向锁的线程,并将对象头
Mark Word
的锁记录指针改成当前线程的锁记录,锁升级为轻量级锁状态(00)。
实战代码
//==================偏向锁实战start=====================
//四秒内创建的对象为:无锁不可偏向,即001
public static void testFourSeconds() {
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
}
//代码测试一:让主线程睡眠四秒以上,四秒内创建的对象为:无锁可偏向,即101
public static void testMoreFourSeconds() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
}
//代码测试二:不让主线程睡眠,无锁不可偏向001,然后再对其进行加锁----升级为轻量锁000
public static void test1() throws InterruptedException {
//Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println("当前线程拿到锁,升级为轻量锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.start();
}
/**
* 代码测试三:对象调用hashCode方法后,将会进行撤销偏向锁操作,变成无锁不可偏向状态,之后也不能使用偏向锁。
* 调用hashCode后将撤销偏向锁,并将hashCode值存进对象头。
* @throws InterruptedException
*/
public static void testHashCode() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
// System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.start();
Thread.sleep(5000);//睡眠,让线程打印完对象头
System.out.println(a.hashCode());
System.out.println(Integer.toHexString(a.hashCode()));
System.out.println("对象A调用hashCode方法后的对象头状态");
System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头信息
}
/**
* 代码测试四:3、偏向的线程不存活,开启了重偏向,将会将对象头设置成无锁可偏向的状态,然后重偏向线程,拿到偏向锁。
* 代码说明:线程1启动拿到偏向锁,给主线程个睡眠时间,等线程1结束了后再启动线程2,线程2拿的是可重偏向,就是偏向锁重新偏向了线程2。测试上述可能
*/
public static void test4() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("测试可重偏向");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
thread1.join();
Thread.sleep(6000);
thread2.start();
}
//==================偏向锁实战end=====================
轻量锁
//==================轻量锁start=====================
/**
* JVM启动4s前创建的对象为无锁不可偏向状态,当第一个线程拿锁时,就直接升级为轻量级锁
* @throws InterruptedException
*/
public static void lightWeightTest1() throws InterruptedException {
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println("当前线程拿到锁,升级为轻量锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.start();
}
/**
* 偏向的线程1存活,不在执行同步代码,线程2进来拿锁直接升级为轻量级锁
* @throws InterruptedException
*/
public static void lightWeightTest2() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("thread2拿锁,此时偏向锁thread1存活不再执行同步代码块,锁升级成轻量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
thread1.join();
thread2.start();
}
/**
* 代码测试三:偏向的线程1存活,且在执行同步代码,线程2进来拿锁竞争升级成轻量锁升级重量级锁
*/
public static void lightWeightTest3() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
try {
Thread.sleep(7000); //让线程存过且保持在代码块
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("thread2拿锁,产生资源竞争,导致thread1升级为轻量级锁,线程2自旋拿锁超时,升级为重量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
Thread.sleep(7000);
thread2.start();
}
/**
* 代码测试四:当拿到轻量级锁的线程2执行完毕,不存活的时候就会执行解锁过程
*/
public static void lightWeightTest4() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("thread2拿到锁,thread1存活,升级为轻量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
thread1.join();
thread2.start();
Thread.sleep(6000);//休眠让线程thread2结束运行,轻量级锁解锁为无锁不可偏向状态
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
//==================轻量锁end=====================
重量锁
//==================重量级锁测试start=====================
/**
* 代码测试一:两个线程资源竞争,直接升级为重量级锁。
* 线程1在执行时,是拿到了偏向锁,但是输出对象头信息大概需要3s中,就是同步代码需要执行时间比较长,所以当线程2来拿锁时,
* 线程1还在执行,就造成了锁的竞争,直接升级轻量级锁,升级重量级锁的过程。
*/
public static void weightTest1() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,升级为偏向锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("thread2拿到锁,thread1存活,升级为轻量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
thread2.start();
}
/**
* 代码测试二:当线程执行完毕,不存活后,重量级锁解锁成无锁不可偏向状态(解锁后拿锁又是轻量级、重量那些操作)
*/
public static void weightTest2() throws InterruptedException {
Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
A a = new A();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,产生资源竞争,直接升级为重量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread());
System.out.println("当前线程拿到锁,产生资源竞争,直接升级为重量级锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
thread1.setName("my is thread1");
thread2.setName("my is thread2");
thread1.start();
thread2.start();
Thread.sleep(6000);
System.out.println("重量级锁解锁,线程不存活后,解锁成无锁不可偏向状态");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
//==================重量级锁测试end=====================
Monitor
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
Contention List: 竞争队列,所有请求锁的线程首先被放在这个竞争队列中
Entry List: Contention List中那些有资格成为候选资源的线程被移动到Entry List中
Wait Set: 哪些调用wait方法被阻塞的线程被放置在这里
OnDeck: 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
Owner: 当前已经获取到所资源的线程被称为Owner
过程:
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck线程的锁资源。
synchronized锁的升级过程
三种锁的优缺点比较
标签:Thread,synchronized,System,线程,println,原理,膨胀,偏向,out 来源: https://blog.csdn.net/qq_32470693/article/details/116176348