ReentrantLock源码解析,从创建到加锁对源码进行分析

127 阅读12分钟

ReentrantLock源码解析

概述

ReentrantLock,也就是可重入锁,其和synchronized功能差不多,都是一种互斥锁,只是实现不一样,ReentrantLock是一种乐观锁,并且底层使用CAS实现的。

ReentrantLock一般我们用的比较多,用法也比较简单,只需要我们new一个锁出来,调用lock()和unlock()方法就行了,但是一般我们不会用它的可重入这一特性,那么可重入是什么意思呢?为什么我们加锁多少次就必须解锁多少次呢?这些问题都会在下面源码解析进行解释。

观前提醒

本文会涉及到CAS和AQS的相关内容,如果对CAS和AQS不太了解的,可以先去学习了解,不了解的话可能会对源码中一些代码不太理解。

对于AQS,有两种模式:

独占模式,需要重写tryAcquire(arg) ,tryRelease(int arg)方法。

共享模式,需要重写tryAcquireShared(arg) ,tryReleaseShared(int arg)方法。

ReentrantLock是独占模式的,所以只实现了tryAcquire(arg) ,tryRelease(int arg)方法。但ReentrantLock给我们提供了公平锁和非公平锁的实现,所以有两套tryAcquire(arg) ,tryRelease(int arg)方法。

源码解析

首先我们来看类定义

public class ReentrantLock implements Lock, java.io.Serializable 

ReentrantLock实现了lock、Serializable,Serializable是一个序列化的接口,不多做解释,lock是一个接口,提供了一些基本的方法定义,比如lock(),unlock(),下面是UML图

image.png

然后就开始我们真正的源码分析了:

lock()

1、首先我们从创建ReentrantLock开始,当我们new ReentrantLock();时,会调用以下第一种构造方法:

/**
 * 创建ReentrantLock一个实例。 这相当于使用ReentrantLock(false) 。
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * 使用给定的公平策略创建ReentrantLock的实例。
 * 参数:
 * 公平 - 如果此锁应使用公平排序策略,则为true
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

上面有两个构造方法,我们一般都之用的第一种,可以看到,当我们不传入参数时,默认是用的ReentrantLock的非公平锁的实现,而如果我们想用公平锁的实现方式,就可以在new时传入一个true,就可以了。

这里在构造里面有一个sync,这个东西叫做同步器,特别重要,它是我们ReentrantLock中的内部类,也是我们AQS的实现类,很多常用的乐观锁都是用AQS实现的,例如CountdownLatch、CyclicBarrier等。

2、然后我们来看ReentrantLock加锁的操作,也就是lock()方法:

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

可以看到,lock方法调用了sync.lock(),通过我们之前创建ReentrantLock的时候初始化的sync来判断调用的方法是公平锁还是非公平锁中的lock方法。我们先来看默认的非公平锁:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

可以看到,这里进行了一次CAS操作,尝试将state从0变为1,这里先去尝试获取锁,就体现了非公平锁的机制。 然后是公平锁:

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

对比下就可以看到,两个锁加锁的实现除了非公平锁会先尝试获取锁之外一摸一样。

然后我们来看acquire(1);方法

了解AQS的朋友应该知道,state是AQS中的同步状态,如果锁被某个线程持有,则该状态为正值,如果是0则代表锁没有被占用,这里如果cas失败,代表该所已经被占用了,所以直接调用acquire(1);方法,然后我们来看看这个方法内的实现,这个方法是AQS实现的:

/**
     * 以独占模式获取,忽略中断。 通过至少调用一次tryAcquire ,成功返回。 否则线程会排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。 此方法可用于实现方法Lock.lock 。
     * 参数:
     * arg – 获取参数。 这个值被传递给tryAcquire但不会被解释并且可以代表任何你喜欢的东西。
     */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

可以看到这里调用了tryAcquire(arg),尝试去加锁,如果加锁失败则会调用addWaiter(Node.EXCLUSIVE)这句代码,将其加入到等待队列中,然后调用acquireQueued()方法:

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) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这个方法主要是循环判断自己是否是头节点后面的第一个节点,如果是的话就去尝试获取锁,如果获取失败就返回中断状态,这里就体现了用CAS+AQS+volatile实现乐观锁的方式,JUC中很多常用的锁基本上都是用这种方式实现的。

trylock()

/**
 * 仅当调用时其他线程未持有该锁时才获取该锁。
 * 如果其他线程没有持有锁,则获取该锁并立即返回true值,将锁持有计数设置为 1。 
 * 即使此锁已设置为使用公平排序策略,调用tryLock()将立即获取可用的锁,无论其他线程当前是否正在等待该锁。 
 * 这种“闯入”行为在某些情况下很有用,即使它破坏了公平。 
 * 如果你想尊重这个锁的公平性设置,那么使用tryLock(0, TimeUnit.SECONDS)这几乎是等效的(它也检测中断)。
 * 如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true 。
 * 如果锁被另一个线程持有,则此方法将立即返回false值。
 * 返回值:
 * 如果锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则为true ; 否则为false
 */
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

顾名思义:尝试获取锁,这里调用了sync.nonfairTryAcquire(1);方法,这个方法是sync自己实现的方法:

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

这里只能是使用非公平的方式实现,首先获取到当前同步状态c,如果等于0,代表没有线程持有锁,那么就利用cas去获取锁(也就是state状态交换为1),如果成功了那么就将当前线程设置为该锁的独占线程,返回true表示获取到锁了;

如果当前线程和锁的独占线程一样,代表是我这把锁重复进入了,那么就会将锁状态c加上acquires,也就是加1,然后判断加1后是否小于0,如果小于0就抛出异常,否则就设置好加1后的状态值然后返回true。

如果两个判断都没进,那么就返回false,代表未获取到锁。

还有一个是带参数的trylock():

/**
 * 如果在给定的等待时间内没有被另一个线程持有并且当前线程没有被中断,则获取锁。
 * 如果其他线程没有持有锁,则获取该锁并立即返回true值,将锁持有计数设置为 1。 如果此锁已设置为使用公平排序策略,则如果任何其他线程正在等待该锁,则不会获取可用锁。 这与tryLock()方法形成对比。 如果你想要一个允许插入公平锁的定时tryLock ,那么将定时和非定时形式组合在一起:
 *
 *  if (lock.tryLock() ||
 *      lock.tryLock(timeout, unit)) {
 *    ...
 *  }
 * 如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true 。
 * 如果锁被另一个线程持有,那么当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下三种情况之一:
 * 锁被当前线程获取; 或者
 * 其他一些线程中断当前线程; 或者
 * 经过指定的等待时间
 * 如果获取了锁,则返回值true并将锁保持计数设置为 1。
 * 如果当前线程:
 * 在进入此方法时设置其中断状态; 或者
 * 在获取锁时被中断,
 * 然后抛出InterruptedException并清除当前线程的中断状态。
 * 如果指定的等待时间过去,则返回值false 。 如果时间小于或等于零,则该方法根本不会等待。
 * 在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁,并优先报告等待时间的过去。
 * 参数:
 * timeout - 等待锁定的时间
 * unit – 超时参数的时间单位
 * 返回值:
 * 如果锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则为true ; 如果在获取锁之前等待时间已经过去,则为false
 * 顶:
 * InterruptedException – 如果当前线程被中断
 * NullPointerException – 如果时间单位为空
 */
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

这个方法就是不带参数的升级版本(个人理解),在注释中有这么一句话: 如果在给定的等待时间内没有被另一个线程持有并且当前线程没有被中断,则获取锁。

其实就是AQS给我们实现了一个定时的不断尝试获取锁的方法,如果在这段时间过后还没有获取到锁,那么就会返回false,或者在获取锁的过程中被中断了就会抛出InterruptedException,通过这个方法我们可以有效的防止死锁的产生,如果一个线程用lock加锁的话,如果没加上锁那么就会一直阻塞,这样就可能会产生死锁,用tryLock(long timeout, TimeUnit unit)这种方式去加锁的话,如果一段时间内没有加锁成功,那么就会放弃竞争锁,这样的话就能成功避免死锁问题。

unlock()释放锁

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

上面是释放锁的源码,调用了同步器中的release(1)方法,然后我们来看这个release(1)方法:这个方法是AQS给我们提供的

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可以看到这里调用了tryRelease(arg)方法,如果成功,那么就会去取到头节点,如果头节点不为空并且等待状态不为0,则代表后面还有节点在等待队列等待,那么就会调用unparkSuccessor(h);方法去唤醒下一个节点。

这里说一下waitStatus的含义:


/** 由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。 */
static final int CANCELLED =  1;
/** SIGNAL:此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点,
为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。*/
static final int SIGNAL    = -1;
/** 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),
直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。) */
static final int CONDITION = -2;
/** 传播:应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。*/
static final int PROPAGATE = -3;


/** waitStatus为0:以上数值均未按数字排列以简化使用。非负值表示节点不需要发出信号。
所以,大多数代码不需要检查特定的值,只需要检查符号。
对于正常同步节点,该字段初始化为0;对于条件节点,该字段初始化为条件。
它是使用CAS修改的(或者在可能的情况下,使用无条件的volatile写入)。*/

CANCELLED:值为1,在队列中的节点,由于超时或中断,所以置为取消状态,被取消节点的线程不会再阻塞。

SIGNAL:值为-1,当前节点在入队后、进入休眠状态前,应确保将其prev节点类型改为SIGNAL,以便后者取消或释放时将当前节点唤醒。

CONDITION:值为-2,该节点处于条件队列中,当其他线程调用了Condition的signal()方法后,节点转移到AQS的等待队列中,特别要注意的是,条件队列和AQS的等待队列并不是一回事。

PROPAGATE:值为-3。对于这个状态到底是做什么的,网上大多数博客,包括书籍都是简单提了下这是共享模式下专用的,和传播有关,但是没有更深的解释。无奈,菜的抠脚的我至今也没能领悟这个状态值的含义。

0:默认值。

接下来我们看一下tryRelease(arg)方法:

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

1、首先将释放锁后状态C= state - releases,也就是state - 1,然后判断当前线程是不是持有锁的线程,如果不是则抛出异常

2、然后判断释放后状态C是否等于0,如果等于0的话,就将free设置为true,然后将持有锁线程设置为空

3、接下来设置state为C,返回free(free:锁是否被彻底释放,也就是资源是否被锁定)

到这里基本上ReentrantLock就说的差不多了,如果有什么错误欢迎指正,麻烦大家点歌赞,这次一定