ThreadPoolExecutor逻辑链梳理 - 4 Worker的AQS属性

【从问题开始】

在使用线程池的时候往往会看到线程池reject相关日志,例如以下日志:

[Running, pool size = 80, active threads = 3, queued tasks = 3, completed tasks = 1674]

遂产生疑问:为什么线程池大小为80,队列长度为3的线程池,只有3个active threads就会触发reject机制?

 在第二篇分析拒绝处理机制时了解到:

  • nactive:活跃线程数量,实际上是取的worker中处于isLocked状态的数量。

同时在第三篇分析addWorker方法时发现线程池实际将Runnable的执行委托给了Worker。结合场景,需要进一步了解Worker在线程池中扮演的角色。

先看Worker类的声明:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable

可以发现,Worker继承自AQS,实现Runnable接口,基本可以了解Worker本质是一个带锁的可执行对象。

看Worker的核心成员变量:

// 负责执行的线程
final Thread thread;
// Worker要执行的任务
Runnable firstTask;
// Worker执行完成的任务,具备线程统计的能力
volatile long completedTasks;

同时还有一部分继承自AQS的成员变量:

// 阻塞队列的头节点
private transient volatile Node head;
// 阻塞队列的尾节点
private transient volatile Node tail;
// 锁状态
private volatile int state;
// 继承自AbstractOwnableSynchronizer的持锁对象变量
private transient Thread exclusiveOwnerThread;

再看Worker的构造函数:

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

既然Worker继承自AQS,那么一定会重写AQS锁竞争释放相关的代码,同时继承AQS状态相关的属性。在构造函数中就可以看出这一点。setState方法首先将AQS状态设置为-1。这里先放下state不看,后面的firstTask即线程池execute方法传入的Runnable任务,thread是新建的执行线程

Worker中与AQS相关的代码如下:

// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
    return getState() != 0;
}
protected boolean tryAcquire(int unused) {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}
protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
}
public void lock()        { acquire(1); }
public boolean tryLock()  { return tryAcquire(1); }
public void unlock()      { release(1); }
public boolean isLocked() { return isHeldExclusively(); }

isLocked方法和isHeldExclusively方法在第二篇中已经见过一面

for (Worker w : workers) {
    ncompleted += w.completedTasks;
    if (w.isLocked())
        // 这里可知nactive实际取的是加锁成功的worker数量,而非有一个算一个
        ++nactive;
}

[Running, pool size = 80, active threads = 3, queued tasks = 3, completed tasks = 1674]

因此实际上在日志中输出的active threads是锁state状态不为0的Worker。

那么state的值都有什么呢?

可以看到lock方法实际上调用的是AQS中的acquire方法,tryLock调用的是子类覆盖的tryAcquire方法,实际在AQS中调用的底层方法都是tryAcquire方法

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

在AQS中acquire方法的作用是先尝试调用子类的tryAcquire方法,如果获取不到锁,则将AQS节点加入到AQS阻塞队列中,同时尝试取出阻塞队列的队头,开启一个死循环尝试获取锁,如果获取不到,将节点的状态修正为SINGNAL(等待唤醒)同时对持有线程做park操作,等待持锁线程调用tryRelase方法将锁释放,同时唤醒队头的持有线程,继续循环获取锁。

注:这里的节点状态是AQS中的Node节点状态,即waitStatus属性,而非AQS的同步锁状态state属性,这里是一个非常容易混淆的地方。state属性是前面提到的Worker统计isLocked时使用的属性,同样也是后面加锁和释放锁使用的属性。

对于AQS的操作流程后续另行记录。此处先看Worker对tryAcquire的实现。

既然AQS将tryAcquire交给子类实现,正体现了锁的多样性。Worker对其实现就是典型的不可重入锁。

// Worker的不可重入锁实现
protected boolean tryAcquire(int unused) {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

// ReentrantLock类公平锁的tryAcquire实现
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    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;
}

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

这里对比ThreadPoolExecutor中Worker类和ReentrantLock中FairSync公平锁的实现。

Worker是典型的不可重入锁,使用CAS判断State状态为0,即设置为1,表示已加锁成功,然后设置持锁线程为当前线程,即说明已加锁成功,返回true

FairSync是典型的可重入锁,首先获取自己的state,这里有两个场景:

  • 如果是0,还需要判断此时队列中已没有阻塞的等锁节点(这里是公平锁和非公平锁的差异,公平锁需要判断阻塞队列无任务等锁,非公平锁则无需判断队列,直接自己尝试获取),满足条件,执行CAS判断当前状态是0,修正为acquires,从方法的调用方可以看出,acquire传入是1,即由0变为1。变更成功则设置持锁线程为自身,返回true
  • 如果state不是0,这里要先判断持锁线程是不是自身,如果是自身,拿到的state与传入的acqires相加,即state += 1,然后再设置state值。这就实现了每次当前线程拿锁,state值自增1.

同样的,Worker和FairSync的tryRelase方法实现如下:

// Worker中的不可重入的tryRelease方法实现
protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
}

// ReentrantLock公平锁的可重入的tryRelease方法实现
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;
}

public void unlock() {
    sync.release(1);
}

Worker的tryRelease方法实现很简单,把持锁线程置空,把state置0即可。

ReentrantLock公平锁的tryRelease方法则同样针对可重入的特性,获取state减掉release,从调用点来看即state -= 1,直到state为0时,才判断为已释放锁,设置持锁线程为null。

综上我们可以得出结论:对于不可重入锁Worker,state状态有初始化时的-1、未加锁状态0、加锁状态1。因此nactive实际体现的是1状态的数量。-1状态实际上初始化后会被立即抹掉。这就需要继续了解Worker的Runnable特性了。

由此,今天的分析也引出了以下几个问题:

由此我们提出以下几个问题,作为后续分析的方向:

  1. Runnable特性是如何运作的?
  2. Worker的state成员变量的变化流程是什么样的?

本篇结论:

  1. Worker继承AQS同时实现了Runnable接口,本质上是一个带锁的可执行对象。
  2. Worker的state有-1、0、1三种状态,而线程池对于Worker的统计,active worker数量实际是state为0的状态
  3. Worker是一个不可重入锁,实现了锁相关的tryAcquire和tryRelease方法。
  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值