zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java 并发编程(Ⅰ)

2023-09-11 14:22:32 时间


1. 概念

1. 基本概念

进程和线程:

  • 图示:
  • 文字:
    • 进程:程序的一个实例,一个程序可以有多个实例,如下图

      现在QQ这一个程序就拥有了两个进程
    • 线程:线程就是进程的执行单元,一个进程中有多个线程
  • 概念:
    • 进程:进程是 分配和管理资源 的基本单位,不同进程会竞争计算机系统资源
    • 线程:线程是 最小调度单位(处理器调度的基本单位)
  • 区别:
    • 独立 | 共享
      • 线程之间共享本进程的地址空间、资源(内存、CPU、IO等)
      • 每个进程都有独立的地址空间,资源独立
    • 健壮 ?
      • 一个线程崩溃之后,整个进程都要崩溃
      • 一个进程崩溃之后,在保护模式下不会对其他进程产生影响
    • 开销?
      • 线程依附于进程运行,显然进程开销大(包括启动开销、切换开销)
    • 通信
      • 同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
      • 线程因为共享进程资源,所以通信很简单,比如说共享一个静态资源

并发与并行:
单核CPU同一时间只能执行一个线程,即 串行执行 。操作系统中的 任务调度器 会将 CPU 的时间片(时间很短)分给不同的线程使用。因为 CPU 在线程间切换非常快,会给人一种多线程同时运行的错觉。

  • 并发(concurrent): 同一时间应对(dealing with)多件事的能力
  • 并行(parallel): 同一时间做(doing)多件事的能力

同步与异步:

  • 同步: 即串行执行,需要等前面的代码返回之后,才能继续执行后续的代码。

    @Slf4j
    public class ShowSync {
    
        public static void count(int i) {
            log.info("第" + i + "次执行count方法");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 3; i++) {
                count(i);
            }
            log.info("线程:" + Thread.currentThread().getName());
        }
    }
    
  • 异步: 不需要等待前面的代码返回,就能执行下面的代码。

    @Slf4j
    public class ShowAsync {
        public static void main(String[] args) {
            for (int i = 0; i < 3; i++) {
                new Thread(() ->
                    log.info("线程:" + Thread.currentThread().getName())
                ).start();
            }
            log.info("线程:" + Thread.currentThread().getName());
        }
    }
    

2. 线程的状态

Thread 类中提供了一个枚举类 State,如下:

public enum State {

        NEW,
        
        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,

        TERMINATED;
    }

可见,一个Java线程有 六大状态 。可实际上,我们会把 RUNNABLE 分为 ReadyRUNNABLE 两大状态,如下图所示,所以我们一般会说:一个Java线程有 七大状态

  • NEW
    • Thread t1 = new Thread() 进入此状态
  • READY
    • t.start() 进入此状态
    • 此时线程已经可以运行,但还未获取到 CPU 时间片,处于就绪状态
  • RUNNING
    • 就绪的线程获取到了 CPU 时间片,进入运行状态
  • BLOCKED
    • 线程进入 synchronized 修饰的方法或代码块
  • TERMINATED
    • 线程的 run() 跑完或者 main() 跑完
  • WAITING
    • 进入此状态的线程不会被分配 CPU时间片,需要被其他线程唤醒,不然会一直等待下去
  • TIMED_WAITING
    • 进入此状态的线程不会被分配 CPU时间片,不需要被其他线程唤醒,到达一定时间之后会自动苏醒

至于线程中的状态转换及相关方法,我将在后文陆续介绍。

2. 线程的初始化

线程的初始化,也就是 new Thread(),我们有三种方式来实现初始化,分别是:

  • 直接 new Thread() 然后重写 run()
  • new Thread(new Runnable()) ,将 Runnablerun() 组装到 Thread
  • new Thread(new FutureTask(new Callable())) ,将 Callable 中的 call() 先组装到 FutureTask 再组装到 Thread

1. new Thread()

直接 new Thread() 初始化线程,是将线程(Thread)和任务(run())绑在一起,实际开发中不建议使用,代码如下:

@Slf4j
public class JustThread {
    public static void main(String[] args) {
        Thread thread1 = new Thread("线程1") {
            @Override
            public void run() {
                log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
            }
        };
        thread1.start();

        log.info("线程:" + Thread.currentThread().getName() + " 运行中...");

        /*
            lambda表达式
         */
        Thread thread2 = new Thread(() -> {
            log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
        }, "线程2");
        thread2.start();
    }
}

2. new Thread(new Runnable())

将线程(Thread)和任务(Runnable)分开初始化,然后组合在一起。

@Slf4j
public class RunnableAndThread {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
            }
        };
        Thread thread = new Thread(runnable, "线程3");
        thread.start();

        /*
            lambda表达式
         */
        Runnable runnable1 = () -> log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
        Thread thread1 = new Thread(runnable1, "线程4");
        thread1.start();
    }
}

3. Thread 和 Runnable 的关系

先看一下 Runnable 的源码,如下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

一个函数式接口,只有一个 run()

然后看一下 Thread 的部分源码:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

好的,就看这几段方法,就不在这里做深入分析了。从上面的代码,我们可以很清晰地弄懂 ThreadRunnable 之间的关系:

  • new Thread 的时候,如果没有传入 Runnable,就会重写 Threadrun()
  • new Thread 的时候,如果传入了 Runnable,就会使用 Runnable 中的 run()

所以,Runnable 接口就是为了给 Thread 提供一个方法体 run()

在实际开发中,我们通常会自定义一个类来 implements Runnable,然后将此类传入 Thread 中。案例如下:

  • 定义一个每间隔十秒打印一下当前时间的线程
public class TimePrinter implements Runnable{
    private SimpleDateFormat dateFormat= new SimpleDateFormat("hh:mm:ss");

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("当前时间为: " + dateFormat.format(new Date()));
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class TaskDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new TimePrinter(), "测试线程");
        thread.start();
    }
}

4. new Thread(new FutureTask(new Callable()))

@Slf4j
public class FutureTaskAndThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {


        FutureTask<Long> futureTask = new FutureTask<>(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                long start = System.currentTimeMillis();
                log.info(Thread.currentThread().getName() + " 运行中...");
                TimeUnit.MILLISECONDS.sleep(123);
                long end = System.currentTimeMillis();
                return end - start;
            }
        });
        Thread thread = new Thread(futureTask, "线程5");
        thread.start();
        log.info(thread.getName() + "花费了 " + futureTask.get() + " 毫秒");
    }
}

Runnable与Callable的关系如下图:
可见,相比于 Runnablerun()Callablecall() 多了一个返回值。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

3. 常用方法

线程状态的切换:

TIMED_WAITING 和 WAITING 的区别:

  • TIMED_WAITING 即使没有外部信号,在等待时间超时后,线程也会恢复
  • WAITING 需要等待外部信号来唤醒

1. start

先来看一下源码:

	// 线程组
	private ThreadGroup group;
	// NEW 状态的线程的 threadStatus = 0
	private volatile int threadStatus = 0;

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
		// 将要启动(start)的线程装入线程组中
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                	// 告诉组,这个线程启动(start)失败
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {

            }
        }
    }

	// native方法,调用本地操作系统开启一个新的线程
    private native void start0();

看完源码之后,可以得出以下结论:

  • start 启动一个线程需要经历以下步骤:
    1. 将调用 start 的线程装入线程组 ThreadGroup
    2. 调用native方法,启动线程

1. 线程组

此处参考链接:https://zhuanlan.zhihu.com/p/61331974

线程组(Thread Group): 一个线程集合,可以很方便地管理一个组中的线程。

线程组树:

  • System线程组: 处理JVM的系统任务的线程组,例如对象的销毁等

  • main线程组:包含至少一个线程——main(用来执行main方法)

  • 面线程组的子线程组: 应用程序创建的线程组

        public static void main(String[] args) {
            // 输出当前线程组——main线程组
            System.out.println(Thread.currentThread().getThreadGroup().getName());
            // 输出当前线程组的父线程组——System
            System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());
        }
    

给线程指定线程组:

  • 在初始化线程的时候如果不指定数组,就会默认指定为 main线程组,代码如下:

    private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc,
                          boolean inheritThreadLocals) {
                          
            /*
    			其余代码省略
    		*/
            Thread parent = currentThread();
            SecurityManager security = System.getSecurityManager();
            if (g == null) {
              
                if (security != null) {
                    g = security.getThreadGroup();
                }
    
                if (g == null) {
                    g = parent.getThreadGroup();
                }
            }
    
            /*
    			其余代码省略
    		*/
    }
    
  • 可以利用以下任意一个构造器来给线程指定一个线程组:

        public Thread(ThreadGroup group, Runnable target)
    
        public Thread(ThreadGroup group, Runnable target) 
    
    	public Thread(ThreadGroup group, Runnable target, String name)
    
        public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
    

    演示一下:

        public static void main(String[] args) {
            ThreadGroup group = new ThreadGroup("我的线程组");
            Thread thread = new Thread(group, "我的线程");
            System.out.println(thread.getName() + " 的线程组是 " + group.getName());
        }
    

线程组方法解析: https://blog.csdn.net/a1064072510/article/details/87455525

小结一下: 将线程装入线程组就是为了方便批量操作线程,比如说我要通知好几个线程要终止了,就可以事先把这几个线程装入一个线程组,然后直接通过线程组批量通知:

// 通知main线程组中的所有线程要终止了
Thread.currentThread().getThreadGroup().interrupt();

2. start 和 run

问:为什么 start 能启动一个线程,而 run 就不行呢?

答:因为 Java 并不能直接作用于操作系统,所以需要调用native方法(比如C、C++等编写的方法)来告诉操作系统开启新的线程。而在 start 方法中调用了native方法 start0 来启动线程。至于 run, 这就是线程中的一个方法,当线程跑起来的时候,就会去执行 run,当 run 方法执行完毕线程就会结束。

2. sleep

先来看一下 Thread.sleep 的源码:

	/**
	*让当前线程睡眠指定毫秒数,线程不会丢失任何monitors的所有权
	*/
    public static native void sleep(long millis) throws InterruptedException;

好的,现在来使用一下:

@Slf4j
public class Sleep_md{
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            long start = System.currentTimeMillis();
            log.info("t1睡眠5s");
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
            }
            long end = System.currentTimeMillis();
            log.info("已过去 {} 毫秒", (end - start));
        });
        t1.start();
    }
}

通过上述代码的运行,可发现 Thread.sleep 可以让当前线程进入 TIMED_WAITING 睡眠一段时间,然后进入 RUNNABLE 。在此期间会让出CPU,给其他线程机会。

1. TimeUtil

除了 Thread.sleepTimeUtil 也有 sleep,可以让线程进入睡眠,两者效果相同。

    public void sleep(long timeout) throws InterruptedException {
        if (timeout > 0) {
            long ms = toMillis(timeout);
            int ns = excessNanos(timeout, ms);
            Thread.sleep(ms, ns);
        }
    }

使用方法:

// 睡眠n纳秒
TimeUtil.NANOSECONDS.sleep(long n)
// 睡眠n微秒
TimeUtil.MICROSECONDS.sleep(long n)
// 睡眠n毫秒
TimeUtil.MILLISECONDS.sleep(long n)
// 睡眠n秒
TimeUtil.SECONDS.sleep(long n)
// 睡眠n分钟
TimeUtil.MINUTES.sleep(long n)
// 睡眠n小时
TimeUtil.HOURS.sleep(long n)
// 睡眠n天
TimeUtil.DAYS.sleep(long n)

2. InterruptedException

当睡眠的线程被打断睡眠,就会抛出 InterruptedException

@Slf4j
public class Sleep_md{
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            log.info("开始睡眠");
            long start = System.currentTimeMillis();
            try {
            	// 睡眠 60s
                Thread.sleep(1000 * 60);
            } catch (InterruptedException e) {
                long end = System.currentTimeMillis();
                log.info("中途停止睡眠");
            }
        });
        thread.start();
        thread.interrupt();
    }
}

肉眼可见, interrupt() 打断了睡眠,抛出了异常,被catch到了。

3. setPriority

setPriority() —— 设置线程优先级,数值由 1 — 10,数值越小,优先级越高。
注意: 更高的优先级表明更有机会争夺到 CPU 时间片,而不是优先级越高就一定越先执行。

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片
  • cpu 闲时,优先级几乎没作用
@Slf4j
public class Priority_md {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> log.info("t1运行中..."), "t1");
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        Thread t2 = new Thread(() -> log.info("t2运行中..."), "t2");
        t2.setPriority(Thread.MIN_PRIORITY);
        t2.start();
        log.info("main运行中...");
    }
}
  • 3个常量

        public final static int MIN_PRIORITY = 1;
        public final static int NORM_PRIORITY = 5;
        public final static int MAX_PRIORITY = 10;
    
  • 当设置优先级的时候,值不在 [1,10] 的区间,就会抛出异常 IllegalArgumentException

  • main线程的优先级是5,自定义线程的优先级初始值也是5

    Thread main = Thread.currentThread();
    log.info("main线程的优先级是: " + main.getPriority());
    

4. yield

Thread.yield() —— 提示线程调度器,当前线程愿意让出CPU时间片回到就绪状态,线程调度器可以选择忽视该提示(可能会让出失败)。

注意:

  • 让出时间片并不一定会成功,这都取决于线程调度器
  • 让出的时间片不一定会被高优先级的线程抢到,因为在CPU不紧张的时候,优先级没啥用,就算CPU比较紧张,也只是提高了高优先级线程抢时间片的能力
@Slf4j
public class Yield_md {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runner(), "张三");
        t1.setPriority(Thread.MIN_PRIORITY);
        Thread t2 = new Thread(new Runner(), "李四");
        t2.setPriority(Thread.MIN_PRIORITY);
        Thread t3 = new Thread(new Runner(), "王五");
        t3.setPriority(Thread.MIN_PRIORITY);

        for (int i = 1; i < 10; i++) {
            Thread thread = new Thread(new Runner(), "路人" + i);
            thread.setPriority(Thread.MAX_PRIORITY);
            thread.start();
        }

        t1.start();
        t2.start();
        t3.start();
    }

    private static class Runner implements Runnable {

        @Override
        public void run() {
            for (int i = 1; i < 8; i++) {
                if (i % 4 == 0) {
                    log.info(Thread.currentThread().getName() + " 摔了一跤!");
                    Thread.yield();
                }
                if (i == 7) {
                    log.info(Thread.currentThread().getName() + " 完成了比赛!");
                }
            }
        }
    }
}

yield 对比 sleep:

  • yield 成功后直接让线程进入 READYsleep 会进入 WAITING 或者 TIMED_WAITING 时间到了之后才会进入。因此 yield 的线程随时都有可能重新获取到CPU继续跑,而 sleep 需要等待睡眠时间结束

5. join

join() 将指定的线程A加入到当前正在运行的线程B,线程B会进入等待状态,并等待A运行完毕之后再执行B。

@Slf4j
public class Join_md {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    log.info("t1----->" + i);
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
                for (int i = 0; i < 3; i++) {
                    log.info("t2----->" + i);
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t2");
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();

        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long end = System.currentTimeMillis();
        log.debug("t1: {} t2: {} cost: {}", t1, t2, end - start);
    }
}

注意:

  • join 就相当于让两个交替执行的线程顺序执行

  • 当一个线程D中有多个线程(比如A、B、C) join 之后,A、B、C会交替执行,如下:

    @Slf4j
    public class Join_md {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    log.info("t1......");
                    for (int i = 0; i < 5; i++) {
                        log.info("t1: " + i);
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                try {
                    log.info("t2......");
                    for (int i = 0; i < 5; i++) {
                        log.info("t2: " + i);
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }, "t2");
            long start = System.currentTimeMillis();
            t1.start();
            t2.start();
    
            log.info("main...");
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            long end = System.currentTimeMillis();
            log.debug("t1: {} t2: {} cost: {}", t1, t2, end - start);
        }
    }
    

join 对比 yield:

  • 从作用来看:join 阻塞当前线程,让其他线程先执行;yield 让当前线程让出位置,让其他线程执行
  • 从状态来看:join 会让当前线程进入 WAITINGyield 让当前线程进入 READY
  • 从效果上看:join 不会失败;yield 有可能让出资源失败

6. interrupt

1. 方法简介

interrupt 仅更改线程的中断状态(Just to set the interrupt flag),并通知那个线程,不会中断一个正在运行的线程。

isInterrupted 判断当前线程的中断状态。

Thread.interrupted 调用的当前线程的isInterrupted,重置当前线程的中断状态为true。
interrupt之后,线程isInterrupted返回为true,Thread.interrupted会将状态重置为false。

@Slf4j
public class Interrupt_md {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            if (Thread.currentThread().isInterrupted()) {
                log.info("t1已经被interrupt......");
                log.info("重置t1中断状态...");
                Thread.interrupted();
                if (!Thread.currentThread().isInterrupted()) {
                    log.info("t1状态恢复...");
                }
            } else {
                log.info("t1还没被interrupt");
            }
        });
        t1.start();
        t1.interrupt();
    }
}

以下几个方法执行中被interrupt中断会throws InterruptedExceptionThread.sleepjoinwait

2. 中断线程

判断标志位,return中断
中断一个线程就是结束它运行的run方法,所以直接return就可以了。

  • 使用 isInterrupted 作为标志位

    @Slf4j
    public class StopThread {
        public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
               if (Thread.currentThread().isInterrupted()) {
                   return;
               }
               for (;;) {
                   log.info("t1 is running...");
               }
            });
            t1.start();
            t1.interrupt();
        }
    }
    
  • 自定义一个标志位

    @Slf4j
    public class StopThread {
        private volatile static boolean flag = false;
    
        public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
               if (flag) {
                   return;
               }
               for (;;) {
                   log.info("t1 is running...");
               }
            });
            t1.start();
            t1.interrupt();
            flag = true;
        }
    }
    

7. park

LockSupport.park 让线程休眠的方法(进入 WAITING 状态),调用后不会直接进入休眠,会先根据一个 permit 进行判断,如果 permit 存在就直接返回,不存在才会进入休眠状态(Disables the current thread for thread scheduling purposes unless the permit is available. If the permit is available then it is consumed and the call returns immediately; otherwise the current thread becomes disabled for thread scheduling purposes

有三种情况会打破休眠:

  • 其他线程调用了 LockSupport.unpark 方法
  • 调用 interrupt
  • 其他原因

LockSupport.parkNanos 相比于 LockSupport.parkLockSupport.park 需要指定一个时间,然后进入休眠状态(TIMED_WAITING),无需唤醒,时间一到就会自然苏醒。

@Slf4j
public class Park_demo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> LockSupport.park(), "t1");
        Thread t2 = new Thread(() -> LockSupport.parkNanos(1000*1000*3000L), "t2");
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        log.info("t1's state: {}, t2's state: {}", t1.getState(), t2.getState());
    }
}

注意:

  • LockSupport.parkNanos 不会抛出异常,所以在休眠中用 interrupt 去打断也不会抛出异常

  • 可用 LockSupport.unpark 去唤醒休眠的线程

    @Slf4j
    public class Park_demo {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                LockSupport.park();
                log.info("t1苏醒...");
            }, "t1");
            t1.start();
            TimeUnit.SECONDS.sleep(1);
            log.info("t1's state: {}, t1's status: {}", t1.getState(), t1.isInterrupted());
            LockSupport.unpark(t1);
            log.info("t1's state: {}, t1's status: {}", t1.getState(), t1.isInterrupted());
        }
    }
    

8. setDaemon

此节参考文章:https://www.cnblogs.com/lixuan1998/p/6937986.html

setDaemon(true) 将线程设置为守护线程。

Java线程分类:

  • 用户线程
  • 守护线程
    在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。最典型的守护线程:垃圾回收线程。
  • 两者区别:守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

使用守护线程的注意事项:

  • setDaemon(true) 必须用在 start 之前,不然会抛出异常IllegalThreadStateException
  • 守护线程中产生的新的线程也是守护线程
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断
@Slf4j
public class Daemon_md {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> log.info("t1.isDaemon: {}", Thread.currentThread().isDaemon()), "t1");
        Thread t2 = new Thread(()->{
            t1.start();
            Thread t3 = new Thread("t3");
            t3.start();
            log.info("t2.isDaemon: {}", Thread.currentThread().isDaemon());
            log.info("t3.isDaemon: {}", t3.isDaemon());
        }, "t2");
        t2.setDaemon(true);
        t2.start();
        log.info("main.isDaemon: {}", Thread.currentThread().isDaemon());
    }
}