java并发编程-AQS

如果觉得写的还可以请关注微信公众号:程序猿的日常分享,定期更新分享。

AQS是什么

AQS的全称为(AbstractQueuedSynchronizer),这个类也是在java.util.concurrent.locks下面,他是用来构建锁或者其他同步组件的基础框架,他使用了一个int的state字段来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
从名字上可以看出他是一个抽象类,那么我们看下继承他的子类有哪些

如图所示,AQS 在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理。

AQS内部原理

AQS使用的是模板方法模式,也就是说使用者继承同步器后需要重写指定的方法,并根据自定义的同步组件的逻辑去实现。
同步器主要要3个重要的方法:
1、getState():获取当前同步状态
2、setState(int newState):设置当前同步状态
3、compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能保证状态设置的原子性。

state的作用

通过上边方法可以看到,他们都是和state相关,那么state到底在同步器中起到了什么作用。首先他的含义是同步状态,代码如下:

/**
     * The synchronization state.
     */
    private volatile int state;

我们可能想到同步状态是不是0表示没人占有,1表示有人,就像开关一样要么开要么关。其实他的含义在不同的实现类中是不一样的,你怎么利用他,是你自己去实现,非常的灵活。举三个例子:
1、在 ReentrantLock 中它表示的是锁的占有情况。就非常符合上边我们的财像。最开始是 0,表示没有任何线程占有锁;如果 state 变成 1,则就代表这个锁已经被某一个线程所持有了。
但ReentrantLock有个特点就是可重入的,每一次重入state就会加1,这时state表示的就是重入的次数。那么在释放的时候也是逐步释放,每次释放都会减1,也就是说只要state大于0,那么就代表锁被占有,直到state等于0才会代表这个锁当前是处于释放状态的,其他线程此时就可以来尝试获取了。
2、在 CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。我们都知道CountDownLatch类似一个倒计时,等到所有的线程都执行完并减1,此时state等于0时,整个任务才会结束。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门闩被放开。
3、在信号量里面,state 表示的是剩余许可证的数量。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,线程执行完就会归还许可证,这个state又会变成10。所以信号量的 state 相当于是一个许可证桶。

修改state

接下来我们结合state的定义看下修改state的两个方法,分别是setState和compareAndSetState。代码如下:

    /**
     * The synchronization state.
     */
    private volatile int state;
    protected final void setState(int newState) {
        state = newState;
    }

我们看到setState方法中非常简单粗暴,state直接就等于newState,这里并没有进行任何的并发安全处理,没有加锁也没有 CAS,那如何能保证线程安全呢?
这就不得不提在定义state的时候加上了volatile关键字,因为state是int类型,int属于基本类型,这里是一个赋值操作,所以volatile在这里起到了原子性的作用,他保证了线程的安全性。这是一个非常典型的volatile 的使用场景。

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

再来看一下 compareAndSetState 方法,这是一个我们非常熟悉的 CAS 操作,它调用了unsafe.compareAndSwapInt(this, stateOffset, expect, update),它利用了 Unsafe 里面的 CAS 操作,利用 CPU 指令的原子性保证了这个操作的原子性。

FIFO队列

AQS 的第二个核心部分,FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“排队管理器”。
这个队列内部是双向链表的形式,其数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。我们来看一下 AQS 作者 Doug Lea 给出的关于这个队列的一个图示:

在队列中,分别用 head 和 tail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,没有成功获取到锁的线程将为成为节点加入到尾部,它们会等待被唤醒,唤醒也是由 AQS 负责操作的。

获取/释放方法

AQS 的第三个核心部分,获取/释放方法。在 AQS 中除了刚才讲过的 state 和队列之外,还有一部分非常重要,那就是获取和释放相关的重要方法,这些方法是协作工具类的逻辑的具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。

获取方法

我们首先来看一下获取方法。获取操作通常会依赖 state 变量的值,根据 state 值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞,下面就让我们来看几个具体的例子。

比如 ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。

再比如,Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。

再举个例子,CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。

总结一下,“获取方法”在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。

释放方法

释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。

比如在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;而在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。所以也可以看出,在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。

如果觉得写的还可以请关注微信公众号:程序猿的日常分享,定期更新分享。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值