Java线程中断机制详细讲解 - 从API到具体代码案例

前言

AQS底层原理用到了线程中断机制,此处我们通过具体案例分析什么是Java线程的中断机制。

Java线程的中断机制

1.1 概述

想要更好的理解AQS原理,需要先来了解什么是:Java线程的中断机制。

注意:synchronized是没有中断机制的,因为它的底层是c++代码写的,无法用Java代码进行调用。

Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断

1.2 中断机制的API

1.2.1 三个API

● interrupt():不会中断线程,只是将线程的中断标志设置为true,相当于发送了一个中断线程信号(Just to set the interrupt flag)

 /**
     * Interrupts this thread.
     */
    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();
    }

● interrupted():判断当前线程的中断标志是否为true,然后无论如何都将中断标志清空(reset),设置为false

 /**
     * Tests whether the current thread has been interrupted.  The
     * interrupted status 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).
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    /**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */
    private native boolean isInterrupted(boolean ClearInterrupted);

● Thread.currentThread().isInterrupted():判断当前线程的中断标志是否为true,但是不清空中断标志

/**
 * Tests whether this thread has been interrupted.  The interrupted
 * status of the thread is unaffected by this method.
 */
public boolean isInterrupted() {
    return isInterrupted(false);
}
1.2.2 如何优雅的中断一个线程

面试题:在JAVA中如何优雅的停止一个线程?

思考1:我们可以设置一个已关闭的标志位,当任务或者线程运行的时候先判断标志位的状态,如果是已经关闭那个这个任务或者线程就直接结束,不过这个标志位需要用volatile关键字修饰,否则可能其他线程已经修改了任务可能仍然在运行。
这种方法可以解决一部分问题,但是当任务可能会被阻塞的时候就会出现问题,就像之前的生产者、消费者模式,如果生产者通过循环往队列里面加元素,在每次循环之前都要判断中断标志位,如果结束了就不往队列中put数据了,当消费者在某些情况下可能不在消费数据所以会设置标志位为已结束。此时如果阻塞队列是满的,而刚好生产者在put阻塞中,由于消费者不在消费,生产者线程就会永远处于阻塞状态。
Java线程中断机制详细讲解 - 从API到具体代码案例_第1张图片

思考2:用Thread.stop()
建议:千万不要那么用,Thread.stop是@deprecated的方法,它注释中有一句话 Forces the thread to stop executing. 比如,当线程刚刚获取了一个锁资源,直接把线程停掉,锁未释放,意味着其他线程都无法获取锁资源了,造成了很严重的死锁问题;再比如任务未完成,直接把线程停掉,会带来脏数据的很多问题。

思考3:调用suspend()和resume()方法
废弃原因(再写具体一点):太过暴力,可能会导致一些清理工作不会完成
调用后会直接释放锁,可能会导致数据不同步的问题https://www.jianshu.com/p/e0ff2e420ab6

答案是 :用线程中断机制

来看一个例子:

package thread;
public class ThreadInterruptTest {
    static int i = 0;
    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    i ++;
                    System.out.println(i);
                    //100毫秒之后才能捕获到中断状态
                    if (Thread.interrupted()) {
                        System.out.println("======");
                        break; // 需要主动中断,break
                    }
                }
            }
        });
        t1.start();
        try {
            //让主线程休眠100毫秒之后,再去让t1线程发出中断信号。
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 发送中断线程信号
        t1.interrupt();
    }
}

输出结果

...
17917
17918
17919
======

解释:t1.start(); 启动线程,t1.interrupt(); 发送中断线程信号,Thread.interrupted() 检测到信号,break循环主动中断。

再来看一个例子:

package thread;
public class ThreadInterruptTest {
    static int i = 0;
    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    i ++;
                    System.out.println(i);
                    if (Thread.interrupted()) {
                        System.out.println("======");
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });
        // 启动线程
        t1.start();
        // 发送中断线程信号
        t1.interrupt();
    }
}

输出结果

begin
1
======
2
3
4
5
6
7
8
9
10

解释:t1.interrupt();设置了中断标志为true,但Thread.interrupted()执行后,会将中断标志设回false,因此只输出了一行======

再来看一个例子
package thread;
public class ThreadInterruptTest {
    static int i = 0;
    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    i ++;
                    System.out.println(i);
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("======");
                        //break; // 需要主动中断,break
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });
        // 启动线程
        t1.start();
        // 发送中断线程信号
        t1.interrupt();
    }
}

输出结果

begin
1
======
2
======
3
======
4
======
5
======
6
======
7
======
8
======
9
======
10
======

解释:t1.interrupt();设置了中断标志为true,Thread.interrupted()执行后,中断标志依然为true,不会清空,因此输出了10行======
Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。

1.2.3 sleep和wait

思考一下,Thread.sleep 和 wait(time) 可以感知中断信号吗?
来看一个例子:

package thread;

public class ThreadInterruptTest {

    static int i = 0;

    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    i ++;
                    System.out.println(i);

                    try {
                        // 会感知到中断信号,且清除中断标志位(会把标志位设为false)
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
               
                    try {
                        // 会感知到中断信号,且清除中断标志位(会把标志位设为false)
                        wait(1000); //wait必须结合synchronized锁使用,所以11行加了锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // Thread.interrupted() 清除中断标志位
                    // Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.interrupted()) {
                        System.out.println("======");
                        break; // 需要主动中断,break
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });

        // 启动线程
        t1.start();
        // 发送中断线程信号
        t1.interrupt();
    }
}

输出结果:

begin
1
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.ThreadInterruptTest$1.run(ThreadInterruptTest.java:18)
	at java.lang.Thread.run(Thread.java:748)
2
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at thread.ThreadInterruptTest$1.run(ThreadInterruptTest.java:25)
	at java.lang.Thread.run(Thread.java:748)
3
4
5
6
7
8
9
10

解释:Thread.sleep 和 wait(time) 都可以感知到中断信号,且会清除中断标志位(都没有进入到Thread.interrupted()代码块)。
补充一个小知识点:System.out 是有缓冲区的,System.err没有缓冲区。

1.2.3 LockSupport.park()

那么 LockSupport.park() 可以感知中断信号吗?

提示: LockSupport.park()和unpark()是AQS中等待唤醒使用的。
LockSupport.park() 可以感知中断信号,且不会清除中断标志位(注意,是不会)

package thread;

import java.util.concurrent.locks.LockSupport;

public class ThreadInterruptTest {

    static int i = 0;

    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    i ++;
                    System.out.println(i);

                    LockSupport.park();//先是线程阻塞,当调用了interrupt后,线程被唤醒。

                    // Thread.interrupted() 清除中断标志位
                    // Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.interrupted()) {
                        System.out.println("======");
                        break; // 需要主动中断,break
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });

        // 启动线程
        t1.start();
        // 发送中断线程信号
        t1.interrupt();
    }
}

输出结果:

begin
1
======

解释:LockSupport.park() 可以感知中断信号,且不会清除中断标志位(注意,是不会)

1.2.4 Park阻塞线程唤醒有两种方式:
1、中断;
2、release(),底层是unPark()。

为什么必须记住park被唤醒的两种方式,因为AQS里面不仅仅有park和unpark,还有interrupt。

将线程唤醒,线程继续执行代码。如果线程又遇到park,那么该线程又会陷入阻塞状态。

@Slf4j
public class Juc01_Thread_LockSupport {

    public static void main(String[] args) {

        Thread t0 = new Thread(new Runnable() {
            @Override
            public void run() {
                Thread current = Thread.currentThread();
                log.info("{},开始执行!",current.getName());
                for(;;){//spin 自旋
                    log.info("准备park住当前线程:{}....",current.getName());
                    LockSupport.park();
                    System.out.println(Thread.interrupted());
                    log.info("当前线程{}已经被唤醒....",current.getName());

                }
            }
        },"t0");

        t0.start();

        try {
            Thread.sleep(2000);
            log.info("准备唤醒{}线程!",t0.getName());
            LockSupport.unpark(t0);
            Thread.sleep(2000);
            t0.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
14:11:45.216 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - t0,开始执行!
14:11:45.222 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备park住当前线程:t0....
    //2s后
14:11:47.214 [main] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备唤醒t0线程!
false
14:11:47.214 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 当前线程t0已经被唤醒....
14:11:47.214 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备park住当前线程:t0....
    //2s后
true
14:11:49.214 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 当前线程t0已经被唤醒....
14:11:49.214 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备park住当前线程:t0....

注意:
代码调用了interrupt后,会让unpark()/unpark(Thread t)失效(相当于代码中没有写unpark);
但是如果又调用了Thread.interrupted()后,unparkunpark()/unpark(Thread t)会再生效(再次阻塞)。

把上面的案例中:注释掉Thread.interrupted(),重新输出一遍就清楚了。

小结

线程中断机制并不是直接停止一个线程,而是使用 thread.interrupt() 方法发送中断信号,然后在线程中需要感知到中断信号,并进行适当的处理。这就是优雅的中断线程的方法。

你可能感兴趣的:(Java基础使用积累,java,开发语言,后端,多线程)