前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文带你读懂JDK源码:JVM提供的5种锁优化

一文带你读懂JDK源码:JVM提供的5种锁优化

作者头像
后台技术汇
发布2022-05-28 12:26:39
2620
发布2022-05-28 12:26:39
举报
文章被收录于专栏:后台技术汇

高效并发是从JDK1.5到1.6的一个重要改进,HotSpot团队用了大量的精力进行锁优化技术,适应性锁(Adaptive Spinnig)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)

锁优化目的是:线程之间更加高效的共享数据,解决资源竞争问题,提高程序执行效率。

下面我们分5点来分别阐述它们的定义,重点关注在“轻量级锁”&“偏向锁”。

winter

必须先提及两个基础概念:Object Header 内存布局 和 CAS。

自旋锁底层用的就是CAS操作,而轻量级锁与偏向锁都用到 Object Header 进行锁状态管理。

Object Header :

  • Part1 是自身运行数据:HashCode、GC 分代年龄等,官方称为“Mark Word”,也是轻量级锁&偏向锁的关键。
  • Part2 是存储指向方法区对象类型数据的指针;

Object Header 的 MarkWord 结构:

总结一下:Mark Word 可以存储多种内容,当锁标志位(00/01/10/11/01)处于不同类型时,该存储区域可以分别存储各种数据(对象hashcode和GC-age/锁记录pointer/重量级锁pointer/null/偏向锁的线程ID)

CAS:

乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。

自旋锁(自适应锁)

自旋锁:线程执行一个忙循环(自旋),这项技术就是自旋锁了(spinlock)。

互斥同步:阻塞的实现导致性能巨大的消耗,因为“挂起和恢复线程的操作都要转入到内核态”。(参考:《操作系统的线程模式》)

使用场景:共享资源的被锁定状态只会持续很短的时间,那么为此频繁挂起和恢复线程并不值得。

实现方法:让后面请求锁的线程“稍等一下”,不要放弃CPU的执行时间,看看持有锁的线程很快就会释放锁了。

JVM配置参数:使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。(JDK1.6之后,默认开启了自旋锁,自旋次数的默认值是10次);超过自旋次数,就应当使用传统方式去挂起线程了。

自旋锁的好处:减少线程上下文切换的消耗(线程切换要从用户态切到内核态,很消耗资源)

自旋锁的不足:如果被锁资源的时间很长,那么自旋的线程只会白白浪费掉处理器资源,没有性能产出。

自适应锁的改进:优化方式:自旋的时间不固定,动态根据前一次在同一个锁的自旋时间&锁拥有者的状态共同决定。(JDK1.6 引入自适应锁)

案例1:(AQS的抽象类 AbstractQueuedSynchronizer,底层就是使用了自旋锁)

代码语言:javascript
复制
public class SpinLockTest {
  AtomicReference<Thread> reference = new AtomicReference<>();

  //加锁
  public void mylock(){
    Thread thread = Thread.currentThread();
    System.out.println(thread.getName() + " try to get lock..");
    //工具类提供了CAS操作
    while (!reference.compareAndSet(null,thread)){
      //do nothing CPU 空转(自旋)
      System.out.println(thread.getName() + " cannot get the lock, so do nothing and waiting...");
    }
  }

  //解锁
  public void unlock(){
    Thread thread = Thread.currentThread();
    reference.compareAndSet(thread,null);
    System.out.println(thread.getName()+ " to release lock.");
  }

  public static void main(String[] args) {
    SpinLockTest spinLockTest = new SpinLockTest();
    new Thread(()->{
      spinLockTest.mylock();
      try {
        TimeUnit.SECONDS.sleep(5);
      }catch (InterruptedException e){
        e.printStackTrace();
      }
      //休眠10s之后,t1 才释放锁(未释放资源期间,其他的线程会一直空转)
      spinLockTest.unlock();
    },"t1").start();

    try {
      TimeUnit.SECONDS.sleep(1);
    }catch (InterruptedException e){
      e.printStackTrace();
    }

    new Thread(()->{
      //t2 启动并且申请锁 & t2 释放锁资源
      spinLockTest.mylock();
      spinLockTest.unlock();
    },"t2").start();
  }
}

输出结果:

代码语言:javascript
复制
t1 try to get lock..
t2 try to get lock..
t2 cannot get the lock, so do nothing and waiting...(可以注释打印代码来禁止输出这段日志)
t1 to release lock.
t2 to release lock.

锁消除

锁消除:JVM 的即时编译器运行时,一些代码要求同步,但是检测到不可能存在共享数据的竞争的锁,那么会对这个锁进行消除处理。

判断依据:堆的数据不会逃逸(变量对象是否逃逸:虚拟机会使用数据流分析来确定),那么可以当做栈的数据,认为线程私有,同步加锁自然不需要进行了。

案例2:JDK1.5之前的字符串连接优化

代码语言:javascript
复制
public class LockRemove {
  public String concatString(String s1,String s2,String s3){
    return s1 + s2 + s3;
  }
}

代码分析:

JDK1.5之前,会转化为StringBuffer的append操作,而StringBuffer的拼接操作都是使用了重量级锁 synchronized;经过分析,此处的s1,s2,s3引用都不会“逃逸到”concatString 方法之外,那么就可以安全的消除掉这个锁。

锁粗化

锁粗化:代码块中对一个对象反复加锁&解锁,甚至在循环出现这种情况,那么可以适当地扩大锁的范围,实现“锁粗化”。

案例3:一个for循环里面对一个对象反复加锁&解锁

代码语言:javascript
复制
public class LockExpend {
  private final static ReentrantLock lock = new ReentrantLock();
  public static void main(String[] args) {
    int[] ints = new int[10];
    for (int i : ints){
      lock.lock();
      try {
        // do something..
        System.out.println(i);
      } finally {
        lock.unlock();
      }
    }
  }
}

代码分析:

JVM探测到有这样一个零碎操作,对同一个对象加锁,会把这个加锁同步的范围扩展到整个操作序列的外部;这个案例里,会拓展到for循环之外。

轻量级锁

轻量级锁,不是传统重量级锁的替代品,而是用于在没有多线程竞争时,减少重量级锁使用操作系统互斥量带来的性能消耗,是用于性能优化的手段。

理解轻量级锁并不难,因为其本质是对象头部的“锁标志”以及堆栈的锁记录更新替换操作,继续看下去。

加锁过程

正常情况下,没有竞态条件发生,轻量级锁加锁过程有3个步骤。

步骤1,见下图:堆栈Stack整个新的“锁记录”区域;

步骤2跟步骤3,见下图:备份锁对象MarkWord后,CAS,改为堆栈Stack的地址,同时要修改MarkWord的锁标志;

至此,通过修改MarkWord,达到一个“轻量级锁”的效果,虚拟机认为锁对象被某个帧栈所在的线程拥有了。

解锁过程

正常情况下,加锁过程中如果没有碰到“第三者线程”进行竞争,那么线程很容易就获取到锁资源的所有权了。这样一来,解锁也变得简单了。

  • 解锁过程,正常情况下,是一个CAS操作,把之前备份的MarkWord,归回原来的锁对象,这样一来,锁对象就丢失跟原拥有者线程的联系了,实现解锁,过程见下图。
  • 解锁过程,非正常情况下,就不太简单了,因为解锁时,发现锁资源还有一直等待的“备胎”线程,那么就要在释放锁的同时,唤醒这个被挂起的线程。

竞争锁资源

我们在上面了解到,正常情况下,没有竞态条件发生,轻量级锁加锁过程有3个步骤。

那如果不凑巧,有两条以上的线程竞争同一个锁呢?(OK,那么轻量级锁就不有效,“膨胀”为重量级锁)。

见下图,膨胀过程,修改MarkWord为互斥锁的指针,并且修改锁标志。

总结“轻量级锁”

轻量级锁的最大作用是:绝大部分锁,在同步周期内不存在多线程竞争访问(经验数据),因此通过CAS操作避免了互斥锁的巨大开销。

优点:偏向锁相对于重量级锁的优点是:因为线程在竞争资源时采用的是自旋,而不是阻塞,也就避免了线程的切换带来的时间消耗,提高了程序的响应速度

不足:轻量级锁涉及到一个自旋的问题,而自旋操作是会消耗CPU资源的。为了避免无用的自旋,当锁资源存在线程竞争时,偏向锁就会升级为重量级锁来避免其他线程无用的自旋操作

偏向锁

偏向锁:无线程竞争下,把整个同步都消除,连CAS都不做了。

JVM配置参数:-XX:+UseBiasedLocking

原理:让第一次访问锁资源的线程将直接获取该资源的所有权(锁对象的MarkWord写入该线程的Thread-ID)。

加锁过程

加锁过程见下图,加锁完成后,该锁对象就进入了一个“可偏向状态”了。

解锁过程

偏向锁的解锁,跟轻量级锁不一样。

因为锁资源已经处于“已偏向状态”了,每次线程访问该资源时,都会检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID

  • 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块;
  • 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁;

何为撤销偏向锁?

偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在发现了线程竞争时,直接锁资源对象“升级到” 被加了轻量级锁,见下面的小点“锁升级过程”。

锁升级过程

锁升级的过程是:偏向锁 -> 轻量级锁 -> 重量级锁。

总结“偏向锁”

偏向锁最大作用:提高带同步但无竞争的程序性能。(对于共享资源极少被多线程同时访问到,因此把轻量级锁的CAS操作也省略了,从而达成了进一步的性能优化。)

偏向锁的特征是:“带效益权衡性质”的优化,如果程序大部分锁都会被多线程竞争访问,那么偏向模式就是多余的。

总结

以上,就是本节的所有内容了,主要介绍了JVM对锁进行的5项锁优化方式:锁自旋、锁消除、锁粗化、轻量级锁以及偏向锁,本节主要侧重了轻量级锁以及偏向锁的讲解。希望对大家理解JVM锁优化有所帮助,晚安~

参考文章:

  1. https://zhuanlan.zhihu.com/p/141554048(轻量级锁加解锁过程详解)
  2. https://www.cnblogs.com/dream2true/p/10759763.html(CAS机制与自旋锁)
  3. https://blog.csdn.net/sinat_41832255/article/details/89309944(偏向锁)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后台技术汇 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自旋锁(自适应锁)
  • 锁消除
  • 锁粗化
  • 轻量级锁
  • 偏向锁
  • 总结
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档