Java多线程——synchronized同步锁的原理深度解析以及多线程加锁优化

Monitor

解释synchronized同步远离之前,首先了解一下Monitor(翻译为监视器,又叫管程,基于操作系统)。

首先明确的是每一个Java对象都可以关联一个Monitor对象,当使用synchronized关键字的时候,该对象就会与一个Monitor对象关联,即该对象的Mark word中就会设置一个指向Monitor对象的指针(一个普通的对象包含64 bits的Object head、32 bits的Mark word 以及32 bits的Klass word)。

对象头格式:

重量级锁加锁原理

  1. 当一个线程 T1 尝试获取到锁了时,被synchronized指定的锁对象与一个Monitor产生了关联,此时如果锁对象没有被其他线程持有,那当前尝试获取锁的线程 T1 将成为这个Monitor的所有者,即Monitor对象中的owner描述的是线程 T1
  2. 此时如果有其他线程 T2 尝试获取锁的时候,就会找synchronized指定的锁对象,看看这个锁对象关联的Monitor是谁,找到了Monitor就会查看该Monitor是否有owner的描述,也就是说看看是否有线程持有这个Monitor。此时发现该Monitor的持有者是线程 T1,此时线程 T2 将被记录在该Monitor的EntryList中,这个EntryList就是存储了在等待该锁资源的线程集合的描述,EntryList中的线程都将进入等待状态(阻塞)。
  3. 当持有Monitor的线程 T1 执行完临界区(加锁代码)业务之后,会释放了锁资源,即Minitor中owner的描述不再指向 T1,此时Monitor会将EntryList中的阻塞线程都唤醒(wake up),然后这些等待的线程将会竞争锁资源,会有一个新的线程称为Monitor的持有者。

轻量级锁

  • 00 代表轻量级锁
  • 10 代表重量级锁
  • 01 代表没有枷锁

使用场景:如果一个对象被多线程访问,但是访问它的多线程的时间是错开的,并不存在同一时间竞争的情况,就可以使用轻量级锁来优化处理;轻量级锁的使用仍然是通过synchronized关键字。

  • 当使用synchronized关键字之后,首先在线程的栈帧中创建一个Lock Record,Lock Record中包含两部分——(Object reference),(lock record 地址 00),用于记录锁对象的引用以及锁对象的Mark word。加锁的时候,首先Object reference记录了锁对象的引用,然后用(lock record 地址 00)与锁对象的Mark word(hashcode age bias 01)交换;如果交换成功,就是锁已经加上了,此时锁对象的Mark word中是(lock record 地址 00),要注意的是只有锁对象Mark word中是 01 的时候才能加锁成功。这个交换的过程就是CAS(原子操作)
  • 如果加锁的过程中,锁对象的Mark word不是 01,已经有其他线程持有了该锁对象,此时加锁失败,进入锁膨胀状态
  • 如果是当前线程发生了锁重入的情况,就会再添加一条Lock record作为记录,新记录中(Object reference)同样需要记录锁对象的引用,但是交换锁对象的Mark word失败(实际上是在交换的过程中,能判断出锁对象Mark Word已经和本线程的一条Lock record交换过了,就不再进行交换了,直接为null)。
  • 当释放锁的时候,如果有Lock record记录中为null的记录,表示有锁重入,清掉这条Lock record就行了,重入计数【减1】,直到释放到Lock record不为null的时候,就要开始做真正的锁释放:还原锁对象的Mark word,再交换回去,就解锁成功了。
  • 如果解锁失败,说明轻量级进行了锁膨胀或者已经升级为重量级锁,此时就要进入重量级锁解锁流程

锁膨胀

如果在加轻量级锁的过程中,CAS失败了,也就是说已经有其他线程为该锁对象已经加上了轻量级锁,此时出现了线程间的锁竞争,这是需要将轻量级锁升级为重量级锁,这就是锁膨胀。当出现锁膨胀的时候,就回到了synchronized加锁原理的情况。

如下:

  1. t1 目前对 object 加上了轻量级锁,交换了 mark word,此时 t2 对 object 进行CAS操作失败,进入锁膨胀流程。
  2. t2 为 object 申请 Monitor,即 object 的Mark word指向了 Monitor地址,此时变成了(monitor 10)。
  3. t2 进入 Monitor 的EntryList,开始阻塞。

重量级锁解锁流程

很明显 t1 在释放锁的时候,无法还原 object 的mark word,因为交换不回去了,object 的mark word 已经变成(monitor 10)了。

  1. t1 根据 object 的mark word 找到 monitor,设置 monitor的owner为空,
  2. 唤醒EntryList里面等地的线程,开始新一轮的竞争

自旋优化

上面在描述重量级锁的时候,提到了竞争失败锁对象的线程将进入Monitor的EntryList阻塞,其实这是简单的说法;实际上在进入EntryList之前,这些线程还进行了自旋优化。

什么是自旋优化,指的就是在竞争失败锁资源之后,在进入EntryList之前,线程会进行若干次获取锁资源的重试操作

如果当前持有锁对象的线程执行的很快,可能就在自旋重试的过程中,就释放了锁,被处于自旋重试过程的线程获取到了,就不用进入EntryList,不用进入阻塞状态,也就避免了一些线程间切换的情况。当然自旋优化只能在多核cpu上使用。

  • java6之后自旋重试是自适应的,也就是说该锁对象上的线程自旋重试成功率大,就会多自旋几次,成功率小就会减少自旋次数,甚至不自旋。
  • 单核自旋优化没有意义
  • java7之后不能设置是否开启自旋优化

偏向锁

在重入锁的情况下,轻量级锁每次重新获取锁对象的时候还是需要CAS操作。

为了优化这些开销,java6之后引入了偏向锁来进一步优化加锁的开销:只有第一次获取锁的时候,通过CAS将线程的id设置到锁对象Mark word,之后的重入动作,发现线程id就是本线程之后,就不会执行CAS。只要不发生锁竞争,这个锁对象就归这条线程持有

一个对象创建时:

  • 如果开启偏向锁(默认开启),那么对象创建后Mark word的最后三位是 1 01,这时thread、epoch、age都是0
  • 如果没有开启偏向锁,那么对象创建后Mark word的最后三位是 0 01,hread、epoch、age都是0
    /*
     * 功能描述: <br>
     * 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
     * @Param: []
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/12/1 15:47
     */
    public void testBiasedMarkword() {
        log.info(ClassLayout.parseInstance(new Person()).toPrintable());
    }

    class Person {

    }

    public static void main(String[] args) {
        BiasedTest biasedTest = new BiasedTest();
        biasedTest.testBiasedMarkword();
    }

执行结果:
15:52:24.893 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
 OFFSET  SIZE                                                          TYPE DESCRIPTION                               VALUE
      0     4                                                               (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                                               (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                                               (object header)                           94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
     12     4   com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0                             (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

为什么最后三位不是1 01呢?不是说好默认是开启偏向锁的嘛

  • 偏向锁是有延迟的,不会在程序启动的时候立刻生效,如果想避免这种情况,可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟,也可以在程序启动之后sleep一下,等待偏向锁生效

修改代码增加延迟:

    /*
     * 功能描述: <br>
     * 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
     * @Param: []
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/12/1 15:47
     */
    public void testBiasedMarkword() throws InterruptedException {
        log.info(ClassLayout.parseInstance(new Person()).toPrintable());
        Thread.sleep(4*1000);
        log.info(ClassLayout.parseInstance(new Person()).toPrintable());
    }

执行结果如下:

配置VM参数:

测试偏向锁特性

修改代码如下:

    /*
     * 功能描述: <br>
     * 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
     * @Param: []
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/12/1 15:47
     */
    public void testBiasedMarkword() throws InterruptedException {
        Person person = new Person();
        //Thread.sleep(5*1000);//偏向锁有延迟性,并不会在程序启动后马上生效,所以sleep,也可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟
        log.info(ClassLayout.parseInstance(person).toPrintable());
        synchronized (person) {
            log.info(ClassLayout.parseInstance(person).toPrintable());
        }
        log.info(ClassLayout.parseInstance(person).toPrintable());
    }

执行结果:
16:17:20.587 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
 OFFSET  SIZE                                                          TYPE DESCRIPTION                               VALUE
      0     4                                                               (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                                                               (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                                               (object header)                           94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
     12     4   com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0                             (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

16:17:20.590 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
 OFFSET  SIZE                                                          TYPE DESCRIPTION                               VALUE
      0     4                                                               (object header)                           05 38 fd 02 (00000101 00111000 11111101 00000010) (50149381)
      4     4                                                               (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                                               (object header)                           94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
     12     4   com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0                             (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

16:17:20.591 [main] INFO com.leolee.multithreadProgramming.concurrent.syn.BiasedTest - com.leolee.multithreadProgramming.concurrent.syn.BiasedTest$Person object internals:
 OFFSET  SIZE                                                          TYPE DESCRIPTION                               VALUE
      0     4                                                               (object header)                           05 38 fd 02 (00000101 00111000 11111101 00000010) (50149381)
      4     4                                                               (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                                               (object header)                           94 ff 00 f8 (10010100 11111111 00000000 11111000) (-134152300)
     12     4   com.leolee.multithreadProgramming.concurrent.syn.BiasedTest Person.this$0                             (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看到Person对象一直都是 101,是一个偏向锁,当加锁执行后,Person对象的Mark word中后面几位变化了,那就是指向的线程id(和Java中Thread.getId()不一样,这是操作系统中线程的标识),并在锁释放后,这个线程id还是存在与Mark word中,这就证明了偏向锁的偏向性:只要没有其他线程的资源竞争,这个锁对象就归这条线程持有

禁用偏向锁

设置VM参数:-XX:-UseBiasedLocking 可禁用偏向锁

当偏向锁禁用之后,该对象被一条线程持有之后,将变成轻量级锁:

使用HashCode导致偏向锁失效

    /*
     * 功能描述: <br>
     * 〈验证偏向锁对象头的mark word信息,需要借助jol-core包〉
     * @Param: []
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/12/1 15:47
     */
    public void testBiasedMarkword() throws InterruptedException {
        Person person = new Person();
        //偏向锁有延迟性,并不会在程序启动后马上生效,所以sleep,也可以添加VM参数: -XX:BiasedLockingStartupDelay=0 来禁用延迟
        //可以使用-XX:-UseBiasedLocking 禁用偏向锁,此时该对象加锁后会编程 000
        //Thread.sleep(5*1000);

        //获取hashcode后导致偏向锁失效
        log.info(String.valueOf(person.hashCode()));

        log.info(ClassLayout.parseInstance(person).toPrintable());
        synchronized (person) {
            log.info(ClassLayout.parseInstance(person).toPrintable());
        }
        log.info(ClassLayout.parseInstance(person).toPrintable());
    }

执行结果:

此时后面几位代表Hashcode

偏向锁的撤销

  • 多个线程访问同一个对象,偏向锁被撤销,升级为轻量级锁
    /*
     * 功能描述: <br>
     * 〈测试多个线程访问同一个对象,偏向锁被撤销〉
     * @Param: []
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/12/1 16:57
     */
    public void testBiasedLockUnusable() {

        Person person = new Person();

        Thread t1 = new Thread(() -> {
            log.info(ClassLayout.parseInstance(person).toPrintable());
            synchronized (person) {
                log.info(ClassLayout.parseInstance(person).toPrintable());
            }
            log.info(ClassLayout.parseInstance(person).toPrintable());
        }, "t1");

        Thread t2 = new Thread(() -> {

            try {
                //保证t2在t1执行完成之后再执行
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            log.info(ClassLayout.parseInstance(person).toPrintable());
            synchronized (person) {
                log.info(ClassLayout.parseInstance(person).toPrintable());
            }
            log.info(ClassLayout.parseInstance(person).toPrintable());
        }, "t2");

        t1.start();
        t2.start();
    }

执行结果:

  • 使用 wait() 和 notify() 也会撤销偏向锁,因为这两个是重量级锁才有的方法

批量重偏向

如果锁对象被多个线程没有竞争的访问,原先偏向线程t1的对象还是可以偏向t2的,这就是重偏向,此时会重置Mark word中的线程id。

注意:当一个对象的偏向锁被撤销次数过多,超过了一定的阈值,JVM会考虑这是否是偏向错误了,是不是应该偏向另外的线程,所以这时候就会触发重偏向。阈值为20。

因为撤销偏向也是一种性能的开销,所以才有此机制来重新设定偏向的线程

代码省略了,自行模拟20次以上没有竞争的其他线程获取偏向锁对象。

批量撤销偏向

当撤销偏向的次数超过阈值40的时候,对于此类所产生的对象竞争过于激烈,JVM将考虑不再对此类实例化出来的对象添加偏向锁,包括新建的对象。

锁消除

java程序在运行的时候,有一个JIT(即时编译器),实际就是对开发者的代码进行一定程度的优化,这个优化过程中,可能在一定情况下,优化掉synchronized。

比如一个同步代码块,锁对象是在方法内的局部变量,并且此局部变量不会发生变量逃离,此时JIT就会把synchronized优化掉。此时加锁的代码和不加锁的代码执行没有区别(性能也是没有区别的)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值