0%

浅谈Java锁机制

概述

Java并发编程中的锁机制是保证多线程安全的关键。本文将探讨Java中的无锁、偏向锁、轻量级锁、重量级锁。同时,我们也会比较这些锁与 java.util.concurrent 包中的 Lock 类之间的差异。

简单的来说,偏向锁,轻量级锁,重量级锁分别解决三个问题:

  1. 只有一个线程进入临界区
  2. 多个线程交替进入临界区
  3. 多个线程同时进入临界区

而锁的升级过程是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

Java对象头与Mark Word

Java对象在内存中的布局通常包括三部分:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。而在研究锁机制之前,了解Java对象头和其中的Mark Word是很有必要的。

对象头

对象头包含两部分信息:Mark Word和类型指针。类型指针用于确定对象的类。Mark Word则存储了对象的锁状态信息、哈希码、GC分代年龄等。

Mark Word

Mark Word是实现轻量级锁和偏向锁的关键。它的值会随着锁标记的变化而变化,包含以下几种状态:

锁状态内存结构(以32位的JVM为例)
25bit4bit1bit2bit
23bit2bit是否偏向锁锁标志位
无锁对象的HashCodeGC分代年龄001
偏向锁线程IDEpochGC分代年龄101
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁的指针10
GC标记11

这些锁状态通过CAS操作来改变,从而实现不同级别的锁。

无锁(Lock-Free)

定义

无锁是指在多线程环境中不使用传统的锁机制来控制对共享资源的访问,而是依赖原子操作保证线程安全。无锁状态不是指没有使用任何同步机制,而是指没有使用阻塞锁。在无锁编程中,通常使用原子操作(如CAS - Compare and Swap)来实现线程安全。这些操作保证在同一时间只有一个线程能够修改共享资源,从而避免了锁的使用。

示例

AtomicInteger atomicInt = new AtomicInteger(0);

public void increment() {
atomicInt.incrementAndGet();
}

AtomicInteger 提供了一种无锁的方式来确保多线程环境下的线程安全。

偏向锁(Biased Locking)

定义

偏向锁是为单一线程访问优化的锁机制。它在锁对象上存储线程ID,使得同一线程的后续锁请求无需再进行完整的同步。

注:JDK15废弃了偏向锁

示例

public synchronized void biasedLockMethod() {
// 偏向锁代码区
}

在JVM中,默认启用偏向锁。首次进入同步块的线程会激活偏向锁。

轻量级锁(Lightweight Lock)

定义

轻量级锁是在偏向锁遇到竞争时的一种状态,介于偏向锁和重量级锁之间。轻量级锁在遇到竞争时,会使用一种称为”自旋”的技术来避免线程立即进入阻塞状态。自旋锁的基本思想是:当线程想要获取锁而锁已被其他线程占用时,线程不会立即挂起,而是在一段时间内循环检查锁是否已被释放,期望在很短的时间内获取到锁,从而减少线程挂起的开销。自旋锁在轻量级锁中是一个重要的优化,它能在适当的场景下提高程序性能,但也需要根据具体情况谨慎使用以避免资源浪费。

线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两个或两个以上的线程竞争同一个锁),则轻量级锁会升级成重量级锁。

示例

public class LightweightLockExample {
private Object lock = new Object();

public void doSomething() {
synchronized (lock) {
// 在这里,当偏向锁遇到竞争时,JVM会自动升级为轻量级锁
// 执行一些操作
}
}
}

在这个示例中,synchronized关键字用于在lock对象上同步。当这个方法被多个线程频繁调用,但并不满足重量级锁的条件(即线程阻塞不是很严重),JVM 会将偏向锁升级为轻量级锁。轻量级锁通过CAS操作和自旋来减少线程阻塞的开销,提高效率。

这个例子本身并不能直接展示轻量级锁的工作,因为这是JVM内部的实现细节。但它可以用来模拟可能触发轻量级锁升级的场景。轻量级锁的转换和工作机制通常是透明的,不需要(也不能)由开发者显式控制。

重量级锁(Heavyweight Lock)

定义

重量级锁是通过操作系统的互斥量实现的锁,当轻量级锁竞争激烈时会升级为重量级锁。

示例

public synchronized void heavyWeightLockMethod() {
// 在高竞争下使用重量级锁
}

多线程竞争同一个锁时,可能会导致轻量级锁升级为重量级锁。

比较java.util.concurrent.Locksynchronized

java.util.concurrent 包中的 Lock 类(如 ReentrantLock)与 synchronized 关键字所使用的内置锁机制(偏向锁、轻量级锁、重量级锁)是不同的。Lock 类提供了一种更为灵活、丰富的线程同步机制,如可中断的锁获取、公平性选择等。以下是一些关键点:

  1. 锁的类型Lock 接口及其实现类(如 ReentrantLock)通常不会使用到JVM内部的偏向锁、轻量级锁、重量级锁。相反,它们通常直接依赖于操作系统的同步机制(类似于重量级锁),或者通过复杂的内部策略来提供锁功能。

  2. 更多控制:与 synchronized 不同,Lock 类提供了更多的控制能力,比如尝试非阻塞地获取锁(tryLock),可中断的锁获取,以及公平性(公平锁或非公平锁)。

  3. 公平性选项:例如,ReentrantLock 允许你选择是创建一个基于公平性原则的锁(先来先服务),还是一个非公平锁。synchronized 不提供这种选项。

  4. 性能和特性:虽然 synchronized 在近期的Java版本中得到了显著优化,但在某些复杂的同步场景中,Lock 类可能提供更好的性能和灵活性。

  5. 功能丰富Lock 接口还提供了一些 synchronized 无法提供的功能,如条件变量(Condition),这为线程间的通信提供了更多的可能性。

总而言之,java.util.concurrent 包中的 Lock 类并不直接使用JVM内部的锁状态(偏向锁、轻量级锁、重量级锁),而是提供了一个更高级、更灵活的线程同步机制。这些锁通常用于更复杂的并发场景,其中需要比 synchronized 提供的更细粒度的锁控制。

结论

理解Java中这四种锁及其适用场景,以及它们与 Lock 类的区别,对于编写高效、线程安全的并发程序至关重要。开发者应根据实际应用场景选择合适的锁机制。