背景
在并发编程学习后,自己想说一下轻量级锁为什么比重量级锁的性能好,但是只想到了减少CAS操作这一点,感觉缺的有点多,于是搜索了下,在此做下记录
轻量级锁为什么比重量级锁的性能高那?
轻量级锁(Lightweight Locking)的性能优势确实部分来源于减少了CAS(Compare-And-Swap)操作的次数,但这并不是唯一的原因。轻量级锁的设计目标是通过减少线程阻塞和上下文切换来提高多线程环境下的性能。以下是轻量级锁相比重量级锁(基于Monitor对象的锁)具有更高性能的几个主要原因:
1. 减少CAS操作的频率
在轻量级锁状态下,锁记录存储在当前线程的栈中,而不是在堆中的Monitor对象中。这意味着:
- 获取锁时:当一个线程尝试获取轻量级锁时,JVM会使用CAS操作将对象头的Mark Word更新为指向当前线程栈中锁记录的指针。如果CAS成功,线程获得锁并继续执行。
- 释放锁时:当线程释放轻量级锁时,它只需要将对象头的Mark Word恢复为原始状态,而不需要再次进行CAS操作。这是因为锁记录是存储在当前线程的栈中的,线程可以直接访问和修改自己的栈。
相比之下,重量级锁需要更多的CAS操作:
- 获取锁时:线程需要通过CAS操作将对象头的Mark Word更新为指向堆中的Monitor对象的指针。
- 释放锁时:线程还需要通过CAS操作将对象头的Mark Word恢复为原始状态。
- 等待队列管理:重量级锁还涉及到更复杂的锁竞争处理,例如管理入口列表(Entry List)和等待集(Wait Set),这可能会导致更多的CAS操作和上下文切换。
因此,轻量级锁通过减少CAS操作的频率,尤其是在锁释放时避免了额外的CAS操作,从而提高了性能。
2. 避免线程阻塞和上下文切换
轻量级锁的一个重要特点是它允许线程在没有竞争的情况下快速获取和释放锁,而不需要进入操作系统级别的线程调度。具体来说:
- 无竞争情况下:如果只有一个线程尝试获取锁,轻量级锁可以通过一次成功的CAS操作直接获取锁,而不需要进入操作系统级别的线程调度。这避免了线程阻塞和上下文切换的开销。
- 有竞争情况下:如果多个线程竞争同一把锁,轻量级锁会进入自旋(Spin Lock)状态,即在一个短时间内的循环中不断尝试获取锁。自旋的时间通常很短,适用于锁的竞争不激烈且同步块较短的场景。如果自旋失败,锁会升级为重量级锁,线程才会被阻塞。
相比之下,重量级锁在每次锁竞争时都会导致线程阻塞,并进入操作系统级别的线程调度。线程阻塞和上下文切换是非常昂贵的操作,尤其是在高竞争场景下,会导致显著的性能下降。
3. 减少内存访问的开销
轻量级锁的锁记录存储在当前线程的栈中,而重量级锁的Monitor对象存储在堆中。栈的访问速度通常比堆快,原因如下:
- 栈的局部性:栈上的数据通常位于CPU缓存中,访问速度非常快。因为栈的空间是线程私有的,访问栈中的数据不需要跨线程同步,也不会引发缓存一致性问题。
- 堆的全局性:堆上的数据是共享的,多个线程可能同时访问同一个堆地址。为了保证数据的一致性,访问堆中的Monitor对象可能会引发缓存一致性问题,导致额外的内存屏障(Memory Barrier)和缓存失效(Cache Invalidations),增加了内存访问的开销。
因此,轻量级锁通过将锁记录存储在栈中,减少了对堆的访问,进一步提高了性能。
4. 减少锁竞争的概率
轻量级锁的设计还包括一些优化措施,以减少锁竞争的概率。例如:
-
偏向锁:在Java中,轻量级锁通常与偏向锁结合使用。偏向锁允许一个线程在第一次获取锁时,将锁偏向该线程,后续的同步操作可以避免频繁的加锁和解锁操作。只有当其他线程尝试获取同一把锁时,偏向锁才会撤销并升级为轻量级锁或重量级锁。这种机制减少了锁竞争的发生,进一步提高了性能。
-
自旋锁:轻量级锁在有竞争的情况下会进入自旋状态,而不是立即阻塞线程。自旋锁适用于锁的竞争不激烈且同步块较短的场景。通过自旋,线程可以在短时间内多次尝试获取锁,避免了线程阻塞和上下文切换的开销。
5. 总结
轻量级锁相比重量级锁具有更高的性能,主要原因包括:
- 减少CAS操作的频率:轻量级锁在锁释放时不需要进行CAS操作,减少了CAS的使用次数。
- 避免线程阻塞和上下文切换:轻量级锁在无竞争情况下可以快速获取和释放锁,避免了线程阻塞和上下文切换的开销。
- 减少内存访问的开销:轻量级锁的锁记录存储在当前线程的栈中,访问速度更快,减少了对堆的访问。
- 减少锁竞争的概率:轻量级锁结合偏向锁和自旋锁,减少了锁竞争的发生,进一步提高了性能。
因此,轻量级锁在大多数低竞争场景下能够提供更好的性能,尤其是在同步块较短且锁竞争不激烈的情况下。然而,在高竞争场景下,轻量级锁可能会频繁升级为重量级锁,此时重量级锁的性能优势会更加明显。