AQS详解

本文深入剖析了AQS(AbstractQueuedSynchronizer)的基本结构、属性和ReentrantLock的源码实现,揭示了线程抢锁与阻塞队列的工作原理,包括公平锁与非公平锁的区别,以及释放锁的流程。
摘要由CSDN通过智能技术生成

AQS

基本介绍

属性

AQS基本上是由四个属性实现的:

  • head:头结点,毕竟是队列嘛,肯定会有一个头结点的;
private transient volatile Node head;
  • tail:尾节点,双向队列嘛,肯定会有一个尾节点:
private transient volatile Node tail;
  • state:状态,锁嘛,肯定会有一个锁状态,0表示没有线程获取锁,非0表示获取锁的次数(重入或者共享锁):
private volatile int state;
  • exclusiveOwnerThread:获取锁的线程,可重入锁的时候,是不是要判断下这个锁被哪个线程获取了?这个属性在父类(AbstractOwnableSynchronizer)里面:
private transient Thread exclusiveOwnerThread;

概念

AQS其实就是一条阻塞队列,然后它是双向的,主要实现的功能就是多个线程去获取锁,获取失败的线程会变成一个节点(Node)进入到这个阻塞队列中,线程先阻塞住随后再去尝试获取锁。看下Node的数据结构吧:

	static final class Node {
        // 共享Node(读锁)
        static final Node SHARED = new Node();
        // 独占Node(写锁)
        static final Node EXCLUSIVE = null;

		// 几个状态常量
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
		
		// 节点等待状态,会用到上面的状态常量
		// 当为CANCELLED时,表示放弃抢锁了(可以实现超时)
        volatile int waitStatus;
		
		// 前置节点,双向链表嘛
        volatile Node prev;

        // 后置节点,链表嘛
        volatile Node next;

        // 线程,因为本质上是线程抢锁,所以节点都是封装线程来的
        volatile Thread thread;

        // 条件队列的下一个节点,这个都不是主流需要关注的了
        Node nextWaiter;

        ...
        // 其他的方法都不重要了
    }

可以看到,Node其实也就四个属性:两个链表属性(prev、next)、一个线程本身(thread)、一个状态(waitStatus),前三个好像都很容易理解,看样子重点在状态属性上。

用ReentrantLock看AQS实现

AQS是一个基础架构,所以其实在日常使用中,我们很少会直接用到,除非我们需要自定义锁,一般我们在需要使用锁的时候,会直接使用JDK已经帮我们实现好了的ReentrantLock,先看个例子:

	public class ProductServiceImpl {

    private ReentrantLock lock = new ReentrantLock();

    public void subSafety(int num){
        lock.lock();
        try {
            // do sub
        }finally {
            lock.unlock();
        }
    }
}

这样就保证了同时只会有一个线程去做减库存操作,然后来看下ReentrantLock的实现源码吧。

ReentantLock获取锁

ReentrantLock是用其内部类Sync实现的,它的构造方法都只能生成一个Sync对象

	public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

然后看下加锁方法(lock)源码,拿公平锁(FairSync)举例说明吧,中间步骤就省略了,直接看AbstractQueuedSynchronizer#acquire方法,所有的AQS实现加锁功能都要调用这个方法

  • AbstractQueuedSynchronizer#acquire
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire方法的含义是尝试去获取锁,获取到了返回true,在AQS中这个方法没有做实现,需要子类自己去实现,这也体现出了面向对象编程的思想,看下FairSync中tryAcquire的实现吧

  • ReentrantLock#FairSync#tryAcquire
	protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取锁的状态
            int c = getState();
            // 0表示没有线程获取到锁
            if (c == 0) {
            	// 公平锁的实现:有人在队列里就乖乖排队
                if (!hasQueuedPredecessors() &&
                // CAS尝试获取锁
                    compareAndSetState(0, acquires)) {
                    // 成功就将AQS中的独占线程设置成自己,表示我抢到锁了
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 可重入锁(加上获取锁的次数就返回了)
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 上面两种都不是就失败了
            return false;
        }

抢锁的代码很简单,逻辑也很清晰,不需要多说什么了,需要注意的是:看过非公平锁的源码实现会发现,非公平锁就少了一句!hasQueuedPredecessors()判断,也就是说,非公平锁的非公平性就体现在第一次抢锁的时候不会去管队列中有没有节点在排队,直接去尝试抢一次锁,抢不到了之后也会乖乖到队列中排队,看源码这点就很清晰了。
回到AbstractQueuedSynchronizer#acquire方法:

	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

会发现,如果tryAcquire(arg)方法抢到锁(返回true)那就结束了,但是如果没有抢到锁按照正常的逻辑是要挂起线程,然后添加到队列里面的,看下addWaiter(Node.EXCLUSIVE)方法,这个是在AQS中实现的:

	private Node addWaiter(Node mode) {
		// 将当前线程生成Node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 添加到队尾
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 注意代码能到这里的逻辑:
        // 队列为空 || CAS入队失败了
        // 所以这个方法的作用就是初始化队列和自旋入队(一次入队失败就多入几次)
        enq(node);
        return node;
    }

这代码逻辑就很简单了,就是生成Node然后入队,但是需要注意下enq方法,看下这个源码吧。

	private Node enq(final Node node) {
		// 自旋
        for (;;) {
            Node t = tail;
            // 队列为空
            if (t == null) { // Must initialize
            	// 生成一个空Node,然后再次循环
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// 正常逻辑是这里
                node.prev = t;
                // 必须入队才退出自旋
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这个方法只要记住能跑到这个方法的条件(队列为空或者入队失败)就很容易理解这个方法的逻辑了,需要注意的是当队列为空时,会先生成一个空Node作为首节点,我们一个称这个节点为哨兵节点,这点还是很重要的。
然后再回到acquire方法,看下acquireQueued方法,addWaiter方法已经将thread方法队列中了,那acquireQueued方法的作用应该就是挂起线程了。

	// 这里的node就是上面入队的node
	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环
            for (;;) {
            	// 这个是获取前置节点
            	// 这里就是前面说的,队列的head不是真正的头部,而是哨兵节点,所以要获取这个节点的前置节点再去比较
                final Node p = node.predecessor();
                // p == head说明现在这个node是头节点,可以直接尝试获取锁,再次调用tryAcquire
                if (p == head && tryAcquire(arg)) {
                	// 这里说明已经获取到锁了,这个方法会将node中的thread设置成null
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }

				// 代码到这里有两种可能:
				// node不是头节点 || 抢锁失败了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法就涉及到了之前提到的哨兵节点,代码逻辑比较简单,就是如果node是第一个节点的话(不能说是头节点)就直接去尝试获取锁,难理解的是下面锁获取失败的处理方法shouldParkAfterFailedAcquire,如果这个方法返回true,那就会调用parkAndCheckInterrupt方法来挂起线程了。

// pred是这个node的前置节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // Node.SIGNAL状态是可以获取锁的状态,直接挂起线程
        if (ws == Node.SIGNAL)
            return true;
        // 大于0表示这个节点已经被放弃了,不再去获取锁了
        if (ws > 0) {
            do {
            	// 因为前置节点不再被唤醒去获取锁了,所以node要循环去找一个正常的前置节点
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
           // node入队的时候状态都是0,所以这里要把它设置成Node.SIGNAL,可唤醒的状态
           // 第一次进这个方法应该都是先走到这里,然后再次循环进入(acquireQueued方法中有一个死循环)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这个方法的作用是是否应该挂起线程,返回结果是一个boolean值,看这个方法之前要先知道一个概念:队列中节点的唤醒是由前置节点来完成的,前置节点在释放锁的时候会唤醒第一个节点去尝试获取锁。
到这里ReentrantLock获取锁部分的源码就看完了,其实也就做了几件事:尝试获取锁、失败就生成node入队、队是空的就创建空头节点(哨兵节点)、挂起线程等待唤醒。

ReentrantLock释放锁

把获取锁的过程弄懂了,释放锁就很简单了,就不一步步地看了,直接看AbstractQueuedSynchronizer#release方法吧

  • AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

和获取锁相似的套路,然后看下ReentrantLock#tryRelease方法。

  • ReentrantLock#tryRelease
	  protected final boolean tryRelease(int releases) {
	  	// 这个是锁的状态,0表示没有被获取,>0表示被获取的次数
	  	// 这里是释放,所以要减去释放的次数
          int c = getState() - releases;
          // 自己的锁当然只能自己释放
          if (Thread.currentThread() != getExclusiveOwnerThread())
              throw new IllegalMonitorStateException();
              // 释放结果:true表示完全释放,false表示还没完全释放
          boolean free = false;
          // 当释放结果为0才表示这个锁完全释放了,否则是只释放了一部分(重入多次)
          if (c == 0) {
              free = true;
              setExclusiveOwnerThread(null);
          }
          setState(c);
          return free;
      }

这个方法逻辑太清晰了吧,甚至都不知道再说些什么了,再回到release方法,看下锁释放成功之后的操作,比如唤醒后续节点什么的

	public final boolean release(int arg) {
        if (tryRelease(arg)) {
    		// 锁成功释放就到这里    	
            Node h = head;
            // 头节点,头节点是哨兵节点!
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

这里再强调下:头节点是哨兵节点,是一个空节点,不是我们实际的节点!然后再看下unparkSuccessor方法,这个方法重要是唤醒后置节点

	// 这里的node就是头节点
	private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

       	// 看,实际操作的都是头节点的后置节点
        Node s = node.next;
        // 如果这个节点取消了等待
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从尾节点开始找,从后往前找
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        
        if (s != null)
        	// 唤醒
            LockSupport.unpark(s.thread);
    }

在AQS中,队列中的很多查找都是从尾部开始的逆查找,至于原因,在网上查的资料说是和并发有关系,我也没太搞懂。
到这里AQS整体的实现就看完了,内容其实不多,而且代码逻辑很清晰简单,好像也没什么总结的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值