AQS源码简单理解一:思路和组件

1. ReentrantLock 使用方式

ReentrantLock.java 中给的 ReentrantLock 的使用方式(好多教程中推荐的写法估计也是从这来的吧)。

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...

    public void m() {
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock()
        }
    }
}

lock.lock() 是加锁,lock.unlock() 锁释放,try{...} 是被加锁的业务逻辑。第三部分是开发者自己写的,那加锁、解锁内代码是怎么执行的呢?

2. 理下思路

在了解加锁、解锁内部代码之前,先来理下思路:

  • 给线程加锁的目的是为了实现多线程同步;
  • 多线程同步的意思是:当一个线程调用代码块 (方法中的一部分代码)时,在没有得到结果之间,这个调用不返回,同时其他线程也不能调用该代码块。
  • 所以给代码块加锁,就是让多线程在该代码块上的并行变成串行,某一时刻只有能有一个线程在执行该代码块,剩余的其他都处于等待状态。

那处于等待状态的线程都在哪里等呢?当前线程怎么知道它是该执行还是该等呢?处于等待状态的线程什么时候(或者说怎么才能知道它要)执行代码块呢?

在 ReentrantLock 内部有一个同步器,它里面维护了个队列用于存放需要等待的线程,有个字段用于标记当前同步器中是否有线程正在执行代码块。当前线程是否需要等待,队列中的等待线如何调度,都由这个同步器负责,并保证某一时刻至多有一个线程在执行代码块。

3. 两个核心组件

3.1 ReentrantLock 里面都有什么。

public class ReentrantLock implements Lock, java.io.Serializable {
    // 两个字段。
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync;
    // 三个内部类。
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    static final class NonfairSync extends Sync {}
    static final class FairSync extends Sync{}
    // 剩下的都是方法。
    public void lock() { sync.lock();}
    public void unlock() {sync.release(1);}
    // ..... methods ..... //
}

serialVersionUID 是序列化用的,这里先不用管。

sync 就是上面提到的同步器,它有两个子类FairSyncFairSync。公平锁和非公平锁就是从这出来的,具体的区别后面再讲。与其说是公平锁非公平锁,更确切的描述是共平同步器和非公平同步器。 看下 ReentrantLock 的构造函数:

public ReentrantLock() {
    sync = new NonfairSync();
}

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

公平锁和非公平锁其实指的是 new ReentrantLock 时,里面的同步器是公平和还是非公平的。默认是非公平的,开发者也可以通过传递参数创建公平锁。

ReentrantLock 在构建对象时只创建了同步器,而且从代码里也可以看出,加锁、解锁(还有其他)的工作实际上都交给了同步器去执行。

3.2 AbstractQueuedSynchronizer 里面有什么

AbstractQueuedSynchronizer 就是常被念叨的 AQS,字面意思:抽象队列同步器,如果忘了 AQS 的大体作用,可以再瞥一眼第 2 部分。

public abstract class AbstractQueuedSynchronizer 
    extends AbstractOwnableSynchronizer 
    implements java.io.Serializable {
    // 个内部类
    static final class Node {}
    // 部分关键字段
    private transient Thread exclusiveOwnerThread;
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
    // 部分关键方法
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

队列是是由一个一个的 Node (节点)作元素组成的,每一个线程在代码中的表现形式就是一个 Node。

static final class Node {
    // 两个引用(指针)。
    volatile Node next;
    volatile Node prev;
    // 线程。
    volatile Thread thread;
    // 线程的状态。
    volatile int waitStatus;
    // 还有其他的字段
}

链表中的节点由两部分构成,指针段和数据段。指针段负责表示当前节点的前驱结点和后继节点在哪里,数据段存放当前数据。thread 表示某一个等待的线程,waitStatus表示该线程的等待状态。一个 thread 对应一个 node。

exclusiveOwnerThread 是 AQS 从父类 AbstractOwnableSynchronizer 继承来的,它表示当前正在执行代码块的线程。

head 指向头节点,tail 指向尾节点,当队列为空时,它俩的值都是 NULL。

state 表示同步状态,status == 0 表示当前时刻队列中没有线程在执行代码块,status == 1 表示当前时刻队列中有线程正在执行代码块。

compareAndSetState 专门用来更新 status 的值。

3.3 小结一下:

开发者调用 ReentrantLock 业务代码执行加锁、解锁,ReentrantLock 将具体的执行交给了 AbstractOwnableSynchronizer,而 AQS 内部维护着线程队列并进行调度,从而实现线程同步。此时得到了个大体的 AQS 框架,一个很 fu qian 的理解。

4. 一个完美的多线程并发。

当多线程并发执行的时候,会出现两种情况:

  1. 对加锁代码块有竞争,此时需要等待确保同步执行,花费稍微长一点时间。
  2. 对加锁代码块没竞争,运气特别好,在加锁代码块上恰好是一个线程执行完毕,另一个线程开始执行。这个是最完美的情况(虽说发生概率极小)。

看下在多线程没竞争的场景下,AQS 细节上是怎么执行的呢?

//套用开头的模板.
@Slf4j
public class SourceCode {
    // 公平锁
  private static ReentrantLock reentrantLock = new ReentrantLock(true);
  public static void main(String[] args) {
    reentrantLock.lock(); // 加锁
    try {
      log.debug("processing ...");  // 处理业务逻辑
    } finally {
      reentrantLock.unlock();  // 解锁
    }
  }
}

假设第一个线程 T1 执行到第六加锁:–> ReentrantLock.java

public void lock() {
    //委托 sync 加锁
    sync.lock();
}

–> ReentrantLock.FairSync.class

final void lock() {
    acquire(1);
}

–> AbstractOwnableSynchronizer.class,实际上依旧是 ReentrantLock.FairSync.class。FairSync 继承了AbstractOwnableSynchronizer,所以 acquire(int arg) 方法也在 FairSync 里面。

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

此时,arg == 1 先执行 tryAcquire(arg) 尝试请求锁(实际上是当前线程请求同步器,让自己执行代码块)。

–> ReentrantLock.FairSync.class

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程。
    final Thread current = Thread.currentThread();
    // 获取同步器状态。
    int c = getState();
    // c(state) == 0 表示没有线程正在执行代码块。
    if (c == 0) {
        if (!hasQueuedPredecessors() &&         // 判断队列中有没有前驱节点。
            compareAndSetState(0, acquires)) {  // 将 status 标记为 1。
            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;
}

T1 怎么才能拿到锁呢(让同步器允许自己执行代码块)?3 个条件:

  1. 同步器的 state == 0 成立。T1 是第一个线程,state 当然等于 0。所以这个条件是成立的。

  2. hasQueuedPredecessors == false 成立,T1 场景成不成立。

    // AbstractOwnableSynchronizer.java
    public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    

    new FairSync() 用的是无参构造函数,所以 headtail 都是默认值 NULL,换个层面想,T1 是第一个线程,之前哪有线程入队呀。即 h != tfalse&& 后面的代码就不执行了。所以 !hasQueuedPredecessors()true

  3. compareAndSetState(0, acquires)== true 成立,这是 cas 操作 T1 是第一个线程,所以执行肯定成功。

三个条件均满足,所以 T1 可以执行加锁代码块(拿锁成功),tryAcquire(int acquires) 返回 True

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

(上面的代码挪下来)接下来,!tryAcquire(arg) == falseif{...} 代码块不会执行, acquire(int arg) 返回。

final void lock() {
    acquire(1);
}

lock() 返回。

@Slf4j
public class SourceCode {
    // 公平锁
  private static ReentrantLock reentrantLock = new ReentrantLock(true);
  public static void main(String[] args) {
    reentrantLock.lock(); // 加锁
    try {
      log.debug("processing ...");  // 处理业务逻辑
    } finally {
      reentrantLock.unlock();  // 解锁
    }
  }
}

reentrantLock.lock(); 执行完毕,开始执行 try{...} 中的业务逻辑,这样 T1 就拿到了锁,可以执行加锁代码块。

当 T1 释放锁后,T2 紧接着开始拿锁。T2 依旧是重复了 T1 的过程,队列依旧是空,T3 、T4 也是同样的过程。这样看,多线程但交替执行,reentrantLock.lock();只不过是多执行了一行代码,比 1.6 版本之前的 synchronized(obj){...} 在同样交替场景下的确是快不少。

4.1 两个小问题

拿锁的判断条件是 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)

问题一:hasQueuedPredecessors() 为什么要判断队列中是否有前驱节点?

上面流程的前提条件是并发但交替执行,但实际中大概率出现竞争。有个场景:T1 正在执行加锁代码块,T2、T3 在同步器的队列中等待,此时 T4 执行到 c = getstate() 恰好遇到 T1 释放锁了,T4 拿到 C == 0 。如果没有前驱判断,那 T4 就执行代码块了,但实际上应该要轮到 T2 执行,T4 入队等待。所以当线程拿到 c == 0 是还需要判断前驱节点由没有。

问题二:!hasQueuedPredecessors() && compareAndSetState() , 为什么要在这 && ?

假设当前同步器队列是空的,T2 、T3 挨的时间很近,几乎同时拿到 c == 0,两线程都判断队列是空,现在就抢谁更快一些,先标记 state == 1 ,慢的那个则 cas 执行失败,当然就进入等待。如果不将 cas 写在判断条件中,而是直接在 if 块中赋值,那么慢的线程即便 cas 失败,也同样能执行加锁代码块。

5. 公平锁和非公平锁的区别

前面提到过,公平锁和非公平锁内部实际上指的是公平同步器和非公平同步器。那这两个锁有什么区别呢?

他们之间的区别在两个地方:
第一处:lock() 方法中:
非公平锁在执行 lock() 时,先尝试让当前线程加一次锁。加锁失败后再走和公平锁一样的加锁流程。

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

第二处:在执行 acquire(1) 方法中的 tryAcquire(int acquires) 时,

如果当前同步器的状态 state == 0 , 非公平锁会直接让线程加锁成功,而公平锁只有再确认同步器中没有前驱节点时,才会让线程加锁成功。

// NonfairSync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 当 state == 0 时
    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;
}
// FairSync
protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     // 当 statue == 0 时
     if (c == 0) {
     	// 还要确认当前等待队列中没有前驱节点,才允许让线程加锁。
         if (!hasQueuedPredecessors() &&
             compareAndSetState(0, acquires)) {
             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;
 }

非公平锁看到state == 0就要执行加锁,完全不顾及等待的线程,不讲武德。

如果是非公平锁,T1 正在执行,T2、T3在队列中等待,T4 如果执行到 compareAndSetState(0, 1) 前一瞬间恰好 T1 释放了锁,T4 直接加锁并执行代码块。

(后面再写并发竞争的加锁流程,估计要写好长)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值