[Java] 线程池的创建方式 + 关键属性设置 以及 注意事项

Posted 削尖的螺丝刀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java] 线程池的创建方式 + 关键属性设置 以及 注意事项相关的知识,希望对你有一定的参考价值。

今天看了一篇关于线程池源码的文章,写的很棒,在此推荐给大家,同时记录一下方便自己回看【线程池之ThreadPoolExecutor线程池源码分析笔记】,因源码部分早已弄懂,所以我更多关注的是实际使用时的需注意事项。

一、创建线程池时候要指定与业务相关的名字,以便于追溯问题(通过重写ThreadFactory接口实现)

我们都知道,线程池中的线程最终是通过ThreadFactory产出的,那么要改线程名字,势必要去了解下ThreadFactory的源码,话不多说,下面贴出源码:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) 
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    

   public static ThreadFactory defaultThreadFactory() 
        return new DefaultThreadFactory();
   

static class DefaultThreadFactory implements ThreadFactory 
        //(1)
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        //(2)
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        //(3)
        private final String namePrefix;

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

        public Thread newThread(Runnable r) 
           //(4)
            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;
        
    

由上代码 DefaultThreadFactory 的实现可知:

  • 代码(1)poolNumber 是 static 的原子变量用来记录当前线程池的编号,它是应用级别的,所有线程池公用一个,比如创建第一个线程池时候线程池编号为1,创建第二个线程池时候线程池的编号为2,这里 pool-1-thread-1 里面的 pool-1 中的 1 就是这个值。

  • 代码(2)threadNumber 是线程池级别的,每个线程池有一个该变量用来记录该线程池中线程的编号,这里 pool-1-thread-1 里面的 thread - 1 中的 1 就是这个值。

  • 代码(3)namePrefix是线程池中线程的前缀,默认固定为pool。

  • 代码(4)具体创建线程,可知线程的名称使用 namePrefix + threadNumber.getAndIncrement() 拼接的。

从上知道我们只需对 DefaultThreadFactory 的代码中 namePrefix 的初始化做手脚,当需要创建线程池是传入与业务相关的 namePrefix 名称就可以了,代码如下(为方便直接拷贝使用,我已把个人信息注释消除):

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadFactory 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;

    CustomThreadFactory(String name) 

        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        if (null == name || name.isEmpty()) 
            name = "pool";
        

        namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
    

    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;
    

那么创建线程池就可以指定线程名了,测试代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorTest 
    static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(),new CustomThreadFactory("ASYN-ACCEPT-POOL"));
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(),new CustomThreadFactory("ASYN-PROCESS-POOL"));

    public static void main(String[] args) 

        //接受用户链接模块
        executorOne.execute(new  Runnable() 
            public void run() 
                System.out.println("接受用户链接线程");
                throw new NullPointerException();
            
        );
        //具体处理用户请求模块
        executorTwo.execute(new  Runnable() 
            public void run() 
                System.out.println("具体处理业务请求线程");
            
        );

        executorOne.shutdown();
        executorTwo.shutdown();
    

. 运行结果如下,一目了然,出现异常可以准确知道是哪种线程报的错:


 
 
后续补充:

当然还有更简单的方法,我们可以直接使用Spring封装好的Executor来创建线程池,所有属性根据需要来设置即可,创建好后直接交由Spring管理,举例如下:

	@Bean(name = "threadPoolTaskExecutor ")
    public ThreadPoolTaskExecutor eventExecutor() 

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(CUSTOMIZED_THREAD_NAME_PREFIX);
        executor.initialize();
        return executor;

    

 
 
 

二、如果在线程池中用了ThreadLocal,要警惕内存泄露问题(应该在用完后对ThreadLocal进行清除)

看下面内存泄露的例子

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolTest 

    static class LocalVariable 
        private Long[] a = new Long[1024 * 1024];
    

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException 
        // (3)
        for (int i = 0; i < 50; ++i) 
            poolExecutor.execute(new Runnable() 
                public void run() 
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible");
                    // 关键在下面这一步,如果在使用完后清除掉就不会有内存泄露问题
                    //Todo localVariable.remove(); 

                
            );

            Thread.sleep(1000);
        
        // (6)
        System.out.println("pool execute over");
    

  • 代码(1)创建了一个核心线程数和最大线程数为 5 的线程池,这个保证了线程池里面随时都有 5 个线程在运行。

  • 代码(2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组。

  • 代码(3)向线程池里面放入 50 个任务

  • 代码(4)设置当前线程的 localVariable 变量,也就是new了一个LocalVariable 放入当前线程的 threadLocals 中。

由于没有调用线程池的 shutdown 或者 shutdownNow 方法所以线程池里面的用户线程不会退出,进而 JVM 进程也不会退出。( 这里只是为了看的更清除,所以没有shutDown线程池,实际使用完线程池还是要shutDown线程池的 ,不然线程池也是一个开销 )
 
重点是在线程池shutDown之前ThreadLocal可能产生内存泄露。

 
可以看到在没有对ThreadLocal进行清除时,也就是没有执行上面的localVariable.remove(); 方法,Jconsole的堆内存使用量如下:

 
 
 
 
在放开注释,执行localVariable.remove(); 方法清除ThreadLocal值后,Jconsole的堆内存情况如下:

 

结果一目了然,当主线程处于休眠时候,运行结果一的进程占用了大概 77M 内存运行结果二则占用了大概 25M 内存,可知运行代码一时候内存发生了泄露…

原因:
运行结果一的代码,在设置线程的 localVariable 变量后没有调用 localVariable.remove()方法,导致线程池里面的 5 个线程的 threadLocals 变量里面的 new LocalVariable() 实例没有被释放,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到 JVM 进程被杀死。
 
这里需要注意的是由于 localVariable 被声明了 static,虽然线程的 ThreadLocalMap 里面是对localVariable的弱引用,localVariable也不会被回收。
 
运行结果二的代码由于线程在设置 localVariable 变量后及时调用了 localVariable.remove() 方法进行了清理,所以不会存在内存泄露。
 
总结:线程池里面设置了 ThreadLocal 变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的 threadLocals 变量一直会持有 ThreadLocal 变量。

 
 

三、建议线程池所有参数自定义,防止OOM等问题

这个就不展开讲了,在阿里代码规范里也有说明,线程池要自定义,不要用原生的,比如CachedThreadPool:最大线程范围是Int的最大值,可能会创建大量线程导致OOM。
 
另外核心线程和最大线程的取舍也是有讲究的,可以根据CPU密集型IO密集型来设置。
 
还有拒绝策略也有讲究,一般如果一定要执行任务,没什么问题的话,就在拒绝策略发生时,抛给调用线程执行,如果其他情况就要换策略,这个还是具体情况具体分析的…

总结:

  1. 创建线程池需传自定义的ThreadFactory来实现线程名的定制
  2. 线程池中用完ThreadLocal要主动清除,防止内存泄露
  3. 线程池自定义,也就是所有参数需视情况来自定义。

以上是关于[Java] 线程池的创建方式 + 关键属性设置 以及 注意事项的主要内容,如果未能解决你的问题,请参考以下文章

线程池的创建

如何设置Java线程池大小?

Java线程池的四种创建方式

Java线程池容量设置

java线程池之一:创建线程池的方法

创建线程池的核心方式 --- ThreadPoolExecutor