AQS、ReentrantLock详解

AQS、ReentrantLock详解

深入理解Java并发框架AQS系列(一):线程 - 昔久 - 博客园

⛵ReentrantLock

🚍简介

ReentrantLock是一个可重入且独占式的锁,基于AQS实现【AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架从ReentrantLock的实现看AQS的原理及应用 | JavaGuide】,ReentrantLocksynchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

🛥️设计思想

作为一把锁,其最有个重要的功能就是将没有获取到锁的线程进行阻塞,然后等到有锁的时候再将线程继续运行 ,如果需要将线程进行阻塞,我们可以采用wait()sleep()park()循环的方式进行线程阻塞。还需要定义一个锁的状态,表示锁已经被占有,并且设置锁的状态我们需要原子操作。基于以上设计思想我们可以自己实现一个简单的ReentrantLock

  1. 首先我们要
csharp 复制代码
/**
 * 自定义锁
 * @author hanLin.liu
 * @create 2024-11-12 10:19
 */
public class MyReentrantLock {
    /** 锁的占有状态 0-未被占有 大于等于1表示占有 */
    private volatile int status;
    /** 表示持有锁的当前线程 */
    private volatile Thread currentThread;
    /** 用来处理原子操作 */
    private static Unsafe unsafe;
    /**
     * 解锁方法
     */
    public void unlock(){
        unsafe.getAndAddInt(this, getStatusOffset(), -1);
    }
    /**
     * 加锁方法
     */
    public void lock(){
        // 加锁成功跳出循环,加锁失败一直循环阻塞
        while (!setStatus()){}
    }
    /**
     * 进行加锁
     * @return
     */
    private boolean setStatus(){
        // 当锁状态为0,可以加锁
        if (status == 0){
            boolean b = unsafe.compareAndSwapInt(this, getStatusOffset(), 0, 1);
            if(b){
                currentThread = Thread.currentThread();
                return true;
            }
        }else if(currentThread == Thread.currentThread()){
            unsafe.getAndAddInt(this, getStatusOffset(), 1);
            return true;
        }
        return false;
    }
    /**
     * 获取status在对象中的偏移量
     * @return
     */
    private long getStatusOffset(){
        try {
            return unsafe.objectFieldOffset(MyReentrantLock.class.getDeclaredField("status"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            return -1;
        }
    }
    /**
     * 获取 Unsafe 实例
     */
    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

定义完自定义锁后,我们来个方法测试一下

csharp 复制代码
public static void main(String[] args) {
        MyReentrantLock lock = new MyReentrantLock();
        for(int i = 1; i <= 10 ; i++){
            new Thread( () -> {
                System.out.println("【" + Thread.currentThread().getName() + "】:开始抢锁...");
                lock.lock();
                System.out.println("【" + Thread.currentThread().getName() + "】:抢锁成功...");
                System.out.println("【" + Thread.currentThread().getName() + "】:执行临界区代码...");
                System.out.println("【" + Thread.currentThread().getName() + "】:释放锁...");
                lock.unlock();
            }, "线程" + i).start();
        }
    }

执行后输出如下,可以看出我们自定义的这个锁可以实现一个简单的锁功能。

上述代码我们实现了一个简单的自定义lock,但是还存在许多的问题,比如线程大量自旋,线程饥饿等 ,对于这些问题,我们可以使用AQS进行解决,我们现在来看一下ReentrantLock的源码以及实现原理。

✈️AQS

⛰️简介

AQS是一个抽象类,其内部定义了同步队列Condition单向链表等数据结构,对外提供了一系列的方法,用于构建定制化的锁和同步器;然后AQS内部也封装了一系列的原子操作,使用AQS可以非常轻松的实现一个自定义的锁。

🗻 CLH

AQS的设计是借鉴了CLH锁的,CLH是一种自旋锁,可以实现线程之间排队等待锁,避免线程饥饿。我们来看一下代码。

java 复制代码
/**
 * CLH锁
 * @author hanLin.liu
 * @create 2024-11-26 14:01
 */
public class CLHLock {
​
    // 用来表示当前线程节点
    ThreadLocal<Node> cruNode = ThreadLocal.withInitial(Node::new);
    // 用来表示尾指针
    AtomicReference<Node> tail = new AtomicReference<>();
    class Node{
        volatile boolean locked = false;
    }
    CLHLock(){
        tail.set(new Node());
    }
    public void lock(){
        final Node cru = cruNode.get();
        // 表示加锁
        cru.locked = true;
        // 表示得到前驱节点,将尾指针设置成当前节点
        Node pre = tail.getAndSet(cru);
        // 如果前驱节点没有释放锁就一直自旋等待,等到
        while (pre.locked);
    }
    public void unLock(){
        // 相当于将当前线程解锁
        final Node node = cruNode.get();
        node.locked = false;
        // 这里如果不写这一行代码的话,可能会导致死锁
        // 假设现在释放锁之后,在后面的线程还没有抢到锁之前,这个线程又加锁了,对应的节点还是之前那个,这个线程就会一直自旋等待上一个线程释放锁
        cruNode.set(new Node());
    }
}

根据上述代码我们来画一个图

这是每个线程对应Node节点的结构图,可以看出通过ThreadLocal,每个线程相当于都有一个locked字段,来表示后续线程是否要进行自旋。

当第一个线程执行lock()方法时,tail设置指向T1(线程一)对应的Node节点并且返回初始化节点,由于初始化节点的locked为false,不需要自旋,所以T1加锁成功。如下图

当第二个线程T2再执行lock()方法时,tail设置指向T2对应的Node2节点并且返回T1对应的Node1,由于Node1locked为true,所以T2会进行自旋,所以T2阻塞直到T1释放锁。如下图

T1调用unlock()方法释放锁时,Node1locked被设置成falseT2检查到Node1lockedfalse,停止自旋,加锁成功,如下图

通过上述代码和图可以了解到CLH的一个结构以及加锁解锁,自旋的过程,

CLH存在的问题?

  • 由于没有获取到锁的线程都会进行自旋,如果抢锁的线程比较多的情况下,会导致系统大量线程自旋,导致性能下降,这点在AQS中进行了改进,AQS只让队列中第一个等待的节点自旋,对其他线程进行park
  • 基本的 CLH 锁功能单一,不改造不能支持复杂的功能。

🚁队列Node节点

我们先来看一下Node节点的源码

arduino 复制代码
// Node 是 AQS 内部实现的一个核心数据结构
    static final class Node {
        // 表示共享锁的节点
        static final Node SHARED = new Node();
        // 表示独占锁的节点
        static final Node EXCLUSIVE = null;
        
        // 状态:线程状态
        static final int CONDITION = -2; // 线程处于等待条件的状态
        static final int CANCELLED = 1;  // 线程被取消
        static final int SIGNAL = -1;    // 线程被设置为可被唤醒
        static final int RUNNING = 0;    // 线程处于运行状态
​
        volatile int waitStatus;  // 线程的等待状态
        volatile Node prev;       // 前驱节点
        volatile Node next;       // 后继节点
        volatile Thread thread;   // 当前节点所对应的线程
        Node nextWaiter;          // 下一个等待者(仅用于条件变量的队列)
​
        // 构造函数
        Node(Thread thread) {
            this.thread = thread;
        }
​
        // 构造函数,带状态
        Node(Thread thread, int waitStatus) {
            this.thread = thread;
            this.waitStatus = waitStatus;
        }
​
        // 判断节点是否处于共享模式
        final boolean isShared() {
            return nextWaiter == null;
        }
​
        // 设置节点状态
        static Node newNode(Thread thread, int waitStatus) {
            return new Node(thread, waitStatus);
        }
​
        static Node newConditionNode() {
            return new Node(null, CONDITION);
        }
    }

我们主要解释一下作用waitStatus的作用

  • waitStatus

    • CANCELLED (1):表示线程因为超时或者中断而被取消。节点一旦被设置为CANCELLED状态,它将不会被再次使用。
    • SIGNAL (-1):表示节点的后继节点正在(或即将)被阻塞(通过park操作),因此当前节点在释放或取消时需要唤醒它的后继节点。
    • CONDITION (-2):表示节点当前在条件队列中。它将不会用于同步队列,直到被转移到同步队列中(当条件被满足时)。
    • PROPAGATE (-3):共享模式下,头节点可能会被设置为PROPAGATE状态,以确保唤醒后继节点。
    • 0:表示节点在初始状态,新创建的节点默认是这个状态。

根据此源码,我们可以画出一个节点的结构图

通过该Node的数据结构,可以组成一个队列,用于维护阻塞和等待的线程,能按照正确的顺序被唤醒和执行。

🛫Condition单向链表

Condition链表是用来存放调用了await()方法的线程节点,调用signal()方法用来将在Condition链表中的节点加入到同步队列中进行排队抢锁。使用Node节点构成Condition链表时,结构图如下

🚀同步队列

AQS中有个head和tail,用于当线程调用tryAcquire()方法获取尝试获取锁失败时,会调用addWait()方法将其线程创建一个对应的Node节点,并且将该节点加入到队列中。

🌌源码分析

我们这里以ReentrantLock举例来分析AQS的源码,我们查看ReentrantLock源码,我们可以看到有两个构造器,一个默认创建的是非公平锁,一个带参的可以控制创建非公平锁还是公平锁。

我们查看其lock()方法,可以发现其调用的sync.lock()方法,sync.lock()有两种方式,公平锁方式和非公平锁方式,如图

我们以公平锁来进行举例分析。我们点进去后看到acquire(int arg)方法,这是AQS类的方法这里我们解释一下该方法

scss 复制代码
public final void acquire(int arg) {
    // 如果加锁失败&&线程停止=>设置线程状态为中断
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
java 复制代码
abstract static class Sync extends AbstractQueuedSynchronizer {...}
​
// 公平锁的实例
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // 调用公平锁的加锁方法
    final void lock() {
        // 【1】调用AQS的获取锁
        acquire(1);
    }
    // 【3】尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 如果锁状态为未被占有
        if (c == 0) {
             // 如果加锁的这个线程不处于队列的第一个&&设置锁状态成功&&设置持有锁的线程成功=>加锁成功
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                    return true;
            }
        }
        // 如果锁状态已经被占有,判断占有锁的线程是否为当前线程,是当前线程则将锁状态+1表示可重入锁=>加锁成功  
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 否则加锁失败
        return false;
    }
}
​
// AQS类
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {  
    // 锁状态
    private volatile int state;
    // 指示节点正在以独占模式等待的标记
    static final Node EXCLUSIVE = null;
    // 【2】获取锁,入参为1
    public final void acquire(int arg) {
        // 加锁失败&&该节点不是第一个排队的节点&&
        if (!tryAcquire(arg) && 
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    // 当前线程是否处于队列的第一个排队节点中,也就是说需不需要进行排队
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    // 【4】为当前线程和给定模式创建节点并将其排入队列
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 如果尾节点不为null,也就是说该节点不是第一个入到等待队列里的节点
        if (pred != null) {
            node.prev = pred;
            // 原子操作将tail节点设置为当前节点,如果成功则将前置节点的next指向当前节点并返回,
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果尾节点为null,则说明是第一个入等待队列的节点
        enq(node);
        return node;
    }
    // 【5】如果是第一个入队列的节点或者第一次加入队列失败,需进行CAS自旋加入等待队列中
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    // 【6】这个方法就是判断是否为第一个排队的节点,如果是第一个排队的,则自旋,不是则进行park,在这里进行park后
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}
相关推荐
广东王多鱼1 小时前
一个人 + Claude = 全栈开发团队:从零构建 AI 自动化开发系统的技术实现
后端·vibecoding
Rust研习社1 小时前
Rust Clippy 实用指南:写出更优雅、安全的 Rust 代码
后端·rust·编程语言
小撒的私房菜1 小时前
Agent = Model + Harness:这个公式,让我重新理解了 AI 工程
人工智能·后端
掘金者阿豪1 小时前
Go 语言操作金仓数据库(下篇):SQL 执行、类型映射与超时控制
后端
IVEN_1 小时前
全栈开发必看:从内存变量到关系型数据库的完整旅程
后端
MacroZheng1 小时前
横空出世!IDEA最强MyBatis插件来了,功能很全!
java·后端·mybatis
codebetter1 小时前
X86 Windows Docker Desktop 运行 arm64 容器
后端
掘金者阿豪1 小时前
Go 语言操作金仓数据库(上篇):环境搭建与连接管理
后端
何陋轩1 小时前
Spring AI Function Calling:让AI调用你的Java方法
人工智能·后端·ai编程