30分钟彻底弄懂 synchronized 锁升级过程

在Java的并发编程领域中,我们进行会使用到锁这个东西,例如在多线程环境下为了预防某些线程安全问题,这里面可能会产生一些预想不到的问题,所以下边我整理了一系列关于JDK中锁的问题,帮助大家更加深入地了解它们。

synchronized真的是重量级锁嘛?

这个问题相信大部分人在面试的时候都有遇到过,答案是否定的。这个要看JDK的版本来进行判断。如果JDK的版本在1.5之前使用synchronized锁的原理大概如下:

  1. 给需要加锁的资源前后分别加入一条“monitorenter”和“monitorexit”指令。

  2. 当线程需要进入这个代码临界区的时候,先去参与“抢锁”(本质是获取monitor的权限)

  3. 抢锁如果失败,就会被阻塞,此时控制权只能交给操作系统,也就会从 user mode 切换到 kernel mode, 由操作系统来负责线程间的调度和线程的状态变更, 需要频繁的在这两个模式下切换(上下文转换)。

可以看出,老模式的条件下去获取锁的开销是比较大的,所以后来JDK的作者才会在JDK中设计了Lock接口,采用CAS的方式来实现锁,从而提升性能。

但是当竞争非常激烈的时候,采用CAS的方式有可能会一直获取不到锁的话,不管进行再多的CAS也是在浪费CPU,这种状态下的性能损耗会比synchronized还要高。所以这类情况下,不如直接升级加锁的方式,让操作系统介入。

正因为这样,所以后边才会有了锁升级的说法。

synchronized的锁升级

偏向锁

synchronized进行升级的过程中,第一步会升级为偏向锁。所谓偏向锁,它的本质就是让锁来记住请求的线程。

在大多数场景下,其实都是单线程访问锁的情况偏多,JDK的作者在重构synchronized的时候,给对象头设计了一个bit位,专门用于记录锁的信息,具体我们可以通过下边这个实际案例来认识下:

public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    System.out.println("还没有进入到同步块");
    System.out.println("markword:" + ClassLayout.parseInstance(o).toPrintable());
    //默认JVM启动会有一个预热阶段,所以默认不会开启偏向锁
    Thread.sleep(5000);
    Object b = new Object();
    System.out.println("还没有进入到同步块");
    System.out.println("markword:" + ClassLayout.parseInstance(b).toPrintable());
    synchronized (o){
        System.out.println("进入到了同步块");
        System.out.println("markword:" + ClassLayout.parseInstance(o).toPrintable());
    }
}

注意要引入一些第三方的依赖,辅助我们查看对象头的信息:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    //这个版本号的不同,查看的内容格式也不同
    <version>0.16</version>
</dependency>

控制台输出的结果如下:

还没有进入到同步块
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
markword:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


还没有进入到同步块
markword:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


进入到了同步块
markword:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007000050ee988 (thin lock: 0x00007000050ee988)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

这个案例中,如果你仔细观察控制台的内容,可以发现,当JVM刚启动的时候,对象头部的锁标志位是无锁状态。但是过了一整子(大概4秒之后),就会变成一个biasable的状态。如果需要调整这个延迟的时间,可以通过参数 -XX:BiasedLockingStartupDelay=0 来控制。

这里我解释下biasable的含义:

biasable是JVM帮我们设置的状态,在这种状态下,一旦有线程访问锁,就会直接CAS修改对象头中的线程id。如果成功,则直接升级为偏向锁。否则就会进入到锁的下一个状态--轻量级锁。

ps:JVM因为在启动预热的阶段中,会有很多步骤使用到synchronized,所以在刚启动的前4秒中,不会直接将synchronized锁的标记升级为biasable状态。这是为了较少一些无必要的性能损耗。

轻量级锁

当锁被一个线程访问的时候,它会变成偏向锁的状态,那么当新的线程再次访问该锁的时候,锁会有什么变化吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 非常感谢您的问题。synchronized升级过程是Java中的优化技术之一,它可以减少的竞争,提高程序的执行效率。在Java中,每个对象都有一个(也称为内置或监视器),线程在执行同步代码块时会获取该对象的,并在代码块执行完成后释放该。当多个线程同时访问同一个对象的同步代码块时,就会出现竞争的情况,影响程序的执行效率。 在Java6之前,synchronized的实现存在较大的性能问题,当多个线程竞争同一个时,会导致线程频繁地进入和退出阻塞状态,增加了线程切换的开销,降低了程序的执行效率。为了解决这个问题,Java6引入了偏向、轻量级和重量级等优化技术,并采用升级的方式在不同之间进行转换,从而提高程序的执行效率。 具体来说,当一个线程第一次进入同步代码块时,会先尝试获取对象的偏向,如果对象没有被其他线程访问过,那么当前线程会获取到该对象的偏向,并将当前线程ID记录在对象头中,以后该线程每次进入同步代码块时都无需重新获取,直接使用偏向即可。如果有其他线程访问了该对象,那么当前线程会尝试将偏向升级为轻量级。轻量级是一种自旋,它采用CAS(Compare And Swap)指令来保证线程的原子性,避免了线程阻塞和唤醒的开销。如果当前线程获取轻量级成功,那么就可以在自旋过程中执行同步代码,避免了线程阻塞和唤醒的开销。如果轻量级获取失败,那么当前线程就会尝试将轻量级升级为重量级,此时当前线程会阻塞等待其他线程释放该对象的,直到获取到重量级才能执行同步代码。 总之,synchronized升级过程是Java中的优化技术之一,它可以提高程序的执行效率,减少的竞争。 ### 回答2: synchronized是Java中用于保证多个线程访问同一个共享资源时的线程安全的关键字。当一个线程想要执行synchronized修饰的同步代码块时,需要先获取对象的,如果被其他线程占用,则当前线程会被阻塞,直到获取到为止。 synchronized升级过程分为偏向、轻量级和重量级三个阶段。 首先,在没有竞争的情况下,将被优化为偏向。偏向的作用是,当一个线程获取到时,会在对象的头部标记记录该线程的ID。在下次该线程再次申请时,无需再次进行同步操作,直接获取即可。 然后,如果有多个线程竞争同一个,偏向将转为轻量级。这时每个线程都会在自己的线程栈帧中保存的副本。线程会通过CAS(比较并交换)来进行的获取和释放,而不再阻塞线程。 最后,如果多个线程仍然竞争同一个,轻量级升级为重量级。重量级的实现是利用操作系统提供的互斥量机制,当一个线程获取后,其他线程将被阻塞,直到持有的线程释放升级过程在多线程环境下进行,根据的状态切换来提高并发效率。通过合理地选择的类型以及的级别,可以更好地平衡性能与安全性之间的关系。 ### 回答3: synchronized升级过程是指在Java中保证多线程访问同步代码时的一种优化机制。其主要目的是提高多线程并发访问共享资源时的性能和效率。 当一个线程尝试进入同步代码块时,会先尝试获取对象的无状态。如果成功获取无状态,则可以直接执行同步代码,并将对象标记为偏向。这是的第一级别,也是最轻量级的。如果在此时另一个线程也想要进入同步代码,就会造成竞争。 如果存在竞争,偏向就会升级为轻量级。轻量级是通过在对象头中的标识字段中记录指向线程栈中记录的指针来实现的。如果线程竞争太激烈,轻量级就会升级为重量级。 重量级是指同步代码块被多个线程访问时,会将线程阻塞并等待释放。重量级采用操作系统的互斥量实现,所以比较耗时和耗资源。 在升级过程中,的状态会从无状态到偏向,再到轻量级,最后到重量级。在逐步升级过程中,的开销也会逐渐增加。 需要注意的是,在JDK 6之后,引入了消除和膨胀机制。消除指的是JVM在编译器优化时发现某些代码分支中不存在线程竞争时,会去除相应的操作;膨胀指的是JVM会根据竞争情况,将轻量级升级为重量级。 综上所述,synchronized升级过程是为了提高多线程并发访问同步代码时的性能和效率。通过从无状态到偏向,再到轻量级,最后到重量级升级过程,JVM可以根据竞争情况选择最适合的状态,以实现最佳的性能和资源利用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值