JUC源码分析-JUC锁(一):ReetrantLock

1. 概述

ReentrantLock是一个可重入的互斥锁,也被称为“独占锁”。在上一篇讲解AQS的时候已经提到,“独占锁”在同一个时间点只能被一个线程持有;而可重入的意思是,ReentrantLock可以被单个线程多次获取。
ReentrantLock又分为“公平锁(fair lock)”和“非公平锁(non-fair lock)”。它们的区别体现在获取锁的机制上:在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”机制下,如果锁是可获取状态,不管自己是不是在队列的head节点都会去尝试获取锁。

2. 数据结构和核心参数

ReetrantLock继承关系

可以看到ReetrantLock继承自AQS,并实现了Lock接口。Lock源码如下:

public interface Lock {
    //获取锁,如果锁不可用则线程一直等待
    void lock();
    //获取锁,响应中断,如果锁不可用则线程一直等待
    void lockInterruptibly() throws InterruptedException;
    //获取锁,获取失败直接返回
    boolean tryLock();
    //获取锁,等待给定时间后如果获取失败直接返回
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放锁
    void unlock();
    //创建一个新的等待条件
    Condition newCondition();
}

Lock提供的获取锁方法中,有lock()lockInterruptibly()tryLock()tryLock(long time, TimeUnit unit)四种方式,他们的区别如下:

  • lock() 获取失败后,线程进入等待队列自旋或休眠,直到锁可用,并且忽略中断的影响
  • lockInterruptibly() 线程进入等待队列park后,如果线程被中断,则直接响应中断(抛出InterruptedException
  • tryLock() 获取锁失败后直接返回,不进入等待队列
  • tryLock(long time, TimeUnit unit) 获取锁失败等待给定的时间后返回获取结果

ReetrantLock通过AQS实现了自己的同步器Sync,分为公平锁FairSync和非公平锁NonfairSync。在构造时,通过所传参数boolean fair来确定使用那种类型的锁。

本篇会以对比的方式分析两种锁的源码实现方式。

3. 源码解析

3.1 lock()

lock()方法用于获取锁,两种类型的锁源码实现如下:

//获取锁,一直等待锁可用
public void lock() {
    sync.lock();
}

//公平锁获取
final void lock() {
    acquire(1);
}

//非公平锁获取
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

说明:公平锁的lock方法调用了AQS的acquire(1);而非公平锁则直接通过CAS修改state值来获取锁,当获取失败时才会调用acquire(1)来获取锁。
关于acquire()方法,在上篇介绍AQS的时候已经讲过,印象不深的同学可以翻回去看一下,这里主要来看一下tryAcquire在ReetrantLock中的实现。

公平锁tryAcquire:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//获取锁状态state
    if (c == 0) {
        if (!hasQueuedPredecessors() && //判断当前线程是否还有前节点
            compareAndSetState(0, acquires)) {//CAS修改state
            //获取锁成功,设置锁的持有线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//当前线程已经持有锁
        int nextc = c + acquires;//重入
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//更新state状态
        return true;
    }
    return false;
}

说明:公平锁模式下的tryAcquire,执行流程如下:

  1. 如果当前锁状态state为0,说明锁处于闲置状态可以被获取,首先调用hasQueuedPredecessors方法判断当前线程是否还有前节点(prev node)在等待获取锁。如果有,则直接返回false;如果没有,通过调用compareAndSetState(CAS)修改state值来标记自己已经拿到锁,CAS执行成功后调用setExclusiveOwnerThread设置锁的持有者为当前线程。程序执行到现在说明锁获取成功,返回true;
  2. 如果当前锁状态state不为0,但当前线程已经持有锁(current == getExclusiveOwnerThread()),由于锁是可重入(多次获取)的,则更新重入后的锁状态state += acquires 。锁获取成功返回true。

非公平锁tryAcquire

//非公平锁获取
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {//CAS修改state
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;//计算重入后的state
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

说明:通过对比公平锁和非公平锁tryAcquire的代码可以看到,非公平锁的获取略去了!hasQueuedPredecessors()这一操作,也就是说它不会判断当前线程是否还有前节点(prev node)在等待获取锁,而是直接去进行锁获取操作。

3.2 unlock()

//释放锁
public void unlock() {
    sync.release(1);
}

说明:关于release()方法,在上篇介绍AQS的时候已经讲过,印象不深的同学可以翻回去看一下,这里主要来看一下tryRelease在ReetrantLock中的实现:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//计算释放后的state值
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;//锁全部释放,可以唤醒下一个等待线程
        setExclusiveOwnerThread(null);//设置锁持有线程为null
    }
    setState(c);
    return free;
}

说明:tryRelease用于释放给定量的资源。在ReetrantLock中每次释放量为1,也就是说,在可重入锁中,获取锁的次数必须要等于释放锁的次数,这样才算是真正释放了锁。在锁全部释放后(state==0)才可以唤醒下一个等待线程。

3.3 等待条件Condition

在上篇介绍AQS中提到过,在AQS中不光有等待队列,还有一个条件队列,这个条件队列就是我们接下来要讲的Condition。
Condition的作用是对锁进行更精确的控制。Condition中的await()、signal()、signalAll()方法相当于Object的wait()、notify()、notifyAll()方法。不同的是,Object中的wait()、notify()、notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与Lock捆绑使用的。

Condition函数列表

//使当前线程在被唤醒或被中断之前一直处于等待状态。
void await()

//使当前线程在被唤醒、被中断或到达指定等待时间之前一直处于等待状态。
boolean await(long time, TimeUnit unit)

//使当前线程在被唤醒、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout)

//使当前线程在被唤醒之前一直处于等待状态。
void awaitUninterruptibly()

//使当前线程在被唤醒、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline)

//唤醒一个等待线程。
void signal()

//唤醒所有等待线程。
void signalAll()

下面我们来看一下Condition在AQS中的实现

3.3.1 await()

//使当前线程在被唤醒或被中断之前一直处于等待状态。
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();//添加并返回一个新的条件节点
    int savedState = fullyRelease(node);//释放全部资源
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //当前线程不在等待队列,park阻塞
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            //线程被中断,跳出循环
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();//解除条件队列中已经取消的等待节点的链接
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);//等待结束后处理中断
}

说明: await()方法相当于Object的wait()。把当前线程添加到条件队列中调用LockSupport.park()阻塞,直到被唤醒或中断。函数流程如下:

  1. 首先判断线程是否被中断,如果是,直接抛出InterruptedException,否则进入下一步;
  2. 添加当前线程到条件队列中,然后释放全部资源/锁;
  3. 如果当前节点不在等待队列中,调用LockSupport.park()阻塞当前线程,直到被unpark或被中断。这里先简单说一下signal方法,在线程接收到signal信号后,unpark当前线程,并把当前线程转移到等待队列中(sync queue)。所以,在当前方法中,如果线程被解除阻塞(unpark),也就是说当前线程被转移到等待队列中,就会跳出while循环,进入下一步;
  4. 线程进入等待队列后,调用acquireQueued方法获取锁;
  5. 调用unlinkCancelledWaiters方法检查条件队列中已经取消的节点,并解除它们的链接(这些取消的节点在随后的垃圾收集中被回收掉);
  6. 逻辑处理结束,最后处理中断(抛出InterruptedException或把忽略的中断补上)。

3.3.2 signal()

//唤醒线程
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);//唤醒条件队列的首节点线程
}

//从条件队列中移除给定节点,并把它转移到等待队列
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null; //解除首节点链接
    } while (!transferForSignal(first) && //接收到signal信号后,把节点转入等待队列
             (first = firstWaiter) != null);
}

//接收到signal信号后,把节点转入等待队列
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        //CAS修改状态失败,说明节点被取消,直接返回false
        return false;

    Node p = enq(node);//添加节点到等待队列,并返回节点的前继节点(prev)
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //如果前节点被取消,说明当前为最后一个等待线程,unpark唤醒当前线程
        LockSupport.unpark(node.thread);
    return true;
}

说明:signal方法用于发送唤醒信号。在不考虑线程争用的情况下,执行流程如下:

  1. 获取条件队列的首节点,解除首节点的链接(first.nextWaiter = null;);
  2. 调用transferForSignal把条件队列的首节点转移到等待队列的尾部。在transferForSignal中,转移节点后,转移的节点没有前继节点,说明当前最后一个等待线程,直接调用unpark()唤醒当前线程。

Condition的其他例如awaitNanos(long nanosTimeout)、signalAll()等方法这里这里就不多赘述了,执行流程都差不多,同学们可以参考上述分析阅读。

synchronized和ReentrantLock的选择

ReentrantLock在加锁和内存上提供的语义与内置锁synchronized相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁从性能方面来说,在JDK5的早期版本中,ReentrantLock的性能远远好于synchronized,但是从JDK6开始,JDK在synchronized上做了大量优化,使得两者的性能差距不大。synchronized的优点就是简洁。 所以说,两者之间的选择还是要看具体的需求,ReentrantLock可以作为一种高级工具,当需要一些高级功能时可以使用它。



作者:泰迪的bagwell
链接:https://www.jianshu.com/p/38fe92bcca7e
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值