java多线程之线程创建和状态

一、概述

1、上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现 这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切 换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是 便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第 多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文 切换也会影响多线程的执行速度。

2、多任务

当我们打开电脑,可以一边打开qq音乐听歌,一边打开浏览器浏览网页,还算可以上qq聊天。电脑是同时可以执行多个任务的,

CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:

这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样

类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。

3、进程和线程

计算中,把一个任务称为一个进程,如上面的qq是一个进程,浏览器也是一个进程,每个子任务称作一个线程,比如qq聊天打字的同时也可以接收消息,就是两个子任务即两个线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

  • 使用多进程
  • 使用单进程多线程
  • 使用多进程+多线程

具体采用哪种方式,要考虑到进程和线程的特点。

和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:

多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

4、多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

5、用户线程 守护线程

Java中线程分为用户线程和守护线程两种。用户线程是用户自定义的线程,当主线程停止用户线程不会停止,守护线程当进程不存在或者主线程停止,守护线程也会停止,通过setDaemon(true)将一个线程设置为守护线程

6、什么是JUC

在 Java 中, 线程部分是一个重点, 本篇文章说的 J UC 也是关于线程的。 J UC 就是 java.util . concurrent 工具包的简称。 这是一个处理线程的工具包, JDK 1 . 5 开始出现的。

7、串行、并发、并行

7.1、概念

串行:串行是一次只能取得一个任务,并执行这个任务

并发:指一个处理器同时处理多个任务。(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)

并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

并行,是每个cpu运行一个程序。

7.2、案例

1、并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。

2、多个人同时做一件事 ,多个人同时做不同的事

二、多线程创建

在Java中,我们是如何使⽤ 多线程的呢? ⾸先,我们需要有⼀个“线程”类。JDK提供了 Thread 类和 Runnalble 接⼝来让我们 实现⾃⼰的“线程”类。

1、继承Thread类

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        for (int i = 0; i < 100; i++) {
            String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(),
                    Thread.currentThread().getThreadGroup().getName(),i);
            System.out.println(log);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

2、实现Runbable接口

public class Test {
    public static void main(String[] args) {
        //也可以直接使用lamda表达式
        Thread thread = new Thread(new MyThread(),"myThread");
        thread.start();
        for (int i = 0; i < 100; i++) {
            String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(),
                    Thread.currentThread().getThreadGroup().getName(),i);
            System.out.println(log);
        }
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

Thread类常用的方法

currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;

start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;

yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿 意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield() ⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;

sleep():静态⽅法,使当前线程睡眠⼀段时间;

Thread.setPriority(int n) // 1~10, 默认值5 可以对线程设定优先级 ,优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

两种方式比较:

由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。

Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。

Runnable接⼝出现,降低了线程对象和线程任务的耦合性。 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量。

所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类

3、Callable、Future与FutureTask

目前我们学习了有两种创建线程的方法

一种是通过创建 Thread 类,另一种是 通过使用 Runnable 创建线程。但是,Runnable 缺少的一项功能是,当线程 终止时(即 run()完成时),我们无法使线程返回结果。为了支持此功能, Java 中提供了 Callable 接口。现在我们学习的是创建线程的第三种方案---Callable 接口

Callable 接口的特点如下

  • 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于 Callable,需要实现在完成时返回结果的 call()方法
  • call()方法可以引发异常,而 run()则不能
  • 为实现 Callable 而必须重写 call 方法
  • 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable

由于因为 Thread 类的构造方法根本没有 Callable,那⼀般是怎么使⽤ Callable 的呢? Callable ⼀般是配合线程池⼯ 具 ExecutorService 来使⽤的。我们会在后续章节解释线程池的使⽤。这⾥只介 绍 ExecutorService 可以使⽤ submit ⽅法来让⼀个 Callable 接⼝执⾏。它会返回 ⼀个 Future ,我们后续的程序可以通过这个 Future 的 get ⽅法得到结果。简单的使⽤demo

// ⾃定义Callable
class Task implements Callable<Integer>{
 @Override
 public Integer call() throws Exception {
 // 模拟计算需要⼀秒
 Thread.sleep(1000);
 return 2;
 }
 public static void main(String args[]){
 // 使⽤
 ExecutorService executor = Executors.newCachedThreadPool();
 Task task = new Task();
 Future<Integer> result = executor.submit(task);
 // 注意调⽤get⽅法会阻塞当前线程,直到得到结果。
 // 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。
 System.out.println(result.get()); 
 }
}

当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可 以知道该线程返回的结果。为此,可以使用 Future 对象 ,将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦 Callable 返回)。Future 基本上是主线程可以跟踪进度以及其他线程的结果的 一种方式。要实现此接口,必须重写 5 种方法

Future 接⼝只有⼏个⽐较简单的⽅法

public abstract interface Future<V> {
 public abstract boolean cancel(boolean paramBoolean);
 public abstract boolean isCancelled();
 public abstract boolean isDone();
 public abstract V get() throws InterruptedException, ExecutionException;
 public abstract V get(long paramLong, TimeUnit paramTimeUnit)
 throws InterruptedException, ExecutionException, TimeoutException;
}

 cancel ():⽅法是试图取消⼀个线程的执⾏。 注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些 其它因素不能取消,存在取消失败的可能。用于停止任务。 如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true 时才会中断任务。 boolean 类型的返回值是“是否取消成 功”的意思。参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。 所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable 。 如果为了可取消性⽽使⽤ Future 但⼜不提供可⽤的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。

get():抛出 InterruptedException,ExecutionException: 用于获取任务的结果。 如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。

isDone():如果任务完成,则返回 true,否则返回 false 可以看到 Callable 和 Future 做两件事-Callable 与 Runnable 类似,因为它封 装了要在另一个线程上运行的任务,而 Future 用于存储从另一个线程获得的结 果。实际上,future 也可以与 Runnable 一起使用。

FutureTask

上⾯介绍了 Future 接⼝。这个接⼝有⼀个实现类叫 FutureTask 。 FutureTask 是 实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝,该类型实现 Runnable 和 Future,并方 便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建 FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建 Thread 对象。因此,间接地使用 Callable 创建线程,如下案例

public class MyThread implements Callable<Long> {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		MyThread yMyThread = new MyThread();
		FutureTask<Long> futureTask = new FutureTask<Long>(yMyThread);
		new Thread(futureTask).start();
		Long timeCurrent = futureTask.get();
		System.out.println(timeCurrent);
	}

	@Override
	public Long call() throws Exception {
		Thread.sleep(2000);
		return System.currentTimeMillis();
	}
}

当然futuretask也可以结合线程池一起使用,如下案例

import java.util.concurrent.*;
public class Test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executorService.submit(futureTask);
        executorService.shutdown();
        
        System.out.println("主线程在执行任务...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
         
        try {
            System.out.println("task运行结果:"+futureTask.get());
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } catch (ExecutionException ex) {
            ex.printStackTrace();
        }
         
        System.out.println("所有任务执行完毕");
    }
}
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在执行任务...");
        //模拟任务耗时
        Thread.sleep(5000);
        return 1000;
    }
}

FutureTask的⼏个状态

/**
 *
 * state可能的状态转变路径如下:
 * NEW -> COMPLETING -> NORMAL
 * NEW -> COMPLETING -> EXCEPTIONAL
 * NEW -> CANCELLED
 * NEW -> INTERRUPTING -> INTERRUPTED
 */
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
state表示任务的运⾏状态,初始状态为NEW。运⾏状态只会在set、
setException、cancel⽅法中终⽌。COMPLETING、INTERRUPTING是任
务完成后的瞬时状态。

小结:

1、在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态

2、一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去 获取结果

3、仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计 算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成 时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异 常。

三、线程组和线程优先级

1、线程组

Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。 ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必 然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组

public class Demo {
 public static void main(String[] args) {
 Thread testThread = new Thread(() -> {
 System.out.println("testThread当前线程组名字:" +
 Thread.currentThread().getThreadGroup().getName());
 System.out.println("testThread线程名字:" +
 Thread.currentThread().getName());
 });
 testThread.start();
 System.out.println("执⾏main⽅法线程名字:" + Thread.currentThread().getNa
 }
}

结果

执⾏main⽅法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0

 ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状 结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回 收

2、线程优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给 操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系 统决定。

Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会 在线程被调⽤之前设定。 通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们 使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。

public class Demo {
 public static void main(String[] args) {
 Thread a = new Thread();
 System.out.println("我是默认线程优先级:"+a.getPriority());
 Thread b = new Thread();
 b.setPriority(10);
 System.out.println("我是设置过的线程优先级:"+b.getPriority());
 }
}

 结果

我是默认线程优先级:5
我是设置过的线程优先级:10

 既然有1-10的级别来设定了线程的优先级,这时候可能有些读者会问,那么我是不 是可以在业务实现的时候,采⽤这种⽅法来指定⼀些线程执⾏的先后顺序? 对于这个问题,我们的答案是:No! Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给 操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的 线程调度算法决定的。

Java提供⼀个线程调度器来监视和控制处于RUNNABLE状态的线程。线程的调度 策略采⽤抢占式,优先级⾼的线程⽐优先级低的线程会有更⼤的⼏率优先执⾏。在 优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有⼀个默认的主线 程,就是通过JVM启动的第⼀个线程main线程。 还有⼀种线程称为守护线程(Daemon),守护线程默认的优先级⽐较低

如果某线程是守护线程,那如果所有的⾮守护线程结束,这个守护线程也会 ⾃动结束。 应⽤场景是:

当所有⾮守护线程结束时,结束其余的⼦线程(守护线程)⾃ 动关闭,就免去了还要继续关闭⼦线程的麻烦。

⼀个线程默认是⾮守护线程,可以通过Thread类的setDaemon(boolean on) 来设置。

 在之前,我们有谈到⼀个线程必然存在于⼀个线程组中,那么当线程和线程组的优 先级不⼀致的时候将会怎样呢?

public static void main(String[] args) {
 ThreadGroup threadGroup = new ThreadGroup("t1");
 threadGroup.setMaxPriority(6);
 Thread thread = new Thread(threadGroup,"thread");
 thread.setPriority(9);
 System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());
 System.out.println("我是线程的优先级"+thread.getPriority());
}

结果 

我是线程组的优先级6
我是线程的优先级6

如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先 级将会失效,取⽽代之的是线程组的最⼤优先级。

3、线程组常见的方法

//获取线程组名字
Thread.currentThread().getThreadGroup().getName()

//复制线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
TheadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);

//线程组统⼀异常处理

public class ThreadGroupDemo {
 public static void main(String[] args) {
     ThreadGroup threadGroup1 = new ThreadGroup("group1") {
     // 继承ThreadGroup并重新定义以下⽅法
     // 在线程成员抛出unchecked exception
     // 会执⾏此⽅法
         public void uncaughtException(Thread t, Throwable e) {
             System.out.println(t.getName() + ": " + e.getMessage());
         }
     };
     // 这个线程是threadGroup1的⼀员
     Thread thread1 = new Thread(threadGroup1, new Runnable() {
         public void run() {
             // 抛出unchecked异常
                 throw new RuntimeException("测试异常");
             }
     });
     thread1.start();
   }
}

4、线程组的数据结构

 线程组还可以包含其他的线程组,不仅仅是线程。源码如下

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
 private final ThreadGroup parent; // ⽗亲ThreadGroup
 String name; // ThreadGroupr 的名称
 int maxPriority; // 线程最⼤优先级
 boolean destroyed; // 是否被销毁
 boolean daemon; // 是否守护线程
 boolean vmAllowSuspension; // 是否可以中断
 int nUnstartedThreads = 0; // 还未启动的线程
 int nthreads; // ThreadGroup中线程数⽬
 Thread threads[]; // ThreadGroup中的线程
 int ngroups; // 线程组数⽬
 ThreadGroup groups[]; // 线程组数组
// 私有构造函数
private ThreadGroup() { 
 this.name = "system";
 this.maxPriority = Thread.MAX_PRIORITY;
 this.parent = null;
}
// 默认是以当前ThreadGroup传⼊作为parent ThreadGroup,新线程组的⽗线程组是⽬前正在运⾏线
public ThreadGroup(String name) {
 this(Thread.currentThread().getThreadGroup(), name);
}
// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
 this(checkParentAccess(parent), parent, name);
}
// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
 this.name = name;
 this.maxPriority = parent.maxPriority;
 this.daemon = parent.daemon;
 this.vmAllowSuspension = parent.vmAllowSuspension;
 this.parent = parent;
 parent.add(this);
}

}

第三个构造函数⾥调⽤了 checkParentAccess ⽅法,这⾥看看这个⽅法的源码

// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
 parent.checkAccess();
 return null;
}
// 判断当前运⾏的线程是否具有修改线程组的权限
public final void checkAccess() {
 SecurityManager security = System.getSecurityManager();
 if (security != null) {
 security.checkAccess(this);
 }
}

 这⾥涉及到 SecurityManager 这个类,它是Java的安全管理器,它允许应⽤ 程序在执⾏⼀个可能不安全或敏感的操作前确定该操作是什么,以及是否是 在允许执⾏该操作的安全上下⽂中执⾏它。应⽤程序可以允许或不允许该操 作。 ⽐如引⼊了第三⽅类库,但是并不能保证它的安全性。 其实Thread类也有⼀个checkAccess()⽅法,不过是⽤来当前运⾏的线程是 否有权限修改被调⽤的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)

总结来说,线程组是⼀个树状的结构,每个线程组下⾯可以有多个线程或者线程 组。线程组可以起到统⼀控制线程的优先级和检查线程的权限的作⽤

四、线程状态

1、线程状态概述

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态在java中是有个枚举类如下:

public enum State {
 NEW,//新创建的线程,尚未执行;
 RUNNABLE,//运行中的线程,正在执行run()方法的Java代码;
 BLOCKED,//运行中的线程,因为某些操作被阻塞而挂起;
 WAITING,//运行中的线程,因为某些操作在等待中;
 TIMED_WAITING,//运行中的线程,因为执行sleep()方法正在计时等待;
 TERMINATED;//线程已终止,因为run()方法执行完毕。
}

用一个状态转移图表示如下:

         ┌─────────────┐
         │     New     │
         └─────────────┘
                │
                ▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
 ┌─────────────┐ ┌─────────────┐
││  Runnable   │ │   Blocked   ││
 └─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
 │   Waiting   │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                │
                ▼
         ┌─────────────┐
         │ Terminated  │
         └─────────────┘

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

详细转换图如下

NEW

处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法

private void testStateNew() {
 Thread thread = new Thread(() -> {});
 System.out.println(thread.getState()); // 输出 NEW 
}

从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。

关于start()的两个引申问题

1. 反复调⽤同⼀个线程的start()⽅法是否可⾏?

2. 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程 的start()⽅法是否可⾏?

public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
               
            }
        }
    }

我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。

我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:

@Test
public void testStartMethod() {
 Thread thread = new Thread(() -> {});
 thread.start(); // 第⼀次调⽤
 thread.start(); // 第⼆次调⽤
}

我是在start()⽅法内部的最开始打的断点,叙述下在我这⾥打断点看到的结果: 第⼀次调⽤时threadStatus的值是0。 第⼆次调⽤时threadStatus的值不为0。 查看当前线程状态的源码:

// Thread.getState⽅法源码:
public State getState() {
 // get current thread state
 return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
 if ((var0 & 4) != 0) {
 return State.RUNNABLE;
 } else if ((var0 & 1024) != 0) {
 return State.BLOCKED;
 } else if ((var0 & 16) != 0) {
 return State.WAITING;
 } else if ((var0 & 32) != 0) {
 return State.TIMED_WAITING;
 } else if ((var0 & 2) != 0) {
 return State.TERMINATED;
 } else {
 return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
 }
}

两个问题的答案都是不可⾏,在调⽤⼀次start()之后,threadStatus的值会改 变(threadStatus !=0),此时再次调⽤start()⽅法会抛出 IllegalThreadStateException异常。 ⽐如,threadStatus为2代表当前线程状态为TERMINATED。

RUNNABLE

可运行状态,可运行状态可以包括:运行中状态和就绪状态。

Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和 running两个状态的。

BLOCKED

阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进⼊同步区。 我们⽤BLOCKED状态举个⽣活中的例⼦:

假如今天你下班后准备去⻝堂吃饭。你来到⻝堂仅有的⼀个窗⼝,发现前⾯ 已经有个⼈在窗⼝前了,此时你必须得等前⾯的⼈从窗⼝离开才⾏。 假设你是线程t2,你前⾯的那个⼈是线程t1。此时t1占有了锁(⻝堂唯⼀的 窗⼝),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。

WAITING

等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。 调⽤如下3个⽅法会使线程进⼊等待状态: Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;

Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印startt线程再打印hellomain线程最后再打印end

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。

你等了好⼏分钟现在终于轮到你了,突然你们有⼀个“不懂事”的经理突然来 了。你看到他你就有⼀种不祥的预感,果然,他是来找你的。 他把你拉到⼀旁叫你待会⼉再吃饭,说他下午要去作报告,赶紧来找你了解 ⼀下项⽬的情况。你⼼⾥虽然有⼀万个不愿意但是你还是从⻝堂窗⼝⾛开 了。 此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗 ⼝)了,“不速之客”来了你还是得释放掉锁。此时你t2的状态就是 WAITING。然后经理t1获得锁,进⼊RUNNABLE状态。 要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能⼀直等待 了。

TIMED_WAITING

超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。 调⽤如下⽅法会使线程进⼊超时等待状

1、Thread.sleep(long millis):使当前线程睡眠指定时间

2、Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;

3、Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则 会⼀直执⾏;

4、LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线 程进⾏线程调度指定时间

5、LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时 间;

到了第⼆天中午,⼜到了饭点,你还是到了窗⼝前。 突然间想起你的同事叫你等他⼀起,他说让你等他⼗分钟他改个bug。 好吧,你说那你就等等吧,你就离开了窗⼝。很快⼗分钟过去了,你⻅他还 没来,你想都等了这么久了还不来,那你还是先去吃饭好了。 这时你还是线程t1,你改bug的同事是线程t2。t2让t1等待了指定时间,t1先 主动释放了锁。此时t1等待期间就属于TIMED_WATING状态。 t1等待10分钟后,就⾃动唤醒,拥有了去争夺锁的资格。

TERMINATED

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

终⽌状态。此时线程已执⾏完毕。

2、线程状态的转换

我们从上面可以看到线程之间状态切换主要就是围绕RUNNABLE与BLOCKED、RUNNABLE与WAITING、TIMED_WAITING与RUNNABLE。下面从代码案例分析这三个状态转换

RUNNABLE与BLOCKED

处于BLOCKED状态的线程是因为在等待锁的释放。假如这⾥有 两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状 态

public void blockedTest() {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        }, "a");
        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        }, "b");
        a.start();
        b.start();
        System.out.println(a.getName() + ":" + a.getState()); // 输出?
        System.out.println(b.getName() + ":" + b.getState()); // 输出?
    }
    // 同步⽅法争夺锁
    private synchronized void testMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 ⼤家可能会觉得线程a会先调⽤同步⽅法,同步⽅法内⼜调⽤了 Thread.sleep()⽅法,必然会输出TIMED_WAITING,⽽线程b因为等待线程a释放 锁所以必然会输出BLOCKED。 其实不然,有两点需要值得⼤家注意,⼀是在测试⽅法blockedTest()内还有⼀个 main线程,⼆是启动线程后执⾏run⽅法还是需要消耗⼀定时间的。不打断点的情 况下,上⾯代码中都应该输出RUNNABLE

测试⽅法的main线程只保证了a,b两个线程调⽤start()⽅法(转化为 RUNNABLE状态),还没等两个线程真正开始争夺锁,就已经打印此时两个 线程的状态(RUNNABLE)了。

要是我想要打印出BLOCKED状态我该怎么处理呢?其实就 处理下测试⽅法⾥的main线程就可以了,你让它“休息⼀会⼉”,打断点或者调⽤ Thread.sleep⽅法就⾏。 这⾥需要注意的是main线程休息的时间,要保证在线程争夺锁的时间内,不要等到 前⼀个线程锁都释放了你再去争夺锁,此时还是得不到BLOCKED状态的。 我们把上⾯的测试⽅法blockedTest()改动⼀下:

public void blockedTest() throws InterruptedException {
 ······
 a.start();
 Thread.sleep(1000L); // 需要注意这⾥main线程休眠了1000毫秒,⽽testMethod()⾥休眠了2000毫秒
 b.start();
 System.out.println(a.getName() + ":" + a.getState()); // 输出?
 System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

在这个例⼦中,由于main线程休眠,所以线程a的run()⽅法跟着执⾏,线程b再接 着执⾏。 在线程a执⾏run()调⽤testMethod()之后,线程a休眠了2000ms(注意这⾥是没有 释放锁的),main线程休眠完毕,接着b线程执⾏的时候是争夺不到锁的,所以这 ⾥输出:

a:TIMED_WAITING
b:BLOCKED

RUNNABLE与WAITING

根据转换图我们知道有3个⽅法可以使线程从RUNNABLE状态转为WAITING状态。 我们主要介绍下Object.wait()和Thread.join()

调⽤wait()⽅法前线程必须持有对象的锁。 线程调⽤wait()⽅法时,会释放当前的锁,直到有其他线程调⽤ notify()/notifyAll()⽅法唤醒等待锁的线程。 需要注意的是,其他线程调⽤notify()⽅法只会唤醒单个等待锁的线程,如有 有多个线程都在等待这个锁的话不⼀定会唤醒到之前调⽤wait()⽅法的线程。 同样,调⽤notifyAll()⽅法唤醒所有等待锁的线程之后,也不⼀定会⻢上把时 间⽚分给刚才放弃锁的那个线程,具体要看系统的调度。

Thread.join(),调⽤join()⽅法不会释放锁,会⼀直等待当前线程执⾏完毕(转换为 TERMINATED状态),我们再把上⾯的例⼦线程启动那⾥改变⼀下:

public void blockedTest() {
 ······
 a.start();
 a.join();
 b.start();
 System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
 System.out.println(b.getName() + ":" + b.getState());
}

要是没有调⽤join⽅法,main线程不管a线程是否执⾏完毕都会继续往下⾛。 a线程启动之后⻢上调⽤了join⽅法,这⾥main线程就会等到a线程执⾏完毕,所以 这⾥a线程打印的状态固定是TERMIATED。⾄于b线程的状态,有可能打印RUNNABLE(尚未进⼊同步⽅法),也有可能打印 TIMED_WAITING(进⼊了同步⽅法)。

TIMED_WAITING与RUNNABLE

TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是 指定的。

Thread.sleep(long) 使当前线程睡眠指定时间。需要注意这⾥的“睡眠”只是暂时使线程停⽌执 ⾏,并不会释放锁。时间到后,线程会重新进⼊RUNNABLE状态。

Object.wait(long) wait(long)⽅法使线程进⼊TIMED_WAITING状态。这⾥的wait(long)⽅法与 ⽆参⽅法wait()相同的地⽅是,都可以通过其他线程调⽤notify()或notifyAll() ⽅法来唤醒。 不同的地⽅是,有参⽅法wait(long)就算其他线程不来唤醒它,经过指定时间 long之后它会⾃动唤醒,拥有去争夺锁的资格。

Thread.join(long) join(long)使当前线程执⾏指定时间,并且使线程进⼊TIMED_WAITING状 态。 我们再来改⼀改刚才的示例:

public void blockedTest() {
 ······
 a.start();
 a.join(1000L);
 b.start();
 System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_W
 System.out.println(b.getName() + ":" + b.getState());
}

这⾥调⽤a.join(1000L),因为是指定了具体a线程执⾏的时间的,并且执⾏时 间是⼩于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。 b线程状态仍然不固定(RUNNABLE或BLOCKED) 

 五、线程中断

当执行一个很耗时的任务时,比如下载文件,用户随时可能取消下载,当前取消下载,我们应在服务端中断当前下载文件的线程。

中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

简单介绍下Thread类⾥提供的关于线程中断的⼏个⽅法:

  • Thread.interrupt():中断线程。这⾥的中断线程并不会⽴即停⽌线程,⽽是设 置线程的中断状态为true(默认是flase);
  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个⽅法 的影响,意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这 个线程的中断状态重新转为false;
  • Thread.isInterrupted():测试当前线程是否被中断。与上⾯⽅法不同的是调⽤ 这个⽅法并不会影响线程的中断状态。

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 启动hello线程
        try {
            hello.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记(volatile先做了解,后续文章详细讲解),确保每个线程都能读取到更新后的变量值。

为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
           Main Memory
│                               │
   ┌───────┐┌───────┐┌───────┐
│  │ var A ││ var B ││ var C │  │
   └───────┘└───────┘└───────┘
│     │ ▲               │ ▲     │
 ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
      │ │               │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐   ┌ ─ ─ ┼ ┼ ─ ─ ┐
      ▼ │               ▼ │
│  ┌───────┐  │   │  ┌───────┐  │
   │ var A │         │ var C │
│  └───────┘  │   │  └───────┘  │
   Thread 1          Thread 2
└ ─ ─ ─ ─ ─ ─ ┘   └ ─ ─ ─ ─ ─ ─ ┘

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

小结

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException

目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;

通过标志位判断需要正确使用volatile关键字;

volatile关键字解决了共享变量在线程间的可见性问题;

六、线程执行顺序

调用Thread的start()方法启动线程时,线程的执行顺序是不确定的。也就是说,在同一个方法中,连续创建多个线程后,调用线程 的start()方法的顺序并不能决定线程的执行顺序。 例如,这里,看一个简单的示例程序,如下所示。

public class ThreadSort01 {
public static void main(String[] args){
    Thread thread1 = new Thread(() -> {
        System.out.println("thread1");
    });
    Thread thread2 = new Thread(() -> {
        System.out.println("thread2");
    });
    Thread thread3 = new Thread(() -> {
        System.out.println("thread3");
    });
    thread1.start();
    thread2.start();
    thread3.start();
    }
}

多次执行上面代码,可以看到,每次运行程序时,线程的执行顺序可能不同。线程的启动顺序并不能决定线程的执行顺序

在实际业务场景中,有时,后启动的线程可能需要依赖先启动的线程执行完成才能正确的执行线程中的业务逻辑。此时,就需要确 保线程的执行顺序。那么如何确保线程的执行顺序呢? 可以使用Thread类中的join()方法来确保线程的执行顺序

public class ThreadSort01 {
public static void main(String[] args){
    Thread thread1 = new Thread(() -> {
        System.out.println("thread1");
    });
    Thread thread2 = new Thread(() -> {
        System.out.println("thread2");
    });
    Thread thread3 = new Thread(() -> {
        System.out.println("thread3");
    });
    thread1.start();
    thread1.join();
    thread2.start();
    thread2.join();
    thread3.start();
    thread3.join();
    }
}

 可以看到,每次运行的结果都是相同的,所以,使用Thread的join()方法能够保证线程的先后执行顺序

既然Thread类的join()方法能够确保线程的执行顺序,我们就一起来看看Thread类的join()方法到底是个什么鬼。 进入Thread的join()方法,如下所示

public final void join() throws InterruptedException {
join(0);
}

 可以看到join()方法调用同类中的一个有参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");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

可以看到,有一个long类型参数的join()方法使用了synchroinzed修饰,说明这个方法同一时刻只能被一个实例或者方法调用。由 于,传递的参数为0,所以,程序会进入如下代码逻辑

if (millis == 0) {
while (isAlive()) {
wait(0);
}
}

首先,在代码中以while循环的方式来判断当前线程是否已经启动处于活跃状态,如果已经启动处于活跃状态,则调用同类中的 wait()方法,并传递参数0。继续跟进wait()方法,如下所示。

public final native void wait(long timeout) throws InterruptedException

可以看到,wait()方法是一个本地方法,通过JNI的方式调用JDK底层的方法来使线程等待执行完成。 需要注意的是,调用线程的wait()方法时,会使主线程处于等待状态,等待子线程执行完成后再次向下执行。也就是说,在 ThreadSort02类的main()方法中,调用子线程的join()方法,会阻塞main()方法的执行,当子线程执行完成后,main()方法会继 续向下执行,启动第二个子线程,并执行子线程的业务逻辑,以此类推。 

七、守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程

class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?

然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

答案是使用守护线程(Daemon Thread)。

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

因此,JVM退出时,不必关心守护线程是否已结束。

如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

小结

守护线程是为其他线程服务的线程;

所有非守护线程都执行完毕后,虚拟机退出;

守护线程不能持有需要关闭的资源(如打开文件等);

参考

https://www.liaoxuefeng.com/wiki/1252599548343744/1306580767211554

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序三两行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值