JUC_4 之 synchronized

168 阅读7分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

用户态和内核态

JDK早期,synchronized叫做重量级锁,因为申请锁资源必须通过kernel,系统调用

1629181234.jpg

使用JOL工具查看对象信息

在pom.xml中导入meaven依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

java代码演示

import org.openjdk.jol.info.ClassLayout;

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

打印出结果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)--markword                 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)--markword                 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)--klass pointer            e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           c8 f6 00 03 (11001000 11110110 00000000 00000011) (50394824)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看出,加锁之后,markword部分发生了变化,说明加锁的过程就是对markword做了修改。

对象在内存中的布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data) 和对齐填充(Padding)。

HotSpot虚拟机的对象头包括两部分信息:

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。占8个字节

  • 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。‐XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer 占4个字节,如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8个字节。

Hotspot 32位的实现 1629186509.jpg

Hotspot 64位的实现(马士兵)

1629187599.jpg

锁升级

锁升级过程流程图(马士兵)

1629188414.jpg

  • 偏向锁、自旋锁都是用户空间完成
  • 重量级锁是需要向内核申请

偏向锁

偏向锁是不用涉及锁竞争机制的,第一个线程获得锁,直接把自己的线程id写入mark word中。

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞 争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心 思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争 的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就 失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的 是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

默认开启偏向锁

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking

默认情况下偏向锁有个时延,默认是4秒。

Why?因为虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行撤销和锁升级的操作,效率极低 XX:BiasedLockingStartupDelay=0偏向锁延迟启动时间-XX:BiasedLockingStartupDelay=0 偏向锁延迟启动时间

如果设定上述参数

new Object()->101 偏向锁->线程ID为0->Anonymous BiasedLock

打开偏向锁,new出来的对象,默认就是一个可偏向的匿名对象101

如果有线程上锁

上偏向锁,指的就是把mark word的线程ID改为自己线程ID的过程

偏向锁不可重偏向、批量偏向、批量撤销

轻量级锁(自旋锁)

LR--Lock Record

由于出现锁竞争,先撤销偏向锁,升级为轻量级锁。多个线程用CAS操作将mark word设置为指向自己这个线程LR的指针,设置成功者得到锁。

虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情 况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线 程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。

重量级锁

锁竞争比较激烈,就去操作系统那里去申请资源

竞争加剧:有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程超过CPU核数的一半,1.6之后,加入自适应自旋Adapative Self Spinning,JVM自己控制。

升级重量级锁:向操作系统申请资源,Linux mutex,CPU从3级->0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后映射回用户空间。

面试题

为什么有自旋锁还需要重量级锁?

自旋锁是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

JVM启动过程,会有很多线程竞争(明确知道),所以默认情况启动时不打开偏向锁,过一段时间再打开