Java中ReentrantLock,公平锁,非公平锁与synchronized

本文内容及代码参考视频:https://www.bilibili.com/video/BV1ta4y1H73X/?spm_id_from=333.788.recommend_more_video.-1

ReentrantLock是Java开发中最常用到的组件之一,简单来说,可以把ReentrantLock理解为基于AQS所实现的公平锁与非公平锁,用于实现对共享资源对象的同步。它和synchronized关键字一样支持可重入,但在调度上相较于synchronized更加的灵活。接下来我们从其内部继承和功能实现上来对其进行详解。本文内容基于AQS的基础上讲解,如果对AQS不了解请先了解AQS的内容。

1.ReentrantLock的内部结构

ReentrantLock的源码定义如下:

public class ReentrantLock implements Lock, java.io.Serializable

我们可以看出,ReentrantLock是Lock接口的一个实现类,那它必然就会对Lock接口中定义的方法进行了实现,下面我们先看一下Lock接口中都定义了哪些方法:

public interface Lock {

    //尝试获取锁,如果锁被占有则等待获取
    void lock();

    //与lock类似,区别在于当前线程等待锁时,如果被中断,则直接抛出中断异常
    void lockInterruptibly() throws InterruptedException;

    //尝试获取锁,无论成功与否都直接返回
    boolean tryLock();
    //规定尝试获取所得时间
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //解锁
    void unlock();
    //为当前lock绑定一个Condition对象
    Condition newCondition();
}

我们可以看出Lock接口中主要定义了加锁,解锁,锁中断和锁关联条件这几个方法,那么ReentrantLock中具体是怎么实现这些方法的呢?在了解这些具体的实现之前,我们不妨先来看一看ReentrantLock中都有哪些值的注意的内部属性:

 private final Sync sync;

其实ReentrantLock类内部只有一个内部属性,就是Sync类型的sync,它的初始化是在ReentrantLock初始化时候传入的,代码如下:

    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

我们可以看出ReentrantLock在初始化时,如果不传入值,会默认构造一个NonfairSync()实例赋值,而如果传入值则会根据传入值构造NonfairSync()或FairSync()实例,而这就是我们常说的公平锁和非公平锁。说到这,我们就会更加好奇这个Sync类到底是什么了,接下来,让我们看一下Sync的具体实现:

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        
        abstract void lock();

        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) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }


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

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

从上面的代码我们可以看出,Sync是一个继承了AQS的抽象类,它的内部有一些实现好的方法也有一些抽象方法,而我们常说的公平锁和非公平锁也都是它的具体实现子类。接下来我们来一个一个分析Sync中的在线方法。

首先来看,Sync类中只有lock方法与readObject方法没有被final修饰,而只有lock方法被abstract修饰,简单来说,其要求子类自己在lock方法中重写自己的加锁逻辑,而readObject方法则是反序列化的一个方法,使用不是很频繁。

接下来,我们看Sync中一个重要的方法nonfairTryAcquire,看名字是非公平的获取锁,但我们之前说了,公平锁和非公平锁都是Sync的子类,那为什么要把nonfairTryAcquire这样一个非公平获取锁的方式写在Sync类中呢?

其实这里正是ReentrantLock实现可重入性的方法,我们看这个方法的具体流程,首先,它提供AQS中的getState方法获取到当前资源对象锁的占有状态,记为c,同时获取当前想要加锁的线程ID,如果c==0,代表此时资源没有任何锁,那么就用CAS的操作修改资源的状态值,然后将当前线程设置为独占状态,并返回加锁成功;若如果此时资源状态值不为0则代表当前资源状态已经被加锁了,那么就获取当前占有当前资源状态的线程ID,与当前线程比较,若一样修改资源状态值为当前资源状态值+加锁次数,然后返回加锁成功。值得一提,之前讲解AQS时我们说过,资源状态state是用int型数据表示的,那么就代表一个资源最多同时被加锁的值为int的最大值,超出后会溢出为负数。通过对nonfairTryAcquire方法的描述,我们就可以看出ReentrantLock是如何实现锁的可重入性的。

到此我们了解了ReentrantLock是如何加锁的,那么接下来我们研究tryRelease方法看看ReentrantLock是如何解锁的吧:

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

解锁的代码实现比较简单,其实还是利用AQS中的state记录资源被锁的占有状态,如果对比发现当前线程与占有资源的线程不一致,则抛出异常,如果当前解锁操作会导致资源完全释放则直接置资源独占线程置为空。最后统一修改资源状态state即可。

Sync类中其它的方法都比较简单,在此就不一一叙述,接下来让我们来看一下它的子类,也就是我们常说的公平锁与非公平锁的实现。

2.非公平锁NonfairSync

所谓的公平锁与非公平锁简单来说就是获取所得顺序与线程到达的顺序是否一致,如果一致,那么它就是公平的,不一致就是不公平的,在ReentrantLock中,这两个锁都是基于Sync类实现的,下面让我们先看一下非公平锁NonfairSync的实现:

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

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

从代码中我们可以看出,非公平锁NonfairSync的实现非常简单,它的内部有一个加锁的方法lock,里面就是通过CAS的操作去判断能否修改AQS中资源状态state的值,如果可以,那就直接给改线程加锁,如果失败,那就调用AQS中acquire方法进入阻塞队列。

而这里也正好体现了非公平锁的特性,即不按到达顺序分配锁,而是每个线程到达后都先执行以此加锁操作,如果加锁成功,那么该线程直接获取锁,不用考虑是否还有其它线程在等待,如果失败则将其加入阻塞队列。

NonfairSync中还有一个方法tryAcquire,这里我们要注意,这个tryAcquire其实是在重写AQS中的tryAcquire方法,要知道我们在使用acquire方法时,内部首先会调用tryAcquire方法,而这里的tryAcquire方法则是调用了其父类Sync中的nonfairTryAcquire,这个方法我们前面说过了,它用来实现锁的重入性。

到此非公平所得内容我们就讲解完成,下面我们来看一下公平锁的实现。

3.公平锁FairSync

公平锁FairSync的实现代码如下:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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方法里面直接调用AQS的acquire方法,而对于acquire中首先会调用的tryAcquire方法,FairSync也对其进行了重写,其实这里的代码与父类Sync中的nonfairTryAcquire十分相似,只是在资源对象的状态state值为0时,先会判断一下当前阻塞队例里面有没有在自己前面的线程,如果有则直接返回false,没有就直接加锁,剩下的内容就是与父类Sync中的nonfairTryAcquire一样实现锁的可重入性。

4.ReentrantLock的方法实现

在第一部分我们有说过,ReentrantLock实现了Lock接口方法,现在我们详细的讲解了ReentrantLock中重要的Sync内部类及其公平锁与非公平锁实现,接下来我们就可以详细的看一看ReentrantLock方法是如何实现Lock接口中的方法的:

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

    
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

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

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

    
    public Condition newCondition() {
        return sync.newCondition();
    }

其实很明显就能看出来,ReentrantLock对各方法的实现都是基于Sync实例化的sync对象,这里不在赘述。而刚才提到的Sync中的nonfairTryAcquire不写在具体子类而是实现类的原因也出来了,因为ReentrantLock默认是非公平锁的,方便tryLock方法调用。


版权声明:本文为qq_40076948原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>