AQS队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

顾名思义,AQS不是一个实际的类,它是一个抽象类,需要继承该类并且实现抽象方法来管理同步状态。而管理同步状态时不免要对同步状态进行更改,这就需要使用到以下三个方法:

  • getState() 获取当前同步状态。
  • setState(int newState) 设置当前同步状态。
  • compareAndSetState(int expect,int update) 使用CAS设置当前状态,该方法能够保证状态 设置的原子性。

子类推荐被定义为自定义同步组件的静态内部 类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步 器实现锁的语义。可以这样理解二者之间的关系:

  • 锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
  • 同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

锁和同 步器很好地隔离了使用者和实现者所需关注的领域。

同步器接口实例:

同步器可重写的方法:

方法名称 方法描述
boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前
状态并判断同步状态是否符合预期,然后再进行
CAS设置同步状态
boolean tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的
线程将有机会获取同步状态
int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0的值,
表示获取成功,反之,获取失败
boolean tryReleaseShared(int arg) 共享式释放同步状态
boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,
一般该方法表示是否被当前线程所独占

实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下表所示

方法名称 方法描述
void acquire(int arg) 独占式获取同步状态,如果当前线程获取同
步状态成功,则由该方法返回 否则,将会进
入同步队列等待,该方法将会调用重写的
tryAcquire(int arg)方法
void acquireInterruptibly(int arg) 与 acquire(int arg)相同,但是该方法响应中断
,当前线程未获取到 同步状态而进入同步
队列中,如果当前线程被中断,则该方法会
抛出Interruptedexception并返回
tryAcquireNanos(int arg, long nanosTimeout) 在 acquireinterruptibly(int arg)基础上增加
了超时限制,如果当前线程在超时时间内没
有获取到同步状态,那么将会返回 false,
如果获取到了返回true
void acquireShared(int arg) 共享式的获取同步状态,如果当前线程未获
取到同步状态,将会进入同步队列等待,与
独占式获取的主要区别是在同一时刻可以
有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg) 与 acquireShared(int arg)相同,该方法响应中断
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) 在 acquireSharedinterruptibly(int arg)
基础上增加了超时限制
boolean release(int arg) 独占式的释放同步状态,该方法会在释放同
步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg) 共享式的释放同步状态
Collection<Thread> getQueuedThreads() 获取等待在同步队列上的线程集合

同步器提供的模板方法基本上分为3类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放
  • 同步状态和查询同步队列中的等待线程情况

通过上述模板方法,我们可以将锁大致分为独占式锁与共享式锁。

独占式锁

顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能 处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。下面我们通过一段代码来演示一下:

public class Mutex implements Lock {
    // 静态内部类,自定义同步器 
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态 
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 当状态为0的时候获取锁 
        @Override
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0 
        @Override
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 返回一个Condition,每个condition都包含了一个condition队列 
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 仅需要将操作代理到Sync上即可 
    private final Sync sync = new Sync();

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

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

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

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

共享式锁

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况如下图所示:


共享式与独占式访问资源的对比

上图中左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。

下面我们通过一段代码来实际演示共享式同步锁的使用:

public class TwinsLock implements Lock {
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero.");
            }
            setState(count);
        }

        public int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }

        public boolean tryReleaseShared(int returnCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }
    }

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

    public void unlock() {
        sync.releaseShared(1);
    }
    // 其他接口方法略
}

在上述示例中,TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock()方法获取锁,随后调用unlock()方法释放锁,而同一时刻只能有两个线程同时获取到锁。TwinsLock同时包含了一个自定义同步器Sync,而该同步器面向线程访问和同步状态控制。以共享式获取同步状态为例:同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正确设置,当tryAcquireShared(int reduceCount)方法返回值大于等于0时,当前线程才获取同步状态,对于上层的TwinsLock而言,则表示当前线程获得了锁。

下面编写一个测试来验证TwinsLock是否能按照预期工作。在测试用例中,定义了工作者线程Worker,该线程在执行过程中获取锁,当获取锁之后使当前线程睡眠0.5秒(并不释放锁),随后打印当前线程名称,最后再次睡眠1秒并释放锁,测试用例如下:

    @Test
    public void twinsLockTest() throws InterruptedException {
        final Lock lock = new TwinsLock();
        class Worker extends Thread {
            public void run() {
                while (true) {
//                    System.out.println("before - " + Thread.currentThread().getName());
                    lock.lock();
                    try {
                        Thread.sleep(1000);
                        System.err.println("got - " + Thread.currentThread().getName());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    } finally {
                        lock.unlock();
                    }
//                    System.out.println("after - " + Thread.currentThread().getName());
                }
            }
        }
        // 启动10个线程
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.start();
        }
        // 每隔1秒换行
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println();
        }
    }

运行该测试用例,可以看到线程名称成对输出,也就是在同一时刻只有两个线程能够获 取到锁,这表明TwinsLock可以按照预期正确工作。

启动运行之后会看到基本获取到共享资源的线程都是固定的那两个,这是因为在没获取到共享资源时该线程会被加入AQS的等待队列,在释放资源之后再被唤醒重新竞争资源,而由于之前等待的线程需要被唤醒才能重新竞争共享资源,而释放资源的线程由于不需要唤醒,所以大概率会比其他线程优先再次获取到锁,可以将before和after打印语句取消注释在运行,这样可以解决这个问题。

对于AQS队列同步器的实现分析我们放到下一节去分析,本节主要简单的讲解和举例AQS对于共享式和独占式锁的实现

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