阅读 63

ReentrantLock源码分析

前言

ReentranLock是JDK中基于AQS实现的一种可重入排它锁,实现了Lock接口,提供了锁的基本操,例如加锁、解锁;

使用

public class ReentratLockDemo{
    private static int state = 0;
    private Lock lock = new ReentrantLock();
  
    public static void main(String[] args){
        lock.lock();  //加锁
        try{ 
            state++;
        }finally{
            lock.unlock(); 解锁
        }
    }
}
复制代码

源码解析

构造函数

//无参构造
public ReentrantLock() {
    sync = new NonfairSync();
}
//有参构造 传入fair
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); //如果等于true创建一个公平锁,反之创建一个非公平锁
}
复制代码

加锁过程

lock()方法

final void lock() {
    //如果修改AQS中的同步状态state成功,表示抢占到锁
    if (compareAndSetState(0, 1))
        //将锁的持有线程设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 下面的acquire方法
}
复制代码

acquire()

尝试加锁,但是tryAcquire方法取非,如果加锁失败,那么才会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))。

可以分解为 addWaiter(Node.EXCLUSIVE) 和 acquireQueued(node,arg)。其中 addWaiter(Node.EXCLUSIVE) 是将当前线程封装为一个节点,在个节点是一个在AQS队列中的节点,这个就不多赘述了就是一层简单的封装,主要讨论下后面的acquireQueued;acquireQueued(node,arg)是将节点存放到AQS中。

public final void acquire(int arg) {
    // tryAcquire尝试去获取锁,该方法定义在父类AQS中由子类实现 -> Sync内部类里面的nonfairTryAcquire(int acquires)
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

nonfairTryAcquire()

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread(); //获取当前线程
    int c = getState(); //拿到AQS的同步状态
    //锁未被抢占
    if (c == 0) {  
        //尝试抢占加锁
        if (compareAndSetState(0, acquires)) {
            //加锁成功,将锁的持有线程修改为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果锁被抢占了,判断是否是当前线程(可重入性)的体现
    else if (current == getExclusiveOwnerThread()) {
        // 将同步状态累加
        int nextc = c + acquires;
        if (nextc < 0) //超出了int的最大数,表示超过了允许的最大可重入数
            throw new Error("Maximum lock count exceeded");
        // 和上面 int nextc = c + acquires; 一样没有使用CAS操作,是因为当前操作是在加锁的线程中执行的,是线程安全的
        setState(nextc);
        return true;
    }
    return false;
}
复制代码

addWaiter(Node.EXCLUSIVE)

将线程封装为一个Node节点,并且将其存放到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) { 
            //创建一个头结点,就是初始化队列这么个意思
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
             //将当前节点设置为尾节点,就是相当于是入列了
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
复制代码

acquireQueued(node,arg)

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) &&
        		//如果前面判断为true就需要阻塞当前节点中的线程
                parkAndCheckInterrupt())
                interrupted = true; //interrupted 设置为true的原因我们先卖个关子,后面再用写段代码来说
        }
    } finally {
        if (failed)
            //取消正在进行的获取尝试
            cancelAcquire(node);
    }
}
复制代码

shouldParkAfterFailedAcquire(p,node)

需要在解释一下关于Node中的一些状态值

//表示线程已经取消
static final int CANCELLED =  1;
//表示后继线程需要 unparking
static final int SIGNAL    = -1;  
//表示当前线程在某一个Condition上等待 (在Reentrant Lock中暂时没有使用)
static final int CONDITION = -2;
//表示下一次accquireShared要无条件传播 (在Reentrant Lock中暂时没有使用)
static final int PROPAGATE = -3;
复制代码

是否应该在尝试获取失败后加锁

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //状态是否等于 -1,说明后继节点需要被唤醒,当前节点需要被阻塞
    if (ws == Node.SIGNAL)
        return true;
    //后面这个节点中的线程已经被取消了
    if (ws > 0) {
        do {
            //往前遍历直到找到一个需要被阻塞的线程
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将其等待状态改为 SIGNAL,而不是马上被阻塞。需要调用方重试一次,确保在park之前不能获取到锁。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
复制代码

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    //将线程阻塞
    LockSupport.park(this);
    //返回线程的中断标志
    return Thread.interrupted();
}

复制代码

小节总结(加锁过程)

上面所有涉及到的方法都是用于加锁的过程中,简单的通过画图梳理一下方法的调用情况。

ReentrantLock Lock加锁流程.png

解锁过程

unlock()

非常简单,直接调用内部同步器锁类的release()

public void unlock() {
    sync.release(1);
}
复制代码

release(int arg)

public final boolean release(int arg) {
    //解锁
    if (tryRelease(arg)) {
        Node h = head;
        //如果解锁成功,唤醒AQS队列中的头节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
复制代码

tryRelease(int arg)

尝试释放锁,将AQS的同步状态标识state减为0

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //如果不是当前线程持有的锁 抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //减到0了,说明所有的重入都被抵消,释放锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
复制代码

unparkSuccessor

唤醒节点的后继节点

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);
}
复制代码

小节总结(解锁过程)

ReentrantLock Lock解锁流程.png

总结

将ReentrantLock的源码,按照加锁和解锁的过程分别分析,然后画出了流程图,并且每行代码都有注释分析。我相信现在有资格和面试官Battel一下ReentranLock的源码了

面试官: 小伙子,了解过JUC吗?知道ReentrantLock,给我讲讲呢?

我:那是必须了解过。RenntrantLock是基于AQS同步器队列实现的一种可重入的支持非公平和公平加锁特性的排它锁。接下来我仔细讲讲它的加锁和解锁过程

面试官:你请

我: 巴拉巴拉....(喝口水) 巴拉巴拉....

面试官:小伙子,真不错!那你了解过 ReentrantLock的Condtion吗?它是怎么实现的呢?可以给我讲讲吗?

我:且看下篇分享

文章分类
后端
文章标签