多线程之线程池 · 下

前言

线程池中这块的内容确实要比我预期的多,当然也可能是我讲的太细了,所以比较费字,但是这样也好,不仅让各位小伙伴能更清楚相关逻辑和原理,而且对我而言,让我可以做到知其然知其所以然,从这个层面上来讲,我觉得一切都值得,甚至还有点意外的收获。

今天是线程池相关知识点收官之作,今天的内容完结后,基本上线程池这块的内容就可以告一段落了,今天主要从以下几方面开展:

  • 线程池的其他参数
    • 线程工厂
    • 拒绝策略处理器

好了,话不多说,让我们直接开始吧。

线程池

线程工厂

先看第一个参数——线程工厂,这个参数的作用是创建线程。在最开始的时候,我们创建线程池的时候并没有指定这个参数,但是构造器内部会自动为我们引入默认的线程工厂:

多线程之线程池 · 下

下面我们就来看下默认的线程工厂是如何实现的:

多线程之线程池 · 下

多线程之线程池 · 下

在线程工厂中,主要有两部分的操作,第一部分就是设定创建线程时的信息,包括线程组、线程名称、堆栈大小;第二部分主要是设置线程的优先级,如果线程是守护线程的话,会把它修改为非守护线程(看到这里,我发现关于线程组、线程得好好补补课了,后面要跟进下)。

我们一般自定义线程工厂主要是为了修改线程名称,这样方便我们在排查问题的时候找到我们自己定义的线程池,我们要自顶一下线程工厂只需要冲洗ThreadFactorynewThread方法即可:

static class MyThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        MyThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "syske-" +
                    poolNumber.getAndIncrement() +
                    "-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

这里我就直接把默认线程工厂的实现copy过来,然后只改了名字,然后在构造线程池的时候传入即可:

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new MyThreadFactory());

然后运行下,我们就会发现线程名称已经变成我们修改之后的了:
多线程之线程池 · 下

当然如果只是单纯想修改线程池中线程名称,这样就太奢侈了,我们可以通过guava提供的ThreadFactoryBuilder直接修改(就是我们之前分享的guava):

ThreadFactory build = new ThreadFactoryBuilder().setNameFormat("syske-task-%d").build();

当然,guava还可以设定其他属性,甚至可以指定线程工厂,UncaughtExceptionHandler表示未捕获到的异常处理器:

多线程之线程池 · 下

拒绝策略(饱和策略)

我们在之前的内容中说过,如果任务数超过corePoolSize + maximumPoolSize + workQueue.size(),线程池就会报拒绝连接的错误,这个错误就是这里抛出了的,所以接下来我们要分享的就是线程池的最后一个参数——RejectedExecutionHandler

和线程工厂一样,在我们不传拒绝策略处理器的时候,其实构造方法内部为我们制定了默认的处理器:

多线程之线程池 · 下

多线程之线程池 · 下

默认拒绝策略处理器的实现也很简单:

多线程之线程池 · 下

也可以说是简单粗暴,直接就抛出了RejectedExecutionException异常。

当然我们也可以定义自己的拒绝策略处理器:

 static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("线程池拒绝连接,资源已耗尽:r = " + r + ", executor = " + executor);
            throw new RejectedExecutionException();
        }
    }

然后也是在构造线程池的时候传入:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new MyThreadFactory(), new MyRejectedExecutionHandler());

我们把工作对了调小,把循环次数调大,把休眠时间调长,然后运行就会触发饱和策略,进入我们的的rejectedExecution方法:

多线程之线程池 · 下

根据我的测试,我发现如果在rejectedExecution方法中直接抛出RejectedExecutionException,会导致主线程进入阻塞状态,这样整个系统就卡死了

多线程之线程池 · 下

但如果不抛出RejectedExecutionException异常的话,则不会导致阻塞:

多线程之线程池 · 下

所以考虑到实际应用情况,我觉得我们还是有必要重写rejectedExecution方法的,而且我们在方法内部还可以做一些业务操作,比如线程池扩容,或者睡眠等待:

多线程之线程池 · 下

或者可以做一些业务告警等操作。

总结

关于线程池的创建、参数以及参数的作用,经过我们这两天的详细分析和演示,我想大家一定也对线程池有了新的认知,应该说在以后的工作和学习中,不论是线程池的使用还是解决线程池相关的问题,都可以站得更高,看的更远。

当然,最重要的是,这几天分享的内容都比较实用。明天我们会对线程池这块做一个小结,然后小结之外我们会有一些遗漏知识点的补充。好了,今天就先到这里吧!

上一篇:Java++:如何判断一个线程池中的所有线程都执行完成?


下一篇:为什么阿里Java规约禁止使用Java内置Executors创建线程池?