用火车购票的方式打开 AQS同步器(一)

引言

AQS(AbstractQueuedSynchronizer,下文直接使用AQS的简称)是java JUC包下提供的,基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架,使用者可以继承它来实现阻塞锁或者其他功能。由于是一个框架,所以其概念会比较抽象,因此这里会使用乘火车车购票的例子来帮助理解。


AQS同步器结构-成员变量

AQS的三个重要的成员变量,其功能都是围绕这三个变量实现的。

  /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
  • State: 同步器的状态量。类似于火车对外出售的车票总数量。
  • head:等待队列的头节点。类似于车票售完后,第一个预约候补的人,可以理解为此时这人已经再候补了。
  • tail:等待队列的尾节点。类似于所有预约候补的最后一个人。


    image

AQS同步器结构-Node

然后来看看Node的结构是怎么样的?

// 状态:可能退出状态等
volatile int waitStatus;
// 前面的人
 volatile Node prev;
// 后面的人
 volatile Node next;
// 我自己
 volatile Thread thread;
/**节点模式:排他或者共享*/
 Node nextWaiter;

waitStatus: 等待状态。 AQS提供四种状态:

  1. CANCELLED:退出状态。类似购车票时,由于官方原因候补失败的人
  2. SIGNAL:唤醒状态。正在候补或者候补完成,此时后面排队的人可以尝试候补了。
  3. CONDITION: 条件状态。这个是指在指定条件下,该乘客才会去预约候补(比如有紧急事务,必须今天去某地)
  4. PROPAGATE:传播状态。对于排队的队列,可以共享候补的信息可以向后传播。


    image

AQS同步器排他逻辑实现

排他,是指资源被独占,此时其他线程无法获取此资源,只能等待到当前资源被释放(资源独享,比如买我了车票后,这辆火车就只有我能使用,其他人都不能使用),其原理主要是通过tryAcquire方法返回资源是否被占用,如果是就进入等待状态,如果否就执行当前任务。具体实现如下:

接下来来看看AQS提供的主要方法(排他和共享)
排他(资源独享,比如买我了车票后,这辆火车就只有我能使用,其他人都不能使用):

 获取车票,没有获取到则进入等待
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  // 尝试获取车票,由于是独占的,所以返回值直接使用true or false
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 没有买到车的人进行排队候补
            selfInterrupt(); // 是否自我中断(买车票的时候,总是选择可以退票或者不买票了吧)
    }
    addWaiter(Node.EXCLUSIVE): 以排他模式加入到排队当中;
尝试获取车票,否则等待
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); // 由于系统原因人员无法候补,标记为退出状态
        }
    }

是否需要进行阻塞
  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 前面的人已经是唤醒状态,正在等待有车票的时候
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;  // 自己可以挂起了,只要等待有车票的时候会通知自己
        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;
        } 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); // 将排在前面的人改为需要唤醒状态
                                                                                 // 并再给一次机会去获取车票
        }
        return false;  // 自己不需要挂起
    }

释放资源:

释放资源(其中有人退票了或者下车了,又有空余的车票了,就通知队列里的人可以尝试候补):
public final boolean release(int arg) {
        if (tryRelease(arg)) {  // 自己退票或下车,有空余的车票了
            Node h = head;  // 候补队列中第一人
            if (h != null && h.waitStatus != 0)   // 存在 并且状态已经不是0
                unparkSuccessor(h);  // 通知下一个人很快就可以候补了
            return true;
        }
        return false;
    }


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