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

1、ReentrantLock介绍

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

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

一般状况下建议将释放锁的操做放置在finally{}语句块中,以下面代码:并发

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

2、源码分析

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

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 {}
复制代码

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

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

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

public final void acquire(int arg) {
    // 一、首先尝试获取锁,若是获取失败,那么就假如等待队列中
    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();
    // 一、若是锁尚未被别人获取,及同步状态为0
    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;
}
复制代码

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

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

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

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;
}
复制代码

忽然,小编发现,这个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;
        // 一、自旋,其余参与同一个锁竞争的线程也在同时不断自旋
        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 {
        // 因为中断或者超时,必须将状态改为cancel
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

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

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

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

接着深刻release()方法,源码以下:

public final boolean release(int arg) {
    // 一、尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 二、唤醒那些因为中断或其它状况致使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;
}
复制代码

3、ReentrantLock与synchronized的区别

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

  • 一、如等待可中断

    持有锁的线程若是长期不释放锁,正在等待的线程能够选择放弃等待。

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

    2.调用lockInterruptibly()方法,若是线程中断了,则结束获取锁操做;

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

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

有一点须要注意就是,释放锁的操做必定要在finally块执行,不然可能出现死锁等意外状况。

4、应用场景

参考文章:

ReentrantLock使用场景和实例

ReentrantLock使用场景以及注意事项

相关文章
相关标签/搜索