Java并发之AQS

497 阅读6分钟

AQS是什么?

我们说到Java就不得不提到JUC,说到JUC就不得不提到AQS了,但是AQS到底是什么呢?

AQS全称为AbstractQueueSynchronizer,我们可以直译为抽象的队列同步器。

    AQS使用虚拟队列的方式来管理线程中锁的获取与释放,同时也提供了各种情况下的线程中断。
    这个类虽然提供了默认的同步实现,但是获取锁和释放锁的实现被定义为抽象方法,由其子类实现

AQS维护了一个volatitle int state 和一个FIFO线程等待队列,这个队列使用双向链表实现,当多线程争用资源被阻塞时会进入队列。只有Head节点释放资源后,下一个线程才能获得资源。state就是AQS的同步状态标量。

A6U)5FR_IWQ}B1KJWQ`~}3D.png

AQS中state成员变量

private volatile int state

state成员变量用于记录有多少个线程在等待资源,如果为n(n > 0)证明有n个线程在等待资源释放,如果为0证明此时没有线程等待释放资源!

我们看到,这里定义state字段有一个volatile关键字。

这里我简单来叙述一下volatile关键字

volatile

我们经常将volatile与synchronized对比,我们知道synchronized能保证线程安全,但是效率很低,volatile字段不保证线程安全,但是volatile字段能保证线程安全三大条件的两个条件,一个是有序性,一个是可见性,还有一个原子性volatile是不能保证的,所以他不能保证线程安全。

我们都知道volatile能保证可见性,那到底什么是可见性呢?

  可见性:当多个线程在访问一个变量的时候,一个线程修改了变量的值,其他线程应该能立即看到修改后的值
  

那volatile是怎么实现可见性的呢?

  volatile的实现原理是通过内存屏障来实现的
  其原理为,当CPU写数据时候,如果发现该数据在其他CPU中有副本,那么就会发出信号
  告诉其他CPU将该副本对应的缓存设置为无效状态,当CPU读取这个数据的时候
  发现这是一个无效数据,然后会重新从主存中读取
  

我们还知道volatile能保证有序性,那什么是有序性呢?

1. int yl = 0;
2. yl++;
3. boolean z = false;
4. z = true;

我们都知道程序是按照顺序执行的,当i++执行早于int i = 0的时候程序就会报错,但是int yl = 0一定早于boolean z = false吗?

并不一定,当然这在单线程环境下没有任何问题,但是多线程环境下,会出现问题,因为在单线程环境下,指令发生重排不会影响结果,我们将z = true;移到yl++之前也没有问题,但是多线程环境下就会出现问题了,我们看下面这段代码。

int yl = 0;
boolean z = false;
//线程一
Thread.sleep(5);
yl++;
z = true;
//线程二
while(!z){
System.out.println(yl);
}

这段代码应该是线程二不停打印0,然后打印一个1结束,实际情况不一定是这样,因为会发生指令重排 因为i++和 z = true没有任何关联性,所以我们可能得到线程二一直打印0然后结束

所以我们可以使用volatile关键字修饰yl,这样我们最后得到的结果就会出现我们期望的结果了!

    有序性:程序执行的顺序应当按照代码的先后顺序执行

volatile不能保证原子性,我们学过数据库的朋友应该都明白原子性,因为这就是事务的ACID特性的其中一个。

    原子性:一个或多个操作,要么全部连续执行且不会被任何因素中断,要么就都不执行

我们回到AQS,既然volatile关键字不能保证原子性,那我们用什么呢?

其实我们可以使用synchronizaed,但是这样做明显效率不是很高,因为在高并发下,这样做会导致很多很多线程争抢一个资源,AQS如果使用这个,那肯定会被骂很惨(狗头保命),所以AQS在这里使用的是CAS.

可能有的朋友要说了,我就想学个AQS,为什么要延申出来这么多东西,这些东西是学习AQS的必须品,所以我们也要了解知道。

CAS

CAS全称为Compare And Swap,直译为比较并交换

它的作用:对指定内存地址的数据,校验它的值是否为期望值,如果是,那么修改为新值,返回值表示是否修改成功

AQS的等待队列

说完上面这些,我们回到AQS,在AQS中有两种模式,一个是独占模式,一个是共享模式。

独占模式

    表示该锁会被一个线程占用着,其他线程必须等到持有线程释放锁才能获取到锁继续执行
    简单来说,就是在同一时间内,只能由一个线程获取到这个锁        

共享模式

    表示多个线程获取同一个锁的时候,有可能(并非一定)会成功。        

在JUC中,ReentrantLock就是采用的独占模式而ReadLock采用的是共享模式。

AQS的原理在于每当有新的线程请求资源的时候,该线程都会进入一个等待队列,只有当持有锁的线程释放资源之后,该线程才能持有资源。

在AQS的等待队列中,每个线程都被包裹在一个Node结点中,我们前面说过了,这个等待队列是一个双向链表,说到这个Node结点就不得不说一下Node结点的四种状态了。

  • 线程已经取消(CANCELLED) 1

  • 线程需要唤醒(SIGNAL) -1

  • 线程在等待(CONDITION)-2

  • 后继节点会传播唤醒操作(PROPAGATE)这个状态仅在共享模式下生效 -3 具体的流程:

  • 先尝试加锁

    • 成功

      中断线程的阻塞状态
      
    • 失败

      就将当前线程加入等待队列,通过自旋不停地尝试获取资源
      当成功获取到的时候就发起中断,中断线程的阻塞状态
      

如下图所示

AQS加锁.png 这里其实包含了很多细节,我只是将我的理解叙述出来

在将线程添加到等待队列的时候,分为两种情况:

  • 等待队列没有结点的时候进行队列初始化
  • 等待队列具备结点的时候,把当前线程封装为队列结点,确保它插入到队列尾部

在添加一个新的 线程结点的过程中,会检查它的前驱节点,只要有一个结点的状态为SIGNAL, 就表示当前结点之前的结点正在等待被唤醒,那么当前线程就需要被阻塞

node结点入队.png

在出队的过程,先把head结点的状态设置为0,表示队列中没有需要终端阻塞的线程,然后确定需要被唤醒的结点,该结点是队列中第一个状态值小于等于0的结点,然后唤醒进程,并将head的状态值"修改"回去,需要注意的是,这里是修改并非真正的修改,而是使用node1结点替换head结点,如下图所示。

node结点唤醒过程.png