Java线程池满了是什么原因?如何解决Java线程池满了情况?

Java线程池在项目中起到的作用真的是太大了,java线程池设置不管是大或者小都会对开发有影响,所以开发人员要合理设置java线程池,那Java线程池满了是什么原因?下面来我们就来给大家讲解一下。

Java线程池满了是什么原因.jpg

造成线程池爆满的原因自然是某些线程执行耗时太久,一直占用线程池。

如何解决Java线程池满了情况?

首先看图:

Java线程池满了是什么原因?如何解决Java线程池满了情况?.png

public static ExecutorService newFixedThreadPool(int nThreads)
{
    return new ThreadPoolExecutor(nThreads, nThreads
        , 0 L, TimeUnit.MILLISECONDS
        , new LinkedBlockingQueue < Runnable > ());
}

Java的Executors框架提供的定长线程池内部默认使用LinkedBlockingQueue作为任务的容器,这个队列是没有限定大小的,可以无限向里面submit任务。

当线程池处理的太慢的时候,队列里的内容会积累,积累到一定程度就会内存溢出。即使没有内存溢出,队列的延迟势必会变大,而且如果进程突然遇到退出信号,队列里的消息还没有被处理就被丢弃了,那必然会对系统的消息可靠性造成重大影响。

那如何解决线程池的过饱问题呢?从队列入手,无外乎两种方法

1.增加消费者,增加消费者处理效率

2.限制生产者生产速度

增加消费者就是增加线程池大小,增加消费者处理效率就是优化逻辑处理。但是如果遇到了IO瓶颈,消费者处理的效率完全取决于IO效率,在消费能力上已经优化到了极限还是处理不过来怎么办?或者系统突然遇到用户高峰,我们所配置的线程池大小不够用怎么办?

这时候我们就只能从生产者入手,限制生产者的生产速度。那如何限制呢?

public LinkedBlockingQueue(int capacity)
{
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node < E > (null);
}

LinkedBlockingQueue提供了capacity参数可以限制队列的大小,当队列元素达到上线的时候,生产者线程会阻塞住,直到队列被消费者消费到有空槽的时候才会继续下去。这里似乎只要给队列设置一个大小就ok了。

但是实际情况并不是我们所想的那样。

public void execute(Runnable command)
{
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize)
    {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command))
    {
        #
        here
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);#
    here
}

翻开源码可以发现生产者向队列里塞任务用的方法是workQueue.offer(),这个方法在遇到队列满时是不会阻塞的,而是直接返回一个false,表示抛弃了这个任务。然后生产者调用reject方法,进入拒绝处理逻辑。

接下来我们看看这个reject方法到底干了什么

final void reject(Runnable command)
{
    handler.rejectedExecution(command, this);
}

1.png

我们看到JDK默认提供了4中拒绝策略的实现。

1.AbortPolicy 默认策略,抛出RejectedExecutionException异常

2.CallerRunsPolicy 让任务在生产者线程里执行,这样可以降低生产者的生产速度也不会将生产者的线程堵住

3.DiscardPolicy 直接抛弃任务,不抛异常

4.DiscardOldestPolicy 直接抛弃旧任务,不抛异常

一般比较常用的是CallerRunPolicy,比较优雅的解决了过饱问题。如果你觉得这种方式不那么优雅的话,还可以使用下面的几种方式。这几种方式都是通过处理RejectExecution来实现生产者的阻塞的目的。

public class BlockWhenQueueFullHandler implements RejectedExecutionHandler
{
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
    {
        pool.getQueue()
            .put(new FutureTask(r));
    }
}

这种方案是使用put方法会阻塞生产者线程的原理达到了目的。

public class BlockWhenQueueFull implements RejectedExecutionHandler
{
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
    {
        try
        {
            long waitMs = 10;
            Thread.sleep(waitMs);
        }
        catch (InterruptedException e)
        {}
        executor.execute(r);
    }
}

这种方案显而易见,用sleep达到了阻塞的目的。

public class BoundedExecutor
{
    private final Executor exec;
    private final Semaphore semaphore;
    public BoundedExecutor(Executor exec, int bound)
    {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }
    public void submitTask(final Runnable command)
    throws InterruptedException, RejectedExecutionException
    {
        semaphore.acquire();
        try
        {
            exec.execute(new Runnable()
            {
                public void run()
                {
                    try
                    {
                        command.run();
                    }
                    finally
                    {
                        semaphore.release();
                    }
                }
            });
        }
        catch (RejectedExecutionException e)
        {
            semaphore.release();
            throw e;
        }
    }
}

这种方案是通过信号量的大小都限制队列的大小,也不需要特别限定executor队列大小了,同样的原理还可以使用wait/notifyAll机制来达到一样的目的。

这样我们就能够解决java线程池满了的情况了,java线程池满了会影响系统运行,所以遇到java线程池满的情况,一定要及时找到原因,然后再去解决!最后大家如果想要了解更多java入门知识,敬请关注奇Q工具网。

推荐阅读:

java多线程面试题有哪些?java多线程面试算法

java写函数的步骤是什么?Java函数重载怎么操作?

Java代码怎么写?Java写代码有哪些技巧?