java源码 - ReentrantLock图解加锁过程

开篇

用图形化的方式加深加锁和解锁过程的解释性。

java源码 - ReentrantLock
java源码 - ReentrantLock之FairSync
java源码 - ReentrantLock之NonfairSync
java源码 - ReentrantLock图解加锁过程

加锁流程

  • 1、首先我们分析非公平锁的的请求过程。我们假设在这个时候,还没有任务线程获取锁,这个时候,第一个线程过来了(我们使用的是非公平锁),那么第一个线程thread1会去获取锁.
    这时它会调用下面的方法,通过CAS的操作,将当前AQS的state由0变成1,证明当前thread1已经获取到锁,并且将AQS的exclusiveOwnerThread设置成thread1,证明当前持有锁的线程是thread1。:
final void lock() {
  if (compareAndSetState(0, 1))
      setExclusiveOwnerThread(Thread.currentThread());
  else
      acquire(1);
}


  • 2、此时来了第二个线程thread2,并且我们假设thread1还没有释放锁,因为我们使用的是非公平锁,那么thread2首先会进行抢占式的去获取锁,调用NonFairSync.lock方法获取锁。
    NonFairSync.lock方法的第一个分支是通过CAS操作获取锁,很明显,这一步肯定会失败,因为此时thread1还没有释放锁。那么thread2将会走NonFairSync.lock方法的第二个分支,进行acquire(1)操作。
    acquire(1)其实是AQS的方法,acquire(1)方法内部首先调用tryAcquire方法,ReentrantLock.NonFairLock重写了tryAcquire方法,并且ReentrantLock.NonFairLock的tryAcquire方法又调用了ReentrantLock.Sync的nonfairTryAcquire方法.
    nonfairTryAcquire方法如下:
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;
      }

这个方法的执行逻辑如下:

  • 1. 获取当前将要去获取锁的线程,在此时的情况下,也就是我们的thread2线程。

  • 2. 获取当前AQS的state的值。如果此时state的值是0,那么我们就通过CAS操作获取锁,然后设置AQS的exclusiveOwnerThread为thread2。很明显,在当前的这个执行情况下,state的值是1不是0,因为我们的thread1还没有释放锁。

  • 3. 如果当前将要去获取锁的线程等于此时AQS的exclusiveOwnerThread的线程,则此时将state的值加1,很明显这是重入锁的实现方式。在此时的运行状态下,将要去获取锁的线程不是thread1,也就是说这一步不成立。

  • 4. 以上操作都不成立的话,我们直接返回false。

既然返回了false,那么之后就会调用addWaiter方法,这个方法负责把当前无法获取锁的线程包装为一个Node添加到队尾。通过下面的代码片段我们就知道调用逻辑:

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

我们进入到addWaiter方法内部去看:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

很明显在addWaiter内部:

第一步:将当前将要去获取锁的线程也就是thread2和独占模式封装为一个node对象。并且我们也知道在当前的执行环境下,线程阻塞队列是空的,因为thread1获取了锁,thread2也是刚刚来请求锁,所以线程阻塞队列里面是空的。很明显,这个时候队列的尾部tail节点也是null,那么将直接进入到enq方法。

第二步:我们首先看下enq方法的内部实现。首先内部是一个自悬循环。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }



第一次循环:
t为null,随后我们new出了一个空的node节点,并且通过CAS操作设置了线程的阻塞队列的head节点就是我们刚才new出来的那个空的node节点,其实这是一个“假节点”,那么什么是“假节点”呢?那就是节点中不包含线程。设置完head节点后,同时又将head节点赋值给尾部tail节点,到此第一次循环结束。此时的节点就是如下:



第二次循环:
现在判断尾部tail已经不是null了,那么就走第二个分支了。将尾部tail节点赋值给我们传递进来的节点Node的前驱节点,此时的结构如下:



然后再通过CAS的操作,将我们传递进来的节点node设置成尾部tail节点,并且将我们的node节点赋值给原来的老的那个尾部节点的后继节点,此时的结构如下:

这个时候代码中使用了return关键字,也就是证明我们经过了2次循环跳出了这个自悬循环体系。

按照代码的执行流程,接下来将会调用acquireQueued方法,主要是判断当前节点的前驱节点是不是head节点,如果是的话,就再去尝试获取锁,如果不是,就挂起当前线程。这里可能有人疑问了,为什么判断当前节点的前驱节点是head节点的话就去尝试获取锁呢?因为我们知道head节点是一个假节点,如果当前的节点的前驱节点是头节点即是假节点的话,那么这个假节点的后继节点就有可能有获取锁的机会,所以我们需要去尝试。

现在我们看下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 {
            if (failed)
                cancelAcquire(node);
        }
    }



第一次循环:获取我们传入node的前驱节点,判断是否是head节点,现在我们的状态是:

很明显满足当前node节点的前驱节点是head节点,那么现在我们就要去调用tryAcquire方法,也就是NonfairSync类的tryAcquire方法,而这个方法又调用了ReentrantLock.Sync.nonfairTryAcquire方法。

很明显此时thread2获取锁是失败的,直接返回false。按照调用流程,现在进入了当前节点的前驱节点的shouldParkAfterFailedAcquire方法,检查当前节点的前驱节点的waitstatus。shouldParkAfterFailedAcquire方法内部如下:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • 1. 如果前驱节点的waitStatus为-1,也就是SIGNAL,就返回true。
  • 2. 如果当前节点的前驱节点的waitstatus大于0,也就是说被CANCEL掉了,这个时候我们会除掉这个节点。
  • 3. 如果都不是以上的情况,就通过CAS操作将这个前驱节点设置成SIGHNAL。

很明显,我们在这里的情况是第3种情况,并且这个方法运行后返回false。此时的结构如下,主要是head节点的waitStatus由0变成了-1。

第二次循环:获取我们传入node的前驱节点,判断是否是head节点,现在我们的状态是:

很明显满足当前node节点的前驱节点是head节点,那么现在我们就要去调用tryAcquire方法,也就是NonfairSync类的tryAcquire方法,而这个方法又调用了ReentrantLock.Sync.nonfairTryAcquire方法。

很明显此时thread2获取锁是失败的,直接返回false。按照调用流程,现在进入了当前节点的前驱节点的shouldParkAfterFailedAcquire方法,检查当前节点的前驱节点的waitstatus。此时waitstatus为-1,这个方法返回true。

shouldParkAfterFailedAcquire返回true后,就会调用parkAndCheckInterrupt方法,直接将当前线程thread2阻塞。

仔细看这个方法acquireQueued方法,是无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,在这里,当然不会出现死循环。因为parkAndCheckInterrupt会把当前线程阻塞。分析到这里,我们的thread2线程已经被阻塞了,这个线程不会再继续执行下去了。


  • 3、假设现在我们的thread1还没有释放锁,而现在又来了一个线程thread3。

thread3首先调用lock方法获取锁,首先去抢占锁,因为我们知道thread1还没有释放锁,这个时候thread3肯定抢占失败,于是又调用了acquire方法,接着又失败。接着会去调用addWaiter方法,将当前线程thread3封装成node加入到线程阻塞队列的尾部。现在的结构如下:

addWaiter如下:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }



第一步:将当前将要去获取锁的线程也就是thread3和独占模式封装为一个node对象。并且我们也知道在当前的执行环境下,线程阻塞队列不是空的,因为thread2获取了锁,thread2已经加入了队列。
很明显,这个时候队列的尾部tail节点也不是null,那么将直接进入到if分支。将尾部tail节点赋值给我们传入的node节点的前驱节点。如下:



第二步:通过CAS将我们传递进来的node节点设置成tail节点,并且将新tail节点设置成老tail节点的后继节点。

到此,addWaiter方法执行完毕,接着执行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 {
            if (failed)
                cancelAcquire(node);
        }
    }



第一次循环:获取我们传入node的前驱节点,判断是否是head节点,现在我们的状态是:

我们传入node的前驱节点不是head节点,那么直接走第二个if分支,调用shouldParkAfterFailedAcquire方法。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • 1. 如果前驱节点的waitStatus为-1,也就是SIGNAL,就返回true。
  • 2. 如果当前节点的前驱节点的waitstatus大于0,也就是说被CANCEL掉了,这个时候我们会除掉这个节点。
  • 3. 如果都不是以上的情况,就通过CAS操作将这个前驱节点设置成SIGHNAL。

很明显,我们在这里的情况是第3种情况,并且这个方法运行后返回false。
此时的结构如下,主要是t节点的waitStatus由0变成了-1。




第二次循环:获取我们传入node的前驱节点,判断是否是head节点,现在我们的状态是:

很明显我们传入node的前驱节点不是head节点,那么直接进入shouldParkAfterFailedAcquire方法。

1. 如果前驱节点的waitStatus为-1,也就是SIGNAL,就返回true。
2. 如果当前节点的前驱节点的waitstatus大于0,也就是说被CANCEL掉了,这个时候我们会除掉这个节点。
3. 如果都不是以上的情况,就通过CAS操作将这个前驱节点设置成SIGHNAL。

很明显,我们在这里的情况是第1种情况,并且这个方法运行后返回true。
然后就会调用parkAndCheckInterrupt方法,直接将当前线程thread3阻塞。现在thread2和thread3都已经被阻塞。

释放锁的过程

现在thread1要开始释放锁了。调用unlock方法,unlock方法又调用了内部的release方法:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  • 获取当前AQS的state,并减去1,判断当前线程是否等于AQS的exclusiveOwnerThread,如果不是,就抛异常,这就保证了加锁和释放锁必须是同一个线程。
  • 如果(state-1)的结果不为0,说明锁被重入了,需要多次unlock。
    如果(state-1)等于0,我们就将AQS的ExclusiveOwnerThread设置为null。
  • 如果上述操作成功了,也就是tryRelase方法返回了true,那么就会判断当前队列中的head节点,当前结构如下:


如果head节点不为null,并且head节点的waitStatus不为0, 我们就调用unparkSuccessor方法去唤醒head节点的后继节点。

    private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            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节点的waitStatus,如果小于0,就通过CAS操作将head节点的waitStatus修改为0,现在是:



第二步:寻找head节点的下一个节点,如果这个节点的waitStatus小于0,就唤醒这个节点,否则遍历下去,找到第一个waitStatus<=0的节点,并唤醒。现在thread2线程被唤醒了,我们知道刚才thread2在acquireQueued被中断,现在继续执行,又进入了for循环,当前节点的前驱节点是head并且调用tryAquire方法获得锁并且成功。那么设置当前Node为head节点,将里面的thead和prev设置为null。

调用完毕后,acquireQueued返回false。并且现在thread2自由了。到此,已经全部分析完毕。

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