第 2 章和第 3 章主要是从概念上区了解 Thread,本章中,我们将细致的学习 Thread 所有 API 的作用以及用法。
4.1、线程 sleep
sleep 是一个静态方法,其有两个重载方法,其中一个需要传入毫秒数,另外一个既需 要毫秒数也需要纳秒数。
4.1.1、sleep 方法介绍
public static void sleep (long millis) throws InterruptedException
public static void sleep (long millis, int nanos) throws InterruptedException
sleep 方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间, 但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不 会放弃 monitor 锁的所有权(线程同步和锁的时候会重点介绍 monitor),下面我们来看 一个简单的例子。
package com.bjsxt.chapter04.demo01;
public class ThreadSleep {
public static void main(String[] args) {
// 子线程
new Thread(()->{
long startTime = System.currentTimeMillis();
sleep(2000);// Thread.sleep是使得当前线程休眠的时间,不是所有线程的休眠时间。
long endTime = System.currentTimeMillis();
System.out.printf("Total spent %d ms.\n",endTime - startTime);
}).start();
// main 线程
long startTime = System.currentTimeMillis();
sleep(3000);
long endTime = System.currentTimeMillis();
System.out.printf("main thread spent %d ms.",endTime - startTime);
}
private static void sleep(long ms) {
try{
Thread.sleep(ms);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的例子中,我们分别在自定义的线程和主线程中进行了休眠,每个线程的休眠互 不影响,从结果看,Thread.sleep 只会导致当前线程进入指定时间的休眠。
4.1.2、使用 TimeUniT 替代 Thread.sleep
在 JDK1.5 以后,JDK 引入了一个枚举 TimeUnit,其对 sleep 方法提供了很好的封装, 使用它可以省去时间单位的换算步骤,比如线程想休眠 3 小时 24 分 17 秒 88 毫秒,使用 TimeUnit 来实现就非常简便优雅了:
package com.bjsxt.chapter04.demo01;
import java.util.concurrent.TimeUnit;
public class ThreadTimeUnit {
public static void main(String[] args) {
try{
long startTime = System.currentTimeMillis();
//Thread.sleep(12257088L);
Thread.sleep(17088);
long endTime = System.currentTimeMillis();
System.out.printf("main thread slept spent %d ms \n",endTime - startTime);
startTime = System.currentTimeMillis();
//TimeUnit.HOURS.sleep(3);
//TimeUnit.MINUTES.sleep(24);
TimeUnit.SECONDS.sleep(17);
TimeUnit.MILLISECONDS.sleep(88);
endTime = System.currentTimeMillis();
System.out.printf("main thread slept spent %d ms \n",endTime - startTime);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同样的时间表达,TimeUnit 显然清晰很多,强烈建议在使用 Thread.sleep 的地方, 完全使用 TimeUnit 来代替,因为 sleep 能做的事情,TimeUnit 全部都能完成,并且可以 做的更好,后面内容中,我将全部采用 TimeUnit 替代 sleep。
4.1.3、Thread.sleep(0)
Thread.sleep(0) 表示挂起 0 毫秒,你可能觉得没作用。其实 Thread.sleep(0) 并 非是真的要线程挂起 0 毫秒,意义在于这次调用 Thread.sleep(0)的当前线程确实的被冻 结了一下,让其他线程有机会优先执行。Thread.sleep(0) 是你的线程暂时放弃 cpu,也 就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。
在线程中,调用 sleep(0)可以释放 cpu 时间,让线程马上重新回到就绪队列而非等 待队列,sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系 统切换其他线程来执行,提升效率。
4.2、线程 yield
4.2.1、yield 方法介绍
yield 方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的 CPU 资源,如 果 CPU 的资源不紧张,则会忽略这种提醒。
调用 yield 方法会使当前线程从 Running 状态切换到 Runnable 状态,一般这个方法 不太常用,接下来我们看一个案例:
package com.bjsxt.chapter04.demo02;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ThreadYield {
public static void main(String[] args) {
// IntStream.range(0, 2).mapToObj(ThreadYield::create).forEach(Thread::start);
Thread t1 = create(0);
Thread t2 = create(1);
t1.start();
t2.start();
}
private static Thread create(int index){
return new Thread(()->{
if(index == 0)
Thread.yield();
System.out.println(index);
});
}
}
上面的程序运行多次,你会发现输出结果不一致,有时候是 0 最先打印出来,有时候是 1 最先打印出来。根据代码运行结果分析如下:
第一个线程如果最先获得了 CPU 资源,它会比较谦虚,主动告诉 CPU 调度器试放原本 属于自己的资源,但是 yield 只是一个提示(hint),CPU 调度器并不会担保每次都能满 足 yield 提示。
4.2.2、yield 和 sleep
看过前面的内容之后,会发现 yield 和 sleep 有一些混淆的地方,在 JDK1.5 以前的
版本中 yield 的方法事实上是调用了 sleep(0),但是他们之间存在着本质的区别,具体如 下:
sleep 会导致当前线程暂停指定的时间,没有 CPU 时间片的消耗;
yield 只是对 CPU 调度器的一个提示,如果 CPU 调度器没有忽略这个提示,它会导致 线程上下文的切换;
sleep 会使线程短暂 block,会在给定的时间内试放 CPU 资源;
yield 会使 Running 状态的 Thread 进入 Runnable 状态(如果 CPU 调度器没有忽略 这个提示的话);
sleep 几乎百分之百的完成了给定时间的休眠,而 yield 的提示并不能一定保证。
4.3、设置线程优先级
public final void setPriority (int newPriority)
public final int getPriority()
4.3.1、线程优先级介绍
进程有进程的优先级,线程同样也有优先级,理论上是优先级比较高的线程会获取优先 被 CPU 调度的机会,但是事实上往往并不会如你所愿,设置线程的优先级同样也是一个 hint 操作,具体如下。
对于 root 用户,他会 hint 操作系统你想要设置的优先级别,否则它会被忽略。
如果 CPU 比较忙,设置优先级可能会获得更多的 CPU 时间片,但是闲时优先级的高低 几乎不会有任何作用。
所以,不要在程序设计当中企图使用线程优先级绑定某些特定的业务,或者让业务严重 依赖于线程优先级,这可能会让你大失所望。
举个简单的例子,可能不同情况下的与运行效果不会完全一样,但是我们只是想让优先 级比较高的线程获得更多的信息输出机会,代码如下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority01 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
int i = 0;
while(true){
System.out.printf("t1 run times : %d\n",i++);
}
});
t1.setPriority(3);
Thread t2 = new Thread(()->{
int i = 0;
while (true){
System.out.printf("t2 run times : %d\n",i++);
}
});
t2.setPriority(10);
t1.start();t2.start();
}
}
运行上面的程序,会发现 t2 出现的频率很明显要高一些,当然这也和笔者当前 CPU 的 资源情况有关系,不同情况下的运行会有不一样的结果。
4.3.2、线程优先级源码分析
设置线程的优先级,只需要调用 setPriority 方法即可,下面我们打开 Thread 源码, 一起来分析一下:
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
通过上面源码的分析,我们可以看出,线程的优先级不能小于 1 也不能大于 10,如果 指定的线程优先级大于线程所在 group 的优先级,那么指定的优先级将会失败,取而代之 的是 group 的最大优先级,下面我们通过一个例子来证明一下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority02 {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("test");
group.setMaxPriority(7);
Thread t1 = new Thread(group,()->{
});
t1.setPriority(10);
System.out.println(t1.getPriority());
}
}
上面的结果输出为 7,而不是 10。因为它超过了所在线程组的优先级。
4.3.3、关于优先级的一些总结
一般情况下,不会对线程设定优先级别,更不会让某些业务严重的依赖线程的优先级别, 比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使 用默认的优先级就好了,那么线程默认的优先级是多少呢?
线程默认的优先级和它的父类保持一致,一般情况下都是 5,因为 main 线程的优先级 就是 5,所以它派生出来的线程都是 5,代码如下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority03 {
public static void main(String[] args) {
// 元数据区域 堆 线程共享
// main 线程有 程序计数器 java虚拟机栈 本地方法栈
// main 线程的java虚拟机栈中存放一个线程对象 t1 的引用
// t1 存放在 堆
Thread t1 = new Thread("t1");
// main 线程 通过地址获取 t1 的优先级,输出
System.out.println("t1 thread priority is :"+t1.getPriority());
// main 线程的java虚拟机栈保存一个线程对象t2的引用
// t2存放在 堆
Thread t2 = new Thread(()->{
// t2 线程有 程序计数器 java虚拟机栈 本地方法栈
// t2 线程创建一个线程保存到堆里,t3引用变量保存线程的地址
Thread t3 = new Thread();
// t2 线程通过t3引用获取堆里对象的优先级,输出
System.out.println("t3 thread priority is : "+t3.getPriority());
});
// 通过地址设置t2的优先级为8
t2.setPriority(8);
// main 线程通过t2获取堆中对象线程的优先级
System.out.println("t2 thread priority is : "+t2.getPriority());
// main 线程通过t2访问堆中线程对象的start方法,调用vm本地方法栈中的start0方法
// t2线程对象 获取程序计数器 java虚拟机栈 本地方法栈
// cpu 执行
t2.start();
}
}
上面的程序的输出结果是 t1 的优先级为 5,因为 main 线程的优先级是 5;t2 的优先 级是8,因为显示的将其指定为 8;t3 的优先级为 8,没有显示值当,因此其父线程保持一 致
4.4、获取线程 ID
public long getId() 获取线程的唯一 ID,线程的 ID 在整个 JVM 进程中都会是 唯一的,并且是从 0 开始逐次递增。如果你在 main 线程(main 函数)中创建了一个唯一 的线程,并且调用 getId()后发现其并不等于 0,也许你会纳闷,不应该是从 0 开始的吗? 之前已经说过了在一个 JVM 进程启动的时候,实际上是开辟了很多个线程,自增序列已经 有了一定的消耗,因此我们自己创建的线程绝非第 0 号线程。
package com.bjsxt.chapter04.demo04;
import java.util.concurrent.TimeUnit;
public class ThreadId {
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
System.out.println(t1.getId());
while(true){
}
}
}
运行结果
jconsole查看其他线程,或者也可在代码中获取线程看一下,其他有那些已经创建了的线程~~
package com.bjsxt.chapter04.demo04;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class ThreadId {
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
System.out.println(t1.getId());
System.out.println(Thread.currentThread().getId());
Set<Map.Entry<Thread, StackTraceElement[]>> entries = Thread.getAllStackTraces().entrySet();
List<ThreadInfo> infos= new ArrayList<>();
for(Map.Entry<Thread, StackTraceElement[]> t:entries)
{
infos.add(new ThreadInfo(t.getKey().getName(),t.getKey().getId()));
}
List<ThreadInfo> list = infos.stream().sorted(Comparator.comparing(ThreadInfo::getId)).collect(Collectors.toList());
list.forEach(System.out::println);
while(true){
}
}
}
class ThreadInfo{
String name;
long id;
public long getId() {
return id;
}
ThreadInfo(String name, long id){
this.id=id;
this.name=name;
}
@Override
public String toString() {
return this.name+"-"+this.id;
}
}
运行结果
看见在自己创建的线程里id是14,main线程是1,垃圾回收线程是3.
4.5、获取当前线程
public static Thread currentThread() 用于返回当前执行线程的引用,这个 方法虽然很简单,但是使用非常广泛,我们在后面的内容中会大量的使用该方法,来看一段 示例代码
package com.bjsxt.chapter04.demo05;
import java.util.concurrent.TimeUnit;
public class ThreadCurrentThread {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread() == this);
}
};
t1.start();
// 保证启动
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName().equals("main"));
}
}
4.6、线程 interrupt
线程 interrupt,是一个非常重要的 API,也是经常使用的方法,与线程中断相关, 相关的 API 有以下几个,在本节中我们也将 Thread 深入源码对其进行详细的剖析。
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
4.6.1、interrupt
以下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt 方法, 就可以打断阻塞。
Object 的 wait 方法;
Object 的 wait(long)方法;
Object 的 wait(lOng, int)方法;
Object 的 sleep(long)方法;
Thread 的 sleep(long)方法;
Thread 的 join 方法;
Thread 的 join(long)方法;
Thread 的 join(long, int)方法;
InterruptIbleChannel 的 io 操作;
Selector 的 wakeup 方法
上述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的 interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打 断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterruptedException 的异常, 这个异常就像一个 signal(信号)一样通知当前线程被打断了,下面我们来看一个例子:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
try{
// 先执行
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
System.out.println("阻塞状态被中断");
e.printStackTrace();
}
});
t1.start();
// 保证t1线程运行
TimeUnit.MILLISECONDS.sleep(100);
// 后执行
t1.interrupt();
}
}
上面的代码创建了一个线程,并且企图休眠 1 分钟的时长,不过很可惜,大约在 100 毫秒之后就被主线调用 interrupt 方法打断,程序的执行结果就是”阻塞状态被中断“。
interrupt 这个方 法到底做了什 么样的事情呢 ?在一个线 程内部存在着 名为 interrupt flag 的标识,如果一个线程被 interrupt,那么它的 flag 将被设置,但是 如果当前线程正在执行可中断方法被阻塞时,调用 interrupt 方法将其中断,反而会导致 flag 被清除,关于这点我们在后面还会做详细的介绍。另外有一点需要注意的是,如果一 个线程已经是死亡状态,那么尝试对其的 interrupt 会直接被忽略。
4.6.2、isInterrupted
isInterrupted 是 Thread 的一个成员方法,它主要判断当前线程是否被中断,该方 法仅仅是对 interrupt 标识的一个判断,并不会影响标识发生任何改变,这个与我们即将 学习到的 interrupted 是存在差别的,下面我们看一个简单的程序:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt02 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
}
});
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(t1.isInterrupted());// false
t1.interrupt();
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
}
}
上面的代码中定义了一个线程,并且在线程的执行单元中(run 方法)写了一个空的死 循环,为什么不写 sleep 呢?因为 sleep 是可中断方法,会捕获到中断信号,从而干扰我 们程序的结果。下面是程序运行的结果,记得手动结束上面的程序运行,或者你也可以将上 面定义的线程指定为守护线程,这样就会随着主线程的结束导致 JVM 中没有非守护线程而 自动退出。运行结果如下:
可中断方法捕获到了中断信号(signal)之后,也就是捕获了 InterruptedException 异常之后会擦除掉 interrupt 的标识,对上面的程序稍作修改,你会发现程序的结果又会 出现很大的不同,示例代码如下:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt03 {
// ??
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
System.out.printf("I am be interrupted? %s\n", this.isInterrupted());// false
e.printStackTrace();
}
}
}
};
t1.setDaemon(true);
t1.start();
// 短暂的阻塞是为了保证 t1 线程已启动
TimeUnit.MILLISECONDS.sleep(100);
System.out.printf("Thread is interrupted? %s\n", t1.isInterrupted());// false
// 中断 t1 线程的阻塞状态
t1.interrupt();
TimeUnit.MILLISECONDS.sleep(100);
/*If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int)
methods of the Objecta class, or of the join(), join(long), join(long, int), sleep(long),
or sleep(long, int), methods of this class,
then its interrupt status will be cleared and it will receive an InterruptedException.*/
System.out.printf("Thread is interrupted? %s\n", t1.isInterrupted());// false
}
}
其实这也不难理解,可中断方法捕获到了中断信号之后,为了不影响线程中其他方法的 执行,将线程的 interrupt 标识复位是一种很合理的设计。如果线程处于阻塞状态,去打终端标记,会报异常,清空终端标记。所以将会看见false。
运行结果
4.6.3、interrupted
interrupted 是一个静态方法,虽然其也用于判断当前线程是否被中断,但是它和成 员方法 isInterrupted 还是又很大的区别,调用该方法会直接擦除掉线程的 interrupt 标识,需要注意的是,如果当前线程被打断了,那么第一次调用 interrupted 方法会返回 true,并且立即擦除了 interrupt 标识;第二次包括以后的调用永远都会返回 false,除 非在此期间线程又一次被打算,下面设计了一个简单的例子,来验证我们的说法:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt04 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(){
@Override
public void run() {
while(true){
System.out.println(Thread.interrupted());
}
}
};
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
t1.interrupt();
}
}
在很多的 false 包围中发现了一个 true,也就是说 interrupted 方法判断到了其被 中断,立即擦除了中断标识,并且只有这一次返回 true,后面的都将会是 false。
4.6.4、interrupt 注意事项
打开 Thread 的源码,不难发现, interrupted 方法和 interrupted 方法都调用了 同一个本地方法:
而 interrupted 静态方法中该参数则为 true,表示想要擦除:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
isInterrupted 方法的源码中该参数为 false,表示不想擦除:
public boolean isInterrupted() {
return isInterrupted(false);
}
其中参数 ClearInterrupted 主要用来控制是否擦除线程 interrupt 的标识。
private native boolean isInterrupted(boolean ClearInterrupted);
在比较详细的学习了 interrupt 方法之后,大家思考一个问题,如果一个线程在没有 执行可中断方法之前就被打断,那么其接下来将执行可中断方法,比如 sleep 会发生什么 样的情况呢?下面我们通过一个简单的实验来回答这个疑问:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt05 {
public static void main(String[] args) throws InterruptedException{
System.out.println(Thread.interrupted());// false
Thread.currentThread().interrupt();// 打标机
System.out.println(Thread.currentThread().isInterrupted());// true
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
System.out.println("I will be interrupted.\n"+"interrupt flag is "+Thread.currentThread().isInterrupted());
e.printStackTrace();
}
System.out.println(Thread.currentThread().isInterrupted());// ? false
}
}
// 线程处于blocked 使用 interrupt标记线程状态,获取到的是false
// 线程使用interrupt 标记,再调用sleep,join,wait使他blocked,获取到的是false
// status will be cleard 回到false
// 如果是false 那么调用静态方法也不会改变值
// 如果是true 那么调用静态方法会改变值
通过运行上面的程序,你会发现,如果一个线程设置了 interrupt 标识,那么接下来 的可中断方法会立即中断,因此最后一步的捕获中断信号部分代码会被执行。(先中断标记 再阻塞 终端标记会被擦除 会看见false)
4.7、线程 join
Thread 的 join 方法同样是一个非常重要的方法,使用它的特性可以实现很多比较强 大的功能,Thread 的 API 为我们提供了三个不同的 join 方法,具体如下。
public final void join() throws InterruptedException
public final void join (long millis) throws InterruptedException
public final void join (long millis, int nanos) throws InterruptedException
在本节中,将会详细介绍 join 方法以及如何在实际应用中使用 join方法。
4.7.1、线程 join 方法详解
join 某个线程 A,会使当前线程 B 进入等待,直到线程 A 结束生命周期,或者到达给 定的时间,那么在此期间 B 线程是处于 Blocked 的,而不是 A 线程,下面就来通过一个简 单的实例解释一下 join 方法的基本用法:
package com.bjsxt.chapter04.demo07;
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
for(int i = 0;i<10;i++){
System.out.println("t1#"+i);
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i<10;i++){
System.out.println("t2#"+i);
}
});
// 先启动线程
t1.start();
t2.start();
// 在调用join
t1.join();
t2.join();
// 两个线程执行完毕后 会执行main线程
for(int i = 0;i<10;i++){
System.out.println("main#"+i);
}
Thread.currentThread().join();// 调用自己会一直阻塞
}
}
上面的代码创建了两个线程,分别启动,并且调用了每个线程的 join 方法(注意:join 方法是被主线程调用的,因此在第一个线程还没结束生命周期的时 候,第二个线程的 join 不会得到执行,但是此时,第二个线程也已经启动了),运行上面 的程序,你会发现线程一和线程二会交替的输出直到他们结束生命周期,main 线程的循环 才会开始运行,程序输出如下:
如果你将第三步下面的 join 全部注释掉,那么三个线程将会交替的输出,程序输出如 下:
结合java8语法
package com.bjsxt.chapter04.demo07;
public class ThreadJoin02 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()-> printNum());
Thread t2 = new Thread(()-> printNum());
// 先启动线程
t1.start();
t2.start();
// 在调用join
t1.join();
t2.join();
// 两个线程执行完毕后 会执行main线程
printNum();
Thread.currentThread().join();// 调用自己会一直阻塞
}
private static void printNum(){
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" # "+i);
}
}
}
join 方法会使当前线程永远的等待下去,直到期间被另外的线程中断,或者 join 的 线程执行结束,当然你也可以使用 join 的另外两个重载方法,指定毫秒数,在指定的时间 到达之后,当前线程也会退出阻塞。 同样思考一个问题,如果一个线程已经结束了生命周期,那么调用它的 join 方法的当 前线程会被阻塞吗?
会阻塞。上面的代码一直在等待中,红色按钮一直有。
4.7.2、join 方法结合实战
本节我们将结合一个实际的案例,来看一下 join 方法的应用场景,假设你有一个 APP, 主要用于查询航班信息,你的 APP 是没有这些实时数据的,当用户发起查询请求时,你需 要到各大航空公司的接口获取信息,最后统一整理加工返回到 APP 客户端,如图所示,当 然 JDK 自带了很多高级工具,比如 CountDownLatch 和 CyclicBarrier 等都可以完成类 似的功能,但是仅就我们目前所学的知识,使用 join 方法即可完成下面的功能。
该例子是典型的串行任务局部并行化处理,用户在 APP 客户端输入出发地“北京”和目的地“上海”,服务器接收到这个请求之后,先来验证用户的信息,然后到各大航空公司的 接口查询信息,最后经过整理加工返回给客户端,每一个航空公司的接口不会都一样,获取 的数据格式也不一样,査询的速度也存在着差异,如果再跟航空公司进行串行化交互(逐个 地查询),很明显客户端需要等待很长的时间,这样的话,用户体验就会非常差。如果我们 将每一个航空公司的査询都交给一个线程去工作,然后在它们结東工作之后统一对数据进行 整理,这样就可以极大地节约时间,从而提高用户体验效果。
package com.bjsxt.chapter04.demo08;
import java.util.List;
/**
* 航班查询
*/
public interface FlightQuery {
// run 方法返回值是 void,定义一个接口,获取堆里的数据
List<String> get();// 航空公司名称-查询时长
}
// 写一个 main
// new 任意个航空公司的 线程对象
// 启动
// 调用 join
// 将所有结果聚合
// 输出一下
// 线程对象执行的内容
//
以上代码中,FlightQuery 提供了一个返回方法,学到这里大家应该注意到了,不管 是 Thread 的 run 方法,还是 Runnable 接口,都是 void 返回类型,如果你想通过某个线 程的运行得到结果,就需要自己定义一个返回接口。 査询 Flight 的 task,其实就是一个线程的子类,主要用于到各大航空公司获取数据, 结合老师滴示例代码,我写的代码如下
package com.bjsxt.chapter04.demo08;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class FlightQueryTask extends Thread implements FlightQuery {
private String origin ;
private String destination;
// 航班信息数据 不能加static!!!否则结果出错,因为加了他们取的村的是同一地址的数据
private final List<String> flightList = new ArrayList<>();
FlightQueryTask(String airline,String origin,String destination){
super("[" + airline + "]");
this.origin = origin;
this.destination = destination;
}
@Override
public void run() {
System.out.printf("query %s from %s to %s\n",this.getName(),origin,destination);
int randomVal = ThreadLocalRandom.current().nextInt(10);
try{
TimeUnit.SECONDS.sleep(randomVal);
}catch (Exception e){
e.printStackTrace();
}
flightList.add(this.getName()+"-"+randomVal);
System.out.printf("The flight :%s list query successfully\n",this.getName());
}
// 返回航班数据
@Override
public List<String> get() {
return this.flightList;
}
}
接口定义好了,查询航班数据的线程也有了,下面就来实现一下从 SH(上海)到 BJ(北 京)的航班查询吧。结合老师的示例,我自己写的代码如下:
package com.bjsxt.chapter04.demo08;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FlightQueryExample {
// 与 app 合作的航空公司
private static List<String> airlineList = Arrays.asList("东方航空","南方航空","海南航空");
public static void main(String[] args) {
List<String> result = search("BJ","SH");
System.out.println("---------query result----------");
result.forEach(System.out::println);
}
private static List<String> search(String origin,String destination){
List<String> result = new ArrayList<>();
// 对每个航空公司查询一次
List<FlightQueryTask> tasks = airlineList.stream()
.map(f -> new FlightQueryTask(f,origin,destination))
.collect(Collectors.toList());
// 启动线程
tasks.forEach(Thread::start);
// 然后调用 join
for (FlightQueryTask task : tasks) {
try{
task.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
// 获取结果
for (FlightQueryTask task : tasks) {
result.addAll(task.get());
}
return result;
}
}
上面的代码,关键的地方已通过注释解释得非常清楚,主线程收到了 search 请求之后, 交给了若干个査询线程分别进行工作,最后将每一个线程获取的航班数据进行统一的汇总。 由于每个航空公司的查询时间可能不一样,所以用了一个随机值来反应不同的查询速度,返 回给客户端(打印到控制台),程序的执行结果输出如下:
4.8、如何关闭一个线程
JDK 有一个 Deprecated 方法 stop,但是该方法存在一个问题,JDK 官方早已经不推 荐使用,其在后面的版本中有可能会被移除,根据官网的描述,该方法在关闭线程时可能不 会释放掉 monitor 的锁,所以强烈建议不要使用该方法结束线程,本节将主要介绍几种关 闭线程的方法。
4.8.1、正常关闭
A. 线程结束生命周期正常结束
线程运行结東,完成了自己的使命之后,就会正常退出,如果线程中的任务耗时比较短, 或者时间可控,那么放任它正常结束就好了。
B. 捕获中断信号关闭线程
我们通过 new Thread 的方式创建线程,这种方式看似很简单,其实它的派生成本是比 较高的,因此在一个线程中往往会循环地执行某个任务,比如心跳检査,不断地接收网络消 息报文等,系统决定退出的时候,可以借助中断线程的方式使其退出,示例代码如下
package com.bjsxt.chapter04.demo09;
import java.util.concurrent.TimeUnit;
public class InterruptThreadExit {
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread("t"){
@Override
public void run() {
System.out.println("I will start work.");
while(!this.isInterrupted()){
// work
}
System.out.println("I will be exiting.");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("Main will be exiting.");
t.interrupt();
}
}
上面的代码是通过检査线程 interrupt 的标识来决定是否退出的,如果在线程中执行 某个可中断方法,则可以通过捕获中断信号来决定是否退出。
C. 使用 volatile 开关控制
由于线程的 interrupt 标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方 法,所以使用 volatile 修饰的开关 flag 关闭线程也是一种常用的做法,具体如下:
package com.bjsxt.chapter04.demo09;
import java.util.concurrent.TimeUnit;
public class InterruptThreadExit {
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread("t"){
@Override
public void run() {
System.out.println("I will start work.");
while(!this.isInterrupted()){
// work
}
System.out.println("I will be exiting.");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("Main will be exiting.");
t.interrupt();
}
}
上面的例子中定义了一个 closed 开关变量,并且是使用 volatile 修饰(关于 volatile 关键字会在后面进行非常细致地讲解, *volatile 关键字在 Java 中是一个革命 性的关键字,非常重要,它是 Java 原子变量以及并发包的基础),*运行上面的程序同样也 可以关闭线程。
4.8.2、异常退出
在一个线程的执行单元中,是不允许抛出 checked 异常的,不论 Thread 中的 run 方 法,还是 Runnable 中的 run 方法,如果线程在运行过程中需要捕获 checked 异常并且判 断是否还有运行下去的必要,那么此时可以将 checked 异常封装成 unchecked 异常 (RuntimeException)抛出进而结束线程的生命周期
4.8.3、进程假死
相信很多程序员都会遇到进程假死的情况,所谓假死就是进程虽然存在,但没有日志输 出,程序不进行任何的作业,看起来就像死了一样,但事实上它是没有死的,程序之所以出 现这样的情况,绝大部分的原因就是某个线程阻塞了,或者线程出现了死锁的情况。
我们需要借助一些工具来帮助诊断,比如 jstack、 jconsole、 jvisualvm 等工具, 在本节中,这一节简单介绍一下 jvisualvm 这个可视化工具,后面我们还会接触这些工具进行死锁的判断等操作。 IntelliJ IDEA 其实也是一个 Java 进程,打开 jvisualvm,选择 IntelliJ IDEA 进程,如下图所示,将右侧的 Tab 切换到【线程】。
如果进程无法退出,则会出现假死的情况,可以打开 jvisualvm 查看有哪些活跃线程 它们的状态是什么,该线程在调用哪个方法而进入了阻塞。
4.9、总结
在本章中,我们比较详细地学习了 Thread 的大多数 API,其中有获取线程信息的方法, 如 getId() 、 getName() 、 getPriority() 、 currentThread(), 也 有 阻 塞 方 法 sleep()、 join()方法等,并且结合若干个实战例子帮助大家更好地理解相关的 API, Thread 的 API 是掌握高并发编程的基础,因此非常有必要熟练掌握。