读懂ReentrantLock源码

友情提示:本文环境是jdk1.8,阅读本文需要对AQS和CMS有一定的了解,不然可能会有点吃力,不过应该也能看懂,我对涉及到AQS的部分有做相关的解释

谈起java并发,Lock和synchronized关键字肯定是避免不了的,两者都可用于并发环境下同步控制线程的安全性,无论是在日常使用还是面试中,都是十分常见的。今天先来品一品ReentrantLock(可重入锁),Lock的实现类之一,可重入指的是一个线程能够对一个临界资源重复加锁。

ReentrantLock构造函数和成员变量

先看一下ReentrantLock的成员变量,很简单,就一个Sync变量,Sync是ReentrantLock的一个抽象内部类,它继承了AbstractQueuedSynchronizer(AQS),同时ReentrantLock类里还有NofairSync和FairSync两个内部类,这两个内部类又都继承了Sync,我们所创建的ReentrantLock对象是公平锁还是非公平锁就是通过这两个类来控制的,通过这两个类的名字就能看得出来。

 private final Sync sync;

看 ReentrantLock的构造方法,一目了然

    //无参构造函数创建的是非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    //我们也可以通过有参构造函数来创建公平锁或非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock的方法

我们在使用锁的时候,最多的就是使用lock和unlock这两个方法了吧,下面通过源码来看看它们是怎么实现的

    public void lock() {
        sync.lock();
    }

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

可以看到,这两个方法都是直接调用了成员变量sync的方法实现的,通过前面的构造函数我们知道sync变量的值取决于我们创建的是公平锁(FairSync)还是非公平锁(NonfairSync)。

注意,前文说到Sync继承了AQS,所以无论是NonfairSync还是FairSync的方法,都会涉及到AQS的成员变量和方法,我这边先简单提一下,以免下面代码不好理解。

AQS有一个state的私有变量,该变量用于记录锁的状态,0代表锁没有被线程占用,1代表有线程持有当前锁,该值可以大于1,因为锁是可重入的,每次重入都加上1。

AQS有一个阻塞队列,用于存放没有获得到锁的线程,注意,该线程的头部节点为获得锁的节点,是不算在阻塞队列中的(感觉有点拗口,但这非常重要)

lock方法实现

先来看NonfairSync内部lock的实现

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;


        final void lock() {
            //尝试用CAS操作去设置State的值,将0改成1
            if (compareAndSetState(0, 1))
                //如果CAS操作成功,将当前线程设置为正在执行的线程,代表该线程成功获得锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //CAS操作失败,调用acquire,该方法是AQS的方法,我把它放在下面
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            //该方法的实现在Sync类中
            return nonfairTryAcquire(acquires);
        }
    }


    //先走第一个判断条件tryAcquire再次尝试获取锁,如果获取失败返回false,执行acquireQueued
    //acquireQueued和addWaiter都是AQS的方法                
    //当线程没有获得锁时会调用它们将线程添加到阻塞队列,我将源码放在下面
    public final void acquire(int arg){
        if(!tryAcquire(arg)&&
            acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
            selfInterrupt();
    }

下面这一段代码是线程获得锁失败时,AQS将线程加入到阻塞队列并将其挂起的实现,对AQS没有了解的同学 读起来可能比较吃力,可以自己打开AQS源码配合着一起看,或者先跳过,但是AQS是java并发的基础,希望大家或多或少的去了解一下。

    //addWaiter方法用于将线程加入到阻塞队列中
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //如果队列不为空
        if (pred != null) {
            // 将当前的队尾节点设置为自己的前节点
            node.prev = pred;
            //通过CAS将自己设置为新的队尾节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果队列为空或者CAS操作失败(有线程竞争入队列)将会调用此方法
        enq(node);
        return node;
    }
    
    // 采用自旋的方式入队
    // 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
    // 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
    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;
                }
            }
        }
    }

    //前面的方法是将线程添加到阻塞队列中,而将线程挂起是由acquireQueued实现的
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获得当前线程节点的前节点
                final Node p = node.predecessor();
                //判断前节点是否为头部节点,是的话调用tryAcquire尝试获得锁
                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);
        }
    }

nonfairTtyAcquire这个方法存在于Sync类中,我这里无法理解为什么要这么设计,而不是像FairSync类一样直接将实现写在tryAcquire方法中,这样调用NonfairSync的tryAcquire时还需要再去调用一次Sync的nonfairTtyAcquire,如果有懂的小伙伴欢迎在下面留言告诉我。

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取state的值
            int c = getState();
            //如果c=0,即当前没有线程获得锁,用CAS尝试去将state的值修改成acquires
            //如果修改成功,则当前线程成功获得锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //修改不成功,判断当前获得锁的线程和该线程是否为同一个线程(可重入的实现)
            //如果是,则将state+1,否则返回false
            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;
        }

简单总结一下非公平锁的lock实现原理,线程1调用lock方法,此时锁还没被任何线程占用,state为0,线程1会通过CAS将state从0设置为1,表示当前锁已经被它所占用,此时线程2也调用了lock方法,线程2会通过CAS尝试将state从0改成1,但是此时的state已经为1了,所以这里失败了,接着往下走,线程2会去判断它 是否和线程1是同一个线程,如果是,那么线程2可以获得锁,并将state+1(这就是可重入锁),如果不是同一个线程,线程2会作为尾节点加入到阻塞队列,然后再将线程2挂起,等待线程1释放锁并将其唤醒。

FairSync内部lock实现

FairSync的lock方法实现大部分和NonfairSync的相同,只是少了一开始就通过CAS去尝试修改state的值(这就是插队,如果这时候锁刚好被释放,那么线程就能直接获取到锁,这就是非公平锁和公平锁的区别),代码和上面的基本一样,应该很容易看懂

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        
        //可以看到,公平锁这里没有一上来就用CAS操作去获得锁,而是调用了acquire
        final void lock() {
            acquire(1);
        }

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

 

unlock方法实现

unlock方法比较简单,无论是公平锁还是非公平锁,他们的调用的都是release(1),release方法的实现在AQS类中

   public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg){
    //Sync类重写了tryRelease方法
    if(tryRelease(arg){
        //tryRelease返回true,则说明该线程成功释放锁,AQS队列将删除对应的节点
        Node h=head;
        if(h!=null && h.waitStatus!=0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//Sync重写的tryRelease方法实现
protected final boolean tryRelease(int releases){
    //获得state的值并将其减去releases
    int c = getState() - releases;
    if(Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllealMonitorStateException();
    boolean free = flase;
    //当c==0的时候才会将free设置为true,此时没有线程占有锁
    //所以调用setExclusiveOwnerThread设置exclusiveOwnerThread为空
    if (c == 0){
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

可以看到,只有当c==0时,才能将state的值修改为0,这和上面一个线程调用lock时state值的叠加是相对应的,所有一个线程想要释放可重入锁,它需要调用和lock相同次数的unlock,才能成功的将锁释放。

至此,ReentrantLock的lock和unlock方法就讲完了,这两个方法也是这个类最常用的两个方法,由于其他的方法都比较的简单,这里就不再继续展开讲,有兴趣的同学可以自行去阅读源码,如果你已经理解了上面的内容,应该很容易读懂其他的几个方法。

由于作者能力有限,如果有什么没看懂或者文章有什么错误的地方,欢迎在下面留言讨论。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值