JUC - 线程中断与线程等待、唤醒(LockSupport)

简介: JUC - 线程中断与线程等待、唤醒(LockSupport)

中断机制


什么是中断机制?


首先

一个线程不应该由其他线程来强制中断或停止,而是应该有线程自己自行停止,自己来决定自己的命运。

所以,Thread.stop, Thead.suspend, Thead.resumer都已经被废弃了。


其次

在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。

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


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


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

接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断


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

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

1673407230635.jpg

通过volatile变量实现

通过AtomicBoolean实现

通过Thread类自带的中断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 interrupt api");
            }
        }, "t1");
        t1.start();
        System.out.println("-----t1的默认中断标志位:"+t1.isInterrupted());
        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        //t2向t1发出协商,将t1的中断标志位设为true希望t1停下来
        new Thread(() -> {
            t1.interrupt();
        },"t2").start();
        //t1.interrupt();
    }
 static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    private static void m2_atomicBoolean()
    {
        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.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            atomicBoolean.set(true);
        },"t2").start();
    }
static volatile boolean isStop = false;
    private static void m1_volatile()
    {
        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.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            isStop = true;
        },"t2").start();
    }


总结:当对一个线程,调用interrupt方法时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true, 仅此而已。

被设置中断标志的线程将继续正常运行,不受影响。

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


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

public static void main(String[] args)
    {
        //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
        Thread t1 = new Thread(() -> {
            for (int i = 1; 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());//false
        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        t1.interrupt();//true
        System.out.println("t1线程调用interrupt()后的的中断标识01:"+t1.isInterrupted());//true
        try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("t1线程调用interrupt()后的的中断标识03:"+t1.isInterrupted());
        //????---false中断不活动的线程不会产生任何影响。
        //ps: 两秒后线程已经执行完
    }


中断协商案例深度解析

public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            while (true)
            {
                if(Thread.currentThread().isInterrupted())
                {
                    System.out.println(Thread.currentThread().getName()+"\t " +
                            "中断标志位:"+Thread.currentThread().isInterrupted()+" 程序停止");
                    break;
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                   // Thread.currentThread().interrupt();//没有它,程序不会停止,为什么要在异常处,再调用一次??
                    e.printStackTrace();
                }
                System.out.println("-----hello InterruptDemo3");
            }
        }, "t1");
        t1.start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> t1.interrupt(),"t2").start();
    }
/**
 * 1 中断标志位,默认false
 * 2 t2 ----> t1发出了中断协商,t2调用t1.interrupt(),中断标志位true
 * 3 中断标志位true,正常情况,程序停止,^_^
 * 4 中断标志位true,异常情况,InterruptedException,将会把中断状态将被清除,并且将收到InterruptedException 。中断标志位false
 *    导致无限循环
 *
 * 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止程序才OK
 */


官方描述

1673407308097.jpg

中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断

public static void main(String[] args)
    {
        //测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
        // 第二次再调用时中断状态已经被清除,将返回一个false。
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted()); //false
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//false
        System.out.println("----1");
        Thread.currentThread().interrupt();// 中断标志位设置为true
        System.out.println("----2");
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//true
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//false
        LockSupport.park();
        Thread.interrupted();//静态方法
        Thread.currentThread().isInterrupted();//实例方法
    }


静态方法interrupted将会清楚中断状态(源码传入的ClearInterrupted为true)


实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)


总结:


线程中断相关的方法:


public void interrupt(); interrupt()方法是一个实例方法

它通知目标线程中断,也仅仅是设置目标线程的中断标志位为true.


public boolean isInterrupted(); isInterrupted()方法也是一个实例方法

它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志


public static boolean interrupted(), Thread类的静态方法interrupted()

返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false, 此方法调用之后会清除当前线程的中断标志位的状态(将中断标志位置为false了),返回当前值并清零置false


线程等待和唤醒


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


文档

1673407357527.jpg

LockSupport中的 park() 和 unpark() 的作用分别是阻塞线程和解除被阻塞的线程


三种线程等待唤醒的方式


使用Object的wait()方法让线程等待,使用 Object中的notify()方法唤醒线程

使用JUC包中Condition的await方法让线程等待,使用signal()方法唤醒线程

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


Object


private static void syncWaitNotify()
    {
        Object objectLock = new Object();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t ----come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t ----被唤醒");
            }
        },"t1").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            synchronized (objectLock){
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t ----发出通知");
            }
        },"t2").start();
    }


wait()和notify()必须放在同步代码块或同步方法中,并且成对出现

必须现wait()在notify(),否则程序无法执行,无法唤醒


Condition


private static void lockAwaitSignal()
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t ----come in");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t ----被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
                System.out.println(Thread.currentThread().getName()+"\t ----发出通知");
            }finally {
                lock.unlock();
            }
        },"t2").start();
    }


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

一定要先await再signal


LockSupport


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

但与Semaphores不同,许可证不会累积,最多只有一个


park()/park(Object blocker):阻塞,permit许可证默认没有不能方向,所以一开始调用park()方法当前线程就会阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒


unpark(Thread thread):唤醒,调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回

public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName() + "\t ----come in"+System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t ----被唤醒"+System.currentTimeMillis());
        }, "t1");
        t1.start();
        //暂停几秒钟线程
        //try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"\t ----发出通知");
        },"t2").start();
    }


LockSupport 天生无锁块要求

之前错误的先唤醒后后等待,LockSupport照样支持,先unpark再park相当于提前有了通行证unpark,park时就没有拦截。park和unpark必须一一对应,因为许可证不会累积,最多只有一个


总结


LockSupport是一个线程阻塞工具类,所有的方法都是静态的,可以让线程在任意位置阻塞,阻塞之后也有对于的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码(native标识的方法即调用底层C++、C代码)。


LockSupport提供的park() 和 unpark()方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(permit)关联。

每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会累加凭证。


理解


线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。


当调用park方法时


如果有凭证,则会直接消耗掉这个凭证然后正常退出;

如果无凭证,就必须阻塞等待凭证可用;

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


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


因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的靠凭证消费,故不会阻塞。

先发放了凭证后续可以畅通无阻。


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


因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;

而调用两次park却需要消费两个凭证,证不够不能放行。

相关文章
|
4月前
|
存储 Java 数据安全/隐私保护
【JUC】ThreadLocal 如何实现数据的线程隔离?
【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
|
4月前
|
安全 算法 Java
剑指JUC原理-19.线程安全集合(上)
剑指JUC原理-19.线程安全集合
25 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
42 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
35 0
|
2月前
|
安全 Java C++
JUC(java.util.concurrent)的常见类(多线程编程常用类)
JUC(java.util.concurrent)的常见类(多线程编程常用类)
|
3月前
|
安全 Java
多线程(进阶三:JUC)
多线程(进阶三:JUC)
45 0
|
4月前
|
安全 Java 数据库
剑指JUC原理-19.线程安全集合(下)
剑指JUC原理-19.线程安全集合
37 0
|
4月前
|
算法 Java 应用服务中间件
剑指JUC原理-13.线程池(下)
剑指JUC原理-13.线程池
33 0
|
4月前
|
设计模式 Java 调度
剑指JUC原理-13.线程池(中)
剑指JUC原理-13.线程池
37 0
|
4月前
|
机器学习/深度学习 消息中间件 存储
剑指JUC原理-13.线程池(上)
剑指JUC原理-13.线程池
36 0