java并发之AbstractQueuedSynchronizer

我们知道,在进行并发编程的时候多多少少总会涉及到对共享资源的并发处理,这个时候就需要使用同步工具来控制线程对资源的并发访问了。我们可以使用synchronized关键字来控制同步,也可以从java.util.concurrent.locks包下的众多lock中挑选一款合适的锁来进行控制。很简单是吗?但是你知道这些锁的内部实现原理是什么样的吗?请听我一一道来,文章很长,相信爱学习的你一定会耐心的看完。

框架及功能

AbstractQueuedSynchronizer(以下简称AQS),是一个用于构建锁或者其他相关同步装置的基础框架,是实现锁的关键,并发包下的锁都是在这个类的基础上封装而成的。

AQS使用一个volatile int state代表共享资源,使用一个FIFO的双向队列(也成CLH)来实现对对资源的同步访问,队列中的每一个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;
        static final int PROPAGATE = -3;
        volatile int waitStatus;//表示线程状态,还可能为0
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
}

AQS定义了两种获取资源的方式,一种是独占式的(EXCLUSIVE),一种是共享式的(SHARED)

作为一个抽象类,AQS在顶层已经帮我们实现好了队列的管理,然后将争用资源的方式定义为不可调用的(如果不复写的话,调用就抛出异常)。不同的同步器通过实现自己特定的获取资源的方式,对外表现出不同的形态。

ReentrantLock源码分析

前面说了那么多,可能大家只是知道了AQS是干什么的,具体其是怎么实现同步的,还是不大明白。这里我就结合ReentrantLock的源码,从头开始一步一步的进行分析,由于ReentrantReadWriteLock的实现原理是和其类似的,这里就不再累赘分析了。

首先,我们看看ReentrantLock里面的成员:

public class ReentrantLock implements Lock, java.io.Serializable{
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer{...}
    static final class NonfairSync extends Sync{...}
    static final class FairSync extends Sync{...}
    public ReentrantLock() {
        sync = new NonfairSync();
    }
}

从上面我们可以看出,ReentrantLock里面含有两个同步器锁,均继承自AQS,默认使用非公平的锁。关于公平锁和非公平锁有什么区别,稍后会进行介绍。

Lock操作分析

假设我们使用非公平锁,然后调用其lock方法,即

ReentrantLock lock = new ReentrantLock()
lock.lock()

这个时候会发生什么呢?其首先会调用sync的lock方法,由于sync的lock方法是抽象的,在具体实现类中实现,因此调用NonfairSync的lock方法:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

lock方法主要做两件事情:
1. 调用CAS操作争抢资源,看是否能够争抢成功,如果成功,就以排他的形式设置自己为资源的owner
2. 如果争抢资源失败,或者资源已经被占用根本就不能争抢,则调用acquire方法

那么acquire方法又是来干什么的呢?很显然,没抢到资源,肯定是要在前面所说的队列中进行等待咯。

/*acquire方法在AQS中进行实现*/
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法首先调用tryAcquire()方法(其是一个抽象方法),然后调用NonfairSync的nonfairTryAcquire方法,那么这个nonfairTryAcquire又是干什么的呢?

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); //获取状态
    if (c == 0) { //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;
}

nonfairTryAcquire方法完成的事情如下:
1. 首先获取当前的状态,如果c==0,说明没有该锁没有被其他的线程获取,使用CAS操作设置锁的状态为acquires,设置当前锁的独占线程为自己,返回获取锁成功。很显然,获取锁成功的线程没有进入等待队列。
2. 如果有两个线程同时获取锁的状态c==0,这时候两个线程均使用CAS设置锁状态为acquires,很显然,只有一个会设置成功,成功的返回true,失败的返回false
3. 如果一个线程调用getState()的时候c!=0,说明该锁已经被获取,执行else——if语句,判断是否是自己获取锁,如果是则重入,设置新的state为c+1,返回true。否则的话返回false

注:重入锁的含义是,成功获取锁的线程还能继续获取锁,每次线程获取锁的时候该锁state就会+1,每次unlock的时候都会-1,state为0的时候表示释放锁

继续回到acquire方法中,当tryAcquire返回true(nonfairTryAcquire返回true,获取资源成功)时,当前线程获取资源成功,acquire返回,一切结束;当tryAcquire返回false(nonfairTryAcquire返回false,获取资源失败)时,先以独占的方式进行addWaiter操作,然后进行acquireQueue操作。那么addWaiter操作又是干什么的呢?是否和其名字一样,是添加等待者呢?看下面源码:

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;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

果然不错所料,addWaiter用来将当前线程节点加入到CLH队列中,其首先通过一个快速入队的方式将其加入到队列的尾部,成功则直接返回,当pred为null,或者compareAndSetTail操作失败的时候,快速入队失败,执行enq操作。

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

enq操作就很简单了,如果队列为空,就首先生成一个只含空Node节点的队列,然后再讲node入队列。这个入队的node,就是当前线程的抽象。

不管以哪种方式,addWaiter方式都会将当前线程节点加入队列尾部,然后对返回的当前线程的Node节点执行acquireQueue操作,那么acquireQueue操作又是啥?(为嘛一个简单的获取锁操作这么复杂,调用链辣么长,囧)

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

这个方法主要干了如下几件事情:
1. 获取当前节点的前继节点,如果其前继节点是head节点,并且尝试获取锁成功(tryAcquire,功能见上面分析),那么将当前节点设置为head节点,返回interrupted=false,不对自己进行中断操作
2. 如果其前继节点不是head节点,或者其尝试获取锁失败,那么就会执行下面的一个if语句,这个if语句只有在shouldParkAfterFailedAcquire方法返回true之后,才会执行parkAndCheckInterrupt()操作,执行完了之后继续循环,直到获取锁成功

可能有人会说,你这样一直在这里循环,不是白白浪费CPU资源吗?嗯,是的,如果一直在这里循环的话,当然会浪费资源,但是这样的事情怎么会发生的?我们首先看parkAndCheckInterrupt操作

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

这个函数主要是调用LockSupport的park操作,使当前争用锁的线程阻塞(park),避免线程无休止的循环。

LockSupport park()和unpark()实现线程的阻塞和唤醒,通过调用JNI的park()和unpark()方法来完成的,内部是调用pthread_mutex_lock()然后调用内核方法使线程阻塞

弄清楚上面那个方法后,我们再来看shouldParkAfterFailedAcquire()这个方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

注释已经说的很清楚了,该方法主要功能如下:
1. 如果这个节点的前一个节点的状态是SIGNAL,那么该方法就返回true,说明自己可以正常的阻塞了,因为前一个线程是SIGNAL的,就可以确保锁被释放的时候,前面的线程可以被唤醒
2. 如果这个节点的状态值大于0(状态为CANCELLED),说明该节点前面的线程由于某种原因,例如超时等取消了对锁的争用,这个时候就需要循环的找到其前面一个非取消的节点,然后返回false,继续acquireQueued的循环
3. 如果其前一个节点状态既非SIGNAL,又非CANCELLED,那么我们就将其prev节点状态设置为SIGNAL。然后继续acquireQueued的循环

在acquireQueued的下一次循环中,如果不能满足第一个if条件,那么就会继续进入shouldParkAfterFailedAcquire这个函数,这时候该函数就会返回true,然后执行parkAndCheckInterrupt使线程阻塞。

总的来说,进入acquireQueued方法后,要么其满足第一个条件,然后争用锁成功,这样获取锁的操作便结束了;要么其满足第二个条件然后使自己阻塞,等待别人释放锁后将其唤醒,或者等待超时后取消争用锁。

在开始分析锁的释放操作前,我们顺便提一下锁的公平性和非公平性是如何区分的,对去非公平锁而言,前面已经介绍过了,总结来说就是一句话:“获取锁的线程先尝试争用锁,争用失败后才会进队列阻塞”,对于公平锁而言,所有的线程都是先入队列,然后再按顺序获取锁。

//Fair
final void lock() {
    acquire(1);
}

公平与非公平锁的区别主要是对新争用锁的线程而言的,对于已经入队列的线程,其始终按照先来后到的顺序获取锁

unlock操作分析

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操作,如果tryRelease操作失败,就直接返回false,否则就执行if里面的逻辑。辣么,这个tryRelease又是干什么的呢?直接撸源码

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

这个tryRelase操作是在Sync里面实现的(FairSync和NonFairSync的抽象父类),主要功能如下:
1. 如果当前线程和锁的持有者不是同一个,则抛出IllegalMonitorStateException异常,否则执行步骤2
2. 计算当前线程释放锁成功后的state值c,如果为c=0,说明当前线程是持有锁的最后一个线程,将锁的持有者设置为null,然后返回true
3. 如果c!=0,说明锁仍然被线程持有,例如线程多次重入锁的时候释放一次就不能完全释放,这时只需要将锁的state设置为c,然后返回false

从上面我们可以看到:tryRelease只有在完全释放掉锁的时候才会返回true,否则返回false

再回到release方法中,当锁完全被释放的时候,如果锁没有后继等待者,那么就可以直接返回,否则,就需要唤醒其后继者

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

由于传进来的是head结点(已经运行完的线程节点),所以这个函数的主要功能是找到队列中最前面且未放弃的线程,然后将其唤醒。为什么要这么做,是因为有些线程在等待锁的过程中由于超时或者其他原因取消了等待,这个时候就需要跳过这些节点,唤醒真正等待锁的线程。至此,整个unlock操作就已经讲解完了。

总结

AQS中比较难以理解的部分我们在前面结合ReentrantLock的源码都已经讲解完了,由于篇幅的原理,还有很多东西没有讲到,例如读写锁具体是什么实现的,这些东西其实和前面基本流程是相同的,只是在处理上有一点区别,这里简单提一下ReentrantReadWriteLock中的一些特性:
1. ReentrantReadWriteLock中也有两种锁:公平锁和非公平锁,默认是非公平锁。
* 非公平锁:并不是按照先来后到的顺序进行争锁,在锁被释放的时候,新争用锁的线程和队列头部线程均可以争取锁,这样有利于写操作。非公平锁的吞吐率要高于公平锁。
* 公平锁:利用AQS的CLH队列,严格按照入队列的先后顺序分配锁。同样一个线程持有写入锁或者其前面已经有一个写线程在等待了,那么试图获取公平锁的所有线程都将被阻塞。直到最先的写线程释放锁。
2. 锁降级:写线程获取写入锁后可以通过获取读取锁,然后释放写入锁,这样就从写入锁变为读取锁,从而实现锁降级的特性
3. 锁升级:读取锁是不能直接升级为写入锁的,因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁试图获取写入锁而都不释放读取锁就会发生死锁
4. 在AQS中state用来表示有多少线程持有锁,在独占的时候这个值通常是0或者1,如果是重入,就是重入的次数;在读写锁中,需要两个数来描述读锁或者写锁的数量。显然一个state不够用,于是ReentrantReadWriteLock中将这个字段一分为二,高位16位表示共享锁的数量, 低位16位表示独占锁的数量。

ReentrantReadWriteLock的缺点:1.可能面临饥饿 2.不支持锁升级(不能将获取的读锁升级为写锁)3.所有的读操作均为悲观读,不支持乐观读。这些缺点将在java8的StampedLock中得以解决。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值