AQS原码分享

210 阅读5分钟

0. 概述

Raido(就是我的名字啦)是JAVA后端开发,以下文字全是我最近学习java锁机制心得总结,记录下来也分享给大家,如果有错误的地方,也请指出,大家一起进步。

0.1 什么是AQS?

AQS核心类AbstractQueuedSynchronizer位于java.util.concurrent.locks包下,翻译过来叫同步队列。

1.0 结构

1.1 一个变量-state

state代表队列当前同步状态,>1代表正在支持同步操作,=0说明没有线程执行同步操作,可以争夺锁(基于CAS)。

1.1.1 为什么特意用volatile修饰? 两个原因:

保证可见性:被volatile修饰的变量,在每次写操作之后都会插入一条store内存屏障命令,将最新值刷新到主内存,在每个读操作之前,都会加入一条load内存屏障命令,强制线程从主内存加载该变量到工作内存;

禁止指令重排:被volatile修饰的变量,便衣的时候会插入内存屏障,告诉CPU和编译器,无论什么命令都不能与该指令重排。

保证原子操作(由Unsaft保证):同步操作必须考虑JAVA内存模型,即原子性,可见性,隔离性。AQS的底采用Unsaft.compareAndWasp(),可以做到硬件级别原子操作,那什么又叫硬件级别原子操作?这涉及到汇编,不是三流程序员清楚的,为了避免误导,简单从名字理解硬件级别原子操作能保证线程的原子操作没问题!!

1.2 一个引用-exclusiveOwnerThread

exclusiveOwnerThread主要用于锁的重入,2.5节讲解。

1.3 一条队列-CLH

CLH是双向虚拟链表,为啥叫虚拟?因为它是由一个个Node节点形成的链表。

2.0 锁分类

2.1 CLH队列特点

CLH队列头尾节点不存储实际数据,当线程抢锁失败,CAS争夺插入尾节点,当线程释放锁之后,公平锁模式下会唤醒头节点的后继节点争夺锁,非公平锁模式下,刚刚抢锁失败的线程获得插队抢锁的机会。从这我们看出来,CLH队列里面的都是渴望获取锁的线程,它们非常守规矩,等队头的线程干完活了,不要锁了,会唤醒头节点的后继节点,这也是为什么AQS使用链表不使用数组的原因,因为AQS全都是插入移除操作,不过它也有遍历操作(2.3节)。

2.2 独占式

代表ReentrantLock

加锁(基于非公平锁)

独占式锁代表有ReentrantLock,上锁时首先用CAS尝试将state设置成1,如果失败,说明其他线程持有锁,执行else操作。

else操作再次尝试获取锁,这里的语法非常美妙,利用&&的断路效应,先执行tryAcquire(arg),返回false的时候后面的语句就不会继续执行下去,比我这种渣渣写的if else好看多了。
我们看看tryAcquire(arg)都干了什么!获取同步状态state,判断是否重入,否则CAS加锁,上述都失败就返回false,这个时候线程就得丢到CLH队列
让我们回过头来看CLH队列怎么添加线程的。首先看addWaiter(),add waiter,看就知道了,添加一个服务员(呸!),是添加一个等待者。它首先获取队列尾元素,再用CAS那套将当前线程塞到队尾,第一次CAS失败后,执行enq(node),该方法里面是jdk处处可见的for(;;),不塞入队伍不返回那种,颇有一种把你CPU榨干为止的气势。
假如线程入队成功,将又是一个for(;;),判断该节点的前驱节点是否为头节点,同时CAS设置state成功才能逃出循环。或者另外一种情况,头节点statue=SIGNAL,(从注解来读)表明该节点可以安全停止,停止自旋,进入阻塞状态。

解锁

解锁很简单,不需要CAS操作,先判断currentThread==exclusiveOwnerThread,true的话还得等state减到0,等state=0的时候,才算解锁成功,唤醒头节点的后继节点。

tryRelease(arg)函数设置减少state到0。 unparkSuccessor(h)在state=0的时候,唤醒头节点的后继节点。也是基于Unsaft.unpark()函数。

2.3 共享式

代表Semaphore信号量,CyclicBarrier屏障,CountDownLatch计数器

初始化(下面教程基于Semaphore)

共享式的特点是多个线程同时持有锁(取决于初始化时传的n,解锁时会唤醒队列所有线程)。

加锁(基于非公平锁)

加锁是一个基于CAS的--state过程,每次传人1,如果state-1小于0,说明锁已经没了,返回false告诉线程入队列去。剩下的没啥好说的!值得一提的是,共享式无论是加锁和解锁都是CAS操作,原因嘛,可能有多个线程同时解锁,所以需要hold住。

2.4 公平锁&非公平锁

非公平锁比公平锁效率高,因为线程抢锁失败到入队前,还有一次抢锁成功的机会,这么设计的原因是线程执行同步代码块的时间比较快(当然啦,这是理想条件),说不定就踩到狗屎运抢到锁,不用入队列。公平锁控制下线程只能乖乖去排队。

2.5 重入锁

重入锁只在独占式存在意义。AQS使用一个变量exclusiveOwnerThread存储获得锁的线程,抢锁的时候判断state状态,如果大于0,说明有锁,接着判断抢锁线程==exclusiveOwnerThread?,如果两者相同,该锁是重入锁(我觉得这里的重入锁只是个概念,方便理解,其锁性质一点没变),++state。解锁的时候也会--state,保证了每次重入的++都有一次释放的--。