本章是 Java 多线程开发入门,重点介绍 Java 的线程状态及其互相转换。
一、根底知识点
多线程开发中,会波及到很多基础知识,这里后行梳理其中两个重要的知识点。
1.1 并发与并行
在多线程开发中,会常常提到并发与并行两个概念,咱们须要先明确这两个概念的含意到底是什么。
并发,concurrency
,是一段时间内解决多件事件的需要,它是问题域 problem domain
的概念。
并行,parallelism
,是在同一时刻同时解决多件事件的形式,它是办法域 solution domain
的概念。
并发以黑盒的角度,将指标零碎看做一个实体,要求该实体在一段时间内可能解决多件事件;并行以白盒的角度,将指标零碎合成为多个实体,这些实体能够同时别离解决多件事件,有多少个实体,就有多少并行,比方一个 8 核的 CPU,能够并行地运行 8 个工作。
并发是问题,是需要;并行是解决并发问题的办法之一。并发只提出要求,理论实现时,能够是单个实体串行解决工作(一个工作解决完再解决下一个),能够是多个实体并行处理工作(每个实体解决一个工作),能够是单个实体一直在多个工作之间来回切换着解决(如单核 CPU),甚至是多个实体一直在多个工作之间来回切换着工作做并行处理(如多核 CPU)。如下图所示:
1.2 过程与线程
了解过程与线程的不同是多线程编程根底中的根底。
操作系统并非一开始就划分了过程和线程,甚至直到现在,Linux 这样的操作系统上,过程与线程的分界仍然不是那么清晰。但咱们能够简略地认为:操作系统将内存调配给不同的过程,将 CPU 调配给不同的线程;同一个过程内有多个线程能够共享该过程调配到的内存,而 CPU 以线程为单位在不同线程间一直切换执行。
勉为其难来个定义:
过程
就是操作系统中一个具备独立性能的程序,操作系统治理所有过程的执行并且以过程为单位调配存储空间。线程
是一个过程中的能够并发的执行流程,是能够取得 CPU 调度和分派的根本执行单元。一个过程能够有多个线程。
进一步开展,过程是从操作系统申请并领有内存的根本单位。每个过程是由公有的虚拟地址空间、代码、数据和其它各种系统资源组成。能够认为,过程是被操作系统加载到内存中的工作单元。
而线程则是 CPU 调度和调配的根本单位。线程是过程外部的一个执行单元。每一个过程至多有一个主线程,它无需由用户去被动创立,是由零碎主动创立的。用户依据须要在应用程序中创立其它线程,多个线程并发地运行于同一个过程中。
从开发语言的角度看,线程能够视为过程中的 运行时
的控制流
。开发语言中动态的控制流由一条条自上而下的语句组成,可能会有分支和循环;这种动态的控制流在运行时的映射就是线程。一个过程至多会蕴含一个线程,因为其中至多会有一个控制流继续运行。因此,一个过程的第一个线程会随着这个过程的启动而创立,这个线程称为该过程的主线程。当然,一个过程往往蕴含多个线程,这些线程都是由以后过程中已存在的线程创立进去的,线程不可能独立于过程存在。它的生命周期不可能超越其所属过程的生命周期。
从计算机资源的应用角度看,过程是计算机资源的拥有者,创立、切换和销毁都有较大的时空开销,而一个过程内的所有线程共享这个过程的资源,更轻量级,相干操作的开销也绝对更小。须要留神的是,对于单核 CPU 零碎而言,并行其实是不存在的,任何时刻 CPU 其实只能被一个线程所获取,线程之间共享了 CPU 的执行工夫。因为切换的速度很快,对外体现为并发执行的样子。
领有多个线程的过程能够并发地执行多个工作,并且即便某个或某些工作被阻塞,也不会影响其余工作失常执行。在资源足够的状况下,这能够大大改善程序的响应工夫和吞吐量。这就是多线程编程的基本思路。当然理论实现的时候,还有很多问题要解决,这个在后续章节会有梳理。
二、创立线程根本形式
让咱们从创立线程开始,看看 Java 中如何进行多线程编程。
Java 创立新的线程有三种根本的形式:继承 Thread 类,实现 Runnable 接口,实现 Callable 接口。
2.1 继承 Thread 类
实现代码如下:
package com.czhao.test.mutithread;
/**
* @author zhaochun
*/
public class ThreadTest {public static void main(String[] args) {
// 间接 new 一个 Thread 对象
Thread subThread = new Printer();
// 调用 Thread 对象的 start 办法,留神不能调用 run 办法
subThread.start();
System.out.println("Print in mainThread.");
}
// 定义一个继承了 Thread 的类
static class Printer extends Thread {
// 重写 run 办法,实现本人的业务逻辑
@Override
public void run() {System.out.println("Print in subThread.");
}
}
}
留神,线程创立后,应用 start()
办法才是启动一个新的线程,不能间接调用 Thread 子类中重写的办法run()
。
2.2 实现 Runnable 接口
认真看一下继承的父类 Thread
,会发现它实现了接口Runnable
,咱们也能够间接实现Runnable
接口:
package com.czhao.test.mutithread;
/**
* @author zhaochun
*/
public class RunnableTest {public static void main(String[] args) {
// 将一个 Runnable 对象作为 Thread 的结构参数
Thread subThread = new Thread(new Printer());
// 调用 Thread 对象的 start 办法,留神不能调用 run 办法
subThread.start();
System.out.println("Print in mainThread.");
}
// 定义一个实现了 Runnable 接口的类
static class Printer implements Runnable {
// 实现 run 办法
@Override
public void run() {System.out.println("Print in Runnable.");
}
}
}
2.3 实现 Callable 接口
Runnable 接口的 run 办法是没有返回值的。当咱们须要子线程运行完结后提供一个返回值时,就须要用到 Callable
接口。Callable
的返回值反对泛型,但因为它的返回值是异步返回的,因而无奈间接在主线程中获取返回值,而是配合 Future
接口或 FutureTask
类来获取返回值。
2.3.1 应用 Future 获取 Callable 返回值
package com.czhao.test.mutithread;
import java.time.LocalDateTime;
import java.util.concurrent.*;
/**
* @author zhaochun
*/
public class CallableTest {public static void main(String[] args) {CallableTest me = new CallableTest();
me.testFuture();}
private void testFuture() {
// Callable 实现类不能间接作为 Thread 结构参数传入,这里应用线程池来提交一个 Callable 工作
ExecutorService executor = Executors.newSingleThreadExecutor();
// 通过 submit 办法向线程池提交 Callable 工作,submit 办法返回的是 Future 对象
Future<LocalDateTime> future = executor.submit(new Printer());
// 线程池不再接管新的工作
executor.shutdown();
System.out.println("Print in mainThread.");
try {// future.get()获取子线程的运行后果,如果子线程此时尚未运行完结,则主线程在该步骤会期待直到子线程完结返回后果
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
}
}
// 定义一个实现了 Callable 接口的类,并指定返回值类型
class Printer implements Callable<LocalDateTime> {
// 实现 call 办法,并返回指定类型的值
@Override
public LocalDateTime call() throws Exception {System.out.println("Print in Callable.");
return LocalDateTime.now();}
}
}
Future
接口是用来获取指标线程执行后果的接口,通常和 Callable
一起通过线程池来应用。线程池技术在后续章节会有梳理,这里理解即可。
Future
接口定义了几个简略的办法:
// 试图勾销一个线程的执行,mayInterruptIfRunning 示意是否尝试中断运行中线程,返回勾销后果
// 线程未必能被勾销,因为线程可能曾经实现,曾经勾销过了,始终在运行中无奈勾销等等
boolean cancel(boolean mayInterruptIfRunning)
// 线程是否已被勾销
boolean isCancelled()
// 工作是否曾经实现
boolean isDone()
// 尝试获取指标线程运行的返回值,即 Callable 的返回值,指标线程没有完结的话以后线程会期待指标线程执行完结
V get()
// 限时的 get
V get(long timeout, TimeUnit unit)
2.3.2 应用 FutureTask 获取 Callable 返回值
package com.czhao.test.mutithread;
import java.time.LocalDateTime;
import java.util.concurrent.*;
/**
* @author zhaochun
*/
public class CallableTest {public static void main(String[] args) {CallableTest me = new CallableTest();
me.testFutureTask();}
private void testFutureTask() {
// Callable 实现类不能间接作为 Thread 结构参数传入,而是须要包装一层 FutureTask 将其转为 Runnable 接口
FutureTask<LocalDateTime> futureTask = new FutureTask<>(new Printer());
Thread subThread = new Thread(futureTask);
subThread.start();
System.out.println("Print in mainThread.");
try {
// 在主线程中获取子线程执行完结后返回的后果,这里是 LocalDateTime 类型的工夫戳。// 要留神的是,如果子线程此时尚未运行完结,则主线程执行 futureTask.get()时会期待,始终到子线程完结返回后果。System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
}
}
// 定义一个实现了 Callable 接口的类,并指定返回值类型
class Printer implements Callable<LocalDateTime> {
// 实现 call 办法,并返回指定类型的值
@Override
public LocalDateTime call() throws Exception {System.out.println("Print in Callable.");
return LocalDateTime.now();}
}
}
FutureTask
是一个实现类,它实现了 RunnableFuture
接口,而 RunnableFuture
接口继承了 Runnable
接口和 Future
接口。所以 FutureTask
可能作为 Thread 的结构参数,同时也能够用来获取指标线程执行后果。Future
自身只是接口,要实现它的办法比较复杂,而有了 FutureTask
就升高了应用 Future
的难度。Future
要联合线程池来应用,而 FutureTask
既能够与线程池配合应用,也能够间接作为 Thread 的结构参数应用,更加不便。
2.4 创立线程小结
本节讲的是线程的创立,有三种形式:
- 继承 Thread
- 实现 Runnable
- 实现 Callable
留神,以上三种形式是 Java 目前仅有的三种创立线程的办法,但仅仅只是创立线程的办法,并没有波及到线程的残缺生命周期的治理。特地要留神的是,线程创立后,并不是就会立刻启动执行,那须要后续的 thread.start()
或者提交到线程池中去,后续章节会有介绍。
三、线程状态及互相转换
Java 中的线程状态与操作系统的线程状态并不相同。操作系统的线程状态是以 CPU 为视角划分的,而 Java 的线程是以 JVM 为视角划分的。它们之间的关系,以及各个状态之间的转换关系,如下图所示:
本节将对操作系统线程状态和 JVM 线程状态进行梳理。
3.1 操作系统线程状态及其转换
大部分操作系统都将线程状态大抵划分为 new
、ready
、running
、waiting
和terminated
五种状态。
很多材料将这种状态划分称为过程状态,次要是因为晚期操作系统只有过程没有线程,或者说是
单线程过程
;而且起初线程呈现后,像 Linux 这样的操作系统实际上并没有独自再实现线程,而是将线程实现为 ” 轻量级过程 ”,对 Linux 内核来说,过程和线程的数据结构是一样的,甚至能够说,对 Linux 来说过程和线程的界线是含糊的。
这五种状态中比拟重要的是 ready
、running
、waiting
这三种状态。上面以古代支流操作系统 (如 linux) 为例,看一下这三种状态的转换。
- 线程创立后会进入
ready
就绪状态。 - 操作系统将 CPU 工夫切分为一个一个间断的周期,个别在 10~20ms,而后依照这个分片轮转地抉择就绪状态的线程去执行,被抉择的线程进入
running
状态。 - 当一次工夫分片完结,操作系统会收回一个中断信号 (interrupt),告诉 CPU 中断以后
running
状态的线程,将其退回就绪状态,并从新从就绪状态的线程中抉择一个执行。即 CPU 切换线程。 - 当某个
running
的线程执行到 IO 操作 (比方读写磁盘) 时,该线程会退出 CPU,因为此时不再由 CPU 执行它,而是磁盘执行它;从 CPU 的角度而言,它进入了waiting
期待 / 阻塞状态;习惯上咱们称这种状态为 IO 阻塞,但其实对线程而言只是执行它的地点从 CPU 换到了磁盘或其余 IO 设施上了而已。此时 CPU 当然会选取其余就绪线程去执行以防止 CPU 资源节约。 - 当 IO 操作完结时,对应线程从
waiting
状态变为就绪状态,操作系统也会给 CPU 收回一个中断信号(interrupt),告诉 CPU 再次切换线程。
当然这 5 种状态只是一个形象或者说总结,实际上不同的操作系统对线程状态的划分和命名不尽相同。比方 waiting
,在 Linux 操作系统中,就对应着 S(浅度睡眠)、D(深度睡眠)、T(暂停态) 等状态,总之咱们将其了解为还可能再次运行的,目前因为 IO 阻塞等起因不占用 CPU 的线程状态即可。
3.2 Java 线程的 6 个状态
从 Thread.State
这个枚举中能够看到 Java 定义的 6 种线程状态:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED\_WAITING
- TERMINATED
其中,NEW
和 TERMINATED
很简略,别离是还没有启动的线程状态,和曾经完结的线程状态。它们与操作系统层面对应的线程状态是统一的。
但其余状态并不与操作系统线程状态一一对应,而是更为简单的一种对应关系:
RUNNABLE
对应着操作系统层面的ready
、running
和 IO 阻塞时的waiting
线程状态;BLOCKED
、WAITING
与TIMED_WAITING
这三个 JVM 线程状态则都对应着操作系统层面的waiting
线程状态,它们在操作系统层面进入waiting
状态不是由操作系统层面的 IO 阻塞或者某些事件引起的,而是 JVM 层面的某些语法被动引起的。
上面咱们别离来看一下这几个状态以及它们之间的转换。
3.2.1 RUNNABLE 状态
JavaDoc 中对 RUNNABLE 状态的阐明:
A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
处于 RUNNABLE 状态的线程正在 Java 虚拟机中执行,但它可能正在期待来自操作系统的其余资源,如处理器。
对于 JVM 来说,不论是 CPU,还是磁盘,网卡,都是资源,即便 CPU 不在执行这个线程,也有其余硬件正在执行这个线程。因而操作系统从 CPU 视角划分的 ready
、running
和 IO 阻塞引起的 waiting
状态对于 JVM 中的线程来说,都属于 RUNNABLE
状态;线程要么是就绪状态,要么就是正在被 CPU/ 磁盘 / 网络接口等硬件执行着,所以都是 RUNNABLE 的。这种设计的根本原因在于,Java 的支流 JVM 比方 HotSpot,目前它的线程模型采纳的是内核线程模型,每个线程实际上都和内核线程 1:1
对应,Java 仅仅在内核线程里面封装了一层而已,对线程的创立 / 调度 / 销毁等操作理论都依赖于操作系统。JVM 自身并不负责调度线程,所以很天然的,操作系统层面的 ready
、running
和waiting
这三种线程状态对 JVM 来说就没有意义。
所以 Java 中有以下景象:
- 应用
Thread.yield()
办法不会导致 Java 线程状态变动。它只是让以后线程在操作系统层面从running
退到ready
,让 CPU 切换线程执行;并且 CPU 有可能又抉择了这个刚退到running
状态的线程来执行。这个过程中,Java 线程状态将始终放弃在RUNNABLE
。 - 当一个线程执行到 IO 语句时,对于操作系统来说,这个线程的确曾经进入了 IO 阻塞
waiting
状态,但对于 JVM 来说,线程仍处于RUNNABLE
状态;在 IO 完结时,操作系统将该线程变为ready
就绪状态,但对于 JVM 来说,线程仍处于RUNNABLE
状态;最初,如果 CPU 给面子抉择了这个刚完结 IO 进入就绪状态的线程来执行,但对于 JVM 来说,线程仍处于RUNNABLE
状态。
3.2.2 BLOCKED 状态
Java 线程状态 BLOCKED
阻塞状态,跟后面讲的 IO 阻塞不是一回事,它特指被 Java 的 synchronized 块或办法所阻塞的状态。实际上就是试图抢占 synchronized 锁失败的话,线程就会进入 BLOCKED
状态。
对于
synchronized 锁
,将在前面的章节中具体梳理,这里理解它是 Java 在语言层面实现的锁,用于同步代码块或办法即可。
JavaDoc 中对 BLOCKED 状态的阐明:
A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling {@link Object#wait() Object.wait}.
处于 BLOCKED 状态的线程正在期待取得监视器锁来进入一个 synchronized 代码块或办法;或者这个线程之前取得过一个 synchronized 代码块或办法的监视器锁,但它起初通过 wait 办法开释了这个锁,并到期了或被其余线程的 notify 唤醒了,当初也处在 BLOCKED 状态,期待从新取得该锁以再次进入这个 synchronized 代码块或办法继续执行。
这段英文及翻译比拟难懂,咱们用两段代码来阐明:
第一段代码:
private void testEnterBlocked() {
class EchoPrinter {
// synchronized 润饰实例办法,则锁为 this,即 EchoPrinter 的一个实例
public synchronized void echoPrint1() {Scanner scanner = new Scanner(System.in);
System.out.println("我是胡汉三,我要筹备跑路了,轻易说点啥吧:");
String content = scanner.nextLine();
System.out.println(content);
}
public void echoPrint2() {System.out.println("我是潘冬子,筹备抢锁,按导演的打算,我会失败...");
// 应用 this 作为锁,即 EchoPrinter 的一个实例
synchronized (this) {System.out.println("我是潘冬子,我抢到锁了...");
}
}
}
// 因为锁是 EchoPrinter 的一个实例,这里须要学生成实例
EchoPrinter echoPrinter = new EchoPrinter();
// 创立线程 1 并启动,线程 1 将运行 echoPrint 办法
Thread t1 = new Thread(echoPrinter::echoPrint1);
// t1.setDaemon(true);
t1.start();
// 主线程期待 1 秒钟,以确保线程 1 启动
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 创立线程 2 并启动,线程 2 将运行 print 办法
Thread t2 = new Thread(echoPrinter::echoPrint2);
// t2.setDaemon(true);
t2.start();
// 主线程期待 1 秒钟,以确保线程 2 启动
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 在主线程输入线程 1 的状态,因为 echoPrint 办法里有从 System.in 读取控制台输出的语句,会在这里陷入操作系统层面的 IO 阻塞,但 JVM 中线程状态依然是 RUNNABLE
System.out.println(String.format("线程 %s 的状态 %s", "胡汉三", t1.getState().toString()));
// 在主线程输入线程 2 的状态,因为 print 办法外部也应用 this 作为锁,而该锁目前依然被线程 1 占用着,所以线程 2 获取锁失败而进入 BLOCKED 状态。// 留神线程 2 的 BLOCKED 状态是 JVM 线程状态,与线程 1 在操作系统层面的 IO 阻塞状态并不相同。System.out.println(String.format("线程 %s 的状态 %s", "潘冬子", t2.getState().toString()));
}
第一段代码解释了什么叫 A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method
,运行期间的线程状态变动如下图所示,留神 潘冬子线程
第一次尝试抢占锁失败后的状态:
第二段代码:
private void testReenterBlocked() {
class EchoPrinterAgain {
// synchronized 润饰实例办法,则锁为 this,即 EchoPrinter 的一个实例
public synchronized void echoPrint1() {Scanner scanner = new Scanner(System.in);
System.out.println("我是胡汉三,我要筹备跑路了,轻易说点啥吧:");
String content = scanner.nextLine();
System.out.println(content);
try {
// 让出锁,以后 JAVA 线程进入 WAITING 状态
this.wait();} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("我胡汉三又回来了。。。over。。。");
}
public void echoPrint2() {System.out.println("我是潘冬子,筹备抢锁,按导演的打算,我会失败...");
// 应用 this 作为锁,即 EchoPrinter 的一个实例
synchronized (this) {System.out.println("我是潘冬子,我抢到锁了...");
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("我是潘冬子,导演让我喊胡汉三回来...");
this.notifyAll();
System.out.println("我是潘冬子,我唤醒了胡汉三,等我劳动三秒...");
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("我是潘冬子,我行将退出舞台。。。");
}
}
}
// 因为锁是 EchoPrinter 的一个实例,这里须要学生成实例
EchoPrinterAgain echoPrinterAgain = new EchoPrinterAgain();
// 创立线程 1 并启动,线程 1 将运行 echoPrint 办法
Thread t1 = new Thread(echoPrinterAgain::echoPrint1);
t1.start();
// 主线程期待 1 秒钟,以确保线程 1 启动
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 创立线程 2 并启动,线程 2 将运行 print 办法
Thread t2 = new Thread(echoPrinterAgain::echoPrint2);
t2.start();
// 主线程每隔一秒打印一次两个子线程的状态
while (true) {
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(String.format("线程 %s 的状态 %s", "胡汉三", t1.getState().toString()));
System.out.println(String.format("线程 %s 的状态 %s", "潘冬子", t2.getState().toString()));
if (Thread.State.TERMINATED.equals(t1.getState())
&& Thread.State.TERMINATED.equals(t2.getState())) {break;}
}
}
第二段代码解释了什么叫 A thread in the blocked state is waiting for a monitor lock to reenter a synchronized block/method after calling {@link Object#wait() Object.wait}
,运行期间的线程状态变动如下图所示,留神 胡汉三线程
如何通过 wait 办法开释锁,又如何从新取得锁继续执行的:
上述代码中还有
Thread.sleep(long)
导致线程进入TIMED_WAITING
状态,以及wait()
导致线程进入WAITING
状态,这两种状态接下来持续梳理。
3.2.3 WAITING 状态
依据 JavaDoc 的形容,当一个线程执行以下办法时,会进入 WAITING
状态:
- 不带时限的
Object.wait
办法 - 不带时限的
Thread.join
办法 LockSupport.park
办法
处于该状态的线程只能无限期期待另一个线程执行一个特地的动作来唤醒该线程进入 RUNNABLE
状态。所谓特地动作包含:
- 一个调用了锁对象的
Object.wait
办法的线程会期待另一个线程调用同一个锁对象的Object.notify()
或Object.notifyAll()
,notify 随机唤醒一个在该锁对象上WAITING
的线程,notifyAll 唤醒在该锁对象上WAITING
的所有线程。 - 一个调用了
Thread.join
办法的线程只能始终处于WAITING
状态直到指定的线程执行完结,个别用于主线程获取子线程执行后果的场景。 - 一个调用了
LockSupport.park
办法的线程只能始终处于WAITING
状态直到有其余线程对该线程执行LockSupport.unpark(thread)
或thread.interrupt()
。
对于 wait/notify
的应用,参考【2) BLOCKED 状态】中的第二段代码即可。咱们对其进行进一步的剖析,看看为什么 Java 要在 BLOCKED
之外,又引入了 WAITING
这个状态。
首先思考一个问题:
- 在什么样的场景下,一个同步代码块执行了一半却须要开释锁呢?
答案很简略,某些业务可能须要满足某种条件能力继续执行。而这种条件的达成,无奈在以后线程实现,只能由其余线程实现。于是本线程开释锁,等其余线程实现相应操作,达成了条件后,本线程再继续执行。这其实是观察者模式的一种利用场景,而 wait/notify
也就总是成对呈现。咱们用一个主动贩卖机的例子来阐明这个过程,请参考下图:
此时咱们明确了为什么会有 wait
办法用于在同步代码块执行了一半的时候开释锁。当初咱们思考第二个问题:
- 为什么执行
wait
办法的线程进入了WAITING
状态而不是间接进入BLOCKED
状态?
对于这个答案,咱们用下图阐明起因:
这样,咱们就明确了为什么要有 WAITING
状态。
而 wait/notify
机制的残缺应用示意,则如下图所示:
对于 wait/notify
的最初一个问题:
- 用
notify
还是notifyAll
?
个别倡议用 notifyAll
。因为在一个对象锁上进入WAITING
的线程可能不止一个,notifyAll
将唤醒全副,让它们都脱离 WAITING
状态,而 notify
只会随机唤醒其中一个。所以如果是多个线程期待雷同条件能力持续,而只有一个线程能达成该条件且只做一次时,notify
将导致有些 WAITING
线程始终处于 WAITING
状态。
对于 Thread.join
办法,它就是让以后线程期待指标线程执行完结后再执行,代码示例如下:
private void testJoin() {
class Poet implements Runnable {
@Override
public void run() {System.out.println("诗人:先喝点小酒。。。");
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
String line = "诗人:大海啊,都是水~~~";
System.out.println(line);
}
}
Thread poet = new Thread(new Poet());
poet.start();
System.out.println("观众:翘首以待。。。");
try {
// 以后线程进入 WAITING 状态,始终到 poet 线程执行完结。poet.join();} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("观众:这样的诗,我每天能写一箩筐。。。");
}
对于 LockSupport.park
机制,理论是对 wait/notify
的一种改善。wait/notify
有以下问题:
- wait 必须在 notify 之前执行,否则 wait 的线程无奈被唤醒;
- 执行 wait 与 notify 的线程必须占有锁,且是同一把锁。
LockSupport.park
机制则能够先执行LockSupport.unpark
,再执行LockSupport.park
;也不须要同步代码块。代码示例如下:
private void testLockSupport() {
class EchoPrinterAgain {public void echoPrint1() {Scanner scanner = new Scanner(System.in);
System.out.println("我是胡汉三,我要跑路了。。。");
// 暂停以后线程,进入 WAITING 状态
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {System.out.println("我胡汉三回来了。。。有人 interrupt。。。");
} else {System.out.println("我胡汉三回来了。。。有人 unpark。。。");
}
}
}
// 因为锁是 EchoPrinter 的一个实例,这里须要学生成实例
EchoPrinterAgain echoPrinterAgain = new EchoPrinterAgain();
// 创立线程 1 并启动,线程 1 将运行 echoPrint 办法
Thread t1 = new Thread(echoPrinterAgain::echoPrint1);
t1.start();
// 尝试在子线程 LockSupport.park 之前先 LockSupport.unpark
// LockSupport.unpark(t1);
// System.out.println("先执行了 LockSupport.unpark(t1)");
// 主线程期待 1 秒钟,以确保线程 1 启动
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(String.format("线程 %s 的状态 %s", "胡汉三", t1.getState().toString()));
// 唤醒 t1
LockSupport.unpark(t1);
// t1.interrupt();}
其实 LockSupport.park
也不是很好用,因为 park
和unpark/interrupt
仍要保障在不同线程中成对呈现。如果多了一个 park
,就会导致线程始终WAITING
上来了。事实上,咱们在理论开发中也简直不会本人间接应用LockSupport
。但很多间接供咱们应用的多线程开发相干类库,都在底层应用了LockSupport
,比方前面章节介绍的 JUC 锁ReentrantLock
,它在试图获取锁时,底层应用的就是LockSupport
。
另外留神,synchronized
以外的锁在获取锁失败时线程并不会进入 BLOCKED
状态,而是进入 WAITING
状态。
3.2.4 TIMED\_WAITING 状态
TIMED_WAITING
状态就是 WAITING
状态减少了一个闹钟,一旦工夫到了还没有其余线程唤醒本人,就本人醒来回到 RUNNABLE
状态。
按 JavaDoc 的定义就是:
带指定等待时间来期待的线程所处的状态。由以下形式能够进入 TIMED_WAITING
状态:
Thread.sleep
:后面的代码示例中曾经屡次应用。- 带时限(timeout)的
Object.wait
- 带时限(timeout)的
Thread.join
LockSupport.parkNanos
与LockSupport.parkUntil
除了 Thread.sleep
,其余都是WAITING
的对应办法的带时限版本。
留神,
Thread.sleep
的工夫参数传入 0 就是真的不 sleep;而其余 wait 之类的,timeout 传入为 0 的话,就是有限期待的意思。
3.3 线程中断 interrupt
Java 中的线程是不能被内部强行中断或进行的,只能由线程本人自行进行。但 Java 仍然提供了线程中断机制,在 Thread 类里提供了以下办法:
// 将该线程的中断标记位设为 true
public void interrupt()
// 获取以后线程中断标记位,并将其复原为 `false`
public boolean isInterrupted()
这两个办法中,Thread.interrupt
的作用是 告诉线程,你最好停下来
,然而线程能够齐全不予理睬。。。
具体来说,如果咱们调用一个线程的 interrupt
办法,那么:
- 如果指标线程正好处于
WAITING
或TIMED_WAITING
状态,即由sleep
,wait
,join
等等办法导致的期待状态中,那么指标线程会立刻退出期待状态,并抛出一个InterruptedException
异样。但作为 Java 程序员的你,应该不会因为这个异样就退出程序运行。通常你都会忽视它,尽管你必须在代码里解决这个受查看异样,但往往只是打一个日志就完了,前面该干嘛干嘛。。。 - 如果指标线程处于失常
RUNNABLE
状态,那么仅仅只是该线程的中断标记被设为 true 而已,如果失常业务逻辑中没有对线程中断标记做判断解决,那么就会忽视interrupt
。。。 - 如果指标线程处于
BLOCKED
状态,也不会响应interrupt
。BLOCKED
状态只是获取synchronized
锁失败时进入的阻塞状态,不是WAITING
或TIMED_WAITING
状态。所以作为 Java 程序员的你,从来不须要在应用synchronized
锁时还要解决烦人的受查看异样InterruptedException
。
那么,失常解决逻辑中,怎么解决 interrupt
呢?
- 在失常业务逻辑的某些适合的点,增加对
Thread.interrupted()
的判断,这个办法会获取以后线程中断标记位,并将其复原为false
。 - 在解决受查看异样时给个体面,该完结解决就不要流连忘返了,间接把异样往上抛或者间接 return 啥的。。。
给个例子:
Thread t1 = new Thread(new Runnable(){public void run(){
// 如果没人中断你,就失常执行业务逻辑
while(!Thread.currentThread.isInterrupted()){
// 该干嘛干嘛
doSomething();}
// 完结业务解决。。。killMe();}
} ).start();
要留神的是,Java 中的线程中断 interrupt
和操作系统 CPU 调度内核线程时的 interrupt
不是一个概念。操作系统的 interrupt
更靠近它的字面意思,中断线程,让 CPU 切换线程;而 Java 里对 JVM 层面线程的 interrupt
就是个告诉而已。
3.4 线程状态小结
Java 的线程状态和操作系统的线程状态不可一概而论,两者的视角是不一样的。但很多时候咱们会将二者混同着讲。比方讲线程因为磁盘 IO 而陷入阻塞,此时讲的是操作系统层面的线程状态而不是 Java 线程状态,这时候 Java 线程状态依然是RUNNABLE
;比方讲线程获取锁失败进入阻塞状态,此时特指 Java 层面的线程状态BLOACKED
。大家要可能依据上下文分辨到底说的哪个层面的线程状态。
四、线程相干属性
Java 线程除了线程状态,还有其余的一些属性,如线程名、是否守护线程、线程组、线程优先级等等。
线程名能够在创立线程的时候指定,但个别都不指定。
4.1 守护线程
守护线程的概念很简略,它不是线程状态,而是线程的一种属性。当一个线程被设置为守护线程时,只有主线程完结,该守护线程就会完结。
示例参考之前【3.2.2 BLOCKED 状态】的第一段代码,将其中的 t1.setDaemon(true);
和t2.setDaemon(true);
从正文中释放出来,就会发现,主线程打印出 t1 和 t2 的状态后就会间接完结 JVM,不会因为 t1 和 t2 尚在运行或阻塞中就也无奈完结。
4.2 线程组和线程优先级
Java 还为线程提供了线程组和优先级别。
ThreadGroup
类来治理线程组,开发这能够应用线程组对线程进行批量管制。每个 Thread 必然存在于一个 ThreadGroup 中,如果没有显式指定,比方间接 new Thread
时没有传入 ThreadGroup
,那么默认将创立子线程的以后父线程的线程组设置为子线程的线程组。Java 程序是从一个main
办法开始的,线程组就是main
。
线程的优先级范畴是1 ~ 10
,它是一个参考值,最终线程的优先级还是由操作系统决定的,所以设置更高的优先级只能减少该线程被优先执行的几率,这很玄学。。。默认状况下,线程的优先级都是 5。
通过以下代码确认:
public static void main(String[] args) {System.out.println("main 主线程的 ThreadGroup:");
System.out.println(Thread.currentThread().getThreadGroup().getName());
Thread t1 = new Thread();
System.out.println("main 主创立的子线程的 ThreadGroup:");
System.out.println(t1.getThreadGroup().getName());
System.out.println("main 主线程的优先级:");
System.out.println(Thread.currentThread().getPriority());
System.out.println("main 主创立的子线程的默认优先级:");
System.out.println(t1.getPriority());
t1.setPriority(10);
System.out.println("设置子线程的优先级:");
System.out.println(t1.getPriority());
Thread t2 = new Thread();
t2.setDaemon(true);
System.out.println("守护线程的默认优先级:");
System.out.println(t2.getPriority());
}
Java 提供线程组次要是为了对立控制线程的优先级以及查看线程权限等。作为开发根本不会用到。后续讲到线程池时,会再次遇到线程组和优先级。
五、多线程入门总结
Java 多线程开发入门次要须要把握以下知识点:
- 并发与并行的关系及区别。
- 什么是线程,以及,如何创立线程。
- JVM 线程状态与操作系统的线程状态的对应关系。
- JVM 线程状态相互间是如何转换的。
这些常识是后续学习多线程开发必须了解把握的。