ReentrantLock 的实现原理以及源码分析

ReentrantLock是一个可重入的锁,内部采用AQS来实现

下面我会对其源码进行详细的分析,因为贴了源码,所以我会直接在源码上进行注释,并加入一些我自己的理解,以求大家可以更容易的理解。废话不多说,直接开始:

ReentrantLock的构造函数:

 

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

可以看到构造函数做了一件事,就是将lock实例的sync字段进行了初始化,从代码可以看出根据参数的不同,将sync初始化为不同的实现,即公平锁FairSync和非公平锁NonfairSync,那sync到底是什么呢?

 

abstract static class Sync extends AbstractQueuedSynchronizer {}

具体的代码就不粘出来了,可以看到Sync继承自AbstractQueuedSynchronizer抽象队列同步器AQS,也就是说ReentrantLock的真正实现其实是依赖了AQS,下面就对AQS进行分析

 

首先看一下AQS中比较重要的属性

exclusiveOwnerThread:独占锁线程,指向了当前获取到锁的线程

state:AQS的核心,AQS就是用这个字段来实现锁的获取和重入,在没有线程获取到锁的时候,锁的状态为0,获取的时候,通过cas对其进行+1,并且每重入一次再 +1,释放一次 -1,具体的后面代码展示

head,tail:AQS维护了一个内部类Node的双向队列,由未获取到锁的线程包装成的Node节点组成,也就是获取锁失败加入队列尾部。

 

AQS的获取锁过程分析

线程通过CAS将state从0设置为1,如果设置成功,说明获取锁成功,并且将exclusiveOwnerThread指向自己;

当调用lock方法时,公平锁和非公平锁的实现有区别

非公平锁的实现:线程会直接进行CAS操作去设置state,如果成功就获取到锁,如果失败,继续调用NonFairSync的acquire(1)方法

公平锁的实现:线程会直接调用FairSync的accquire(1)方法,这两个方法都调用了AQS的方法

 

public final void acquire(int arg) {
    //第一步尝试获取锁
    //如果成功了,就可以执行自身的逻辑了
    //如果失败了,执行下一步addWaiter()方法,也就是将自身线程包装成Node加入到队列中
    //第三步 死循环获取锁,如果失败则将线程挂起
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里需要看一下tryAcquire方法,首先是非公平锁

 

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取AQS的状态
    int c = getState();
    //如果等于0,说明没有线程获取到锁,通过CAS尝试获取锁,跟一开始的逻辑一模一样
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果不等于0,说明有线程已经获取到锁了,这里判断AQS的独占锁指针是不是指向自己
    //如果是,说明是重入,这里就是重入的实现
    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;
}

下面看一下公平锁的实现

 

protected final boolean tryAcquire(int acquires) {
    //前面的逻辑都一样
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //差别就在这里,多了一个hasQueuedPredecessors方法,这是跟非公平锁唯一的区别
        //也就是为什么叫公平锁体现在这里。
        //这里判断有没有别的线程比他更早来,如果返回true说明有,肯定就直接获取失败了
        //如果没有别他更早来的线程,那么自己就可以去尝试获取锁了
        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;
    //这里就是干了这样一件事,判断头节点的下一个节点是不是自己
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

总结一下,AQS中维护了一堆wait线程组成的等待队列,凡是进入了这个队列的线程,之后就会按顺序一个一个获取到锁,执行逻辑,也就是将他们串行化了,那么公平和非公平体现在什么地方呢?

就是tryAcquire这里,公平锁是说新线程进来对于队列中的线程是公平的,如果队列中有等待线程,它就直接往后排,而非公平锁是,新线程对于队列中的等待线程是不公平的,可能存在队列中的头节点释放掉锁之后唤醒下一个线程,结果有一个新的线程进来同时获取锁,这个时候他们机会是平等的,因此说这是非公平锁。

 

下面看一下addWaiter方法

 

private Node addWaiter(Node mode) {
    //将当前线程封装成node
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //如果tail不为空,也就是队列中已经有一些等待线程了,直接加入尾部后返回即可。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果tail为空,就会走到这里来
    enq(node);
    return node;
}
private Node enq(final Node node) {
    //死循环,为什么死循环呢?往下看
    for (;;) {
        //拿到tail,第一次循环这里肯定是null
        Node t = tail;
        if (t == null) { // Must initialize
            //第一次进来,将一个空的节点作为头和尾节点,看一下Node的构造函数和属性
            //可以发现头节点的waitstatus=0
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //第二次进来就到了这里
            node.prev = t;
            //将node节点插入到队列的最后
            //这里为什么这么做呢?向下看
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

解答上面的问题,能这么设计就有他的原因,看一下死循环获取锁的方法

 

final boolean acquireQueued(final Node node, int arg) {
    
    boolean failed = true;
    try {
        boolean interrupted = false;
        //这里是死循环,是核心
        for (;;) {
            //获取node节点的prev节点,如果没有直接报错
            final Node p = node.predecessor();
            //判断p是不是头节点,如果是,尝试获取锁
            //如果能够成功,说明head要么取消,要么已经释放了锁
            //所以将当前节点设置为头节点,也就是说删除头节点的操作是在这里执行的
            //并不是在释放锁的时候执行的
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 将node的状态改为singal之后挂起,等待有线程唤醒他之后
            // 就可以继续循环获取锁了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //这里就是线程可能会被中断,如果中断了就从队列中去除掉
        if (failed)
            cancelAcquire(node);
    }
}
//node节点获取锁失败就会进入这个方法,第一次进入的时候,pred是头节点,并且ws=0

//这个方法的源码先看else块,再看第一个if,再看第二个if
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.
             */
            
            //第二次走这里
            //直接返回true,那么就会调用park方法,线程阻塞
            return true;
        if (ws > 0) {

            //这里就是当node的前置节点不是头节点并且状态是已取消状态
            //那么就是去找下一个正常的前置节点,这里就是维护一下链表的关系
            //之后return false 同样是要进行二次循环
            /*
             * 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.
             */
            //第一次进来走这里,把头节点的ws设置为-1,然后return false
            //也就是外面的循环逻辑会走第二遍,那么第二遍进来的时候,头节点的ws已经是-1了    
            //就会走第一个if
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 

释放锁的原理

 

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    //尝试释放锁,成功之后,准备唤醒头节点的下一个正常节点
    if (tryRelease(arg)) {
        Node h = head;
        //如何代码没有看仔细,会对头节点的waitStatus状态有疑惑
        //以为头节点是一个空姐点,初始状态为0
        //但是经过了shouldParkAfterFailedAcquire方法之后,状态就是变成-1
        //所以这里是true,正常进行释放操作
        if (h != null && h.waitStatus != 0)
            //这个方法就是真正的唤醒头节点的下一个正常的节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //重入性质
    int c = getState() - releases;
    //如果当前线程不是AQS的独占锁线程直接报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //直到全部释放完成,将独占锁线程设置为null,释放成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //重入锁就一直 -- ,然后CAS设置状态就好了
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
   //获取头节点的状态
    int ws = node.waitStatus;
    if (ws < 0)
        //这里将头节点的状态变成0
        //等待后置节点将其替换
        compareAndSetWaitStatus(node, ws, 0);

    // 获取头节点的下一个节点
    Node s = node.next;
    // s如果是null,或者状态是已取消的状态,就从尾部获取下一个正常的可以唤醒的节点
    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;
    }
    // 通过上面的计算获取到的节点,在这里进行唤醒
    // 存在当前node既是head也是tail的情况,这里的thread就是null
    if (s != null)
        LockSupport.unpark(s.thread);
}

 

整个ReentrantLock的源码分析就是这样,笔者在看了这些源码大概几十次之后(但是都是粗略看的,惭愧,认真看也就是两次,一次是开始写这篇文章,另外一次就是修改第一次写的时候一些错误的地方),终于看懂了,其实不是很难,相反理解之后反而觉得非常之简单,自身的收获也很大。欢迎大家对我的分析进行指正,大家一起进步!!!

 

原创不易,且行且珍惜。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值