目录
- 问题说明
- 代码验证:
- 创建全局的线程池。
线程池是一个很好的使用线程的方式, 但是如果使用不当还是会引起问题的。
问题说明
当一个对象变成垃圾后会被垃圾对象回收, 其内部的成员变量自然也会被回收, 但是如果成员变量是线程池, 那么这个就会出现问题。
- 线程池使用后内部还是会维护核心线程存活,而存活的线程不是垃圾对象,反而线程可以作为GC Roots
- 如果确实需要局部使用线程变量,请在使用完后一定要调用shutdown方法。 调用了这个方法后,线程池的中的线程不会再维护存活了,可以回收了。 shutdown方法建议在finally模块中执行
- 线程池内部 allowCoreThreadTimeOut变量,注意是ThreadPoolExecutor的变量, 不是ExecutorService中定义了。 这个变量默认值是false, 如果设置成true后线程池在keepAlive时间后不会再维护核心线程了。
代码验证:
下面我们使用代码了验证下这个问题:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 启动。。。。");
});
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 启动。。。。");
});
executorService = null;
System.gc();
System.out.println("垃圾回收后.........................................");
Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
allStackTraces.forEach((k, v) -> {
String name = k.getName();
System.out.println("存活线程:" + k.getName());
});
System.out.println("程序运行结束后.........................................");
}
运行后的输出如下:
为何会是这个情况, 我根据自己的理解逐个说明下:
- 为何垃圾回收语句最先打印?
因为线程的创建是一个耗资源的过程, 线程池中的两个线程需要经历创建栈空间,竞争CPU资源,竞争锁资源。 当这些准备工作都完成后才会开始打印日志。 但是此时主线程已经是运行中的状态, 它更容易获取到cpu和锁, 所以垃圾回收的语句先打印出来。
我们改下代码再次运行下:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 启动。。。。");
});
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 启动。。。。");
});
executorService = null;
//增加暂停3秒的语句
Thread.sleep(3000);
System.gc();
System.out.println("垃圾回收后.........................................");
Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
allStackTraces.forEach((k, v) -> {
String name = k.getName();
System.out.println("存活线程:" + k.getName());
});
System.out.println("程序运行结束后.........................................");
}
运行后的结果如下:
可以看到日志打印的顺序正常了。 但是还是有其他的问题。
- 垃圾回收后线程池的线程尽然还是存活的?
垃圾回收对于线程是无效的, 线程池中的线程继续存活者。 - 主线程结束后, 整个程序还不会结束?
这说明线程池中的线程不是守护线程, 不会随着主线程的结束而结束的。
创建全局的线程池。
因为线程池不能被简单回收,所以干脆把线程池作为静态变量, 全局享用, 每次我们使用线程池的时候, 看下全局中是否已经有了, 如果已经有了就不要在创建新的。
可以把线程池统一放到一个单例类中管理, 统一管理线程池的创建和销毁, 防止在代码中忘记关闭线程池。
给大家分享个我经常使用的全局线程池类:
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public final class ThreadPoolUtil {
private void ThreadPoolUtil() {
}
static volatile int theadCount = 0;
/**
* 通用的线程池。
*/
public static ThreadPoolExecutor commonPool = new ThreadPoolExecutor(1,
500, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5000)
, r -> new Thread(r, "common thread " + theadCount++)
);
/**
* 定时任务线程池, 注意定时任务要设置合理的定时时间, 要根据任务的耗时来合理设置。 建议定时任务还是使用spring的定时任务功能
*/
public static ScheduledThreadPoolExecutor schedulePool = new ScheduledThreadPoolExecutor(1,
r -> new Thread(r, "schedulePool thread " + theadCount++)
);
/**
* 短时间批量任务的执行
*
* @param poolSize
* @param runnables
*/
public static void createPool(int poolSize, List<Runnable> runnables) {
if (poolSize < 1) {
throw new RuntimeException("请设置合理的线程池数量");
}
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize,
poolSize, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(runnables.size())
, r -> new Thread(r, "create new Pool thread " + theadCount++));
runnables.forEach(runnable -> {
threadPoolExecutor.execute(runnable);
});
//这里只是通知线程池不在接收新任务了,并且所有任务结束后,线程池要关闭
threadPoolExecutor.shutdown();
}
}
当然你们可以根据自己的情况灵活调整线程池的参数。