JVM 对锁进行了哪些优化呢?

相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 synchronized 内置锁的性能进行了很多优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等。有了这些优化措施后,synchronized 锁的性能得到了大幅提高,下面我们分别介绍这些具体的优化。

自适应的自旋锁
首先,我们来看一下自适应的自旋锁。先来复习一下自旋的概念和自旋的缺点。“自旋”就是不释放 CPU,一直循环尝试获取锁,如下面这段代码所

复制代码
public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
 
    return var6;
}
代码中使用一个 do-while 循环来一直尝试修改 long 的值。自旋的缺点在于如果自旋时间过长,那么性能开销是很大的,浪费了 CPU 资源。

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变“聪明”了。比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。

锁消除
第二个优化是锁消除。首先我们来看下面的代码:

复制代码
public class Person {

private String name;
    private int age;

public Person(String personName, int personAge) {
        name = personName;
        age = personAge;
    }

public Person(Person p) {
        this(p.getName(), p.getAge());
    }

public String getName() {
        return name;
    }

public int getAge() {
        return age;
    }
}

class Employee {

private Person person;

// makes a defensive copy to protect against modifications by caller
    public Person getPerson() {
        return new Person(person);
    }

public void printEmployeeDetail(Employee emp) {
        Person person = emp.getPerson();
        // this caller does not modify the object, so defensive copy was unnecessary
        System.out.println("Employee's name: " + person.getName() + "; age: " + person.getAge());
    }
}
在这段代码中,我们看到下方的 Employee 类中的 getPerson() 方法,这个方法中使用了类里面的 person 对象,并且新建一个和它属性完全相同的新的 person 对象,目的是防止方法调用者修改原来的 person 对象。但是在这个例子中,其实是没有任何必要新建对象的,因为我们的 printEmployeeDetail() 方法没有对这个对象做出任何的修改,仅仅是打印,既然如此,我们其实可以直接打印最开始的 person 对象,而无须新建一个新的。

如果编译器可以确定最开始的 person 对象不会被修改的话,它可能会优化并且消除这个新建  person 的过程。

根据这样的思想,接下来我们就来举一个锁消除的例子,经过逃逸分析之后,如果发现某些对象不可能被其他线程访问到,那么就可以把它们当成栈上数据,栈上数据由于只有本线程可以访问,自然是线程安全的,也就无需加锁,所以会把这样的锁给自动去除掉。

例如,我们的 StringBuffer 的 append 方法如下所示:

复制代码
@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}
从代码中可以看出,这个方法是被 synchronized 修饰的同步方法,因为它可能会被多个线程同时使用。

但是在大多数情况下,它只会在一个线程内被使用,如果编译器能确定这个 StringBuffer 对象只会在一个线程内被使用,就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率。

锁粗化
接下来,我们来介绍一下锁粗化。如果我们释放了锁,紧接着什么都没做,又重新获取锁,例如下面这段代码所示:

复制代码
public void lockCoarsening() {
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
}
那么其实这种释放和重新获取锁是完全没有必要的,如果我们把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁,那么就可以把中间这些无意义的解锁和加锁的过程消除,相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的好处在于在线程执行这些代码时,就无须频繁申请与释放锁了,这样就减少了性能开销。

不过,我们这样做也有一个副作用,那就是我们会让同步区域变大。如果在循环中我们也这样做,如代码所示:

复制代码
for (int i = 0; i < 1000; i++) {
    synchronized (this) {
        //do something
    }
}
也就是我们在第一次循环的开始,就开始扩大同步区域并持有锁,直到最后一次循环结束,才结束同步代码块释放锁的话,这就会导致其他线程长时间无法获得锁。所以,这里的锁粗化不适用于循环的场景,仅适用于非循环的场景。

锁粗化功能是默认打开的,用 -XX:-EliminateLocks 可以关闭该功能。

偏向锁/轻量级锁/重量级锁
下面我们来介绍一下偏向锁、轻量级锁和重量级锁。这个锁在我们之前介绍锁的种类的时候也介绍过。这三种锁是特指 synchronized 锁的状态的,通过在对象头中的 mark word 来表明锁的状态。

偏向锁
对于偏向锁而言,它的思想是如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只要打个标记就行了。一个对象在被初始化后,如果还没有任何线程来获取它的锁时,它就是可偏向的,当有第一个线程来访问它尝试获取锁的时候,它就记录下来这个线程,如果后面尝试获取锁的线程正是这个偏向锁的拥有者,就可以直接获取锁,开销很小。

轻量级锁
JVM 的开发者发现在很多情况下,synchronized 中的代码块是被多个线程交替执行的,也就是说,并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决。这种情况下,重量级锁是没必要的。轻量级锁指当锁原来是偏向锁的时候,被另一个线程所访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的方式尝试获取锁,不会阻塞。

重量级锁
这种锁利用操作系统的同步机制实现,所以开销比较大。当多个线程直接有实际竞争,并且锁竞争时间比较长的时候,此时偏向锁和轻量级锁都不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
锁升级的路径

最后,我们看下锁的升级路径,如图所示。从无锁到偏向锁,再到轻量级锁,最后到重量级锁。结合前面我们讲过的知识,偏向锁性能最好,避免了 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。

JVM 默认会优先使用偏向锁,如果有必要的话才逐步升级,这大幅提高了锁的性能。

引用:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=268

Java多线程学习十九:JVM 对锁进行了哪些优化?相关推荐

  1. Java多线程学习十二:悲观锁和乐观锁的本质||

    悲观锁和乐观锁是从是否锁住资源的角度进行分类的. 悲观锁 悲观锁比较悲观,它认为如果不锁住这个资源,别的线程就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时, ...

  2. Java多线程学习十五:公平锁和非公平锁,为什么要“非公平”?

    什么是公平和非公平 公平锁 指的是按照线程请求的顺序,来分配锁: 非公平锁 指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完全的随机,不是说线程可以任意插队,而 ...

  3. Java多线程学习十二: synchronized的工作原理 以及背后的“monitor 锁”

    我们研究下 synchronized 背后的 monitor 锁. 获取和释放 monitor 锁的时机 我们都知道,最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个 ...

  4. Java多线程学习十六:读写锁 ReadWriteLock 获取锁有哪些规则

    读写锁 ReadWriteLock 获取锁有哪些规则呢? 在没有读写锁之前,我们假设使用普通的 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源,因为如果多个读操作同时进 ...

  5. Java多线程学习十四:Lock 有哪几个常用方法?分别有什么用?

    Lock 接口是 Java 5 引入的,最常见的实现类是 ReentrantLock,可以起到"锁"的作用. Lock 和 synchronized 是两种最常见的锁,锁是一种工具 ...

  6. Java多线程学习十:线程池实现“线程复用”的原理

    线程复用原理 我们知道线程池会使用固定数量或可变数量的线程来执行任务,但无论是固定数量或可变数量的线程,其线程数量都远远小于任务数量,面对这种情况线程池可以通过线程复用让同一个线程去执行不同的任务,那 ...

  7. Java多线程学习总结(5)——乐观锁和悲观锁的基本概念、实现方式(含实例)、适用场景及常见面试题

     分享一个大神的人工智能教程.零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到人工智能的队伍中来!点击浏览教程 一.基本概念 乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题. 乐观锁 ...

  8. Java多线程学习 (超详细总结)

    Java多线程学习 一.概要 二. JAVA 线程实现/创建方式 2.1 继承Thread 类 2.2 实现 Runnable 接口 2.3 Thread和Runnable的区别 2.4 总结 三.线 ...

  9. 【转】Java 多线程学习

    原网址:https://www.cnblogs.com/yjd_hycf_space/p/7526608.html Java多线程学习(总结很详细!!!) 此文只能说是java多线程的一个入门,其实J ...

最新文章

  1. 2016年研究数据可视化最不应该错过的10篇文章
  2. Criteria查询之分页显示数据
  3. 11.1 安装配置Apache
  4. 百度更新算法之后我想说
  5. C#中UDP通信过程中出现:远程主机强迫关闭了一个现有的连接0x80004005】的解决方法
  6. 不想当全栈的设计师不是_但我不想成为产品设计师
  7. 在IntelliJ IDEA配置Tomcat
  8. 时间戳TimeStamp处理
  9. LAMP笔记之Apache篇(2)
  10. 小小一方士 C# Async\Await 之 上传/下载文件进度条实现原理
  11. 最有效地戒掉晚睡强迫症(熬夜强迫症、假象失眠症等等)
  12. 2021 腾讯校招 + 后台开发面经(已 offer)
  13. 上传图片到淘宝 API
  14. 通过ICommand和ITool操作地图
  15. Unity编辑器扩展——在Editor下动态添加监听事件
  16. Kepp-alive的实际运用场景(1)
  17. dry的原理_Dry Etch 工艺基本原理及良率剖析(经典讲解)
  18. PTA 最大和最小 (10 分)请使用指针法(间接访问)编写程序,程序的功能是从键盘输入 10 个数,求其最大值和最小值的差。
  19. websocket 爬虫
  20. 2022年艺术品和古董投资策略研究报告

热门文章

  1. 即将绝版!小米最经典旗舰降价甩,以后不会再有了
  2. 中断数周之后 微软网站恢复销售华为笔记本电脑
  3. XML---(2)LIBXML2库Linux使用指南
  4. java中的页面:JSP(已过时)
  5. 史上最详细JVM笔记
  6. 我的内核学习笔记9:Intel内部看门狗iTCO_wdt驱动
  7. jQuery中append()、prepend()与after()、before()的区别
  8. cad文字提取到excel_别怕!CAD表格与EXCEL之间的转化,有它就够了
  9. 【Flink】Flink key 应该分配到哪个 KeyGroup 以及 KeyGroup 分配在哪个subtask
  10. 【flink】flink写入clickhouse Error while starting CH writer AccessDeninedExcepton