Java——聊聊JUC中的线程中断机制 & LockSupport

Posted 宋宋_浩浩_Java工程师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——聊聊JUC中的线程中断机制 & LockSupport相关的知识,希望对你有一定的参考价值。

文章目录:

1.什么是中断机制?

2.如何停止中断运行中的线程?

2.1 通过一个volatile变量实现

2.2 通过AtomicBoolean原子布尔类

2.3 通过Thread类自带的中断API方法实现

3.Thread类的三大API说明

3.1 实例方法interrupt(),没有返回值

3.2 实例方法isInterrupted(),返回布尔值

3.3 当前线程的中断标识为true,是不是线程就立刻停止?

3.4 在3.3中断程序的基础上,添加sleep睡眠

3.5 静态方法public static boolean interrupted()

4.LockSupport

4.1 线程的等待唤醒机制

4.2 wait、notify

4.3 await、signal

4.4 park、unpark


1.什么是中断机制?

  • 首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
  • 其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。

                  因此,Java提供了一种用于停止线程的协商机制——中断。

                  中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

                  若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;

                  接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

                  每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;

                  通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

尚硅谷周阳老师的例子:顾客在无烟餐厅中吸烟,服务员希望他别吸烟了,不是强行停止他吸烟,而是给他的标志位打为true,具体的停止吸烟还是要顾客自己停止。(体现了协商机制)

  • 中断相关的三大API方法如下图:↓↓↓


2.如何停止中断运行中的线程?

2.1 通过一个volatile变量实现

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo1 
    static volatile boolean isStop = false;

    public static void main(String[] args) 
        new Thread(() -> 
            while (true) 
                if (isStop) 
                    System.out.println(Thread.currentThread().getName() + " isStop被修改为true,线程停止");
                    break;
                
                System.out.println(Thread.currentThread().getName() + " hello volatile....");
            
        , "t1").start();

        try 
            TimeUnit.MILLISECONDS.sleep(20);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            isStop = true;
        , "t2").start();
    

2.2 通过AtomicBoolean原子布尔类

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class InterruptDemo2 
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);

    public static void main(String[] args) 
        new Thread(() -> 
            while (true) 
                if (atomicBoolean.get()) 
                    System.out.println(Thread.currentThread().getName() + " isStop被修改为true,线程停止");
                    break;
                
                System.out.println(Thread.currentThread().getName() + " hello AtomicBoolean....");
            
        , "t1").start();

        try 
            TimeUnit.MILLISECONDS.sleep(20);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            atomicBoolean.set(true);
        , "t2").start();
    

2.3 通过Thread类自带的中断API方法实现

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo3 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            while (true) 
                if (Thread.currentThread().isInterrupted()) 
                    System.out.println(Thread.currentThread().getName() + " isStop被修改为true,线程停止");
                    break;
                
                System.out.println(Thread.currentThread().getName() + " hello isInterrupted....");
            
        , "t1");
        t1.start();

        try 
            TimeUnit.MILLISECONDS.sleep(20);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            t1.interrupt();
        , "t2").start();
    


3.Thread类的三大API说明

3.1 实例方法interrupt(),没有返回值

这个interrupt()实例方法,底层实际上调用了interrupt0()这个方法,根据后面的注释可以看到,仅仅是设置中断标识位,而interrupt0这个方法是一个native方法,底层又调用了C。

而在jdk官方文档中可以看到有关这个方法的叙述。

3.2 实例方法isInterrupted(),返回布尔值

这个实例方法的底层调用了一个native方法,传入了一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。

具体来说,当对一个线程,调用 interrupt() 时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。

(中断不活动的线程不会产生任何影响,看下面案例)

3.3 当前线程的中断标识为true,是不是线程就立刻停止?

  • 否,仅仅设置了一个中断状态

  • 看看中断是否会立即停止这个300的线程。否,虽然中断标志位变了。但是i一直输完300次,才最终停止。

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo4 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            for (int i = 0; i < 300; i++) 
                System.out.println("--------- " + i);
            
            System.out.println("t1调用interrupt()之后的中断标识02---- " + Thread.currentThread().isInterrupted());
        , "t1");
        t1.start();
        System.out.println("t1线程默认的中断标识---- " + t1.isInterrupted());

        try 
            TimeUnit.MILLISECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        t1.interrupt();
        System.out.println("t1调用interrupt()之后的中断标识01----  " + t1.isInterrupted());
    

对上面的代码稍作改变,如下:↓↓↓

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo5 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            for (int i = 0; i < 300; i++) 
                System.out.println("--------- " + i);
            
            System.out.println("after t1.interrupt()---第2次---- " + Thread.currentThread().isInterrupted());
        , "t1");
        t1.start();
        System.out.println("before t1.interrupt()---- " + t1.isInterrupted());

        try 
            TimeUnit.MILLISECONDS.sleep(2);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        t1.interrupt();
        System.out.println("after t1.interrupt()---第1次--- " + t1.isInterrupted());
        try 
            TimeUnit.MILLISECONDS.sleep(2000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("after t1.interrupt()---第3次--- " + t1.isInterrupted());
    

在输出结果中,我们可以看到和我们预想的都一样,只有最后一行输出,t1线程它自己不是已经打断了吗?那中断标识就应该是 true 啊?为什么变成了false???

原因是上面的代码中,t1线程打印300次i,而最后一行输出代码是在2000ms之后的,t1线程是完全可以在这个时间内完成300次i的打印工作,所以程序运行到最后一行输出,t1线程已经结束死亡了,再根据 interrupt 方法api中的这句话:

      • 中断不存在的线程不需要任何效果。

我们就懂了,中断不存在的线程没什么意义的,所以这里的中断标识自然就恢复成了默认值 false。

3.4 在3.3中断程序的基础上,添加sleep睡眠

package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo6 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            while (true) 
                if (Thread.currentThread().isInterrupted()) 
                    System.out.println(Thread.currentThread().getName() + " 中断标识位:" +
                            Thread.currentThread().isInterrupted() + " 线程终止....");
                    break;
                
                try 
                    Thread.sleep(200);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                System.out.println("---- hello InterruptDemo6");
            
        , "t1");
        t1.start();

        try 
            TimeUnit.SECONDS.sleep(1);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            t1.interrupt();
        , "t2").start();
    

这个程序是停不下来的,我是不想耗费太多CPU资源,手动停止了。 

原因就是:

如何修改上面的代码,使得程序正常运行停止呢?   → 

  1. 中断标志位 默认是false。
  2. t2 ----->t1发出了中断协商,t2调用t1.interrupt(),中断标志位true。
  3. 中断标志位true,正常情况下,程序停止。
  4. 中断标志位true,异常情况下,InterruptedException,将会把中断状态清除,并且将收到InterruptedException。中断标志位false导致无限循环。
  5. 在catch块中,需要再次给中断标志位设置为true,2次调用停止。
package com.szh.demo.interrupt;

import java.util.concurrent.TimeUnit;

public class InterruptDemo6 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            while (true) 
                if (Thread.currentThread().isInterrupted()) 
                    System.out.println(Thread.currentThread().getName() + " 中断标识位:" +
                            Thread.currentThread().isInterrupted() + " 线程终止....");
                    break;
                
                try 
                    Thread.sleep(200);
                 catch (InterruptedException e) 
                    Thread.currentThread().interrupt(); //关键代码
                    e.printStackTrace();
                
                System.out.println("---- hello InterruptDemo6");
            
        , "t1");
        t1.start();

        try 
            TimeUnit.SECONDS.sleep(1);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            t1.interrupt();
        , "t2").start();
    

3.5 静态方法public static boolean interrupted()

静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态    2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)

package com.szh.demo.interrupt;

public class InterruptDemo7 

    public static void main(String[] args) 
        System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
        System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
        System.out.println("-----1");
        Thread.currentThread().interrupt();//中断标志位设置为true
        System.out.println("-----2");
        System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
        System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
    

前两次调用没啥说的,因为main主线程并没有中断,第三次调用的时候,因为上面已经 interrupt 了,所以被中断了,这里中断标识位肯定就是 true。此时这个静态方法在中断之后第一次调用(返回当前线程的中断状态,被中断了就是true;第二件事,将当前线程的中断标识重置为false)。所以当最后一行再次调用它的时候,就是false了。 

看一下这个静态方法的源码:↓↓↓    在那个isInterrupt实例方法中传入的 一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。

这两个方法在底层都调用了native方法isInterrupted。  只不过传入参数ClearInterrupted一个传参传了true,一个传了false。

静态方法interrupted() 中true表示清空当前中断状态。  实例方法isInterrupted 则不会。


4.LockSupport

用于创建锁和其他同步类的基本线程阻塞原语。

这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫parkpark返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)

核心就是park()unpark()方法

  • park()方法是阻塞线程

  • unpark()方法是解除阻塞线程

4.1 线程的等待唤醒机制

  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程。(有局限性)

  2. 使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程。(有局限性)

  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。

4.2 wait、notify

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;

public class LockSupportDemo1 
    public static void main(String[] args) 
        final Object obj = new Object();

        new Thread(() -> 
            synchronized (obj) 
                System.out.println(Thread.currentThread().getName() + " --- come in");
                try 
                    obj.wait();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
        , "t1").start();

        try 
            TimeUnit.SECONDS.sleep(3L);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            synchronized (obj) 
                obj.notify();
                System.out.println(Thread.currentThread().getName() + " --- 发出通知");
            
        , "t2").start();
    

异常情况1:将 synchronized 同步代码块对应的代码注释掉。 

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;

public class LockSupportDemo1 
    public static void main(String[] args) 
        final Object obj = new Object();

        new Thread(() -> 
            //synchronized (obj) 
                System.out.println(Thread.currentThread().getName() + " --- come in");
                try 
                    obj.wait();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            //
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
        , "t1").start();

        try 
            TimeUnit.SECONDS.sleep(3L);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            //synchronized (obj) 
                obj.notify();
                System.out.println(Thread.currentThread().getName() + " --- 发出通知");
            //
        , "t2").start();
    

异常情况2:将wait和notify顺序调换。 

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;

public class LockSupportDemo1 
    public static void main(String[] args) 
        final Object obj = new Object();

        new Thread(() -> 
            try 
                TimeUnit.SECONDS.sleep(3L);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            synchronized (obj) 
                System.out.println(Thread.currentThread().getName() + " --- come in");
                try 
                    obj.wait();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
        , "t1").start();

//        try 
//            TimeUnit.SECONDS.sleep(3L);
//         catch (InterruptedException e) 
//            e.printStackTrace();
//        

        new Thread(() -> 
            synchronized (obj) 
                obj.notify();
                System.out.println(Thread.currentThread().getName() + " --- 发出通知");
            
        , "t2").start();
    

小总结

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中

  • 必须要先等待后唤醒,线程才能够被唤醒。要保证先wait,后notify才OK。

  • wait和notify方法必须要在同步块或者方法里面,且成对出现使用。

4.3 await、signal

package com.szh.demo.locksupport;

import javax.swing.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo2 
    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) 
        new Thread(() -> 
            lock.lock();
            try 
                System.out.println(Thread.currentThread().getName() + " --- come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
             catch (InterruptedException e) 
                e.printStackTrace();
             finally 
                lock.unlock();
            
        , "t1").start();

        try 
            TimeUnit.SECONDS.sleep(1L);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            lock.lock();
            try 
                condition.signal();
                System.out.println(Thread.currentThread().getName() + " --- 发出通知");
             finally 
                lock.unlock();
            
        , "t2").start();
    

异常情况1:将对应的加锁解锁的代码注释掉,报错信息和第一个案例是一样的。 

异常情况2:先进行 signal,再进行 await,报错信息和第一个案例是一样的。 

小总结

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中

  • 必须要先等待后唤醒,线程才能够被唤醒。一定要先await后signal,不能反了

  • Condition中的线程等待和唤醒方法,需要先获取锁

4.4 park、unpark

调用LockSupport.park()时,发现它调用了unsafe类,并且默认传了一个0。

permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。


调用LockSupport.unpark();时,也调用了unsafe类。

调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。

解决上面两个案例的第一个问题:必须放在锁块中,LockSupport不需要这样做。 

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo3 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            System.out.println(Thread.currentThread().getName() + " --- come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
        , "t1");
        t1.start();

        try 
            TimeUnit.SECONDS.sleep(2L);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        new Thread(() -> 
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " --- 发出通知");
        , "t2").start();
    

解决上面两个案例的第一个问题:必须先等待,后唤醒,LockSupport不需要这样做,先唤醒后等待照样OK。 

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo3 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            try 
                TimeUnit.SECONDS.sleep(3L);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了 " + System.currentTimeMillis());
        , "t1");
        t1.start();

        new Thread(() -> 
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " --- 发出通知");
        , "t2").start();
    

这里会先执行t2线程的unpark方法,此时t1线程手中就有了一张许可证,当t1线程睡眠3秒之后,执行代码,走到park方法不会再阻塞,直接拿出许可证,继续向下执行,所以看代码的花费时间就知道,这里的park是无效没有阻塞的。 

jdk官方文档中说了,与信号量不同,许可证不能累积,最多只有一个。

老子就不信这个邪,我非得给你来两个许可证,看看下面的代码。

package com.szh.demo.locksupport;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo3 

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            try 
                TimeUnit.SECONDS.sleep(3L);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());
            LockSupport.park();
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " --- 被唤醒了 " + System.currentTimeMillis());
        , "t1");
        t1.start();

        new Thread(() -> 
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " --- 发出通知");
        , "t2").start();
    

可以看到,代码卡在这里了,这是因为你虽然发了两个许可证,但是最多只能持有一个,那么当第二次park尝试再去获取许可证时,已经不可能了,因为t1线程手中的那个许可证已经被第一次park的时候消费掉了。

当调用park方法时如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用。

而unpark则相反, 它会增加一个凭证, 但凭证最多只能有1个, 累加无效。 


针对park和unpark方法的代码实测结论:

  1. park:unpark = 1:1,代码正常执行无误。
  2. park:unpark = 1:n,代码正常执行无误。(尽管unpark了多次,但是当前线程最多只能持有1个许可证,之后也只park了一次,消费了一个许可证,所以没问题,但还是不推荐这样写)
  3. park:unpark = n:1,代码卡死无法结束。(当前线程最多只能持有1个许可证,park一次消费一个,park多次直接无证,当前线程无法正常结束)
  4. park:unpark = n:n,代码卡死无法结束。(原因在上面说过了)

以上是关于Java——聊聊JUC中的线程中断机制 & LockSupport的主要内容,如果未能解决你的问题,请参考以下文章

Java——聊聊JUC中的线程中断机制 & LockSupport

Java——聊聊JUC中的线程中断机制 & LockSupport

Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)

Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)

Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)

Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)