深入并发-AQS

概念

AQS是什么

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。

AQS 的两种功能

从使用层面来说,AQS 的功能分为两种:独占和共享
独占锁:每次只能有一个线程持有锁,比如 ReentrantLock 就是以独占方式实现的互斥锁。
共享锁:允许多个线程同时获取锁,并发访问共享资源,比如 ReentrantReadWriteLock。

实现

AQS 的内部实现

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
在这里插入图片描述

释放锁以及添加线程对于队列的变化

当出现锁竞争的时候,AQS 同步队列中的节点会涉及到两个变化

  1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节
    点的前置节点的 next 节点指向自己
  2. 通过 CAS 讲 tail 重新指向新的尾部节点

head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点。如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下

  1. 修改 head 节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可。

源码分析

Node的组成

static final class Node {
    
    // 表明节点在共享模式下等待的标记
    static final Node SHARED = new Node();
    // 表明节点在独占模式下等待的标记
    static final Node EXCLUSIVE = null;

    // 表征等待线程已取消的
    static final int CANCELLED =  1;
    // 表征需要唤醒后续线程
    static final int SIGNAL    = -1;
    // 表征线程正在等待触发条件(condition)
    static final int CONDITION = -2;
    // 表征下一个acquireShared应无条件传播
    static final int PROPAGATE = -3;
    // 当前节点状态(正数的时候表不可用,负数代表处于等待状态)
    volatile int waitStatus;
    
    // 前继节点
    volatile Node prev;
    // 后继节点
    volatile Node next;
    // 持有的线程
    volatile Thread thread;
    // 链接下一个等待条件触发的节点
    Node nextWaiter;

    // 是否为共享锁
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 返回前继节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
    // Shared模式下的Node构造函数
    Node() {  
    }

    // 用于addWaiter
    Node(Thread thread, Node mode) {  
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 用于Condition
    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

锁的抢占流程

我们通过 ReentrantLock 的lock() 方法作为切入点进行分析,ReentrantLock 是一个独占锁。

ReentrantLock lock = new ReentrantLock();
lock.lock();

进入 lock() 可以看到是有调用了 sync.lock() 方法,有两种实现方式对应公平锁和非公平锁。

  1. NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
  2. FailSync: 表示所有线程严格按照 FIFO 来获取锁

这里我们以非公平锁进行分析:
3. 非公平锁和公平锁最大的区别在于,在非公平锁中抢占锁的逻辑是,不管有没有线程排队,我先上来 CAS 去抢占一下
4. CAS 成功,就表示成功获得了锁
5. CAS 失败,调用 acquire(1)走锁竞争逻辑

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

这里还需要了解一下 state 这个属性的意义,它是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
6. 当 state=0 时,表示无锁状态
7. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为 ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁

如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续 acquire(1)操作
这个方法的主要逻辑是
8. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
9. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
10. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。

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

可以看下 NonfairSync.tryAcquire 的实现

  1. 获取当前线程,如果 state = 0 则通过 CAS 操作抢占锁。抢锁成功则将独占锁的当前所有者设置为当前线程,并直接返回 true
  2. 判断当前线程是属于重入,是则增加重入次数
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()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

返回 false 则表示当前线程不是独占锁的当前所有者,通过 addWaiter() 方法将当前线程封装成一个 Node 节点

  1. 将当前线程封装成 node 节点,并获取队列的尾结点
  2. 如果尾结点不为空,则通过 CAS 将当前节点设置为尾结点并将之前的尾结点设置为 node 的前置节点
  3. 如果尾结点为空或 CAS 操作失败,则再次重新获取尾结点并判断是否为空
    a. 如果队列为空,则创建一个新的 Node 节点做为头尾节点
    b. 通过自旋的方式将当前线程的 node 节点设置为尾结点
private Node addWaiter(Node mode) {
    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;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

获取到当前线程等着的 Node 节点后,通过 acquireQueued() 进行阻塞等待唤醒获取锁

  1. 获取当前节点的 prev 节点
  2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
  3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点
  4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
  5. 最后,通过 cancelAcquire 取消获得锁的操作
final boolean acquireQueued(final Node node, int arg) {
        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
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

根据 shouldParkAfterFailedAcquire() 进行判断 waitStatus 的值
Node 有 5 中状态,分别是:

  1. CANCELLED(1): 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取
    消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状
    态后的结点将不会再变化
  2. SIGNAL(-1): 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
  3. CONDITION(-2): 当前节点处于条件队列中,它将不能用作同步队列节点,直到其waitStatus被重置为0
  4. PROPAGATE(-3):共享模式下,PROPAGATE 状态的线程处于可运行状态
  5. 0:初始状态

这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是
否应该被挂起。

  1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false.

返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt
挂起当前线程

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);//这里采用循环,从双向列表中移除 CANCELLED 的节点
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

通过 parkAndCheckInterrupt() 方法将当前线程挂起

  1. 使用 LockSupport.park 挂起当前线程
  2. 返回当前线程是否被其他线程触发过中断请求
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

parkAndCheckInterrupt 方法返回true,意味着在线程抢占锁成功后会执行 selfInterrupt() 方法产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求

private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
    return Thread.interrupted();
}

以上就是 AQS 抢占锁及阻塞的源码分析,这类补充一下 LockSupport 类的信息

LockSupport

LockSupport类是 Java6引入的一个类,提供了基本的线程同步原语。LockSupport
实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数

public static void park(Object blocker)
public static void unpark(Thread thread)

unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次 park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积。

锁的释放流程

我们通用通过 ReentrantLock 的 unlock() 方法作为切入点进行分析
在 unlock 中,会调用 release 方法来释放锁

  1. 释放锁成功
  2. 得到 AQS 的 head 节点
  3. 如果 head 节点不为空并且 waitStatus != 0.调用 unparkSuccessor(h)唤醒后续节点
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先我们来看下释放锁的方法 tryRelease

  1. 将重入次数 state 减1,如果独占锁的当前所有者与当前线程不一致则抛出异常
  2. 如果 state = 0 则将独占锁的当前所有者修改为 null
  3. 重新设置 state 的值,返回释放独占锁的结果
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

查看 unparkSuccessor 是如何唤醒后续节点

  1. 获取 head 节点的状态,如果状态小于0则将其设置为0
  2. 获取 head 节点的下一个节点
  3. 如果下一个节点为 null 或者 status>0 表示 cancelled 状态,则通过从尾部节点开始扫描,找到距离 head 最近的一个 waitStatus<=0 的节点
  4. 然后唤醒这个节点对应的线程即可
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);
}

唤醒线程之后就走 acquireQueued 方法的自旋操作,最后就完成了整个锁释放的过程。

为什么在释放锁的时候是从 tail 进行扫描

线程阻塞增加到队列尾节点的方法,过程

  1. 将新的节点的 prev 指向 tail
  2. 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
  3. t.next=node;设置原 tail 的 next 节点指向新的节点

在 cas 操作之后,t.next=node 操作之前。存在其他线程调用 unlock 方法从 head
开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。
就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问
题。

参考:https://www.jianshu.com/p/0f876ead2846

发布了31 篇原创文章 · 获赞 19 · 访问量 4万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 书香水墨 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览