AQS是一个同步队列,它是一个抽象类,主要用途是处理多线程竞争,将未获取到锁的线程进行排队,是JUC包中重要的基石,其中除了维护一个队列之外,还有两个值比较关键,分别是:
- state 标记位
- exclusiveOwnerThread 当前所属线程
维护队列的两个参数分别是:
- Node head 同步队列头节点
- Node tail 同步队列未节点
维护的队列是一个双向链表,指向来上一个和下一个节点。
AQS是JUC的基石,类似于CountDownLatch、ReentrantLock等都是通过AQS实现的,其中state在不同的实现中作用也是不相同的,这里只拿ReentrantLock来做阐述。
ReentrantLock 锁
- ReentrantLock锁是有两个构造函数的,分别是无参构造和传入布尔值的构造函数,默认无参构造是创建一个非公平锁,布尔值构造函数,传入true则创建公平锁,传入false则创建的是非公平锁。这两种锁的大概区别就是,当有新线程获取锁时候,公平锁会将当前线程直接放入到队列当中,等待前面所有线程任务完成后才获取锁,而非公平锁在获取锁时,不管队列中是否有等待线程,它都会先尝试获取当前锁,如果获取失败,会判断是否是重入,还会去尝试获取当前锁,在加入队列前还会去尝试获取锁。
- ReentrantLock是属于可重入锁,可重入锁的意思就是如果当前线程已经拥有了锁,当当前线程再次去获取锁的时候,会判断获取锁的线程是否就是当前锁拥有的线程,如果相等则直接获取锁,可重入锁的好处就是可以避免死锁问题。前面说的AQS中有两个重要参数,exclusiveOwnerThread是用来保存当前锁所属线程,而state则是保存重入锁次数,如果一个线程获取锁一次,state的值则会加1,释放一次锁state的值会减1,所以在使用的时候,获取来几次锁则要相应的释放几次锁。当state为0时,表示当前锁还没有被线程占用。
- 在操作重要参数时都是使用的CAS方式设置值的,为的是解决多线程竞争修改同一个值的问题,这里的CAS是属于乐观锁,它主要的思想是<比较-替换> ,操作方法是使用的java的native方法,直接操作内存。<比较-替换>的意思就是有两个值,一个是旧的值,一个是我将要设置的新值,当JVM内存中的值与传入的旧值相等时,才将新值设置。如果就是不相等,则设置失败。CAS无法解决ABA的问题。
- 当创建的是非公平锁时,调用lock()方法
final void lock() { if (compareAndSetState(0, 1)) //判断是否当前锁是否被占用,如果未被占用则直接获取锁 setExclusiveOwnerThread(Thread.currentThread()); // 设置锁的所属线程 else acquire(1); //再次尝试获取锁 }
public final void acquire(int arg) { if (!tryAcquire(arg) && // 再次尝试获取锁,判读是否是重入 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果再次尝试获取锁失败则准备加入等待队列 selfInterrupt(); // 通知线程中断 }
- tryAcquire(arg) 再次尝试获取锁(判断是否是重入),经过两次跳转后进入到nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 判断是否属于无锁状态,如果是则直接获取当前锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 判断是重入,如果是则将AQS中的state重新设置值 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
- 如果再次获取锁失败,则准备加入队列 acquireQueued(final Node node, int arg),这里的Node指将当前的线程组装为一个Node节点,也是通过CAS+死循环的方式将节点加入到等待队列中,使用死循环是为了多线程竞争导致加入队列失败,再次重新加入。
final boolean acquireQueued(final Node node, int arg) { // node是指当前线程组成的节点,已经加入到了队列 boolean failed = true; try { boolean interrupted = false; for (;;) { //每个节点都在执行死循环,判断是否轮到自己获取当前线程了 final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { //判断是否轮到自己获取锁了,下面将详细说明 setHead(node); p.next = null; // help GC //将已经释放了锁的节点从队列中清除,顺便解除GC Root,帮助GC回收 failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
- p == head && tryAcquire(arg) ,判断是否可以获取锁,p == head表示当前节点的前继节点是否是头结点,如果第一个条件成功,则再次尝试获取锁,在获取锁的方法中,会判断state是否为0(无锁状态),如果为0,则表示当前节点的前继节点(头节点)已经使用完毕释放了锁,当前节点获取锁成功返回true,当两个条件都为true时,表示当前节点的前继节点执行完毕,需要剔除队列,当前节点应该设置为头节点。
- 释放锁比较简单,调用unlock()方法后进入下面的方法
public final boolean release(int arg) { if (tryRelease(arg)) { // Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
第二行代码:CAS将state值减少 ,如果不是所属线程去释放锁会抛异常,如果state减为0,则将所属线程设置为空
第五行代码:将唤醒后继节点获取锁。