ReentrantLock实现源码剖析

    作为一个可重入的独占锁,ReentrantLock与隐式监控锁synchronized有着相同的行为和语义,不过ReentrantLock有着更高的扩展性。

简介

    ReentrantLock由上一次成功获取锁,但还没有释放锁的线程所拥有。如果锁没有被任何线程所拥有,那么此时有线程调用lock方法则会成功获取锁并返回。另外,如果当前获取锁的线程再次调用lock方法,该方法会马上返回。这个可以通过利用方法isHeldByCurrentThread,以及getHoldCount检查。
    类的构造方法有一个可选的fairness参数。如果为true,则在竞争条件下,锁的获取会倾向于等待最长的线程。否则(设为false),则当前锁的获取不会有特殊的规律。实用公平锁可能在总体上会有更加低的吞吐量(也就是说更加慢,一般是慢很多),但对比其那些使用默认配置,公平锁对于不同线程获取锁的时间上会有更少差异,并且保证减少等待饥饿的出现。尽管如此,锁的公平性不保证线程调度的公平,因此,可能会出现一条或者数条线程为了成功获取公平锁会出现多次获取,与此同时,其它活动线程只能白白等待也没法获取锁。

    另外注意的是,非超时版本的tryLock方法没有提供公平性调度。该方法会在其它线程等待的情况下也可以成功获取锁。

具体实现

    ReentrantLock主要在内部定义了一个Sync类,重载类AbstractQueuedSynchronizer,主要处理一些关于可重入的逻辑,然后还定义类NonfairSync,以及类FairSync,分别对应于非公平以及公平策略的锁实现。ReentrantLock的主要public接口均是依靠这几个内部类的方法来实现。
    获取锁和释放锁方法如下。
    public void lock() {
        sync.lock();
    }


    public void unlock() {
        sync.release(1);
    }
    sync变量在构造的时候初始化
    public ReentrantLock() {
        sync = new NonfairSync();
    }


    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    我们主要集中剖析FairSync类和NonfairSync类的实现。

(1)NonfairSync类

    该类实现如下。
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }


        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    这里首先说明一下,ReentrantLock的锁状态值
    1.大于等于1(大于1表示单一线程重复获取锁)表示锁已经被获取;
    2.0表示锁还没有被获取;
    另外我们还要回想一下acquire方法,也就是调用AbstractQueuedSynchronizer的获取锁方法,会尝试获取锁,如果失败则会进入内部的FIFO等待队列,直到能够重新获取锁为止。
    lock方法的实现,首先尝试通过CAS把状态从0变为1,如果成功,则设置当前线程为独占锁的线程;如果失败,则会调用acquire重新尝试竞争获取锁。这里会有一个CAS的原因是由于这是一个非公平锁的策略决定的。
    关于非公平策略,可以假设这一种情况,如果一把锁已经有几个线程在等待队列里等候获取,如果这时候锁释放的时候,刚好此时出现没有进入等待队列的线程尝试获取锁,此时就会出现竞争,非公平策略对于这种情况是允许外来线程能够插队获取(immediate barge),如果CAS成功的话,便马上获取锁,等待队列里的线程便会重新进入等待状态,这样就做成一种不公平的情况。
    同样道理,我们看看tryAcquire的实现,调用的是基类的nonfairTryAcquire方法,方法实现如下。
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (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;
    }
    就如名字一样nonfairTryAcquire,是一个非公平的tryAcquire版本实现,首先调用AQS的getState方法获取当前锁状态值,如果为0则调用CAS尝试更改状态值,如果成功则把当前线程设置为独占锁线程,并且返回true表示已经获取锁。如果锁状态不为0,则必须判断当前的线程是否当前被设置的独占锁线程,如果是,则表明这次是一个重入操作,重新把锁状态值加上请求值acquires,并且返回true。如果以上均失败,表示获取锁失败,需要把当前结点进入等待队列。

    接下来来看看FairSync类的实现。

(2)FairSync类
    所谓的公平策略,其实就是在之前说明的例子里,不允许外来线程能够插队获取锁,如果等待队列中有线程在等待获取锁,则所有外来线程必须要进入等待队列,这样就保证每次获取锁的必定是队列中第一个等待线程,避免了线程等待过长的时间。
  static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;


        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;
        }
    }
    公平策略下的lock的实现很简单,只是调用acquire(1),去掉了之前的允许插队的CAS逻辑。然后再看看tryAcquire的实现。对比起非公平策略的版本,在获取锁的判断里增加了hasQueuedPredecessors的判断,该函数是AQS基类方法,判断当前等待队列里是否有前继结点在等待,如果有则返回true,如果没有则返回false,这样如果hasQueuedPredecessors返回false的时候,表明自己就是队列里第一个等待获取锁的线程,这样才可以顺利获取锁。
    来看看hasQueuedPredecessors的实现。
    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    函数返回true的话,表明在等待队列里有前继结点;返回false表示当前线程不在等待队列,或者等待队列为空,或者等待队列里有前继结点。函数实现很简单,只有几个判断。回想一下AQS的实现里等待队列的特征,就很容易理解这个函数实现。头结点不等于尾结点表明等待队列中至少有一个线程在等待,此时如果头结点的next为null,这是因为头结点已经出队列(可以看看acquireQueued的p.next=null),因此表明有前继结点要返回true,如果不为null,则继续判断后继结点的线程,如果不为当前线程,则也要返回true。

    这样,tryAcquire里添加hasQueuedPredecessors来实现来公平策略。

(3)tryRelease实现
    接下来看看释放锁的时候,sync类重载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;
    }

    函数首先计算当前锁状态值与releases(目前实现必定为1)的差值c,然后如果当前线程不是已经获取锁的独占线程则要抛出IllegalMonitorStateException异常,如果差值c刚好为0,则表示要释放锁,同时把独占线程设为null,返回true表示锁已经被释放,如果c不为0,则表示要为之前重入的一次释放,因此重新设置新的锁状态值,然后返回false。实现主要是注意一下可重入的逻辑,没有其它特别要注意的地方。

总结

    到此,ReentrantLock的解析就完成了。虽然还有其它一些方法来表示当前锁状态,有兴趣可以自行深入了解,这里不再赘述。ReentrantLock要注意的地方有两个,一个就是tryAcquire和tryRelease的实现里要注意可重入逻辑的判断;另外一个就是关于公平策略。其实对于公平锁来说,由于在竞争的条件下必须强制外来线程进入等待队列,由于线程调度是不可控的,因此会大大降低了吞吐量,也使得整个锁的性能降低,但公平锁可以避免某一线程由于需要等待锁要耗费过长的时间,而导致饥饿性,适用于某些场合,因此开发者必须要按照实际情况进行取舍,选择最适合的锁来达到需求。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值