浅浅聊下守护线程与用户线程的区别

本质区别

守护线程:为了守护用户线程而存在,当所有用户线程运行完毕后,自动结束,然后JVM才正常退出;因此守护线程通常不会执行一次指定任务就结束,而是持续运行,直到用户线程全部退出后自动结束。
用户线程:其他普通线程基本都可以归到用户线程下,包括主线程。JVM会等待用户线程运行结束才退出。

常用线程池

JDK提供了个工具类 Executors,用于快速创建各种场景下的线程池。

  • newCachedThreadPool
  • newFixedThreadPool
  • newScheduledThreadPool
  • newSingleThreadExecutor
  • newSingleThreadScheduledExecutor
  • newWorkStealingPool线程池类型为:ForkJoinPool,内部线程为守护线程

以上只有,最后一个线程池所创建的线程为守护线程,其他的都是用户线程。

说到ForkJoinPool线程池,就绕不开这个类CompletableFuture默认也是基于ForkJoinPool 线程池运行,也可以手动指定为自定义的线程池;它也是随着ForkJoinPool 一起来的,集成了各种线程池的便捷操作)

结合上面的结论:

  • 对于 ForkJoinPool,内部线程均为守护线程 ,只要其他用户线程执行完毕,JVM 不会 等待该线程池内的任务执行,会直接退出。

  • 而对于其他基于 ThreadPoolExecutor创建的线程池,内部线程均为用户线程,JVM 等待该线程池内的任务执行完毕后,再退出。

测试以上线程池与用户线程及JVM关系

  1. 先创建一个模拟耗时任务
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() + " 任务结束");
}
  1. 向JVM注册一个关闭钩子函数
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now() + "JVM 退出")));
  1. 在不同线程池环境下执行,看结果
  • 基于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)。

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值