AQS
:全称为Abstract Quened Synchronizer
,抽象的队列式同步器,是一个抽象类,是除了java
自带的synchronized
关键字之外的锁机制,这个类在java.util.concurrent.locks
包,可以用来构造锁和同步类,如ReentrantLock
,Semaphore
,CountDownLatch
,CyclicBarrier
。
AQS
的核心思想如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH
(Craig,Landin,and Hagersten
)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS
的原理
AQS
内部有三个核心组件,一个是state
代表加锁状态初始值为0
,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以CAS
的形式将state
变为1
,CAS
成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state
是不是0
,不是0
再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的
可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1
,释放锁的时候就将state-1
。当state
减到0
的时候就去唤醒阻塞队列的第一个线程。
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
AQS
是将每一条请求共享资源的线程封装成一个CLH
锁队列的一个结点(Node
),来实现锁的分配。
简单来说,AQS
就是基于CLH
队列,用volatile
修饰共享变量state
,线程通过CAS
去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS
是自旋锁: 在等待唤醒的时候,经常会使用自旋(while(!cas())
)的方式,不停地尝试获取锁,直到被其他线程获取成功。
注意和数据库中的锁概念要区分开
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS
的衍生物。这些类内部持有一个AbstractQuenedSynchronizer
的实例,通过该实例,实现共享、独占等特性。
AQS
分为独占锁和共享锁
可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS
去抢占锁,公平锁会按队列顺序排队
设定一个信号量,当调用acquire()
时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()
时释放一个信号量,唤醒阻塞线程。
应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票
给计数器设置一个初始值,当调用CountDown()
时计数器减一,当调用await()
时判断计数器是否归0
,不为0
就阻塞,直到计数器为0
。
应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
给计数器设置一个目标值,当调用await()
时会计数+1
并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值 。
应用场景:多线程计算数据,最后合并计算结果的应用场景
是锁底层的实现的划分,AbstractQuenedSynchronizer
的子类都是自旋锁,通过反复尝试进行修改,加入等待逻辑,实际上避免了强制加锁;与此相对的是 Synchronized
,强制加锁!
同一时间,只有一个线程能访问某个资源。
ReentrantLock
和Synchronized
都是互斥锁
允许2个以上的线程同时访问 。当然根据不同的场景,可以划分为多个不同的实现 ,信号量、栅栏,都是共享锁
与互斥锁不同的是读写锁的规则是可以共享读,但只能一个写。涵盖了互斥锁和共享锁
总结起来为:读读不互斥,读写互斥,写写互斥,而一般的独占锁是:读读互斥,读写互斥,写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。
当场景中往往读远远大于写,效率很高,因为不用频繁加锁,2个读之间可以像没有锁那样!
注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。
ReentrantReadWriteLock
就是读写锁