AbstractQueuedSynchronizer
先对AQS的各种特点有个印象,后面执行流程讲解完了再回来对应一下就能理解了
什么是AQS?
字面意思理解:抽象队列同步器,是一个抽象同步框架
java中大多数同步器如Lock,Latch,Barrier等,都是基于AQS框架来实现的
共同行为如:等待队列、条件队列、独占获取、共享获取等,也都是基于AQS实现
实现方式:
1.维护一个内部类Sync继承AQS
2.将同步器所有调用都映射到Sync对应的方法
技术原理:
基于一个共享变量state + 双向虚拟队列FIFO
AQS特性:
阻塞等待队列 共享/独占 公平/非公平 可重入 可中断
状态的访问方式
getState() setState compareAndSetState()---比较后设置,cas方式
AQS中节点状态:
1.值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
2.CANCELLED,值为1,表示当前的线程被取消
3.SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
4.CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
5.PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
资源共享方式:
独占,只有一个线程执行,如ReentrantLock
共享,多个线程共同执行,如Semaphore/CountDownLatch
AQS实现原理:以ReentrantLock为例
从上面这个代码来进行调试,启动两个线程,当线程1启动时,调用lock.lock()
也就是说,调用的应该是Sync类下面的lock,实现就两种,公平和非公平,默认无参构造方法使用的是非公平锁,所以面试问到记得 ReentrantLock默认是非公平的
再看非公平的lock实现
采用比较设置的方式,也就是常说的cas,内部调用unsafe工具包直接操作内存地址的
如果能获取到锁,修改state,继续往下执行。但是现在线程二来了,线程一还在处理业务,并没有释放锁资源,线程二执行lock的时候获取不到,则进入阻塞队列等待
大流程并不复杂,只是里面内容比较多,还有设计类似重入(同一个线程获取了多次锁称为重入),锁释放,公平非公平(先到先得公平,随机或者后到先得非公平)唤醒(唤醒原理也是内容比较多的,可自行了解)等等,没有必要说的太深,太理论了很多朋友也不愿意去了解,大概明白原理,概念,知道怎么用就行了,下面贴一张别人的AQS的流程图,本人画图太烂,就不献丑了
总结一下,AQS就是通过state来确定锁状态,抢不到锁的线程,就放在FIFO队列中进行管理,争取下次再进行争取获取资源
ReentrantLock
字面理解为可重入锁,功能和synchronized是差不太多的,当然也经常有人面试被问到他两的区别:
1.synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
2.synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
3.synchronized是非公平锁,ReentrantLock可以是公平也可以是非公平的
4.synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
5.在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
6.ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
7.synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁,而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
使用方式在上面已经有例子了,关于其他的重入,中断等等方式可以自行了解
ReentrantReadWriteLock
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有Writer线程,读锁可以由多个Reader线程同时持有。也就是说,写锁是独占的,读锁是共享的。
用法差不多,就是分为读锁和写锁分开写,适合读多写少的场景
CountDownLatch
CountDownLathch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象——count不会被重置。如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier。
CountDownLatch应用场景
CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。
CountDownLatch与Thread.join的区别
CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join()方法,但其提供了比join()更加灵活的API。
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。而join()的实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
CyclicBarrier
CyclicBarrier介绍字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier应用场景
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
CyclicBarrier与CountDownLatch的区别
1.CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
2.CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
3.CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
4.CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
5.CyclicBarrier还可以提供一个barrierAction,合并多线程计算结果。
6.CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
Semaphore
Semaphore,俗称信号量,Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
应用场景
可以用于做流量控制,特别是公用资源有限的应用场景
关于AQS常用实现的几个同步器就介绍到这儿,比较理论,用法都不难,但是用好就需要了解他底层源码如何实现