并发基础

162 阅读19分钟

1.并发基础

Java从诞生开始,其就已经内置了对于多线程的支持。当多个线程能够同时执行时,大多数情况下都能够显著提升系统性能,尤其现在的计算机普遍都是多核,所以性能的提升会更加明显。但是,多线程在使用中也需要注意诸多的问题,如果使用不当,也会对系统性能造成非常严重的影响。

1.1并发编程核心概念

要理解并发编程,务必要先理解三个概念,分别为:原子性、可见性、有序性。

1.1.1原子性

所谓原子性即:一个或多个操作,要么全部执行并并且执行的过程不会被任何因素打断,要么就都不执行

在原子操作中,本质上拒绝多线程操作的,无论是单核还是多核服务器,当要对某一个数据进行原子操作时,同一时刻只有有一个线程能够对其进行操作,简单来说,在整个操作过程中不会被线程调度器打断,如a=1就是一个原子操作,但a++则不是一个原子操作,其内部会产生一个新的Integer对象。

举个例子,假设对一个32位的变量赋值,操作分为两步:低16位赋值、高16位赋值。当线程A对低16位数据写入成功后,线程A被打断。而此时另外的线程B去读取a的值,那么读取到的就是错误的数据。

在Java的原子性操作包括:

  1. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性的

  2. 所有引用的赋值操作

  3. java.concurrent.Atomic.* 包中所有原子操作类的一切操作。

1.1.2可见性

所谓可见性:即当多个线程访问同一个共享变量时,一个线程修改了该共享变量的值后,其他线程能够立即查看到修改后的值。

多线程环境下,一个线程对共享变量的操作对其他线程是默认不可见的,也就是说一个线程对某一共享变量的修改,默认其它线程是无法进行查看的。而如果要做到可见,Java的volatile、synchronized、lock都能保证可见性。如一个变量被volatile修饰后,表示当一个线程修改共享变量后,其会立即被更新到内存中,其它线程读取共享变量时,会直接从主内存中读取。而Synchronized和lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前将会对变量的修改刷新到主内存当中。因此可以保证可见性。

public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"线程启动了");
                while(true) {
                    //当a=3 跳出死循环
                    if(a==3) {
                        break;
                    }
                }
                System.out.println(Thread.currentThread().getName()+"线程停止了");
            }
        },"t2").start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"线程启动了");
                while(true) {
                    //当a=3 跳出死循环
                    if(a==3) {
                        break;
                    }
                }
                System.out.println(Thread.currentThread().getName()+"线程停止了");
            }
        },"t1").start();

        Thread.sleep(1000);
        //在主线程里边修改变量,测试其他线程是否对这个修改可见
        a=3;
    }

通过上述程序运行结果,可以发现如果没有对变量加volatile关键字,则线程1和线程2会进入死循环,因为在主线程中修改a,线程1和线程2是无法感知的,而对变量a加入了volatile关键字,则不会死循环,因为其已经可以保证线程的可见性。

1.1.3有序性

所谓有序性:即程序执行的顺序会按照代码的先后顺序执行。

其可以理解为在本线程内,所有的操作都是有序的。而如果在A线程中观察B线程,所有操作都是无序的。在JMM(Java 内存 模型)中为了提升程序的执行效率,允许编译器和处理器对指令重排。对于单线程来说,指令重排并不会产生问题,而在多线程下则不可以(单例模式的双重检测就是解决此指令重排的问题)。

在Java中可以通过synchronized和lock来保证有序性,synchronized和lock保证每个时刻都是有有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外还可以通过volatile来保证有序性。最著名的例子就是单例模式的DCL(双重检查锁)

public class Singleton {

    private volatile static Singleton instance;

    public static Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (null == instance){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

对于并发编程来说,要想保证程序的正确性,对于原子性,可见性,有序性的保证是非常重要的!!!

1.2进程 线程

1.2.1 什么是进程

进程可以理解为就是应用程序的启动实例。如微信、idea、Navicat等,当打开它们后,就相当于开启了一个进程。每个进程都会在操作系统中拥有独立的内存空间、地址、文件资源、数据资源等。进程是资源分配和管理的最小单位

image.png

1.2.2 什么是线程

线程从属于进程,是程序的实际执行者,一个进程中可以包含若干个线程,并且也可以把线程称为轻量级进程。每个线程都会拥有自己的计数器、堆栈、局部变量等属性。并且能够访问共享的内存变量。线程是操作系统(CPU)调度和执行的最小单位。cpu会在这些线程上来回切换(上下文切换),让使用者感觉线程是在同步执行。

image.png

1.2.3 线程使用带来的问题

有很多人存在一个误区,在代码中使用多线程,一定会给系统带来性能的提升,这个观点是错误的。并发编程的目的是为了让程序的运行更快,但是绝对不是说启动的线程越多,性能的提升就越大,其会受到很多因素的影响,如锁的问题,线程状态的切换问题、线程上下文切换问题,还会受到硬件资源的影响,如CPU核数。

1.2.3.1 什么叫做线程的上下文切换

不管是在多核或单核处理器中,都是能够以多线程形式执行代码的,CPU通过给每个线程分配CPU时间片来实现线程执行期间的快速切换。所谓的时间片就是CPU分配给每个线程的执行时间,当某个线程获取到CPU时间片后,就会在一定时间内执行,当时间片到期,则线程就会进入到挂起等状态。时间片一般为几十毫秒,通过在CPU高速切换,让使用者感觉是在同步执行的。

同时要保证线程在切换的过程中,要记录线程被挂起时,已经执行了哪些命令、变量值是多少,那这点则是通过每个线程内部的程序计数器来保证的。

简单来说:线程从挂起到在加载的过程中,就是一次上下文切换。其是比较耗费资源的

引起上下文切换的几种情况;

  1. 时间片用完,CPU正常调度下一个任务。
  2. 被其它优先级更高的任务抢占
  3. 执行任务碰到IO阻塞,调度器挂起当前任务,切换执行下一个任务
  4. 用户代码主动挂起当前任务让出CPU时间
  5. 多任务抢占资源,由于没有抢到被挂起
  6. 硬件中断

1.3CPU时间片轮转机制&优化

之前已经提到了线程的执行,是依赖于CPU给每个线程分配的时间来进行的。在CPU时间片轮转机制中,如果一个线程的时间片到期,则CPU会挂起该线程并给另一个线程分配一定的时间片。如果说进程在时间片结束前阻塞或结束,则CPU会立即进行切换

时间片太短会导致频繁的进程切换,降低了CPU效率,而太长又可能引起对短的交互请求的响应变差。时间片为100ms通常是一个比较合理的折中。

1.4并行与并发的理解

所谓并发就是多个任务交替执行,一般都会附带一个时间单位,也就是所谓的在单位时间内的并发量有多少。

image.png

所谓并行即让多个任务能够同时执行。比如说,你可以一边上厕所,一边玩手机。

image.png

1.5线程启动和终止

线程的实现方式有两种:继承Thread类、实现Runnable接口。有些书籍说是实现Callable接口也算是。但是通过该接口定义线程并不是Java标准的定义方式,而是基于Future思想来完成。

image.png

那么Thread和Runnable有什么区别和联系呢?一般来说,Thread是对一个线程的抽象,而Runnable是对业务逻辑的抽象,并且Thread可以接受任意一个Runnable的实例并执行。

1.5.1 线程启动

/**
 * 新建线程
 */
public class NewThread {

    private static class UseThread extends Thread{
        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName()+": use thread");
        }
    }

    private static class UseRunnable implements Runnable{

        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName()+": use runnable");
        }
    }

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+": use main");

        UseThread useThread = new UseThread();
        useThread.start();

        Thread thread = new Thread(new UseRunnable());
        thread.start();
    }
}

优化:启动线程前,最好为这个线程设置特定的线程名称,这样出现问题时,给开发人员一些提示,可以快速定位到问题的所在

1.6.2 线程终止

线程在正常下当run执行完毕,或出现异常都会让该线程终止

1.6.2.1 理解suspend() resume()stop()

这三个方法对应的是暂停、恢复、结束。对于这三个方法的使用效果演示如下:

public class Srs {
    private static class MyThread implements Runnable{
        @Override
        public void run() {
            DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

            while (true){

                System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread());

        //开启线程
        System.out.println("开启线程");
        thread.start();
        TimeUnit.SECONDS.sleep(3);

        //暂停线程
        System.out.println("暂停线程");
        thread.suspend();
        TimeUnit.SECONDS.sleep(3);

        //恢复线程
        System.out.println("恢复线程");
        thread.resume();
        TimeUnit.SECONDS.sleep(3);

        //中止线程
        System.out.println("中止线程");
        thread.stop();
    }
}

执行结果:

image.png

可以看到这三个方法,很好的完成了本职工作。但是这三个方法已经在Java的源码中标记为过期方法,这是为什么呢?

当调用suspend()时,线程不会将当前持有的资源释放(如锁),而是占有着资源进入到暂停状态,这样的话容易造成死锁的问题。

public class Srs {

    private static  Object obj = new Object();//作为一个锁

    private static class MyThread implements Runnable{


        @Override
        public void run() {

            synchronized (obj){

                DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

                while (true){
                    System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));

                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }


    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"正常线程");
        Thread thread1 = new Thread(new MyThread(),"死锁线程");

        //开启线程
        thread.start();
        TimeUnit.SECONDS.sleep(3);

        //暂停线程
        thread.suspend();
        System.out.println("暂停线程");
        
        thread1.start();
        TimeUnit.SECONDS.sleep(3);
    }
}

在上述代码中,正常线程持有了锁,当调用suspend方法时,因为该方法不会释放锁,所以导致线程死锁,因为获取不到锁而导致无法执行。

当调用stop()时,会立即停止run()中剩余的操作。因此可能会导致一些的工作得不到完成,如文件流,数据库等关闭。并且会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。

public class StopProblem {

    public static void main(String[] args) throws Exception {

        TestObject testObject = new TestObject();

        Thread t1 = new Thread(() -> {
            try {
                testObject.print("1", "2");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        t1.start();
        //让子线程有执行时间
        Thread.sleep(1000);
        t1.stop();
        System.out.println("first : " + testObject.getFirst() + " " + "second : " + testObject.getSecond());
    }
}

class TestObject {

    private String first = "ja";
    private String second = "va";

    public synchronized void print(String first, String second) throws Exception {
        System.out.println(Thread.currentThread().getName());
        this.first = first;
        //2.模拟数据不一致
        TimeUnit.SECONDS.sleep(3);
        this.second = second;
    }

    public String getFirst() {
        return first;
    }

    public String getSecond() {
        return second;
    }
}

输出结果:

image.png

1.6.2.2 线程中止的安全且优雅姿势

Java对于线程安全中止设计了一个中断属性,其可以理解是线程的一个标识位属性。它用于表示一个运行中的线程是否被其他线程进行了中断操作。好比其他线程对这个线程打了一个招呼,告诉它你该中断了。通过interrupt() 实现。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            while (true){

                System.out.println(Thread.currentThread().getName()+" is running");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

添加该方法后,会出现一个异常,但是可以发现并不会线程的继续执行。

线程通过检查自身是否被中断来进行响应,可以通过isInterrupted() 进行判断,如果返回值为true,代表添加了中断标识,返回false,代表没有添加中断标识。通过它可以对线程进行中断操作。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            //while (true){
            while (!Thread.currentThread().isInterrupted()){

                System.out.println(Thread.currentThread().getName()+" is running");
            }

            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

对线程中断属性的判断,可以利用其进行线程执行的中断操作。

线程也可以通过静态方法Thread.interrupted() 查询线程是否被中断,并对中断标识进行复位,如果该线程已经被添加了中断标识,当使用了该方法后,会将线程的中断标识由true改为false。

image.png

public class InterruptDemo {

    private static class MyThread implements Runnable{
        @Override
        public void run() {
            //while (true){
            //while (!Thread.currentThread().isInterrupted()){
            while (!Thread.interrupted()){
                System.out.println(Thread.currentThread().getName()+" is running");
            }
            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyThread(),"myThread");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        thread.interrupt();
    }
}

同时要注意:处于死锁下的线程,无法被中断

1.7深入线程操作常见方法

1.7.1 理解run()和start()方法

这两个方法都可以启动线程,但是它俩有本质的区别的。当线程执行了start()方法后,才真正意义上的启动线程,其会让一个线程进入就绪状态等待分配CPU时间片,分到时间片后才会调用run().注意,同一个线程的start()方法不能被重复调用,否则会出现异常,因为重复调用了start()方法,线程的state就不是new了,那么threadStatus就不等于0.

//start源码分析
public synchronized void start() {
    /**
        Java里面创建线程之后,必须要调用start方法才能创建一个线程,该方法会通过虚拟机启动一个本地线程,本地线程的创建会调用当前系统去创建线程的方法进行创建线程。
        最终会调用run()将线程真正执行起来
        0这个状态,等于‘New’这个状态。
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* 线程会加入到线程分组,然后执行start0() */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

image.png

而run()则仅仅是一个普通方法,与类中的成员方法意义相同。在该方法中可以实现线程执行的业务逻辑。但并不会以异步的方式将线程启动,换句话说就是并不会去开启一个新的线程。其可以单独执行,也可以重复执行

1.7.2 wait() notify()

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。

注意:一定要在线程同步中使用,并且是同一个锁的资源

wait和notify方法例子,打开关闭开关:

public class WaitNotify {

    static boolean flag = false;
    static Object lock = new Object();


    //创建等待线程
    static class WaitThread implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                while (!flag){
                    //条件不满足,进入等待
                    System.out.println(Thread.currentThread().getName()+" flag is false,waiting");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //条件满足,退出等待
                System.out.println(Thread.currentThread().getName()+" flag is true");
            }
        }
    }

    static class NotifyThread implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+" hold lock");
                lock.notify();
                flag=true;

                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        new Thread(new WaitThread(),"wait").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(new NotifyThread(),"notify").start();
    }
}

image.png

1)WaitThread首先获取对象锁 2)WaitThread调用对象的wait()方法,放弃锁进入对象的等待队列WaitQueue,进行等待 3)由于WaitThread释放了对象锁,NotifyThread随机获取对象锁 4)NotifyThread获取对象锁成功后,调用notify()或notifyAll(),将WaitThread从等待队列WaitQueue移到同步队列SynchronizedQueue,此时WaitThread为阻塞状态。 5)NotifyThread释放锁后,WaitThread再次获取锁并从wait方法继续执行

1.7.3.1 等待通知范式

多线程的等待通知是一道非常常见的面试题,常见于笔试中。对于等待通知来说,需要有生产者(通知方)与消费者(等待方)

等待方:

  1. 获取对象锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
  3. 条件满足则执行对应的逻辑
synchronized(对象){
    while(条件不满足){
        对象.wait();
    }
    条件满足,执行业务逻辑。
}

通知方:

  1. 获取对象锁。
  2. 改变条件
  3. 通知等待在该对象上的线程
synchronized(对象){
    改变条件
    对象.notify()
}

1.7.4 wait与sleep的区别

对于sleep方法,首先要知道该方法是属于Thread类的,而wait()方法,则是属于Obejct类中

sleep()方法导致了程序暂停执行指定的时间,让出cpu调度其他线程,但是他的监控状态依然保持者,当指定的时间到了又会恢复运行状态。

wait()是把控制权交出去,然后进入等待此对象的等待锁定池处于等待状态,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态

在调用sleep方法的过程中,线程不会释放锁。而当调用wait()方法的时候,线程会释放锁。

1.7.5 理解yield()

当某个线程调用了这个方法后,该线程立即释放自己持有的时间片。线程会进入到就绪状态,同时CPU会重新选择一个线程赋予时间分片,但注意,调用了这个方法的线程,也有可能被CPU再次选中赋予执行。

而且该方法不会释放锁,如需释放锁的话,可以在调用该方法前自己动手释放。

public class YieldDemo {

    static class MyThread implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+" : "+i);
                if (i == 5){
                    System.out.println(Thread.currentThread().getName());
                    Thread.yield();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
    }
}

从结果看出,当调用了该方法后线程会让出自己的时间分片,但也有可能被再次选中执行。

Thread-3 0
Thread-1 0
Thread-5 0
Thread-5 1
Thread-5 2
Thread-5 3
Thread-5 4
Thread-5 5
Thread-5
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-1
Thread-1 6
Thread-1 7
Thread-1 8
Thread-1 9
Thread-3 1
Thread-3 2
Thread-3 3
Thread-3 4
Thread-3 5
Thread-3
Thread-5 6
Thread-5 7
Thread-5 8
Thread-5 9
Thread-3 6
Thread-3 7
Thread-3 8
Thread-3 9

1.7.6 理解join()

该方法的使用,在实际开发中,应用的是比较少的。但在面试中,常常伴随着产生一个问题,如何保证线程的执行顺序?就可以通过该方法来设置。

1.7.6.1 使用

当线程调用了该方法后,线程状态会从就绪状态进入到运行状态。

public class JoinDemo {

    private static class MyThread extends Thread{
        int i;
        Thread previousThread; //上一个线程
        public MyThread(Thread previousThread,int i){
            this.previousThread=previousThread;
            this.i=i;
        }

        @Override
        public void run() {
            //调用上一个线程的join方法. 不使用join方法解决是不确定的
            //previousThread.join();
            System.out.println("num:"+i);
        }
    }

    public static void main(String[] args) {
        Thread previousThread=Thread.currentThread();
        for(int i=0;i<10;i++){
            //每一个线程实现都持有前一个线程的引用。
            MyThread joinDemo=new MyThread(previousThread,i);
            joinDemo.start();
            previousThread=joinDemo;
        }
    }
}
num:0
num:2
num:3
num:6
num:7
num:1
num:5
num:9
num:8
num:4

可以等到开启了join之后,结果就是有序的了。

num:0
num:1
num:2
num:3
num:4
num:5
num:6
num:7
num:8
num:9

根据结果可以看到,当前线程需要等待previousThread线程终止之后才从thread.join返回。可以理解为,线程会在join处等待。

1.7.6.2 原理剖析

public final void join() throws InterruptedException {
    join(0);
}
...
    public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    //判断是否携带阻塞的超时时间,等于0表示没有设置超时时间
    if (millis == 0) { 
        //isAlive获取线程状态,无限等待直到previousThread线程结束
        while (isAlive()) {
            //调用Object中的wait方法实现线程的阻塞
            wait(0); 
        }
    } else { //阻塞直到超时
        while (isAlive()) { 
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

可以看到该方法是被synchronized修饰的,因为在其内部对于线程阻塞的实现,是通过Object中wait方法实现的,而要调用wait(),则必须添加synchronized。

总的来说,Thread.join其实底层是通过wait/notifyall来实现线程的通信达到线程阻塞的目的;当线程执行结束以后,会触发两个事情,第一个是设置native线程对象为null、第二个是通过notifyall方法,让等待在previousThread对象锁上的wait方法被唤醒。

1.8 线程优先级

操作对于线程执行,是通过CPU时间片来调用执行的,那么一个线程被分配的时间片的多少,就决定了其使用资源的多少,而线程的优先级就是决定线程需要能够使用资源多少的线程属性。

线程的优先级的范围是0-10.一个线程的默认优先级是5,可以在构建线程时,通过setPriority()修改该线程的优先级。优先级高的线程分配时间片的数量会高于优先级低的线程。

一般来说对于频繁阻塞的线程需要设置优先级高点,而偏重计算的线程优先级会设置低些,确保处理器不会被独占。

注意,线程优先级不能作为线程执行正确性的依赖,因为不同的操作系统可能会忽略优先级的设置。

image.png

1.9 守护线程

守护线程是一种支持型的线程,我们之前创建的线程都可以称之为用户线程。通过守护线程可以完成一些支持性的工作,如GC、分布式锁续期。守护线程会伴随着用户的结束而结束。

对于守护线程的创建,可以通过setDaemon()设置。

public class DaemonDemo {
    private static class MyThread implements Runnable{
        @Override
        public void run() {
            while (true){
              System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyThread());
        //设置守护线程
        thread.setDaemon(true);
        thread.start();
        TimeUnit.SECONDS.sleep(2);
    }
}

当线程实例没有被设置为守护线程时,该线程并不会随着主线程的结束而结束。但是当被设置为守护线程后,当主线程结束,该线程也会伴随着结束。同时守护线程不一定会执行finally代码块。所以当线程被设定为守护线程后,无法确保清理资源等操作一定会被执行。

1.10 线程状态

image.png