(SUB)对象头及sync锁升级详解

 image.png

MarkWord

在这里插入图片描述


在这里插入图片描述

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_locklock一起,表达锁状态

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,CMS并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中
thread持有偏向锁的线程ID

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor(互斥量,即重量级锁)的指针

锁升级

通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。JVM内部为提高效率,不会在一开始就使用重量级锁。而是存在锁升级机制

        1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是上图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

        2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如上图第二种情形。

        3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

        4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

问题1:为什么要进行锁升级?锁了就锁了,不就要加锁么?

首先明确早起jdk1.2效率非常低。那时候syn就是重量级锁,申请锁必须要经过操作系统老大kernel进行系统调用,入队进行排序操作,操作完之后再返回给用户态。

内核态:用户态如果要做一些比较危险的操作直接访问硬件,很容易把硬件搞死(格式化,访问网卡,访问内存干掉、)操作系统为了系统安全分成两层,用户态和内核态 。申请锁资源的时候用户态要向操作系统老大内核态申请。Jdk1.2的时候用户需要跟内核态申请锁,然后内核态还会给用户态。这个过程是非常消耗时间的,导致早期效率特别低。有些jvm就可以处理的为什么还交给操作系统做去呢?能不能把jvm就可以完成的锁操作拉取出来提升效率,所以也就有了锁优化。

问题2:为什么要有偏向锁?

其实这本质上归根于一个概率问题,统计表示,在我们日常用的syn锁过程中70%-80%的情况下,一般都只有一个线程去拿锁,例如我们常使用的System.out.println、StringBuffer,虽然底层加了syn锁,但是基本没有多线程竞争的情况。那么这种情况下,没有必要升级到轻量级锁级别了。偏向的意义在于:第一个线程拿到锁,将自己的线程信息标记在锁上,下次进来就不需要在拿去拿锁验证了。如果超过1个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,其实我认为严格意义上来讲偏向锁并不算一把真正的锁,因为只有一个线程去访问共享资源的时候才会有偏向锁这个情况。
无意使用到锁的场景:


/***StringBuffer内部同步***/
public synchronized int length() {
  return count;
} 
 
//System.out.println 无意识的使用锁
public void println(String x) {
   synchronized (this) {
     print(x);
     newLine();
   }
 }

问题3:为什么jdk8要在4s后开启偏向锁?

也就是说在4s内是没有开启偏向锁的,加了锁就直接升级为轻量级锁了。其实这是一个妥协,明确知道在刚开始执行代码时,一定有好多线程来抢锁,如果开了偏向锁效率反而降低,所以上面程序在睡了5s之后偏向锁才开放。为什么加偏向锁效率会降低,因为中途多了几个额外的过程,上了偏向锁之后多个线程争抢共享资源的时候要进行锁升级到轻量级锁,这个过程还的把偏向锁进行撤销在进行升级,所以导致效率会降低。为什么是4s?这是一个统计的时间值。

当然我们是可以禁止偏向锁的,通过配置参数-XX:-UseBiasedLocking = false来禁用偏向锁。jdk15之后默认已经禁用了偏向锁。本文是在jdk8的环境下做的锁升级验证。

问题4:什么情况下轻量级锁要升级为重量级锁呢?

首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它carry不了的情况下才会升级为重量级。那么什么情况下轻量级锁会carry不住。1、如果线程数太多,比如上来就是10000个,那么这里CAS要转多久才可能交换值,同时CPU光在这10000个活着的线程中来回切换中就耗费了巨大的资源,这种情况下自然就升级为重量级锁,直接叫给操作系统入队管理,那么就算10000个线程那也是处理休眠的情况等待排队唤醒。2、CAS如果自旋10次依然没有获取到锁,那么也会升级为重量级。

总的来说2种情况会从轻量级升级为重量级,10次自旋或等待cpu调度的线程数超过cpu核数的一半,自动升级为重量级锁。看服务器CPU的核数怎么看,输入top指令,然后按1就可以看到。
————————————————
版权声明:本文为CSDN博主「阿里云云栖号」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yunqiinsight/article/details/118416218

问题5:都说syn为重量级锁,那么到底重在哪里?

JVM偷懒把任何跟线程有关的操作全部交给操作系统去做,例如调度锁的同步直接交给操作系统去执行,而在操作系统中要执行先要入队,另外操作系统启动一个线程时需要消耗很多资源,消耗资源比较重,重就重在这里。

Klass Word(类指针)

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

数组长度

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

转载自:Java对象结构与锁实现原理及MarkWord详解_打印锁对象的mark word_阿珍爱上了阿强?的博客-CSDN博客

synchronized

实现

JVM 字节码层面:

 Java 虚拟机中的synchronized基于进入和退出Monitor对象(也称为管程或监视器锁)实现,无论显式同步(synchronized作用在同步代码块,有明确的 monitorenter 和 monitorexit 指令) 还是隐式同步(synchronized作用在方法区,调用指令ACC_SYNCHRONIZED 标志)都是使得Monitor对象里面的count计数期增加或者减少来实现。重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的

Thread的等待唤醒机制与synchronized关键字

彻底理解Java并发编程之Synchronized关键字实现原理剖析 - 知乎 (zhihu.com)

所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }

ACC_SYNCHRONIZED
monitorenter monitorexit

使用synchronized的例子:
在这里插入图片描述

生成的JVM指令:


在这里插入图片描述

汇编层:

synchronized的汇编实现为lock cmpxchg指令

1-4-10-9

OS和硬件层面

synchronized的底层是使用操作系统的mutex lock实现的

Mutex Lock

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

Java使用字节码和汇编语言同步分析volatile,synchronized的底层实现_java hsids_深度Java的博客-CSDN博客

重入

可重入锁

作用域

非静态方法:

锁对象实例,在synchronized修饰的方法间是相斥的,调用时都要先获取到对象锁。仅执行非synchronized 代码块时不会阻塞。

public class Test{
    public synchronized void TestSyn(){...}
    public synchronized void TestSyn_One(){...}
}

静态方法:

锁Class实例,在所有synchronized修饰的静态方法间是相斥的。仅执行非synchronized 代码块时不会阻塞。

在jvm中class加载到内存会生成一个唯一class对象指向内存中class这段代码,所以锁class其实就是锁了个特殊的class对象(class对象在同一个classloader加载器空间是单例,即使两个加载器同时生成了class的两个对象,这两个classloader的class也是无法互相访问的,所以只要用到了class对象就可认为单例)

public class Test{  
    public static synchronized void TestSyn(){...}  
    public static synchronized void TestSyn_One{...}  
} 

非静态代码块:

synchronized (this) 锁同步代码块

public void test(){
        synchronized (obj) {
            // 代码块
        }
    }

非静态代码块跟非静态方法一致。多了一点是,可以指定锁定的对象为非当前类实例对象。

静态代码块:

跟静态方法一致。多了的就是,锁定的类可以为任意指定类(静态方法只能锁定当前类)。

static synchronized(XXX.class){  
    // 代码块
}

备注:

synchronized 不能继承,父类的 synchronized 方法被子类重写后默认不是 synchronized 的,必须显示指定

synchronized锁升级

大部分概念前面mark world已经提过,这里具体讲下锁升级每个步骤。

1.偏向锁

偏向锁的获取和撤销逻辑

1. 首先获取锁对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)

 2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord

a) 如果 cas 成功,那么 markword 就会变成偏向锁的状态。 表示已经获得了锁对象的偏向锁,接着执行同步代码块

b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向 锁的线程,并且把它持有的锁升级为轻量级锁(这个 操作需要等到全局安全点,也就是没有线程在执行字 节码)才能执行

3. 如果是已偏向状态,需要检查 markword 中存储的 ThreadID 是否等于当前线程的 ThreadID

a) 如果相等,不需要再次获得锁,可直接执行同步代码 块

b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

 偏向锁的撤销

 偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程 中,发现 cas 失败也就是存在线程竞争时,直接把被偏向 的锁对象升级到被加了轻量级锁的状态。

 对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向

2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过 jvm 参数 UseBiasedLocking 来设置开启或关闭偏向锁,jdk1.6后默认开启

2.轻量级锁

升级为轻量级锁的过程MarkWorld的相应变化:

1. 线程在自己的栈桢中创建锁记录 LockRecord。

2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。

3. 将锁记录中的 Owner 指针指向锁对象。

4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

轻量锁在加锁的过程用到了自旋

自旋锁详见:CAS详解(compare and swap)_cas自旋几次_weixin_38681369的博客-CSDN博客

所以轻量级锁适用同步代码块执行很快的场景(因为执行越快其他线程自旋等待消耗CPU越少)

自旋锁不经过内核在用户态,但占用cpu,因此适用于加锁代码执行时间短,线程数量少的情况

重量级锁经过内核,不占用cpu,因此适用于加锁代码执行时间长,线程数量多的情况

默认情况自旋的次数是 10 次, 可以通过 preBlockSpin 来修改

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并 且持有锁的线程正在运行v中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。

轻量级锁的获取逻辑

1. 首先获取锁对象的 Markword,判断是否处于可偏向状 态。(biased_lock=1、且 ThreadId 为空)

2.若不可偏则设置为轻量级锁

轻量级锁解锁逻辑

1.线程2 CAS 操作超次数,锁膨胀,修改为重量级锁,并进入线程阻塞

2.线程1执行同步体完成,通过 CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的 MarkWord 中

a) 如果成功表示没有竞争。

b) 如果失败(若被线程2升级为重量级锁lock锁状态标记】被改变导致CAS失败),表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

3.重量级锁

通过线程阻塞方式获取释放锁

 我就是厕所所长没错,我就是厕所所长!(一) - 简书 没错,我就是厕所所长!(二) - 简书

注意事项

synchronized(obj)中obj不能用string常量,integer, long等

因为常量在常量池会全局唯一一旦别人锁了自己再锁就出事了

 锁降级

锁降级 - 知乎

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值