写点什么

纯干货!看了 10 多篇 Thread 详解,只有阿里 P7 大佬的这份才是王者

发布于: 2021 年 05 月 08 日
纯干货!看了10多篇Thread详解,只有阿里P7大佬的这份才是王者

上一篇是分享的是《spring 源码系列之 BeanDefinition》,这篇给大家分享《Thread 详解》,主要讲线程生命周期、Thread 类的构造方法以及常用 API、以及介绍线程关闭方法。


1 线程生命周期

1.1 五个阶段

线程生命周期可以分为五个阶段:

  • NEW

  • RUNNABLE

  • RUNNING

  • BLOCKED

  • TERMINATED

1.2 NEW

用 new 创建一个 Thread 对象时,但是并没有使用 start()启动线程,此时线程处于 NEW 状态。准确地说,只是 Thread 对象的状态,这就是一个普通的 Java 对象。此时可以通过 start()方法进入 RUNNABLE 状态。

1.3 RUNNABLE

进入 RUNNABLE 状态必须调用 start()方法,这样就在 JVM 中创建了一个线程。但是,线程一经创建,并不能马上被执行,线程执行与否需要听令于 CPU 调度,也就是说,此时是处于可执行状态,具备执行的资格,但是并没有真正执行起来,而是在等待被调度。

RUNNABLE 状态只能意外终止或进入 RUNNING 状态。

1.4 RUNNING

一旦 CPU 通过轮询或其他方式从任务可执行队列中选中了线程,此时线程才能被执行,也就是处于 RUNNING 状态,在该状态中,可能发生的状态转换如下:

  • 进入 TERMINATED:比如调用已经不推荐的 stop()方法

  • 进入 BLOCKED:比如调用了 sleep()/wait()方法,或者进行某个阻塞操作(获取锁资源、磁盘 IO 等)

  • 进入 RUNNABLE:CPU 时间片到,或者线程主动调用 yield()

1.5 BLOCKED

也就是阻塞状态,进入阻塞状态的原因很多,常见的如下:

  • 磁盘 IO

  • 网络操作

  • 为了获取锁而进入阻塞操作

  • 处于 BLOCKED 状态时,可能发生的状态转换如下:

  • 进入 TERMINATED:比如调用不推荐的 stop(),或者 JVM 意外死亡

  • 进入 RUNNABLE:比如休眠结束、被 notify()/nofityAll()唤醒、获取到某个锁、阻塞过程被 interrupt()打断等

1.6 TERMINATED

TERMINATED 是线程的最终状态,进入该状态后,意味着线程的生命周期结束,比如在下列情况下会进入该状态:

  • 线程运行正常结束

  • 线程运行出错意外结束

  • JVM 意外崩溃,导致所有线程都强制结束

2 Thread 构造方法

2.1 构造方法

Thread 的构造方法一共有八个,这里根据命名方式分类,使用默认命名的构造方法如下:

  • Thread()

  • Thread(Runnable target)

  • Thread(ThreadGroup group,Runnable target)

命名线程的构造方法如下:

  • Thread(String name)

  • Thread(Runnable target,Strintg name)

  • Thread(ThreadGroup group,String name)

  • Thread(ThreadGroup group,Runnable target,String name)

  • Thread(ThreadGroup group,Runnable target,String name,long stackSize)

但实际上所有的构造方法最终都是调用如下私有构造方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
复制代码

在默认命名构造方法中,在源码中可以看到,默认命名其实就是 Thread-X 的命令(X 为数字):

public Thread() {    this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);}
public Thread(Runnable target) { this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);}
private static synchronized int nextThreadNum() { return threadInitNumber++;}
复制代码

而在命名构造方法就是自定义的名字。

另外,如果想修改线程的名字,可以调用 setName()方法,但是需要注意,处于 NEW 状态的线程才能修改。

2.2 线程的父子关系

Thread 的所有构造方法都会调用如下方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
复制代码

其中的一段源码截取如下:

if (name == null) {    throw new NullPointerException("name cannot be null");} else {    this.name = name;    Thread parent = currentThread();    SecurityManager security = System.getSecurityManager();    if (g == null) {        if (security != null) {            g = security.getThreadGroup();        }
if (g == null) { g = parent.getThreadGroup(); } }}
复制代码

可以看到当前这里有一个局部变量叫 parent,并且赋值为 currentThread()currentThread()是一个 native 方法。因为一个线程被创建时的最初状态为 NEW,因此 currentThread()代表是创建自身线程的那个线程,也就是说,结论如下:

  • 一个线程的创建肯定是由另一个线程完成的

  • 被创建线程的父线程是创建它的线程

也就是自己创建的线程,父线程为 main 线程,而 main 线程由 JVM 创建。

另外,Thread 的构造方法中有几个具有 ThreadGroup 参数,该参数指定了线程位于哪一个 ThreadGroup,如果一个线程创建的时候没有指定 ThreadGroup,那么将会和父线程同一个 ThreadGroupmain 线程所在的 ThreadGroup 称为 main

2.3 关于 stackSize

Thread 构造方法中有一个 stackSize 参数,该参数指定了 JVM 分配线程栈的地址空间的字节数,对平台依赖性较高,在一些平台上:

  • 设置较大的值:可以使得线程内调用递归深度增加,降低 StackOverflowError 出现的概率

  • 设置较低的值:可以使得创建的线程数增多,可以推迟 OutOfMemoryError 出现的时间

但是,在一些平台上该参数不会起任何作用。另外,如果设置为 0 也不会起到任何作用。

3 Thread API

3.1 sleep()

sleep()有两个重载方法:

  • sleep(long mills)

  • sleep(long mills,int nanos)

但是在 JDK1.5 后,引入了 TimeUnit,其中对 sleep()方法提供了很好的封装,建议使用 TimeUnit.XXXX.sleep()去代替 Thread.sleep()

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

3.2 yield()

yield()属于一种启发式方法,提醒 CPU 调度器当前线程会自愿放弃资源,如果 CPU 资源不紧张,会忽略这种提醒。调用 yield()方法会使当前线程从 RUNNING 变为 RUNNABLE 状态。

关于 yield()sleep()的区别,区别如下:

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

  • yield()只是对 CPU 调度器的一个提示,如果 CPU 调度器没有忽略这个提示,会导致线程上下文的切换

  • sleep()会使线程短暂阻塞,在给定时间内释放 CPU 资源

  • 如果 yield()生效,yield()会使得从 RUNNING 状态进入 RUNNABLE 状态

  • sleep()会几乎百分百地完成给定时间的休眠,但是 yield()的提示不一定能担保

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

3.3 setPriority()

3.3.1 优先级介绍

线程与进程类似,也有自己的优先级,理论上来说,优先级越高的线程会有优先被调度的机会,但实际上并不是如此,设置优先级与 yield()类似,也是一个提醒性质的操作:

  • 对于 root 用户,会提醒操作系统想要设置的优先级别,否则会被忽略

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

所以,设置优先级只是很大程度上让某个线程尽可能获得比较多的执行机会,也就是让线程自己尽可能被操作系统调度,而不是设置了高优先级就一定优先运行,或者说优先级高的线程比优先级低的线程就一定优先运行。

3.3.2 优先级源码分析

设置优先级直接调用 setPriority()即可,OpenJDK 11 源码如下:

public final void setPriority(int newPriority) {    this.checkAccess();    if (newPriority <= 10 && newPriority >= 1) {        ThreadGroup g;        if ((g = this.getThreadGroup()) != null) {            if (newPriority > g.getMaxPriority()) {                newPriority = g.getMaxPriority();            }
this.setPriority0(this.priority = newPriority); }
} else { throw new IllegalArgumentException(); }}
复制代码

可以看到优先级处于[1,10]之间,而且不能设置为大于当前 ThreadGroup 的优先级,最后通过 native 方法 setPriority0 设置优先级。

一般情况下,不会对线程的优先级设置级别,默认情况下,线程的优先级为 5,因为 main 线程的优先级为 5,而且 main 为所有线程的父进程,因此默认情况下线程的优先级也是 5。

3.4 interrupt()

interrupt()是一个重要的 API,线程中断的 API 有如下三个:

  • void interrupt()

  • boolean isInterrupted()

  • static boolean interrupted()

下面对其逐一进行分析。

3.4.1 interrupt()

一些方法调用会使得当前线程进入阻塞状态,比如:

  • Object.wait()

  • Thread.sleep()

  • Thread.join()

  • Selector.wakeup()

而调用 interrupt()可以打断阻塞,打断阻塞并不等于线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。一旦在阻塞状态下被打断,就会抛出一个 InterruptedException 的异常,这个异常就像一个信号一样通知当前线程被打断了,例子如下:

public static void main(String[] args) throws InterruptedException{    Thread thread = new Thread(()->{        try{            TimeUnit.SECONDS.sleep(10);        }catch (InterruptedException e){            System.out.println("Thread is interrupted.");        }    });    thread.start();    TimeUnit.SECONDS.sleep(1);    thread.interrupt();}
复制代码

会输出线程被中断的信息。

3.4.2 isInterrupted()

isInterrupted()可以判断当前线程是否被中断,仅仅是对 interrupt()标识的一个判断,并不会影响标识发生任何改变(因为调用 interrupt()的时候会设置内部的一个叫 interrupt flag 的标识),例子如下:

public static void main(String[] args) throws InterruptedException{    Thread thread = new Thread(()->{        while (true){}    });    thread.start();    TimeUnit.SECONDS.sleep(1);    System.out.println("Thread is interrupted :"+thread.isInterrupted());    thread.interrupt();    System.out.println("Thread is interrupted :"+thread.isInterrupted());}
复制代码

输出结果为:

Thread is interrupted :falseThread is interrupted :true
复制代码

另一个例子如下:

public static void main(String[] args) throws InterruptedException {    Thread thread = new Thread() {        @Override        public void run() {            while (true) {                try {                    TimeUnit.SECONDS.sleep(3);                } catch (InterruptedException e) {                    System.out.println("In catch block thread is interrupted :" + isInterrupted());                }            }        }    };    thread.start();    TimeUnit.SECONDS.sleep(1);    System.out.println("Thread is interrupted :" + thread.isInterrupted());    thread.interrupt();    TimeUnit.SECONDS.sleep(1);    System.out.println("Thread is interrupted :" + thread.isInterrupted());}
复制代码

输出结果:

Thread is interrupted :falseIn catch block thread is interrupted :falseThread is interrupted :false
复制代码

一开始线程未被中断,结果为 false,调用中断方法后,在循环体内捕获到了异常(信号),此时会 Thread 自身会擦除 interrupt 标识,将标识复位,因此捕获到异常后输出结果也为 false

3.4.3 interrupted()

这是一个静态方法,调用该方法会擦除掉线程的 interrupt 标识,需要注意的是如果当前线程被打断了:

  • 第一次调用 interrupted()会返回 true,并且立即擦除掉 interrupt 标识

  • 第二次包括以后的调用永远都会返回 false,除非在此期间线程又一次被打断

例子如下:

public static void main(String[] args) throws InterruptedException {    Thread thread = new Thread() {        @Override        public void run() {            while (true) {                System.out.println(Thread.interrupted());            }        }    };    thread.setDaemon(true);    thread.start();    TimeUnit.MILLISECONDS.sleep(2);    thread.interrupt();}
复制代码

输出(截取一部分):

falsefalsefalsetruefalsefalsefalse
复制代码

可以看到其中带有一个 true,也就是 interrupted()判断到了其被中断,此时会立即擦除中断标识,并且只有该次返回 true,后面都是 false

关于 interrupted()isInterrupted()的区别,可以从源码(OpenJDK 11)知道:

public static boolean interrupted() {    return currentThread().isInterrupted(true);}
public boolean isInterrupted() { return this.isInterrupted(false);}
@HotSpotIntrinsicCandidateprivate native boolean isInterrupted(boolean var1);
复制代码

实际上两者都是调用同一个 native 方法,其中的布尔变量表示是否擦除线程的 interrupt 标识:

  • true 表示想要擦除,interrupted()就是这样做的

  • false 表示不想擦除,isInterrupted()就是这样做的

3.5 join()

3.5.1 join()简介

join()与 sleep()一样,都是属于可以中断的方法,如果其他线程执行了对当前线程的 interrupt 操作,也会捕获到中断信号,并且擦除线程的 interrupt 标识,join()提供了三个 API,分别如下:

  • void join()

  • void join(long millis,int nanos)

  • void join(long mills)

3.5.2 例子

一个简单的例子如下:

public class Main {    public static void main(String[] args) throws InterruptedException {        List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList());        threads.forEach(Thread::start);        for (Thread thread:threads){            thread.join();        }        for (int i = 0; i < 10; i++) {            System.out.println(Thread.currentThread().getName()+" # "+i);            shortSleep();        }    }
private static Thread create(int seq){ return new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+" # "+i); shortSleep(); } },String.valueOf(seq)); }
private static void shortSleep(){ try{ TimeUnit.MILLISECONDS.sleep(2); }catch (InterruptedException e){ e.printStackTrace(); } }}
复制代码

输出截取如下:

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

线程 1 和线程 2 交替执行,而 main 线程会等到线程 1 和线程 2 执行完毕后再执行。

4 线程关闭

Thread 中有一个过时的方法 stop,可以用于关闭线程,但是存在的问题是有可能不会释放 monitor 的锁,因此不建议使用该方法关闭线程。线程的关闭可以分为三类:

  • 正常关闭

  • 异常退出

  • 假死

4.1 正常关闭

4.1.1 正常结束

线程运行结束后,就会正常退出,这是最普通的一种情况。

4.1.2 捕获信号关闭线程

通过捕获中断信号去关闭线程,例子如下:

public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){        @Override        public void run() {            System.out.println("work...");            while(!isInterrupted()){
} System.out.println("exit..."); } }; t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.interrupt();}
复制代码

一直检查 interrupt 标识是否设置为 true,设置为 true 则跳出循环。另一种方式是使用 sleep()

public static void main(String[] args) throws InterruptedException {    Thread t = new Thread(){        @Override        public void run() {            System.out.println("work...");            while(true){                try{                    TimeUnit.MILLISECONDS.sleep(1);                }catch (InterruptedException e){                    break;                }            }            System.out.println("exit...");        }    };    t.start();    TimeUnit.SECONDS.sleep(5);    System.out.println("System will be shutdown.");    t.interrupt();}
复制代码

4.1.3 volatile

由于 interrupt 标识很有可能被擦除,或者不会调用 interrupt()方法,因此另一种方法是使用 volatile 修饰一个布尔变量,并不断循环判断:

public class Main {    static class MyTask extends Thread{        private volatile boolean closed = false;
@Override public void run() { System.out.println("work..."); while (!closed && !isInterrupted()){
} System.out.println("exit..."); }
public void close(){ this.closed = true; this.interrupt(); } } public static void main(String[] args) throws InterruptedException { MyTask t = new MyTask(); t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.close(); }}
复制代码

4.2 异常退出

线程执行单元中是不允许抛出 checked 异常的,如果在线程运行过程中需要捕获 checked 异常并且判断是否还有运行下去的必要,可以将 checked 异常封装为 unchecked 异常,比如 RuntimeException,抛出从而结束线程的生命周期。

4.3 假死

所谓假死就是虽然线程存在,但是却没有任何的外在表现,比如:

  • 没有日志输出

  • 不进行任何的作业

等等,虽然此时线程是存在的,但看起来跟死了一样,事实上是没有死的,出现这种情况,很大可能是因为线程出现了阻塞,或者两个线程争夺资源出现了死锁。

这种情况需要借助一些外部工具去判断,比如 VisualVMjconsole 等等,找出存在问题的线程以及当前的状态,并判断是哪个方法造成了阻塞。

  • 以上就是《Thread 详解》的分享。

  • 也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。

  • 你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~

用户头像

还未添加个人签名 2021.04.26 加入

还未添加个人简介

评论

发布
暂无评论
纯干货!看了10多篇Thread详解,只有阿里P7大佬的这份才是王者