synchronized根据竞争的激烈程度不同,实际上会经历如下几个过程:
无锁 --> 偏向锁(偏向锁实际上需要先获得轻量级锁,然后在锁重入时才会执行偏向锁优化) --> 轻量级锁(CAS设置markword+自旋) --> 重量级锁(OS层面,在升级为重量级锁时,若在多核cpu上,会出尝试多次自旋,若还是获取不到锁,才就会膨胀为重量级锁)
其中无锁,偏向锁,轻量级锁都是JVM层面所做的工作; 而重量级锁是OS层面的,这就涉及到用户态到内核态的转换.
轻量级锁连续自旋等待超过一定次数时(JVM默认设置为10次),为了避免CPU占用过高,会升级成重量级锁, 对于重量级锁, 在CPU层面是通过CAS指令来实现的.
synchronized在JVM层面是通过设置对象头来实现上述的锁升级/锁优化的,针对64bit的JVM,其对象头中的mark word格式如下:
64bit JVM的对象头实际上分为两部分,即Mark Word和Class Pointer, Mark Word占8字节,Class Pointer占4字节.
了解完以上理论知识,下面来编码验证,这里使用org.openjdk.jol库来解析类对象,不过org.openjdk.jol解析出的数据格式不太友好,特别对于小端机器来说,其二进制是反过来的(基本上x86 CPU以小端为主),这里我封装了解析对象头的函数:
/**
* 获得二进制数据
* @param o
* @return
*/
public static String getObjectHeader(Object o){
ByteOrder order=ByteOrder.nativeOrder();//字节序
String table=ClassLayout.parseInstance(o).toPrintable();
Pattern p=Pattern.compile("(0|1){8}");
Matcher matcher=p.matcher(table);
List header=new ArrayList<>();
while(matcher.find()){
header.add(matcher.group());
}
//小端机器,需要反过来遍历
StringBuilder sb=new StringBuilder();
if(order.equals(ByteOrder.LITTLE_ENDIAN)){
Collections.reverse(header);
}
for(String s:header){
sb.append(s).append(" ");
}
return sb.toString().trim();
}
/**
* 针对64bit jvm的解析对象头函数
* 在64bit jvm中,对象头有两个部分: Mark Word和Class Pointer, Mark Word占8字节,Class Pointer占4字节
* @param s 对象头的二进制形式字符串(每8位,使用一个空格分开)
*/
public static void parseObjectHeader(String s){
String[] tmp=s.split(" ");
System.out.print("Class Pointer: ");
for(int i=0;i<4;++i){
System.out.print(tmp[i]+" ");
}
System.out.println("\nMark Word:");
if(tmp[11].charAt(5)=='0'&&tmp[11].substring(6).equals("01")){//0 01无锁状态,不考虑GC标记的情况
//notice: 无锁情况下mark word的结构: unused(25bit) + hashcode(31bit) + unused(1bit) + age(4bit) + biased_lock_flag(1bit) + lock_type(2bit)
// hashcode只需要31bit的原因是: hashcode只能大于等于0,省去了负数范围,所以使用31bit就可以存储
System.out.print("\thashcode (31bit): ");
System.out.print(tmp[7].substring(1)+" ");
for(int i=8;i<11;++i) System.out.print(tmp[i]+" ");
System.out.println();
}else if(tmp[11].charAt(5)=='1'&&tmp[11].substring(6).equals("01")){//1 01,即偏向锁的情况
//notice: 对象处于偏向锁的情况,其结构为: ThreadID(54bit) + epoch(2bit) + unused(1bit) + age(4bit) + biased_lock_flag(1bit) + lock_type(2bit)
// 这里的ThreadID是持有偏向锁的线程ID, epoch: 一个偏向锁的时间戳,用于偏向锁的优化
System.out.print("\tThreadID(54bit): ");
for(int i=4;i<10;++i) System.out.print(tmp[i]+" ");
System.out.println(tmp[10].substring(0,6));
System.out.println("\tepoch: "+tmp[10].substring(6));
}else{//轻量级锁或重量级锁的情况,不考虑GC标记的情况
//notice: JavaThread*(62bit,include zero padding) + lock_type(2bit)
// 此时JavaThread*指向的是 栈中锁记录/重量级锁的monitor
System.out.print("\tjavaThread*(62bit,include zero padding): ");
for(int i=4;i<11;++i) System.out.print(tmp[i]+" ");
System.out.println(tmp[11].substring(0,6));
System.out.println("\tLockFlag (2bit): "+tmp[11].substring(6));
System.out.println();
return;
}
System.out.println("\tage (4bit): "+tmp[11].substring(1,5));
System.out.println("\tbiasedLockFlag (1bit): "+tmp[11].charAt(5));
System.out.println("\tLockFlag (2bit): "+tmp[11].substring(6));
System.out.println();
}
定义一个对象:
static class Obj{
int i=0;
}
下面来验证,注意一点,测试synchronized锁升级时,若要立即查看测试情况,需要禁止偏向锁延迟: -XX:BiasedLockingStartupDelay=0; 开启偏向锁: -XX:+UseBiasedLocking=0, 不过从jdk6开始就默认开启了,不开启的话就是先使用轻量级锁.
测试偏向锁:
@Test
public void testBiasedLock(){
//需要禁止偏向锁延迟: -XX:BiasedLockingStartupDelay=0
Obj o=new Obj();
parseObjectHeader(getObjectHeader(o));
synchronized (o){
parseObjectHeader(getObjectHeader(o));
}
}
输出:
Class Pointer: 11111000 00000001 10100001 10100100
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000000 00000000 000000
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10100001 10100100
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000011 00110111 100110
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
可以看出,偏向锁标志位为1,锁标志位01,即处于偏向锁状态,即使下一步使用了synchronized,仍然处于偏向锁状态
测试轻量级锁:
@Test
public void testLightLock(){
//需要禁止偏向锁延迟: -XX:BiasedLockingStartupDelay=0
Obj o=new Obj();
parseObjectHeader(getObjectHeader(o));
//升级为轻量级锁,因为下面的线程又占用了o,注意是上面先执行完后,才开启下面这个线程,因此不会升级为重量级锁
new Thread(()->{
synchronized (o){
parseObjectHeader(getObjectHeader(o));
}
}).start();
}
输出:
Class Pointer: 11111000 00000001 10100001 01011001
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000000 00000000 000000
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10100001 01011001
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000010 10101110 100110
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10100001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00100010 00011011 11110110 111000
LockFlag (2bit): 00
可以看出,由偏向锁101升级为了轻量级锁00
下面来测试重量级锁,要测试重量级锁,必须要产生锁竞争:
@Test
public void testHeavyLock(){
//这里没有禁止偏向锁延迟,所以最初是无锁状态
Obj o=new Obj();
parseObjectHeader(getObjectHeader(o));
synchronized (o){
parseObjectHeader(getObjectHeader(o));
}
//重量级锁
for(int i=0;i<2;++i)//线程数大于1时(交错执行),会升级成重量级锁
new Thread(()->{
synchronized (o){
parseObjectHeader(getObjectHeader(o));
}
}).start();
}
输出:
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
hashcode (31bit): 0000000 00000000 00000000 00000000
age (4bit): 0000
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00000011 00000111 11100010 100010
LockFlag (2bit): 00
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00011101 11110111 00111010 100010
LockFlag (2bit): 10
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00011101 11110111 00111010 100010
LockFlag (2bit): 10
可以看出这里是从无锁(001)到轻量级锁(00)到重量级锁(10),这里没有出现偏向锁的原因是没有禁止偏向锁延迟,所以没有测试到偏向锁,作为对比,开启禁止选项后,其输出为:(可以看出直接从偏向锁升级到重量级锁)
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000000 00000000 000000
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
ThreadID(54bit): 00000000 00000000 00000000 00000000 00000011 00111001 100110
epoch: 00
age (4bit): 0000
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00011110 00101010 01011010 010010
LockFlag (2bit): 10
Class Pointer: 11111000 00000001 10110001 01011001
Mark Word:
javaThread*(62bit,include zero padding): 00000000 00000000 00000000 00000000 00011110 00101010 01011010 010010
LockFlag (2bit): 10
这里额外提醒一下,如果使用了对象的hashcode方法,那么将禁止偏向锁,原因就是hashcode需要占用对象头的31bit空间,结果就导致没有空间存储偏向锁标记了,如下图所示:
对于synchronized的几种使用方式,见我的文章:https://blog.csdn.net/lovemylife1234/article/details/102733422
更多Java多线程相关代码见我的github: https://github.com/xycodec/Java-MultiThread-Study