关于jvm:JVM学习笔记六锁优化与CAS

25次阅读

共计 5820 个字符,预计需要花费 15 分钟才能阅读完成。

1 起源

  • 起源:《Java 虚拟机 JVM 故障诊断与性能优化》——葛一鸣
  • 章节:第八章

本文是第八章的一些笔记整顿。

2 概述

本文次要讲述了 JVM 在运行层面和代码层面的锁优化策略,最初介绍了实现无锁的其中一种办法CAS

3 对象头

JVM中每个对象都有一个对象头,用于保留对象的零碎信息,64bit JVM的对象头构造如下图所示:

其中:

  • Mark Word64bit 组成,一个性能数据区,能够寄存对象的哈希、对象年龄、锁的指针等信息
  • KClass Word在没有开启指针压缩的状况下,64bit组成,然而 64bit JVM 会默认开启指针压缩(+UseCompressedOops),所以会压缩到32bit

另外,从图中能够看到,不同的锁对应于不同的Mark Word

  • 无锁:25bit空 +31bit哈希值 +1bit空 +4bit分代年龄 +1bit是否偏差锁 +2bit锁标记
  • 偏差锁:54bit持有偏差锁的线程 ID+2bit 偏差工夫戳 +1bit空 +4bit分代年龄 +1bit是否偏差锁 +2bit锁标记
  • 轻量锁:62bit栈中锁记录指针 +2bit锁标记
  • 分量锁:62bit重量级锁指针 +2bit锁标记

JVM如何辨别锁次要看两个字段:biased_locklock,对应关系如下:

  • biased_lock=0 lock=00:轻量级锁
  • biased_lock=0 lock=01:无锁
  • biased_lock=0 lock=10:重量级锁
  • biased_lock=0 lock=11GC标记
  • biased_lock=1 lock=01:偏差锁

4 锁的运行时优化

很多时候 JVM 都会对线程竞争的操作在 JVM 层面进行优化,尽可能解决竞争问题,也会试图打消不必要的竞争,实现的办法包含:

  • 偏差锁
  • 轻量级锁
  • 重量级锁
  • 自旋锁
  • 锁打消

4.1 偏差锁(JDK15默认敞开)

4.1.1 简介

偏差锁是 JDK 1.6 提出的一种锁优化形式,核心思想是,如果线程没有竞争,则勾销曾经获得锁的线程同步操作,也就是说,某个线程获取到锁后,锁就会进入偏差模式,当线程再次申请该锁时,无需再次进行相干的同步操作,从而节俭操作工夫。而在此期间如果有其余线程进行了锁申请,则锁退出偏差模式。

开启偏差锁的参数是 -XX:+UseBiasedLocking,处于偏差锁时,Mark Word 会记录取得锁的线程(54bit),通过该信息能够判断以后线程是否持有偏差锁。

留神 JDK15 后默认敞开了偏差锁以及禁用了相干选项,能够参考 JDK-8231264。

4.1.2 加锁流程

偏差锁的加锁过程如下:

  • 第一步:拜访 Mark Word 中的 biased_lock 是否设置为 1lock 是否设置为 01,确认为可偏差状态,如果biased_lock0,则是无锁状态,间接通过 CAS 操作竞争锁,如果失败,执行第四步
  • 第二步:如果为可偏差状态,测试线程 ID 是否指向以后线程,如果是,达到第五步,否则达到第三步
  • 第三步:如果线程 ID 没有指向以后线程,通过 CAS 操作竞争锁,如果胜利,将 Mark Word 中的线程 ID 设置为以后线程ID,而后执行第五步,如果失败,执行第四步
  • 第四步:如果 CAS 获取偏差锁失败,示意有竞争,开始锁撤销
  • 第五步:执行同步代码

4.1.3 例子

上面是一个简略的例子:

public class Main {private static List<Integer> list = new Vector<>();
    public static void main(String[] args){long start = System.nanoTime();
        for (int i = 0; i < 1_0000_0000; i++) {list.add(i);
        }
        long end = System.nanoTime();
        System.out.println(end-start);
    }
}

Vectoradd 是一个 synchronized 办法,应用如下参数测试:

-XX:BiasedLockingStartupDelay=0 # 偏差锁启动工夫,设置为 0 示意立刻启动
-XX:+UseBiasedLocking # 开启偏差锁

输入如下:

1664109780

而将偏差锁敞开:

-XX:BiasedLockingStartupDelay=0
-XX:-UseBiasedLocking

输入如下:

2505048191

能够看到偏差锁还是对系统性能有肯定帮忙的,然而须要留神偏差锁在锁竞争强烈的场合没有太强的优化成果,因为大量的竞争会导致持有锁的线程不停地切换,锁很难始终放弃在偏差模式,这样不仅仅不能优化性能,反而因为频繁切换而导致性能降落,因而竞争强烈的场合能够尝试应用 -XX:-UseBiasedLocking 禁用偏差锁。

4.2 轻量级锁

4.2.1 简介

如果偏差锁失败,那么 JVM 会让线程申请轻量级锁。轻量级锁在外部应用一个 BasicObjectLock 的对象实现,该对象外部由:

  • 一个 BasicLock 对象
  • 一个持有该锁的 Java 对象指针

组成。BasicObjectLock对象搁置在 Java 栈的栈帧中,在 BasicLock 对象还会保护一个叫 displaced_header 的字段,用于备份对象头部的Mark Word

4.2.2 加锁流程

  • 第一步:通过 Mark Word 判断是否无锁(biased_lock是否为 0lock01),如果是无锁,会创立一个叫锁记录(Lock Record)的空间,用于存储以后Mark Word 的拷贝
  • 第二步:将对象头的 Mark Word 复制到锁记录中
  • 第三步:拷贝胜利后,应用 CAS 操作尝试将锁对象 Mark Word 更新为指向锁记录的指针,并将线程栈帧中的锁记录的 owner 指向 ObjectMark Word
  • 第四步:如果操作胜利,那么就胜利领有了锁
  • 第五步:如果操作失败,JVM会查看 Mark Word 是否指向以后线程的栈帧,如果是就阐明以后线程曾经领有了这个对象的锁,就能够间接进入同步块继续执行,否则会让以后线程尝试自旋获取锁,自旋达到肯定次数后如果还没有取得锁,那么会收缩为重量级锁

4.3 重量级锁

4.3.1 简介

当轻量级锁自旋肯定次数后还是无奈获取锁,就会收缩为重量级锁。相比起轻量级锁,Mak Word寄存的是指向锁记录的指针,重量级锁中的 Mark Word 寄存的是指向 Object Monitor 的指针,如下图所示:

(图源见文末)

因为锁记录是线程公有的,不能满足多线程都能拜访的需要,因而重量级锁中引入了能线程共享的ObjectMonitor

4.3.2 加锁流程

首次尝试加锁时,会先 CAS 尝试批改 ObjectMonitor_owner字段,后果如下:

  • 第一种:锁没有其余线程占用,胜利获取锁
  • 第二种:锁被其余线程占用,则以后线程重入锁,获取胜利
  • 第三种:锁被锁记录占用,而锁记录是线程公有的,也就是属于以后线程的,这样就属于重入,重入次数为 1
  • 第四种:都不满足,再次尝试加锁(调用EnterI()

而再次尝试加锁的过程,是一个循环,一直尝试获取锁直到胜利为止,流程简述如下:

  • 屡次尝试获取锁
  • 获取失败把线程包装后放进阻塞队列
  • 再次尝试获取锁
  • 失败后将本人挂起
  • 被唤醒后持续尝试获取锁
  • 胜利则退出循环,否则持续

4.4 自旋锁

自旋锁能够使线程没有获得锁时不被挂起,而是去执行一个空循环(也就是所谓的自旋),在若干个空循环后如果能够获取锁,则继续执行,如果不能,挂起以后线程。

应用自旋锁后,线程被挂起的概率绝对减小,线程执行的连贯性绝对增强,因而对于锁竞争不是很强烈、锁占用并发工夫很短的并发线程具备肯定的积极意义,然而,对于竞争强烈且锁占用工夫长的并发线程,自旋期待后仍无奈获取锁,还是会被挂起,节约了自旋工夫。

JDK1.6 中提供了 -XX:+UseSpinning 参数开启自旋锁,然而 JDK1.7 后,自旋锁参数被勾销,JVM不再反对由用户配置自旋锁,自旋锁总是被执行,次数由 JVM 调整。

4.5 锁打消

4.5.1 简介

锁打消就是把不必要的锁给去掉,比方,在一些单线程环境下应用一些线程平安的类,比方StringBuffer,这样就能够基于逃逸剖析技术可打消这些不必要的锁,从而进步性能。

4.5.2 例子

public class Main {
    private static final int CIRCLE = 200_0000;
    public static void main(String[] args){long start = System.nanoTime();
        for (int i = 0; i < CIRCLE; i++) {createStringBuffer("Test",String.valueOf(i));
        }
        long end = System.nanoTime();
        System.out.println(end-start);
    }

    private static String createStringBuffer(String s1,String s2){StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();}
}

参数:

-XX:+DoEscapeAnalysis
-XX:-EliminateLocks
-Xcomp
-XX:-BackgroundCompilation
-XX:BiasedLockingStartupDelay=0

输入:

260642198

而开启锁打消后:

-XX:+DoEscapeAnalysis
-XX:+EliminateLocks
-Xcomp
-XX:-BackgroundCompilation
-XX:BiasedLockingStartupDelay=0

输入如下:

253101105

能够看到还是有肯定性能晋升的,然而晋升不大。

5 锁的应用层优化

锁的应用层优化就是在代码层面对锁进行优化,办法包含:

  • 缩小持有工夫
  • 减小粒度
  • 锁拆散
  • 锁粗化

5.1 缩小持有工夫

缩小锁持有工夫就是尽可能减少某个锁的占用工夫,以缩小线程互斥工夫,比方:

public synchronized void method(){A();
    B();
    C();}

如果只有 B() 是同步操作,那么能够优化为在必要时进行同步,也就是在执行 B() 的时候进行同步操作:

public void method(){A();
    synchronized(this){B();
    }
    C();}

5.2 减小粒度

所谓的减小锁粒度,就是指放大锁定的对象范畴,从而减小锁抵触的可能性,进而进步零碎的并发能力。

减小粒度也是一种减弱多线程竞争的无效伎俩,比方典型的就是 ConcurrentHashMap,在JDK1.7 中的 segment 就是一个很好的例子。每次并发操作的时候只加锁某个特定的segment,从而进步并发性能。

5.3 锁拆散

锁拆散就是将一个独占锁分成多个锁,比方 LinkedBlockingQueue。在take()put()操作中,应用的并不是同一个锁,而是拆散成了一个 takeLock 和一个putLock

private final ReentrantLock takeLock;
private final ReentrantLock putLock;

初始化操作如下:

this.takeLock = new ReentrantLock();
this.notEmpty = this.takeLock.newCondition();
this.putLock = new ReentrantLock();

take()put()操作如下:

public E take() throws InterruptedException {takeLock.lockInterruptibly();  // 不能两个线程同时 take
    //...
    try {//...} finally {takeLock.unlock();
    }
    //...
}

public void put(E e) throws InterruptedException {
    //...
    putLock.lockInterruptibly();  // 不能两个线程同时 put
    try {//...} finally {putLock.unlock();
    }
    //...
}

能够看到通过 putLock 以及 takeLock 两把锁实现了真正的取数据与写数据拆散

5.4 锁粗化

通常状况下,为了保障多线程的无效并发,会要求每个线程持有锁的工夫尽可能短,然而,如果对同一个锁不停申请,自身也会耗费资源,反而不利于性能优化,于是,在遇到一连串间断对同一个锁一直进行申请和开释的操作时,会把所有的锁操作整合成对锁的一次申请,缩小对锁的申请同步次数,这个过程就叫锁粗化,比方

public void method(){synchronized(lock){A();
    }
    synchronized(lock){B();
    }
}

会被整合成如下模式:

public void method(){synchronized(lock){A();
        B();}
}

而在循环内申请锁,比方:

for(int i=0;i<10;++i){synchronized(lock){}}

应将锁粗化为

synchronized(lock){for(int i=0;i<10;++i){}}

6 无锁:CAS

毫无疑问,为了保障多线程并发的平安,应用锁是一种最直观的形式,然而,锁的竞争有可能会称为瓶颈,因而,有没有不须要锁的形式去保证数据一致性呢?

答案是有的,就是这一大节介绍的配角:CAS

CAS就是 Compare And Swap 的缩写,CAS蕴含三个参数,模式为CAS(V,E,N),其中:

  • V示意内存地址值
  • E示意期望值
  • N示意新值

只有当 V 的值等于 E 的值时,才会把 V 设置为 N,如果V 的值和 N 的值不一样,那么示意曾经有其余线程做了更新,以后线程什么也不做,最初 CAS 返回以后 V 的值。

CAS的操作是抱着乐观的态度进行的,总认为本人能够胜利实现操作,当多个线程同时应用 CAS 操作同一个变量的时候,只会有一个胜出并胜利更新,其余均会失败。失败的线程不会被挂起,仅被告知失败,并且容许再次尝试,当然也容许失败的线程放弃操作。

7 参考

  • CSDN-java 对象头信息
  • JVM 系列之: 详解 java object 对象在 heap 中的构造
  • StackOverflow-What is in Java object header?
  • CSDN-Java 中锁是如何一步步收缩的(偏差锁、轻量级锁、重量级锁)
  • 简书 -Java Synchronized 重量级锁原理深刻分析上(互斥篇)

正文完
 0