Skip to content

Java 锁机制详解

· 11 min

Java对象头的组成#

Java对象头主要是由Mark WordClass Pointer(类指针)array length 组成

Mark Word#

biased_locklock状态
001无锁
101偏向锁
000轻量级锁
010重量级锁
011GC标记

下面是不同锁状态下的mark word信息:

image-20251120172525691

Class Pointer(类指针)#

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

array length#

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位

锁升级#

基于我们对mark word的了解,我们可以看到锁总共有以下几种:无锁、偏向锁、轻量级锁、重量级锁锁的级别 只升不降(除非发生批量撤销等特殊情况)

无锁#

对象创建时处于无锁状态,没有任何同步的开销

偏向锁#

触发条件:

  1. JVM 开启 -xx:+UseBiasedLocking (JDK 15 后默认关闭)

  2. 第一个线程进入 synchronized 块

JVM 将对象 Mark Word 中的 偏向锁标志设为 1,并且该线程下次再进来的时候就无需CAS,直接就可以获取到锁

轻量级锁#

触发条件:

  1. 第二个线程尝试获取锁时,偏向锁被撤销,升级为轻量级锁
  2. 偏向锁在jvm上被关闭的时候

会通过 CAS自旋获取锁

CAS:核心思想是乐观锁。尝试将对象头中的 Lock Record 指针指向自己线程的锁记录,如果获取失败线程会自旋等待

重量级锁#

触发条件:

  1. 多个线程竞争轻量级锁
  2. 线程自旋超过阈值(默认10次,可以通过-XX:PreBlockSpin调整)

Lock#

🔑 Lock 是“程序员可控的锁”,而 synchronized 是“JVM 自动管理的锁”。

Java 中的 Lock 接口是 JDK 1.5 引入的,比 synchronized 更灵活、功能更强大的显式锁机制。它提供了对锁的精细化控制,支持尝试获取锁、可中断等待、超时获取、公平锁等高级特性。

核心方法#

public interface Lock {
void lock(); // 阻塞获取锁(不可中断)
// 可中断地获取锁。
//线程在等待获取锁的过程中,可以响应中断(interrupt()),从而提前退出等待,而不是像 lock() 那样“死等”下去
void lockInterruptibly() throws InterruptedException;
boolean tryLock(); // 尝试获取锁,立即返回 true/false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 带超时的尝试
void unlock(); // 释放锁(必须手动调用!)
Condition newCondition(); // 创建等待/通知条件对象
}

请注意:使用 Lock 必须在 finally 块中释放锁,否则可能死锁!

Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须!
}

Lock 与 synchronized 区别#

相同点:

  1. 都可以保证线程的原子性、可见性、有序性
  2. synchronized 和 reentrantLock 都可以可重入

不同点

  1. synchronized 是JVM控制的,Lock是程序员手动控制
  2. Lock有lockInterruptibly()方法,可以中断阻塞中的锁,但是synchronized 只能闷着头阻塞
  3. synchronized锁只能让一个线程拥有,但是Lock中的读写锁中的读锁可以被多个线程持有
  4. Lock中可以自己设置公平锁和非公平锁,synchronized不行

我们接下来重点关注一些Lock的实现

image-20251121153106617

ReentrantLock源码解析#

接下来我会逐步分析从创建ReentrantLock对象到加锁过程的源码。

构造参数#

ReentrantLock中无参构造默认创建一个不公平锁,当然我们可以传入boolean值来指定创建

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

这里的NonfairSync()、FairSync()方法都是ReentrantLock内部实现的,我们继续分析

NonfairSync和FairSync#

acquire(int arg) 是AbstractQueuedSynchronizer(AQS)类里面的方法,他调用了tryAcquire(int arg)。Sync实现了tryAcquire(int arg)

//非公平锁
static final class NonfairSync extends Sync {
final void lock() {
//判断是不是第一次获取锁,如果是之直接独占
//compareAndSetState方法的意思是:以原子的方式对比state的值是否是0.如果是的话就设置为acquires
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//可以看到
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
=====================================================================
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//核心!!!!!!!
//先判断是否有线程排队,然后再对比state的值
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//这说明线程有锁
else if (current == getExclusiveOwnerThread()) {
//增加重试的次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//次数设置到state里面
setState(nextc);
return true;
}
return false;
}
}

可以看到,公平锁的实现和非公平锁的本质区别是tryAcquire()方法的实现。

Sync#

abstract static class Sync extends AbstractQueuedSynchronizer {
//获取锁
abstract void lock();
//非公平的方式获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取state值,这个state是记录线程获取的次数
int c = getState();
if (c == 0) {
//compareAndSetState方法的意思是:以原子的方式对比state的值是否是0.如果是的话就设置为acquires
if (compareAndSetState(0, acquires)) {
//设置锁被当前线程独占
setExclusiveOwnerThread(current);
return true;
}
}
//这说明线程有锁
else if (current == getExclusiveOwnerThread()) {
//增加重试的次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//次数设置到state里面
setState(nextc);
return true;
}
return false;
}
}

总结#

读完源码相信大家对ReentrantLock有了新的理解。下面有两个问题请大家思考:

  1. reentrantlock怎么做到锁重入的?
  2. 公平锁和非公平锁的原理是什么?

读写锁 ReentrantReadWriteLock#

之前我们提到的锁都是排他锁,这次这个锁是可以允许多个线程进行读访问的。但是在写线程访问的时候,所有的读写都被阻塞

实现原理#

他的原理类似我们上面讲到的ReentranLock,也是内部维护了一个AQS,只不过这次AQS的state属性不能通过累加了,而是高16位维护读锁,低16位维护写锁来实现读写分离。

读锁支持重入,并且不管什么线程获取,读状态都增加,写锁不支持重入

读锁#

  1. 没有写线程访问的时候,读锁随时都可以获取到
  2. 如果当前线程获取读锁的时候,已经有写锁占用,那么进入等待状态
  3. 读锁支持重入,并且不管什么线程获取,读状态都增加

写锁#

  1. 写锁是支持重进入的排他锁,如果当前线程获取写锁,那么写状态增加
  2. 当写的状态为 0 时释放锁

锁降级#

锁降级指的是写锁降级为读锁。当前拥有写锁的线程,再获取一次读锁,然后释放自己写锁的过程

流程:拥有写锁的线程 获取读锁 释放写锁