【JUC】AQS 队列同步器

一、概述

AbstractQueuedSynchronizer(AQS)队列同步器,是用来构建其它同步组件的基础框架,同步组件也是构建各种Lock工具(例如:ReentrantLock)的必备组件

例如:下面的自定义锁MyLock的伪代码

public class MyLock implements Lock {

    // 自定义的同步组件
    private static class MySync extends AbstractQueuedSynchronizer {

        // 根据需要重写AbstractQueuedSynchronizer模板类中的方法
        @Override
        protected boolean tryAcquire(int arg) {
           ...
        }

        @Override
        protected boolean tryRelease(int arg) {
            ...
        }

        @Override
        protected boolean isHeldExclusively() {
            ...
        }
        ...
        ...
        ...
    }

    // 同步组件
    private final MySync sync = new MySync();
	
	// 实现Lock接口的基本方法
    @Override
    public void lock() {
        // acquire()会调用自定义同步组件重写的tryAcquire()方法
        sync.acquire(1); 
    }
	...
	...
	...
}

自定义锁时,基本就是按照上面的框架来

自定义Lock类的逻辑就像是Controller-Service框架,Lock是Controller层负责根据请求判断调用什么方法,自定义的同步组件 MySync就像是Service层,实现具体的业务逻辑

AbstractQueuedSynchronizer类一个模板类,采用模板方法设计模式

AbstractQueuedSynchronizer类将大量与锁相关的通用的代码提炼出来,留下不通用的部分提供接口供子类去实现,子类通过重写部分方法实现不同功能的同步器,从而实现不同的锁

二、AQS主要成员

2.1 AQS主要成员属性

AQS内部成员中主要维护了一个表示锁状态的变量state和一个以双向链表实现的同步队列,该链表具有尾指针

private volatile int state;

// 同步队列头结点
private transient volatile Node head;

// 同步队列尾节点
private transient volatile Node tail;

2.2 内部类Node

同步队列中的元素,将thread作为Node中属性封装成Node对象
同步队列的控制就是通过操纵队列中的Node结点来控制

static final class Node {
        
        static final Node SHARED = new Node();
        
        static final Node EXCLUSIVE = null;

        
        static final int CANCELLED =  1; // 线程被取消,放弃竞争锁
        
        static final int SIGNAL    = -1; // 线程释放资源后需唤醒后序结点
        
        static final int CONDITION = -2; // 线程等待condition唤醒
        
        static final int PROPAGATE = -3; // 和共享锁有关
 		
 		// 当前结点中线程处于什么状态
 		// 1,0,-1,-2,-3五个值。分别对应上面的值,0表示初始状态
        volatile int waitStatus;
        
        volatile Node prev;
        
        volatile Node next;
		
		// 当前node绑定的thread
        volatile Thread thread; 
		
		// 等待条件的下一个结点
        Node nextWaiter;
        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        
        // 获取前驱结点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
		...
		...
		...
    }

三、AQS主要方法

AQS内部有两种锁的框架,一种是独占锁一种是共享锁
独占锁:同一时刻只能有至多一个线程持有锁
共享锁:同一时刻可以有多个线程持有锁

下文以独占锁为例说明主要的成员方法

3.1 获取独占锁

  • acquire(int arg)
    获取锁的方法,自定义锁上锁时会调用MyLock.lock(),MyLock.lock()内部会调用acquire()
// 
public final void acquire(int arg) {
	// tryAcquire(arg)是自定义的MySync需要重写的方法,里面实现获取同步状态(锁)的逻辑,方法返回false表示获取锁失败
    if (!tryAcquire(arg) &&
    	// 获取锁失败则会在addWaiter()方法中将线程封装成Node(内部类)对象放入同步队列中
    	// acquireQueued()会让此次放入同步队列的结点不停自旋尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • addWaiter(Node mode)
    将当前线程封装成node对象,放入到同步队列尾部
private Node addWaiter(Node mode) {
		// 将当前线程封装成node对象
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // tail不为空说明同步队列中有还在等待的线程
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { // CAS修改tail指针指向node
                pred.next = node;
                return node;
            }
        }
        // tail为空说明同步队列为空,enq()初始化队列
        enq(node);
        return node;
    }
  • enq(final Node node)
    初始化同步队列
private Node enq(final Node node) {
 	for (;;) { // 不停自旋直到初始化成功
        Node t = tail;
        // 这里还要再判断一次tail是否为空
        // 因为其它线程可能在当前线程进入enq后,也进入enq并且率先完成初始化
        if (t == null) { 
            if (compareAndSetHead(new Node())) // CAS设置防止其它线程抢先初始化同步队列
                tail = head; // 初始化成功则将头尾指针指向nodeshu
        } else { // 进入此分支说明其它线程率先完成初始化
        	// 下面则将当前线程生成的node放到队列尾部而不是初始化队列
            node.prev = t; 
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

看到这里不禁感叹,源码这里写得确实漂亮,考虑的非常细致滴水不漏

  • acquireQueued(final Node node, int arg)
    获取资源失败后使线程不断自旋
    在每次自旋失败后判断线程是否需要被挂起,为什么会出现需要被挂起的情况会在下面的shouldParkAfterFailedAcquire()中说明
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // 获取node的前驱结点
                // p == head表示前驱结点是头结点,也就是当前获取锁的结点
                if (p == head && tryAcquire(arg)) { // 前驱节点是head就尝试才会自旋获取锁
                    setHead(node); 
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire(p, node):判断在本次自旋失败后是否需要park()进入WAITING状态
                // parkAndCheckInterrupt():让线程WAITING
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
 }

从源码可以很清楚的看到只有前驱节点为head时,才有机会去获取锁,因此如果自定义的组件不重写逻辑的话,直接使用acquire()这个方法,最终会调用acquireQueued()方法,这样一种机制其实会导致所有线程获取锁的状态时"公平的",先加入队列的线程优先获取锁,锁获取是有顺序的,这就是公平锁

  • shouldParkAfterFailedAcquire(p, node)

判断当前线程是否需要被挂起,分以下几种情况:

  1. 前驱节状态为SIGNAL时:前驱节点释放资源后会唤醒后续节点,也就是本线程,所以本线程可以放心的挂起,等待被唤醒
  2. 前驱节点状态 > 0时(CANCELLED): 一直往前找,找到第一个状态<0的(有效的线程)
  3. 除上述两种情况:则CAS将前驱节点状态设置为SIGNAL
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 获取前驱节点状态
        if (ws == Node.SIGNAL) // 前驱节点为SIGNAL状态,则其释放资源后
            return true;
        if (ws > 0) {           
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
 }

可以看到,一般情况下一个线程最多自旋两次,第一次将前驱节点设置为SIGNAL状态,第二次自旋会使自身沉睡

  • parkAndCheckInterrupt
    线程挂起
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

LockSupport定义了一组静态方法,提供最基本的线程阻塞和唤醒的功能
LockSupport.park()阻塞当前线程

3.2 释放独占锁

锁调用unlock()方法,内部调用同步组件release()方法

  • release(int arg)
    由于独占锁只有head节点的线程占有锁,所以只需释放head节点
public final boolean release(int arg) {
        if (tryRelease(arg)) { // tryRelease(arg)尝试释放锁
            Node h = head; 
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒后继结点
            return true;
        }
        return false;
}
  • unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next; 
        // 找到head后第一个有效的后继结点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); // 唤醒后继节点
}

源码分析可以看到,单个的自定义同步组件MySync无法同时支持公平与非公平机制,因此如果自定义的锁要支持两种机制,需要在MyLock内定义两个同步器,分别实现公平锁和非公平锁的逻辑

四、自定义简易独占锁示例

public class MyLock implements Lock {

    final class MySync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            if(arg == 1) {
                if(compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread()); // 设置当前拥有锁的线程(用以支持重入锁)
                    return true;
                }
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
           if(getState() == 0) {
               throw new IllegalMonitorStateException();
           }
           setExclusiveOwnerThread(null);
           setState(arg);  // 设置为无锁状态setState(0);
           return true;
        }
    }

    AbstractQueuedSynchronizer mySync = new MySync();


    @Override
    public void lock() {
        mySync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        mySync.release(0);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

可以看到有了AQS自定义锁及其简单,而锁的种类按照不同特性可以分为:

  • 可重入/不可重入锁:构建可重入锁时,对状态变量 state执行+1的动作,获取锁成功;非获取锁的线程想要获取时加入阻塞队列尾部
  • 公平/非公平锁
  • 独占/共享锁:通过state的数值来区分,独占锁在state > 0时就不允许再获取锁,共享锁可以在state < n时获取锁,n为允许共享的线程数

上述锁可以通过改写AQS逻辑,或者通过几个自定义的同步组件组合形成新的锁

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值