「JUC篇」之 一篇文章掌握线程中断机制与LockSupport

51 阅读9分钟

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

1. 什么是中断机制

1.1 三个废弃方法

image.png

为什么这三个方法被废弃了?

首先,一个线程不应该由其他线程强制中断或者停止,而是应该由自己的线程来停止。 倘若别的线程能够肆无忌惮的结束你的线程,这不就乱套了,因此这三个api被废弃。

其次,在java中 没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。 因此,Java提供了一种用于停止线程的机制——中断

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

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

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

每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断; 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

1.2 中断机制的API

image.png

image.png

2. 如何使用中断标识停止线程

2.1 思路

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。可以是修改状态,也可以是停止程序运行等等、、

2.2 方法一 volatile变量实现

private 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("-------hello interrupt");
        }
    },"t1").start();

    //暂停几秒钟线程
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    isStop = true;
}

image.png

2.2 方法二 AtomicBoolean

    private final static AtomicBoolean atomicBoolean = new AtomicBoolean(true);

    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            while(atomicBoolean.get())
            {
                try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("-----hello");
            }
        }, "t1");
        t1.start();

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

        atomicBoolean.set(false);
    }

image.png

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

 public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            while(true)
            {
                if(Thread.currentThread().isInterrupted())
                {
                    System.out.println("-----t1 线程被中断了,break,程序结束");
                    break;
                }
                System.out.println("-----hello");
            }
        }, "t1");
        t1.start();

        System.out.println("**************"+t1.isInterrupted());
        //暂停5毫秒
        try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        t1.interrupt();
        System.out.println("**************"+t1.isInterrupted());
    }

image.png

image.png

3. 源码分析

3.1 实例方法interrupt()源码分析

image.png image.png

由上图可知,interrupt()方法底层核心是执行native方法interrupt0()

重点是上上图中框出的部分我们可知,如果这个线程由于 wait/join/sleep而阻塞,那么此时调用interrupt方法会清空打断标记并且抛出异常InterruptedException

3.2 实例方法isInterrupted()源码分析

image.png

3.3 静态方法Thread.interrupted()源码分析

image.png

image.png

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

4.1 说明

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

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

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

4.2 代码演示

 public static void main(String[] args) throws InterruptedException
    {
        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());
        //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
        t1.interrupt();
        //活动状态,t1线程还在执行中
        try { TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("after t1.interrupt()--第1次---: "+t1.isInterrupted());
        //非活动状态,t1线程不在执行中,已经结束执行了。
        try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("after t1.interrupt()--第3次---: "+t1.isInterrupted());
    }

image.png

image.png

image.png

4.3 在sleep时调用interrupt()

public static void main(String[] args) throws InterruptedException
{
    Thread thread = new Thread(() -> {
        System.out.println("Thread start----");
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName() + "线程-=---isInterrupted() = true,自己退出");
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                //Thread.currentThread().interrupt();
                e.printStackTrace();
            }

            System.out.println("hello");
        }
    }, "t1");

    thread.start();

    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e){ e.printStackTrace(); }
    thread.interrupt();

下面是代码中保留注释那一行代码的结果 image.png

可见在抛出异常后,没有进入上面末尾为的break的代码块。

下面是不注释那一行代码的结果

image.png

可见代码在抛出异常后,catch代码块中又将线程的中断标识置为了true

结论:

image.png

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

5. 静态方法Thread.interrupted()

5.1 代码演示

   public static void main(String[] args) throws InterruptedException
    {
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println("111111");
        Thread.currentThread().interrupt();
        System.out.println("222222");
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
    }

image.png

这怎么一会true一会false的????

5.2 说明

image.png

image.png

5.3 interrupted与isInterrupted对比

image.png

方法的注释也清晰的表达了“中断状态将会根据传入的ClearInterrupted参数值确定是否重置”。

所以, 静态方法interrupted将 会清除中断状态(传入的参数ClearInterrupted为true),

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

6. 三个中断API总结

interrupt()方法是一个实例方法 它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。

isInterrupted()方法也是一个实例方法 它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志

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

7. 什么是LockSupport

image.png

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

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

8. 线程等待唤醒机制

8.1 3种让线程等待和唤醒的方法

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

8.2 wait和notify代码演示

要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作

正常情况

  public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }
        },"t2").start();
    }

image.png

异常情况1:两个都去掉同步代码块后看运行效果

    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            objectLock.notify();
        },"t2").start();
    }

image.png

异常情况1结论:Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。

异常情况2:将notify放在wait方法前先执行,t1先notify了,3秒钟后t2线程 再执行wait方法


    public static void main(String[] args)//main方法,主线程一切程序入口
    {
        Object objectLock = new Object(); //同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t1").start();

        //t1先notify了,3秒钟后t2线程再执行wait方法
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
        },"t2").start();
    }

image.png

异常情况2结论:先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒

8.3 wait和notify小总结

  1. wait和notify方法必须要在同步块或者方法里面,且成对出现使用
  2. 先wait后notify才OK

8.4 await后signal代码演示

正常情况

  public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"start");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t2").start();

    }

image.png

异常情况1:不在lock和unlock方法中

 public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"start");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
        },"t2").start();

    }

image.png

异常情况1结论:调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用

异常情况2:先signal()后await()

 public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
                System.out.println(Thread.currentThread().getName()+"\t"+"signal");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"等待被唤醒");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"t2").start();

    }

image.png

异常情况2结论:先await()后signal才OK,否则线程无法被唤醒

8.5 await和signal小总结

  1. Condtion中的线程等待和唤醒方法之前,需要先获取锁
  2. 一定要先await后signal,不要反了

8.6 Object和Condition使用的限制条件

  1. 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  2. 必须要先等待后唤醒,线程才能够被唤醒

9. LockSupport类中的park等待和unpark唤醒

9.1 是什么

通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

image.png

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

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit), permit只有两个值1和零,默认是零。 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1

9.2 API介绍

image.png

9.3 阻塞

park() /park(Object blocker) | 阻塞当前线程/阻塞传入的具体线程

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

image.png

9.4 唤醒

unpark(Thread thread) | 唤醒处于阻塞状态的指定线程

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

image.png

9.5 正常情况+无锁块代码演示

 public static void main(String[] args)
    {
        //正常使用+不需要锁块
Thread t1 = new Thread(() -> {
    System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
    LockSupport.park();
    System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
},"t1");
t1.start();

//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"   -----LockSupport.unparrk() invoked over");

    }

image.png

9.6 之前错误的先唤醒后等待,LockSupport照样支持

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

        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---unpark over");
    }

image.png

原因:

image.png

成双成对要牢记

那么这一章就讲完了,希望能让小伙伴们彻底掌握LockSupport以及线程中断机制