AQS= volatile修饰的state变量(同步状态) +FIFO队列(CLH改善版的虚拟双向队列,用于阻塞等待唤醒机制)
队列里维护的Node节点主要包含:等待状态waitStatus,前后指针,等待的线程。
AQS是个抽象队列同步器,是JUC体系中用来构建锁和其他同步器如 ReentrantLock/CountDownLatch/Semphore的基石。AQS内部通过内置的FIFO先进先出的LCH(虚拟双向链表)队列来完成线程排队,并通过volatile 修饰的int类型状态变量来表示持有锁的状态。
简单的说,AQS通过volatile 修饰的int类型状态变量来表示同步状态,加volatial的目的是保证可见性。然后如果状态变量大于等于1是表示资源被占用,这时候抢不到资源的线程就要进入排队等候队列,等待资源的释放,这里面就需要阻塞等待唤醒机制来实现,AQS通过把等待获取资源的线程封装为Node<Thread>
节点入队,在资源释放后通过LockSupport.park().unPark()
来唤醒线程,通过CAS自旋来进行资源的抢占。
ReentrantLock默认是非公平锁,如果要实现公平锁构造函数中传入true表示创建的是公平锁。
公平锁相较于非公平锁体现在公平锁会先判断队列中是否有等待的线程,有的话优先获取到锁资源。
以3个线程分别为ABC争抢锁为例。
public class ReentrantLockTest { //模拟银行排队 public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); //第一个获取到锁的客户,执行自己的业务60秒 new Thread(() -> { lock.lock(); try { System.out.println("A 获取到锁,执行任务------------"); TimeUnit.SECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "A").start(); //第二个客户获取不到锁,阻塞 new Thread(() -> { lock.lock(); try { System.out.println("B 获取到锁,执行任务------------"); } finally { lock.unlock(); } }, "B").start(); //第三个客户获取不到锁,阻塞 new Thread(() -> { lock.lock(); try { System.out.println("C 获取到锁,执行任务------------"); } finally { lock.unlock(); } }, "C").start(); } }
acquireQueued
方法继续尝试,线程B会通过自旋判断自己在队列中的位置,如果线程B的前节点是哨兵节点,那么线程B进行自旋处理,首先会继续CAS尝试加锁,这时候如果还是不成功,就会设置线程B的前缀节点的等待状态从0变成-1,表示等待被唤醒状态。继续进入自旋逻辑,还是会再尝试CAS尝试加锁一次,还是失败就会调用LockSupport.park(this);
方法把线程设置为阻塞状态,等待被唤醒。1.如果没有哨兵节点,那么每次执行入队操作,都需要判断head是否为空,如果为空则head=new Node如果不为空则head.next=new Node,而有哨兵节点则可以大胆的head.next=new Node.
2.如果没有哨兵节点,可能存在之前所说的安全性问题,当只有一个节点的时候执行入队方法,无法保证last和head不为空。哪怕执行enqueue入队之前last和head还指向一个节点,可能由于并发性在具体调用enqueue方法操作last的时候head和last共同指向的头节点已经完成出队,此时last和head都为null,所以enqueue方法中的last.next=new node会抛空指针异常,且由于线程并发性的问题,last始终可能随时为空的问题不使用哨兵节点是无法解决的。