本章是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()// 限时的getV 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类里提供了以下办法:
// 将该线程的中断标记位设为truepublic 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线程状态相互间是如何转换的。
这些常识是后续学习多线程开发必须了解把握的。