并发编程-线程基础
文章目录
零、说在前面
有必要为友友们简介一款极简的原生态AI:阿水AI6,需不需要都点点看看:👇👇👇
https://ai.ashuiai.com/auth/register?inviteCode=XT16BKSO3S
先看看美景养养眼,再继续以下乏味的学习,内容有点多,建议收藏分多次食用。
一、基础理论
1.1 同步&异步
同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。
1.2 并发(Concurrency)和并行(Parallelism)
并发: 并发说的是在一个时间段内,多件事情在这个时间段内交替执行。两个及两个以上的作业在同一 时间段 内执行。
并行: 并行说的是多件事情在同一个时刻同事发生。两个及两个以上的作业在同一 时刻 执行。
实际上,如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换多任务)。真实的并行也只可能出现在拥有多个CPU的系统中(比如多核CPU)。
1.3 临界区资源
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。在并行程序中,临界区资源是保护的对象
1.4 阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞通常用来形容很多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他线程阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。
1.5 死锁(Deadlock)、饥饿(Starvation)
1、死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
2、产生死锁的4个必要条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
3、死锁检测
- 使用
jmap
、jstack
等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,jstack
的输出中通常会有Found one Java-level deadlock:
的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top
、df
、free
等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。 - 采用 VisualVM、JConsole 等工具进行排查。
4、如何预防和避免死锁
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
<P1、P2、P3.....Pn>
序列为安全序列。
5、饥饿
饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。
二、进程和线程
2.1 进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。当main()方法结束后,主线程运行完成,JVM进程也随即退出。
一个进程由程序段、数据段和进程控制块三部分组成。
- 程序段一般也被称为代码段。代码段是进程的程序指令在内存中的位置,包含需要执行的指令集合;
- 数据段是进程的操作数据在内存中的位置,包含需要操作的数据集合;
- 程序控制块(ProgramControl Block,PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志。
- 进程的描述信息:主要包括:进程ID和进程名称,进程ID是唯一的,代表进程的身份;进程的状态,比如运行、就绪、阻塞;进程优先级,是进程调度的重要依据。
- 进程的调度信息:主要包括:程序起始地址,程序的第一行指令的内存地址,从这里开始程序的执行;通信信息,进程间通信时的消息队列。
- 进程的资源信息:主要包括:内存信息,内存占用情况和内存管理所用的数据结构;I/O设备信息,所用的I/O设备编号及相应数据结构;文件句柄,所打开文件的信息。
- 进程上下文:主要包括执行时各种CPU寄存器的值、当前的程序计数器(PC)的值以及各种栈的值等,即进程的环境。在操作系统切换进程时,当前进程被迫让出CPU,当前进程的上下文就保存在PCB结构中,供下次恢复运行时使用。
2.2 线程
早期的操作系统只有进程而没有线程。进程是程序执行和系统进行并发调度的最小单位。随着计算机的发展,CPU的性能越来越高,从早期的20MHz发展到了现在2GHz以上,从单核CPU发展到了多核CPU,性能提升了成千上万倍。为了充分发挥CPU的计算性能,提升CPU的硬件资源的利用率,同时弥补进程调度过于笨重产生的问题,进程内部演进出了并发调度的诉求,于是就发明了线程。
线程是“进程代码段”的一次的顺序执行流程。线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程是CPU调度的最小单位。 一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。
一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存。
程序计数器: 记录着线程下一条指令的代码段内存地址。
栈内存: 是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB大小的栈内存。栈内存和堆内存不同,栈内存不受垃圾回收器管理。
线程描述信息:
- 线程ID(Thread ID,线程标识符)。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
- 线程名称。主要是方便用户识别,用户可以指定线程的名字,如果没有指定,系统就会自动分配一个名称。
- 线程优先级。表示线程调度的优先级,优先级越高,获得CPU的执行机会就越大。
- 线程状态。表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
- …
2.3 线程与进程的区别
进程与线程的区别,主要有以下几点:
-
线程是“进程代码段”的一次的顺序执行流程。一个进程由一个或多个线程组成;一个进程至少有一个线程。
-
线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
-
线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥CPU的计算性能,又弥补了进程调度过于笨重的问题。
-
进程之间是相互独立的,但进程内部各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
-
切换速度不同,线程上下文切换比进程上下文切换要快得多。所以,有时线程也称为轻量级进程。
总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
2.4 Java线程和操作系统线程的区别
JDK 1.2 之前:
Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核)。
JDK 1.2 及以后:
Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用),创建和切换成本低,但是不可以利用多核。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问),创建和切换成本高,但是可以利用多核。
现在的 Java 线程的本质其实就是操作系统的线程。
线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种:
- 一对一(一个用户线程对应一个内核线程)
- 多对一(多个用户线程映射到一个内核线程)
- 多对多(多个用户线程映射到多个内核线程)
在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。
三、创建线程的方式
3.1 Thread类详解
一个线程在Java中使用一个Thread实例来描述。Thread类是Java语言一个重要的基础类,位于java.lang包中。
package java.lang;
public class Thread implements Runnable {
//...
}
1、实现接口
Runnable接口是一个函数式接口,只定义一个run()方法,代码如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
2、重要属性及对应方法
public class Thread implements Runnable {
//一:线程id,此属性用于保存线程的ID。private类型属性,外部只能使用getId()方法进行访问线程的ID。
private long tid;
public long getId(){} //获取线程ID,线程ID由JVM进行管理,在进程内唯一。
//二:线程名称
private volatile String name;
public final synchronized void setName(String name);
public final String getName();
Thread(String threadName)//通过此构造方法给线程设置一个定制化的名字
//三:线程优先级
private int priority;
public final void setPriority(int newPriority);
public final int getPriority();
//Java线程优先级的最大值为10,最小值为1,默认值为5。这三个优先级值为三个常量值,也是在Thread类中使用类常量定义,三个类常量如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
//四:是否为守护线程,保存线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程。
private boolean daemon = false;
public final boolean isDaemon();
public final void setDaemon(boolean on);
//五:线程状态
private volatile int threadStatus = 0;
public State getState();
//状态枚举类,在Java线程的状态中,就绪状态和运行状态在内部都用同一种状态RUNNABLE表示。就绪状态表示线程具备运行条件,正在等待获取CPU时间片;运行状态表示线程已经获取了CPU时间片,CPU正在执行线程代码逻辑。
public enum State {
NEW, //新建
RUNNABLE, //就绪、运行
BLOCKED, //阻塞
WAITING, //等待
TIMED_WAITING, //限时等待
TERMINATED; //结束
}
}
3、构造方法
可以通过构造函数,传入参数如异步执行任务runnable、线程名称name、线程组threadGroup等。
public Thread();
public Thread(Runnable target);
public Thread(Runnable target, String name);
public Thread(String name);
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 Thread(ThreadGroup group, String name);
Thread(Runnable target, AccessControlContext acc);
4、常用方法
package java.lang;
public class Thread implements Runnable {
//1、线程的启动和运行,注意:启动一个线程执行的是start()方法,启动之后真正执行的是run()方法的内容
//如果直接调用run()方法,只是属于方法的调用,不属于线程启动
public synchronized void start(); //用来启动一个线程,当调用start()方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配需要的资源。
public void run();//作为线程代码逻辑的入口方法。run()方法不是由用户程序来调用的,当调用start()方法启动一个线程之后,只要线程获得了CPU执行时间,便进入run()方法去执行具体的用户线程代码。
//2、取得当前线程,什么是当前线程呢?就是当前在CPU上执行的线程。
public static native Thread currentThread();
//3、线程睡眠
}
3.2 创建线程方法一:继承Thread类
1、实现步骤
- 创建一个线程类,继承Thread类。
- 在创建的线程类中,重写Thread类中的run()方法,在run()方法中实现具体的业务逻辑。
2、代码实现
public class CreateDemo {
public static final int MAX_TURN = 5;
static int threadNo = 1;
//继承Thread类
static class DemoThread extends Thread {
public DemoThread() {
super("Mall-" + threadNo++);
}
//重写run()方法,实现具体的业务逻辑
public void run() {
for (int i = 1; i < MAX_TURN; i++) {
Print.cfo(getName() + ", 轮次:" + i);
}
Print.cfo(getName() + " 运行结束.");
}
}
public static void main(String args[]) throws InterruptedException {
Thread thread = null;
//方法一:使用Thread子类创建和启动线程
for (int i = 0; i < 2; i++) {
thread = new DemoThread();
thread.start();
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}
3.3 创建线程方法二:实现Runnable接口
1、为什么可以通过实现Runnable接口创建线程?
在Thread类的run()方法中,会先判断target属性是否为空,如果target属性不为空,则执行target属性的run()方法,target是一个Runnable类型的实例,具体逻辑如下:
public void run() {
if (target != null) {
target.run();
}
}
// target 属性
private Runnable target;
在Thread的一堆构造方法当中,有的需要传入一个Runnable类型的参数。
public Thread(Runnable target);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name,long stackSize);
Thread(Runnable target, AccessControlContext acc);
通过上面这些构造方法,可以给Thread对象传入一个Runnable对象的参数,这样在启动线程的时候,就可以通过运行Thread的run()方法从而执行Runnable中的run()中的真正逻辑。通过这种方法,就不用使用继承Thread类的方法实现一个线程而是通过实现Runnable接口重写其run()方法的方式创建线程了。
2、Runnable接口介绍
Runnable是一个极为简单的接口,位于java.lang包中。Runnable仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的实现版本将被异步调用。具体的源代码如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
3、通过实现 Runnable 接口创建线程类
在通过实现Runnable接口创建线程的方式中,将需要异步执行的业务逻辑代码放在Runnable实现类的run()方法中,将Runnable实例作为target执行目标传入Thread实例。该方法的具体步骤如下:
- 定义一个新类实现Runnable接口。
- 实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
- 通过Thread类创建线程对象,将Runnable实现类实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
- 调用Thread实例的start()方法启动线程。
- 线程启动之后,线程的run()将被JVM执行,该run()方法将调用到target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。
4、实现代码
public class CreateDemo2 {
public static final int MAX_TURN = 5;
static int threadNo = 1;
//实现Runnable接口
static class RunTarget implements Runnable //① 实现Runnable接口
{
public void run() //② 在这些写业务逻辑
{
for (int j = 1; j < MAX_TURN; j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}
public static void main(String args[]) throws InterruptedException {
Thread thread = null;
//方法2.1:使用实现Runnable的实现类创建和启动线程
for (int i = 0; i < 2; i++) {
Runnable target = new RunTarget(); //声明一个Runnable类型实例
thread = new Thread(target, "RunnableThread" + threadNo++);
//调用Thread线程实例的start()方法启动新线程的并发执行。这时,Runnable实例的run()方法会在新线程Thread的实例方法run()方法中被调用。
thread.start();
}
}
}
5、改进:通过匿名类创建Runnable线程目标类
在实现Runnable的编写target执行目标类时,如果target实现类是一次性类,可以使用匿名实例的形式。上面的的执行目标类是一个静态内部类,现在改写成匿名实例的形式,代码如下:
public static void main(String args[]) throws InterruptedException {
//方法2.2:使用实现Runnable的匿名类创建和启动线程
for (int i = 0; i < 2; i++) {
//此处使用了匿名类给Thread类构造方法传参,直接在构造方法的参数内部new 一个Runnable对象
thread = new Thread(new Runnable() {
//实现run()方法,完成实际的业务逻辑
@Override
public void run() {
for (int j = 1; j < MAX_TURN; j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}, "RunnableThread" + threadNo++);
thread.start();
}
}
6、改进:使用 Lambda 表达式创建 Runnable 线程目标类
Runnable接口是一个函数式接口,在接口实现时可以使用Lambda表达式提供匿名实现,编写出比较优雅的代码。上面用了实现类以及匿名类的方法,现在可以优化为使用Lambda的方式。
public static void main(String args[]) throws InterruptedException {
//方法2.3:使用实现lambor表达式创建和启动线程
for (int i = 0; i < 2; i++) {
//此处使用Lambda
thread = new Thread(() ->
{
for (int j = 1; j < MAX_TURN; j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}, "RunnableThread" + threadNo++);
thread.start();
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
7、实现 Runnable 接口的方式创建线程目标类的优缺点
优点:
- 可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。所以在已经存在继承关系的情况下,只能使用实现Runnable接口的方式。
- 逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源被多个线程逻辑去异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。
缺点:
- 所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
- 如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。
3.4 创建线程方法三:使用 Callable 和 FutureTask 创建线程
在上面两种方式当中,都存在一个问题:不能获取到异步执行的结果,这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,因为其run()方法是不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
1、Callable接口
Callable是一个泛型接口,也是一个函数式接口,其唯一的抽象方法call()有返回值,返回值的类型为Callable接口的泛型形参类型。call()抽象方法还有一个Exception的异常声明,容许方法的实现版本的内部异常直接抛出,并且可以不予捕获。
与Runnable接口相比,Callable的抽象方法call(),有返回值,并且声明了受检异常,其功能比Runnable的run()方法要强。Callable接口源码如下:
package java.util.concurrent; //JUC包下
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。这时候就需要一个中间桥梁,下面的RunnableFuture就出来了。
2、Future接口
Future是一个对异步任务进行交互、操作的接口。Future部分功能如下:
- 判断异步任务是否执行完成。
- 获取异步任务完成后的执行结果。
- 能够取消异步执行中的任务。
Future源码如下:
package java.util.concurrent;
public interface Future<V> {
//取消异步任务
boolean cancel(boolean mayInterruptIfRunning);
//判断任务是否已取消
boolean isCancelled();
//任务是否已经完成
boolean isDone();
//获取异步任务执行结果,这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。
V get() throws InterruptedException, ExecutionException;
//限时阻塞等待获取异步任务执行结果,该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一个阻塞时长限制,不会无限制地阻塞和等待,如果其阻塞时间超过设定的timeout时间,该方法将抛出异常,调用线程可捕获此异常。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
3、RunnableFuture接口
RunnableFuture接口同时继承了Future接口和Runnable接口,所以RunnableFuture接口同时实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。
RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了通过它可以获取未来的异步执行结果。
因此,RunnableFuture可以起到从Callable接口到Thread线程之间搭桥的作用。
ublic interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
4、FutureTask类
FutureTask类实现了RunnableFuture接口。RunnableFuture接口很关键,既可以作为Thread线程实例的target目标,也可以获取并发任务执行的结果,是Thread与Callable之间一个非常重要的搭桥角色。但是,RunnableFuture只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask。所以说,FutureTask类才是真正的在Thread与Callable之间搭桥的类。
从 FutureTask 类 的 UML 关 系 图 可 以 看 到 : FutureTask 实 现 了 RunnableFuture 接 口 , 而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能当作一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
FutureTask内部有一个Callable类型的成员——callable实例属性以及一个非常重要的Object 类型的成员——outcome实例属性,具体如下:
public class FutureTask<V> implements RunnableFuture<V> {
//callable实例属性用来保存并发执行的Callable<V>类型的任务,
//并且callable实例属性需要在FutureTask实例构造时进行初始化。
//FutureTask类实现了Runnable接口,在其run()方法的实现版本中会执
// 行callable成员的call()方法。
private Callable<V> callable;
//outcome实例属性用于保存callable成员call()方法的异步执行结果。
//在FutureTask类run()方法完成callable成员的call()方法的执行之后,
//其结果将被保存在outcome实例属性中,供FutureTask类的get()方法获取。
private Object outcome;
//通过构造方法,初始化callable 属性值。
public FutureTask(Callable<V> callable) ;
public FutureTask(Runnable runnable, V result);
}
5、使用 Callable 和 FutureTask 创建线程的具体步骤
-
创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以
有返回值。
-
使用Callable实现类的实例构造一个FutureTask实例。
-
使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
-
调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执Callable实现类的call()方法。
-
调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
6、代码实现
public class CreateDemo3 {
public static final int MAX_TURN = 5;
public static final int COMPUTE_TIMES = 100000000;
//定义Callable类型类
static class ReturnableTask implements Callable<Long> {
//在call方法中实现异步任务逻辑:返回并发执行的时间
public Long call() throws Exception {
//获取当前时间
long startTime = System.currentTimeMillis();
Print.cfo(getCurThreadName() + " 线程运行开始.");
Thread.sleep(1000);
for (int i = 0; i < COMPUTE_TIMES; i++) {
int j = i * 10000;
}
//计算消耗时间
long used = System.currentTimeMillis() - startTime;
Print.cfo(getCurThreadName() + " 线程运行结束.");
return used;
}
}
public static void main(String args[]) throws InterruptedException {
//1、定义一个Callable类型类
ReturnableTask task = new ReturnableTask();
//2、通过FutureTask构造函数传入callable类型对象构造FutureTask对象
FutureTask<Long> futureTask = new FutureTask<Long>(task);
//3、使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例
Thread thread = new Thread(futureTask, "returnableThread");
//4、启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执Callable实现类的call()方法。
thread.start();
Thread.sleep(500);
Print.cfo(getCurThreadName() + " 让子弹飞一会儿.");
Print.cfo(getCurThreadName() + " 做一点自己的事情.");
for (int i = 0; i < COMPUTE_TIMES / 2; i++) {
int j = i * 10000;
}
Print.cfo(getCurThreadName() + " 获取并发任务的执行结果.");
try {
//5、调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
Print.cfo(thread.getName() + "线程占用时间:" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}
3.5 创建线程方法四:线程创建方法四:通过线程池创建线程
详情请看:并发编程-线程池原理与实战
3.6 创建线程方法五:使用CompletableFuture类
CompletableFuture
是JDK1.8
引入的新类,可以用来执行异步任务,如下:
public class UseCompletableFuture {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println("5......");
return "zhuZi";
});
// 需要阻塞,否则看不到结果
Thread.sleep(1000);
}
}
CompletableFuture 后续深入探讨。
3.7 创建线程方法六:使用ForkJoin或Stream并行流
ForkJoin
是JDK1.7
引入的新线程池,基于分治思想实现。而后续JDK1.8
的parallelStream
并行流,默认就基于ForkJoin
实现,如下
public class UseForkJoinPool {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.execute(()->{
System.out.println("10A......");
});
List<String> list = Arrays.asList("10B......");
list.parallelStream().forEach(System.out::println);
}
}
ForkJoin后续深入研究。
四、线程的核心原理
4.1 线程的调度
线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。
- 分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU。分时调度模型在时间片调度的分配上,所有线程“人人平等”。
- 分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU。分时调度模型在时间片调度的分配上,所有线程“人人平等”。
目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。
4.2 线程的优先级
Thread类中关于线程优先级的属性及方法代码如下:
class Thread implements Runnable {
//该属性保存一个Thread实例的优先级,即1~10之间的值
private int priority;
//获取线程优先级
public final int getPriority() {
return priority;
}
//设置线程的优先级
public final void setPriority(int newPriority);
//定义的三个关于线程优先级的常量,Thread实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。优先级最大值为10,最小值为1
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
}
Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会越多,但也不是绝对的。
4.3 线程的生命周期和状态
1、线程生命周期
Thread类中有线程状态相关的属性和方法定义,具体如下:
class Thread implements Runnable {
//以整数的形式保存线程的状态。
private volatile int threadStatus = 0;
//获取线程状态
public State getState(){...} ;
//Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态,具体如下:
public enum State {
NEW, //新建
RUNNABLE, //就绪、运行
BLOCKED, //阻塞
WAITING, // 等待
TIMED_WAITING, //限时等待
TERMINATED; //终止
}
}
1.NEW状态:
初始状态,线程被创建出来但没有被调用 start()
。
2.RUNNABLE状态:
Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。
- 就绪状态:就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序选中,线程就永远是就绪状态;当前线程进入就绪状态的条件大致包括以下几种:
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程的执行时间片用完。
- 线程睡眠(sleep)操作结束。
- 对其他线程合入(join)操作结束。
- 等待用户输入结束。
- 线程争抢到对象锁(Object Monitor)。
- 当前线程调用了yield()方法出让CPU执行权限。
- 运行状态:线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。
3.BLOCKED状态:
处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:
-
线程等待获取锁:等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
-
IO阻塞:线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程就会进入阻塞状态。IO包
括磁盘IO、网络IO等。IO阻塞的一个简单例子:线程等待用户输入内容后继续执行。
4.WAITING状态:
处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法让自己进入无限等待状态:
- Object.wait()方法,对应的唤醒方式为:Object.notify() / Object.notifyAll()。
- Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕。
- LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)。
5.TIMED_WAITING状态:
处于TIMED_WAITING(限时等待)状态的线程不会被分配CPU时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:
- Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束。
- Object.wait(time)方法,对应的唤醒方式为:调用Object.notify() / Object.notifyAll()去主动唤醒,或者限时结束。
- LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
6.TERMINATED状态:
线程结束任务之后,将会正常进入TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。
进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。
2、线程状态转换图
3、线程状态转换实例
public class StatusDemo {
//每个线程执行的轮次
public static final long MAX_TURN = 5;
//线程编号
static int threadSeqNumber = 0;
//全局的静态线程列表
static List<Thread> threadList = new ArrayList<>();
//输出静态线程列表中,所有线程的状态
private static void printThreadStatus() {
for (Thread thread : threadList) {
Print.tco(thread.getName() + " 状态为 " + thread.getState());
}
}
//向全局的静态线程列表加入线程
private static void addStatusThread(Thread thread) {
threadList.add(thread);
}
static class StatusDemoThread extends Thread {
public StatusDemoThread() {
super("statusPrintThread" + (++threadSeqNumber));
//将自己加入到全局的静态线程列表
addStatusThread(this);
}
public void run() {
Print.tco(getName() + ", 状态为" + getState());
for (int turn = 0; turn < MAX_TURN; turn++) {
//线程睡眠
sleepMilliSeconds(500);
//输出所有线程的状态
printThreadStatus();
}
Print.tco(getName() + "- 运行结束.");
}
}
public static void main(String args[]) throws InterruptedException {
addStatusThread(Thread.currentThread());
Thread sThread1 = new StatusDemoThread();
Print.cfo(sThread1.getName() + "- 状态为" + sThread1.getState());
Thread sThread2 = new StatusDemoThread();
Print.cfo(sThread2.getName() + "- 状态为" + sThread2.getState());
Thread sThread3 = new StatusDemoThread();
Print.cfo(sThread3.getName() + "- 状态为" + sThread3.getState());
sThread1.start();
sleepMilliSeconds(500);//等待500ms启动第二个线程
sThread2.start();
sleepMilliSeconds(500);//等待1000ms启动第三个线程
sThread3.start();
sleepSeconds(100);//睡眠100秒
}
@Test
//让线程处于TIMED_WAITING状态
public void testTimedWaiting() {
final Object lock = new Object();
synchronized (lock) {
try {
lock.wait(30 * 1000);
} catch (InterruptedException e) {
}
}
}
//让线程处于WAITING状态
@Test
public void testWaiting() {
final Object lock = new Object();
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
}
}
}
//让线程一直处于BLOCKED
@Test
public void testBlocked() {
final Object lock = new Object();
new Thread() {
public void run() {
synchronized (lock) {
System.out.println("i got lock, but don't release");
ThreadUtil.sleepMilliSeconds(1000 * 1000);
}
}
}.start();
ThreadUtil.sleepMilliSeconds(100 * 1000);
synchronized (lock) {
ThreadUtil.sleepMilliSeconds(30 * 1000);
}
}
}
4.4 线程的上下文切换
线程在执行过程中会有自己的运行条件和状态(也称上下文),如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了
sleep()
,wait()
等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
五、线程的基本操作
5.1 线程名称相关操作
1、相关方法
class Thread implements Runnable {
// 线程名称 存储属性 以及设置、获取方法
private volatile String name;
public final String getName();
public final synchronized void setName(String name);
//可以通过构造方法 初始化线程名称
public Thread(Runnable target, String name);
public Thread(String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize);
public Thread(ThreadGroup group, String name);
}
2、注意事项
- 线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。
- 允许两个Thread对象有相同的名称,但是应该避免。
- 如果程序没有为线程指定名称,系统会自动为线程设置名称。
3、操作实例
略
5.2 线程的休眠-sleep操作
1、方法介绍
sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。sleep()方法定义在Thread类中,是一组静态方法,有两个重载版本
class Thread implements Runnable {
//方法一 使目前正在执行的线程休眠millis毫秒,sleep()方法会有InterruptException受检异常抛出,如果调用了sleep()方法,就必须进行异常审查,捕获InterruptedException异常,或者再次通过方法声明存在InterruptedException异常。
public static native void sleep(long millis) throws InterruptedException;
//方法二 使目前正在执行的线程休眠millis毫秒 nanos 纳秒。sleep()方法会有InterruptException受检异常抛出,如果调用了sleep()方法,就必须进行异常审查,捕获InterruptedException异常,或者再次通过方法声明存在InterruptedException异常。
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
//从这个逻辑看出来其实并不会具体等待到纳秒级别
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
//调用native的sleep方法进行线程睡眠操作。
sleep(millis);
}
}
2、操作实例
public class SleepDemo {
public static final int SLEEP_GAP = 5000;//睡眠时长
public static final int MAX_TURN = 50;//睡眠次数
//继承Thread类实现线程类
static class SleepThread extends Thread {
static int threadSeqNumber = 1;
public SleepThread() {
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}
//重写run方法
public void run() {
try {
//循环50次
for (int i = 1; i < MAX_TURN; i++) {
Print.tco(getName() + ", 睡眠轮次:" + i);
// 线程睡眠一会
Thread.sleep(SLEEP_GAP);
}
} catch (InterruptedException e) {
Print.tco(getName() + " 发生异常被中断.");
}
Print.tco(getName() + " 运行结束.");
}
}
public static void main(String args[]) throws InterruptedException {
//启动5个线程
for (int i = 0; i < 5; i++) {
Thread thread = new SleepThread();
thread.start();
}
Print.tco(getCurThreadName() + " 运行结束.");
}
@Test
public void sleepForever() {
//获取进程id,避免去任务管理器查找
Logger.cfo("进程ID=" + getProcessID());
try {
//main线程,无限制等待
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.3 线程的结束-stop & interrupt操作
1、stop()方法
JDK提供了stop()方法终止正在运行的线程,但是stop()太暴力了,stop()会直接把线程中断,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能被释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于使用stop()方法来终止线程可能会产生不可预料的结果,因此并不推荐调用stop()方法。
如下所示:
2、interrupt()方法
使用interrupt()方法,本质不是用来中断一个线程,而是将线程设置为中断状态。当我们调用线程的interrupt()方法时,它有两个作用:
- 如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()、Thread.join()和Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而过早终结被阻塞状态。
- 如果此线程正处于运行中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。
3、使用实例
public class InterruptDemo {
public static final int SLEEP_GAP = 5000;//睡眠时长
public static final int MAX_TURN = 50;//睡眠次数
static class SleepThread extends Thread {
static int threadSeqNumber = 1;
public SleepThread() {
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}
public void run() {
try {
Print.tco(getName() + " 进入睡眠.");
// 线程睡眠一会
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
Print.tco(getName() + " 发生被异常打断.");
return;
}
Print.tco(getName() + " 运行结束.");
}
}
public static void main(String args[]) throws InterruptedException {
Thread thread1 = new SleepThread();
thread1.start();
Thread thread2 = new SleepThread();
thread2.start();
sleepSeconds(2);//等待2秒
thread1.interrupt(); //打断线程1
sleepSeconds(5);//等待5秒
thread2.interrupt(); //打断线程2,此时线程2已经终止
sleepSeconds(1);//等待1秒
Print.tco("程序运行结束.");
}
//测试用例:获取异步调用的结果
@Test
public void testInterrupted2() {
Thread thread = new Thread() {
public void run() {
Print.tco("线程启动了");
//一直循环
while (true) {
Print.tco(isInterrupted());
sleepMilliSeconds(SLEEP_GAP);
//如果调用 interrupt 为true,退出死循环
if (isInterrupted()) {
Print.tco("线程结束了");
return;
}
}
}
};
thread.start();
sleepSeconds(2);//等待2秒
thread.interrupt(); //打断线程1
sleepSeconds(2);//等待2秒
thread.interrupt();
}
}
5.4 线程的合并-join操作
1、方法介绍
线程合并join()操作涉及到的方法如下:
class Thread implements Runnable {
// 此方法会把当前线程变为WAITING,直到被合并线程执行结束
public final void join() throws InterruptedException {
join(0);
}
//此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行millis 的时间
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()) {
//进入waiting状态
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
//进入timed_waiting状态
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
//此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行millis+nanos的时间
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
}
2、使用实例
public class JoinDemo {
public static final int SLEEP_GAP = 5000;//睡眠时长
public static final int MAX_TURN = 50;//睡眠次数
static class SleepThread extends Thread {
static int threadSeqNumber = 1;
public SleepThread() {
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}
public void run() {
try {
Print.tco(getName() + " 进入睡眠.");
// 线程睡眠一会
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
Print.tco(getName() + " 发生被异常打断.");
return;
}
Print.tco(getName() + " 运行结束.");
}
}
public static void main(String args[]) {
Thread thread1 = new SleepThread();
sleepSeconds(20);
Print.tco("启动 thread1.");
thread1.start();
try {
thread1.join();//合并线程1,不限时
} catch (InterruptedException e) {
e.printStackTrace();
}
sleepSeconds(20);
Print.tco("启动 thread2.");
//启动第二条线程,并且进行限时合并,等待时间为1秒
Thread thread2 = new SleepThread();
thread2.start();
try {
thread2.join(5000);//限时合并,限时1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
Print.tco("线程运行结束.");
}
}
5.5 线程的让步-yield操作
1、方法介绍
Thread.yield()方法会使当前线程让出CPU。让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行。
yield()可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。
public static native void yield();
Thread.yield()方法有以下特点:
- yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
- yield不能保证使得当前正在运行的线程迅速转换到就绪状态。
- 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。
2、使用实例
public class YieldDemo {
public static final int MAX_TURN = 100;//执行次数
public static AtomicInteger index = new AtomicInteger(0);//执行编号
// 记录线程的执行次数
private static Map<String, AtomicInteger> metric = new HashMap<>();
//输出线程的执行次数
private static void printMetric() {
Print.tco("metric = " + metric);
}
static class YieldThread extends Thread {
static int threadSeqNumber = 1;
public YieldThread() {
super("YieldThread-" + threadSeqNumber);
threadSeqNumber++;
metric.put(this.getName(), new AtomicInteger(0));
}
public void run() {
for (int i = 1; i < MAX_TURN && index.get() < MAX_TURN; i++) {
Print.tco("线程优先级:" + getPriority());
index.incrementAndGet();
metric.get(this.getName()).incrementAndGet();
if (i % 2 == 0) {
//让步:出让执行的权限
// Thread.yield();
yieldThread();
}
}
//输出线程的执行次数
printMetric();
Print.tco(getName() + " 运行结束.");
}
}
@Test
public void test1() {
Thread thread1 = new YieldThread();
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new YieldThread();
thread2.setPriority(Thread.MIN_PRIORITY);
Print.tco("启动线程.");
thread1.start();
thread2.start();
sleepSeconds(100);
}
}
5.6 守护线程设置操作
1、什么是守护线程
Java线程分类:
- 用户线程
- 守护线程
守护线程:
守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行着一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。
守护线程和用户线程的本质区别:
二者与JVM虚拟机进程终止的方向不同。用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止
2、守护线程操作相关方法
class Thread implements Runnable {
private boolean daemon = false;
public final boolean isDaemon();
public final void setDaemon(boolean on) ;
}
3、使用实例
public class DaemonDemo {
public static final int SLEEP_GAP = 500; //每一轮的睡眠时长
public static final int MAX_TURN = 4; //用户线程执行轮次
//守护线程实现类
static class DaemonThread extends Thread {
public DaemonThread() {
super("daemonThread");
}
public void run() {
Print.synTco("--daemon线程开始.");
for (int i = 1; ; i++) {
Print.synTco("--轮次:" + i + "--守护状态为:" + isDaemon());
// 线程睡眠一会
sleepMilliSeconds(SLEEP_GAP);
}
}
}
public static void main(String args[]) throws InterruptedException {
Thread daemonThread = new DaemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
Thread userThread = new Thread(() ->
{
Print.synTco(">>用户线程开始.");
for (int i = 1; i <= MAX_TURN; i++) {
Print.synTco(">>轮次:" + i + " -守护状态为:" + getCurThread().isDaemon());
sleepMilliSeconds(SLEEP_GAP);
}
Print.synTco(">>用户线程结束.");
}, "userThread");
userThread.start();
//主线程合入userThread,等待userThread执行完成
// userThread.join();
Print.synTco(" 守护状态为:" + getCurThread().isDaemon());
Print.synTco(" 运行结束.");
}
}
4、守护线程的要点
使用守护线程时,有以下几点需要特别注意:
- 守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。
- 守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
- 守护线程创建的线程也是守护线程。
参考:
https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html
https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html
极致经典(卷2):Java高并发核心编程(卷2 加强版) 作者:尼恩