记一次线程池引发的问题

102 阅读3分钟

1.背景

线程是写程序过程中处理并发和异步的利器,为了提高资源的利用率,一般都会使用线城池重复利用线程资源(每一次使用线程都向CPU申请线程,申请过程会使用中断,将程序由用户态切换到内核态,然后由内核给线程分配相应线程资源,是一种极大的浪费);

2.线程池的基本知识

2.1 线程池的主要参数

java 中的线程池主要使用类ThreadPoolExecutor,构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize: 核心线程数量
  • maximumPoolSize: 线程池拥有的最大worker的数量
  • keepAliveTime: 最大空闲生存时间
  • unit: keepAliveTime的单位
  • workQueue: 线程池的阻塞队列,用户排队或缓存任务
  • threadFactory: 线程的工厂类,构造线程
  • handler:拒绝策略,当队列已满,线程池不能再继续新增woker数量时,执行拒绝策略;ThreadPoolExecutor中的默认实现有四种; AbortPolicy, DiscardOldestPolicy,DiscardPolicy, CallerRunsPolicy,根据名称可以很容知道具体的实现,默认是 AbortPolicy:抛出 RejectedExecutionException 异常;

2.2 execute方法

int c = ctl.get(); // 线程池状态和线程数量的结合
if (workerCountOf(c) < corePoolSize) { // 若是当前线程数量 < 核心线程数
    if (addWorker(command, true)) // 向线程池中添加worker,并且执行command 任务
        return;                   // 若是成功,那么执行返回;有可能失败,因为多线程添加,到当前线程添加的时候,可能核心线程已经满了
    c = ctl.get(); // 添加worker失败;此时需要再次获取当前线程池的状态 c
}
if (isRunning(c) && workQueue.offer(command)) {  //判断当前线程池的状态是否为Running && 当前队列中是否能插入任务,如果不能插入,则会返回fasle
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command)) // 再次检查线程状态,如果不是runing,则移除掉刚刚插入的元素(任务)
        reject(command); // 执行拒绝策略(防止多线程时,线程池被调用了shutdown(), 导致线程terminated 方法
    else if (workerCountOf(recheck) == 0) // 若是线程池中已经不存在worker了
        addWorker(null, false);
}
else if (!addWorker(command, false))  // 队列中不能再添加任务,则执行添加worker,若是不能添加,执行拒绝策略
    reject(command);

2.3 addworker 方法

总结功能是,新建Worker,并且将新建的worker添加到workers队列中,然后启动worker;
核心代码如下:

...省略
w = new Worker(firstTask);  //新建一个Worker
final Thread t = w.thread;
if (t != null) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // Recheck while holding lock.
        // Back out on ThreadFactory failure or if
        // shut down before lock acquired.
        int rs = runStateOf(ctl.get());

        if (rs < SHUTDOWN ||
            (rs == SHUTDOWN && firstTask == null)) {
            if (t.isAlive()) // precheck that t is startable
                throw new IllegalThreadStateException();
            workers.add(w);                 // 将新建的worker加入workers队列中
            int s = workers.size();
            if (s > largestPoolSize)
                largestPoolSize = s;
            workerAdded = true;
        }
    } finally {
        mainLock.unlock();
    }
    if (workerAdded) {
        t.start();                     // 如果worker添加成功,启动线程执行任务
        workerStarted = true;
    }
}
...省略

2.4 Worker

Worker是Thread和Task的包装,构造方法如下,注意thread中传入的任务是this,也就是当前Worker

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this); // 将this作为线程的接收参数
}

线程启动会调用Worker#run()方法
主要逻辑是循环调用getTask()从阻塞队列中获取Task,然后触发任务执行

public void run() {
    runWorker(this);
}
final void runWorker(Worker w) {
    ...
// 两种情况:1.任务初始化,task != null,直接进入循环执行任务;
// 之后都是从 getTask() 获取任务执行;当前线程抛出异常时会向上抛异常,并且结束当前线程; 
//当新的任务进来时,又通过ThreadFactory生成新的Thread,组装成Worker放入workerQueue
while (task != null || (task = getTask()) != null) { 
    w.lock();
    try {
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
            task.run();
        } catch (RuntimeException x) {
            thrown = x; throw x;
        } catch (Error x) {
            thrown = x; throw x;
        } catch (Throwable x) {
            thrown = x; throw new Error(x);
        } finally {
            afterExecute(task, thrown);
        }
    } finally {
        task = null;
        w.completedTasks++;
        w.unlock();
    }
}

ThreadPoolExecutor#getTask()

核心线程会卡在 workerQueue.take(),一直拿任务,非核心线程会是用poll(Timeout, TimeUnit),拿不到就放弃了?

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

3.遇到的问题

设置参数 corePoolSize = 200, maxPoolSize = 200, workerQueue = LinkedBockingQueue(1); 由以上分析,知道如果当前缓存的worker数量已经达到200,则会进入队列,然后worker线程将任务从队列中取出来执行;任务取出和任务加入是在不同线程中,由于任务队列较小,所以一次性提交任务多个,worker线程还没有来得及取出任务;则会查看maxPoolSize=200,说明无法再申请新的线程执行任务,导致线程池执行拒绝错误;所以你会发现一个奇怪的现象,线程池存在空闲的线程,但依旧会执行抛出任务拒绝错误