写点什么

面试官竟然一直和我聊线程的启动和终止

用户头像
Simon郎
关注
发布于: 2020 年 05 月 11 日
面试官竟然一直和我聊线程的启动和终止

线程的启动和终止



1、线程的构造



在运行线程之前首先要构造一个线程对象,java.Lang.Thread中为我们提供了一个用于创建线程时的初始化方法。主要对线程中的属性进行初始化



主要的属性



ThreadGroup g:线程组

Runnable target:可以调用run方法的对象

String name:构造的线程名字

long stackSize:新线程所需的堆栈大小,参数为0时表示被忽略



主要源码解析



private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//判断线程名是否为空,否则抛出异常
if (name == null) {
throw new NullPointerException("name cannot be null");
}
//线程名称
this.name = name;
//当前线程就是该线程的父线程
Thread parent = currentThread();
//线程组
this.group = g;
//使用父线程的Damon、Priority属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
//加载父线程的contextClassLoader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//从父线程中拿到inheritThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
//分配一个线程ID
tid = nextThreadID();
}

这表明一个新的构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程,至此一个能够运行的线程就初始化好了。



讲解了初始化方式后,我们来学习下怎么多线程编程?有哪些方式?这些方式之间有什么特点?



1.1继承Thread类



创建一个类去继承Thread类,重写里的run方法,在main方法中调用该类的实例对象的start方法就可以实现多线程的并发



测试代码



**
* @Author: Simon Lang
* @Date: 2020/5/2 17:05
*/
public class UseThread {
public static void main(String[] args){
MyThread thread1=new MyThread("线程A");
MyThread thread2=new MyThread("线程B");
thread1.start();
thread2.start();
}
}
//构造MyThread线程
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run() {
for (int i=1;i<=3;i++){
System.out.println("线程"+name+" :"+i);
}
}
}

测试结果



第一次测试



第二次测试

我们测试了两次结果,因为线程是并发执行的,所以结果可能是不同的,运行的结果与代码的执行顺序或者调用顺序无关。



1.2实现Runnable接口



/**
* @Author: Simon Lang
* @Date: 2020/5/2 17:12
*/
public class UseRunnable {
public static void main(String[] args){
MyRunnable runnable1=new MyRunnable("线程C");
MyRunnable runnable2=new MyRunnable("线程D");
Thread thread1=new Thread(runnable1);
Thread thread2=new Thread(runnable2);
thread1.start();
thread2.start();
}
}
class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name){
this.name=name;
}
public void run() {
for (int i=1;i<=3;i++){
System.out.println(name+" :"+i);
}
}
}

测试的结果也不是固定的,这里就不贴图了,这里也用到了Thread类,它的作用是把run方法包装成线程执行体,被包装后可以使用start方法执行线程。



Note:继承Thread类只能是单继承,如果实现Runnable接口,可以使得开发更加的灵活。



2、启动线程



线程的启动时调用start()方法,此时的线程会进入就绪状态,这表明它可以由JVM调度调度并执行,但是这并不意味着它会立即执行,当CPU给这个线程分配时间片后,就会开始run方法。



我们里来查看看satrt方法的源码看看都具体做了些什么



start方法源码解析



start()方法执行流程:①判断当前线程是不是首次创建②如果是,调用strat0方法(JVM)进行资源调度,回调run方法执行具体的操作



public synchronized void start() {
//不能重复执行start,否则将会抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//将该线程添加到线程组
group.add(this);
boolean started = false;
//start0()是一个native方法,这表明线程的实现与java无关
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

那么,我们可能会有疑问,为什么不能直接调用run方法呢?



这是因为start方法是用于启动线程的,可以实现并发,而run方法只是一个普通的方法,是不能实现并发的,只是在并发执行时调用



NOTE: start()方法中的native方法是本地方法,使用C/C++写的,这个方法的使用与java平台无关,java所做的只是将Thread对象映射到操作系统所提供的线程上面,对外提供统一的接口。



3、线程的中断



中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其它线程进行了中断操作,但是通过中断并不能直接终止另一个线程,而是需要中断的线程自己处理。



java.lang.Thread类提供了几个方法操作中断状态:



  • interrupted()



测试当前线程是否被中断,该方法可以消除线程的中断状态,如果连续调用该方法,第二次的调用会返回false。

public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
  • isInterrupted()



测试线程是否已经中断,中断的线程返回true,中断的状态不受该方法的影响



public boolean isInterrupted() {
return isInterrupted(false);
}
  • interrupt()



中断线程,将中断状态设置为true,



public void interrupt() {
//省略,具体的操作是本地方法interrupt0
}



Note:这三个方法中,只有interrupt()方法是修改中断标志的,另外的两个检测方法都是用于检测中断状态的



线程共有6中状态,中断主要用于运行态/阻塞态/等待态/超时等待,这四种状态的中断机制下又可分为两类。一类是设置中断标志,让被中断的线程判断后执行相应的代码。另一类是遇到中断时抛出InterruptedException异常,并清空中断标志位。



3.1运行态的中断/阻塞态中



对处于运行态和阻塞态执行中断后,它们的执行状态不变,但是中断标志位已被修改,并不会实际的中断线程的运行,我们可以利用java程序中的中断标志位来进行自我中断,因为这两种状态的操作都是类似的,所以我们只讲解运行态。



测试代码



public class TestInterrupte {
public static void main(String[] args) throws InterruptedException {
//创建一个子线程
MyThreadA thread=new MyThreadA();
//开启这个子线程
thread.start();
//打印中断前线程的状态和标志位
System.out.println(thread.getState());
System.out.println(thread.isInterrupted());
thread.interrupt();
//中断后线程的执行状态和标志位
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
//MyThreadA子线程
class MyThreadA extends Thread{
@Override
public void run() {
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("该线程执行中断");
break;
}
}
}
}

测试结果

Note:从测试结果可以看出,线程的中断不会立马影响线程的状态,线程中断前默认标志位为false,中断后标志位被修改true,标志后被修改后,子线程并没有马上执行中断,而是在主线程继续执行一段时间后才执行中断(从先打印RUNNABLE,后打印“该线程执行中断”可以看出)。

3.2等待态的中断/超时等待态的中断



这两种状态很类似,它们均是在线程运行的过程中缺少某些条件而被挂载在某个对象的等待对列中,当这些线程遇到中断操作的时候,就会抛出一个InterruptedException异常,并清空中断标志位,这里以等待态为例编写测试代码



测试代码



public class TestInterrupte {
public static void main(String[] args) throws InterruptedException {
//创建一个MyThreadB子线程
MyThreadB thread=new MyThreadB();
//开启子线程
thread.start();
//睡眠500ms,使得该线程阻塞
MyThread.sleep(500);
//打印中断前线程的状态和标志位
System.out.println(thread.getState());
System.out.println(thread.isInterrupted());
thread.interrupt();
ThreadB.sleep(500);
//打印中断前线程的状态和标志位
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
class MyThreadB extends Thread{
@Override
public void run() {
synchronized (this){
try {
wait();
}catch (InterruptedException e){
System.out.println("发生中断了,我要抛出异常");
}
}
}
}

测试结果

NOTE:从结果上可以看出,线程被启动后就被挂载到了等待队列中了,我们执行中断后,输出了我们要打印的异常语句,因为标志位被清空,所以打印出来的标志位时false,执行完中断后立马进入到阻塞态。



中断总结:



线程中断不会使得线程立马退出,而是会给线程发送一个通知,告诉目标线程你需要退出了,具体的退出操作是由目标线程来执行,这也就是为什们上面测试代码中,从等待态--->阻塞态的原因



4、线程的终止



在早期的jdk版本时,经常使用stop方法来强制终止线程,但是这种操作是不安全的,会导致数据的丢失,所以,可以使用interrupt来中断线程,除此之外,还可以利用中断标志使线程正常退出,也就是当run方法执行完后终止线程。



所以总结来说,线程的终止有三种方式



stop方法强制退出

interrupt方法中断线程

使用退出标志



因为第一种方法不安全,本文将重点讲解是由退出标志位进和使用中断来终止线程



测试代码



public class ShutDown {
public static void main(String[] args) throws InterruptedException {
Runner one=new Runner();
Thread countThread=new Thread(one,"CountThread");
countThread.start();
//睡眠1后,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);//子线程在1s不断累加
countThread.interrupt();//采用中断结束
Runner two=new Runner();
countThread=new Thread(two,"CountThread");
countThread.start();//重新开启线程
//睡眠1s,main线程对Runner two进行取消,使得CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();//采用标志位进行结束
}
private static class Runner implements Runnable{
private long i;
//定义标志变量
private volatile boolean on=true;
public void run() {
//on为true且没有执行了中断
while (on && !Thread.currentThread().isInterrupted()){
i++;
}
//执行中断或标志位为false时,打印输出
System.out.println("Count i= " +i);
}
public void cancel(){
on=false;
}
}
}

NOTE:程序创建了线程CountThread,它不断的进行变量的累加,而主线程尝试对其进行中断操作和停止操作。分别使用了cancel方法和中断操作来终止线程,这两种方法都是有效的。



参考文献



[1]方腾飞.java并发编程的艺术



[2]https://blog.csdn.net/weixin_43490440/article/details/101519957



[3]https://www.cnblogs.com/yangming1996/p/7612653.html



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

Simon郎

关注

自学;制学;博学 2019.10.09 加入

公众号:小郎码知答

评论 (4 条评论)

发布
用户头像
感谢分享原创内容,我推荐到InfoQ官网首页了。
2020 年 05 月 11 日 18:39
回复
谢谢,会持续输出原创内容
2020 年 05 月 11 日 20:20
回复
用户头像
排版看着很舒服哦~
2020 年 05 月 11 日 18:23
回复
谢谢,会继续努力的
2020 年 05 月 11 日 20:19
回复
没有更多了
面试官竟然一直和我聊线程的启动和终止