AQS简单介绍与使用

java 专栏收录该内容
38 篇文章 1 订阅

AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器。这个抽象类对于JUC并发包非常重要,JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。

AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列(CHL队列),还有一个STATE标志,并且通过CAS来改变它的值。

CLH队列:

  1. 链表结构,在头尾结点中,需要特别指出的是头结点是一个空对象结点,无任何意义,即傀儡结点;

  2. 每一个Node结点都维护了一个指向前驱的指针和指向后驱的指针,结点与结点之间相互关联构成链表;

  3. 入队在尾,出队在头,出队后需要激活该出队结点的后继结点,若后继结点为空或后继结点waitStatus>0,则从队尾向前遍历取waitStatus<0的触发阻塞唤醒;

  4. 队列中节点状态值(waitStatus,只能为以下值)

    //常量:表示节点的线程是已被取消的
        static final int CANCELLED =  1;
        //常量:表示当前节点的后继节点的线程需要被唤醒
        static final int SIGNAL    = -1;
        //常量:表示线程正在等待某个条件
        static final int CONDITION = -2;
        //常量:表示下一个共享模式的节点应该无条件的传播下去
        static final int PROPAGATE = -3;
    

线程获取或释放锁的本质是去修改AQS内部那个可以表征同步状态的变量的值。比如说,我们创建一个ReentrantLock的实例,此时该锁实例内部的状态的值为0,表征它还没有被任何线程所持有。当多个线程同时调用它的lock()方法获取锁时,它们的本质操作其实就是将该锁实例的同步状态变量的值由0修改为1,第1个抢到这个操作执行的线程就成功获取了锁,后续执行操作的线程就会看到状态变量的值已经为1了,即表明该锁已经被其他线程获取,它们抢占锁失败了。这些抢占锁失败的线程会被AQS放入到CHL队列里面去维护起来。

AQS是一个抽象类,当我们继承AQS去实现自己的同步器时,要做的仅仅是根据自己同步器需要满足的性质实现线程获取和释放资源的方式(修改同步状态变量的方式)即可,至于具体线程等待队列的维护(如获取资源失败入队、唤醒出队、以及线程在队列中行为的管理等),AQS在其顶层已经帮我们实现好了,AQS的这种设计使用的正是模板方法模式

AQS支持线程抢占两种锁——独占锁和共享锁:

  • 独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等,它又可分为:
    1. 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    2. 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • 共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等

AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。

通过AQS可以很方便的实现一个自定义的同步器,子类只需要通过重写以下方法来控制AQS内部的一个叫做state的同步变量:

//独占模式获取锁与释放锁
//返回值表示获取锁是否成功
protected boolean tryAcquire(int arg)
//返回值表示释放锁是否成功
protected boolean tryRelease(int arg)
//共享模式获取锁与释放锁
//返回值表示获得锁后还剩余的许可数量
protected int tryAcquireShared(int arg)
//返回值表示释放锁是否成功
protected boolean tryReleaseShared(int arg)

//调用该方法的线程是否持有独占锁,一般用到了condition的时候才需要实现此方法
 protected boolean isHeldExclusively()

这些方法是子类需要实现的,可以选择实现其中的一部分。根据实现方式的不同,可以分为两种:独占锁和共享锁。其中JUC中锁的分类为:

  • 独占锁:ReentrantLock、ReentrantReadWriteLock.WriteLock
  • 共享锁:ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore

其实现方式为:
独占锁需要实现的是 tryAcquire(int)、tryRelease(int)
共享锁需要实现的是 tryAcquireShared(int)、tryReleaseShared(int)

在重写这些方法时,如果想要使用state同步变量,必须使用AQS内部提供的以下方法来控制:

/** 返回同步状态的当前值(此操作具有volatile变量的读语义) **/
protected final int getState() {  //方法被final修饰,不允许被重写
        return state;
}
 /** 设置同步状态的值(此操作具有volatile变量的写语义) **/
protected final void setState(int newState) { //方法被final修饰,不允许被重写
        state = newState;
}
/**
 * 原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(此操作具有volatile变量的读写语义)
 * @return  成功返回true,失败返回false,意味着当操作进行时同步状态的当前值不是expect
**/
protected final boolean compareAndSetState(int expect, int update) { //方法被final修饰,不允许被重写
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS中还提供了一个内部类ConditionObject,它实现了Condition接口,可以用于await/signal。采用CLH队列的算法,唤醒当前线程的下一个节点对应的线程,而signalAll唤醒所有线程。

总的来说,AQS提供了三个功能:

  • 实现独占锁
  • 实现共享锁
  • 实现Condition模型

一个AQS的使用示例如下(这里用AQS实现一个简单的不可重入锁):

public class SimpleLock extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int unused) {
        //使用compareAndSetState控制AQS中的同步变量
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        //使用setState控制AQS中的同步变量
        setState(0);
        return true;
    }

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

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

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

    /**
     * 发现线程是顺序获得锁的
     * 因为AQS是基于CLH锁的一个变种实现的FIFO调度
     */
    public static void main(String[] args) throws InterruptedException {
        final SimpleLock lock = new SimpleLock();
        lock.lock();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    System.out.println(Thread.currentThread().getId() + " acquired the lock!");
                    lock.unlock();
                }
            }).start();
            // 简单的让线程按照for循环的顺序阻塞在lock上
            //目的是让线程顺序启动
            Thread.sleep(100);
        }

        System.out.println("main thread unlock!");
        lock.unlock();
    }
}

其实这个基本上就是ThreadPoolExecutor的内部类Worker对AQS的实现。

再看一个栗子:

public class MyLock implements Lock {

    private Sync sync = new Sync();

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

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

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

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

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

    @Override
    public void unlock() {
        sync.release(1);
    }
	
	
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg){
            //如果第一个线程进来,拿到锁,返回TRUE

            //如果第二个线程进来,返回FALSE,拿不到锁

            int state = getState();
            if(state == 0){
                if(compareAndSetState(0,arg)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }

            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            //锁的获取和释放需要11对应,那么调用这个方法的线程,一定是当前线程让。
            if(Thread.currentThread() != getExclusiveOwnerThread()){
                throw new RuntimeException();
            }
            int state = getState() - arg;
            setState(state);
            if(state == 0){
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }
        
        // 判断当前独占锁是否被持有
     	protected boolean isHeldExclusively() {
       		return getState() == 1;
     	}
     	
		// 提供条件队列功能
        Condition newCondition() {
            return new ConditionObject();
        }
    }
}

上面MyLock的大多数方法,其实我们根本不用操心,统一交给AQS的方法来帮我们完成就好。而我们真正要实现的就是tryAcquire和tryRelease 这2个操作。

首先我们来实现tryAcquire,本质就是如果第一个线程进来就拿到锁,后面进来的RETURN FALSE。然而之前我们声明了一个BOOL变量。这里我们可以直接AQS 的STATE变量。我们可以用初始值为0来表示没有线程拿到锁,一旦第一个线程进来了,就把它设置为非0.

我们再来思考释放,因为锁的获取和释放是一一对应的,我们首要判断锁住的线程是不是同一个,如果是,我们就把STATE - ARG。如果STATE回到0,则代表锁释放成功。

测试类Sequence来测试下我们写的锁了。可以发现输出的是50,锁是成功的。

public class Sequence {
    private Lock lock = new MyLock();
    private int value;

    public int getNext(){
        lock.lock();
        value++;
        lock.unlock();
        return value;
    }

    public static void main(String[] args) {
        Sequence s = new Sequence();
        for(int i = 0; i < 5; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0;i<10;i++){
                        System.out.println(s.getNext());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }).start();
        }
    }
}

然后去测试重入锁的DEMO,发现会卡主。我们要再加一些逻辑来实现重入锁。

protected boolean tryAcquire(int arg){
    //如果第一个线程进来,拿到锁,返回TRUE

    //如果第二个线程进来,返回FALSE,拿不到锁

    Thread t = Thread.currentThread();
    int state = getState();
    if(state == 0){
        if(compareAndSetState(0,arg)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
    }else if(getExclusiveOwnerThread() == t){
        //因为只有一个线程可以进这个IF,所以没有线程安全性问题
        setState(state+arg);
        return true;
    }

    return false;
}

测试可重入锁

public class Test {
	Lock lock = new MLock();
    public void a(){
        lock.lock();
        System.out.println("a");
        b();
        lock.unlock();
    }
    public void b(){
        lock.lock();
        System.out.println("b");
        lock.unlock();
    }

    public static void main(String[] args) {
        Test d = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                d.a();
            }
        }).start();
    }
}

最后来看看基于AQS实现的 CountDownLatch 源码, CountDownLatch 用同步状态持有当前计数,countDown方法调用 release从而导致计数器递减;当计数器为0时,解除所有线程的等待;await调用acquire,如果计数器为0,acquire 会立即返回,否则阻塞。通常用于某任务需要等待其他任务都完成后才能继续执行的情景。源码如下:

public class CountDownLatch {
    /**
     * 基于AQS的内部Sync
     * 使用AQS的state来表示计数count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
 
        Sync(int count) {
            // 使用AQS的getState()方法设置状态
            setState(count);
        }
 
        int getCount() {
            // 使用AQS的getState()方法获取状态
            return getState();
        }
 
        // 覆盖在共享模式下尝试获取锁
        protected int tryAcquireShared(int acquires) {
            // 这里用状态state是否为0来表示是否成功,为0的时候可以获取到返回1,否则不可以返回-1
            return (getState() == 0) ? 1 : -1;
        }
 
        // 减少count的值,如果count为0则发出释放信号。
		//这里使用了"自旋+CAS”的方式来原子性的将state的值减少1,
		//如果在此过程中state已经为0了(在并发情况下,可能已经被其他线程修改为了0),
		//则返回false。否则根据修改后state的值是否等于0来返回boolean值。
        protected boolean tryReleaseShared(int releases) {
            // 在for循环中Decrement count直至成功;
            // 当状态值即count为0的时候,返回false表示 signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
 
    private final Sync sync;
 
    // 使用给定计数值构造CountDownLatch
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
 
    // 让当前线程阻塞直到计数count变为0,或者线程被中断
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
 
    // 阻塞当前线程,除非count变为0或者等待了timeout的时间。当count变为0时,返回true
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
 
    // count递减
    public void countDown() {
        sync.releaseShared(1);
    }
 
    // 获取当前count值
    public long getCount() {
        return sync.getCount();
    }
 
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

使用AQS分别实现独占锁与共享锁

① 独占模式,独占模式采取的例子是银行服务窗口,假如某个银行网点只有一个服务窗口,那么此银行服务窗口只能同时服务一个人,其他人必须排队等待,所以这种银行窗口同步装置是一个独占模型。第一个类是银行窗口同步装置类,它按照推荐的做法使用一个继承AQS同步器的子类实现,并作为子类出现。第二个类是测试类,形象一点地说,有三位良民到银行去办理业务,分别是tom、jim和jay,我们使用BankServiceWindow就可以约束他们排队,一个一个轮着办理业务而避免陷入混乱的局面。

/**
 * 独占模式
 */
public class BankServiceWindow {
    private final Sync sync;

    public BankServiceWindow() {
        sync = new Sync();
    }
    private static class Sync extends AbstractQueuedSynchronizer {
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        protected boolean tryRelease(int releases) {
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }
    public void handle() {
        sync.acquire(1);
    }
    public void unhandle() {
        sync.release(1);
    }

}

测试类

public class BankServiceWindowTest {
    public static void main(String[] args) {

        final BankServiceWindow bankServiceWindow = new BankServiceWindow();
        Thread tom = new Thread() {
            public void run() {
                bankServiceWindow.handle();
                System.out.println("tom开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("tom结束办理业务");
                bankServiceWindow.unhandle();
            }
        };
        Thread jim = new Thread() {
            public void run() {
                bankServiceWindow.handle();
                System.out.println("jim开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("jim结束办理业务");
                bankServiceWindow.unhandle();
            }
        };
        Thread jay = new Thread() {
            public void run() {
                bankServiceWindow.handle();
                System.out.println("jay开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("jay结束办理业务");
                bankServiceWindow.unhandle();
            }
        };
        tom.start();
        jim.start();
        jay.start();
    }
}

明显tom、jim、jay仨是排队完成的,但是无法保证三者的顺序,可能是tom、jim、jay,也可能是tom、jay、jim,因为在入列以前的执行先后是无法确定的,它的语义是保证一个接一个办理。如果没有同步器限制的情况,输出结果将不可预测。

② 共享模式,共享模式采取的例子同样是银行服务窗口,随着此网点的发展,办理业务的人越来越多,一个服务窗口已经无法满足需求,于是又分配了一位员工开了另外一个服务窗口,这时就可以同时服务两个人了,但两个窗口都有人占用时同样也必须排队等待,这种服务窗口同步器装置就是一个共享型。第一个类是共享模式的同步装置类,跟独占模式不同的是它的状态的初始值可以自由定义,获取与释放就是对状态递减和累加操作。第二个类是测试类,tom、jim和jay再次来到银行,一个有两个窗口甚是高兴,他们可以两个人同时办理了,时间缩减了不少。

/**
 * 共享模式
 */
public class BankServiceWindowsShare {
    private final Sync sync;
    public BankServiceWindowsShare(int count) {
        sync = new Sync(count);
    }
    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);
        }

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

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

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


    public void unhandle() {
        sync.releaseShared(1);
    }
}

测试类

public class BankServiceWindowShareTest {
    public static void main(String[] args) {
        final BankServiceWindowsShare bankServiceWindows = new BankServiceWindowsShare(2);
        Thread tom = new Thread() {
            public void run() {
                bankServiceWindows.handle();
                System.out.println("tom开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("tom结束办理业务");
                bankServiceWindows.unhandle();
            }
        };
        Thread jim = new Thread() {
            public void run() {
                bankServiceWindows.handle();
                System.out.println("jim开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("jim结束办理业务");
                bankServiceWindows.unhandle();
            }
        };
        Thread jay = new Thread() {
            public void run() {
                bankServiceWindows.handle();
                System.out.println("jay开始办理业务");
                try {
                    this.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("jay结束办理业务");
                bankServiceWindows.unhandle();
            }
        };
        tom.start();
        jim.start();
        jay.start();
    }

}

参考文章:https://www.jianshu.com/p/8b5bdce4efdd;https://blog.csdn.net/wangyangzhizhou/article/details/40052353
其他文章:http://www.cnblogs.com/dream-to-pku/p/9186126.html

  • 4
    点赞
  • 2
    评论
  • 21
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页

打赏

zxc123e

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值