synchronized锁膨胀过程

一、相关概念

(1)synchronized关键字

        synchronized也叫同步监视器,同步监视器的作用:阻止多个线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。
        synchronized同步监视器是借用jvm调用操作系统的互斥量(mutex)实现的。在JDK1.6之前,synchronized同步都是调用操作系统函数实现的,JDK1.6之后对synchronized进行了优化,于是就出现了偏向锁、轻量级锁、重量级锁的概念。锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

(2)什么是CAS?

​         加锁和解锁过程中,会多次涉及CAS,我们先需要知道CAS是什么?CAS即Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动);设置:如果是,将A更新为B,结束;如果不是,则什么都不做。

(3)全局安全点(Safe Point)?

(i)GC Safe Point

safepoint这个词我们在GC中经常会提到,简单来说就是其代表了一个状态,在该状态下所有线程都是暂停的。在GC整体流程中,第一步是雷打不动的根节点枚举。在目前所有的收集器中,根节点枚举都必须要暂停所有用户线程,原因是如果该过程中根节点在不断的变化,将无法保证后续可达性分析的准确性。

(ii)Biased Safe Point

在synchronized的偏向锁机制中同样存在需要与Safe Point(安全点)相配合的操作,即偏向锁的撤销。与GC Safe Point(GC安全点)最大的区别在于,Biased Safe Point(偏向锁安全点)是一个“局部”安全点,即其并不需要暂停所有的用户线程,而只需暂停偏向线程即可。​

 (4)什么是锁膨胀?

        synchronized中锁升级的过程如下:锁升级:偏向锁 → 轻量级锁 → 重量级锁。

        一个锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。线程T1第一次执行完同步代码块后,当线程T2尝试获取锁的时候,发现是偏向锁,会判断线程T1是否仍然存活。如果线程T1仍然存活,会在线程到达全局安全点将线程T1暂停,此时偏向锁升级为轻量级锁,之后线程T1继续执行,线程T2自旋;如果判断结果是线程T1不存在了,则线程T2持有此偏向锁,锁不升级。

二、Java对象头分析

(1)Java对象布局

        在JVM中,对象在内存中的存储布局包含三个部分:Java对象头、实例数据、对齐填充。

  • Java对象头

        对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。类型指针是指向该对象所属类对象的指针,mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。

  • 实例数据

        存放类实例的属性数据信息,包括父类的属性信息。

  • 对齐填充

        由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。对齐填充会将对象的字节码大小填充为最接近的8的整数倍。

        

synchronized锁膨胀过程_第1张图片

(2)Java对象头Mark word 详解

synchronized锁膨胀过程_第2张图片

synchronized锁膨胀过程_第3张图片

        可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。

(i)Mark Word结构详解

(ii)JOL工具包  

        我们可以借助JOL来分析Java的对象存储布局。后续synchronized锁膨胀时我们也是通过JOL工具包查看对象MarkWord中的锁状态变化。


    org.openjdk.jol
    jol-core
    0.8

测试代码

public class JavaObject {

    private boolean flag;

    private int version;

    private long serialNo;

    ……get()/set()
}

public class JOLLayoutExample {

    static JavaObject object = new JavaObject();

    public static void main(String[] args) {
        //没有计算HASHCODE之前的对象头
        out.println(ClassLayout.parseInstance(object).toPrintable());
        //JVM 计算的hashcode
        out.println("jvm------------0x"+Integer.toHexString(object.hashCode()));
        HashUtil.countHash(object);
        //当计算完hashcode之后,我们可以查看对象头的信息变化
        out.println("after hash");
        out.println(ClassLayout.parseInstance(object).toPrintable());

        JavaObject[] objects = new JavaObject[1];
        objects[0] = object;
        out.println("JavaObject[] array");
        out.println(ClassLayout.parseInstance(objects).toPrintable());
    }
}

我们看看JavaObject的对象存储布局

synchronized锁膨胀过程_第4张图片

三、锁的分类

        synchronized锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。但是锁只有三种偏向锁、轻量级锁和重量级锁。

(1)偏向锁

        在锁不存在多线程竞争情况下,为了减小线程获取锁的代价而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入同步块时只需简单判断下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

  • 如果本身是无锁状态(初始状态),只需CAS设置偏向锁指向自己即可

  • 判断当前对象是否是偏向锁,判断拥有该偏向锁的线程是否还存在,不存在时直接CAS设置偏向锁指向自己线程(拥有偏向锁的线程使用完毕后不会主动释放)

  • 如果拥有该偏向锁的线程还存在,则会暂停拥有偏向锁的线程,这一步操作是在全局安全点进行的。设置锁标志位为00,偏向锁标志位为0,从拥有偏向锁线程A的空闲monitor record中读取一条,放至线程A的当前monitor record中,然后更新mark word,将mark word指向线程A中monitor record的指针,这样就完成了偏向锁升级为轻量级锁。之后持有锁的线程会继续执行,竞争该轻量级锁的线程自旋获取该对象

  • 注意:轻量级锁的获取释放需要多次CAS操作,而偏向锁只是在置换ThreadID时进行一次CAS操作。

  • 偏向锁获取后线程不会主动释放,偏向锁只有在其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放(被动释放,此时会发生锁升级)

  • 偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态

  • 偏向锁的撤销需要在全局安全点上进行,它会暂停所有持有偏向锁的线程,判断锁对象是否处于锁定状态。

  • 可以发现偏向锁适用于从始至终都只有一个线程在运行的情况,省略掉了自旋获取锁,以及重量级锁互斥的开销,这种锁的开销最低,性能最好接近于无锁状态,但是如果线程之间存在竞争的话,就需要频繁的去暂停拥有偏向锁的线程然后检查状态,决定是否重新偏向还是升级为轻量级别锁,性能就会大打折扣了,如果事先能够知道可能会存在竞争那么可以选择关掉偏向锁。

(2)轻量级锁

        轻量级锁(Lightweight Lock)是JDK 1.6 时加人的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

        在无竞争的情况下,轻量级锁使用CAS操作来实现锁的获取和释放,避免了线程的阻塞和唤醒,从而提高了并发性能

(i)轻量级锁的获取过程

        线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

        轻量级锁也会有锁重入的情况,如果线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针失败时,会先判断对象头中的Mark Word是否已经指向当前线程锁记录的指针,如果是,说明是锁重入,这时线程会新创建一个栈帧,对象头中的Mark Word设置为null,表示重入计数器。object reference(也叫owner指针)指向对象的对象头中的Mark Word。

  • 轻量级锁在加锁失败进行CAS达到一定次数后(自旋锁默认的次数为10次可以通过 -XX:PreBlockSpin 来更改),就会升级为重量级锁;在解锁失败,锁也会升级为重量级锁。

  • 一旦锁升级成重量级锁(就不会再恢复到轻量级锁状态),当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

加锁成功

synchronized锁膨胀过程_第5张图片

加锁失败

synchronized锁膨胀过程_第6张图片

        

(ii)轻量级锁的释放过程

        轻量级锁什么时候会解锁失败呢?在发生锁竞争时并且占用锁的线程未释放,这时(自旋默认了10次还是未获取到锁)竞争锁的线程就会将Mark Word修改为重量级锁,并且将自己阻塞在该锁的monitor对象(操作系统层面对象)上。之后占用锁的线程将栈帧中的Mark Word进行CAS替换回对象头的Mark Word的时候,发现有其它线程竞争该锁(已经由竞争锁的线程更改了锁状态),然后它释放锁并且唤醒在等待的线程,后续的线程操作就全部都是重量级锁了。

        轻量级锁重入的释放过程:判断线程栈帧中Displaced Mark Word对象是否为null,是则说明是重入锁的释放,只需要将object reference(也叫owner指针)指向null即可。

  • 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

  • 如果替换成功,整个同步过程就完成了。

  • 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

(3)重量级锁

        重量级锁也就是普通的悲观锁了,也就是竞争锁失败会阻塞等待唤醒再次竞争那种。重量级锁是借助操作系统来完成的。java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。所以重量级锁的性能会比较差。

  • 它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中

  • Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中

  • Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中

  • Wait Set:哪些调用wait方法被阻塞的线程被放置在这里

  • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck

  • Owner:当前已经获取到所资源的线程被称为Owner

  • !Owner:当前释放锁的线程

  • JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”

  • OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中

  • 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)

  • Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源

synchronized锁膨胀过程_第7张图片

四、锁的膨胀过程

(1)各种锁存在的情形

  • 偏向锁:单线程环境下,偏向锁不会每次加锁都调用os函数。
  • 轻量级锁:多个线程交替执行,无资源竞争。轻量级锁借助CAS完成加锁和解锁。
  • 重量级锁:多个线程互斥执行,存在资源竞争。Java中锁的实现是借用os函数(pthread_mutex_lock()上锁)实现的锁。Java调用操作系统会涉及状态切换等,比较消耗资源。

(2)jvm的延迟偏向机制

        JVM默认开启了偏向锁延迟,因为在JVM虚拟机启动的时候,初始化虚拟机方法中会调用synchronized()方法,而且jvm认为这些初始化方法中的synchronized()方法会被多个线程调用,存在资源。如果线程之间存在竞争的话,就需要频繁的去暂停拥有偏向锁的线程然后检查状态, 决定是否重新偏向还是升级为轻量级别锁,性能就会大打折扣了,如果事先能够知道可能会存在竞争那么可以选择关掉偏向锁如果一开始就开始偏向锁的话, 所以jvm开启了延迟偏向,确保了虚拟机初始化时不会因为频繁的锁重偏向和锁升级造成性能损耗。 (关闭延迟偏向:-XX:BiasedLockingStartupDelay=0)

public static void main(String[] args) {

    JavaObject object = new JavaObject();
    out.println("before lock +++++++++++");
    out.println(ClassLayout.parseInstance(object).toPrintable();

    synchronized (object){
        out.println("after lock+++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

开启延迟偏向时锁状态

synchronized锁膨胀过程_第8张图片

 现在通过-XX:BiasedLockingStartupDelay=0指令把jvm的偏向锁延迟关了,再看看锁的状态

synchronized锁膨胀过程_第9张图片

 (3)无锁状态-->偏向锁

        偏向锁存在的情形是只有一个线程持有锁。第一种情况是对象第一次加锁,是无锁状态(初始状态),只需CAS设置偏向锁指向自己。第二种情况是对象是偏向锁,但是偏向的那个线程不存在了,直接CAS设置偏向锁指向自己线程。

public static void main(String[] args) throws Exception{

        JavaObject object = new JavaObject();
        out.println("before lock +++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                out.println("bias " + Thread.currentThread().getName() + " locking +++++++++++");
                out.println(ClassLayout.parseInstance(object).toPrintable());
            
            }
        }, "t1");
        t1.start();
        t1.join();
        Thread t2 = new Thread(() -> {
            synchronized (object){
                out.println(" bias ? lightWeight "+ Thread.currentThread().getName()+ " locking +++++++++++");
                out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        },"t2");
        Thread.sleep(1000);
        while (true){
            //t1死亡
            if (!t1.isAlive()){
                t2.start();
                break;
            }
        }

        t2.join();
        out.println("after Lock +++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());


    }
}

 线程t1在对象无锁状态下对对象object加锁,线程t2在线程t1死亡后对对象object加锁,这两种情况都是偏向锁。

 synchronized锁膨胀过程_第10张图片

偏向锁解锁同步块后,不会释放锁。

 synchronized锁膨胀过程_第11张图片

 (4)偏向锁-->轻量级锁

        偏向锁升级为轻量级锁是存在多个线程交替执行,但是不存在资源竞争。

public static void main(String[] args) throws Exception{

        JavaObject object = new JavaObject();
        out.println("before lock +++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());

        
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
               
            }
            try {
                //t1退出同步块后继续执行
                Thread.sleep(2000);
                out.println(Thread.currentThread().getName() + " running +++++++++++");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(() -> {
            synchronized (object){
                out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
            }
        },"t2");
        t2.start();
        t2.join();
        out.println("after Lock +++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());
}

 synchronized锁膨胀过程_第12张图片

(i)偏向锁的批量重偏向和批量撤销 

  • 查看偏向锁批量重偏向和批量撤销的阈值

 可以通过命令java -XX:+PrintFlagsFinal 来看JVM中的默认配置。

BiasedLockingBulkRebiasThreshold:偏向锁批量重偏向的默认阀值为20次。BiasedLockingBulkRevokeThreshold:偏向锁批量撤销的默认阀值为40次。BiasedLockingDecayTime:距上次批量重偏向25秒内,撤销计数达到40,就会发生批量撤销。每隔(>=)25秒,会重置在[20, 40)内的计数,这意味着可以发生多次批量重偏向。

  • 为什么有批量重偏向/批量撤销

当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。这个过程是要消耗一定的成本的,所以如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

注意:

1)批量重偏向和批量撤销是针对类(class)的优化,和对象无关。

2)偏向锁重偏向一次之后不可再次重偏向。

3)当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

  • 批量重偏向/批量撤销的原理

        以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

        当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

1)首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。

2)每当遇到一个全局安全点时,比如要对class A进行批量再偏向,则首先对 class A中保存的epoch进行增加操作,得到一个新的epoch_new。

3)然后扫描所有持有 class A 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。

4)退出安全点后,当有线程需要尝试获取偏向锁时,直接检查 class A 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等,则说明该对象的偏向锁已经无效了(因为(3)步骤里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class A里面的epoch值是epoch_new,而当前对象的epoch里面的值还是epoch),此时竞争线程可以尝试对此对象重新进行偏向操作。

  • 代码演示

批量重偏向(bulk rebias)

/**
     * 批量重偏向
     * 当一个线程t1运行结束后,所有的对象都偏向t1。
     * 线程t2只对前30个对象进行了同步,0-18的对象会由偏向锁(101)升级为轻量级锁(00),19-29的对象由于撤销次数达到20,触发批量重偏向,偏向线程t2。
     * t2结束后,0-18的对象由轻量级锁释放后变成了无锁,19-29的对象偏向t2,30-49的对象还是偏向t1。
     *
     * 总结:批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,
     * 当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向于当前线程。
     */
    public static void batchReBias() throws Exception{

        for (int i = 0; i < 40; i++) {
            D d = new D();
            objects.add(d);

        }

        t1 = new Thread(() -> {
            for (int i = 0; i < objects.size(); i++) {
                synchronized (objects.get(i)) {// 50个对象全部偏向t1 101
                }
            }
            //唤醒t2
            LockSupport.unpark(t2);
        });

        t2 = new Thread(() -> {
            //阻塞当前线程
            LockSupport.park();
            //循环了30次
            for (int i = 0; i < 30; i++) {
                Object a = objects.get(i);
                synchronized (a) {
                    //分别打印第19次和第20次偏向锁重偏向结果
                    if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        // 第19次轻量级锁00,第20次偏向锁101,偏向t2
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
        });
        t1.start();
        t2.start();
        t2.join();

        System.out.println("打印list中第11个对象的对象头:");
        // 01 无锁,轻量级锁退出同步块后变成无锁状态,且不可偏向
        System.out.println((ClassLayout.parseInstance(objects.get(10)).toPrintable()));
        System.out.println("打印list中第26个对象的对象头:");
        // 101 偏向t2,偏向锁退出同步块后不会释放锁
        System.out.println((ClassLayout.parseInstance(objects.get(25)).toPrintable()));
        System.out.println("打印list中第35个对象的对象头:");
        // 101 偏向t1
        System.out.println((ClassLayout.parseInstance(objects.get(34)).toPrintable()));


    }

synchronized锁膨胀过程_第13张图片synchronized锁膨胀过程_第14张图片 

批量撤销偏向(bulk revoke)

 /**
     * 批量撤销偏向
     * t1执行完后,0-39的对象偏向t1。
     * t2执行完后,0-18的对象为轻量级锁,19-39偏向t2。
     * t3执行时由于之前执行过批量重偏向了,所以这里后20个对象(20-39)会升级为轻量级锁。达到批量撤销的阈值
     * t4休眠前对象45为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象
     * 的锁状态由偏向锁变为轻量级锁,而不在执行同步的对象的锁状态不会改变(如对象46)。
     *
     * 总结:
     * 批量重偏向和批量撤销是针对类的优化,和对象无关。偏向锁重偏向一次之后不可再次重偏向。当某个类
     * 已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利。
     */
    public static void batchRevokeBias() throws Exception{

        for (int i = 0; i < 60; i++) {
            D d = new D();
            objects.add(d);

        }

        t1 = new Thread(() -> {
            //必须大于或等于40
            for (int i = 0; i < 40; i++) {
                D d = objects.get(i);
                synchronized (d) {
                }
            }
            LockSupport.unpark(t2);
        }, "t1");

        t2 = new Thread(() -> {
            LockSupport.park();
            //必须大于或等于40
            for (int i = 0; i < 40; i++) {
                D d = objects.get(i);
                synchronized (d) {
                    //0-19 00 偏向锁撤销20次
                    //19-39 101 偏向t2

                }


            }
        }, "t2");

        t3 = new Thread(() -> {
            LockSupport.park();
            System.out.println("t3");
            //批量撤销的阈值40
            for (int i = 20; i < 40; i++) {
                D d = objects.get(i);
                synchronized (d) {
                    //后面20个对象由偏向锁变成轻量级锁,偏向锁撤销20次,与t2线程撤销的20次相加一共40次,触发批量撤销的阈值
                    if(i == 20 || i == 39){
                        out.println("t3 " + i + " : "+ ClassLayout.parseInstance(objects.get(i)).toPrintable()); // 00
                    }


                }
            }
        }, "t3");
        t4 = new Thread(() -> {
            synchronized (objects.get(44)) {
                out.println("t4 begin (45) " + ClassLayout.parseInstance(objects.get(44)).toPrintable()); // 101
                LockSupport.unpark(t3);
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t3触发批量撤销,还在同步块的class都升级为轻量级锁
                out.println("t4 end (45) " + ClassLayout.parseInstance(objects.get(44)).toPrintable()); // 00
                //不在同步块的d对象,匿名偏向
                out.println("t4 end (46) " + ClassLayout.parseInstance(objects.get(45)).toPrintable()); // 101

            }
        }, "t4");
        t4.start();
        t1.start();
        t2.start();
        t3.start();
        t3.join();
        t4.join();

        //撤销偏向
        System.out.println(ClassLayout.parseInstance(new D()).toPrintable()); // 01

    }

 synchronized锁膨胀过程_第15张图片

 synchronized锁膨胀过程_第16张图片

 (5)轻量级锁-->重量级锁

     重量级锁存在的情形是多个线程存在资源竞争。

public static void main(String[] args) throws Exception{

        JavaObject object = new JavaObject();
        out.println("before lock +++++++++++");
        out.println(ClassLayout.parseInstance(object).toPrintable());
        
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
            }
            try {
                //t1退出同步块后继续执行
                Thread.sleep(2000);
                out.println(Thread.currentThread().getName() + " running +++++++++++");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(() -> {
            synchronized (object){
                out.println(Thread.currentThread().getName()+ " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
                try {
                    //确保t2,t3会存在资源竞争
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2");
        t2.start();
        Thread t3 = new Thread(() -> {
            synchronized (object){
                out.println(Thread.currentThread().getName()+ " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
            }
        },"t3");
        t3.start();

        //确保t2,t3,都执行完同步块,且已经销毁
        while (true){
            if(!t2.isAlive() && !t3.isAlive()){
                out.println("after Lock +++++++++++");
                out.println(ClassLayout.parseInstance(object).toPrintable());
                break;
            }
        }



    }

synchronized锁膨胀过程_第17张图片

synchronized锁膨胀过程_第18张图片

五、不同锁状态下的性能对比 

public class A {
    int i=0;
    boolean b = false;
    long c = 0l;
   // boolean flag =false;
    public synchronized void parse(){
        i++;
        BiasDetails.countDownLatch.countDown();
    }

    public synchronized void biasParse(){
        i++;
    }

    public void noLockParse(){
        i++;
    }
}

(1)无锁状态

/**
     * 无锁下的性能 
     * 执行三次的耗时:836ms 859ms 779ms
     * @param a
     */
    public static void noLock(A a){

        long start = System.currentTimeMillis();
        //调用无锁方法来计算1000000000L的++,对比无锁和偏向锁的性能
        for(int i=0;i<1000000000L;i++){
            a.noLockParse();
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms", end - start));

    }

(2)偏向锁状态

/**
     * 偏向锁下的性能
     * 执行三次的耗时:18969ms 18699ms 18640ms
     * @param a
     */
    public static void bias(A a){

        long start = System.currentTimeMillis();
        //调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
        for(int i=0;i<1000000000L;i++){
            a.biasParse();
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms", end - start));

    }

(3)轻量级锁状态

 /**
     * CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,用来作为线程间的通信而不是互斥作用。
     * CountDownLatch中多个线程是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。
     */
    static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
……
/**
     * 轻量级锁下的性能
     * 执行三次的耗时:28313ms 28763ms 29495ms
     * @param a
     */
    public static void lightWeightLock(A a){

        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 2; i++) {
                new Thread(() -> {
                    while (countDownLatch.getCount() > 0 )
                        a.parse();

                }).start();
            }
            //线程调用countdownLatch.await(),通过其他线程调用countdownLatch.countDown()减少计数器,直到减少到0,被await()挂起的线程恢复执行。
            countDownLatch.await();
            long end = System.currentTimeMillis();
            System.out.println(String.format("%sms", end - start));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }

通过上述简单的代码实验,发现锁之间的性能差别还是很大的,说明synchronized的优化是必要的。

六、总结

 锁升级过程:偏向锁 → 轻量级锁 → 重量级锁

  • 初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改锁对象的MarkWord里的锁标志位)。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在锁对象头里),如果是则正常执行同步代码块。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
  • 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
  • 在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为锁定,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
  • 长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折中的做法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
  • 显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁。

        一个锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

你可能感兴趣的:(JAVA,java,JUC,synchronized,锁膨胀,高并发编程)