写点什么

多线程源码明白了吗?不明白的话来看腾讯大牛给你画的面试重点

用户头像
小Q
关注
发布于: 2020 年 11 月 28 日

叮铃铃~


谁啊,大早上的扰人清梦,不知道好不容易有个周末吗?接电话知道是朋友约我出去钓鱼,顺便跟我聊一下前段时间让他帮忙内推一下我小侄子去实习的事情


见面之后,他直接开门见山,小侄子在面试的时候表现不错,最后一面是他来面的,问的相当深入,侄子表现也不错,但是在多线程这个地方,他稍微问的深入了一些,小朋友明显的慌张起来,很多知识点回答的相当不好(我说这小子怎么面试回来之后都不敢找我问问题了),朋友说问题不大,这里他不会卡他,过几天应该就能收到 offer 了


虽然朋友这样说,但是心里却记下了,多线程这一块该给这小子补习一下了


个人公众号:Java 架构师联盟,每日更新技术好文呢



其实多线程应该是现在很多朋友的难点吧,没什么时间看源码,工作的时候又很少能应用得到,但是在面试的时候,这一块又是面试的重点,那应该怎么办呢?其实不需要慌张,因为面试也算是考试,考试就会划重点,我总共总结出以下几点,大家可以先看一下能详细的回想起来多少


线程 sleep

线程 yield

设置线程的优先级获取线程 ID

获取当前线程

设置线程上下文类加载器

线程 interrupt

线程 join

如何关闭—个线程


如果你能回答出来 80%以上,那基本就没什么问题了,不然,你可能要仔细的往下看,有的朋友会说我还年轻,不需要这个,那我要告诉你,这些知识点确实可能不会阻挡你拿 offer,但是,会影响你后期的发展速度以及在公司享受的资源,不信就去问你老大


好了,话不多说,我们来看一下从源码角度,这些问题你都该回答那些东西


线程 sleep


sleep 是一个静态方法,其有两个重载方法,其中一个需要传入毫秒数,另外一个既需 要毫秒数也需要纳秒数。


sleep 方法介绍


 public static void sleep(long millis) throws InterruptedException
复制代码


 public static void sleep(long millis, int nanos) throws InterruptedException
复制代码

sleep 方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的 时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性, 那就是其不会放弃 monitor 锁的所有权(在后文中讲解线程同步和锁的时候会重点介绍 monitor),下面我们来看一个简单的例子:


package com.wangwenjun,concurrent,chapter03;
复制代码


public class Threadsleep
复制代码


public static void main(String[] args)
复制代码


new Thread(()->
复制代码


复制代码


复制代码


long startTime = System.currentTimeMillis();
复制代码


sleep(2_000L);
复制代码


long endTime = System.currentTimeMillis();
复制代码


System.out.printin(String.format("Total spend %d ms", (endTime - startTime)));
复制代码


}).start();
复制代码


long startTime = System.currentTimeMillis();
复制代码


sleep(3_000L);
复制代码


long endTime = System.currentTimeMillis();
复制代码


System.out.printin(String.format("Main thread total spend %d ms", (endTime - startTime)));
复制代码


}
复制代码


private static void sleep(long ms)
复制代码


{
复制代码


try
复制代码


{
复制代码


Thread.sleep(ms);
复制代码


} catch (InterruptedException e)
复制代码


{
复制代码


}
复制代码


}
复制代码


}
复制代码

在上面的例子中,我们分别在自定义的线程和主线程中进行了休眠,每个线程的休眠 互不影响,Thread.sleep 只会导致当前线程进入指定时间的休眠。


使用 TimeUnit 替代 Thread.sleep


在 JDK1.5 以后,JDK 引入了一个枚举 TimeUnit,其对 sleep 方法提供了很好的封装, 使用它可以省去时间单位的换算步骤,比如线程想休眠 3 小时 24 分 17 秒 88 毫秒,使用 TimeUnit 来实现就非常的简便优雅了 :


Thread.sleep(12257088L);
复制代码


TimeUnit.HOURS.sleep(3);
复制代码


TimeUnit・ MINUTES.sleep(24);
复制代码


TimeUnit, SECONDS・ sleep(17);
复制代码


TimeUnit.MILLISECONDS.sleep(88);
复制代码

同样的时间表达,TimeUnit 显然清晰很多,笔者强烈建议,在使用 Thread.sleep 的地 方,完全使用 TimeUnit 来代替,因为 sleep 能做的事,TimeUnit 全部都能完成,并且功能 更加的强大,在本书后面的内容中,我将全部采用 TimeUnit 替代 sleep。


线程 yield


yield 方法介绍


yield 方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的 CPU 资源,如果 CPU 的资源不紧张,则会忽略这种提醒。


调用 yield 方法会使当前线程从 RUNNING 状态切换到 RUNNABLE 状态,一般这个方 法不太常用:


package com.wangwenjun.concurrent,chapter03;
复制代码


import java.util.stream.IntStream;
复制代码


public class ThreadYield
复制代码


{
复制代码


public static void main(String[] args)
复制代码


{
复制代码


IntStream.range(0, 2).mapToObj(ThreadYield::create)
复制代码


.forEach(Thread::start);
复制代码


}
复制代码


private static Thread create(int index)
复制代码


{
复制代码


return new Thread(() ->
复制代码


{
复制代码


//①注释部分
复制代码


//if (index == 0)
复制代码


// Thread, yield();
复制代码


System.out.printin(index);
复制代码


});
复制代码


}
复制代码


}
复制代码

上面的程序运行很多次,你会发现输出的结果不一致,有时候是 0 最先打印出来,有


时候是 1 最先打印出来,但是当你打开代码的注释部分,你会发现,顺序始终是 0, 10 因为第一个线程如果最先获得了 CPU 资源,它会比较谦虚,主动告诉 CPU 调度器是 放了原本属于自己的资源,但是 yield R 是一个提示(hint), CPU 调度器并不会担保每次都 能满足 yield 提示。


yield sleep


看过前面的内容之后,会发现 yield 和 sleep 有一些混淆的地方,在 JDK1.5 以前的版本 中 yield 的方法事实上是调用了 sleep(O),但是它们之间存在着本质的区别,具体如下。


□ sleep 会导致当前线程暂停指定的时间,没有 CPU 时间片的消耗。


□ yield 只是对 CPU 调度器的一个提示,如果 CPU 调度器没有忽略这个提示,它会导


致线程上下文的切换。


□ sleep 会使线程短暂 block,会在给定的时间内释放 CPU 资源。


□ yield 会使 RUNNING 状态的 Thread 进入 RUNNABLE 状态(如果 CPU 调度器没有 忽略这个提示的话)。


□ sleep 几乎百分之百地完成了给定时间的休眠,而 yield 的提示并不能一定担保。


□ 一个线程 sleep 另一个线程调用 interrupt 会捕获到中断信号,而 yield 则不会。


设置线程的优先级


□ public final void setPriority(int newPriority)为线程设定优先级。


□ public final int getPriority()获取线程的优先级。


线程优先级介绍


进程有进程的优先级,线程同样也有优先级,理论上是优先级比较高的线程会获取优 先被 CPU 调度的机会,但是事实上往往并不会如你所愿,设置线程的优先级同样也是一个 hint 操作,具体如下。


□对于 root 用户,它会 hint 操作系统你想要设置的优先级别,否则它会被忽略。


□如果 CPU 比较忙,设置优先级可能会获得更多的 CPU 时间片,但是闲时优先级的 高低几乎不会有任何作用。


所以,不要在程序设计当中企图使用线程优先级绑定某些特定的业务,或者让业务严 重依赖于线程优先级,这可能会让你大失所望。举个简单的例子,可能不同情况下的运行 效果不会完全一样,但是我们只是想让优先级比较高的线程获得更多的信息输出机会,示 例代码如下:


package com.wangwenjun.concurrent.chapter03;
复制代码


public class Threadpriority
复制代码


{
复制代码


public static void main(String[] args)
复制代码


{
复制代码


Thread tl = new Thread(()->
复制代码


{
复制代码


while (true)
复制代码


{
复制代码


System.out.printin("tl");
复制代码


}
复制代码


});
复制代码


tl・ setPriority(3);
复制代码


复制代码


复制代码


Thread t2 = new Thread(()->
复制代码


{
复制代码


while (true)
复制代码


{
复制代码


System. out. printin (,lt2");
复制代码


}
复制代码


});
复制代码


t2.setPriority(10);
复制代码


tl.start();
复制代码


t2.start();
复制代码


}
复制代码


}
复制代码

运行上面的程序,会发现 t2 出现的频率很明显要高一些,当然这也和笔者当前 CPU 的 资源情况有关系,不同情况下的运行会有不一样的结果。


线程优先级源码分析


设置线程的优先级,只需要调用 setPriority 方法即可,下面我们打开 Thread 的源码, 一起来分析一下:


public final void setPriority(int newPriority) {
复制代码


ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException();
复制代码


}
复制代码


if((g = getThreadGroup()) 1= null) {
复制代码


if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority();
复制代码


}
复制代码


setPriorityO(priority = newPriority);
复制代码


}
复制代码


}
复制代码

通过上面源码的分析,我们可以看出,线程的优先级不能小于 1 也不能大于 10,如果 指定的线程优先级大于线程所在 group 的优先级,那么指定的优先级将会失效,取而代之 的是 group 的最大优先级,下面我们通过一个例子来证明一下:


package com.wangwenjun.concurrent.chapter03;
复制代码


public class Threadpriority
复制代码


{
复制代码


public static void main(String[] args)
复制代码


{
复制代码


//定义一个线程组
复制代码


ThreadGroup group = new ThreadGroup("test");
复制代码


//将线程组的优先级指定为7
复制代码


group.setMaxPriority(7);
复制代码


//定义一个线程,将该线程加入到group中
复制代码


Thread thread = new Thread(group, "test-thread");
复制代码


//企图将线程的优先级设定为10
复制代码


thread.setPriority(10);
复制代码


/ /企图未遂
复制代码


System.out.printin(thread.getPriority());
复制代码


}
复制代码


}
复制代码

上面的结果输出为 7,而不是 10,因为它超过了所在线程组的优先级别


关于优先级的一些总结


一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级 别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的 时候使用默认的优先级就好了,那么线程默认的优先级是多少呢?


线程默认的优先级和它的父类保持一致,一般情况下都是 5,因为 main 线程的优先级 就是 5,所以它派生出来的线程都是 5,示例代码如下:


package com.wangwenjun.concurrent.chapter03;
复制代码


public class Threadpriority
复制代码


{
复制代码


public static void main(String[] args)
复制代码


{
复制代码


Thread tl = new Thread();
复制代码


System.out,println("tl priority " + tl.getPriority());
复制代码


Thread t2 = new Thread(()->
复制代码


{
复制代码


Thread t3 = new Thread();
复制代码


System.out.printIn("t3 priority " + t3.getPriority());
复制代码


});
复制代码


t2 ・ setPriority(6);
复制代码


t2 ・ start();
复制代码


System.out.printin("t2 priority " + t2.getPriority());
复制代码

上面程序的输出结果是 tl 的优先级为 5,因为 main 线程的优先级是 5 ; t2 的优先级 是 6,因为显式地将其指定为 6; t3 的优先级为 6,没有显式地指定,因此其与父线程保持 一致。


获取线程 ID


public long getld()获取线程的唯一 ID,线程的 ID 在整个 JVM 进程中都会是唯一的,


并且是从 0 开始逐次递增。如果你在 main 线程(main 函数)中创建了一个唯一的线程,并 且调用 getld()后发现其并不等于 0,也许你会纳闷,不应该是从 0 开始的吗?之前已经说 过了在一个 JVM 进程启动的时候,实际上是开辟了很多个线程,自增序列已经有了一定的 消耗,因此我们自己创建的线程绝非第 0 号线程。


获取当前线程


public static Thread currentThread()用于返回当前执行线程的引用,这个方法虽然很简 单,但是使用非常广泛,我们在后面的内容中会大量的使用该方法,来看一段示例代码:


 package com.wangwenjun.concurrent.chapter03;
复制代码


public class CurrentThread
复制代码


{
复制代码


public static void main(String[] args)
复制代码


{
复制代码


Thread thread = new Thread()
复制代码


{
复制代码


@Override
复制代码


public void run()
复制代码


{
复制代码


//always true System.out.printin(Thread.CurrentThread() == this);
复制代码


}
复制代码


};
复制代码


thread, start();
复制代码


String name = Thread.CurrentThread().getName(); System.out.printIn("main".equals(name));
复制代码


}
复制代码


}
复制代码

上面程序运行输出的两个结果都是 true。


设置线程上下文类加载器


□ public ClassLoader getContextClassLoader()获取线程上下文的类加载器,简单来说 就是这个线程是由哪个类加器加载的,如果是在没有修改线程上下文类加载器的情 况下,则保持与父线程同样的类加载器。


□ public void setContextClassLoader(ClassLoader cl)设置该线程的类加载器,这个方法 可以打破 JAVA 类加载器的父委托机制,有时候该方法也被称为 JAVA 类加载器的 后门。


关于线程上下文类加载器的内容我们将在本书的第 11 章重点介绍,并且结合 jdbc 驱动 包的源码分析 JDK 的开发者为什么要留有这样的后门。


线程 interrupt


线程 interrupt,是一个非常重要的 API,也是经常使用的方法,与线程中断相关的 API 有如下几个,在本节中我们也将 Thread 深入源码对其进行详细的剖析。


□ public void interrupt()
复制代码


□ public static boolean interrupted()
复制代码


□ public boolean islnterrupted()
复制代码

interrupt


如下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt 方法,就 可以打断阻塞。


□ Object 的 wait 方法。
复制代码


□ Object 的 wait(long)方法。
复制代码


□ Object 的 wait(long,int)方法。
复制代码


□ Thread 的 sleep(long)方法。
复制代码


□ Thread 的 sleep(long,int)方法。
复制代码


□ Thread 的 join 方法。
复制代码


□ Thread 的 join(long)方法。
复制代码


□ Thread 的 join(long,int)方法。
复制代码


□ InterruptibleChannel 的 io 操作。
复制代码


□ Selector 的 wakeup 方法。
复制代码

□其他方法。


上述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的 interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,.记住,打断一 个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。


一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterruptedException 的异常,这 个异常就像一个 signal (信号)一样通知当前线程被打断了,下面我们来看一个例子:


package com.wangwenjun,concurrent,chapter03;
复制代码


import java.util.concurrent .TimeUnit;
复制代码


public class Threadinterrupt
复制代码


public static void main(String[] args) throws InterruptedException
复制代码


{
复制代码


Thread thread = new Thread(()->
复制代码


{
复制代码


try
复制代码


{
复制代码


TimeUnit•MINUTES.sleep(1);
复制代码


} catch (InterruptedException e)
复制代码


{
复制代码


System.out.printIn("Oh, i am be interrupted.");
复制代码


} 、
复制代码


});
复制代码


thread.start();
复制代码


//short block and make sure thread is started.
复制代码


TimeUnit, MILLISECONDS.sleep(2);
复制代码


thread, interrupt();
复制代码


}
复制代码


}
复制代码

上面的代码创建了一个线程,并且企图休眠 1 分钟的时长,不过很可惜,大约在 2 毫 秒之后就被主线程调用 interrupt 方法打断,程序的执行结果就是“Oh, i am be interrupted.”


interrupt 这个方法到底做了什么样的事情呢?在一个线程内部存在着名为 interrupt flag 的标识,如果一个线程被 interrupt,那么它的 flag 将被设置,但是如果当前线程正在执行 可中断方法被阻塞时,调用 interrupt 方法将其中断,反而会导致 flag 被清除,关于这点我 们在后面还会做详细的介绍。另外有一点需要注意的是,如果一个线程已经是死亡状态, 那么尝试对其的 interrupt 会直接被忽略。


islnterrupted


islnterrupted 是 Thread 的一个成员方法,它主要判断当前线程是否被中断,该方法仅 仅是对 interrupt 标识的一个判断,并不会影响标识发生任何改变,这个与我们即将学习到 的 interrupted 是存在差别的,下面我们看一个简单的程序:


package com.wangwenjun,concurrent•chapter03;
复制代码


import java.util.concurrent.TimeUnit;
复制代码


public class ThreadisInterrupted
复制代码


{
复制代码


public static void main(String[] args) throws InterruptedException
复制代码


Thread thread = new Thread()
复制代码


{
复制代码


@Override
复制代码


public void run()
复制代码


{
复制代码


while (true)
复制代码


{
复制代码


//do nothing, just empty loop.
复制代码


}
复制代码


}
复制代码


};
复制代码


thread.start();
复制代码


TimeUnit.MILLISEC0NDS.sleep(2);
复制代码


System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted()); thread.interrupt();
复制代码


System.out.printf("Thread is interrupted ? %s\n", thread.islnterrupted());
复制代码


}
复制代码


}
复制代码

上面的代码中定义了一个线程,并且在线程的执行单元中(run 方法)写了一个空的死 循环,为什么不写 sleep 呢?因为 sleep 是可中断方法,会捕获到中断信号,从而干扰我们 程序的结果。下面是程序运行的结果,记得手动结束上面的程序运行,或者你也可以将上 面定义的线程指定为守护线程,这样就会随着主线程的结束导致 JVM 中没有非守护线程而 自动退出。


Thread is interrupted ? false
复制代码


Thread is interrupted ? true
复制代码

可中断方法捕获到了中断信号(signal)之后,也就是捕获了 InterruptedException 异常 之后会擦除掉 interrupt 的标识,对上面的程序稍作修改,你会发现程序的结果又会出现很 大的不同,示例代码如下:


package com.wangwenjun.concurrent.chapter03;
复制代码


import java.util.concurrent.TimeUnit;
复制代码


public class ThreadisInterrupted
复制代码


{
复制代码


public static void main(String[] args) throws InterruptedException
复制代码


{
复制代码


Thread thread = new Thread()
复制代码


{
复制代码


@Override
复制代码


public void run()
复制代码


while (true)
复制代码


复制代码


复制代码


try
复制代码


{
复制代码


TimeUnit.MINUTES.sleep(1);
复制代码


} catch (InterruptedException e)
复制代码


{
复制代码


//ignore the exception
复制代码


//here the interrupt flag will be clear.
复制代码


System.out.printf("I am be interrupted ? %s\n", islnterrupted());
复制代码


}
复制代码


}
复制代码


}
复制代码


};
复制代码


thread.setDaemon(true);
复制代码


thread.start();
复制代码


TimeUnit.MILLISECONDS.sleep(2);
复制代码


System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted()); thread.interrupt();
复制代码


TimeUnit, MILLISECONDS・ sleep(2); System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
复制代码


}
复制代码


}
复制代码

由于在 run 方法中使用了 sleep 这个可中断方法,它会捕获到中断信号,并且会擦除 interrupt 标识,因此程序的执行结果都会是 false,程序输岀如下:


Thread is interrupted ? false
复制代码


I am be interrupted ? false
复制代码


Thread is interrupted ? false
复制代码

其实这也不难理解,可中断方法捕获到了中断信号之后,为了不影响线程中其他方法 的执行,将线程的 interrupt 标识复位是一种很合理的设计。


interrupted


interrupted 是一个静态方法,虽然其也用于判断当前线程是否被中断,但是它和成员方 法 islnterrupted 还是有很大的区别的,调用该方法会直接擦除掉线程的 interrupt 标识,需 要注意的是,如果当前线程被打断了,那么第一次调用 interrupted 方法会返回 true,并且 立即擦除了 interrupt 标识;第二次包括以后的调用永远都会返回 false,除非在此期间线程 又一次地被打断,下面设计了一个简单的例子,来验证我们的说法:


package com.wangwenjun.concurrent.chapter03;import java.util.concurrent.TimeUnit;public class Threadinterruptedpublic static void main(String[] args) throws InterruptedException{Thread thread = new Thread(){@Overridepublic void run(){while (true){System.out.printIn(Thread.interrupted());}}};thread.setDaemon(true);thread.start();//shortly block make sure the thread is started.TimeUnit, MILLISECONDS.sleep(2); thread.interrupt();}}
复制代码

同样由于不想要受到可中断方法如 sleep 的影响,在 Thread 的 run 方法中没有进行任 何短暂的休眠,所以运行上面的程序会出现非常多的输出,但是我们通过对输出的检查会 发现如下所示的内容,其足以作为对该方法的解释。


falsefalsetruefalsefalse
复制代码

在很多的 false 包围中发现了一个 true,也就是 interrupted 方法判断到了其被中断,立 即擦除了中断标识,并且只有这一次返回 true,后面的都将会是 false。


interrupt 注意事项


打开 Thread 的源码,不难发现,islnterrupted 方法和 interrupted 方法都调用了同一个 本地方法:


private native boolean islnterrupted(boolean Clearlnterrupted);
复制代码

其中参数 Clearlnterrupted 主要用来控制是否擦除线程 interrupt 的标识。


islnterrupted 方法的源码中该参数为 false,表示不想擦除:


public boolean islnterrupted() { return islnterrupted(false);}
复制代码

而 interrupted 静态方法中该参数则为 true,表示想要擦除:


public static boolean interrupted() {return currentThread().islnterrupted(true);}
复制代码

在比较详细地学习了 interrupt 方法之后,大家思考一个问题,如果一个线程在没有执 行可中断方法之前就被打断,那么其接下来将执行可中断方法,比如 sleep 会发生什么样的 情况呢?下面我们通过一个简单的实验来回答这个疑问:


public static void main(String!] args){//① 判断当前线程是否被中断System.out.printin("Main thread is interrupted? " + Thread.interrupted());//②中断当前线程Thread.currentThread().interrupt();//③判断当前线程是否已经被中断System.out.printin("Main thread is interrupted? " + Thread.currentThread(). islnterrupted());try{//④ 当前线程执行可中断方法 TimeUnit.MINUTES.sleep(1);} catch (InterruptedException e){//⑤捕获中断信号System.out.printin("I will be interrupted still.");}}
复制代码

通过运行上面的程序,你会发现,如果一个线程设置了 interrupt 标识,那么接下来的 可中断方法会立即中断,因此注释⑤的信号捕获部分代码会被执行,请大家注意注释①和注 释③中判断线程中断方法的不同,也希望读者结合本节的内容思考为什么要这么做?


线程 join


Thread 的 join 方法同样是一个非常重要的方法,使用它的特性可以实现很多比较强大 的功能,与 sleep -样它也是一个可中断的方法,也就是说,如果有其他线程执行了对当前 线程的 interrupt 操作,它也会捕获到中断信号,并且擦除线程的 interrupt 标识,Thread 的 API 为我们提供了三个不同的 join 方法,具体如下。


□ public final void join() throws InterruptedException□ public final synchronized void join(long millis, int nanos)throws InterruptedException□ public final synchronized void join(long millis)throws InterruptedException
复制代码

在本节中,笔者将会详细介绍 join 方法以及如何在实际应用中使用 join 方法。


线程 join 方法详解


join 某个线程 A,会使当前线程 B 进入等待,直到线程 A 结束生命周期,或者到达给 定的时间,那么在此期间 B 线程是处于 BLOCKED 的,而不是 A 线程,下面就来通过一个 简单的实例解释一下 join 方法的基本用法:


package com.wangwenjun.concurrent.chapter03;import java.util.List;import java.util.concurrent.TimeUnit;import java.util.stream.IntStream;import static java.util.stream.Collectors.toList;public class Threadjoin{public static void main(String[] args) throws InterruptedException{//①定义两个线程,并保存在threads中List<Thread> threads = IntStream.range(1, 3).mapToObj(Threadjoin::create).collect(toList());//②启动这两个线程threads•forEach(Thread::start);//③ 执行这两个线程的join方法for (Thread thread : threads){thread.join();}//④main线程循环输出for (int i = 0; i < 10; i++)System.out.printin(Thread.currentThread().getName() + "+ i); shortSleep();

//构造一个简单的线程,每个线程只是简单的循环输出private static Thread create(int seq){return new Thread(() ->{for (int i = 0; i < 10; i++){System・ out・ printin(Thread.currentThread()・ getName() + "#" + i); shortSleep();}}, String.valueOf(seq));}private static void shortSleep(){try{TimeUnit, SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}}}
复制代码

上面的代码结合 Java 8 的语法,创建了两个线程,分别启动,并且调用了每个线程的 join 方法(注意:join 方法是被主线程调用的,因此在第一个线程还没有结束生命周期的时 后,第二个线程的 join 不会得到执行,但是此时,第二个线程也已经启动了),运行上面的 程序,你会发现线程一和线程二会交替地输出直到它们结束生命周期,main 线程的循环才 会开始运行,程序输岀如下:


2#81#82#91#9main#0main#lmain#2main#3
复制代码

如果你将注释③下面的 join 全部注释掉,那么三个线程将会交替地输出,程序输出如下:


main#22#21#2main#31#32#3 main#4
复制代码

join 方法会使当前线程永远地等待下去,直到期间被另外的线程中断,或者 join 的线 程执行结束,当然你也可以使用 join 的另外两个重载方法,指定毫秒数,在指定的时间到 达之后,当前线程也会退出阻塞。同样思考一个问题,如果一个线程已经结束了生命周期, 那么调用它的 join 方法的当前线程会被阻塞吗?


join 方法结合实战


本节我们将结合一个实际的案例,来看一下 join 方法的应用场景,假设你有一个 APP, 主要用于查询航班信息,你的 APP 是没有这些实时数据的,当用户发起查询请求时,你需 要到各大航空公司的接口获取信息,最后统一整理加工返回到 APP 客户端,如图 3-1 所示, 当然 JDK 自带了很多高级工具,比如 CountDownLatch 和 CyclicBarrier 等都可以完成类似 的功能,但是仅就我们目前所学的知识,使用 join 方法即可完成下面的功能。



该例子是典型的串行任务局部并行化处理,用户在 APP 客户端输入出发地“北京”和 目的地“上海”,服务器接收到这个请求之后,先来验证用户的信息,然后到各大航空公司 的接口查询信息,最后经过整理加工返回给客户端,每一个航空公司的接口不会都一样, 获取的数据格式也不一样,查询的速度也存在着差异,如果再跟航空公司进行串行化交互 (逐个地查询),很明显客户端需要等待很长的时间,这样的话,用户体验就会非常差。如果 我们将每一个航空公司的查询都交给一个线程去工作,然后在它们结束工作之后统一对数 据进行整理,这样就可以极大地节约时间,从而提高用户体验效果。


代码清单 3-1 查询接口 FightQuery


package com.wangwenjun.concurrent.chapter03;import j ava.util.List;public interface FightQuery{List<String> get();}
复制代码

在代码清单 3-1 中,FightQuery 提供了一个返回方法,写到这里大家应该注意到了,不 管是 Thread 的 run 方法,还是 Runnable 接口,都是 void 返回类型,如果你想通过某个线 程的运行得到结果,就需要自己定义一个返回的接口。


查询 Fight 的 task,其实就是一个线程的子类,主要用于到各大航空公司获取数据,示 例代码如下:


package com.wangwenjun,concurrent.chapter03;import java.util.ArrayList;import j ava.util.List;import j ava.util.concurrent.ThreadLocalRandom;import java.util.concurrent.TimeUnit;public class FightQueryTask extends Thread implements FightQuery

public FightQueryTask(String airline, String origin, String destination)super("[" + airline + "]"); this.origin = origin;this.destination = destination; }@Override public void run(){System.out.printf("%s-query from %s to %s \n", getName(), origin, destination);int randomVal = ThreadLocalRandom.current().nextlnt(10);try{TimeUnit.SECONDS.sleep(randomVal);this.flightList.add(getName() + + randomVal);System.out.printf("The Fight:%s list query successful\n", getName());} catch (InterruptedException e){}}@Overridepublic List<String> get(){return this.flightList;}}
复制代码

接口定义好了,查询航班数据的线程也有了,下面就来实现一下从 SH (上海)到北京 (BJ)的航班查询吧!示例代码如下:


package com.wangwenjun.concurrent.chapter0 3;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import static java.util.stream.Collectors.toList;public class FightQueryExample{//①合作的各大航空公司private static List<String> fightCompany = Arrays.asList("CSA", "CEA", "HNA");public static void main(String[] args){List<String> results = search("SH", "BJ");System.out.printin("===========result===========");results , forEach(System•out::printin);}private static List<String> search(String original, String dest){final List<String> result = new ArrayList<>();//②创建查询航班信息的线程列表List<FightQueryTask> tasks = fightCompany.stream().map(f -> createSearchTask(f, original, dest)).collect(toList());//③分别启动这几个线程tasks , forEach(Thread::start);
〃④分别调用每一个线程的join方法,阻塞当前线程 tasks.forEach(t ->tryt.join();} catch (InterruptedException e){}});//⑤在此之前,当前线程会阻塞住,获取每一个查询线程的结果,并且加入到result中tasks.stream().map(FightQuery::get).forEach(result::addAll);return result;}FightQueryTask createSearchTask( fight,original, String dest)return new FightQueryTask(fight, original, dest);}
复制代码

上面的代码,关键的地方已通过注释解释得非常清楚,主线程收到了 search 请求之后, 交给了若干个查询线程分别进行工作,最后将每一个线程获取的航班数据进行统一的汇总。 由于每个航空公司的查询时间可能不一样,所以用了一个随机值来反应不同的查询速度, 返回给客户端(打印到控制台),程序的执行结果输出如下:


[CSA]-query from SH to BJ [CEA]-query from SH to BJ [HNA]-query from SH to BJ The Fight:[HNA] list query The Fights[CSA] list query The Fight:[CEA] list query ===========result=========: [CSA]-4[CEA]-7[HNA]-2
复制代码

如何关闭一•个线程


JDK 有一个 Deprecated 方法 stop,但是该方法存在一个问题,JDK 官方早已经不推荐 使用,其在后面的版本中有可能会被移除,根据官网的描述,该方法在关闭线程时可能不


会释放掉 monitor 的锁,所以强烈建议不要使用该方法结束线程,本节将主要介绍几种关闭 线程的方法。


正常关闭


\1. 线程结束生命周期正常结束


线程运行结束,完成了自己的使命之后,就会正常退出,如果线程中的任务耗时比较 短,或者时间可控,那么放任它正常结束就好了。


\2. 捕获中断信号关闭线程


我们通过 new Thread 的方式创建线程,这种方式看似很简单,其实它的派生成本是比 较高的,因此在一个线程中往往会循环地执行某个任务,比如心跳检查,不断地接收网络 消息报文等,系统决定退出的时候,可以借助中断线程的方式使其退出,示例代码如下:


package com.wangwenjun.concurrent.chapter0 3;import java.util.concurrent. TimeUnit;public class InterruptThreadExit{public static void main(String[] args) throws InterruptedException{Thread t = new Thread(){@Overridepublic void run(){System.out.printin("I will start work");while (!islnterrupted()){//working.}System.out.printin("I will be exiting.");}};t.start();TimeUnit.MINUTES・ sleep(1); System.out.printin("System will be shutdown."); t.interrupt();}}
复制代码

上面的代码是通过检查线程 interrupt 的标识来决定是否退出的,如果在线程中执行某 个可中断方法,则可以通过捕获中断信号来决定是否退出。


@Overridepublic void run()System.out.printin("I will start work"); for (;;)//working, tryTimeUnit.MILLISECONDS.sleep(l); catch (InterruptedException e)break;}}System.out.printin("I will be exiting.");}
复制代码

上面的代码执行结果都会导致线程正常的结束,程序输出如下:


I will start workSystem will be shutdown.I will be exiting.
复制代码

\3. 使用 volatile 开关控制


由于线程的 interrupt 标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方法, 所以使用 volatile 修饰的开关 flag 关闭线程也是一种常用的做法,具体如下:


package com.wangwenjun.concurrent.chapter03;import java.util•concurrent.TimeUnit;public class FlagThreadExitstatic class MyTask extends Threadprivate volatile boolean closed = false;@Override public void run()System.out.printIn("I will start work"); while (Iclosed && !islnterrupted())//正在运行}System.out.printin("I will be exiting.");public void close(){this.closed = true;this , interrupt();}?public static void main(String[] args) throws InterruptedException{MyTask t = new MyTask();t.start();TimeUnit・ MINUTES.sleep(1);System.out.printIn("System will be shutdown.");t.close();}}
复制代码

上面的例子中定义了一个 closed 开关变量,并且是使用 volatile 修饰(关于 volatile 关 键字会在本书的第 3 部分中进行非常细致地讲解,volatile 关键字在 Java 中是一个革命性的 关键字,非常重要,它是 Java 原子变量以及并发包的基础)运行上面的程序同样也可以关 闭线程。


异常退出


在一个线程的执行单元中,是不允许抛出 checked 异常的,不论 Thread 中的 run 方 法,还是 Runnable 中的 run 方法,如果线程在运行过程中需要捕获 checked 异常并且 判断是否还有运行下去的必要,那么此时可以将 checked 异常封装成 unchecked 异常 (RuntimeException)抛出进而结束线程的生命周期。


发布于: 2020 年 11 月 28 日阅读数: 46
用户头像

小Q

关注

还未添加个人签名 2020.06.30 加入

小Q 公众号:Java架构师联盟 作者多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果能为您提供帮助,请给予支持(关注、点赞、分享)!

评论

发布
暂无评论
多线程源码明白了吗?不明白的话来看腾讯大牛给你画的面试重点