文档章节

ReentrantLock之公平锁源码分析

o
 osc_gu9d45li
发布于 2019/04/19 15:14
字数 2381
阅读 7
收藏 0

精选30+云产品,助力企业轻松上云!>>>

  本文分析的ReentrantLock所对应的Java版本为JDK8。

  在阅读本文前,读者应该知道什么是CAS、自旋。

本文大纲

  1.ReentrantLock公平锁简介
  2.AQS
  3.lock方法
  4.unlock方法

1. ReentrantLock公平锁简介

  ReentrantLock是JUC(java.util.concurrent)包中Lock接口的一个实现类,它是基于AbstractQueuedSynchronizer(下文简称AQS)来实现锁的功能。ReentrantLock的内部类Sync继承了AbstractQueuedSynchronizer,Sync又有FairSync和NonFairSync两个子类。FairSync实现了公平锁相关的操作,NonFairSync实现了非公平锁相关的操作。它们之间的关系如下:

 

  公平锁的公平之处主要体现在,对于一个新来的线程,如果锁没有被占用,它会判断等待队列中是否还有其它的等待线程,如果有的话,就加入等待队列队尾,否则就去抢占锁。

  下面这段代码展示了公平锁的使用方法:

private final Lock lock = new ReentrantLock(true); // 参数true代表创建公平锁

public void method() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock();
     }
}

2. AQS

  下面简单介绍一下AQS中的Node内部类和几个重要的成员变量。

2.1 Node

  AQS中,维护了一个Node内部类,用于包装我们的线程。我们需要关注Node中的如下属性:

  •   pre:当前节点的前驱节点。
  •   next:当前节点的后继节点。
  •   thread:thread表示被包装的线程。
  •   waitStatus:waitStatus是一个int整型,可以被赋予如下几种值:
static final int CANCELLED =  1; // 线程被取消
static final int SIGNAL  = -1; // 后继节点中的线程需要被唤醒
static final int CONDITION = -2; // 暂不关注
static final int PROPAGATE = -3; // 暂不关注

  另外,当一个新的Node被创建时,waitStatus是0。

2.2 head

  head指向队列中的队首元素,可以理解为当前持有锁的线程。

2.3 tail

  tail指向队列中的队尾元素。

2.4 state

  state表示在ReentrantLock中可以理解为锁的状态,0表示当前锁没有被占用,大于0的数表示锁被当前线程重入的次数。例如,当state等于2时,表示当前线程在这把锁上进入了两次。

2.5 exclusiveOwnerThread

  表示当前占用锁的线程。

2.6 等待队列

  下图简单展示了AQS中的等待队列:

3. lock方法

  有了上面的AQS的基础知识后,我们就可以展开对ReentrantLock公平锁的分析了,先从lock方法入手。

  ReentrantLock中的lock方法很简单,只是调用了Sync类(本文研究公平锁,所以应该是FairSync类)中的lock方法:

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

  我们跟到FairSync的lock方法,代码也很简单,调用了AQS中的acquire方法:

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

  acquire方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 调用tryAcquire尝试去获取锁,如果获取成功,则方法结束
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取锁失败,执行acquireQueued方法,将把当前线程排入队尾
        selfInterrupt();
}

  tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取锁的状态
    if (c == 0) { // 如果状态是0,表示锁没有被占用
        if (!hasQueuedPredecessors() && // 判断是队列中是否有排队中的线程
            compareAndSetState(0, acquires)) { // 队列中没有排队的线程,则尝试用CAS去获取一下锁
            setExclusiveOwnerThread(current); // 获取锁成功,则将当前占有锁的线程设置为当前线程
            return true;
        }
    }
    // 锁被占用、队列中有排队的线程或者当前线程在获取锁的时候失败将执行下面的代码
    else if (current == getExclusiveOwnerThread()) { // 当前线程是否是占有锁的线程
        int nextc = c + acquires; // 是的话,表示当前线程是重入这把锁,将锁的状态进行加1
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded"); // 锁的重入次数超过int能够表示最大的值,抛出异常
        setState(nextc); // 设置锁的状态
        return true;
    }
    return false; // 没有获取到锁
}

  hasQueuedPredecessors方法:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t && // 队列中的队首和队尾元素不相同
        ((s = h.next) == null || s.thread != Thread.currentThread()); // 队列中的第二个元素不为null,且第二个元素中的线程不是当前线程。这里如果返回true,说明队列中至少存在tail、head两个节点,就会执行acquireQueued将当前线程加入队尾
}

  如果tryAcquire没有获取到锁,将执行:

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

  我们先分析addWaiter方法:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); // 将当前线程包装成Node,mode参数值为null,表示独占模式
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred; // 如果队列中的尾节点不为空,将当前node的前驱节点设置为之前队列中的tail
        if (compareAndSetTail(pred, node)) { // 用CAS把当前node设置为队尾元素
            pred.next = node; // 成功的话,则将之前队尾元素的后继节点设置为当前节点。如果这里不清楚的话,请结合前面讲等待队列的那张图进行理解。
            return node;
        }
    }
    enq(node); // 队尾节点为空,或者用CAS设置队尾元素失败,则用自旋的方式入队
    return node;
}

  enq方法:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            if (compareAndSetHead(new Node())) // 队尾元素为空,创建一个空的Node,并设置为队首
                tail = head; // 设置队首和队尾为同一个空Node,进入下一次循环
        } else {
            node.prev = t; // 如果队列中的尾节点不为空,将当前node的前驱节点设置为之前队列中的tail
            if (compareAndSetTail(t, node)) { // 用CAS把当前node设置为队尾元素
                t.next = node; // 成功的话,则将之前队尾元素的后继节点设置为当前节点
                return t;
            }
        }
    }
}

   下面这张图反应了上面enq方法的处理流程:

  经过上面的方法,当前node已经加入等待队列的队尾,接下来将执行acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 获取node的前驱节点
            if (p == head && tryAcquire(arg)) { // 如果node的前驱是head,它将去尝试获取锁(tryAcquire方法在前面已经分析过)
                setHead(node); // 获取成功,则将node设置为head
                p.next = null; // 将之前的head的后继节点置空
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 当前node的前驱不是head,将为当前node找到一个能够将其唤醒的前驱节点;或者当前node的前驱是head,但是获取锁失败
                parkAndCheckInterrupt()) // 将当前线程挂起
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

  shouldParkAfterFailedAcquire方法的作用就是找到一个能够唤醒当前node的节点:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 开始时是0
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true; // 前驱节点的状态是-1,会唤醒后继节点,可以将线程挂起
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev; // 前驱节点中的线程被取消,那就需要一直循环直到找到一个没有被设置为取消状态的前驱节点
        } while (pred.waitStatus > 0);
        pred.next = node; // 从后向前找,将第一个非取消状态的节点,设置这个节点的后继节点设置为当前node
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // waitStatus是0或者-3的时候,这时waitStatus都将被设置为-1
                                                        // 即后继节点需要前驱节点唤醒
    }
    return false; // 上层代码再进行一次循环,下次进入此方法时,将进入第一个if条件
}

  找到了合适的前驱节点,parkAndCheckInterrupt方法当前线程挂起:

private final boolean parkAndCheckInterrupt() { // 将线程挂起,等待前驱节点的唤醒
    LockSupport.park(this);
    return Thread.interrupted();
}

 4. unlock方法

  ReentrantLock的unlock方法调用AQS中的release方法:

public void unlock() {
    sync.release(1); // 调用AQS的release方法
}

  release方法:

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 尝试去释放锁
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 释放锁成功,head不为空,并且head的waitStatus不为0的情况下,将唤醒后继节点
        return true;
    }
    return false;
}

  tryRelease方法:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 将锁的状态减1
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException(); // 准备释放锁的线程不是持有锁的线程,抛出异常
    boolean free = false;
    if (c == 0) {
        free = true; // 锁的状态是0,说明不存在重入的情况了,可以直接释放了
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

  锁释放成功,将唤醒后继节点,unparkSuccessor方法:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus; // 注意,这个node是head节点
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); // 当前node的状态是小于0,将其状态设置为0

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next; // head节点的后继节点
    if (s == null || s.waitStatus > 0) {
        s = null; // 执行到这表示head的后继节点是1,处于取消的状态
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t; // 从等待队列的队尾向前找,找到倒序的最后一个处于非取消状态的节点
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 唤醒head后面的处于非取消状态的第一个(正序)节点
}

  到此,全文结束,大家看代码的时候结合图来理解会容易很多。

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
ReentrantLock源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8 LinkedList源码分析--jdk1.8 HashMap源码分析--jdk1.8 AQS源码分析--jdk1.8 ReentrantLock源码分析--jdk1.8 ReentrantLock概述   1. ReentrantLock是独占......

osc_in99ca4i
2019/08/15
21
0
【JDK】JDK源码分析-ReentrantLock

概述 在 JDK 1.5 以前,锁的实现只能用 synchronized 关键字;1.5 开始提供了 ReentrantLock,它是 API 层面的锁。先看下 ReentrantLock 的类签名以及如何使用: public class ReentrantLock...

osc_i6ddt53t
2019/08/06
15
0
ReentrantLock 源码分析 - 并发小知识

前言 接下来会出一系列文章来分析 的源码, 在此之前需要具备很多基础, 因为说到 A 知识点可能牵扯出 B 知识点, 又扯出 C 知识点, 一环扣一环, 本文先从最基础的开始了解。 正文 先来看一些并...

nimo10050
04/26
61
0
Java的重入锁ReentrantLock

简介 ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。 在jav...

Yeyes
2019/07/15
0
0
JDK源码分析-ReentrantLock

概述 在 JDK 1.5 以前,锁的实现只能用 synchronized 关键字;1.5 开始提供了 ReentrantLock,它是 API 层面的锁。先看下 ReentrantLock 的类签名以及如何使用: 典型用法: 该用法和使用 sy...

WriteOnRead
2019/06/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

聚焦餐饮行业,研究院昨发布数据显示

谈话,聚焦餐饮行业,研究院昨发布数据显示,今年上半年,全国餐饮行业招聘需求增长46.18%,平均月薪6387元.随着餐饮行业的快速发展,"如何留人"也成为餐饮企业的思考题. 记者了解到,中国饭店协会...

点击fojewio
26分钟前
20
0
3·15晚会曝光上海氪信、招财旺旺SDK包泄露隐私 后台上传交易验证码敏感信息

来源 | 央视 7月16日,央视3·15晚会曝光国美易卡、美的空调遥控器、姨妈日历、银码头等50多款软件中内嵌的SDK包读取、上传用户隐私问题。上海氪信信息技术有限公司、北京招财旺旺信息技术有...

镭射财经
36分钟前
9
0
名称=''的无效表单控件不可聚焦 - An invalid form control with name='' is not focusable

问题: I have an acute problem on my website. 我的网站上有一个严重的问题。 In Google Chrome some customers are not able to proceed to my payment page. 在Google Chrome浏览器中,某......

技术盛宴
37分钟前
14
0
Hacker News 简讯 2020-07-17

更新时间: 2020-07-17 00:00 D 2.093.0 - (dlang.org) D 2.093.0 得分:32 | 评论:2 Let’s avoid talk of ‘chemical imbalance’: it’s people in distress - (psyche.co) 让我们避免谈论“......

FalconChen
42分钟前
80
0
【LeetCode】 59 在排序数组中查找元素的第一个和最后一个位置

题目: 解题思路: 二分法 https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/zai-pai-xu-shu-zu-zhong-cha-zhao-yuan-su-de-di-yi-/ 代......

JaneRoad
昨天
17
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部