[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密集型
来设置。
还有拒绝策略也有讲究,一般如果一定要执行任务,没什么问题的话,就在拒绝策略发生时,抛给调用线程执行,如果其他情况就要换策略,这个还是具体情况具体分析的…
总结:
- 创建线程池需传自定义的ThreadFactory来实现线程名的定制
- 线程池中用完ThreadLocal要主动清除,防止内存泄露
- 线程池自定义,也就是所有参数需视情况来自定义。
以上是关于[Java] 线程池的创建方式 + 关键属性设置 以及 注意事项的主要内容,如果未能解决你的问题,请参考以下文章