AQS源码分析之独占锁

一.简介

AQS即AbstractQueuedSynchronizer,队列同步器,很多并发工具都使用它作为基础框架,像

锁(ReenTrantLock、ReentTrantReadWriteLock),工具类(CountDownLatch、CyclicBarrier、

Semaphore)都是依赖它来完成。

二.CLH队列同步器:

1.AQS内部维护一个FIFO的队列,这个队列就是CLH,通过CLH来完成同步状态的管理。这个

CLH是一个双向队列,是通过双向链表实现,也是一个变种的同步队列,原生的是让线程自旋,

而CLH是让线程睡眠(通过调用底层的park()方法来让线程睡眠并且释放cpu资源)。

2.CLH其实是通过一个双向的Node来实现的,通过对Node类型的区别,来达到共享锁和独占锁

的实现。

通过Node我们可以看到里面维护了两种Node方式,分别是独占和共享,所以CLH队列是可以为

独显锁或者共享锁的线程创建node对象

//共享节点
static final Node SHARED = new Node();
//独占节点
static final Node EXCLUSIVE = null;
//waitStatus的四种状态,其实还有一种,就是0,表示初始化状态
//因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态
static final int CANCELLED =  1;
//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
static final int SIGNAL    = -1;
//节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
static final int CONDITION = -2;
//表示下一次共享式同步状态获取,将会无条件地传播下去
static final int PROPAGATE = -3;
//表示当前NOde的状态
volatile int waitStatus;
//当前node的上一个node
volatile Node prev;
//当前node的下一个node
volatile Node next;
//当前线程
volatile Thread thread;
//下一个node
Node nextWaiter;

这个方法是获取node的上一个节点

//获取当前node的上一个node节点
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

往队列中添加Node,如果当前队列为空,则通过调用enq()创建空Node作为head,此Node的pre为空,thread也为空,next指向添加进来的Node,新Node的pre指向这个空Node,且tail指向这个新添加进来的Node

/**
 * 往队列中添加新节点
 * @param mode
 * @return
 */
private Node addWaiter(Node mode) {
    //创建节点,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;
}
/**
 * 当队列为空时,调用此方法
 * @param node
 * @return
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //创建初始的空节点
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                //tail和head分别指向空节点
                tail = head;
        } else {
            //空节点的next指向新增节点,新增节点的pre指向
            node.prev = t;
            //设置tail以及空节点的next指向新增节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
//判断下一个节点是独占节点还是共享节点
final boolean isShared() {
    return nextWaiter == SHARED;
}

三.AQS的独占锁

AQS的核心就是CLH同步队列器,通过刚才对CLH源码的分析可以看出AQS是支持独占锁以及共

享锁,对于独占锁来说,以ReentrantLock为例。

使用lock.lock()方法进行加锁时,会调用以下方法,在非公平锁中会通过compareAndSetState(0,

1)方法进行尝试加锁,如果加锁成功,则直接设置状态线程(ExclusiveOwnerThread)为当前线

程;否则加锁成功则直接执行acquire(1)方法,而公平锁是直接执行

//非公平锁调用
final void lock() {
    //加锁设置
    if (compareAndSetState(0, 1))
        //加锁成功设置占有线程为自己
        setExclusiveOwnerThread(Thread.currentThread());
    else
         //加锁失败
        acquire(1);
}
//公平锁调用
final void lock() {
    acquire(1);
}

在acquire(1)方法中,会进行加锁或者进入CLH操作

/**
 * 获取同步锁
 * @param arg
 */
public final void acquire(int arg) {
    //第一个方法是判断能否获取同步成功,如果成功则返回true
    //则后面的就不走了。如果返回false,则走第二方法讲线程添加进CLH中
    //第三个方法是线程中断
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在上面的调用中,最大会调用三个方法,我把它称为加锁三部曲。首先一部曲是调用

tryAcquire(arg)方法,这个方法在公平锁和非公平也是不同的操作,在公平锁中是判断CLH队列

是否还有节点,如果没有则进行加锁操作,如果加锁成功则返回true,否则则返回失败,同时这

里也是支持可重入锁操作。而且非公平锁中则是调用nonfairTryAcquire(int acquires)方法,该方

法跟公平锁走的逻辑一样,只是少了一步判断CLH的操作,这个也是公平锁跟非公平锁的区别,

公平锁是如果队列中还有节点,就不去抢占锁而去进入队列中排队,而非公平锁则事不管队列中

有没有,就直接去抢占锁,抢占不到则去队列中排队。

 /**
     * 公平锁- 尝试去加锁
     * @param acquires
     * @return
     */
    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;
    }
}
//为空则返回false
public final boolean hasQueuedPredecessors() {
    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());
}

/**
 * 非公平锁tryAcquires调用
 * @param acquires
 * @return
 */
final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程以及state
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

当一部曲tryAcquire(arg)返回true时,则表明加锁成功,因为前面有个!,则表明结果是false,后面的二三部曲讲不会执行,当加锁失败返回false是,则结果是true,则会执行二部曲acquireQueued(addWaiter(Node.EXCLUSIVE), arg),而addWaiter()方法刚才讲过,是一个往CLH队列中添加节点的操作,后面Node.EXCLUSIVE表明我们添加的是独占锁的节点。当我们添加节点成功之后,会返回该节点对象作为acquireQueued()方法的参数。而在acquireQueued()方法中,我们会有一个自旋的操作。我们会判断当前节点的pre节点是不是头节点,以及尝试去加锁,如果都成功,则讲该节点设置为head节点并且清空节点的线程信息以及pre指引。并且将原head节点指向null方便被回收。如果操作成功则放回当前线程中断状态-false。如果失败则进行下一步的睡眠操作。

/**
 * 将添加进队列的线程,通过调用底层的park方法进行睡眠
 * @param node
 * @param arg
 * @return
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //节点自旋的去尝试获取锁
        for (;;) {
            final Node p = node.predecessor();
            //如果该节点的pre是head,将其pre节点的next指向null,方便被GC回收
            //另外将自己节点的pre和thread置为空,作为一个空节点,指向head
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //加锁失败,说明pre不是head,将线程进行中断睡眠
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

/**
 * 设置当前节点为头节点并且清空相关信息
 * @param node
 */
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

睡眠操作中,会首先通过shouldParkAfterFailedAcquire(Node pred, Node node)判断该节点的上一个的waitStatus值。addWaiter()构造的新节点,waitStatus的默认值是0。此时,进入最后一个if判断,CAS设置pred.waitStatus为SIGNAL==-1。最后返回false。回到第五步acquireQueued()中后,由于shouldParkAfterFailedAcquire()返回false,会继续进行循环。假设node的前继节点pred仍然不是头结点或锁获取失败,则会再次进入shouldParkAfterFailedAcquire()。上一轮循环中,已经将pred.waitStatus设置为SIGNAL==-1,则这次会进入第一个判断条件,直接返回true,表示应该阻塞。因为创建的节点的waitstatus都是0,所以都会通过设置来讲waitstatus改变成-1.

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //prev节点通知下一个节点阻塞等待,表示当前node节点可以安全的被park
        return true;
    if (ws > 0) {
        //删除prev节点,不停的调用上一个节点给prev赋值,直到prev的状态大于-
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 0 或者 Node.PROPAGATE
        //通过cas设置,讲状态改成Node.SIGNAL 状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false,则接着循环执行上一层的for循环
    return false;
}

当返回true表示应该阻塞时,则会调用parkAndCheckInterrupt()方法将线程进行睡眠。Thread.interrupted();操作是避免当前线程被其他线程中断了,所以通过这个来唤醒。但是。在这里,当对线程park()之后,线程就不会在执行了,因为线程被睡眠了。所以后面的selfInterrupt()方法,以及返回,还有自旋都不会在执行了,直到当前线程被唤醒。

/**
 * 线程睡眠已经线程中断
 * @return
 */
private final boolean parkAndCheckInterrupt() {
    //调用os底层方法对线程进行睡眠
    LockSupport.park(this);
    //判断当前线程有没有被其他线程中断操作过,如果有就返回标志并且清除。
    return Thread.interrupted();
}

第三部曲就是设置当前线程中断。因为线程被睡眠了。返回了true,所以进行线程中断操作。

static void selfInterrupt() {
    //如果当前线程被其他线程中断过,因为park后请出了中断,所以要再次中断。
    Thread.currentThread().interrupt();
}

加锁成功后,后执行业务逻辑操作,当释放锁时,会调用lock.unlock()方法。这时候会调用release(int arg)方法进行释放锁。这时候则会首先通过tryRelease(int releases)来改变state的值,也就是减1,然后在释放占有线程。并且将head节点指向null便于GC回收,然后清空当前节点的thread和pre信息,设置waitsratus=0,并将自己设置为head节点。最后通过LockSupport.unpark(s.thread)唤醒下一个线程

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//尝试去释放锁。
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;
}
/**
 * 唤醒下一个节点
 * @param node
 */
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    
    Node s = node.next;
    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);
}

拿别人的一张图来展示CLH队列关系


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值