Loading...
墨滴

UNREMITTINGLY

2021/12/06  阅读:49  主题:雁栖湖

并发编程-线程基础

笔者把整个java生态分为多个模块,java并发编程是其中一个重要的模块,本篇讲的就是java并发编程模块中的线程基础

java并发编程在整个java生态中的地位举足轻重,贯穿整个java开发应用过程以及面试流程中,而在并发编程中,线程是其基础,要想掌握并发编程的精髓,必须先精通线程基础,本篇就带你搞清楚线程到底是什么?


1线程概念

虽然我知道很多读者的基础已经很好,但是为了篇幅的完整性,我还是先要把线程概念阐述出来。

进程

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

扩展

我们知道,线程是比进程更轻量级的调度执行单位,操作系统把处理器资源分配给进程,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。目前线程是Java里面进行处理器资源调度的最基本单位,不过如果日后引入纤程(Fiber)的话,可能就会改变这一点。

主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经调用过start()方法且还未结束的java.lang.Thread类的实例就代表着一个线程。我们注意到Thread类与大部分的Java类库API有着显著差别,它的所有关键方法都被声明为Native。在Java类库API中,一个Native方法往往就意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用Native方法,不过通常最高效率的手段也就是平台相关的手段),接下来我们以一个通用的应用程序的角度来看看线程是如何实现的。

实现线程主要有三种方式:

  • 使用内核线程实现(1:1实现)
  • 使用用户线程实现(1:N实现)
  • 使用用户线程加轻量级进程混合实现(N:M实现)

1.内核线程实现

内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核。

程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,轻量级进程就是我们通常意义上所讲的线程,而每个轻量级进程都由一个内核线程支持,因此实现了用户线程和内核线程的绑定,这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。轻量级进程也具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。

2.用户线程实现

可以这样理解,用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。

用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。因为使用用户线程实现的程序通常都比较复杂,除了有明确的需求外,一般的应用程序都不倾向使用用户线程。Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如Golang、Erlang等,使得用户线程的使用率有所回升。

3.混合实现

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为N:M实现。

在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,是N:M的关系,这种就是多对多的线程模型。 许多UNIX系列的操作系统,如Solaris、HP-UX等都提供了M:N的线程模型实现。在这些操作系统上的应用也相对更容易应用M:N的线程模型。

Java线程是如何来实现的呢?

Java线程在早期的Classic虚拟机上(JDK 1.2以前),是基于一种被称为“绿色线程”(Green Threads)的用户线程实现的,但从JDK 1.3起,“主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用1:1的线程模型。

操作系统支持怎样的线程模型,在很大程度上会影响上面的Java虚拟机的线程是怎样映射的,这一点在不同的平台上很难达成一致,因此《Java虚拟机规范》中才不去限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编码和运行过程来说,这些差异都是完全透明的。

2Thread线程创建

线程两种创建方式解析

我们知道,在写代码时候经常用的创建线程的方式有两种:

  • 继承Thread
  • 实现Runnable

接下来我们先来看下上面两种创建方式的逻辑是怎样的,先看下应用中具体的代码写法

//Thread实现方式
class Student1 extends Thread{
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("学习"+i);
        }
    }
    
    public static void main(String[] args) {
       Student1 xiaoming = new Student1();
       xiaoming.start();
    }
}



//Runnable实现方式
class Student2 implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("学习"+i);
        }
    }
    
    public static void main(String[] args) {
       Thread xiaoming = new Thread(new Student2());
       xiaoming.start();
    }
}

从上面两种方式的代码中可以看出:

  • 两种方式在调用start方法之前必须都要先创建Thread对象(上面的方式虽然没有直接创建Thread对象,但是Student1继承了Thread,所以Student1本身也可以算是Thread);

  • 两种方式调用的都是Thread对象的start方法

从上面的代码中可以读取到以上信息,可以得出一个小的结论,那就是创建线程其实只有一种方式,那就是new Thread(),只不过一个是调用Thread类的无惨构造方法,一个是调用Thread类的有参数构造方法。

接下来我们在以上结论的基础上看下Thread源码,Thread类的源码比较长,我只把上面两种方式相关的贴出来,源码如下:

public class Thread implements Runnable {

  private Runnable target;
    
    public Thread() {
      init(null, null, "Thread-" + nextThreadNum           (), 0);
    }
    
  public Thread(Runnable var1) {
   this.init((ThreadGroup) null, var1, "Thread-"       + nextThreadNum(), 0L);
  }

  private void init(ThreadGroup var1, Runnable var2, String var3, long var4) {
   this.init(var1, var2, var3, var4, (AccessControlContext) null, true);
  }

  private void init(ThreadGroup var1, Runnable var2, String var3, long var4, AccessControlContext var6, boolea n var7) {
   this.target = var2;
  }
  
  public synchronized void start() {
   if (this.threadStatus != 0) {
    throw new IllegalThreadStateException();
   } else {
    this.group.add(this);
    boolean var1 = false;
    try {
     this.start0();
     var1 = true;
    } finally {
     try {
      if (!var1) {
       this.group.threadStartFailed(this);
      }
     } catch (Throwable var8) {
     }
    }
   }
  }

  private native void start0();
  
  public void run() {
   if (this.target != null) {
    this.target.run();
   }
  }
 }
 

在上面的源码中重点看下两个构造方法和一个成员变量,我们总结为以下几个点:

  • Thread类实现了Runnable类,进入Runnable类源码可以看到只有一个run方法,所以不难看出Runnable仅仅是一个定义任务执行体的接口,而Thread类中的run方法就是其执行体的具体实现,看到这里我们不得不觉得这是不是有点多此一举,Thread类完全可以自己定义一个执行体,干嘛要通过实现Runnable类的方式来重写执行体呢,其实这里并非多次一举,而是充分体现了java面向接口的编程思想;
  • 有参构造方法最终把传入的Runnable对象赋值给成员变量target;
  • run方法中加了一个target为空的判断,如果为空执行自己的run方法,如果不为空,就会执行target实例中的run方法。

看到这里我们大概明白了,线程类中最重要的两个部分:

  • 一个是线程创建,启动等线程控制相关的操作;
  • 一个是任务执行的逻辑,即执行体;

继承Thread的方式中,执行体是有子类自己实现的,最终执行的线程自己的执行体;

实现Runnable的方式中,执行体是由外部的Runnable对象实现,最终执行的是Runnable对象的执行体。

那么两者相比到底哪个更好呢? 第二种方式是更好的实践,原因如下:

1.java语言中只能单继承,通过实现接口的方式,可以让实现类去继承其它类。而直接继承thread就不能再继承其它类了;

2.线程控制逻辑在Thread类中,业务运行逻辑在Runnable实现类中。解耦更为彻底;

3.也正是因为结构执行体和线程控制单元,实现Runnable的实例,可以被多个线程共享并执行。而实现thread是做不到这一点的。

线程start和run方法的联系

通过上面的源码对于run方法的逻辑我们已经了解了,run作为执行体,那Thread在调用start方法启动后,又是怎么调用到run方法的呢?我们跟一下start方法的源码

在上面源码中可以看到start方法的主要逻辑为:

1.检查线程的状态,是否可以启动;
2.把线程加入到线程group中;
3.调用了start0()方法。

我们看到最终调用为了start0方法。

private native void start0();

源码到这里就没有了,我们看到start0方法是一个native方法,调用的是本地的c++实现的代码,可以理解为调用了jvm底层的逻辑,按照我们正常的想法,start方法一定会在某个环节调用run方法完成任务的执行才对,但是start0方法内部我们是看不到的,但是源码中一般不会这样没头没尾,所以仔细研究源码会发现在注释中有这样一段描述

* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.

其中这一句*the Java Virtual Machine calls the run method of this thread。*告诉我们虚拟机会主动调用当前的run方法,由此我们可以推断出整个执行流程如下:

至此,我们已经分析清楚从线程创建到调用run方法执行业务逻辑的过程。

3Thread线程状态

我们用new关键字创建 Thread 对象,此时状态为 NEW,此时Thread 对象还不需要去执行任务,所以他并没在等待 CPU 的队列中。假如时机已到,我们调用 start 方法,Thread 对象开始排队等待CPU的调度。此时Thread 对象状态为 RUNNABLE。当排队获取到CPU调度后,Thread 对象就会立即开始执行任务,也就是触发 run 方法。此时Thread 对象状态为 RUNNING。当任务执行完毕,Thread 对象就要让出 CPU 了,并且标识自己为 TERMINATED 状态。此时Thread 对象生命周期已经结束了。

如果在running过程中遇到抢占锁失败,就会进入blocked状态,如果锁被释放,当前线程获取到了锁,则重新进入runable状态等待被cpu选中。

如果在同步代码块中调用wait等方法,线程就会进入waiting状态,只能等待notify唤醒才能重新进入runable状态等待被cpu选中。

这里提出一个问题blocked和waiting有什么区别呢?

这里只提一个点,blocked是被动状态,waiting是主动状态。其他还可以从转换的节点和触发的机制等方面分析。

接下里详细说明下各个状态

NEW 状态

当一个 Thread 对象刚刚被创建时,状态为 NEW。此状态仅仅表示 Thread 对象被创建出来了,但此时 Thread 对象和其它 Java 对象没有什么不同,仅仅是存在于内存之中。

RUNNABLE 状态

Thread 对象进入 RUNNABLE 状态只有一条路可以走,就是调用 start 方法。在调用 start 方法后,这个线程对象才在 JVM 中挂上号了,JVM 才知道有这么个线程对象要搞事情。我们上面分析了,start最终是调用了strat0方法,此方法是native方法,是有jvm底层实现,而且上面又说过,java采用的是1:1线程模型,那就不难猜出,内核线程就是在这个阶段开始创建并和java线程绑定了。但是线程对象此时还不能执行任务,为什么呢?因为它只是就绪状态,还未被cpu选中,因此需要等到cpu调度分配时间片才能执行任务。

RUNNABLE 状态的线程只可能变迁为 RUNNING 和 TERMINATED 状态。当等到 CPU 调度时,状态变为 RUNNING。而在等待 CPU 调度期间,如果被意外终止,那么则会直接进入 TERMINATED 状态。

RUNNING 状态

RUNNABLE 状态的线程,一旦被 CPU 选中执行,他就会变为 RUNNING 状态。此时才会真正运行 run 方法逻辑。

RUNNING 状态的线程可以变迁为如下状态:

  • 处于以下动作的RUNNING状态的线程会变迁为BLOCKED 状态

    1.阻塞的 I/O 操作

    2.进入到锁的阻塞队列

  • 处于以下动作的RUNNING状态的线程会变迁为TERMINATED

    1.run 方法正常执行结束

    2.运行意外中止

    3.调用 stop 方法(已经不推荐使用)

  • 处于以下动作的RUNNING状态的线程会变迁为WAITING

    1.RUNNING 的线程如果在执行过程中调用了wait或者sleep或者park方法,就会进入WAITING状态

  • 处于以下动作的RUNNING状态的线程会变迁为RUNNABLE

    1.CPU 轮转,放弃该线程的执行

    2.调用了Thread的yield方法,提示CPU可以让出自己的执行权。如果CPU对此响应,则会进入到RUNNABLE状态,而不是TERMINATED。

BLOCKED 状态

只有RUNNING状态的线程才会进入BLOCKED状态,进入BLOCKED状态的原因在上面已经讲过。处于BLOCKED状态的线程,可以转换为如下状态:

  • 处于以下动作的BLOCKED状态的线程会转换为RUNNABLE状态

    1.阻塞操作结束了,那么线程将切换回RUNNABLE状态。注意不是RUNNING状态,此时线程需要等待CPU的再一次选中。

    2.由于等待锁而被BLOCKED的线程。一旦获取了锁,那么便会回到 RUNNABLE状态。

  • 处于以下动作的BLOCKED状态的线程会转换为TERMINATED状态

    1.线程BLOCKED状态时,有可能由于调用了stop或者意外终止,而直接进入了TERMINATED状态。

WAITING 状态

只有RUNNING状态的线程才会进入WAITING状态,进入WAITING状态的原因在上面已经讲过。处于WAITING状态的线程,可以转换为如下状态:

  • 处于以下动作的WAITING状态的线程会转换为RUNNABLE状态

    1.由于其它线程调用了interrupt方法中断了可中断方法,也会回到RUNNABLE状态。

    2.线程休眠的时间结束了,也会回到RUNNABLE状态。

    3.由于wait操作进入BLOCKED状态的线程,其他线程发出了notify或notifyAll,则会唤醒它,回到RUNNABLE状态。

  • 处于以下动作的WAITING状态的线程会转换为TERMINATED状态

    1.线程WAITING状态时,有可能由于调用了stop或者意外终止,而直接进入了TERMINATED状态。

TERMINATED 状态

TERMINATED状态意味着线程的生命周期已经走完。这是线程的终止状态。此状态的线程不会再转化为其它任何状态。

处于 RUNNING 或者 BLOCKED 状态的线程都有可能变为 TERMINATED 状态,但原因是类似的,如下:

1.线程运行正常结束

2.程序运行异常终止

3.JVM 意外终止

4Thread线程api

上面讲解了 Thread 的几种状态,以及状态间的变化。有些状态的变化是被动发生的,比如 run 方法执行完后进入 TERMINATED 状态。不过更多时候,状态的变化是由于主动调用了某些方法。而这些方法大多数是 Thread 类的 API。接下来要讲解的api在Thread源码中都可找到。

sleep 方法

顾名思义,线程的 sleep 方法会使线程休眠指定的时间长度。休眠的意思是,当前逻辑执行到此不再继续执行,而是等待指定的时间。但在这段时间内,该线程持有的 monitor 锁并不会被放弃。我们可以认为线程只是工作到一半休息了一会,但它所占有的资源并不会交还。这样设计很好理解,因为线程在 sleep 的时候可能是处于同步代码块的中间位置,如果此时把锁放弃,就违背了同步的语义。所以 sleep 时并不会放弃锁,等过了 sleep 时长后,可以确保后面的逻辑还在同步执行。 关于这个方法在源码中有两个重载方法

public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException

这里需要注意的是,虽然第二个重载方法,支持传入一个纳秒参数,但是实际上,sleep方法内部是不支持纳秒级别的睡眠时间的,看源码,一目了然

public static void sleep(long millis, int nanos)
throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }
    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }
    sleep(millis);
}

可以清楚的看到,最终调用的还是第一个毫秒级别的 sleep 方法。而传入的纳秒会被四舍五入。如果大于 50 万,毫秒 ++,否则纳秒被省略。

yield 方法

yield 方法我们平时并不常用。yield 单词的意思是让路,在多线程中意味着本线程愿意放弃 CPU 资源,也就是可以让出 CPU 资源。不过这只是给 CPU 一个提示,当 CPU 资源并不紧张时,则会无视 yield 提醒。如果 CPU 没有无视 yield 提醒,那么当前 CPU 会从 RUNNING 变为 RUNNABLE 状态,此时其它等待 CPU 的 RUNNABLE 线程,会去竞争 CPU 资源。讲到这里有个问题,刚刚 yield 的线程同为 RUNNABLE 状态,是否也会参与竞争再次获得 CPU 资源呢?经过我大量测试,刚刚 yield 的线程是不会马上参与竞争获得 CPU 资源的,请记住这一点。

public class YieldExampleClient {

    public static void main(String[] args) {
        Thread xiaoming = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("小明--" + i);
//                if (i == 2) {
//                    Thread.yield();
//                }
            }
        });

        Thread jianguo = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("建国--" + i);
            }
        });

        xiaoming.start();
        jianguo.start();
    }
}

每次结果有所区别,但是一般都是小明输出到 5 以后,建国才开始输出。一是因为线程启动需要时间,另外也是因为 CPU 紧张, jianguo 线程在排队。

当放开小明线程注解部分,让输出到 xiaoming 线程输出到 2 的时候 yield ,结果就变成了小明打印到 2 的时候,就会打印建国,由此可见在cpu紧张时候yield会让出cpu使用权,不过如果 CPU 资源丰富,那么会无视 yield 方法,xiaoming 也无需让出 CPU 资源。

yield 方法为了提升线程间的交互,避免某个线程长时间过渡霸占 CPU 资源。但 yield 在实际开发中用的比较少,源码的注解也提到这一点:“It is rarely appropriate to use this method.”。

currentThread 方法

此方法用于获取当前线程的实例。用法很简单,如下:

Thread.currentThread();

拿到线程的实例后,我们还可以获取 Thread 的名称:

Thread.currentThread().getName();

此外我们还可以获取线程 ID :

Thread.currentThread().getId();

setPriority 方法

此方法用于设置线程的优先级。每个线程都有自己的优先级数值,当 CPU 资源紧张的时候,优先级高的线程获得 CPU 资源的概率会更大。请注意仅仅是概率会更大,并不意味着就一定能够先于优先级低的获取。这和摇车牌号一个道理,比如我现在中签概率是标准的9倍,但摇中依然摇摇无期。而身边却时不时的出现第一次摇号就中的朋友。如果在 CPU 比较空闲的时候,那么优先级就没有用了,人人都有肉吃,不需要摇号了。

优先级别高可以在大量的执行中有所体现。在大量数据的样本中,优先级高的线程会被选中执行的次数更多。

join 方法

这个方法功能强大,也很实用。我们用它能够实现并行化处理。比如主线程需要做两件没有相互依赖的事情,那么可以起 A、B 两个线程分别去做。通过调用 A、B 的 join 方法,让主线程 block 住,直到 A、B 线程的工作全部完成,才继续走下去,直接看下实现代码吧

public class JoinClient {
    public static void main(String[] args) throws InterruptedException {
        Thread backendDev = createWorker("backed dev""backend coding");
        Thread frontendDev = createWorker("frontend dev""frontend coding");
        Thread tester = createWorker("tester""testing");

        backendDev.start();
        frontendDev.start();
    
        backendDev.join();
        frontendDev.join();
      
        tester.start();
    }

    public static Thread createWorker(String role, String work) {
        return new Thread(() -> {
            System.out.println("I finished " + work + " as a " + role);
        });
    }
}

这种情况下,主线程会阻塞住,等待backendDev和frontendDev执行完,才会执行tester.start()代码。

interrupt 相关方法

在Thread类里面有三个相关的方法

private native boolean isInterrupted(boolean ClearInterrupted);

public boolean isInterrupted() {
        return isInterrupted(false);
    }
    
public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

interrupt:成员方法,设置线程可中断状态为ture;

isInterrupted:成员方法,获取线程可中断状态;

interrupted:静态方法,获取可中断状态,如果为true,则将其还原为false。

其实三个方法很简单,网上一些资料说的有些复杂,我仔细探究了一下,interrupt方法只不过是给线程标记可中断状态为true,就像是告诉其他人,我现在已经处理业务完毕,可以被中断了,但是在真正中断前还是处于运行的,所谓中断,我们大家应该都知道能够让线程中断的方式有哪些,我们这里列举下:

  • wait();
  • sleep();
  • park();

当调用这些方法的时候,线程才会真正中断,并且会把线程的中断状态还原为false。

还有一种情况是如果线程已经处于睡眠状态,比如调用了sleep或者wait方法,在此时调用interrupt方法,会中断正在执行的sleep方法,并且抛出异常,可中断状态的不会改变,依然为false,这里贴出验证的代码,可以自行去验证:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            while (1==1){
                System.out.println("I'm doing my work");
                try {
                    System.out.println("I will sleep");
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    System.out.println("My sleeping was interrupted");
                }
                System.out.println("I'm interrupted?"+Thread.currentThread().isInterrupted());
               // System.out.println("I'm interrupted?"+Thread.interrupted());
            }
        });

        thread.start();
        Thread.sleep(3000);
        thread.interrupt();

    }

总结一下interrupt方法:

调用此方法,可以设置线程的可中断状态为true,意图是让别人知道自己的已经可以中断了,但是如果线程正处于中断中,例如正在调用sleep方法,那么此时调用interrupt方法,会使线程在中断中苏醒,继续工作。

能够让线程可中断状态变为true的场景只有非中断的时候调用interrupt方法。

这里必须区分中断和可中断两个概念

讲到这里你可能会问这几个方法有什么用的,其实在应用中,高级的工程师在写一些业务的时候可能会用到,一般的java工程师很少会涉及到这些方法的应用,但是请注意,并发编程中凡是涉及到多线程应用,就免不了会有线程之间的通信,那么这里的可中断状态,就是一种线程之间通信的方式,在后面讲并发编程工具源码的时候,会看到大量的对interrupt相关方法的应用,这里不过多讲解。

到此,已经把线程基础讲完,本篇只为后面并发编程奠定了基础

本篇文章来自慕课网某专栏文档,需要更多慕课网,即可时间,各种机构音视频文档资料可在公众号找我索要

UNREMITTINGLY

2021/12/06  阅读:49  主题:雁栖湖

作者介绍

UNREMITTINGLY