深扒AQS(三)之ReentrantLock

前言

前边两节我们分别讲了AQS的阻塞队列和条件队列,算是对AQS有了一个基本的了解,这一节中,来看一哈AQS在JUC中的一些锁上的应用

ReentrantLock

可重入锁,顾名思义,获得这个锁的线程还可以再次获得,与此对应的,放弃这个锁的次数必须跟获得次数对应,不然就不算彻底放弃。Synchronized也是可重入锁。

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    // 当前持有锁的线程
    private transient Thread exclusiveOwnerThread;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // some methods
    }
    
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

}
复制代码

锁一般都会继承Lock接口,接口中主要定义一些锁的操作方法,lock() 之类的,我们不细说。 可以看到其实ReentrantLock有一个内部类属性sync,Sync继承自AQS.后边我们会看到大多数锁的方法都是基于这个内部类实现的。

ReentrantLock分为公平锁和非公平锁两种,均是Sync的子类

获取锁

public void lock() {
        sync.lock();
}
复制代码

正如我们上边所说,通过内部类sync获得锁,sync又分为FairSync和NonfairSync.

非公平获取锁

static final class NonfairSync extends Sync {

        final void lock() {
            // CAS设置state为1
            if (compareAndSetState(0, 1))
                // 设置该锁持有者为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            // CAS失败
            else
                acquire(1);
        }
}

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}
复制代码

可以看到,若是CAS设置state为1成功就直接完了,否则的话acquire(1),可以看到这里的参数为1,参数指的其实就是更新的state的值,其实state在ReentrantLock中的语义就是重入次数。这里获取到锁的话,重入次数加1,所以这里的参数为1.

// 参数为 1
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
复制代码

这个代码熟悉吗,又跳回到第一节中的AQS中来了,正是独占获取资源,我们之前说过AQS不提供tryAcquire(),所以tryAcquire()又会回ReentrantLock,同时,不要忘了我们这里是非公平锁,我们去看非公平锁的tryAcquire()

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // c==0说明当前没有线程持有锁
            if (c == 0) {
                // cas成功的话
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //  c!=0且当前线程是锁持有者
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 更新state,此时当前线程持有锁,不需要CAS
                setState(nextc);
                return true;
            }
            // 没有获得锁,放入阻塞队列 
            return false;
    }
}
复制代码

总结下非公平锁的获取流程:

  1. CAS设置state由0到1,0的语义即锁空闲,CAS成功的话直接获得锁结束流程
  2. CAS失败的话,进入nonfairTryAcquire
  3. 若是state为0,再次尝试CAS,成功代表获取到了锁,将锁的持有者设为当前线程,返回true结束流程
  4. 若是state不为0,首先判断当前线程是否是锁的持有者,若是的话直接再次获取,直接更新state,返回true
  5. 走到这一步,当前线程只能进入阻塞队列了,后续流程参考第一节在阻塞队列中怎么获取锁
公平获取锁

所谓公平就是按顺序先来后到获取锁,比如A先来获取锁且A前边没有别的线程来获取锁,即便此刻轮不到A,下一个锁的持有者也必须是A

final void lock() {
            acquire(1);
}
复制代码

可以看到这里相比非公平没有上来就CAS,上来就CAS没法保障A上来的时候state为0可以CAS成功,假如A之后又来个线程B请求锁,碰巧此时state为0,B就CAS成功了,而B在A之后,所以上来就CAS不能保证公平。

接着还是一样的去AQS中调用acquire,再回ReentrantLock调用公平锁的tryAcquire()

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            
            // state为0
            if (c == 0) {
                
                // 如果当前线程是第一个来请求锁的且CAS成功
                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;
}
复制代码

跟非公平相比,只多了一个判定条件,hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        
        // h==t说明队列为空
        return h != t &&
            // h!=t并且s==null说明会有元素作为AQS第一个节点入队列
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
复制代码

这是AQS中的一个方法,如果当前AQS队列为空或者当前线程是AQS的第一个节点(非哨兵节点)返回false。

其他方法获取锁
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            // AQS中的方法
            doAcquireInterruptibly(arg);
}
复制代码

这个方法可以响应中断

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
}
复制代码

这个方法不会阻塞,成功获取锁就直接返回true,否则返回false,可以看到采用的是非公平方式。

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
复制代码

带有超时时间的获取锁

释放锁

public void unlock() {
        
        // 参数依然为1
        sync.release(1);
}
复制代码

类似于上边的获得锁,参数依然为1,毕竟每释放一次重入次数减一,调用AQS的release,接着回调到ReentrantLock的tryRelease

protected final boolean tryRelease(int releases) {
            
            //  state-=1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                // c为0的话当前线程不再持有锁
                setExclusiveOwnerThread(null);
            }
            // c不为0更新state,设置重入次数为原次数减一
            setState(c);
            // c!=0的话,返回失败,即并没有释放锁,只是冲入次数减一
            return free;
}
复制代码

这个代码本身很简单,不过这里可以提一下前边一个埋点,第二节中有个fullyRelease方法还有印象不,这个方法中先得到state,然后全部release掉,即state清空,这也是我们说的await必须要释放锁,必须清空state

总结

ReentrantLock算是比较简单的,内部使用AQS实现的可重入锁,state指的是可重入次数,state为0表示当前锁空闲,state大于0说明锁被占用,同时还有非公平和公平获取锁两种方式,同一时刻只有一个线程持有该锁,从方法中也可以看出来,调用的都是AQS中的独占的方式。


个人公众号:

猜你喜欢

转载自juejin.im/post/5dbd7757e51d452a161e0016