目录
- 一、进程与线程
-
- 1.什么是进程
- 2.进程的基本原理
- 3.线程的基本原理
- 4.进程与线程的区别
- 二、创建线程的4种方法
-
- 1.Thread类详解
- 2.创建一个空线程
- 3.线程创建方法一:继承Thread类创建线程类
- 4.线程创建方法二:实现Runnable接口创建线程目标类
-
- 4.1 Runnable接口
- 4.2 通过实现Runnable接口创建线程类
- 4.3 优雅创建Runnable线程目标类的两种方式
- 4.4 实现Runnable接口方式优缺点
- 5.线程创建方法三:使用Callable和FutureTask创建线程
-
- 5.1 Callable接口
- 5.2 RunnableFuture接口
- 5.3 Future接口
- 5.4 FutureTask类
- 5.5 使用Callable和FutureTask创建线程的具体步骤
- 6.线程创建方法四:通过线程池创建线程
- 三、线程的核心原理
-
- 1.线程的调度与时间片
- 2.线程的优先级
- 3.线程的生命周期
- 4.Jstack工具查看线程状态
- 四、线程的基本操作
-
- 1.线程名称的设置和获取
- 2.线程休眠:sleep
- 3.线程中断:interrupt
- 4.线程合并:join
- 5.线程让步:yield
- 6.守护线程:daemon
- 7.线程状态总结
- 五、线程池原理与实战
- 六、确定线程池的线程数
一、进程与线程 1.什么是进程 简单来说,进程是程序的一次启动执行。什么是程序呢?程序是存放在硬盘中的可执行文件,主要包括代码指令和数据。一个进程是一个程序的一次启动和执行,是操作系统将程序装入内存,给程序分配必要的系统资源,并且开始运行程序的指令。进程与程序是什么关系呢?同一个程序可以多次启动,对应多个进程。
2.进程的基本原理 在计算机中,CPU是核心的硬件资源,承担了所有的计算任务;内存资源承担了运行时数据的保存任务;外存资源(硬盘等)承担了数据外部永久存储的任务。其中,计算任务的调度、资源的分配由操作系统来统领。应用程序以进程的形式运行于操作系统之上,享受操作系统提供的服务。
进程的定义一直以来没有完美的标准。一般来说,一个进程由程序段、数据段和进程控制块三部分组成。进程的大致结构如图1-2所示。
文章图片
图1-2
进程的大致结构程序段一般也被称为代码段。代码段是进程的程序指令在内存中的位置,包含需要执行的指令集合;数据段是进程的操作数据在内存中的位置,包含需要操作的数据集合;程序控制块(Program Control Block,PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志。
PCB主要由四大部分组成:
(1)进程的描述信息。主要包括:进程ID和进程名称,进程ID是唯一的,代表进程的身份;进程状态,比如运行、就绪、阻塞;进程优先级,是进程调度的重要依据。
(2)进程的调度信息。主要包括:程序起始地址,程序的第一行指令的内存地址,从这里开始程序的执行;通信信息,进程间通信时的消息队列。
(3)进程的资源信息。主要包括:内存信息,内存占用情况和内存管理所用的数据结构;I/O设备信息,所用的I/O设备编号及相应的数据结构;文件句柄,所打开文件的信息。
(4)进程上下文。主要包括执行时各种CPU寄存器的值、当前程序计数器(PC)的值以及各种栈的值等,即进程的环境。在操作系统切换进程时,当前进程被迫让出CPU,当前进程的上下文就保存在PCB结构中,供下次恢复运行时使用。
现代操作系统中,进程是并发执行的,任何进程都可以同其他进程一起执行。在进程内部,代码段和数据段有自己的独立地址空间,不同进程的地址空间是相互隔离的。
Java程序的进程
Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。在这个JVM进程内部,所有Java程序代码都是以线程来运行的。JVM找到程序的入口点main()方法,然后运行main()方法,这样就产生了一个线程,这个线程被称为主线程。
3.线程的基本原理 早期的操作系统只有进程而没有线程。进程是程序执行和系统进行并发调度的最小单位。随着计算机的发展,CPU的性能越来越高,从早期的20MHz发展到了现在的2GHz以上,从单核CPU发展到了多核CPU,性能提升了成千上万倍。为了充分发挥CPU的计算性能,提升CPU硬件资源的利用率,同时弥补进程调度过于笨重产生的问题,进程内部演进出了并发调度的诉求,于是就发明了线程。线程是指“进程代码段”的一次顺序执行流程。线程是CPU调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。Java程序的进程执行过程就是标准的多线程的执行过程。每当使用Java命令执行一个class类时,实际上就是启动了一个JVM进程。理论上,在该进程的内部至少会启动两个线程,一个是main线程,另一个是GC(垃圾回收)线程。实际上,执行一个Java程序后,线程数量远远不止两个,达到了18个之多。
一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存,如图1-3所示。
文章图片
图1-3 线程的大致结构 在线程的结构中,线程描述信息即**线程的基本信息**,主要包括: (1)线程ID(Thread ID,线程标识符)。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
(2)线程名称。主要是方便用户识别,用户可以指定线程的名字,如果没有指定,系统就会自动分配一个名称。
(3)线程优先级。表示线程调度的优先级,优先级越高,获得CPU的执行机会就越大。
(4)线程状态。表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
(5)其他。例如是否为守护线程等,后面会详细介绍。
在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段内存地址。在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB大小的栈内存。栈内存和堆内存不同,栈内存不受垃圾回收器管理。
下面是一段简单的代码,演示一个Java程序的线程信息:
public class StackAreaDemo {
public static void main(String args[]) throws InterruptedException {
Print.cfo("当前线程名称:"+Thread.currentThread().getName());
Print.cfo("当前线程ID:"+Thread.currentThread().getId());
Print.cfo("当前线程状态:"+Thread.currentThread().getState());
Print.cfo("当前线程优先级:"+Thread.currentThread().getPriority());
int a = 1, b = 1;
int c = a / b;
anotherFun();
Thread.sleep(10000000);
}
private static void anotherFun() {
int a = 1, b = 1;
int c = a / b;
anotherFun2();
}
private static void anotherFun2() {
int a = 1, b = 1;
int c = a / b;
}
}
程序执行的结果如下:
[StackAreaDemo:main]:当前线程名称:main
[StackAreaDemo:main]:当前线程ID:1
[StackAreaDemo:main]:当前线程状态:RUNNABLE
[StackAreaDemo:main]:当前线程优先级:5
这里使用了java.lang包中的Thread.currentThread()静态方法,用于获取正在执行的当前线程。从结果中可以看出,正在执行main()方法的当前线程的描述信息为:线程ID为1,名称为main,状态为RUNNABLE,线程的优先级为5。
在Java中,执行程序流程的重要单位是“方法”,而栈内存的分配单位是“栈帧”(或者叫“方法帧”)。方法的每一次执行都需要为其分配一个栈帧(方法帧),栈帧主要保存该方法中的局部变量、方法的返回地址以及其他方法的相关信息。当线程的执行流程进入方法时,JVM就会为方法分配一个对应的栈帧压入栈内存;当线程的执行流程跳出方法时,JVM就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。以前面的StackAreaDemo示例代码为例,详细介绍一下main线程的栈内存。该示例中定义了三个方法——main、anotherFun和anotherFun2,这三个方法有相同的三个局部变量a、b和c。整体的执行流程如下:
文章图片
三个方法的栈帧弹出的过程与压入的过程刚好相反。anotherFun2()方法执行完成后,其栈帧从main线程的栈内存首先弹出,执行流程回到anotherFun()方法。anotherFun()方法执行完成后,其栈帧从main线程的栈内存弹出之后,执行流程回到main()方法。main()方法执行完成后,其栈帧弹出,此时main线程的栈内存已经全部弹空,没有剩余的栈帧。至此,main线程结束。
正是由于栈帧(方法帧)的操作是后进先出的模式,这也是标准的栈操作模式,因此存放方法帧的内存也被叫作栈内存。
4.进程与线程的区别 (1)线程是“进程代码段”的一次顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。
(2)线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
(3)线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥了CPU的计算性能,又弥补了进程调度过于笨重的问题。
(4)进程之间是相互独立的,但进程内部的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
(5)切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称为轻量级进程
二、创建线程的4种方法 1.Thread类详解 一个线程在Java中使用一个Thread实例来描述。Thread类是Java语言的一个重要的基础类,位于java.lang包中。Thread类有不少非常重要的属性和方法,用于存储和操作线程的描述信息,该类的结构图大致如图1-5所示。
文章图片
图1-5 Thread类的结构图 1.线程ID
属性:private long tid,此属性用于保存线程的ID。这是一个private类型的属性,外部只能使用getId()方法访问线程的ID。
方法:public long getId(),获取线程ID,线程ID由JVM进行管理,在进程内唯一。比如,1.2节的实例中,所输出的main线程的ID为1。
2.线程名称
属性:private String name,该属性保存一个Thread线程实例的名字。
方法一:public final String getName(),获取线程名称。
方法二:public final void setName(String name),设置线程名称。
方法三:Thread(String threadName),通过此构造方法给线程设置一个定制化的名字。
3.线程优先级
属性:private int priority,保存一个Thread线程实例的优先级。
方法一:public final int getPriority(),获取线程优先级。
方法二:public final void setPriority(int priority),设置线程优先级。
Java线程的最大优先级值为10,最小值为1,默认值为5。这三个优先级值为三个常量值,在Thread类中使用类常量定义。
4.是否为守护线程
属性:private boolean daemon=false,该属性保存Thread线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程。
方法:public final void setDaemon(boolean on),将线程实例标记为守护线程或用户线程,如果参数值为true,那么将线程实例标记为守护线程。
说明
什么是守护线程呢?守护线程是在进程运行时提供某种后台服务的线程,比如垃圾回收(GC)线程。守护线程的知识后面会专门详细介绍。
5.线程的状态
属性:private int threadStatus,该属性以整数的形式保存线程的状态。
方法:public Thread.State getState(),返回表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
Thread的内部静态枚举类State用于定义Java线程的所有状态,具体如下:
public static enum State {
NEW,//新建
RUNNABLE,//就绪、运行
BLOCKED,//阻塞
WAITING,//等待
TIMED_WAITING,//计时等待
TERMINATED;
//结束
}
在Java线程的状态中,就绪状态和运行状态在内部用同一种状态RUNNABLE表示。就绪状态表示线程具备运行条件,正在等待获取CPU时间片;运行状态表示线程已经获取了CPU时间片,CPU正在执行线程代码逻辑。
6.线程的启动和运行
方法一:public void start(),用来启动一个线程,当调用start()方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配需要的资源。
方法二:public void run(),作为线程代码逻辑的入口方法。run()方法不是由用户程序来调用的,当调用start()方法启动一个线程之后,只要线程获得了CPU执行时间,便进入run()方法体去执行具体的用户线程代码。
总之,这两个方法非常重要,start()方法用于线程的启动,run()方法作为用户代码逻辑的执行入口。
7.取得当前线程
方法:public static Thread currentThread(),该方法是一个非常重要的静态方法,用于获取当前线程的Thread实例对象。什么是当前线程呢?就是当前在CPU上执行的线程。在没有其他的途径获取当前线程的实例对象的时候,可以通过Thread.currentThread()静态方法获取。
8.其他的属性和方法Thread类中还有很多重要的属性和方法,本章后面会对Thread类进行深入的介绍,具体参见后面的内容。
2.创建一个空线程 第一个创建线程的方法是通过继承Thread类创建一个线程实例。这里为大家奉上一个非常简单的例子,让大家先体验一下如何通过Thread类完成线程的创建、启动和运行。首先演示一下如何创建一个空线程。空线程在启动后不会执行任何用户代码逻辑。创建一个空线程的参考代码如下:
public class EmptyThreadDemo {
public static void main(String args[]) throws InterruptedException {
//使用Thread类创建和启动线程
Thread thread = new Thread();
Print.cfo("线程名称:"+thread.getName());
Print.cfo("线程ID:"+thread.getId());
Print.cfo("线程状态:"+thread.getState());
Print.cfo("线程优先级:"+thread.getPriority());
Print.cfo(getCurThreadName() + " 运行结束.");
thread.start();
}
}代码非常简单,通过new Thread()创建了一个线程实例,然后调用thread.start()实例方法启动线程的执行,并且示例程序在start线程启动前输出了线程的一些描述信息:[EmptyThreadDemo:main]:线程名称:Thread-0
[EmptyThreadDemo:main]:线程ID:11
[EmptyThreadDemo:main]:线程状态:NEW
[EmptyThreadDemo:main]:线程优先级:5
在thread线程信息输出完成后,程序调用thread.start()实例方法启动新线程thread的执行。从上一小节大家知道,这时新线程的执行会调用Thread的run()实例方法,该方法作为用户业务代码逻辑的入口。查看一下Thread类的源码,其run()方法的具体代码如下:
public void run() {
if(this.target != null) {
this.target.run();
}
}
这里的target属性是Thread类的一个实例属性,该属性是很重要的,在后面会用到和讲到。在Thread类中,target的属性值默认为空。在这个例子中,thread线程的target属性默认为null。所以在thread线程执行时,其run()方法其实什么也没有做,线程就执行完了。总之,以上的简单例子向大家展示了通过Thread类如何进行线程的新建和启动。例子中的thread线程的run实例方法确实执行了,只是由于target为空,什么也没有做而已。
3.线程创建方法一:继承Thread类创建线程类 (1)需要继承Thread类,创建一个新的线程类。
(2)同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。
具体如下:
public class CreateDemo {
public static final int MAX_TURN = 5;
public static String getCurThreadName() {
return Thread.currentThread().getName();
}
//线程的编号
static int threadNo = 1;
static class DemoThread extends Thread {//①
public DemoThread() {
super("DemoThread-" + threadNo++);
//②
}
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() + " 运行结束.");
}
}
运行该实例,结果如下:
[CreateDemo:main]:main 运行结束.
[CreateDemo$DemoThread:run]:DemoThread-1, 轮次:1
[CreateDemo$DemoThread:run]:DemoThread-1, 轮次:2
[CreateDemo$DemoThread:run]:DemoThread-1, 轮次:3
[CreateDemo$DemoThread:run]:DemoThread-1, 轮次:4
[CreateDemo$DemoThread:run]:DemoThread-1 运行结束.
[CreateDemo$DemoThread:run]:DemoThread-2, 轮次:1
[CreateDemo$DemoThread:run]:DemoThread-2, 轮次:2
[CreateDemo$DemoThread:run]:DemoThread-2, 轮次:3
[CreateDemo$DemoThread:run]:DemoThread-2, 轮次:4
[CreateDemo$DemoThread:run]:DemoThread-2 运行结束.
4.线程创建方法二:实现Runnable接口创建线程目标类 重温一下Thread类的run()方法的代码,里边其实有点玄机,其代码如下:
public class Thread implements Runnable {
...
private Runnable target;
//执行目标
public void run() {
if(this.target != null) {
this.target.run();
//调用执行目标的run()方法
}
}
public Thread(Runnable target) {//包含执行目标的构造器
init(null, target, "Thread-" + nextThreadNum(), 0);
}
}
在Thread类的run()方法中,如果target(执行目标)不为空,就执行target属性的run()方法。而target属性是Thread类的一个实例属性,并且target属性的类型为Runnable。
Thread类的target属性在什么情况下非空呢?Thread类有一系列的构造器,其中有多个构造器可以为target属性赋值,这些构造器包括如下两个:
(1)public Thread(Runnable target)
(2)public Thread(Runnable target,String name)
使用这两个构造器传入target执行目标实例(Runnable实例),就可以直接通过Thread类的run()方法以默认方式实现,达到线程并发执行的目的。在这种场景下,可以不通过继承Thread类实现线程类的创建。
在为Thread的构造器传入target实例前,先来看看Runnable接口是何方神圣。
4.1 Runnable接口
Runnable是一个极为简单的接口,位于java.lang包中。接口Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的实现版本将被异步调用。
4.2 通过实现Runnable接口创建线程类
(1)定义一个新类实现Runnable接口。
(2)实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
(3)通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
(4)调用Thread实例的start()方法启动线程。
(5)线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。
实例代码:
public class CreateDemo2
{
public static final int MAX_TURN = 5;
static int threadNo = 1;
static class RunTarget implements Runnable//①实现Runnable接口
{
public void run()//②在这里编写业务逻辑
{
for (int j = 1;
j < MAX_TURN;
j++)
{
Print.cfo(ThreadUtil.getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}public static void main(String args[]) throws InterruptedException
{
Thread thread = null;
for (int i = 0;
i < 2;
i++)
{
Runnable target = new RunTarget();
//通过Thread 类创建线程对象,将Runnable实例作为实际参数传入
thread = new Thread(target, "RunnableThread" + threadNo++);
thread.start();
}
}
}
说明 值得注意的是,run()方法实现版本中在获取当前线程的名称时,所用的方法是在外部类ThreadUtil中定义的getCurThreadName()静态方法,而不是Thread类的getName()实例方法。原因是:这个RunTarget内部类和Thread类不再是继承关系,无法直接调用Thread类的任何实例方法。 通过实现Runnable接口的方式创建的执行目标类,如果需要访问线程的任何属性和方法,必须通过Thread.currentThread()获取当前的线程对象,通过当前线程对象间接访问。 完成了Runnable的实现类后,需要调用Thread类的构造器创建线程,并将Runnable实现类的实例作为实参传入。可以调用的构造器(即构造函数)包括如下三个:
(1)public Thread(Runnable target)
(2)public Thread(Runnable target,String name)
(3)public Thread(ThreadGroup group,Runnable target)
4.3 优雅创建Runnable线程目标类的两种方式
(1)通过匿名类优雅地创建Runnable线程目标类。
(2)使用Lambda表达式优雅地创建Runnable线程目标类。
1.通过匿名类优雅地创建Runnable线程目标类在实现Runnable编写target执行目标类时,如果target实现类是一次性类,可以使用匿名实例的形式。
public class CreateDemo2 {
public static final int MAX_TURN = 5;
static int threadNo = 1;
public static void main(String args[]) throws InterruptedException {
Thread thread = null;
//使用Runnable的匿名类创建和启动线程
for (int i = 0;
i < 2;
i++) {
thread = new Thread(new Runnable() { //① 匿名实例
@Override
public void run() { //② 异步执行的业务逻辑
for (int j = 1;
j < MAX_TURN;
j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}, "RunnableThread" + threadNo++);
thread.start();
}Print.cfo(getCurThreadName() + " 运行结束.");
}
}
2.使用Lambda表达式优雅地创建Runnable线程目标类
回顾一下Runnable接口,其源代码中还有一个小玄机,具体如下:
@FunctionalInterface
public interface Runnable {
void run();
}
源代码中的小玄机为:在Runnable接口上声明了一个@FunctionalInterface注解。该注解的作用是:标记Runnable接口是一个“函数式接口”。在Java中,“函数式接口”是有且仅有一个抽象方法的接口。反过来说,如果一个接口中包含两个或两个以上的抽象方法,就不能使用@FunctionalInterface注解,否则编译会报错。
@FunctionalInterface注解不是必需的,只要一个接口符合“函数式接口”的定义,使用时加不加@FunctionalInterface注解都没有影响,都可以当作“函数式接口”来使用。
Lambda表达式的形式,代码如下:
public class CreateDemo2 {
public static final int MAX_TURN = 5;
static int threadNo = 1;
public static void main(String args[]) throws InterruptedException {
Thread thread = null;
//使用Lambda表达式形式创建和启动线程
for (int i = 0;
i < 2;
i++) {
thread = new Thread( ()-> {//①Lambda表达式
for (int j = 1;
j < MAX_TURN;
j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}, "RunnableThread" + threadNo++);
thread.start();
}Print.cfo(getCurThreadName() + " 运行结束.");
}
}
总体而言,经过对比可以发现:使用Lambda表达式创建target执行目标实例,代码已经做到了极致的简化。
4.4 实现Runnable接口方式优缺点
缺点:
(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。
优点:
(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。
(2)逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源被多个线程逻辑异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。
1.“逻辑和数据更好地分离”演示实例
通过实现Runnable接口的方式创建线程目标类更加适合多个线程的代码逻辑去共享计算和处理同一个资源的场景。这个优点不是太好理解,接下来通过具体例子说明一下。
public class SalesDemo
{
public static final int MAX_AMOUNT = 5;
//商品数量//商店商品类(销售线程类),一个商品一个销售线程,每个线程异步销售4次
static class StoreGoods extends Thread
{
StoreGoods(String name)
{
super(name);
}private int goodsAmount = MAX_AMOUNT;
public void run()
{
for (int i = 0;
i <= MAX_AMOUNT;
i++)
{
if (this.goodsAmount > 0)
{
Print.cfo(getCurThreadName() + " 卖出一件,还剩:"
+ (--goodsAmount));
sleepMilliSeconds(10);
}
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}//商场商品类(target销售线程的目标类),一个商品最多销售4次,可以多人销售
static class MallGoods implements Runnable
{
//多人销售可能导致数据出错,使用原子数据类型保障数据安全
private AtomicInteger goodsAmount = new AtomicInteger(MAX_AMOUNT);
public void run()
{
for (int i = 0;
i <= MAX_AMOUNT;
i++)
{
if (this.goodsAmount.get() > 0)
{
Print.cfo(getCurThreadName() + " 卖出一件,还剩:"
+ (goodsAmount.decrementAndGet()));
sleepMilliSeconds(10);
}
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}public static void main(String args[]) throws InterruptedException
{
Print.hint("商店版本的销售");
for (int i = 1;
i <= 2;
i++)
{
Thread thread = null;
thread = new StoreGoods("店员-" + i);
thread.start();
}Thread.sleep(1000);
Print.hint("商场版本的销售");
MallGoods mallGoods = new MallGoods();
for (int i = 1;
i <= 2;
i++)
{
Thread thread = null;
thread = new Thread(mallGoods, "商场销售员-" + i);
thread.start();
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}
运行代码,输出的结果如下:
[main|Print.hint]:/--商店版本的销售--/
[SalesDemo$StoreGoods.run]:店员-2 卖出一件,还剩:4
[SalesDemo$StoreGoods.run]:店员-1 卖出一件,还剩:4
[SalesDemo$StoreGoods.run]:店员-2 卖出一件,还剩:3
[SalesDemo$StoreGoods.run]:店员-1 卖出一件,还剩:3
[SalesDemo$StoreGoods.run]:店员-2 卖出一件,还剩:2
[SalesDemo$StoreGoods.run]:店员-1 卖出一件,还剩:2
[SalesDemo$StoreGoods.run]:店员-1 卖出一件,还剩:1
[SalesDemo$StoreGoods.run]:店员-2 卖出一件,还剩:1
[SalesDemo$StoreGoods.run]:店员-2 卖出一件,还剩:0
[SalesDemo$StoreGoods.run]:店员-1 卖出一件,还剩:0
[SalesDemo$StoreGoods.run]:店员-1 运行结束.
[SalesDemo$StoreGoods.run]:店员-2 运行结束.
[main|Print.hint]:/--商场版本的销售--/
[SalesDemo.main]:main 运行结束.
[SalesDemo$MallGoods.run]:商场销售员-1 卖出一件,还剩:3
[SalesDemo$MallGoods.run]:商场销售员-2 卖出一件,还剩:4
[SalesDemo$MallGoods.run]:商场销售员-1 卖出一件,还剩:2
[SalesDemo$MallGoods.run]:商场销售员-2 卖出一件,还剩:1
[SalesDemo$MallGoods.run]:商场销售员-1 卖出一件,还剩:0
[SalesDemo$MallGoods.run]:商场销售员-2 运行结束.
[SalesDemo$MallGoods.run]:商场销售员-1 运行结束.
2.“逻辑和数据更好地分离”原理分析
在上面的例子中,静态内部类StoreGoods继承Thread类实现了一个异步销售类。在main()方法中创建销售线程时创建了3个商店商品的销售线程实例。
上面的代码新建了n个线程,相当于n个不同的商店店员,每个商店店员负责一个数量,并且负责将自己的数量卖完。每个商店店员(线程)各卖各的,其剩余数量都是从4卖到0,没有关联。商店店员的售卖过程大致如图1-6所示。
文章图片
再来看另一个内部类MallGoods。在main()方法中创建销售线程的时候创建了1个公用的MallGoods商品销售对象。
以上代码新建了n(这里为2)个线程,相当于商场招聘了n个不同的商场销售员。每个商场销售员一个线程,n个线程共享了一个Runnable类型的target执行目标实例——mallGoods实例。
这里的关键点是:n个商场销售员线程通过线程的target.run()方法共同访问mallGoods实例的同一个商品数量goodsAmount,剩余数量从4卖到0,大家一起售卖,卖一个少一个,卖完为止。其售卖过程大致如图1-7所示。
文章图片
通过对比可以看出:
(1)通过继承Thread类实现多线程能更好地做到多个线程并发地完成各自的任务,访问各自的数据资源。
(2)通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务,访问同一份数据资源。多个线程的代码逻辑可以方便地访问和处理同一个共享数据资源(如例子中的MallGoods.goodsAmount),这样可以将线程逻辑和业务数据进行有效的分离,更好地体现了面向对象的设计思想。
(3)通过实现Runnable接口实现多线程时,如果数据资源存在多线程共享的情况,那么数据共享资源需要使用原子类型(而不是普通数据类型),或者需要进行线程的同步控制,以保证对共享数据操作时不会出现线程安全问题。
总之,在大多数情况下,偏向于通过实现Runnable接口来实现线程执行目标类,这样能使代码更加简洁明了。后面介绍线程池的时候会讲到,异步执行任务在大多数情况下是通过线程池去提交的,而很少通过创建一个新的线程去提交,所以更多的做法是,通过实现Runnable接口创建异步执行任务,而不是继承Thread去创建异步执行任务。
5.线程创建方法三:使用Callable和FutureTask创建线程 前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。
这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
5.1 Callable接口
Callable接口位于java.util.concurrent包中,查看它的Java源代码,如下:
package java.util.concurrent;
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
Callable接口是一个泛型接口,也是一个“函数式接口”。其唯一的抽象方法call()有返回值,返回值的类型为Callable接口的泛型形参类型。call()抽象方法还有一个Exception的异常声明,容许方法的实现版本的内部异常直接抛出,并且可以不予捕获。
问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?一个在Callable接口与Thread线程之间起到搭桥作用的重要接口马上就要登场了。
5.2 RunnableFuture接口
这个重要的中间搭桥接口就是RunnableFuture接口,该接口与Runnable接口、Thread类紧密相关。RunnableFuture是如何在Callable与Thread之间实现搭桥功能的呢?RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture接口的代码:
public interface RunnableFutureextendsRunnable, Future {
void run();
}
通过源代码可以看出:RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。
5.3 Future接口
Future接口至少提供了三大功能:
(1)能够取消异步执行中的任务。
(2)判断异步任务是否执行完成。
(3)获取异步任务完成后的执行结果。
Future接口的源代码如下:
public interface Future {
boolean cancel(boolean mayInterruptRunning);
//取消异步执行
boolean isCancelled();
boolean isDone();
//判断异步任务是否执行完成
//获取异步任务完成后的执行结果
V get() throws InterruptedException, ExecutionException;
//设置时限,获取异步任务完成后的执行结果
V get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException;
...
}
Future接口的主要方法详细说明如下:
·V get():获取异步任务执行的结果。注意,这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。
·V get(Long timeout,TimeUnit unit):设置时限,(调用线程)阻塞性地获取异步任务执行的结果。该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一个阻塞时长限制,不会无限制地阻塞和等待,如果其阻塞时间超过设定的timeout时间,该方法将抛出异常,调用线程可捕获此异常。·
boolean isDone():获取异步任务的执行状态。如果任务执行结束,就返回true。
·boolean isCancelled():获取异步任务的取消状态。如果任务完成前被取消,就返回true。·
boolean cancel(boolean mayInterruptRunning):取消异步任务的执行。
总体来说,Future是一个对异步任务进行交互、操作的接口。但是Future仅仅是一个接口,通过它没有办法直接完成对异步任务的操作,JDK提供了一个默认的实现类——FutureTask。
5.4 FutureTask类
FutureTask类是Future接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask类不仅实现了Future接口,还实现了Runnable接口,或者更加准确地说,FutureTask类实现了RunnableFuture接口。
前面讲到RunnableFuture接口很关键,既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread与Callable之间一个非常重要的搭桥角色。但是,RunnableFuture只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask。所以说,FutureTask类才是真正的在Thread与Callable之间搭桥的类。FutureTask类的UML关系图大致下图所示。
文章图片
从FutureTask类的UML关系图可以看到:FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
FutureTask内部有一个Callable类型的成员——callable实例属性,具体如下:
private Callable callable;
callable实例属性用来保存并发执行的Callable类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask类实现了Runnable接口,在其run()方法的实现版本中会执行callable成员的call()方法。
此外,FutureTask内部还有另一个非常重要的Object类型的成员——outcome实例属性:
private Object outcome;
FutureTask的outcome实例属性用于保存callable成员call()方法的异步执行结果。在FutureTask类的run()方法完成callable成员的call()方法的执行之后,其结果将被保存在outcome实例属性中,供FutureTask类的get()方法获取。
5.5 使用Callable和FutureTask创建线程的具体步骤
通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:
(1)创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
(2)使用Callable实现类的实例构造一个FutureTask实例。
(3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
按照以上步骤,通过Callable接口和Future接口相结合创建多线程,实例如下:
public class CreateDemo3 {
public static final int MAX_TURN = 5;
public static final int COMPUTE_TIMES = 100000000;
//①创建一个 Callable 接口的实现类
static class ReturnableTask implements Callable {
//②编写好异步执行的具体逻辑,可以有返回值
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 {
ReturnableTask task=new ReturnableTask();
//③
FutureTask futureTask = new FutureTask(task);
//④
Thread thread = new Thread(futureTask, "returnableThread");
//⑤
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 {
Print.cfo(thread.getName()+"线程占用时间:"
+ futureTask.get());
//⑦
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
}
执行实例程序,结果如下:
[CreateDemo3$ReturnableTask:call]:returnableThread 线程运行开始.
[CreateDemo3:main]:main 让子弹飞一会儿.
[CreateDemo3:main]:main 做一点自己的事情.
[CreateDemo3:main]:main 获取并发任务的执行结果.
[CreateDemo3$ReturnableTask:call]:returnableThread 线程运行结束.
[CreateDemo3:main]:returnableThread线程占用时间:1008
[CreateDemo3:main]:main 运行结束.
main线程和returnableThread线程的执行流程大致如图1-9所示。
文章图片
通过futureTask的get实例方法获取异步执行的结果。这里有两种情况:
(1)futureTask的结果outcome不为空,callable.call()执行完成。在这种情况下,futureTast.get会直接取回outcome结果,返回给main线程(结果获取线程)。
(2)futureTask的结果outcome为空,callable.call()还没有执行完。在这种情况下,main线程作为结果获取线程会被阻塞住,一直阻塞到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒main线程,去提取callable.call()执行结果。
6.线程创建方法四:通过线程池创建线程 前面的示例中,所创建的Thread实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资源耗费上都很高(稍后会介绍),在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。
Java中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类。
1.线程池的创建与执行目标提交
通过Executors工厂类创建一个线程池,一个简单的示例如下:
//创建一个包含三个线程的线程池
private static ExecutorService pool = Executors.newFixedThreadPool(3);
以上示例通过工厂类Executors的newFixedThreadPool(int threads)方法创建了一个线程池,所创建的线程池的类型为ExecutorService。工厂类的newFixedThreadPool(int threads)方法用于创建包含固定数目的线程池。ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。
向ExecutorService线程池提交异步执行target目标任务的常用方法有:
//方法一:执行一个 Runnable类型的target执行目标实例,无返回
void execute(Runnable command);
//方法二:提交一个 Callable类型的target执行目标实例, 返回一个Future异步任务实例
Future submit(Callable task);
//方法三:提交一个 Runnable类型的target执行目标实例, 返回一个Future异步任务实例
Future> submit(Runnable task);
2.线程池的使用实战
使用Executors创建线程池,然后使用ExecutorService线程池执行或者提交target执行目标实例的示例代码,大致如下:
public class CreateDemo4
{
public static final int MAX_TURN = 5;
public static final int COMPUTE_TIMES = 100000000;
//创建一个包含三个线程的线程池
private static ExecutorService pool = Executors.newFixedThreadPool(3);
static class DemoThread implements Runnable
{
@Override
public void run()
{
for (int j = 1;
j < MAX_TURN;
j++)
{
Print.cfo(getCurThreadName() + ", 轮次:" + j);
sleepMilliSeconds(10);
}
}
}
static class ReturnableTask implements Callable
{
//返回并发执行的时间
public Long call() throws Exception
{
long startTime = System.currentTimeMillis();
Print.cfo(getCurThreadName() + " 线程运行开始.");
for (int j = 1;
j < MAX_TURN;
j++)
{
Print.cfo(getCurThreadName() + ", 轮次:" + j);
sleepMilliSeconds(10);
}
long used = System.currentTimeMillis() - startTime;
Print.cfo(getCurThreadName() + " 线程运行结束.");
return used;
}
}
public static void main(String[] args) {
pool.execute(new DemoThread());
//执行线程实例,无返回
pool.execute(new Runnable()
{
@Override
public void run()
{
for (int j = 1;
j < MAX_TURN;
j++)
{
Print.cfo(getCurThreadName() + ", 轮次:" + j);
sleepMilliSeconds(10);
}
}
});
//提交Callable 执行目标实例,有返回
Future future = pool.submit(new ReturnableTask());
Long result = (Long) future.get();
Print.cfo("异步任务的执行结果为:" + result);
sleepSeconds(Integer.MAX_VALUE);
}
}
运行程序,输出结果如下:
[CreateDemo4$DemoThread.run]:pool-1-thread-1, 轮次:1
[CreateDemo4$1.run]:pool-1-thread-2, 轮次:1
[CreateDemo4$1.run]:pool-1-thread-2, 轮次:2
[CreateDemo4$DemoThread.run]:pool-1-thread-1, 轮次:2
[CreateDemo4$DemoThread.run]:pool-1-thread-1, 轮次:3
[CreateDemo4$1.run]:pool-1-thread-2, 轮次:3
[CreateDemo4$DemoThread.run]:pool-1-thread-1, 轮次:4
[CreateDemo4$1.run]:pool-1-thread-2, 轮次:4
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3 线程运行开始.
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3, 轮次:1
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3, 轮次:2
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3, 轮次:3
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3, 轮次:4
[CreateDemo4$ReturnableTask.call]:pool-1-thread-3 线程运行结束.
[CreateDemo4.main]:异步任务的执行结果为:45
ExecutorService线程池的execute(…)与submit(…)方法的区别如下:
(1)接收的参数不一样
submit()可以接收两种入参:无返回值的Runnable类型的target执行目标实例和有返回值的Callable类型的target执行目标实例。而execute()仅仅接收无返回值的target执行目标实例,或者无返回值的Thread实例。
(2)submit()有返回值,而execute()没有
submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。
说明 本小节的案例仅供学习使用,实际生产环境禁止使用Executors创建线程池,线程池是一个很重要的知识点,后面会详细介绍。
具体详解见:【多线程】线程池ThreadPoolExecutor(全面详解)
三、线程的核心原理 现代操作系统(如Windows、Linux、Solaris)提供了强大的线程管理能力,Java不需要再进行独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如Solaris操作系统)上,JVM甚至将每个Java线程一对一地对应到操作系统的本地线程,彻底将线程调度委托给操作系统。
1.线程的调度与时间片 由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。对于不同的操作系统、不同的CPU,线程的CPU时间片长度都不同。
目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。
线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。
(1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”。如下图所示是一个分时调度的简单例子,三个线程轮流得到CPU时间片,一个线程执行时,另外两个线程处于就绪状态。
文章图片
(2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。
2.线程的优先级 在Thread类中有一个实例属性和两个实例方法,专门用于进行线程优先级相关的操作。与线程优先级相关的成员属性为:
private int priority;//该属性保存一个Thread实例的优先级,即1~10的值
与Thread类线程优先级相关的实例方法为:
方法1:public final int getPriority(),获取线程优先级。
方法2:public final void setPriority(int priority),设置线程优先级。
Thread实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。优先级最大值为10,最小值为1,Thread类中定义的三个优先级常量如下:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多,但也不是绝对的,执行机会的获取具有随机性,优先级高的不一定获得的机会多。
3.线程的生命周期 Java中线程的生命周期分为6种状态。Thread类有一个实例属性和一个实例方法专门用于保存和获取线程的状态。其中,用于保存线程Thread实例状态的实例属性为:
private int threadStatus;//以整数的形式保存线程的状态
Thread类用于获取线程状态的实例方法为:
public Thread.State getState();
//返回当前线程的执行状态,一个枚举类型值
Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态,具体如下:
public static enum State {
NEW,//新建
RUNNABLE,//可执行:包含操作系统的就绪、运行两种状态
BLOCKED,//阻塞
WAITING,//等待
TIMED_WAITING,//限时等待
TERMINATED;
//终止
}
在Thread.State定义的6种状态中,有4种是比较常见的状态,它们是:
NEW(新建)状态、RUNNABLE(可执行)状态、TERMINATED(终止)状态、TIMED_WAITING(限时等待)状态。
1.NEW状态
Java源码对NEW状态的说明是:创建成功但是没有调用start()方法启动的Thread线程实例都处于NEW状态。
当然,并不是Thread线程实例的start()方法一经调用,其状态就从NEW状态到RUNNABLE状态,此时并不意味着线程立即获取CPU时间片并且立即执行,中间需要一系列操作系统的内部操作。
2.RUNNABLE状态
前面讲到,当调用了Thread实例的start()方法后,下一步如果线程获取CPU时间片开始执行,JVM将异步调用线程的run()方法执行其业务代码。
那么在run()方法被异步调用之前,JVM做了哪些事情呢?
JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状态。
操作系统中线程的就绪状态是什么状态的呢?JVM的线程状态与其幕后的操作系统线程状态之间的转换关系简化后如图1-11所示。
文章图片
3.TERMINATED状态
处于RUNNABLE状态的线程在run()方法执行完成之后就变成终止状态TERMINATED了。当然,如果在run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止,线程也会变成TERMINATED状态。
【#|【多线程】多线程的“前世”,“今生”与“未来”】4.TIMED_WAITING状态
线程处于一种特殊的等待状态,准确地说,线程处于限时等待状态。能让线程处于限时等待状态的操作大致有以下几种:
(1)Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为n毫秒。
(2)Object.wait():带时限的抢占对象的monitor锁。
(3)Thread.join():带时限的线程合并。
(4)LockSupport.parkNanos():让线程等待,时间以纳秒为单位。
(5)LockSupport.parkUntil():让线程等待,时间可以灵活设置。
4.Jstack工具查看线程状态 有时,服务器CPU占用率会一直很高,甚至一直处于100%。如果CPU使用率居高不下,自然是有某些线程一直占用着CPU资源。那如何查看占用CPU较高的线程呢?或者说,如何查看线程的状态呢?
一种比较快捷的办法是使用Jstack工具。
Jstack工具是Java虚拟机自带的一种堆栈跟踪工具。Jstack用于生成或导出(DUMP)JVM虚拟机运行实例当前时刻的线程快照。线程快照是当前JVM实例内每一个线程正在执行的方法堆栈的集合,生成或导出线程快照的主要目的是定位线程出现长时间运行、停顿或者阻塞的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过Jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
Jstack命令的语法格式如下:
jstack //pid表示Java进程id,可以用jps命令或者ps命令查看
通过Jstack输出的线程信息主要包括:JVM线程、用户线程等。其中,JVM线程在JVM启动时就存在,主要用于执行譬如垃圾回收、低内存的检测等后台任务,这些线程往往在JVM初始化的时候就存在。而用户线程则是在程序创建了新的线程时才会生成。
这里需要注意的是:
(1)在实际运行中,往往一次DUMP的信息不足以确认问题。建议产生三次DUMP信息,如果每次DUMP都指向同一个问题,我们才能确定问题的典型性。
(2)不同的Java虚拟机的线程导出来的DUMP信息格式是不一样的,并且同一个JVM的不同版本,DUMP信息也有差别。
JVM线程往往在JVM初始化的时候就存在。在Java程序刚启动时,通过Jstack命令可以立即导出来一些JVM后台线程。下面是一些JVM线程的例子:
"VM Thread" os_prio=2 tid=0x00000000150c8000 nid=0x3b8c runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002a48000 nid=0x41f8 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000002a49800 nid=0x3254 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000002a4b000 nid=0x271c runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002a4c800 nid=0x1578 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000016594000 nid=0x1d10 waiting on condition
其中,GC task thread为垃圾回收线程,此类线程会负责进行垃圾回收。通常JVM会启动多个GC线程,在GC线程的名称中,#后面的数字会累加,如GC task thread#1、GC task thread#2等。
其中,VM Periodic Task Thread线程是JVM周期性任务调度的线程,该线程在JVM内使用得比较频繁,比如定期的内存监控、JVM运行状况监控。
Jstack指令所输出的信息中包含以下重要信息:
(1)tid:线程实例在JVM进程中的id。
(2)nid:线程实例在操作系统中对应的底层线程的线程id。
(3)prio:线程实例在JVM进程中的优先级。
(4)os_prio:线程实例在操作系统中对应的底层线程的优先级。
(5)线程状态:如runnable、waiting on condition等。
用户线程往往是执行业务逻辑的线程,是大家所关注的重点,也是最容易产生死锁的地方。
四、线程的基本操作 Java线程的常用操作基本上都定义在Thread类中,包括一些重要的静态方法和线程实例方法。
1.线程名称的设置和获取 在Thread类中可以通过构造器Thread(…)初始化设置线程名称,也可以通过setName(…)实例方法设置线程名称,取得线程名称可以通过getName()方法完成。
关于线程名称有以下几个要点:
(1)线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。
(2)允许两个Thread对象有相同的名称,但是应该避免。
(3)如果程序没有为线程指定名称,系统会自动为线程设置名称。线程将使用“Thread-”加上自动编号的形式进行自动命名,如Thread-0、Thread-1等。
说明
编程规范要求:创建线程或线程池时,需要指定有意义的线程名称,方便出错时回溯。
2.线程休眠:sleep sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。Sleep()方法定义在Thread类中,是一组静态方法,有两个重载版本:
//使目前正在执行的线程休眠millis毫秒
public static void sleep(long millis) throws InterruptException;//使目前正在执行的线程休眠millis毫秒,nanos纳秒
public static void sleep(long millis,int nanos) throws InterruptException;
sleep()方法会有InterruptException受检异常抛出,如果调用了sleep()方法,就必须进行异常审查,捕获InterruptedException异常,或者再次通过方法声明存在InterruptedException异常。
举一个例子演示一下sleep()静态方法的调用,具体如下:
public class SleepDemo
{
public static final int SLEEP_GAP = 5000;
// 睡眠时长5秒
public static final int MAX_TURN = 50;
// 睡眠次数,值稍微大点方便使用Jstack
static class SleepThread extends Thread
{
static int threadSeqNumber = 1;
public SleepThread()
{
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}public void run()
{
try
{
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
{
for (int i = 0;
i < 5;
i++)
{
Thread thread = new SleepThread();
thread.start();
}
Print.tco(getCurThreadName() + " 运行结束.");
}
}
运行以上程序,然后通过Jstack命令可以查看4个睡眠线程的状态,不过在此之前需要使用jps指令查找出以上程序对应的JVM进程SleepDemo的进程ID,具体的命令使用过程及其大致输出的信息截取如下:
C:\Users\user>jps
8468 Jps
18024 SleepDemoC:\Users\user>jstack 18024
// 省略不相干的输出
"sleepThread-4" #17 prio=5 os_prio=0 tid=0x000000001fd21800 nid=0x462c waiting on condition [0x0000000020cbf000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ...SleepDemo$SleepThread.run(SleepDemo.java:35)"sleepThread-3" #16 prio=5 os_prio=0 tid=0x000000001fd1e800 nid=0x28a4 waiting on condition [0x0000000020bbf000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ...SleepDemo$SleepThread.run(SleepDemo.java:35)"sleepThread-2" #15 prio=5 os_prio=0 tid=0x000000001fd1e000 nid=0x1264 waiting on condition [0x0000000020abf000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ...SleepDemo$SleepThread.run(SleepDemo.java:35)"sleepThread-1" #14 prio=5 os_prio=0 tid=0x000000001fd29000 nid=0x1914 waiting on condition [0x00000000209be000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ...SleepDemo$SleepThread.run(SleepDemo.java:35)
通过以上的Jstack指令输出,可以看到在进行线程DUMP的时间点,所创建的4个sleepThread线程都处于TIMED_WAITING(sleeping)状态。
当线程睡眠时间满后,线程不一定会立即得到执行,因为此时CPU可能正在执行其他的任务,线程首先进入就绪状态,等待分配CPU时间片以便有机会执行。
3.线程中断:interrupt Java语言提供了stop()方法终止正在运行的线程,但是Java将Thread的stop()方法设置为过时,不建议大家使用。
为什么呢?
因为使用stop()方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于调用stop()方法来终止线程可能会产生不可预料的结果,因此不推荐调用stop()方法。
一个线程什么时候可以退出呢?当然只有线程自己才能知道。所以,这里介绍一下Thread的interrupt()方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态。
当我们调用线程的interrupt()方法时,它有两个作用:
(1)如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()、Thread.join()和Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早终结被阻塞状态。
(2)如果此线程正处于运行之中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。
说明
如果线程的interrupt()方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。
下面是一个调用interrupt()方法的实例,代码如下:
// 省略import
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();
//打断线程1sleepSeconds(5);
//主线程等待5秒
thread2.interrupt();
//打断线程2,此时线程2已经终止sleepSeconds(1);
//主线程等待1秒
Print.tco("程序运行结束.");
}
}
运行程序,结果大致如下:
[sleepThread-2]:sleepThread-2 进入睡眠.
[sleepThread-1]:sleepThread-1 进入睡眠.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ....InterruptDemo$SleepThread.run(InterruptDemo.java:33)
[sleepThread-1]:sleepThread-1 发生被异常打断.
[sleepThread-2]:sleepThread-2 运行结束.
[main]:程序运行结束.
从结果可以看到,sleepThread-1线程在大致睡眠了两秒后,被主线程打断(或者中断)。被打断的sleepThread-1线程停止睡眠,并捕获到InterruptedException受检异常。程序在异常处理时直接返回了,其后面的执行逻辑被跳过。
从结果还可以看到,sleepThread-2线程在睡眠了7秒后,被主线程中断,但是在sleepThread-2线程被中断的时候,已经执行结束了,所以thread2.interrupt()中断操作没有产生实质性的效果。
Thread.interrupt()方法并不像Thread.stop()方法那样中止一个正在运行的线程,其作用是设置线程的中断状态位(为true),至于线程是死亡、等待新的任务还是继续运行至下一步,就取决于这个程序本身。线程可以不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。总之,Thread.interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,线程是否停止执行,需要用户程序去监视线程的isInterrupted()状态,并进行相应的处理。
下面的示例程序演示如何调用isInterrupted()实例方法监视线程的中断状态,如果发现线程被中断,就进行相应的处理,具体的代码如下:
// 省略import
public class InterruptDemo
{//测试用例:获取异步调用的结果
@Test
public void testInterrupted2()
{
Thread thread = new Thread()
{
public void run()
{
Print.tco("线程启动了");
//一直循环
while (true)
{
Print.tco(isInterrupted());
sleepMilliSeconds(5000);
//如果线程被中断,就退出死循环
if (isInterrupted())
{
Print.tco("线程结束了");
return;
}
}
}
};
thread.start();
sleepSeconds(2);
//等待2秒
thread.interrupt();
//中断线程
sleepSeconds(2);
//等待2秒
thread.interrupt();
}
}
运行程序,输出的结果如下:
[Thread-0]:线程启动了
[Thread-0]:false
[Thread-0]:线程结束了
4.线程合并:join 什么是线程的合并呢?举一个例子,假设有两个线程A和B。现在线程A在执行过程中对另一个线程B的执行有依赖,具体的依赖为:线程A需要将线程B的执行流程合并到自己的执行流程中(至少表面如此),这就是线程合并,被动方线程B可以叫作被合并线程。这个例子中的线程A合并线程B的伪代码大致为:
class ThreadA extends Thread
{
void run()
{
Thread threadb = new Thread("thread-b");
threadb.join();
}
}
1.线程的join操作的三个版本
join()方法是Thread类的一个实例方法,有三个重载版本:
//重载版本1:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束
public final void join() throws InterruptedException://重载版本2:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis的时间
public final synchronized void join(long millis) throws InterruptedException://重载版本3:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis+nanos的时间
public final synchroinzed void join(long millis, int nanos) throws InterruptedException:
调用join()方法的要点:
(1)join()方法是实例方法,需要使用被合并线程的句柄(或者指针、变量)去调用,如threadb.join()。执行threadb.join()这行代码的当前线程为合并线程(甲方),进入TIMED_WAITING等待状态,让出CPU。
(2)如果设置了被合并线程的执行时间millis(或者millis+nanos),并不能保证当前线程一定会在millis时间后变为RUNNABLE。
(3)如果主动方合并线程在等待时被中断,就会抛出InterruptedException受检异常。
说明
调用join()方法的语句可以理解为合并点,合并的本质是:线程A需要在合并点等待,一直等到线程B执行完成,或者等待超时。
为了方便表达,本书将依赖的线程A叫作甲方线程,被依赖的线程B叫作乙方线程。简单理解线程合并就是甲方线程调用乙方线程的join()方法,在执行流程上将乙方线程合并到甲方线程。甲方线程等待乙方线程执行完成后,甲方线程再继续执行.
如果乙方线程无限制长时间地执行,甲方线程可以进行限时等待:甲方线程等待乙方线程执行一定时间后,如果乙方还没有完成,甲方线程再继续执行。调用join()方法的优势是比较简单,劣势是join()方法没有办法直接取得乙方线程的执行结果。
线程合并的示意图
文章图片
2.join线程的WAITING状态
线程的WAITING(等待)状态表示线程在等待被唤醒。处于WAITING状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于WAITING状态:
(1)执行没有时限(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于WAITING状态,一直等线程B执行完成。
(2)执行没有时限(timeout)参数的object.wait()调用:指一个拥有object对象锁的线程,进入相应的代码临界区后,调用相应的object的wait()方法去等待其“对象锁”(Object Monitor)上的信号,若“对象锁”上没有信号,则当前线程处于WAITING状态,如图下图所示。
文章图片
3.join线程的TIMED_WAITING状态
线程的TIMED_WAITING状态表示在等待唤醒。处于TIMED_WAITING状态的线程不会被分配CPU时间片,它们要等待被唤醒,或者直到等待的时限到期。
在线程合入场景中,若线程A在调用B.join()操作时加入了时限参数,则在B执行期间线程A处于TIMED_WAITING状态。若B在等待时限内没有返回,则线程A结束等待TIMED_WAITING状态,恢复成RUNNABLE状态。
5.线程让步:yield 线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程。处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行。
yield()方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次.
6.守护线程:daemon Java中的线程分为两类:守护线程与用户线程。
守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。
举一个比较通俗的例子,守护线程在JVM中相当于保姆的角色:只要JVM实例中尚存在任何一个用户线程没有结束,守护线程就能执行自己的工作;只有当最后一个用户线程结束,守护线程随着JVM一同结束工作。
守护线程的要点使用守护线程时,有以下几点需要特别注意:
(1)守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。具体来说,如果线程为守护线程,就必须在线程实例的start()方法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性值为true。
(2)守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
(3)守护线程创建的线程也是守护线程。在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。
7.线程状态总结 1.NEW状态
通过new Thread(…)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式本质上都是通过new Thread()创建线程,仅仅是创建了不同的target执行目标实例(如Runnable实例)。
2.RUNNABLE状态
Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。
(1)就绪状态
就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远处于就绪状态。当前线程进入就绪状态的条件大致包括以下几种:·
调用线程的start()方法,此线程就会进入就绪状态。·
当前线程的执行时间片用完。·
线程睡眠(Sleep)操作结束。·
对其他线程合入(Join)操作结束。
·等待用户输入结束。
·线程争抢到对象锁(Object Monitor)。
·当前线程调用了yield()方法出让CPU执行权限。
(2)执行状态
线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。
3.BLOCKED状态
处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁
等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2)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)时限结束。
进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。
6.TERMINATED状态
线程结束任务之后,将会正常进入TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。
五、线程池原理与实战 篇幅有限,详解见:【多线程】线程池ThreadPoolExecutor(全面详解)
六、确定线程池的线程数 篇幅有限,详解见:【多线程】线程池ThreadPoolExecutor(全面详解)
推荐阅读
- #|【多线程】线程池ThreadPoolExecutor(全面详解)
- 数据结构|回顾下接雨水问题
- 蓝桥杯|2021模拟赛 跳跃 java_dfs_动态规划
- java|利用Java计算圆的面积
- 蓝桥杯Java真题|19年蓝桥杯Java B组省赛第三题(数列求值)
- Hbu|2-35 判断回文字符串
- 13届蓝桥杯夺奖冲刺营|蓝桥杯省赛夺奖冲刺营散列表篇
- 13届蓝桥杯夺奖冲刺营|蓝桥杯省赛夺奖冲刺营贪心算法
- Java设计模式|Java设计模式之概述与七大设计原则