线程池原来是这么玩的

引言

面试官:如果让你设计线程池,你会怎么设计?

小贱:… 发生肾么事情了,面试官你不讲码德。

面试官:出门右拐,坐三轮车走成华大道到二仙桥。

对于线程池,有经验的程序员一定不会陌生,在Java中用Executor框架,啪的一下,很快啊就搞定了。实现一个线程有多种方式,我们为什么要使用线程池呢?

我们为什么要用线程池

在Hotspot虚拟机中,Java线程与操作系统的线程是一一对应,所以线程的创建与销毁都需要与操作系统线程同步。对于cpu密集型的线程任务,这样做无疑是灾难性的,因为频繁的创建与销毁线程会消耗大量的资源,所以我们引入了线程池来对线程进行管理。

线程池主要有以下几个优点:

  1. 通过线程复用,避免频繁创建与销毁线程,节约资源的同时,提高了响应速度;
  2. 通过线程池,方便我们管理线程;
  3. 通过线程池,我们可以控制最大并发数,避免无限制创建线程导致系统资源消耗殆尽,最终导致系统挂掉(限流组件hystrix中就有通过线程池的方式来进行限流)。

其实这种“池化”的思想很常见,比如:数据库中连接池、tomcat连接池,继续回到本文要分析的在Executor线程池。

线程池分析

如下图所示,在Executor框架,关于线程池的主要有两个关键的实现类ThreadPoolExecutorScheduledThreadPoolExecutor,后者也是基于前者实现的,所以本文重点分析前者。

微信公众号:肖说一下 类依赖

老规矩,既然要使用,肯定要先构造一个对象实例。线程池的基本原理要从ThreadPoolExecutor入手,所以我们先扒开ThreadPoolExecutor的源码,可以发现根据参数的不同,它有很多构造函数,最基础的构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

从源码中可以发现,构造函数中参数也很多,下面我们分析这些参数的作用:

  1. corePoolSize: 线程池中核心线程数,当线程池中的线程数量小于corePoolSize时,添加一个任务,会新创建一个线程来执行任务;
  2. maximumPoolSize: 线程池中最大线程数,当工作队列满了以后,如果有新的任务添加进来,线程池会创建新的线程来执行任务。线程池中的线程数大于maximumPoolSize时,新添加的任务会触发线程池的拒绝策略;
  3. workQueue: 工作队列,即线程池的等待队列,当线程池中的线程数量等于corePoolSize时,新添加的任务会被放入等待队列中;
  4. handler: 拒绝策略,框架中已经帮我们定义好了4种拒绝策略:

AbortPolicy:丢弃并抛出异常,线程池默认拒绝策略;
CallerRunsPolicy:用调用者来执行任务,该策略下任务一定会被执行;
DiscardPolicy: 丢弃任务,但是不抛出任何异常;
DiscardOldestPolicy:丢弃队列中最老的任务;
除此之外,还可以自己定义拒绝策略,只需要实现RejectedExecutionHandler接口的rejectedExecution(...)方法即可。

  1. keepAliveTime: 非核心线程(最大线程-核心线程)闲置时最大的存活时间,超过这个时间仍然空闲的话,非核心线程将会被回收;
  2. unit: 非核心线程闲置最大存活时间的时间单位,比如:TimeUnit.SECONDS:秒,TimeUnit.MINUTES:分;
  3. threadFactory: 创建新线程的线程工厂。

综上,我们可以总体流程如下图所示:

微信公众号:肖说一下

具体我们分析源码,任务提交源码如下

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
   
    int c = ctl.get(); //获取线程池状态
    if (workerCountOf(c) < corePoolSize) { //如果workerCount小于核心线程数
        if (addWorker(command, true)) //添加新线程
            return; //成功就返回
        c = ctl.get(); //失败就再次获取线程池状态
    }
    //如果workerCount大于等于核心线程数,判断线程池状态,并添加任务到等待队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
    //如果workerCount大于maximumPoolSize,拒绝策略
        reject(command);
}

从代码中可以发现,任务提交就主要思想就是:根据线程池状态,判断是否应该新开线程、添加到队列还是应该拒绝,基本上和流程图吻合。

接着我们继续看看添加任务的方法:addWorker(...)

//workers集合
private final HashSet<Worker> workers = new HashSet<Worker>();

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c); //获取线程池状态
        // Check if queue empty only if necessary.
        //线程池状态大于等于SHUTDOWN,说明已经进入关闭阶段,所以不应该添加
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;//当线程数大于corePoolSize或者maximumPoolSize 直接返回false
            if (compareAndIncrementWorkerCount(c)) //CAS添加线程数
                break retry; //跳出retry循环
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs) //如果线程池状态发生改变,重新retry循环
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    
    //前面已经完成了workerCount+1  下面才开始添加新的worker
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        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);//将woker加入到work集合
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start(); //如果添加成功,则启动线程
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted) //如果加入失败
            addWorkerFailed(w); //将前面的workerCount数-1
    }
    return workerStarted;//返回是否添加成功
}

该方法有两个参数Runnable firstTask表示任务, boolean core 布尔类型变量core如果为true表示添加核心线程,否者表示添加普通线程。整体分为两大部分:

  1. 通过CAS 将workerCount加1;
  2. 添加worker对象到wokers集合。

从上述分析中,我们可以发现,任务提交时,会判断线程池是否处于RUNNING状态,那么线程池的状态有哪些?又是如何表示的呢?(小朋友你是否有很多问号)

在源码中有一个巧妙的设计,就是ThreadPoolExecutor的成员变量ctl,这是一个原子类型的复合整型变量,其底层通过CAS和volatile来保证线程安全,其中前3位表示线程池状态,后29位表示有效工作线程数,结构如下图所示。

微信公众号:肖说一下

相关源码如下:

微信公众号:肖说一下

从源码中可以看到线程池的状态用整型变量表示,一共有:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED五种状态,具体的整型值还需要通过位计算得到,看上去很复杂,但是具体的值我觉得根本不用去记,只需要抓住重点就好,有以下几个重点:

  1. RUNNING状态值是唯一为负的状态值,可以简单记为-1,相应的SHUTDOWN可以记为0,STOP记为1,以此类推;
  2. 线程池状态的迁移,只能从小到大,不能逆向迁移,即可以从-1->0,但是不可能0->-1。

具体的状态迁移如下图所示:

微信公众号:肖说一下

shutdown()和shutdownNow()的区别及实现原理

从上图中我们可以发现,调用shutdown()或shutdownNow()方法后,线程池并不会马上关闭,而是需要一个过程,最终调用terminated()函数后才会真正关闭线程池,那么shutdown()和shutdownNow()有什么区别呢?

先说结论:

  1. shutdown()不会清空任务队列,会等待所有任务执行完成,shutdownNow()会清空任务队列;
  2. shutdown()只中断空闲线程,shutdownNow()会中断所有线程。

接着我们再通过源码分析下它们的实现原理,源码如下:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock(); //获取锁
    try {
        checkShutdownAccess();//检查是否有关闭线程池权限
        advanceRunState(SHUTDOWN);//把线程池状态设为SHUTDOWN
        interruptIdleWorkers(); //中断空闲线程
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();//释放锁
    }
    tryTerminate();
}

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();//获取锁
    try {
        checkShutdownAccess();//检查是否有关闭线程池权限
        advanceRunState(STOP);//把线程池状态设为STOP
        interruptWorkers();//中断所有线程
        tasks = drainQueue();//清空队列
    } finally {
        mainLock.unlock();//释放锁
    }
    tryTerminate();
    return tasks;
}

其实两个方法的执行过程还是比较简单的,根据上面讲的线程池生命周期及区别能比较好的理解,关键区别在于interruptIdleWorkers()interruptWorkers()这两个方法,前者只中断空闲线程,后者中断所有线程。 线程池是如何判断一个线程是否是空闲线程的呢?源码下面无秘密,接着扒源码,如下:

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();//加锁
    try {
        for (Worker w : workers) { //遍历所有的Worker对象
            Thread t = w.thread;//取出对象中的线程
            //w.tryLock()尝试获取锁,如果获取成功则说明线程空闲,
            //获取失败则说明线程正在执行某个任务
            if (!t.isInterrupted() && w.tryLock()) { 
                try {
                    t.interrupt(); //中断线程
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock(); //释放锁
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();//释放锁
    }
}

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
            w.interruptIfStarted(); //不尝试获取锁,一律直接发送中断信号
    } finally {
        mainLock.unlock();
    }
}

从上面可以看到,判断一个线程是否为空闲线程的关键在于:Worker对象是否能加锁成功,如果能加锁成功,则说明是空闲线程,如果加锁失败则说明正在执行某个任务。所以我们盲猜一下,Work对象在进行任务获取时,肯定会先获取锁,那么Worker到底是什么呢?

private final class Worker extends AbstractQueuedSynchronizer
    implements Runnable{
    ...
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    public void run() {
        runWorker(this);
    }
    ...
}

朋友们,看到Worker继承了AbstractQueuedSynchronizer是不是恍然大悟了,这玩意继承了AQS啊,AQS我应该不用多讲了吧,不知道的可以看看,所以它的本质就是一把锁,上面代码中只摘录了部分源码:

  1. 构造函数:可以看到把锁的状态置为-1,并把任务作为firstTask赋给该Worker。
  2. run()方法:这个方法是比较关键的一个方法,通过该方法执行任务,是通过调用ThreadPoolrunWorker(...)方法来实现。
任务执行过程分析

源码如下所示:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;//获取任务
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
        //通过getTask()不断从队列中获取任务执行
            w.lock();//执行任务前要先加锁,和我们猜的一模一样
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);//任务开始前的钩子函数
                Throwable thrown = null;
                try {
                    task.run();//执行任务的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++;//成功完成任务,completedTasks+1
                w.unlock();//释放锁
            }
        }
        completedAbruptly = false;//判断worker是否正常退出
    } finally {
        processWorkerExit(w, completedAbruptly);//worker退出
    }
}

总结任务执行过程:

  1. 获取worker对象的task,如果为空,则通过getTask()去队列中获取,如果获取不到,则一直阻塞,直到获取到任务或者被中断,如果是被中断则转到步骤8,且记录work为非正常退出;
  2. 获取到任务后,work对象获取锁;
  3. 判断当前线程池状态,如果是STOP状态则不允许执行任务;
  4. 执行任务开始前的钩子函数;
  5. 调用任务run()方法执行任务;
  6. 执行任务完成后的钩子函数;
  7. 累加成功任务数,释放锁;
  8. worker退出。
任务获取

在上面的过程中,总体流程都比较清晰,其中重要的两个方法是获取任务的getTask()和work退出的processWorkerExit(...),接着我们再分析这两个方法,源码如下:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get(); //获取ctl
        int rs = runStateOf(c); //获取线程池状态
        // Check if queue empty only if necessary.
        //关键点 1
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();//CAS 减少线程数
            return null;//return null 则 runWorker(..)中退出while循环
        }
        int wc = workerCountOf(c); //获取工作线程数
        // Are workers subject to culling?
        //allowCoreThreadTimeOut 是否允许核心线程超时,默认为false
        //timed 表示需不需要做超时控制
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        
        //如果工作线程数>maximumPoolSize 或者 超时且需要做超时控制
        //且工作线程数>1,且工作队列为空
        if ((wc > maximumPoolSize || (timed && timedOut)) //关键点2
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;//return null 则 runWorker(..)中退出while循环
            continue;
        }
        try {
        //关键点 3
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
                //如果获取到任务,返回任务
            if (r != null)
                return r;
                //超过超时等待都没有获取到任务
            timedOut = true;
        } catch (InterruptedException retry) {
            //中断设置为false,进入下一次循环
            timedOut = false;
        }
    }
}

在源码中有两个比较关键的地方:

  1. 根据线程池状态rs判断是否应该返回null:

如果rs >= SHUTDOWN,说明调用了shutdown()方法且此时等待队列为空workQueue.isEmpty(),则说明没有任务可以获取,返回null;

如果rs >= STOP,即调用了shutdownNow()方法,此处返回为空。

当getTask返回null 则 runWorker(…)中退出while循环。

  1. 通过关键点2处的判断,来控制线程池中线程的数量尽量维持在corePoolSize内;

  2. 队列为空时,poll(…)或者take()就会阻塞,它们的区别就是poll()带超时,take()不带;一旦捕获到异常,就会响应中断。

总结任务获取过程:

  1. 首先通过一些方法获取线程池相关信息,并根据线程池状态判断是否返回为空;
  2. 通过一系列判断来维持线程池中线程的数量最好在核心线程数以内;
  3. 通过阻塞队列获取任务,如果为空则进行阻塞,并会响应中断。

流程图如下:

微信公众号:肖说一下

Worker对象退出

接着分析Wokrer对象的退出方法:processWorkerExit(...)

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        //当非正常退出线程将线程数减1
        decrementWorkerCount();
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);//把自己从workers中移除
    } finally {
        mainLock.unlock();
    }
    //和shutdown()中一样,尝试调用这个方法将线程池关闭
    tryTerminate();
    
    int c = ctl.get();
    
    //当要退出时,检查线程池状态如果小于STOP,且队列不为空,
    //但是当前没有可以执行的工作线程,
    //则添加一个新的线程去消耗队列中的任务
    //起到一个保证线程池中的正常任务一定会被执行的作用
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

总结woker退出时,清理工作:

  1. 判断是否为非正常退出,如果是非正常退出则通过CAS将线程数-1,正常退出的线程在getTask()中已经通过CAS将线程数减1;
  2. 会尝试调用tryTerminate()方法来关闭线程池;
  3. 判断当前线程池是否需要新添加线程来保证未执行的任务会被执行。
线程池关闭

在线程池的生命周期中我们也提及到线程池最终会通过terminated()钩子函数来关闭线程池,在shutdown(), shutdownNow()及processWorkerExit(…)三个方法中都通过tryTerminate()来关闭线程池,所以接着分析线程池的关闭方法tryTerminate()

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();//获取线程池状态
        //如果线程池状态为RUNNING直接返回,不需要关闭
        //如果线程池状态大于TIDYING,已经到了关闭,不需要关闭,直接返回
        //如果是SHUTDOWN状态,且等待队列不为空,说明还需要执行任务,不需要关闭,直接返回
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
            
        //workerCount大于0,不能停止线程池,需要唤醒那些等待的空闲线程
        if (workerCountOf(c) != 0) { // Eligible to terminate
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        //当等待队列为空且workerCount为0,且等待队列为空时
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {   //通过CAS改变线程池状态为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    terminated();//钩子函数关闭线程池
                } finally {
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}
如何优雅的关闭线程池

从上面的分析我们已经知道,线程池的关闭是一个过程,那么我们应该如何优雅的关闭线程池呢?

先看怎么用

executorService.shutdown();
//或者
executorService.shutdownNow();
try {
    boolean loop = true;
    do {
        loop = !executorService.awaitTermination(2, TimeUnit.SECONDS);
        //阻塞, 直到线程池里所有任务结束
    } while (loop);
} catch (InterruptedException ex) {
    ...
}

可以看到我们可以调用awaitTermination(...)方法来等待线程池关闭。

private final Condition termination = mainLock.newCondition();

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

可以看到awaitTermination(...)方法很简单,两个参数:timeout是阻塞时间,unit是时间单位。内部的逻辑也很清晰,就是不断循环判断线程池是否已经达到了最终TERMINATED状态,是就返回true,不是的话通过条件变量termination来阻塞一段时间,苏醒继续判断。

Executors工具类提供的线程池

为了便于我们开发使用线程池,Executors工具类提供了几个线程池工程方法,具体如下:

public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                        0L, TimeUnit.MILLISECONDS,
                          new LinkedBlockingQueue<Runnable>());
    }
    
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
    }
    
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
   }
   
   public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
    }
}
  1. newFixedThreadPool(int nThreads):固定大小的线程池

a. corePoolSize等于maximumPoolSize,即该线程池全部是核心线程;
b. 该线程池的等待队列为LinkedBlockingQueue为无界队列,使用时可能会出现不断往队列里添加任务导致系统资源耗尽,最终OOM。

  1. newSingleThreadExecutor():核心线程数和最大线程数均为1的固定大小线程池

同样,该线程池的等待队列为LinkedBlockingQueue为无界队列,使用时可能会出现不断往队列里添加任务导致系统资源耗尽,最终OOM。

  1. newCachedThreadPool():可缓存的线程池

a. 该线程池中没有核心线程,最大线程数为Integer.NAX_VALUE,基本相当于无限大,即有需要就会创建线程来执行任务,没有需要就回收线程;
b. 该线程池等待队列为SynchronousQueue,该队列本身没有容量,一个线程调用put()会阻塞,直到另一线程调用get(),然后两线程同时解锁,所以使用时需要注意,如果生产速度大于消费速度,会导致创建很多线程,最终OOM。

  1. newWorkStealingPool():一种基于ForkJoinPool线程池实现的线程池。

综上: 我们在使用线程池时,最好使用new ThreadPoolExecutor(…)的方式来构造,这样做到线程池状态心里有数从而可控,避免无界队列或者创建大量线程导致资源耗尽,导致OOM,最终影响系统。

看到这里了,各位大帅逼和大漂亮就来个素质三连,点赞,评论,关注吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值