定义

  1. 全称是 AbstractQueuedSynchronizer
  2. 阻塞式和相关的同步器工具框架
  3. AQS用一个变量 (volatile state) 属性来表示锁的状态,子类去维护这个状态
  4. getState、compareAndSetState CAS改变这个变量
  5. 独占模式是只有一个线程能够访问资源
  6. 而分享模式可以允许多个线程访问资源(读写锁)
  7. 内部维护了一个FIFO等待队列,类似 synchronized 关键字当中的 Monitor 的 EntryList
  8. 条件变量来实现等待、唤醒机制,支持多个条件变量,类似 Monitor 的 WaitSet
  9. 内部维护了一个 Thread exclusiveOwnerThread 来记录当前持有锁的那个线程

功能

  1. 实现阻塞获取锁 acquire 拿不到锁就去阻塞 等待锁被释放再次获得锁
  2. 实现非阻塞尝试获取锁 tryAcquire 拿不到锁则直接放弃
  3. 实现获取锁超时机制
  4. 实现通过打断来取消
  5. 实现独占锁及共享锁
  6. 实现条件不满足的时候等待

AQS关键设计

  1. private transient volatile Node head; //队首
  2. private transient volatile Node tail;//尾
  3. private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
  4. private transient Thread exclusiveOwnerThread;//持有锁的线程

image.png

Node的关键设计

这里的node可以看成是一个线程的包装类,简而言之一个node对象代表一个线程

  1. public class Node{
  2. volatile Node prev; //上一个节点
  3. volatile Node next; //下一个节点
  4. volatile Thread thread; //当前node包含的线程对象
  5. volatile int waitStatus;
  6. }

waitStatus
waitStatus >0 代表取消的节点
只有上一个节点是SIGNAL,当前节点才有可能被上一个节点唤醒

  1. // CANCELLED:由于超时或中断,此节点被取消。
  2. 节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。
  3. static final int CANCELLED = 1;
  4. // SIGNAL:此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点
  5. // 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
  6. static final int SIGNAL = -1;
  7. // 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),
  8. 直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
  9. static final int CONDITION = -2;
  10. //传播:应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点)
  11. ,以确保传播继续,即使此后有其他操作介入。
  12. static final int PROPAGATE = -3;
  13. //0:以上数值均未按数字排列以简化使用。非负值表示节点不需要发出信号。
  14. 所以,大多数代码不需要检查特定的值,只需要检查符号。
  15. //对于正常同步节点,该字段初始化为0;对于条件节点,该字段初始化为条件。
  16. 它是使用CAS修改的(或者在可能的情况下,使用无条件的volatile写入)。

image.png

ReentrantLock和AQS的关系

image.png

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

ReentrantLock加锁过程

第一个线程t1 没有加锁之前 ReentrantLock被实例化之后 队列的状态
image.png
lock方法的过程(分为公平和非公平)

  1. 获取当前线程 final Thread current = Thread.currentThread();
  2. 获取锁的状态 getState(); int c = getState();
  3. 判断锁的状态 if (c == 0)
  4. 如果锁是自由状态则第5步,如果不是自由状态则第7步
  5. 公平锁需要判断自己是否需要排队,非公平锁不需要

什么情况下当前线程不需要排队(排队=入队+阻塞)

  1. 队列没有初始 对头和队尾等于null的时候是不需要排队
  2. 队列当中只有一个人的时候是不需要排队的
    1. 如果不需要排队——则cas加锁 成功则直接返回;加锁流程结束,执行临界区的代码
    2. 判断是否重入(一般情况下不重入)如果第6步执行,则没有这一步;这是第4步的分支
    3. 直接返回false(加锁失败,不考虑重入)
    4. 加锁失败之后会调用addWaiter,主要是入队(入队不等于排队)

入队完成之后第一个节点是一个虚拟出来的thread等于null的节点,而不是我们入队的节点?
为什么这里需要虚拟出来一个节点,而不是拿当前节点作为头部?

  1. 判断是否需要自旋

自旋的条件也很简单,判断自己的上一个节点是否头节点

  1. 如果不需要自选则直接park
  2. 如果需要自旋,则再次尝试加锁,失败则park,成功则维护好队列然后返回执行临界区代码

第一种情况

第一个线程t0获取锁的时候 代价基本为0(cas改变锁的状态,继而记录当前持有锁的线程);队列的状态如下图
image.png
t0加锁成功之后
image.png
当t0释放锁之后;队列的状态如下图
image.png

第二种情况

当第一个线程t0释放锁之后第二个线程t1来加锁(t0 和t1是没有竞争执行),代价基本为0,(cas改变锁的状态,继而记录当前持有锁的线程)
image.png
当前状态 这种情况下t1加锁成功后队列状态如下图
image.png
如果t1释放锁则如下图
image.png
总结:如果是线程交替执行(没有竞争) 不管并发量多大 性能都是非常高的,因为仅仅是CAS操作,队列压根就没有

第三种情况

t0没有释放锁这个t1来加锁;因为t0没有释放锁故而队列的情况如下图(注意这个时候是t1来加锁还未成功的时候的状态)
image.png
t1会加加锁失败;返回false,然后调用addWaiter方法区初始化队列,然后自己入队

  1. 把t1封装成为一个node Node node = new Node(Thread.currentThread(), mode);
  2. 调用ENQ方法 enq(node);

    1. private Node enq(final Node node) {
    2. for (;;) {
    3. Node t = tail;
    4. if (t == null) { // Must initialize
    5. if (compareAndSetHead(new Node()))
    6. tail = head;
    7. } else {
    8. node.prev = t;
    9. if (compareAndSetTail(t, node)) {
    10. t.next = node;
    11. return t;
    12. }
    13. }
    14. }
    15. }
  3. 设置一个空的节点为尾部节点 if (compareAndSetHead(new Node()))

  4. 让头部节点和尾部节点都指向这个new出来的空节点 tail = head;
  5. 设置t1-node的上一个节点=new node;node.prev = t;
  6. 设置队列的尾部为t1-node if (compareAndSetTail(t, node))
  7. 设置new-node的下一个节点为t1-node t.next = node;

到此为止 addWaiter方法执行完 t1-node 入队

image.png

  1. 入队成功之后调用acquireQueued

    1. final boolean acquireQueued(final Node node, int arg) {
    2. boolean failed = true;
    3. try {
    4. boolean interrupted = false;
    5. for (;;) {
    6. final Node p = node.predecessor();
    7. if (p == head && tryAcquire(arg)) {
    8. setHead(node);
    9. p.next = null; // help GC
    10. failed = false;
    11. return interrupted;
    12. }
    13. if (shouldParkAfterFailedAcquire(p, node) &&
    14. parkAndCheckInterrupt())
    15. interrupted = true;
    16. }
    17. } finally {
    18. if (failed)
    19. cancelAcquire(node);
    20. }
    21. }
  2. acquireQueued 首先获取t1-node的上一个节点;final Node p = node.predecessor();获取上一个节点

  3. 判断上一个节点是否头节点
  4. 如果是头结点则再次获取锁(一次自旋) if (p == head && tryAcquire(arg))
  5. 如果能够获取到锁表示锁被释放了
    1. setHead(node);//设置t1-node(当前加锁的线程)为头节点
    2. node.thread = null; //t1-node(当前node)的thread=null
    3. node.prev = null;//t1-node(当前node)的上一个节点=null
    4. p.next = null; // help GC 把new-node(上一个节点)的下一个节点指向null,为了gc
  6. t0没有释放锁,t2自旋之后还是没有获取到锁则park 排队

image.png

第四种情况

t2来加锁,t0没有释放锁,t1这个时候已经排队(和第三种情况的区别在于t2入队之后不会自旋,直接排队);为什么不需要自旋?
t2加锁成功之后状态如下图
image.png

RenntrantLock解锁流程

第一种情况

只有一个t0加锁了

当调用unlock解锁的时候

  1. sync.release(1);
  2. tryRelease

int c = getState() - releases; //把锁的状态改为自由状态

  1. if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();

这种情况一般不会出现,就是解锁的线程和持有的线程不一样,则会出异常 除非你没有加锁直接解锁则会异常

  1. boolean free = false; 只是标识一下目前还没有释放锁成功,因为你仅仅把锁改成了自由状态,线程没有释放(而且还有可能是重入),所以这个变量是一个过渡变量
  2. setExclusiveOwnerThread(null) 把持有锁的线程改为null 锁彻底释放了
  3. 返回true
  4. 以上是释放锁,接下来可能需要唤醒队列当中的阻塞线程去获取锁(也有可能不需要唤醒)
  5. Node h = head;//拿到对头
  6. if (h != null && h.waitStatus != 0);//判断是否有对头(是否有人在排队)
  7. 由于当前这种情况(只有t0线程存在)肯定没有人排队则不需要唤醒,则return true 标识解锁成功

第二种情况

就是队列当中有人排队(不考虑有几个人)

  1. 调用tryRelease方法释放
  2. if (h != null && h.waitStatus != 0) 判断是否需要唤醒下一个,当前这种情况肯定需要唤醒一个
  3. unparkSuccessor(h); 这个h对象表示的是头结点,因为Node h = head;把头结点传给unparkSuccessor
    1. compareAndSetWaitStatus(node, ws, 0); 把头结点的ws改为0(因为头结点的ws是-1,这里需要把它为0)
    2. Node s = node.next;得到队列当中第一个排队的人 也就是图上的t1所标识的node对象
    3. LockSupport.unpark(s.thread); //唤醒t1线程
    4. 由于t1在lock方法中被阻塞那么唤醒则也是从lock方法中被唤醒往下执行
    5. Thread.interrupted()这个方法主要干嘛?清除打断标记
      1. private final boolean parkAndCheckInterrupt() {
      2. //简单的唤醒t2
      3. LockSupport.park(this);
      4. //这里为什么需要调用一下Thread.interrupted()
      5. return Thread.interrupted();
      6. }