高并发编程从入门到精通(四)

1,386 阅读7分钟

面试中最常被虐的地方一定有并发编程这块知识点,无论你是刚刚入门的大四萌新还是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程中的笔记和总结,并提供调试代码供大家玩耍

上章回顾

1.sleep对象的monitor lock有释放吗?如何证明?

2.sleep(0)是什么意思?

3.线程如何正常关闭?

4.join内部是通过那个函数来实现顺序执行的?

请自行回顾以上问题,如果还有疑问的自行回顾上一章哦~

本章提要

本章内容本来是要和上一章一起讲的,但是由于篇幅过长涉及到的内容较多,并且也有很多实战代码的调试,所以决定还是另开一章来讲比较好一些。本章主要介绍interrupt这个API。学习完成,同学们会对interrupt有深入了解,明白其内部的处理机制,同时对beginend也有较为深刻的认识。

本章代码下载

Interrupt

在本章中我们一共要讲解与interrupt相关的三个API方法,这个API我学的时候也是费了不少劲,所以相对来说会多写一点,同学们先泡杯茶慢慢看呀~😄

  • public void interrupt()
  • public boolean isInterrupted()
  • public static boolean interrupted()

interrupt

老套路,直接上API接口说明

     * @revised 6.0
     * @spec JSR-51
     */
    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();
    }

这么长篇幅的介绍莫怕,我们一起来看这货到底在说什么鸟语。

首先,第一段句概括了这个接口的作用中断此线程

    * Interrupts this thread.

第二段

    //除非当前线程自己把自己中断了,否则的话会一直允许checkAccess()调用
    //并且在调用的时候可能还会抛出SecurityException异常
     * <p> Unless the current thread is interrupting itself, which is
     * always permitted, the {@link #checkAccess() checkAccess} method
     * of this thread is invoked, which may cause a {@link
     * SecurityException} to be thrown.

这个checkAccess()我们这边不做深入探讨,我们只要知道这是一个校验接口,打个比方,比如在中断JVM自身线程的时候,它就会去查询是否有足够权限来执行这个命令

第三段

含义就是线程在调用下列方法之后可以调用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方法

  • 其他方法

第四段

//在线程由于I/O操作而导致阻塞的时候,阻塞通道会被关闭,然后线程中断标志被设置
//为true并且同时会收到ClosedByInterruptException异常
 * <p> If this thread is blocked in an I/O operation upon an {@link
     * java.nio.channels.InterruptibleChannel InterruptibleChannel}
     * then the channel will be closed, the thread's interrupt
     * status will be set, and the thread will receive a {@link
     * java.nio.channels.ClosedByInterruptException}.

这部分的内容后面会用一些篇幅来进一步说明,这里就不展开来讲。

第五段

//当前线程由于调用Selector选择器方法而阻塞的时候,线程中断标志会被设置为true
//并且当前线程会立马返回
  * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
     * then the thread's interrupt status will be set and it will return
     * immediately from the selection operation, possibly with a non-zero
     * value, just as if the selector's {@link
     * java.nio.channels.Selector#wakeup wakeup} method were invoked.

最后一段

//中断一个不活跃的线程不会产生任何结果
Interrupting a thread that is not alive need not have any effect.

好到这里大家应该已经等不及上手一试了,我们先来一个常规的中断sleep的操作

private static class MySleep implements Runnable {

    @Override
    public void run() {
      System.out.println("我开始睡觉了,谁也别来吵我");
      try {
        TimeUnit.HOURS.sleep(12);
      } catch (InterruptedException e) {
        System.out.println("你代码炸啦!!!");
        e.printStackTrace();
      }
      System.out.println("起床");
    }
  }

  public static void main(String[] args) {
    Thread thread = new Thread(new MySleep());
    thread.start();
    //调用中断方法
    thread.interrupt();
  }

输出:

我开始睡觉了,谁也别来吵我
你代码炸啦!!!
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at src.com.lyf.page3.InterruptSleep$MySleep.run(InterruptSleep.java:19)
	at java.lang.Thread.run(Thread.java:748)
起床

可以看到这边我们打算睡它个12小时,但是好景不长,由于代码炸了,我们只能立马起床干活。好的这是一个悲伤的故事,希望大家都不要遇到😂。

这边我们注意到,在调用interrupt中断了sleep之后,我们的线程接收到了一个java.lang.InterruptedException异常,这里需要我们对异常做友好处理。无论你是try起来还是直接往外抛,但是捕获一定要做好,不然我们对日志会出现一堆java.lang.InterruptedException异常。


以上我们已经基本学会了interrupt的使用,现在我们追踪到方法内部看看做了哪些操作。这里我们把这段代码分成三部分来讲。

 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();
    }

(1)判断中断的线程和Thread.currentThread()指向是不是同一个

        if (this != Thread.currentThread())
            checkAccess();

判断当前中断的线程是不是自己,很多同学这里可能会有疑问,thisThread.currentThread()难道指向的不是同一个对象吗?

我们用测试代码来试验一下

private static class MySleep implements Runnable {

    @Override
    public void run() {
      System.out.println("下班啦,我要睡觉了,谁也别来吵我");
      try {
        TimeUnit.HOURS.sleep(12);
      } catch (InterruptedException e) {
        System.out.println("你代码炸啦!!!");
//        e.printStackTrace();
      }
      System.out.println("起床");
    }
  }


  public static void main(String[] args) {

    Thread t1 = new Thread(() -> {
      Thread t2 = new Thread(new MySleep(), "一号");
      t2.start();
      t2.interrupt();
    }, "二号");
    t1.start();
  }

打断点我们可以看到结果

这里的作用其实就是判断一号线程是不是在二号线程内部执行的,如果是那么一号线程就是依托与二号线程而存活的,所以这里需要调用checkAccess()方法来进行校验。

(2)判断是否是I/O阻塞而触发的线程中断

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

不管是调用sleep还是wait我们都会发现始终不会进入到这块到逻辑代码里面,原因就是blocker是null并没有被设值。那么blocker到底是什么时候被设值到呢?

⚠️方法论来了

当接口内部有部分代码调试不到或者代码无法理解当时候,我们可以回过头去看接口说明。

这里我们再来看下这段说明

//在线程由于I/O操作而导致阻塞的时候,阻塞通道会被关闭,然后线程中断标志被设置
//为true并且同时会收到ClosedByInterruptException异常
 * <p> If this thread is blocked in an I/O operation upon an {@link
     * java.nio.channels.InterruptibleChannel InterruptibleChannel}
     * then the channel will be closed, the thread's interrupt
     * status will be set, and the thread will receive a {@link
     * java.nio.channels.ClosedByInterruptException}.

这里我们给同学们提供一个案例来调试这块代码,代码内容如下:

//src.com.lyf.page3.InterruptNIO
public static void main(String[] args) throws IOException {
    ServerSocket server = new ServerSocket(8080);
    InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
    SocketChannel sc1 = SocketChannel.open(isa);
    SocketChannel sc2 = SocketChannel.open(isa);

    Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
    t1.start();

    Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
    t2.start();
    //打断标志
    t1.interrupt();
    t2.interrupt();
  }
//src.com.lyf.page3.NIOBlockExample
private final SocketChannel socketChannel;

  public NIOBlockExample(SocketChannel sc) {
    this.socketChannel = sc;
  }

  @Override
  public void run() {
    try {
      System.out.println("Waiting for read() in " + this);
      socketChannel.read(ByteBuffer.allocate(1));
    } catch (ClosedByInterruptException e) {
      e.printStackTrace();
      System.out.println("ClosedByInterruptException");
    } catch (AsynchronousCloseException e) {
      e.printStackTrace();
      System.out.println("AsynchronousCloseException");
    } catch (IOException e) {
      e.printStackTrace();
      throw new RuntimeException(e);
    }
    System.out.println("Exiting NIOBlocked.run() " + this);
  }

这里代码比较多建议大家对照src.com.lyf.page3.InterruptNIOsrc.com.lyf.page3.NIOBlockExample一边调试一边喝茶来细品下面这块内容。

(1) 创建NIOBlockExample

首先我们创建一个NIOBlockExample线程对象,通过调用SocketChannel.read来模拟可能产生I/O阻塞的场景,这里同学们不需要过于关注SocketChannel.read这个方法,我们这边只需要知道这是一个读操作就行,这里我们的目的是为了模拟I/O阻塞场景来调试interrupt代码。

(2) 初始化服务端和通道信息
    ServerSocket server = new ServerSocket(8080);
    InetSocketAddress isa = new InetSocketAddress("localhost", 8080);

模拟开启8080端口,然后配置套接字通道信息为本地的8080端口。

(3) 开启两个通道,用两个线程来掉用
SocketChannel sc1 = SocketChannel.open(isa);
    SocketChannel sc2 = SocketChannel.open(isa);

    Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
    t1.start();

    Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
    t2.start();
(4)调用interrupt
    //打断标志
    t1.interrupt();
    t2.interrupt();

断点打在if判断条件上,我们可以看到blocker指向的是一个AbstractInterruptibleChannel抽象类对象,那么我们初步可以断定,应该是AbstractInterruptibleChannel中的某个类似的set方法来完成对blocker对赋值工作的

所以我们继续往下追踪到java.nio.channels.spi.AbstractInterruptibleChannel,同学们可以通过

这段代码来往下追踪

老套路先看他一波API说明

//可中断通道的基本实现类
/**
 * Base implementation class for interruptible channels.
 *
//方法在可能出现I/O阻塞的情况下必须先调用begin,然后再调用end,并且是需要在一
//个try{}finally{}里面来完成这些通道的开启和关闭
 * <p> This class encapsulates the low-level machinery required to implement
 * the asynchronous closing and interruption of channels.  A concrete channel
 * class must invoke the {@link #begin begin} and {@link #end end} methods
 * before and after, respectively, invoking an I/O operation that might block
 * indefinitely.  In order to ensure that the {@link #end end} method is always
 * invoked, these methods should be used within a
 * <tt>try</tt>&nbsp;...&nbsp;<tt>finally</tt> block:

这里我们了解到这个抽象类是一个可中断通道的基本实现类,并且API说明里面也给我们提供了调用的代码模版

 * boolean completed = false;
 * try {
 *     begin();
 *     completed = ...;    // 执行阻塞I/O操作
 *     return ...;         // 返回结果
 * } finally {
 *     end(completed);

在这里我们可以看到之前我们的SocketChannel.read对应这边的应该是执行阻塞I/O操作,也就是说,在我们中断方法之前,该类已经调用了begin方法了。同学们这里可能会感觉很奇怪,他是怎么做到的。我们来看下SocketChannel的继承关系就一目了然了

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel

可以看到SocketChannel继承了AbstractSelectableChannel抽象类,这样一切都顺理成章了。

接下来我们再来看下begin到底是何方神圣

/**
    //标记一个可能发生I/O阻塞的操作的开始
     * Marks the beginning of an I/O operation that might block indefinitely.
     *
     //该方法需要和end方法同步调用,为了实现异步阻塞通道的关闭和中断
     * <p> This method should be invoked in tandem with the {@link #end end}
     * method, using a <tt>try</tt>&nbsp;...&nbsp;<tt>finally</tt> block as
     * shown <a href="#be">above</a>, in order to implement asynchronous
     * closing and interruption for this channel.  </p>
     */
    protected final void begin() {
        if (interruptor == null) {
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (!open)
                                return;
                            open = false;
                            interrupted = target;
                            try {
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }

断点打在同步块入口处

此时我们确定interrupt甚至还没有开始打标志,但是这时候我们发现blocker已经被赋值了,这里就证明了,在阻塞I/O操作之前会初始化blocker状态。也就是说在还没有调用interrupt之前就已经调用了begin。我们这时候断点打在begin,一探究竟。

同学们注意看我红色方框标注出来的地方,我们在begin入口处打断点,发现这边发掉调用的是我们最初的open方法,并且我们也看到这边发起调用的是我们的主程序main函数

所以我们可以得出结论:在open函数调用的时候会初始化一个new Interruptible()对象,并且是由parent Thread 来创建的。

⚠️这里有个巨坑

当我们把断点打在begin之后,debug的时候会发现和我们直接run的结果完全不同,直接上图大家看下

debug情况下的结果:

Connected to the target VM, address: '127.0.0.1:52529', transport: 'socket'
Exception in thread "main" java.nio.channels.ClosedByInterruptException
	at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
	at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:659)
	at java.nio.channels.SocketChannel.open(SocketChannel.java:189)
	at src.com.lyf.page3.InterruptNIO.main(InterruptNIO.java:50)
Disconnected from the target VM, address: '127.0.0.1:52529', transport: 'socket'

run情况下的结果:

Waiting for read() in src.com.lyf.page3.NIOBlockExample@168b9408
Waiting for read() in src.com.lyf.page3.NIOBlockExample@740759f
一号你炸啦!!!!!!!!!!!!
二号你炸啦!!!!!!!!!!!!
java.nio.channels.ClosedByInterruptException
	at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
	at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:407)
	at src.com.lyf.page3.NIOBlockExample.run(NIOBlockExample.java:26)
	at java.lang.Thread.run(Thread.java:748)
一号 = ClosedByInterruptException
java.nio.channels.ClosedByInterruptException
一号 = Exiting NIOBlocked.run() src.com.lyf.page3.NIOBlockExample@168b9408
	at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
	at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:407)
	at src.com.lyf.page3.NIOBlockExample.run(NIOBlockExample.java:26)
	at java.lang.Thread.run(Thread.java:748)
二号 = ClosedByInterruptException
二号 = Exiting NIOBlocked.run() src.com.lyf.page3.NIOBlockExample@740759f

为什么会这样呢?为什么呢?我也是调试来很久,最后发现如果断点打在begin,会导致main线程被阻塞,从而导致socket通道关闭。也就是说我们的断点是不能阻断main线程的运行的。光说结论没有事实可不行,这里我们改造一下我们的代码。

 ServerSocket server = new ServerSocket(8080);
    InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
    SocketChannel sc1 = null;
    SocketChannel sc2 = null;
    try {
      sc1 = SocketChannel.open(isa);
      sc2 = SocketChannel.open(isa);
    } catch (ClosedByInterruptException e) {

    }

    Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
    t1.start();

    Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
    t2.start();
    //打断标志
    t1.interrupt();
    t2.interrupt();

open方法无耻地try起来,这时候我们再次debug就能看到结果如下:

同时我们把begin不打断点打结果图拿来对比着看

同学们这时候应该已经可以确信我们上面得出的结论了吧,也就是如果断点打在begin,会导致main线程被阻塞,从而导致socket通道关闭,这里我们可以清楚地看到sc1sc2都为null。

好的巨坑已经给同学们填好了终于可以放心地debug了😄


我们现在再回到我们的begin来讲,在open的时候帮我们初始化了对象之后,紧接着他调用了blockedOn(interruptor);,鉴于不能把main阻塞的原则我们在断点上添加过滤条件!"main".equals(Thread.currentThread().getName()),继续在blockedOn(interruptor);处打断点,我们可以看到一号线程和二号线程都会进来。

终于我们可以确定是blockedOn函数帮助我们完成了private volatile Interruptible blocker;的赋值,关于具体这么实现的,这里我们不再深入探究,感兴趣的同学可以自行查看System内部相关代码。

我们既然讲了begin,顺便我们把java.nio.channels.spi.AbstractInterruptibleChannel#end也讲一讲。

/**
    标志I/O操作的结束
     * Marks the end of an I/O operation that might block indefinitely.
     *
     //确保begin和end一起调用
     * <p> This method should be invoked in tandem with the {@link #begin
     * begin} method, using a <tt>try</tt>&nbsp;...&nbsp;<tt>finally</tt> block
     * as shown <a href="#be">above</a>, in order to implement asynchronous
     * closing and interruption for this channel.  </p>
     *
     * @param  completed
     *         <tt>true</tt> if, and only if, the I/O operation completed
     *         successfully, that is, had some effect that would be visible to
     *         the operation's invoker
     *
     * @throws  AsynchronousCloseException
     *          If the channel was asynchronously closed
     *
     * @throws  ClosedByInterruptException
     *          If the thread blocked in the I/O operation was interrupted
     */
    protected final void end(boolean completed)
        throws AsynchronousCloseException
    {
        blockedOn(null);
        Thread interrupted = this.interrupted;
        if (interrupted != null && interrupted == Thread.currentThread()) {
            interrupted = null;
            throw new ClosedByInterruptException();
        }
        if (!completed && !open)
            throw new AsynchronousCloseException();
    }

这边给我们提供了两种异常处理

1.AsynchronousCloseException 只有在通道被异步关闭的时候会抛出这个异常。

2.ClosedByInterruptException 线程因为I/O操作而导致Blocked的情况下会抛出这个异常。

结合我们之前main打断点的而导致通道被关闭抛出ClosedByInterruptException异常可知我们的断点其实是会导致main被blocked中断了导致的I/O异常。


关于interruptAPI我们也研究地差不多了,这里问同学们一个问题,我们一旦调用interrupt之后,线程就会立马被中断吗?

答案是不会的,我们来看个例子

private static class MySleep implements Runnable {

    @Override
    public void run() {
      try {
        TimeUnit.SECONDS.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      for (int i = 0; i < 10; i++) {
        System.out.println("我一直都在");
      }
    }
  }

  public static void main(String[] args) {

    Thread thread = new Thread(new MySleep(), "我是个没有感情的标签");
    thread.start();
    System.out.println("我开始打断你");
    thread.interrupt();
    System.out.println("你已经被我打断了");

  }

输出:

我开始打断你
你已经被我打断了
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at src.com.lyf.page3.InterruptIsJustSign$MySleep.run(InterruptIsJustSign.java:17)
	at java.lang.Thread.run(Thread.java:748)
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在

我们可以看到即使被中断了sleep线程还是会把剩下的逻辑执行完成,也就是说interrupt是一个标志,线程本身是不是中断停止还是要看线程本身是否结束。我们的interrupt只作用于我们之前列出的那几类方法。

到这里我们的interrupt大致就介绍完了

isInterrupted

老套路

 /**
    //查看线程有没有被中断,此方法不会影响线程的中断状态
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     //线程中断被忽略的时候此方法一直返回false
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }

这个方法其实就是Thread的一个成员方法,相对来说比较简单,但是再简单我们也要看下到底是怎么个用法,下面给出对应的使用案例。

  private static class MyInterrupted implements Runnable {

    @Override
    public void run() {
      System.out.println("我已经被启动了");
      try {
        TimeUnit.SECONDS.sleep(10);
      } catch (InterruptedException e) {
        System.out.println("我被中断了");
      }
    }
  }

  public static void main(String[] args) {
    Thread thread = new Thread(new MyInterrupted(), "一号");
    System.out.println("线程未启动时候中断标志:" + thread.isInterrupted());
    thread.start();
    System.out.println("线程启动时候中断标志:" + thread.isInterrupted());
    thread.interrupt();
    System.out.println("线程启动并调用interrupt时候中断标志:" + thread.isInterrupted());
    
  }

输出:

线程未启动时候中断标志:false
线程启动时候中断标志:false
线程启动并调用interrupt时候中断标志:true
我已经被启动了
我被中断了

interrupted

 /**
    测试当前线程是否已经被中断,这个方法会清除中断标志,也就是第一次调用是
    中断之后第一次调用会返回true,除非再次中断不然返回都是false
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

isInterrupted的区别就是interrupted会清除中断状态,这个我们用具体代码来理解一下

public static void main(String[] args) {

    System.out.println("调用中断方法之前isInterrupted()标记为:" + Thread.currentThread().isInterrupted());
    System.out.println("调用中断方法之前interrupted标记为:" + Thread.interrupted());
    Thread.currentThread().interrupt();
    System.out.println("调用中断方法之后isInterrupted()标记为,第一次输出:" + Thread.currentThread().isInterrupted());
    System.out.println("interrupted()标记为,第一次输出:" + Thread.interrupted());
    System.out.println("isInterrupted()标记为,第二次输出:" + Thread.currentThread().isInterrupted());
    System.out.println("interrupted()标记为,第二次输出:" + Thread.interrupted());
  }

输出:

调用中断方法之前isInterrupted()标记为:false
调用中断方法之前interrupted标记为:false
调用中断方法之后isInterrupted()标记为,第一次输出:true
interrupted()标记为,第一次输出:true
isInterrupted()标记为,第二次输出:false
interrupted()标记为,第二次输出:false

前面两段很好理解,在没有调用interrupt中断线程之前,线程中断状态返回默认都是false,第三行和第四行也好理解表示调用中断方法之后,线程的中断状态变为true。我们主要来看第五第六行,我们发现调用了interrupted()之后,线程状态编程了false,这也印证了我们开头说的interupted会清除中断状态


终于把这块写出来了,本章的重点都在调试interrupt这块内容上,虽然比较繁琐,但是同学们如果自己也能亲手跟着我的代码一起去探索一下的话相信会给大家带来很大的提升。最后希望同学们点点赞,点点关注呀~~🙏