java中的锁(二)AQS

一、概述

在我们上一章中,我们可以看到FairSync与NonfairSync均继承自Sync,而Sync又继承了AbstractQueuedSynchronizer,在这一章,我们就主要了解一下AbstractQueuedSynchronizer的相关内容。本章在讲解AQS的同时也会对共享锁\互斥锁可重入锁乐观锁\悲观锁进行讲解。

二、AQS

AQS,全称为AbstractQueuedSynchronizer,可以将其翻译为“抽象队列同步器”。它本身是一个抽象类,定义了一系列的多线程访问共享资源的框架,供其子类进行相应实现。子类通过继承此抽象类并实现其抽象方法来实现线程同步。

2.1 State

我们查看AQS的源码,可以找到很多与state有关的操作:

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) {
    return STATE.compareAndSet(this, expect, update);
}

state使用关键字volatile修饰,以保证其内存可见性。state可以说是一个资源获取状态的标识符,当state>0时,表示资源处于被获取状态,也就是上锁状态,state=0时表示资源处于可获取状态,即锁被释放。

这里要说明一下,compareAndSetState是一种CAS操作,CAS的标准情况是Unsafe类下的compareAndSwap(V,E,N),V为变量,E为期望值,N为新值,期望值在执行CAS操作前从V中取出,执行CAS操作时,若V的值就是E,则将V赋值为N,若V的值不为E,则表示V的值已经被其他线程修改,则取消本次操作,可以使用循环再次获取E发起一次CAS。而在jdk9中,java摒弃了Unsafe,采用了VarHandle(变量句柄)作为代替,其CAS操作与Unsafe相似,均为native修饰符修饰。CAS操作主要保证了变量的原子性。

三、共享锁与独占锁

独占锁,顾名思义,就是只能有一个线程持有的锁,其主要代表有ReentrantLock。

共享锁,则允许多个线程同时持有锁,例如ReadWriteLock。

独占锁是一种典型的悲观锁,所谓悲观锁就是悲观的认为每次对加锁对象的访问都会造成读写冲突,于是只允许同时对加锁对象进行一个读操作或一个写操作。

而共享锁属于乐观锁,所谓乐观锁就是乐观的认为每次对加锁对象的访问不会造成读写冲突,所以允许多个线程同时对对象进行访问,它只在必要的时候进行相应的判断,如在更新变量的时候查看是否其他线程对变量进行了修改(具体类似于之前说的CAS操作,CAS也是一种乐观锁的实现)。所以它同时允许进行一个写操作或多个读操作。

悲观锁与乐观锁不仅用于java的并发中,也用在数据库的操作中,在这里就不多解释。

3.1 AQS中的独占锁

在AQS中,对锁来说,获取锁是一个重要的操作,下面是AQS中对于独占模式获取锁的底层实现

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个之前我们有过分析,使用tryAcquire进行获取锁的操作,如果未获取到,则向同步队列中添加一个独占锁节点。Node就是在AQS中定义的同步队列的节点,其有两种模式,EXCLUSIVE和SHARED,其中EXCLUSIVE对应的就是独占节点。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

我们打开tryAcquire方法,发现是直接抛出错误,这是因为tryAcquire方法需要我们在继承的子类中进行重写的,其中需要我们自行定义获取锁的逻辑,例如ReentrantLock中对于tryAcquire方法进行了如下重写

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

主要是通过判断state的值来重写的获取逻辑。

这里要说明一点,为什么tryAcquire这个方法在AQS中要抛出错误,然后让子类重写,而不是直接将其设置为抽象方法让子类实现呢。这是因为之前讲过,AQS的具体实现中有独占锁也有共享锁,独占锁只需要实现tryAcquire和tryRelease,共享锁只需要实现tryAcquireShared和tryReleaseShared,若设置为抽象方法,那其子类无论是独占锁还是共享锁,都需要实现这四种方法。

同样,有了获取锁,也就有释放锁,释放独占锁的底层实现是

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

其中unparkSuccessor方法的作用是唤醒处于等待状态,位于等待队列的下一个线程。

tryRelease方法也是与tryAcquire一样交给子类重写

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

在ReentrantLock中对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;
}

3.2 AQS中的共享锁

了解了独占锁之后,再去了解共享锁也就简单多了。

共享锁获取锁的底层实现主要除了acquireShared之外涉及两个方法,这里要注意,其判断标准与独占锁不同,独占锁因为在释放锁之后state为0,所以使用!tryAcquire()来判断,共享锁因为可以进行一次写操作或者进行多个读操作,多以在获取锁的时候要进行读锁与写锁的判断,不能获取到锁就返回-1,故用是否小于0来判断获取锁。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

其中tryAcquireShared与独占锁类似,都是交给子类来实现

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

在可重入读写锁中,对其的实现如下

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    //若当前写锁有其他线程占用,则获取锁失败
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //开始获取读锁
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null ||
                rh.tid != LockSupport.getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

而doAcquireShared方法做的只是添加节点的功能,这里就不在赘述了。

附录

是否独占锁的state最大为1?(可重入锁)

虽然独占锁同时只允许一个线程进行操作,但其实际上state最大并不一定是1。

这就涉及到了一个可重入锁的概念,作为可重入锁,在资源锁释放之前,如果持有锁的线程再次对资源进行申请,可重入锁会判断申请的线程是否为当前持有锁的线程,如果是,则允许该线程对资源再次进行访问,此时,执行了两次获取锁,尚未进行释放锁,state的值就为2,多次重新申请会使state增大。

在释放锁时,进行过几次lock就需要进行几次unlock,直到state重置为0。我们之前讲的ReentrantLock的直接翻译就是可重入锁。

在网上可以找到一个可重入锁的简单范例。

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

参考文章
Java并发之AQS详解
Java并发-独占锁与共享锁
可重入锁与不可重入锁


  转载请注明: 天井 java中的锁(二)AQS

 上一篇
Java的动态代理与静态代理 Java的动态代理与静态代理
代理在现实生活中,我们如果想要卖房,大都不会去自己亲自去跑业务、找买主、谈买卖,而是会找一些第三方的中介,由中介为我们处理卖房前后的一些事务,这个“中介”就是我们的代理人,在代码中,我们很多时候也经常不想让执行的对象直接去处理某些业务逻辑,
2018-09-02
下一篇 
java中的锁(一)公平锁与非公平锁 java中的锁(一)公平锁与非公平锁
在很多程序语言以及中间件中都存在“锁”的概念,在Java中同样根据不同的特性有不同的锁,其中我们常见的锁有以下几类: 公平锁/非公平锁 可重入锁 独享锁/共享锁 互斥锁/读写锁 乐观锁/悲观锁 分段锁 偏向锁/轻量级锁/重量级锁 自旋锁
2018-09-01
  目录