多线程锁

对象头和monitor

对象头

当我们创建一个对象在内存中,一共有三个部分

1、对象头
2、实例数据
3、对齐填充字节

对象头有三部分组成:
普通对象
在这里插入图片描述
数组对象
在这里插入图片描述
因为这里主要讲线程锁有关系,重点介绍Mark word 部分

Mark Word

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同
在32位JVM中是这么存的:
在这里插入图片描述
在64位JVM中是这么存的:
在这里插入图片描述
下面以64位虚拟机来介绍
正常模式:前面25 bit是没有用到的,后31 bit存得是该对象的哈希值,使用synchronized给该对象加锁后,该值会替换到该线程中栈帧创建出来的锁记录中,后4 bit是age,下一个字节表示是否开启偏向锁的锁标志位,biased_lock =1 表示开启,0表示不开启,默认开启偏向锁。

偏向锁模式:前54 bit存的是该线程的id,后2bit存的是epoch偏向锁时间戳,后4bit是age,biased_lock值为表示已上偏向锁,后2bit和正常模式相同

轻量级锁模式:前62 bit存储的是锁记录(该线程中栈帧创建出来的锁记录) 最后2bit 是00

重量级锁模式:前64 bit存储的是锁记录(该线程中栈帧创建出来的锁记录) 最后2bit 是10

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁,具体如何升级得下面再介绍。

klass word(指向类的指针)

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Java对象的类数据保存在方法区。

数组长度

只有数组对象保存了这部分数据。
该数据在32位和64位JVM中长度都是32bit

代码演示

        //偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,
        //可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
        Cat cat = new Cat();
        System.out.println(cat.hashCode());
        System.out.println(ClassLayout.parseInstance(cat).toPrintable());

在这里插入图片描述
因为head前8位是从高位到地位,所以mark word展示为:00000000 00000000 00000000 01100110 11010011 11000110 00010111 00000001

hash code 为:1100110 11010011 11000110 00010111,换算为十进制为1725154839

monitor

概念

Monitor 被翻译为监视器或管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针,而不是上述的hashcode。

结构

在这里插入图片描述
当给对象加锁后,线程的状态切换是通过该对象头上指向的monitor对象来进行管理的。

waitSet:图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
wait-notify 时会分析

EntryList:集合里面存放的是同样要使用该对象锁的线程,待Owner 为null时候,list里面的线程进行竞争,竞争是非公平的。如果不使用该对象的线程会有另外一个Monitor

Owner:刚开始 Monitor 中 Owner 为 null,当前获得该对象锁的线程,线程执行完成后会把Owner的引用置为null。

synchronized进阶

cas名词解释

Compare and Swap的缩写,翻译过来就是比较并替换

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新生成锁记录和CAS。以后只要不发生竞争,这个对象就归该线程所有

static final Object obj = new Object();
public static void m1() {
 synchronized( obj ) {
 // 同步块 A
 m2();
 }
}
public static void m2() {
 synchronized( obj ) {
 // 同步块 B
 m3();
 }
}
public static void m3() {
 synchronized( obj ) {
      }
 }

在这里插入图片描述
在这里插入图片描述
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到hashcode 时才会赋值

轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争,使用wait/notify来进行先后控制,如果不控制,两个线程产生了竞争,则从轻量级锁升级为重量级锁),那么可以使用轻量级锁来优化,轻量级锁对于使用者来说是透明的,即语法还是 synchronized

    Object object = new Object();
    public  void method1(){
        synchronized (object){
        }
    }

    public void  method(){
        synchronized (object{
        }
    }

在这里插入图片描述

  • 该线程的栈帧会创建一个锁记录lock record,里面有锁地址和对象引用,
  • 项目如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,效果如下:
    在这里插入图片描述
  • 如果失败,有两种情况
  1. 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
    在这里插入图片描述
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
    入计数减一

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    1、成功,则解锁成功
    2、失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

重量级锁

当两个线程同时给某个对象加锁,产生竞争关系,则将轻量级锁直接升级成重量级锁。
在这里插入图片描述

  • 锁记录的Oject reference 指向的是加锁对象
  • 生成一个monitor对象,object的markword 存放的是Monitor的引用对象,当前阻塞的线程放在EntryList里面

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

    Object object = new Object();
    public  void method1(){
        synchronized (object){
        }
    }
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    在这里插入图片描述

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    1、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    2、然后自己进入 Monitor 的 EntryList BLOCKED
    在这里插入图片描述

  • 当 Thread-0 退出同步块解锁时,使用 cas 将锁对象Object的 Mark Word 的对象头中的Monitor 地址和Hashcode age bias01 替换,这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程,进行非公平竞争。

自旋优化

当重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 下面是自旋成功
    在这里插入图片描述
  • 自旋失败
    在这里插入图片描述
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

锁消除

偏向锁

Compare and Swap的缩写,翻译过来就是比较并替换

二级目录

三级目录

轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值