【JUC】3.中断机制

发布于:2022-11-29 ⋅ 阅读:(149) ⋅ 点赞:(0)

1. 什么是中断机制?

首先,一个线程不应该由其他线程来强制中断或停止,应该是由线程自己自行停止的,自己来决定自己命运才合理

其次,Java没办法立刻停止一条线程,所以停止线程显得尤为重要,如取消一个耗时操作

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

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

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

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

每个线程对象都有一个中断标识位,表示该线程是否被中断;

  • 如果该线程位为true表示中断
  • 如果该线程位为false表示未中断

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


2. 中断的相关API

中断相关的API有三个

  • publib void interrupt()
  • publib static bollean interrupted()
  • publib bollean isInterrupted()

这三个方法执行的效果如下

  • interrupt:实例interrupt方法仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
  • interrupted:这个方法是判断线程是否中断并清除当前中断状态
    1. 返回当前线程的中断状态,测试当前线程是否已被中断
    2. 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
    3. 如果连续调用两次该方法,则两次调用将返回false,因为连续两次调用的结果可能不一样
  • isInterrupted:判断当前线程是否被中断(通过检查中断标志位)

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

前面说到一个线程不应该由其他线程来强制中断或停止,应该是由线程自己自行停止的,自己来决定自己命运才合理

因此这里有三种方式实现

  • 通过一个volatile变量实现
  • 通过AtomicBoolean实现
  • 通过Thread类自带的中断api实例方法实现

3.1 通过一个volatile变量实现

volatile的作用主要有如下两个:

  1. 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
  2. 顺序一致性:禁止指令重排序

测试类代码

public class InterruptDemo {

    static volatile Boolean isStop = false;

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

        try {
            TimeUnit.MICROSECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

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

当t2线程执行将isStop设置为true的时候,t1线程就会退出循环,结束线程

在这里插入图片描述


3.2 通过AtomicBoolean实现

AtomicBoolean提供了一种原子性地读和写布尔类型变量的解决方案,通常情况下,该类将被用于原子性地更新状态标识位

public class InterruptDemo {

    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() + "\t AtomicBoolean被修改为true,程序停止");
                    break;
                }
                System.out.println("t1 - hello - AtomicBoolean");
            }
        }, "t1").start();

        try {
            TimeUnit.MICROSECONDS.sleep(20);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

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

在这里插入图片描述


3.3 通过Thread类自带的中断api实例方法实现

通过上面三种API实现

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

    try {
        TimeUnit.MICROSECONDS.sleep(20);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

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

在这里插入图片描述


3.4 总结

这三种方法虽然实现方式不一样,但是本质来说都是一个线程修改另一个线程的中断标志位,接着另一个线程识别到标志位修改后,自行中断线程。


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

如果线程处于正常活动状态下,调用interrupt方法,会将该线程的中断标志设置为true,仅此而已

被设置中断的线程仍然会继续正常运行,不会受到影响

所以interrupt方法并不能真正中断线程,需要被调用的线程自己进行配合才行

public class InterruptDemo2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 300; i++) {
                System.out.println("---------:" + i);
            }
        }, "t1");
        t1.start();

        System.out.println("t1 默认线程的中断标识02:" + t1.isInterrupted());

        try {
            TimeUnit.MICROSECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        t1.interrupt();
        System.out.println("t1 默认线程的中断标识01:" + t1.isInterrupted());

        try {
            TimeUnit.MICROSECONDS.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        t1.interrupt();
        System.out.println("t1 默认线程的中断标识03:" + t1.isInterrupted());
    }
}

在这个测试类中,即使线程t1的中断标志被设置为true,不会立刻停止线程t1,t1仍然继续运行打印输出完

t1 默认线程的中断标识02:false
t1 默认线程的中断标识01:true
t1 默认线程的中断标识02:false

在设置中断标识后,休眠5s,然后再次设置中断标识位,最后输出发现中断标识位为false

这是因为5s后该线程已经运行完不活动了,而interrupt的api规定中断不活动的线程不会产生任何影响

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

public class InterruptDemo03 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread() + "中断标志位" + Thread.currentThread().isInterrupted() + "程序停止");
                    break;
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("----hello InterruptDemo03");
            }

        }, "t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

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

在这里插入图片描述

当线程调用wait/sleep/wait方法的时候,那么该线程的中断状态就会被清除,并且收到InterruptedException异常

这里的案例,一开始中断标识位默认为false

t2线程向t1线程发出了中断协商,t2调用了t1的interrupt,设置t1的中断标志位为true

正常情况下,程序应该停止

异常情况下,出现InterruptedException异常,将会把中断状态清除,收到InterruptedException,中断标志位为false,导致无限循环

解决方法:在catch代码块中再次设置中断标识位为true,也就是二次调用才可以停止程序


5. 静态方法Thread.interrupted()

这个方法是用来判断线程是否被中断并清除当前中断状态

  • 返回当前线程的中断状态,测试当前线程是否已被中断
  • 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
public class InterruptedDemo {
    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();
        System.out.println("----------------------2");
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
    }
}

在这里插入图片描述

main线程默认的中断标志为false,第一次调用 Thread.interrupted()方法,返回当前的默认标志位也就是true,并清空当前中断状态(由于是false,所以清空也是false)

第二次同上,

在第三次之前,执行了Thread.currentThread().interrupt();,将中断标志位设置为true

第三次,首先返回当前的中断标志位true,然后清空当前中断标志位,也就是将中断标志位被设置为false

第四次的操作同第一次


6. LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

其中LockSupport的park()和unpark()的作用阻塞线程和解除阻塞线程

在线程中有三种方法实现等待唤醒机制

  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  2. 使用JUC包中的Condittion的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

其中LockSuport类使用了一种名为Permit(许可)的概念做到了阻塞和唤醒线程的功能,每个线程都有一个许可(permit)

但是许可累加上限是1

LockSuport有两个方法是比较重要的

  • park:用于线程获取许可证
  • unpark:用于其他线程给某个线程发放许可证

当调用park方法时:

  • 如果有凭证,则会直接消耗这个凭证然后正常退出
  • 如果无凭证,就必须阻塞等待凭证可用

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

public class LockSupportDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\tcome in");
            //获取许可证
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t被唤醒");
        }, "t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

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

    }
}

在这里插入图片描述

注意:

  • 三种实现线程等待和唤醒,前面两种等待和唤醒的顺序不能调换,必须现有等待,才可以唤醒;但是第三种LockSupport可以先发放许可证,等到这个线程用到了再获取许可证(这当中过程没有阻塞),这操作就有点像上高速的ETC通道,先把卡给了司机,司机要过关卡的时候就不需要等待,可以无畅通过
  • 线程只能获取一个通行证,就上面案例而已,就算t2给t1发布了一万个凭证,这效果也和一个是一样的

6.1 面试题

为什么LockSupoort可以突破wait/notify原来的调用顺序

因为unpark获得一个凭证,之后再调用park方法,就可以名正言顺地凭证消费,故不会阻塞。先发放凭证后续畅通无阻

为什么唤醒两次阻塞两次,但最终结果还是会阻塞

因为凭证的数量最多是1,连续调用两次unpark和调用一次unpark效果一样的,只会增加一个凭证;而调用两次park则需要消耗两次凭证,凭证不够,故不能放行


本文含有隐藏内容,请 开通VIP 后查看