一、AQS

首先回顾一下管程,管程是基于MESA模型实现的,包括同步等待队列和条件等待对象,同步等待队列是否获取锁有关,条件等待队列是阻塞唤醒机制有关。
JVM层面中实现的管程是ObjectMonitor数据结构,每个ObjectMonitor中有一个_cxq队列,获取锁失败的线程会进入cxq队列,即同步等待队列。在调用wait()的线程会进入waitset队列中,这个就是synchronized重量级锁的实现。
synchronized在同步代码块中,加锁和解锁是自动的,而且在膨胀引入Monitor之后,是一个重量级操作。
AQS就是JAVA层面实现的一个抽象层,其中包括同步等待队列(双向链表)和条件等待队列(单向链表),同步等待队列中使用CAS来修改state是否成功来判断是否加锁成功,加锁失败则入队,唤醒后抢到锁并出队。独占锁或共享锁可以使用这种逻辑来保证线程安全,但是加锁、解锁的具体逻辑根据独占或共享的逻辑不同由自己实现。条件队列由Condition接口提供await(获取锁的线程调用此方法,即释放锁,进入条件队列),signal(进入同步等待队列,等待被唤醒),signalAll方法。
java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的。
1.jpgAQS提供了公平锁和非公平锁,在ReentrantLock等工具中都是基于AQS来实现的。

1、AQS的特性

阻塞等待队列:线程进入队列后会park阻塞,等其他线程执行完后,会唤醒unpark头节点的线程。
共享/独占:基于等待队列和条件队列实现。
公平/非公平:当新的线程想要获取锁时,如果直接进入队列则为公平,如果直接尝试抢锁,则为非公平,抢锁失败则进入队列,在高并发的场景下非公平锁效率会高一些。
可重入:方法嵌套调用时,外层的方法使用锁,在被调用的方法中也有加锁逻辑那么就会产生重入锁。
允许中断:多个线程执行某个逻辑,但只需要一个线程执行即可,其他线程则需要等待,当获取锁的线程执行完某个逻辑后,其他线程不需要再次执行该逻辑,则可以发起中断信号,中断其他线程或执行后续逻辑。
AQS内部维护属性volatile int state,state表示资源的可用状态,使用CAS来修改这个状态来判断是否加锁成功,对于共享锁,这个属性也代表可以获取资源的个数。

2、AQS定义两种资源共享方式

Exclusive-独占,只有一个线程能执行,如ReentrantLock。
Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch。

3、AQS定义两种队列

同步等待队列: 主要用于维护获取锁失败时入队的线程。
条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。

4、AQS 定义了5个队列中节点状态

1)值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
2)CANCELLED,值为1,表示当前的线程被取消。
3)SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark。
4)CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中。
5)PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。
不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
1)isHeldExclusively()
该线程是否正在独占资源。只有用到condition才需要去实现它。
2)tryAcquire(int)
独占方式。尝试获取资源,成功则返回true,失败则返回false。
3)tryRelease(int)
独占方式。尝试释放资源,成功则返回true,失败则返回false。
4)tryAcquireShared(int)
共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
5)tryReleaseShared(int)
共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以上五个方法在不同的同步器实现中都有各自的实现,例如 ReentrantLock 中实现了 tryAcquire(int),Semaphore,CountDownLatch,ReentrantReadWriteLock 中实现了 tryAcquireShared(int) 方法等。

二、同步等待队列

AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列尾部,同时会阻塞当前线程。
当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。

2.jpg三、条件等待队列

AQS中条件队列是使用单向列表保存的,用nextWaiter来连接,调用await方法阻塞线程,当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)。
Condition接口详解:
调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

四、ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全,在竞争比较激烈的场景下ReentrantLock的性能更高,如果想能不是很激烈的情况下synchronized的性能比较高,轻量级锁没有阻塞线程,只是在用户态进行的,ReentrantLock虽然也会cas尝试获取锁,但是概率相对较低,最终还是使用阻塞线程。
相对于 synchronized, ReentrantLock具备如下特点:可中断、可以设置超时时间、可以设置为公平锁、支持多个条件变量、与 synchronized 一样,都支持可重入。

1、synchronized和ReentrantLock的区别

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在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁。

2、ReentrantLock的使用

  1. //加锁
  2. lock.lock();
  3. try {
  4. // 临界区代码
  5. // TODO 业务逻辑:读写操作不能保证线程安全
  6. for (int j = 0; j < 10000; j++) {
  7. sum++;
  8. }
  9. } finally {
  10. // 解锁
  11. lock.unlock();
  12. }

ReentrantLock实现了Lock接口,AQS的实现是其的一个属性:
4.jpg
在Lock接口中,定义了一些方法的规范:
3.jpg Condition newCondition():返回一个Condition 对象,因为await方法需要一个条件对象来条用,每个条件对象代表一个条件队列。
boolean tryLock():方法返回一个boolean 类型的返回值,返回是否获取锁成功。

五、条件变量

java.util.concurrent类库中提供Condition类来实现线程之间的协调。调用Condition.await() 方法使线程等待,其他线程调用Condition.signal() 或Condition.signalAll() 方法唤醒等待的线程。
调用Condition的await()和signal()方法,都必须在lock保护之内。

  1. private static ReentrantLock lock = new ReentrantLock();
  2. private static Condition cigCon = lock.newCondition();
  3. private static Condition takeCon = lock.newCondition();
  4. private static boolean hashcig = false;
  5. private static boolean hastakeout = false;
  6. //送烟
  7. public void cigratee(){
  8. lock.lock();
  9. try {
  10. while(!hashcig){
  11. try {
  12. log.debug("没有烟,歇一会");
  13. cigCon.await();
  14. }catch (Exception e){
  15. e.printStackTrace();
  16. }
  17. }
  18. log.debug("有烟了,干活");
  19. }finally {
  20. lock.unlock();
  21. }
  22. }
  23. //送外卖
  24. public void takeout(){
  25. lock.lock();
  26. try {
  27. while(!hastakeout){
  28. try {
  29. log.debug("没有饭,歇一会");
  30. takeCon.await();
  31. }catch (Exception e){
  32. e.printStackTrace();
  33. }
  34. }
  35. log.debug("有饭了,干活");
  36. }finally {
  37. lock.unlock();
  38. }
  39. }
  40. public static void main(String[] args) {
  41. ReentrantLockDemo6 test = new ReentrantLockDemo6();
  42. new Thread(() ->{
  43. test.cigratee();
  44. }).start();
  45. new Thread(() -> {
  46. test.takeout();
  47. }).start();
  48. new Thread(() ->{
  49. lock.lock();
  50. try {
  51. hashcig = true;
  52. log.debug("唤醒送烟的等待线程");
  53. cigCon.signal();
  54. }finally {
  55. lock.unlock();
  56. }
  57. },"t1").start();
  58. new Thread(() ->{
  59. lock.lock();
  60. try {
  61. hastakeout = true;
  62. log.debug("唤醒送饭的等待线程");
  63. takeCon.signal();
  64. }finally {
  65. lock.unlock();
  66. }
  67. },"t2").start();
  68. }

代码中创建了两个条件队列,cigCon和takeCon,条件队列的await方法,调用了park方法进行阻塞,signal方法调用了unpark方法进行唤醒。ArrayBlockingQueue就是两个条件队列来实现的。

六、ReentrantLock源码分析

1、多线程断点

在断点处选择Thread
4.png 可以在控制台切换不同线程进入断点
5.png

2、lock()方法

在创建ReentrantLock对象时,在构造方法中,可以传入一个公平、非公平的布尔类型参数,如果不传默认为非公平锁。ReentrantLock的sync属性,是AQS的子类,公平锁和非公平锁则是sync的子类。lock()方法就是执行sync属性的lock()方法,这个属性的值就是公平锁或非公平锁的实例。

  1. public ReentrantLock() {
  2. sync = new NonfairSync();
  3. }
  4. public ReentrantLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. }

公平锁和非公平锁的区别在于,非公平锁首先会尝试修改state属性的值,公平锁则直接调用acquire(1)方法。6.png 队列的入队和出队,线程的阻塞和唤醒这些公共逻辑,都是由AQS来实现的,在独占锁中,使用了等待队列,是一个双向链表,每个线程绑定一个Node节点,head头节点指针指向正在获取锁的线程,每次唤醒head头节点的下一个节点来获取锁。获取锁是使用CAS来修改state属性,如果修改成功那么表示获取锁成功,如果相同的线程再次获取锁,那么state会进行加一操作。
加锁操作中入队操作(enq方法)和获取锁操作(acquireQueued方法,获取锁失败阻塞)是由两个for循环来保证成功的。
ReentrantLock-lock.png

3、unlock()方法

当线程执行完成后,会修改state,然后修改等待队列的头节点的信号量,在唤醒头节点的下一个节点,当线程被唤醒之后,会进行出队操作。
ReentrantLock-unlock.png