【JUC进阶】06. 轻量级锁

目录

1、前言

2、轻量级锁

2.1、什么是轻量级锁

2.2、工作原理

2.2.1、回顾Mark Word

2.2.2、工作流程

2.3、解锁

3、适用场景

4、注意事项

5、轻量级锁与偏向锁的对比

扫描二维码关注公众号,回复: 15491935 查看本文章

6、小结


1、前言

前面一节我们讲到了偏向锁。当偏向锁被撤销,或其他线程竞争的时候,偏向锁会撤销并且升级为轻量级锁。轻量级锁(Lightweight Lock)机制,它是一种介于偏向锁和重量级锁之间的锁实现。

2、轻量级锁

2.1、什么是轻量级锁

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

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

2.2、工作原理

2.2.1、回顾Mark Word

关于HotSpot对象头的具体内容,可以查看《【JUC进阶】03. Java对象头和内存布局》文章。

这里需要注意的是:对于对象头的设计,考虑到JVM的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存能够存储尽可能多的信息。如32位虚拟机中,对象未被锁定的状态下,有25bit用来存储对象hashcode,而当进入偏向模式后,存储hashcode的内存空间被用来存储线程ID和Epoch。

2.2.2、工作流程

  1. 初始状态:对象的对象头中的锁标志位为无锁状态。
  2. 获取锁:当线程尝试获取轻量级锁时,首先会将对象头中的锁标志位复制到线程的栈帧中的锁记录(Lock Record)中。
  3. CAS操作:线程使用CAS操作将对象头中的锁标志位替换为指向线程栈帧的指针。如果CAS操作成功,表示当前线程成功获取了锁,并进入临界区代码执行。如果CAS操作失败,说明存在竞争,进入下一步操作。

在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录 (Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 (官方为这份拷贝加了一个 Displaced 前缀,即 Displaced MarkWord)。

线程堆栈与对象头状态:

然后,虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位 (Mark Word 的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。

如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。

那么,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了。否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁(关于锁膨胀,后面会有专门文章来聊聊),锁标志的状态值变为“10”,此时 Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

当CAS操作成功,对象处于轻量级锁状态下,线程堆栈与对象头的状态:

2.3、解锁

上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向线程的锁记录,那就用CAS操作把对与对象的状态象当前的Mark Word 和线程中复制的Displaced MarkWord 替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

  1. 检查是否存在竞争:当线程要释放轻量级锁时,首先会检查自己是否存在竞争,即检查对象头中的锁标志位是否与线程栈帧中的锁记录相等。如果不相等,表示当前线程已经失去了锁的控制权,无法解锁,需要进行额外的处理。
  2. CAS操作释放锁:如果线程检查通过,说明当前线程仍然持有锁,可以使用CAS操作将对象头中的锁标志位恢复为无锁状态,表示锁已经释放。
  3. 自旋失败后恢复为重量级锁:如果CAS操作失败,即存在竞争,当前线程会将锁膨胀为重量级锁,进入阻塞状态。

3、适用场景

  1. 对象锁竞争不激烈:轻量级锁适用于多线程对同一对象进行频繁竞争的情况下。当竞争不激烈时,轻量级锁可以减少线程阻塞和唤醒的开销,提高程序的并发性能。
  2. 短时间的同步块:轻量级锁适用于同步块的执行时间较短的情况下。由于轻量级锁的获取和释放操作开销较小,适用于保护临界区代码执行时间较短的场景。
  3. 无锁或偏向锁失败:当对象处于无锁状态或偏向锁状态时,如果有其他线程尝试获取对象的锁,但偏向锁失败,那么会自动升级为轻量级锁。因此,轻量级锁适用于无锁或偏向锁的竞争情况下。

需要注意的是,轻量级锁并不适用于所有场景。当竞争激烈或同步块执行时间较长时,轻量级锁的性能可能不如重量级锁。此时,可以考虑使用其他同步机制,如重量级锁或并发集合类,以满足并发性能的需求。

代码示例:

public class LightweightLockDemo {
    private static int count = 0;

    public static void main(String[] args) {
        final LightweightLockDemo demo = new LightweightLockDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    demo.increment();
                }
            }).start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + count);
    }

    public synchronized void increment() {
        count++;
    }
}

上面的示例代码中,我们定义了一个LightweightLockDemo类,其中的increment()方法使用synchronized关键字来实现轻量级锁。在main()方法中,我们创建了5个线程,并通过调用increment()方法对count变量进行累加操作。每个线程都会多次调用increment()方法。最后,我们等待2秒钟,确保所有线程执行完毕后,打印出最终的count值。

4、注意事项

在使用轻量级锁时,需要注意以下事项和考虑优化的技巧:

  1. 减少锁的竞争:轻量级锁的性能取决于锁竞争的程度。因此,减少锁的竞争是提高轻量级锁性能的关键。可以通过减小同步块的粒度、减少锁的持有时间等方式来降低锁竞争。
  2. 避免锁的扩大:轻量级锁在竞争激烈的情况下可能会退化为重量级锁,这会增加线程的阻塞和唤醒开销。因此,需要尽量避免锁的扩大,即尽量减少锁的竞争范围,仅在必要时才使用锁。
  3. 注意锁的释放时机:轻量级锁的释放是通过线程解锁和CAS操作实现的。在释放锁之前,需要确保没有其他线程在竞争同一个锁。因此,在释放锁之前要仔细考虑同步块的执行流程,避免出现不正确的解锁操作。
  4. 合理设置线程优先级:线程的优先级可以影响轻量级锁的竞争情况。在高优先级线程和低优先级线程竞争同一个锁时,高优先级线程更有可能获取到轻量级锁。因此,根据实际需求和线程优先级的设置,合理调整锁的竞争情况。
  5. 使用适当的锁机制:轻量级锁适用于竞争不激烈、同步块执行时间较短的场景。如果竞争激烈或同步块执行时间较长,可以考虑使用其他锁机制,如重量级锁、读写锁等,以满足性能和并发性的需求。

5、轻量级锁与偏向锁的对比

  1. 竞争情况:轻量级锁和偏向锁都是为了减少锁竞争而设计的,但适用于不同的竞争情况。偏向锁适用于存在线程间交替获取锁的情况下,锁的竞争程度较低的场景。轻量级锁适用于锁的竞争程度较高的场景。
  2. 锁状态转换:偏向锁会经历无锁状态、偏向锁状态和轻量级锁状态三个状态的转换。当只有一个线程获取锁时,会进入偏向锁状态,无需加锁和解锁操作。当其他线程竞争同一个锁时,会将偏向锁升级为轻量级锁状态。而轻量级锁则会直接从无锁状态转变为轻量级锁状态。
  3. 锁的竞争程度:由于偏向锁是针对低竞争场景设计的,所以在竞争激烈的情况下,偏向锁的性能可能不如轻量级锁。轻量级锁采用CAS操作进行加锁和解锁,当多个线程竞争同一个锁时,会进行自旋操作,减少了线程的阻塞和唤醒开销。
  4. 锁的撤销:偏向锁在发生竞争时需要撤销偏向锁状态,回到无锁状态。而轻量级锁在发生竞争时会退化为重量级锁,需要使用互斥量进行加锁和解锁。因此,偏向锁的撤销操作开销较小,而轻量级锁的撤销操作开销较大。

偏向锁适用于线程间竞争较少的情况下,可以减少不必要的锁操作开销。

轻量级锁适用于锁竞争较多的情况下,通过自旋操作减少线程阻塞和唤醒的开销。

6、小结

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过 CAS 操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/131428923