• 深入理解Java AQS:从原理到源码分析


    由于AQS(AbstractQueuedSynchronizer)是Java并发包中的一个关键组件,它提供了一种实现同步器(如锁和其他同步工具)的框架,用于实现各种同步器(如ReentrantLock、Semaphore等)。AQS的核心思想是基于FIFO队列实现阻塞和唤醒线程,以及维护同步状态。

    AQS的设计原理

    AQS 使用一个整数(state)表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它的主要使用方式是继承,子类通过继承AQS并实现它的几个protected方法来管理其状态(acquire 和 release)并控制线程的排队和阻塞。

    1、队列节点 Node 和 FIFO队列结构

    AQS 内部通过一个叫做Node的静态内部类来表示队列中的每一个等待线程。而这些节点组成了一个双向链表,这就是AQS的等待队列。每一个节点都包含了线程引用、状态和前驱及后继节点的连接。AQS的队列被设计成FIFO,确保了首个节点通常是等待时间最长的节点。

    2、state 的作用

    state是AQS中的核心变量,它用来表示同步器的状态。不同的同步器可以使用state来表示不同的意义。例如,ReentrantLock用它来表示锁的持有次数;Semaphore用它来表示当前可用的许可证数量。

    3、公平锁与非公平锁

    AQS支持两种锁模式:公平锁和非公平锁。公平锁意味着当锁可用时,AQS会按照等待时间最长的线程来分配(即FIFO),而非公平锁则允许新线程插队,可能会忽略排队时间较长的线程。非公平锁的吞吐量一般比公平锁要高。

    AQS 源码解析

    为了深入理解AQS的工作机制,我们将分析几个关键的方法。

    1、Node节点

    Node是AQS内部定义的一个帮助类,表示等待队列中的一个节点,它的定义如下:

    static final class Node {
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    waitStatus 表示节点的状态。
    prev 指向前一个节点。
    next 指向下一个节点。
    thread 是执行线程的引用。
    nextWaiter 用于构建条件队列。

    2、acquire(int)

    获取资源的过程,它的基本逻辑是如果当前状态允许,则尝试获取资源。如果失败,则构建节点并进入等待队列,可能会循环直到成功获取。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    tryAcquire 是由子类实现的方法,在锁实现中,它尝试直接获取资源。
    addWaiter 方法将当前线程封装成节点并加入等待队列。
    acquireQueued 方法是使线程在队列中等待,必要时阻塞,并尝试获取资源。

    3、release(int)

    当一个线程完成了对资源的使用后,它会调用release方法来释放资源。这个方法会尝试设置同步状态并唤醒后续节点。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    tryRelease 是由子类实现的方法,用于释放资源。
    unparkSuccessor 用于唤醒等待队列中的后续节点。

    4、自旋(Spin)

    在阻塞队列中,当一个线程尝试获取但是未能成功时,AQS会把这个线程放入队列。在队列中的线程会不断地检查是否能够成为头结点并获取资源,这个过程叫做自旋。

    5、公平性与 FIFO

    公平锁的实现考虑到了队列的FIFO顺序,在公平锁中,如果队列中有比当前线程更早的线程在等待,则当前的线程将加入队列等待。非公平锁允许插队,在性能上可能有一定优势,但在高并发下可能会导致某些线程饿死。

    protected final boolean isHeldExclusively() {
        // 该方法在子类中实现,用来查询当前线程是否独占该锁
    }
    
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    以上是用于扩展的几个方法。对于独占模式,需要实现tryAcquire和tryRelease方法;对于共享模式,则需要实现tryAcquireShared和tryReleaseShared。

    基于AQS实现的几种同步器

    1、ReentrantLock:可重入独占锁

    ReentrantLock 是一种可重入的互斥(独占)锁,它允许已经获取到锁的线程再次获取锁而不会被阻塞。这种锁的实现基于AQS。

    这里是一个精简版本的ReentrantLock获取锁(lock)和释放锁(unlock)的基本原理:

    public class ReentrantLock {
        // 内部持有AQS的一个子类
        private final Sync sync;
        
        // Sync是AQS的一个子类
        abstract static class Sync extends AbstractQueuedSynchronizer {
            // 获取锁 
            abstract void lock();
            
            // 尝试释放锁
            protected final boolean tryRelease(int releases) {
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread()) {
                    throw new IllegalMonitorStateException();
                }
                boolean free = false;
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
        }
        
        // ... 其他方法省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2、ReentrantReadWriteLock:可重入读写锁

    ReentrantReadWriteLock允许多个线程同时读取资源,但只允许一个线程写入资源。适用于多读少写场景。它也是基于AQS实现的。

    其内部主要有两个类:ReadLock和WriteLock,这两个类都是通过控制AQS中的状态来实现锁的功能:

    public class ReentrantReadWriteLock {
        private final ReentrantReadWriteLock.ReadLock readerLock;
        private final ReentrantReadWriteLock.WriteLock writerLock;
        
        // AQS的子类实现
        private final Sync sync;
        
        static final class Sync extends AbstractQueuedSynchronizer {
            // 写锁的获取和释放
            // ...
            
            // 读锁的获取和释放
            // ...
        }
        
        public class ReadLock {
            // 获取读锁
            public void lock() {
                sync.acquireShared(1);
            }
            // 释放读锁
            public void unlock() {
                sync.releaseShared(1);
            }
        }
    
        public class WriteLock {
            // 获取写锁
            public void lock() {
                sync.acquire(1);
            }
            // 释放写锁
            public void unlock() {
                sync.release(1);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    3、Semaphore:信号量

    Semaphore 信号量用于同时多个线程访问场景,可以初始化时设置同时访问线程数。其核心方法是acquire()和release(),它们分别用于获取和释放许可。

    public class Semaphore {
        private final Sync sync;
        
        // AQS的子类实现
        static abstract class Sync extends AbstractQueuedSynchronizer {
            // 获取许可
            abstract void acquireShared(int permits);
            // 释放许可
            final boolean releaseShared(int permits) {
                // ...
            }
        }
        
        public void acquire() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
    
        public void release() {
            sync.releaseShared(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4、CountDownLatch:倒计时门闩

    CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待

    public class CountDownLatch {
        private final Sync sync;
        
        // AQS的子类实现
        private static final class Sync extends AbstractQueuedSynchronizer {
            Sync(int count) {
                setState(count);
            }
            
            // 减少锁存器计数
            public void countDown() {
                releaseShared(1);
            }
            
            // 等待直到锁存器计数为零
            public void await() throws InterruptedException {
                acquireSharedInterruptibly(1);
            }
        }
        
        public CountDownLatch(int count) {
            this.sync = new Sync(count);
        }
    
        public void countDown() {
            sync.countDown();
        }
        
        public void await() throws InterruptedException {
            sync.await();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    以上代码只是为了解释概念,并未展示真正的实现细节。在实际的JDK实现中,这些类的内部结构更为复杂,包括了性能优化、状态管理、线程调度和锁升级等机制。您需要直接查看JDK源码以获取完整和准确的实现。

    结语

    AQS是Java并发编程中的重石,它通过内置的FIFO队列以及对共享资源状态的管理极大地简化了同步器的实现。通过继承AQS并实现其方法,我们可以创造出性能卓越和功能强大的同步器。当理解了其背后的原理和源码后,我们就能更好地利用这一强大工具来构建可靠的并发应用。

  • 相关阅读:
    【ajax核心01】ajax底层原理
    Javaweb回炉简要学习笔记
    Sangria:类似Nova folding scheme的relaxed PLONK for PLONK
    spark—算子详解
    关于MySQL安装时一直卡在starting sever......手把手教你搞定
    【3D Modeling Kit】建模服务,是否可以支持自定义图片上传?
    C# yolov8 OpenVINO 同步、异步接口视频推理
    编译Redis时报错: jemalloc/jemalloc.h: No such file or directory
    JUC编程
    Linux——gdb调试时多进程切换方法(attach/follow-fork-mode)
  • 原文地址:https://blog.csdn.net/weixin_44183847/article/details/134543630