守护线程与用户线程区别
本质区别
守护线程:为了守护用户线程而存在,当所有用户线程运行完毕后,自动结束,然后JVM才正常退出;因此守护线程通常不会执行一次指定任务就结束,而是持续运行,直到用户线程全部退出后自动结束。
用户线程:其他普通线程基本都可以归到用户线程下,包括主线程。JVM会等待用户线程运行结束才退出。
常用线程池
JDK提供了个工具类 Executors,用于快速创建各种场景下的线程池。
- newCachedThreadPool
- newFixedThreadPool
- newScheduledThreadPool
- newSingleThreadExecutor
- newSingleThreadScheduledExecutor
- newWorkStealingPool(线程池类型为:ForkJoinPool,内部线程为守护线程)
以上只有,最后一个线程池所创建的线程为守护线程,其他的都是用户线程。
说到ForkJoinPool线程池,就绕不开这个类CompletableFuture(默认也是基于ForkJoinPool 线程池运行,也可以手动指定为自定义的线程池;它也是随着ForkJoinPool 一起来的,集成了各种线程池的便捷操作)
结合上面的结论:
-
对于 ForkJoinPool,内部线程均为守护线程 ,只要其他用户线程执行完毕,JVM 不会 等待该线程池内的任务执行,会直接退出。
-
而对于其他基于 ThreadPoolExecutor创建的线程池,内部线程均为用户线程,JVM 会 等待该线程池内的任务执行完毕后,再退出。
测试以上线程池与用户线程及JVM关系
- 先创建一个模拟耗时任务
private static void simulate(){
System.out.println(LocalDateTime.now() + " 任务开始");
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(LocalDateTime.now() + " 任务结束");
}
- 向JVM注册一个关闭钩子函数
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now() + "JVM 退出")));
- 在不同线程池环境下执行,看结果
- 基于ThreadPoolExecutor的线程池,newFixedThreadPool
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now() + "JVM 退出")));
Executors.newFixedThreadPool(1).execute(() -> {
simulate();
});
System.out.println(LocalDateTime.now() + " 主线程结束");
}
控制台输出如下:
2023-02-01T15:08:42.269099200 主线程结束
2023-02-01T15:08:42.269099200 任务开始
2023-02-01T15:08:47.284508500 任务结束
并且JVM一直处于运行状态;不停止的原因正如上面所说:JVM会等待用户线程运行结束才退出。
由于这里的线程是以线程池形式运行,因此线程的生命周期实际上由线程池控制,可以看看newFixedThreadPool相关的源码。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面指定了核心线程数量为:1
根据以上的配置:得到一个核心线程和最大线程数都是1,永不销毁的线程池;那么这个线程会一直保持;因此JVM会一直处于运行状态。
- 基于ThreadPoolExecutor的线程池,newCachedThreadPool
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now() + "JVM 退出")));
Executors.newCachedThreadPool().execute(() -> {
simulate();
});
System.out.println(LocalDateTime.now() + " 主线程结束");
}
控制台输出如下:
2023-02-01T15:18:17.669162900 任务开始
2023-02-01T15:18:17.669162900 主线程结束
2023-02-01T15:18:22.679011 任务结束
2023-02-01T15:19:22.692124900JVM 退出
可以看到,最终JVM退出了,并且是在线程任务结束1分钟后退出的。
出现以上现象的原因,还是看看源码。
这是创建 newCachedThreadPool 线程池的代码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到:
核心线程数为:0,也就是说,实际的工作线程都是线程池启动后再创建的非核心线程。
并且,看到有设置非核心线程的存活时间为:60s。
也就是说,60s后,这个线程就会被回收;也就没有用户线程了(主线程一早就运行完了),因此这个线程只要一回收,JVM就退出。
- 基于ForkJoinPool的线程池,newWorkStealingPool
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now() + "JVM 退出")));
Executors.newWorkStealingPool().execute(() -> {
simulate();
});
System.out.println(LocalDateTime.now() + " 主线程结束");
}
控制台输出如下:
2023-02-01T15:25:09.377743700 任务开始
2023-02-01T15:25:09.377743700 主线程结束
2023-02-01T15:25:09.382729100JVM 退出
可以看到:JVM几乎是在主线程运行结束的同时就退出了,而没有等线程池中的任务执行完毕。
这就符合上面的守护线程的特征了。
直接看源码,以下是创建 newWorkStealingPool 线程池的源码
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
继续
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(parallelism, factory, handler, asyncMode,
0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS);
}
/**
* Default idle timeout value (in milliseconds) for the thread
* triggering quiescence to park waiting for new work
*/
private static final long DEFAULT_KEEPALIVE = 60_000L;
继续
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode,
int corePoolSize,
int maximumPoolSize,
int minimumRunnable,
Predicate<? super ForkJoinPool> saturate,
long keepAliveTime,
TimeUnit unit)
一步步下来,
可以看到核心线程数为:0;
线程存活时间为:60s
到这里,如果按照用户线程的特征的话,结果应该与 newCachedThreadPool 线程池保持一致;实际并没有。
接下来看看 newWorkStealingPool 内部实际线程。
源码中可以看到实际的工作线程是由
ForkJoinPool.defaultForkJoinWorkerThreadFactory 这个工厂创建的,点进去看看。
在 ForkJoinPool 中的一个静态代码块中找到了相关代码。
defaultForkJoinWorkerThreadFactory = new DefaultForkJoinWorkerThreadFactory();
继续往下面跟
static final class DefaultForkJoinWorkerThreadFactory
implements ForkJoinWorkerThreadFactory {
// ACC for access to the factory
@SuppressWarnings("removal")
private static final AccessControlContext ACC = contextWithPermissions(
new RuntimePermission("getClassLoader"),
new RuntimePermission("setContextClassLoader"));
@SuppressWarnings("removal")
public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return AccessController.doPrivileged(
new PrivilegedAction<>() {
public ForkJoinWorkerThread run() {
return new ForkJoinWorkerThread(null, pool, true, false);
}},
ACC);
}
}
可以看到,线程池内实际的线程是 ForkJoinWorkerThread 这个类
点进去
ForkJoinWorkerThread(ThreadGroup group, ForkJoinPool pool,
boolean useSystemClassLoader, boolean isInnocuous) {
super(group, null, pool.nextWorkerThreadName(), 0L);
UncaughtExceptionHandler handler = (this.pool = pool).ueh;
this.workQueue = new ForkJoinPool.WorkQueue(this, isInnocuous);
super.setDaemon(true);
if (handler != null)
super.setUncaughtExceptionHandler(handler);
if (useSystemClassLoader)
super.setContextClassLoader(ClassLoader.getSystemClassLoader());
}
看看,找到了啥
super.setDaemon(true);
这句代码,调用的是Thread 类方法,将当前线程设置为守护线程
到这里,就明了了。
ForkJoinPool 线程池中运行的线程对象是 ForkJoinWorkerThread,而这个线程是守护线程。
- 而:CompletableFuture 这个类,会根据CPU是否为多核选择性使用 ForkJoinPool 或者 ThreadPerTaskExecutor.
private static final boolean USE_COMMON_POOL =
(ForkJoinPool.getCommonPoolParallelism() > 1);
/**
* Default executor -- ForkJoinPool.commonPool() unless it cannot
* support parallelism.
*/
private static final Executor ASYNC_POOL = USE_COMMON_POOL ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
也就是说,在多核CPU中,使用ForkJoinPool,单核CPU中使用ThreadPerTaskExecutor,这一点留意(不过现在服务端开发中基本很少单核CPU)。