代码解释Synchronized锁升级过程

synchronized 用法

//获得的锁是 当前对象
public synchronized void testSynchronized(){}

//获得的锁是 当前对象的Class对象
public synchronized static void testStaticSynchronized(){}

public final Object lock = new Object();
public void test(){
    //获得的锁是 lock对象
    synchronized (lock){}
}

synchronized定义在方法体上生成的字节码会多 ACC_SYNCHRONIZED
定义在代码块上字节码会生成 monitorenter,和两个monitorexit,其中第二个monitorexit是防止程序异常可以看到多了athrow字节码 。通过javap -v 命令查看字节码

javap-synchronized.png

不得不说的Java对象头分析(64位机器)

首先一个Java对象大小由三部分组成:对象头,对象体(一般认为对象中的属性大小),对齐字节。
对象头包括MarkWord(8字节)、Klass Pointer(指向Class对象的指针)
对象如果是数组,对象头额外还会包含4字节的数组长度
下面通过代码演示。代码分析工具使用jol

1.对象中不添加任何属性

public class T{ }
public static void main(String[] args) {
    T t = new T();
    ClassLayout classLayout = ClassLayout.parseInstance(t); //'org.openjdk.jol', name: 'jol-core', version: '0.9'
    System.out.println(classLayout.toPrintable());
}
打印结果
 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        (loss due to the next object alignment)  
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,整个T对象占用16bytes大小,其中对象头(object header)占用12字节
loss due to the next object alignment: 对齐补充,虚拟机要求对象所占用的内存大小必须是8的整数,当不足的时候,对齐补充
2.对象中添加一个属性

public static class T{ 
    long testLong;
}
T类型里面添加一个long属性后,展示效果。同时在TYPE字段也标识了T中的变量属性为long
 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        (alignment/padding gap)                  
     16     8   long T.testLong                                0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到,原本object header占用12字节,long占用8字节。需要对齐补充到8的倍数,所以占用24字节的空间。可以试试int, byte, short 类型自测,也可以证明int占用4字节,short 2字节 ,byte 1字节

3.添加-XX:-UseCompressedOops参数

UseCompressedOops参数用于指针压缩,64位机器上指针占用的大小应该是8字节,但是由于上两次实验得到Klass Pointer 占用4字节。在运行参数中关闭UseCompressedOops,对象头已经更改为占用 16Byte大小。JVM默认开启此参数

T类型里面添加一个byte属性后,展示效果。同时在TYPE字段也标识了T中的变量属性为byte
 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)                           10 35 92 1c (00010000 00110101 10010010 00011100) (479343888)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     1   byte T.testLong                                0
     17     7        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total



4.对象数组

int[] t = new int[1];
ClassLayout classLayout = ClassLayout.parseInstance(t); //'org.openjdk.jol', name: 'jol-core', version: '0.9'
System.out.println(classLayout.toPrintable());
[I 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)                           68 0b 98 1b (01101000 00001011 10011000 00011011) (462949224)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     20     4        (alignment/padding gap)                  
     24     4    int [I.<elements>                             N/A
     28     4        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
对象头里多了4字节,表示数组的长度。这也是为什么获取数组长度复杂度是O(1)的原因,是直接从对象头中取出数组的长度

JOL中的对象头如何阅读

      0     4        (object header)                           05 e8 06 01 (00000101 11101000 00000110 00000001) (17229829)
                                                                               8        7        6        5
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (0)
                                                                                4        3       2        1

读取方式应该是 1-2-3-4-5-6-7-8
00000000 00000000 00000000 00000001 00000001 00000110 11101000 00000101 从尾往前读

理解了对象的组成结构,接下来着重说明对象头中的MarkWord。在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头

64classheader.png

着重查看最后3bit的内容,锁的变换对应了对象头的不同标志

演示代码
1.偏向锁

Thread.sleep(5000); //等待jvm开启偏向锁
T t = new T();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
synchronized (t){
    System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
 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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.mouxf.jol.JolTestObjectDetail$T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e8 06 01 (00000101 11101000 00000110 00000001) (17229829)
      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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到最后三位是 101 代表着偏向锁,至于为什么两次打印都是101而不是一次 001和101?why?

2.轻量级锁

public static void test3() throws Exception{
    Thread.sleep(5000);
    T t = new T();
    print(t);
    for (int i = 0; i < 1; i++) {
        Thread thread = new Thread(() -> {print(t);});
        thread.start();
        thread.join();
    }
}
public static void print(T t) {
    synchronized (t){
        System.out.println(Thread.currentThread().getName());
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
    }
}
main
com.mouxf.jol.JolTestObjectDetail$T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e8 ff 02 (00000101 11101000 11111111 00000010) (50325509)
      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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread-0
com.mouxf.jol.JolTestObjectDetail$T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           d8 f1 1c 1f (11011000 11110001 00011100 00011111) (521990616)
      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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以发现t中最后3位由 101 -> 000 转变为轻量级锁

3.重量级锁

public static void test4() throws Exception{
    T t = new T();
    print(t);
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {print(t);});
        thread.start();
    }
}
由于打印内容较多,只拿出一部分
com.mouxf.jol.JolTestObjectDetail$T object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           fa c8 46 03 (11111010 11001000 01000110 00000011) (54970618)
      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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以发现t中最后3位由 010 转变为重量级

以上就是演示synchronized的锁升级过程,其实可以看出synchronized锁升级过程是 无锁->偏向锁->轻量级锁->重量级锁

synchronized锁升级过程

  • 偏向锁
    1. 检查对象头标志 01 与 是否偏向 0 (对象头最后 3bit)
    2. CAS修改MarkWord获取偏向锁,修改成功后将markword中线程ID指向当前线程A
    3. 当另一个线程B获取对象锁的时候,程序达到安全点后,查看对象头中线程情况。线程A不存活或者线程A不再使用该对象,线程竞争设置偏向锁
    4. 如果线程A继续持有对象锁,暂停当前线程,撤销偏向锁,升级为轻量级锁
  • 轻量级锁
    1. 当前线程开辟一块记录空间(Lock Recored),同时复制Markword
    2. CAS修改MarkWord,修改成功升级为轻量级锁 00
    3. 线程B CAS修改失败自旋,达到自旋次数后还未获得锁则升级为重量级锁

盗个图,总结得很好(侵删)=

synchronized_lock.png

锁的实现原理

每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。

monitor是由ObjectMonitor实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的。Java 程序调用wait(),notfiy(),notfiyAll()方法底层实现也是ObjectMonitor

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0; //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  //_owner指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;  //保护等待队列
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //阻塞在entry上最近可达的线程列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

相关参数

-XX:BiasedLockingStartupDelay=0 偏向锁在Java6 Java7默认是开启的,但是它在程序启动几秒之后在激活,关闭延迟
-XX:-UseBiasedLocking=false 关闭偏向锁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354