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);
        }
    }
}
相关推荐
红尘散仙3 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记4 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆4 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪5 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6165 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364575 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao6 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒7 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰8 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox8 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全