Java自定义互斥锁的实现方法示例
作者:va学弟
今天我们利用AQS实现一个基础的自定义的互斥锁。
1.AQS
AbstractQueueSynchronizer,简称AQS,是 Java 并发编程的核心基础框架,它为各种同步器(如ReentrantLock、Semaphore、CountDownLatch等)提供了统一的底层实现,解决了并发场景中线程的同步与协作问题。它的设计思想可以概括为:用一个 volatile 状态变量控制同步逻辑,用一个 CLH 队列管理等待线程。下面我们简单介绍一下其核心内容。
一、AQS 的核心结构
AQS 的核心由两部分组成:同步状态(State) 和等待队列(CLH 队列)。
1.同步状态(State)
定义:AQS 通过 private volatile int state 变量来维护同步状态,该状态值的具体语义由实现类(子类)自行定义。以 ReentrantLock 为例:
state = 0表示锁未被占有state > 0表示锁被持有,数值代表当前线程的重入次数(线程每次获取锁时 state 递增,释放锁时 state 递减)
2. 等待队列(CLH 队列)
当线程获取锁失败时,AQS 会将线程封装成一个节点(Node) 加入等待队列,这个队列是一个双向链表,基于 CLH(Craig, Landin, and Hagersten)锁队列改进而来。
二、AQS 的核心机制
AQS(AbstractQueuedSynchronizer)的核心机制分为独占式和共享式两种同步模式,分别对应不同的锁获取与释放逻辑。
1.独占式模式(Exclusive)
独占式模式下,同一时刻只有一个线程能获取锁(如 ReentrantLock)。其核心流程如下:
获取锁(acquire)
调用 tryAcquire(int arg):由子类实现,尝试获取锁(如检查 state 是否为 0,并通过 CAS 修改 state)。若成功,直接返回;若失败,进入后续步骤。
调用 addWaiter(Node.EXCLUSIVE):将当前线程封装为独占模式的 Node 节点,并加入等待队列尾部。
调用 acquireQueued(Node node, int arg):节点在队列中自旋等待,直到获取锁或被取消。在此过程中:
- 节点检查前驱节点是否为头节点:若是,则再次尝试
tryAcquire;若成功,将自己设为新头节点。 - 若前驱节点状态不是
SIGNAL,则将其状态改为SIGNAL(确保前驱释放锁时能唤醒自己)。 - 若获取失败,通过
LockSupport.park(this)阻塞当前线程。
释放锁(release)
调用 tryRelease(int arg):由子类实现,尝试释放锁(如修改 state 的值)。
若成功,检查头节点的状态:若为 SIGNAL,则调用 unparkSuccessor(Node node) 唤醒后继节点。
被唤醒的后继节点会重新进入自旋,尝试获取锁。
2.共享式模式(Shared)
共享式模式下,同一时刻多个线程可获取锁(如 Semaphore、CountDownLatch)。其核心流程如下:
获取锁(acquireShared)
调用 tryAcquireShared(int arg):由子类实现,尝试获取共享锁(如检查 state 是否大于 0)。
若返回值 >=0(成功),直接返回;若 <0(失败),进入后续步骤。
调用 doAcquireShared(int arg):将线程封装为共享模式的 Node 节点,加入等待队列并自旋等待。
与独占式不同,共享模式下,一个节点获取锁后可能会唤醒后续所有共享节点(如 Semaphore 的 release 会释放多个许可)。
释放锁(releaseShared)
调用 tryReleaseShared(int arg):由子类实现,尝试释放共享锁(如增加 state 的值)。
若成功,唤醒后续等待的共享节点。
三、AQS 的核心方法
AQS 基于模板方法模式设计,将同步器的核心逻辑分为两部分:父类定义的固定流程(模板方法)和子类实现的定制逻辑(钩子方法)。
1.模板方法(AQS 已实现)
独占式锁操作
acquire(int arg):阻塞式获取独占锁,忽略中断。release(int arg):释放独占锁,唤醒后续线程。tryAcquireNanos(int arg, long nanosTimeout):支持超时和中断的独占锁获取。
共享式锁操作
acquireShared(int arg):阻塞式获取共享锁。releaseShared(int arg):释放共享锁,唤醒等待线程。tryAcquireSharedNanos(int arg, long nanosTimeout):支持超时和中断的共享锁获取。
辅助功能
getQueuedThreads():获取等待队列中的线程集合。
2.钩子方法(子类需重写)
独占式锁实现
tryAcquire(int arg):定义独占锁的获取逻辑,返回成功状态。tryRelease(int arg):定义独占锁的释放逻辑,返回成功状态。
共享式锁实现
tryAcquireShared(int arg):定义共享锁的获取逻辑,返回剩余可用资源数。tryReleaseShared(int arg):定义共享锁的释放逻辑,返回是否完全释放。
条件变量支持
isHeldExclusively():判断当前线程是否独占持有锁,用于实现 Condition。
通过重写钩子方法,子类可灵活实现公平/非公平锁、读写锁等同步机制,而 AQS 负责队列管理、阻塞唤醒等底层操作。
2.自定义互斥锁
MLock通过静态内部类Sync继承 AQS,并重写关键方法,实现了一个独占式非重入锁(同一线程不能重复获取锁)。
public class MLock {
private Sync sync = new Sync();
private static class Sync extends AbstractQueuedLongSynchronizer{
@Override
protected boolean tryAcquire(long arg) {
//利用CAS算法把state变量改成1
if(compareAndSetState(0,arg)){
//操作成功后把当前线程设置成独占
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(long arg) {
//清空当前线程
setExclusiveOwnerThread(null);
setState(arg);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
//加锁
public void lock(){
sync.acquire(1);
}
//释放锁
public void unlock(){
sync.release(0);
}
}
一、Sync内部类(AQS 子类)
Sync 作为锁的核心实现,重写了 AQS 中的 3 个关键方法和 1 个条件变量方法:
1.tryAcquire(int arg) - 独占锁获取
执行逻辑:当 state 为 0(未锁定状态)时,通过 CAS 操作将其置为 1,并记录当前线程为锁持有者
2.tryRelease(int arg) - 独占锁释放
执行逻辑:先清除锁持有者线程信息,再将 state 重置为 0,允许其他线程获取锁
3.isHeldExclusively() - 独占锁状态检查
执行逻辑:通过检查 state 是否为 1 来判断锁是否被占用(注:简化实现,实际还需验证持有者线程)
4.newCondition() - 条件变量创建
功能说明:提供基于锁的等待/通知机制,支持 await() 和 signal() 等操作
二、MLock对外提供的方法
这些方法是锁的使用接口,内部通过调用Sync(AQS)的方法实现:
lock():获取锁(阻塞式)逻辑:如果
tryAcquire成功(获取锁),直接返回;否则,当前线程会被加入 AQS 的等待队列,进入阻塞状态,直到被唤醒并成功获取锁tryLock():尝试获取锁(非阻塞式)
逻辑:仅尝试一次获取锁,成功返回true,失败立即返回false,不会阻塞。unlock():释放锁逻辑:释放锁后,AQS 会唤醒等待队列中的一个线程,让其尝试获取锁。
newCondition():获取条件变量用于线程间的协作(如生产者 - 消费者模型),基于当前锁实现等待 / 通知。
总结
到此这篇关于Java自定义互斥锁实现方法的文章就介绍到这了,更多相关Java自定义互斥锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
