谈谈JUC----------ReentrantLock源码分析

一、ReentrantLock介绍

ReentrantLock是一个和synchronized拥有相同语义但同时扩展了额外功能的可重入互斥锁实现。ReentrantLock将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock() 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用isHeldByCurrentThread()getHoldCount() 方法来检查此情况是否发生。

ReentrantLock有公平锁和非公平锁两种,通过构造器传入一个boolean fair参数指定,该参数是可选的,默认为false,也就是说,默认是非公平锁实现。但请注意,这里所说的公平与非公平,只是说获取锁的时候是否顺序进行,并不保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可能会连续多次获得它。公平锁即在线程相互争用锁的情况下,它会更偏向于让等待时间最长的那个线程获得锁(队头),但相比较非公平锁,使用多个线程访问公平锁的程序吞吐量比较低,或者明显更慢。

通常情况下建议将释放锁的操作放置在finally{}语句块中,如下面代码:

public void m() {
   lock.lock();  // block until condition holds
   try {
   // ... method body
   } finally {
        lock.unlock()
  }
 }
复制代码

二、源码分析

查看ReentrantLock源码,发现该类只有一个Sync的成员变量:

private final Sync sync;
复制代码

Sync为继承自AQS的一个同步器实现,其内部同时提供了一个lock()抽象方法供子类实现,完成获取锁操作。SyncFairSync,NonfairSync两个子类,分别提供公平锁和非公平锁的相关操作。

abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
复制代码

下面分别从公平和非公平两种实现探讨其获取锁和释放锁的操作:

1、公平锁如何获取锁?
final void lock() {
    acquire(1);
}
复制代码

可以看出其直接调用AQSacquire(int)方法获取锁,接下来看下acquire()实现:

public final void acquire(int arg) {
    // 1、首先尝试获取锁,如果获取失败,那么就假如等待队列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

接着查看FairSync中提供的tryAcquire(int)方法:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 1、如果锁还没有被别人获取,及同步状态为0
    if (c == 0) {
        // 2、判断是否已经有其他线程在等待获取该锁,如果没有
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3、判断持有锁的线程是否是自己
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
复制代码

从上面代码第2步可以看出,公平锁会偏向于给队列中等待时间最长的线程优先获得锁,如果此时没有其他线程在等待,则执行CAS争抢锁资源。第3步如果该锁已经被持有,则判断持有该锁的线程是否当前线程本身,如果是,那么同步状态state递增(加锁次数)。可以看出,公平锁按等待队列顺序分配锁资源,高并发下性能,效率不高。

2、非公平锁如何获取锁?
final void lock() {
    // 1、直接参与竞争,如果该锁已被其它线程持有,那么就执行else
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
复制代码

接着继续查看Sync提供的nonfairTryAcquire()方法,源码如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 1、无需额外判断是否有其它线程处于等待状态
        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;
}
复制代码

突然,小编发现,这个nonfairTryAcquire()方法怎么和上面讲到的tryAcquire(int)有点相似,原来tryAcquire(int)nonfairTryAcquire()多了一步判断同步队列中是否有其它线程正在等待,因此,这也是公平和非公平的区别所在,如果nonfairTryAcquire()方法也没能获取锁,那么将被挂到同步等待队列中。

那么,我们可能会问,既然会被挂到同步队列中,那当前被挂起的这个线程后续是怎么被唤醒抢夺锁资源的呢?还是按顺序出队列吗?如果还是按顺序出队列,是不是就和公平锁一样了呢?其实很简单,还是回到下面这个方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

首先执行tryAcquire(arg)获取锁失败之后,会执行addWaiter()将自己封装成Node节点入队,接着调用acquireQueued()方法,答案就在acquireQueued()方法中,源码如下:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 1、自旋,其他参与同一个锁竞争的线程也在同时不断自旋
        for (;;) {
            final Node p = node.predecessor();
            // 2、不断尝试获取锁资源
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 3、处理中断情况
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 由于中断或者超时,必须将状态改成cancel
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

原来非公平锁参与锁竞争失败、被挂到等待队列之后,会cas+自旋直到获取锁成功,确实很不公平,哈哈。

3、锁的释放
public void unlock() {
    sync.release(1);
}
复制代码

ReentrantLock中释放锁统一为unlock()方法,从上面源码可以看出,每调用一次unlock()方法,同步状态就会减一,也就是说,lock()多少次,就要对应unlock()多少次。

接着深入release()方法,源码如下:

public final boolean release(int arg) {
    // 1、尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 2、唤醒那些由于中断或其它情况导致waitStatus不为0的节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
复制代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 如果线程不是当前线程持有,那么就报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 该锁已经没有线程持有了,同步状态为0了,那么其它线程就可以从cas+自旋中退出了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
复制代码

三、ReentrantLock与synchronized的区别

都说synchronized使用的是重量级锁,性能非常低下,但是新版本的jdk已经做了如偏向锁之类的优化,性能其实还可以。synchronized属于jvm层面的加锁机制,而Lock属于API层面上的加锁,那么它们到底有什么区别呢?

  • 1、如等待可中断

    持有锁的线程如果长期不释放锁,正在等待的线程可以选择放弃等待。

    1.设置超时方法tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;

    2.调用lockInterruptibly()方法,如果线程中断了,则结束获取锁操作;

  • 2、synchronized为非公平锁,ReentrantLock同时支持公平锁和非公平锁

  • 3、ReentrantLock可结合Condition条件进行使用,可分别对多种条件加锁,对线程的等待、唤醒操作更加详细和灵活,在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合

有一点需要注意就是,释放锁的操作一定要在finally块执行,否则可能出现死锁等意外情况。

四、应用场景

参考文章:

ReentrantLock使用场景和实例

ReentrantLock使用场景以及注意事项

猜你喜欢

转载自juejin.im/post/5e251664f265da3e4a583034