JAVA-Lock解析-四-ReentrantLock源码分析

ReentrantLock 顾名思义就是一把可重入锁,另外 ReentrantLock 还支持公不公平锁,默认是不公平锁。此外ReentrantLock 不涉及 AQS 中的共享锁,所以它还是一把互斥锁,所有的获取锁操作都互斥(除了锁重入),这个和 ReentrantReadWriteLock 有点区别,ReentrantReadWriteLock 读与读之间共享。它的基本功能和 synchronized 一样,但额外还提供一些别的功能,例如中断锁,超时锁。

继承

ReentrantLock 的锁操作基于 AQS,如果你非常了解 AQS 那这篇文章对你来说会非常轻松。不了解 AQS 的请看 上一篇文章 AQS。还有本文不会对 AQS 做分析,只会对 AQS 中的模板方法,例如 tryAcquire,tryRelease 进行分析。

AQS state 成员变量在 ReentrantLock 中的意义:0 表示锁空闲,> 0 表示当前线程重入锁的次数。这也是 ReentrantLock 的关键。

首先我们看下 ReentrantLock 的基本构造:

public class ReentrantLock implements Lock, java.io.Serializable {
    public ReentrantLock() {
        sync = new NonfairSync();//默认是非公平锁
    }

    //指定公平锁还是非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

另外 ReentrantLock 有三个静态内部类 Sync,NonfairSync,FairSync。

1. 获取锁

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

sync 由构造方法确定。可能是公平锁也可能是非公平锁。

1.1. NonfairSync#lock

非公平锁的情况:

final void lock() {
      if (compareAndSetState(0, 1))//CAS 修改 state 的值为1,如果修改成功说明目前锁空闲。
           //把锁的拥有者改为当前线程。
           //这里涉及到 AbstractOwnableSynchronizer 类,是 AQS 的父类,维护谁获取了当前锁
           setExclusiveOwnerThread(Thread.currentThread());
      else
           //失败说明当前锁存在竞争,且竞争失败
           acquire(1);//调用 AQS 的 acquire 方法
}
1.1.1. AQS#acquire
public final void acquire(int arg) {//在 ReentrantLock 里 arg 表示当前线程获取锁的次数增加一次
    if (!tryAcquire(arg) && //tryAcquire 失败则进入同步队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//详情请看上一篇文章 AQS 分析
       selfInterrupt();
}

tryAcquire 是尝试再次获取锁,如果成功则立刻返回成功,如果失败则把当前线程封装成 Node 并放入等待队列,并且阻塞当前线程,直到前面的 Node 唤醒自己(可能会 blocking and unblocking 多次)或者被其他线程中断。如果发现自己已中断,则自己再次中断自己。

就像我们在 AQS 分析时说的,tryAcquire 就是真正的获取锁操作。

1.1.2. NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
     return nonfairTryAcquire(acquires);
}

这里没有像公平锁一样把 tryAcquire 直接实现,而是使用 Sync#nonfairTryAcquire,因为 ReentrantLock#tryLock 也会使用 nonfairTryAcquire 方法(无论它是公不公平锁)。

1.1.3. Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();//获取当前线程
      int c = getState(); //获取锁的状态值
      if (c == 0) { //0 说明锁空闲
          if (compareAndSetState(0, acquires)) {//CAS 获取锁
              setExclusiveOwnerThread(current);//获取成功
              return true;
          }
      }
      //当前线程就是拥有锁的线程,则不需要获取锁,直接返回成功。锁的重入性也在这里体现
      else if (current == getExclusiveOwnerThread()) {
          int nextc = c + acquires;//锁的重入次数 + 1
          //重入次数 > 2147483647 (二进制 1111111111111111111111111111111)就溢出。
          //因为 + 1 之后符号位由 0 变成 1,变成了负数
          if (nextc < 0) // overflow 
              throw new Error("Maximum lock count exceeded");
        //设置 state 这里没有使用 CAS 操作,因为当前线程已获得锁,此操作在锁里
         setState(nextc);
         return true;
     }
     return false;
}

这里我有个疑惑,为什么重入次数 > 2147483647后不反回 false 进入同步队列呢?

1.2. FairSync#lock

公平锁的情况:首先要明白公平锁就是对于每个线程获取锁他都是公平的,至少对于 ReentrantLock 来说满足 FIFO (先进先得),对于读写锁来说会有点区别,这里不做过多的讨论。

final void lock() {
      acquire(1);//即 AQS 的 acquire 方法
}

明白了公平锁的含义,我想对于这里为什么没有像非公平锁一样没有首先获取一次锁操作很好理解。

这里我们主要看下获取锁操作 tryAcquire

1.2.1. FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {//因为是独占锁,c == 0,说明是锁空闲
        if (!hasQueuedPredecessors() && //查看队列中是否有 Node 存在,如果有就阻塞。
                                        //这也是公平锁的体现,和非公平锁唯一区别的地方
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //c != 0 且当前线程就是占用锁的线程,锁重入的体现
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        //重入次数 > 2147483647 (二进制 1111111111111111111111111111111)就溢出。
        //因为 + 1 之后符号位由 0 变成 1,变成了负数
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//设置 state
        return true;
    }
    return false;
}

可以看到,公平锁的获取/重入大体上和非公平锁是相同的,唯一不同的就是 hasQueuedPredecessors 方法,那我们就看下这个方法。

1.2.2. AQS#hasQueuedPredecessors

该方法主要作用就是:查询是否队列中有等待的 Node

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t && //头 != 尾,如果头等于尾说明没有等待线程
        ((s = h.next) == null || s.thread != Thread.currentThread());//头下一个是 null 或 头下一个线程不是当前线程
}

何时会出现 h != t && (s = h.next) == null 的情况?
答:首先 s == t,h != t 为 true 后此时 s 正好获取锁

获取锁就讲到这里,另外 tryLocktryLock(long timeout, TimeUnit unit) 这里不做详细分析,相信大家看下源码没问题。

2. 释放锁

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

这里调用 AQS 的 release 方法,很显然 ReentrantLock 的解锁需要一次一次的释放之前加的锁,无法一次性解锁所有的锁,因为这里使用了 1 作为参数。

2.1. AQS#release

这是释放独占锁的方法,在 AQS 已详细讲过。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {//模板方法,调用子类相应的方法
            Node h = head;
            //h == null 说明没有发生过竞争,自然也没有后继Node需要唤醒
            //如果 h != null && h.waitStatus == 0 说明链表没有后继 Node,也就是说它自己是最后一个 Node。
            //因为每次加入 Node 时会把前一个 Node 设为 -1,尾部 Node 没有后继 Node,自然 waitStatus 也不会被修改
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

如果 tryRelease 返回成功,说明锁已经释放,此时如果链表 != null 且 还有后继 Node 则唤醒后继 Node。至于如何唤醒,请查看 AQS 分析,这里我们主要看 tryRelease 方法时如何释放的。

2.1.1. Sync#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) {//如果是最后一次解锁,则返回 true
        free = true;
        setExclusiveOwnerThread(null);//清除当前拥有锁的线程
    }
    //修改state,因为还在锁里面,并且 state 是 volatile 修饰的,所以可以不使用 CAS
    setState(c);
    return free;
}

可以看到 tryRelease 的目的就是把 state 计数 -1,直到减到 0,锁才是真正释放。这也是独占锁的重入的体现。

3. 另外 ReentrantLock 支持 newCondition 方法

详情请看 Condition

总结

ReentrantLock 最重要的两个方法就是 tryAcquiretryRelease,理解了这两个方法你就理解了大部分。希望对大家有所帮助,如果有错误请大家指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容