Java 并发 AQS 重入锁

实现重入锁ReentrantLock锁使用到的技术

  1. CAS 保证操作原子性
  2. AQS 带有头尾节点的队列 链表实现
Node {
//Node代表了等待的线程
  Node prev 前一个Node
  Node next 后面一个Node
}

AQS{
Node head 头节点
Node tail 尾结点
}
  1. 自旋操作

用到的一些方法的含义(公平锁实现情况)(代码为Open-JDK13中的)

  1. tryAcquire 本线程尝试获取锁,获取成功返回true
  2. hasQueuedPredecessors 判断本线程是否需要排队,需要排队返回true
  3. addWaiter 往队列中添加一个Node 即添加一个排队的线程

源码具体执行流程
步骤一:

上锁.png

步骤二:
获取锁.png

步骤三:
获取锁具体过程.png

这里开始麻烦起来了,第一步本线程第一次尝试获取锁,调用 tryAcquire, 如果获取成功,则上锁过程结束,如果获取失败就执行另外两步。
先看下tryAcquire干什么了(为公平锁实现的tryAcquire)

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            获取锁的状态
            int c = getState();
            如果状态为0,说明此时锁时空闲的
            但是注意可能有多个线程同时检测到锁空闲,那么就会有多个线程都进入下面的IF
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
         ... 省略一部分代码,先不讨论
            return false;
        }

如果是空闲状态,那么就要先看看是不是要排队(因为可能有多个线程同时,非公平锁就不用检测要不要排队)

    public final boolean hasQueuedPredecessors() {
        Node h, s;     h表示头结点,s表示后续节点
        如果h==null 那么就说明此此时队列都没创建 本线程是第一个线程,说明没有其他线程,就**不用排队**
        if ((h = head) != null) {
            s==null说明下一个没有 但也可能因为并发的原因 不能保证后面也是空的
            s.waitStatus>0 说明下一个被取消了,要看后面的是什么情况
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) {   从后往前找
                    找到一个不是null的也不是被取消的
                    也就是找到队列中第一个真正在等待的
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
            不用排队情况(人即线程)
                情况一:根本没有在等待,前面可能有人,也可能没有人
                情况二:有在等待的,但是等待的人是自己,前面肯定有人
            要排队的情况
                有在真正等待的,并且第一个真正在等待的不是自己
              s==null 就说明没有等待的   **不用排队** 队列已经初始化过了
              s!=null就说明有等待的,
                    等待的人不是自己    **要排队**
                    等待的人是自己   **不用排队** 这里会比较困惑,因为这个情形的出现要结合后面的**acquireQueued方法来看**
                  因为如果你是第一个等待者,那么acquireQueued会再调用一次tryAcquire,而tryAcquire会再调用一次hasQueuedPredecessors,
                  那么这种情形就会出现了即等待的人是自己
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
        return false;
    }

这个判断排队比较绕。。。
然后就是回到了之前的tryAcquire了,如果不需要排队,那么直接用CAS操作尝试修改锁的状态,如果修改成功那么获取锁就成功了。
如果不需要带队,但是获取锁失败了那就说明实际上还是需要排队的,因为这种情况就说明了存在多线程竞争
如果需要排队就要调用addWaiter往队列中添加等待者,即排队,这个方法内部就不展开了,就是入队,它用CAS操作保证了入队顺序是线程安全的。 注意排好队后,线程还没有阻塞。
排好队后队列中排队的人看看自己能不能获取锁,如果不能就阻塞线程,直到被唤醒

    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            死循环,直到获取锁为止,当然获取失败就会阻塞,直到被唤醒,然后再尝试获取 如此循环
            for (;;) {
                final Node p = node.predecessor();
                情形一 加入队列后如果自己是第一个等待的那么就在试着获取一次锁,
                        原因是再自己第一次尝试获取锁失败加入队列后的这段时间,拥有锁的那个线程可能释放了锁
                更后面的等待者是没有机会尝试获取锁的
                情形二阻塞后被唤醒
                说明自己是第一个等待的 再尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
               决定是否阻塞  根据它前面的人的状态
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();  阻塞,并且阻塞醒来也是从这里开始
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

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 {
            第一次执行 把前面的等待者状态设置好,因为它肯定已经阻塞等待
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

这里说的比较乱。。。
为什么要后面的人来设置前面的人的阻塞状态呢,因为一个线程真正阻塞了,那么它的阻塞状态才能设置,即先有阻塞,然后有阻塞状态,但是阻塞的线程就根本不能动了,自然也不能设置阻塞状态。
拿睡觉来打个比方,你睡着还是没睡着不能由你说了算,你不可能告诉别人你睡着了,只能由别人来看,只有别人才能看到你是否真正睡着了。
并且这段有解锁过程相关性挺高的,以后有时间再说了。


这篇文章主要是为了记录自己在学习AQS时候的一些感受,也方便以后自己复习。所以用到的语言也不够准确,过程不能保证正确。如果有同学看到这篇文章,一定要带着怀疑的心态去看。如果发现错误的地方希望能批评指出。

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

推荐阅读更多精彩内容