JAVA并发之Lock(上——AQS)

本文详细介绍了JAVA并发库中的AQS(AbstractQueuedSynchronizer),它作为锁和其他同步组件的基础框架,如ReentrantLock、ReentrantReadWriteLock等。AQS利用变种CLH队列维护线程竞争,并通过state变量、LockSupport类实现锁的获取与释放。文章涵盖了AQS的前置知识、核心机制、获取与释放锁的原理,以及等待队列condition的使用。
摘要由CSDN通过智能技术生成

一、概述

上文详述了synsynchronized的原理及实现【JAVA并发之Synchronized】,虽然在JDK1.6对其进行了大量优化,但是还是存在缺陷:缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。而java.util.concurrent(简称JUC)下的Lock类解决了这些问题,Lock类的基础之一为AQS,此文将介绍AQS的原理及底层实现。
AQS:全称AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),是JUC并发包中的核心基础组件。

二、AQS前置知识

本部分主要介绍要了解AQS所需要知道的一些前置知识,如CLH同步锁,LockSupport类

CLH同步锁


CLH同步锁:是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

public class CLHLock {
   
    public static class CLHNode {
   
        private boolean isLocked = true; // 默认是在等待锁
    }
    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThreadCLHNode) {
        // 把this里的"tail" 值设置成currentThreadCLHNode
        CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode); 
        if(preNode != null) {
  //已有线程占用了锁,进入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThreadCLHNode) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
            // 还有后续线程
            currentThreadCLHNode. isLocked = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

LockSupport类


AQS并采用该模式LockSupport.park() 和 LockSupport.unpark() 的本地方法来实现线程的阻塞和唤醒。其特点详见使用LockSupport唤醒指定线程

三、AQS核心:变种CLH队列

AQS为了维护线程直接对锁的竞争关系,使用了如下队列,图是在源码注释上稍加改动(表述next节点)。

     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      |      | ----> |     | ----> |     |
     *      +------+  next +-----+       +-----+

其代码如下:

    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;
        static final int PROPAGATE = -3;

        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        ……
    }

此双向链表,head与tail为两个头结点,为便于称呼,本文将head节点称为头结点,tail节点称为尾节点。
head节点是一个空节点,代表当前持有锁的线程,每当有线程获取锁失败,则将其加入到队尾,变为tail节点。

每个节点中的waitStatus变量,用于描述节点当前的状态,一共有5中状态:

CANCELLED 取消状态
SIGNAL 等待触发状态
CONDITION 等待条件状态,用于等待队列
PROPAGATE 用于shared锁队列
0:以上都不是

CANCELLED 是取消状态,当线程出现异常或自己中断时处于此状态。
SIGNAL 是等待触发状态,等待获取锁。
CONDITION 是条件队列中的线程状态
PROPAGATE 状态是支持其他扩展操作
等待队列是FIFO先进先出,只有前一个节点的状态为SIGNAL时,当前节点的线程才能被挂起。

SHARED 与EXCLUSIVE 代表锁的模式,分布代表共享锁与独占锁。

nextWaiter的值有三种情况:SHARED,null,其他
SHARED:表示当前节点是共享模式
null:当前节点是独占模式
其他:当前节点是独占模式,不过这个值也是Condition队列的下一个节点

四、AQS类的属性

AQS类的核心属性如下:

    private transient volatile Node head; //头结点
    private transient volatile Node tail;//尾节点
    private volatile int state;//状态

    protected final int getState() {
        return state;
    }
    protected final void setState(int newState) {
        state = newState;
    }
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

AQS使用一个int成员变量state表示同步状态,通过变种CLH队列(FIFO,双向链表)来完成排队工作。state,head,tail属性都使用volatile修饰,使得多线程之间可见。提供getState,setState,compareAndSetState方法来对state进行操作。

五、AQS获取与释放锁的实现原理

AQS中定义了如下方法供子类实现:

    protected boolean isHeldExclusively() {
  //该线程是否正在独占资源。只有用到condition才需要去实现它。
        throw new UnsupportedOperationException();
    }
    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();
    }

用于支持独占锁与共享锁的操作(需要子类根据自身需求实现),将在下一篇博文中具体讲述实现的实例。

五、AQS独占锁的获取与释放锁

一、获取锁(上锁)过程


跟踪源码需要一个入口,这里通过从最上层作为入口(以ReentrantLock的公平锁为例(只作为切入,具体见下一篇博文)

//ReentrantLock的lock方法
 public void lock() {
        sync.lock();
}
//ReentrantLock的内部类sync的lock方法,实现AQS类
final void lock() {
        acquire(1);
}
//AQS提供的模板方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

通过上述源码,有四个关键函数tryAcquire、addWaiter、acquireQueued、selfInterrupt,下面一一分析。
另,acquire的参数arg,是重入的次数,当锁持有者需要获取锁的时候,则要将锁的state加上arg的值。上述实现传入参数为1。
1.tryAcquire
tryAcquire(arg)方法上文已提及,是AQS的对外提供的方法,需要子类实现(上文已给出源码,只抛出异常),此处还是以ReentrantLock的公平锁FairSync为例static final class FairSync extends Sync

 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;
        }
    }else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
   }
   return false;
}

getState()是获取锁的状态,表示锁当前是否被持有,为0时表示没有线程持有锁,此时当前线程会去争取锁的持有权,大于0表示该锁被持有次数。
若c=0,则通过hasQueuedPredecessors方法(AQS类)判断队列中是否有排在当前线程之前的线程。

    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());
    }

若有,放弃争抢锁(返回false);若无,将state更改为acquires,并调用setExclusiveOwnerThread()方法将当前线程设为锁的拥有者(同包下的AbstractOwnableSynchronizer类中),防止volatile字段被其他线程修改。

若c!=0,则判断当前线程是否是锁的拥有者。若是,则是重入,state+重入次数acquires;若不是,则获取锁失败,返回false。另,当state+acquires之后小于0,则代表数据溢出(此处初看看似没有必要【锁的持有次数不会溢出int类型】,仔细琢磨却充分反应了程序设计的严谨性)。
2.addWaiter
addWaiter(Node.EXCLUSIVE)方法AQS中实现的方法,作用是快速添加一个节点,代码如下:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
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;
            }
        }
    }
}

快速插入队列:判断队列是否为空,若非空,直接将新节点插入队尾成为tail节点;否则,通过enq(node)方法(AQS实现)将当前节点添加至队列中。
enq():为空则初始化队列,然后再将该节点node设置成队尾节点tail。考虑到多线程同时入队,初始化及设置操作均使用CAS,使用无限循环保证最终成功(每次循环前都需要判断是否为空),因此效率较慢。
3.acquireQueued
acquireQueued(node, arg)方法位于AQS中,同步队列中的节点以自旋方式获取锁。如果获取不到则阻塞节点中的线程,而被阻塞的线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。返回值表示在线程等待过程中,是否有另一个线程调用该线程的interrupt方法,发起中断。

    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);
        }
    }

节点入队后,不是立马挂起而是判断是否能获取锁,因为可能在这过程中锁释放了。
若该节点的上一个节点是head,则可以尝试获取锁(上述tryAcquire方法)。若获取成功,则将该节点设置为头结点,并将其next引用设为null(方便GC),将最终失败标志设为false。
若在获取锁的过程中发生异常(在tryAcquire中抛出,用于中断线程),则调用cancelAcquire方法,在5详述。
若该节点的上一个节点不为head或获取锁失败,则进行是否挂起的判断(shouldParkAfterFailedAcquire,AQS实现)。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.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(-1),则返回true,将其挂起。
若该节点的上一个节点状态大于0(只有CANCELLED),则证明上一个节点放弃锁的竞争,继续向前追溯其他节点,直至找到一个状态不大于0的节点,将其放在该节点后,并返回false,不挂起继续尝试一次获取锁。
否则,将该节点的状态使用CAS置为SIGNAL,并返回false,不挂起继续尝试一次获取锁。
将线程挂起的操作如下:

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

使用LockSupport实现阻塞,它会设置一个AQS的blocker,让队列里的线程阻塞在一个地方,然后返回线程是否中断的判断状态。
4.selfInterrupt()
selfInterrupt()是AQS类中的一个方法,用于当前线程休眠

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

acquireQueued会返回boolean值表示在线程等待过程中,是否有另一个线程调用该线程的interrupt方法,发起中断。如果有,则发起中断。
5.cancelAcquire()
cancelAcquire()方法在AQS中,此方法用于将某节点的状态设置为CANCELLED,表示已经从队列中删除,不再唤醒。

  private void cancelAcquire(Node node) {
        if (node == null)
            return;
        node.thread = null;//将node的线程属性置空
        // 跳过那些已取消的节点,在队列中找到在node节点前面的第一次状态不是已取消的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;// 记录pred原来的下一个节点,用于CAS函数更新时使用

        node.waitStatus = Node.CANCELLED;//将node的状态置为CANCELLED
        if (node == tail && compareAndSetTail(node, pred)) {
         // 如果node节点是队列尾节点,那么就将pred节点设置为新的队列尾节点,并且设置pred节点的下一个节点next为null
            compareAndSetNext(pred, predNext, null);
        } else {
            /**
             * 对node的前一个节点pred进行判断
             * 若pred非空,不是头结点,状态为SIGNAL或能设置为SIGNAL,则将node的下一个结点设为pred的下一个结点
             * 否则,唤醒node的下一个非取消状态的结点
             */
            int ws;
            if (pred != head &&
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                            (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }
            //将node的下一个结点置空,辅助GC
            node.next = node; // help GC
        }
    }

对node自身的操作:将node的thread属性置空,waitStatus属性置为CANCELLED,next结点指向自身(方便GC)
对影响的其他节点操作:

  1. 找到前一个非取消节点(pred)
  2. 若node为队尾节点,则将pred节点设置为新的队列尾节点(pred的next为null);否则进入3
  3. 若pred非空,不是头结点,状态为SIGNAL或能设置为SIGNAL,则将node的next设置为pred的next;否则,唤醒node的下一个非CANCELLED状态节点(unparkSuccessor方法,在取消锁的部分讲述)。

过程总结:
此处再贴一次总代码,方便总结顺序:

final void lock() {
        acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 线程调用lock方法获取锁,进入acquire方法
  2. 尝试获取锁,若成功,则方法结束;若失败(!tryAcquire(arg)的结果为true),进入步骤3
  3. 调用addWaiter()将该线程加入获取锁的队列中,具体过程见addWaiter()方法详解
  4. 调用acquireQueued()尝试获取锁(自旋获取,中途可能被挂起,需要调用unpark唤醒继续尝试),返回线程是否在等待过程中被其他线程发起中断,若是,则进行中断线程;否则结束

二、释放锁过程


当获取锁成功,任务执行结束,就需要释放锁,此处同上以ReentrantLock的公平锁为例

//ReentrantLock的unlock方法
public void unlock() {
    sync.release(1);
}
//ReentrantLock的内部类sync的unlock方法,实现AQS类
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

通过上述源码,有四个关键函数tryRelease、unparkSuccessor下面一一分析。
另,release的参数arg,是重入的次数,当锁持有者需要释放锁的时候,则要将锁的state减去arg的值。上述实现传入参数为1。

1.tryRelease()
tryRelease(arg)方法上文已提及,是AQS的对外提供的方法,需要子类实现(上文已给出源码,只抛出异常),此处还是以ReentrantLock的锁为例static final class Sync extends AbstractQueuedSynchronizer

  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;
 }

以上源码将state的值减少释放次数,如果减少次数到0,则将锁的拥有者置空,返回成功与否。
2.unparkSuccessor()
unparkSuccessor()实现在AQS中,用于唤醒node的下一个非CANCELLED状态节点。代码如下:

 private void unparkSuccessor(Node node) {
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值