在Windows操作系统中,进程被分为后台进程和应用进程两类。大部分后台进程在系统开始运行时被操作系统启动,完成操作系统的基础服务功能。大部分应用进程由用户启动,完成用户所需要的具体应用功能,比如听音乐、社交聊天、浏览网站等。
什么是进程呢?简单来说,进程是程序的一次启动执行。什么是程序呢?程序是存放在硬盘中的可执行文件,主要包括代码指令和数据。一个进程是一个程序的一次启动和执行,是操作系统将程序装入内存,给程序分配必要的系统资源,并且开始运行程序的指令。
进程与程序是什么关系呢?同一个程序可以多次启动,对应多个进程。比如,多次打开Chrome浏览器程序,在Process Explorer中可以看到多个Chrome浏览器进程。
进程的定义一直以来没有完美的标准。一般来说,一个进程由程序段、数据段和进程控制块三部分组成。
线程是指“进程代码段”的一次顺序执行流程。线程是CPU调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。
Java程序的进程执行过程就是标准的多线程的执行过程。每当使用Java命令执行一个class类时,实际上就是启动了一个JVM进程。理论上,在该进程的内部至少会启动两个线程,一个是main线程,另一个是GC(垃圾回收)线程。实际上,执行一个Java程序后,通过Process Explorer来观察,线程数量远远不止两个,达到了18个之多。
一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存。
在线程的结构中,线程描述信息即线程的基本信息,主要包括:
(1)线程ID(Thread ID,线程标识符)。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
(2)线程名称。主要是方便用户识别,用户可以指定线程的名字,如果没有指定,系统就会自动分配一个名称。
(3)线程优先级。表示线程调度的优先级,优先级越高,获得CPU的执行机会就越大。
(4)线程状态。表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
(5)其他。例如是否为守护线程等,后面会详细介绍。
在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段内存地址。
在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB大小的栈内存。栈内存和堆内存不同,栈内存不受垃圾回收器管理。
在Java中,执行程序流程的重要单位是“方法”,而栈内存的分配单位是“栈帧”(或者叫“方法帧”)。方法的每一次执行都需要为其分配一个栈帧(方法帧),栈帧主要保存该方法中的局部变量、方法的返回地址以及其他方法的相关信息。当线程的执行流程进入方法时,JVM就会为方法分配一个对应的栈帧压入栈内存;当线程的执行流程跳出方法时,JVM就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。
正是由于栈帧(方法帧)的操作是后进先出的模式,这也是标准的栈操作模式,因此存放方法帧的内存也被叫作栈内存。
下面总结一下进程与线程的区别,主要有以下几点:
(1)线程是“进程代码段”的一次顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。
(2)线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
(3)线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥了CPU的计算性能,又弥补了进程调度过于笨重的问题。
(4)进程之间是相互独立的,但进程内部的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
(5)切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称为轻量级进程。
Thread类除定义了很多操作线程实例的成员方法之外,还有一系列类的静态方法。比如1.2节用到的Thread.currentThread()静态方法就是其中之一,该方法的作用是取得当前CPU内核上正在运行的线程实例。
虽然一个进程有很多个线程,但是在一个CPU内核上,同一时刻只能有一个线程是正在执行的,该线程也被叫作当前线程。
接下来,为大家逐一介绍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类中使用类常量定义,三个类常量如下:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
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()静态方法获取。
新线程如果需要并发执行自己的代码,需要做以下两件事情:
(1)需要继承Thread类,创建一个新的线程类。
(2)同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。
下面的示例将演示如何通过继承Thread类创建一个线程类,新的线程子类重写了Thread的run()方法,实现了用户业务代码的并发执行,具体如下:
package com.crazymakercircle.multithread.basic.create;
import personal.nien.javabook.util.Print;
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 运行结束.
例子中新建了一个静态内部类DemoThread,该内部类继承了Thread线程类。在DemoThread的构造函数中,通过super()调用基类的Thread(String threadName)构造方法设置了线程的名称。
通过继承Thread类并重写它的run()方法只是创建Java线程的一种方式。是否可以不继承Thread类实现线程的新建呢?答案是肯定的。
重温一下Thread类的run()方法的代码,里边其实有点玄机,其代码如下:
package java.lang;
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接口是何方神圣。
1.Runnable接口
Runnable是一个极为简单的接口,位于java.lang包中。接口中只有一个方法run(),具体的源代码如下:
package java.lang;
@FunctionalInterface
public interface Runnable {
void run();
}
Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的实现版本将被异步调用。
2.通过实现Runnable接口创建线程类
创建线程的第二种方法就是实现Runnable接口,将需要异步执行的业务逻辑代码放在Runnable实现类的run()方法中,将Runnable实例作为target执行目标传入Thread实例。该方法的具体步骤如下:
(1)定义一个新类实现Runnable接口。
(2)实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
(3)通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
4)调用Thread实例的start()方法启动线程。
(5)线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。
按照上面的5步,实现一个简单的并发执行的多线程演示实例,代码如下:
package com.crazymakercircle.multithread.basic.create;
// 省略import
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();
}
}
}
实例中静态内部类RunTarget执行目标类,不再是继承Thread线程类,而是实现Runnable接口,需要异步并发执行的代码逻辑被编写在它的run()方法中。
说明
值得注意的是,run()方法实现版本中在获取当前线程的名称时,所用的方法是在外部类ThreadUtil中定义的getCurThreadName()静态方法,而不是Thread类的getName()实例方法。原因是:这个RunTarget内部类和Thread类不再是继承关系,无法直接调用Thread类的任何实例方法。
通过实现Runnable接口的方式创建的执行目标类,如果需要访问线程的任何属性和方法,必须通过Thread.currentThread()获取当前的线程对象,通过当前线程对象间接访问。
public static String getCurThreadName() {
return Thread.currentThread().getName(); // 获取线程名称
}
通过继承Thread类的方式创建的线程类,可以在子类中直接调用Thread父类的方法访问当前线程的名称、状态等信息。这也是使用Runnable实现异步执行与继承Thread方法实现异步执行不同的地方。
完成了Runnable的实现类后,需要调用Thread类的构造器创建线程,并将Runnable实现类的实例作为实参传入。可以调用的构造器(即构造函数)包括如下三个:
(1)public Thread(Runnable target)
(2)public Thread(Runnable target,String name)
(3)public Thread(ThreadGroup group,Runnable target)
若调用以上第二个构造器构造线程时可以指定线程的名称,则实例如下:
thread = new Thread(new RunTarget(), "name" + threadNo++);
线程对象创建完成后,调用Thread线程实例的start()方法启动新线程的并发执行。这时,Runnable实例的run()方法会在新线程Thread的实例方法run()中被调用。
使用Runnable创建线程目标类除了直接实现Runnable接口之外,还有两种比较优雅的代码组织方式:
(1)通过匿名类优雅地创建Runnable线程目标类。
(2)使用Lambda表达式优雅地创建Runnable线程目标类。
1.通过匿名类优雅地创建Runnable线程目标类
在实现Runnable编写target执行目标类时,如果target实现类是一次性类,可以使用匿名实例的形式。上一小节的执行目标类是一个静态内部类,现在改写成匿名实例的形式,代码如下:
package com.crazymakercircle.multithread.basic.create;
// 省略import
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() + " 运行结束.");
}
}
使用Runnable的匿名实例方式和编写普通的执行目标类相比,代码的区别很小、主要的区别体现在代码①处,其他的代码完全相同。在代码①处,通过编写匿名类的实现代码直接创建了一个Runnable类型的匿名target执行目标对象。
2.使用Lambda表达式优雅地创建Runnable线程目标类
回顾一下Runnable接口,其源代码中还有一个小玄机,具体如下:
@FunctionalInterface
public interface Runnable {
void run();
}
源代码中的小玄机为:在Runnable接口上声明了一个@FunctionalInterface注解。该注解的作用是:标记Runnable接口是一个“函数式接口”。在Java中,“函数式接口”是有且仅有一个抽象方法的接口。反过来说,如果一个接口中包含两个或两个以上的抽象方法,就不能使用@FunctionalInterface注解,否则编译会报错。
说明
@FunctionalInterface注解不是必需的,只要一个接口符合“函数式接口”的定义,使用时加不加@FunctionalInterface注解都没有影响,都可以当作“函数式接口”来使用。
Runnable接口是一个函数式接口,在接口实现时可以使用Lambda表达式提供匿名实现,编写出比较优雅的代码。上一小节的执行目标类是一个静态内部类,现在改写成Lambda表达式的形式,代码如下:
package com.crazymakercircle.multithread.basic.create;
// 省略import
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执行目标实例的代码与创建target执行目标匿名实例的代码的区别也很小,区别主要在代码①处,其他的部分完全相同。在代码①处,通过Lambda表达式直接编写Runnable接口的run()方法的实现代码,接口的名称(Runnable)、方法的名称run()统统都被省略,仅剩下了run()方法的形参列表和方法体。
总体而言,经过对比可以发现:使用Lambda表达式创建target执行目标实例,代码已经做到了极致的简化。
通过实现Runnable接口的方式创建线程目标类有以下缺点:
(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。
通过实现Runnable接口的方式创建线程目标类有以下优点:
(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。比如,当一个Dog类继承了Pet类,再要继承Thread类就不行了。所以在已经存在继承关系的情况下,只能使用实现Runnable接口的方式。
(2)逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源被多个线程逻辑异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。
1.“逻辑和数据更好地分离”演示实例
通过实现Runnable接口的方式创建线程目标类更加适合多个线程的代码逻辑去共享计算和处理同一个资源的场景。这个优点不是太好理解,接下来通过具体例子说明一下。
package com.crazymakercircle.multithread.basic.create;
// 省略import
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个商店商品的销售线程实例。
Print.hint("商店版本的销售");
for (int i = 1; i <= 2; i++)
{
Thread thread = null;
thread = new StoreGoods("店员-" + i); //商店商品的销售线程
thread.start();
}
上面的代码新建了n(这里为2)个线程,相当于n个不同的商店店员,每个商店店员负责一个数量,并且负责将自己的数量卖完。每个商店店员(线程)各卖各的,其剩余数量都是从4卖到0,没有关联。商店店员的售卖过程大致如图1-6所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2uHcTCGV-1659331095355)(blob:null/67bc9389-585c-454c-ae08-0e2d232f967c)]
图1-6 n个商店店员的售卖过程
再来看另一个内部类MallGoods。在main()方法中创建销售线程的时候创建了1个公用的MallGoods商品销售对象。
Print.hint("商场版本的销售");
MallGoods mallGoods = new MallGoods(); //创建了1个公用的MallGoods对象
for (int i = 1; i <= 2; i++)
{
Thread thread = null;
thread = new Thread(mallGoods, "商场销售员-" + i); //销售员线程
thread.start();
}
以上代码新建了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去创建异步执行任务。
前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。
这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
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。不同的是,Runnable的唯一抽象方法run()没有返回值,也没有受检异常的异常声明。比较而言,Callable接口的call()有返回值,并且声明了受检异常,其功能更强大一些。
问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?一个在Callable接口与Thread线程之间起到搭桥作用的重要接口马上就要登场了。
2.RunnableFuture接口
这个重要的中间搭桥接口就是RunnableFuture接口,该接口与Runnable接口、Thread类紧密相关。与Callable接口一样,RunnableFuture接口也位于java.util.concurrent包中,使用的时候需要用import导入。
RunnableFuture是如何在Callable与Thread之间实现搭桥功能的呢?RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture接口的代码:
package java.util.concurrent;
public interface RunnableFuture extends Runnable, Future {
void run();
}
通过源代码可以看出:RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。
在这里,一个新的、从来没有介绍过的、非常重要的Future接口马上登场。
3.Future接口
Future接口至少提供了三大功能:
(1)能够取消异步执行中的任务。
(2)判断异步任务是否执行完成。
(3)获取异步任务完成后的执行结果。
Future接口的源代码如下:
package java.util.concurrent;
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。
4.FutureTask类
FutureTask类是Future接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask类不仅实现了Future接口,还实现了Runnable接口,或者更加准确地说,FutureTask类实现了RunnableFuture接口。
前面讲到RunnableFuture接口很关键,既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread与Callable之间一个非常重要的搭桥角色。但是,RunnableFuture只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask。所以说,FutureTask类才是真正的在Thread与Callable之间搭桥的类。
FutureTask类的UML关系图大致如图1-8所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDFqySvy-1659331095356)(https://cdn.jsdelivr.net/gh/WaterMoonMirror/typora-img/img/202208011057789.png)]
图1-8 FutureTask类的UML关系图
从FutureTask类的UML关系图可以看到:FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
FutureTask如何完成多线程的并发执行、任务结果的异步获取呢?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.使用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接口相结合创建多线程,实例如下:
package com.crazymakercircle.multithread.basic.create;
// 省略import
public class CreateDemo3 {
public static final int MAX_TURN = 5;
public static final int COMPUTE_TIMES = 100000000;
//①创建一个 Callable 接口的实现类
static class ReturnableTask implements Callable<Long> {
//②编写好异步执行的具体逻辑,可以有返回值
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<Long> futureTask = new FutureTask<Long>(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()方法的主线程,叫作main;另一个是main线程通过thread.start()方法启动的业务线程,叫作returnableThread。该线程是一个包含FutureTask任务作为target的Thread线程。
main线程通过thread.start()启动returnableThread线程之后,会继续自己的事情,returnableThread线程开始并发执行。
returnableThread线程首先执行的是thread.run()方法,然后在其中会执行到其target(futureTask任务)的run()方法;接着在这个futureTask.run()方法中会执行futureTask的callable成员的call()方法,这里的callable成员(ReturnableTask实例)是通过FutureTask构造器在初始化时传递进来的、自定义的Callable实现类的实例。
main线程和returnableThread线程的执行流程大致如图1-9所示。
FutureTask的Callable成员的call()方法执行完成后,会将结果保存在FutureTask内部的outcome实例属性中。以上演示实例的Callable实现类中,这里call()方法中业务逻辑的返回结果是call()方法从进入到出来的执行时长:
long startTime = System.currentTimeMillis();
...
long used = System.currentTimeMillis() - startTime;
return used;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dff8L8Ku-1659331095356)(https://cdn.jsdelivr.net/gh/WaterMoonMirror/typora-img/img/202208011108538.png)]
图1-9 main线程和returnableThread线程
执行时长返回之后,将被作为结果保存在FutureTask内部的outcome实例属性中。至此,异步的returnableThread线程执行完毕。在main线程处理完自己的事情后(以上实例中是一个消磨时间的循环),通过futureTask的get实例方法获取异步执行的结果。这里有两种情况:
(1)futureTask的结果outcome不为空,callable.call()执行完成。在这种情况下,futureTast.get会直接取回outcome结果,返回给main线程(结果获取线程)。
(2)futureTask的结果outcome为空,callable.call()还没有执行完。在这种情况下,main线程作为结果获取线程会被阻塞住,一直阻塞到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒main线程,去提取callable.call()执行结果。
前面的示例中,所创建的Thread实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资源耗费上都很高(稍后会介绍),在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。Java中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类。
1.线程池的创建与执行目标提交
通过Executors工厂类创建一个线程池,一个简单的示例如下:
//创建一个包含三个线程的线程池
private static ExecutorService pool = Executors.newFixedThreadPool(3);
以上示例通过工厂类Executors的newFixedThreadPool(int threads)方法创建了一个线程池,所创建的线程池的类型为ExecutorService。工厂类的newFixedThreadPool(int threads)方法用于创建包含固定数目的线程池,示例中的线程数量为3。
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执行目标实例的示例代码,大致如下:
package com.crazymakercircle.multithread.basic.create;
// 省略import
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<Long>
{
//返回并发执行的时间
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执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。
说明
:pool-1-thread-1, 轮次:3
[CreateDemo4
1.
r
u
n
]
:
p
o
o
l
−
1
−
t
h
r
e
a
d
−
2
,
轮次:
3
[
C
r
e
a
t
e
D
e
m
o
4
1.run]:pool-1-thread-2, 轮次:3 [CreateDemo4
1.run]:pool−1−thread−2,轮次:3[CreateDemo4DemoThread.run]:pool-1-thread-1, 轮次:4
[CreateDemo4
1.
r
u
n
]
:
p
o
o
l
−
1
−
t
h
r
e
a
d
−
2
,
轮次:
4
[
C
r
e
a
t
e
D
e
m
o
4
1.run]:pool-1-thread-2, 轮次:4 [CreateDemo4
1.run]:pool−1−thread−2,轮次:4[CreateDemo4ReturnableTask.call]:pool-1-thread-3 线程运行开始.
[CreateDemo4
R
e
t
u
r
n
a
b
l
e
T
a
s
k
.
c
a
l
l
]
:
p
o
o
l
−
1
−
t
h
r
e
a
d
−
3
,
轮次:
1
[
C
r
e
a
t
e
D
e
m
o
4
ReturnableTask.call]:pool-1-thread-3, 轮次:1 [CreateDemo4
ReturnableTask.call]:pool−1−thread−3,轮次:1[CreateDemo4ReturnableTask.call]:pool-1-thread-3, 轮次:2
[CreateDemo4
R
e
t
u
r
n
a
b
l
e
T
a
s
k
.
c
a
l
l
]
:
p
o
o
l
−
1
−
t
h
r
e
a
d
−
3
,
轮次:
3
[
C
r
e
a
t
e
D
e
m
o
4
ReturnableTask.call]:pool-1-thread-3, 轮次:3 [CreateDemo4
ReturnableTask.call]:pool−1−thread−3,轮次:3[CreateDemo4ReturnableTask.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创建线程池,线程池是一个很重要的知识点,后面会详细介绍。