JUC第四讲-LockSupport 和线程中断

82 阅读7分钟

推荐一个java届大神:孙哥
孙哥主页

线程中断机制

从阿里蚂蚁金服面试题讲起

image.png 这三个方法区别?

如何中断一个运行中的线程??

如何停止一个运行中的线程??

什么是中断机制

首先
一个个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。
所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了.
其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制一一中断,也即中断标识协商机制。

中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true:
接着你需要自己写代码不断地检测当前线程的标识位,如果为tue,表示别的线程请求这条线程中断,
此时究竞该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为tue表示中断,为false表示未中断:
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

:::info 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现 每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为tue表示中断,为false表示未中断: 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。 :::

中断的三个API

  • public void interrupt()

仅仅设置线程的中断状态为true ,发起一个协商而不会立刻停止线程 2如果线程处于被阻塞状态(例如处于sleep,wait,join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

  • public static boolean interrupted()

判断线程是否被中断并清除当前中断状态。 这个方法做了两件事: 1返回当前线程的中断状态,测试当前线程是否已被中断 2将当前线程的中断状态清零并重新设为fase,清除线程的中断状态

  • public boolean isInterrupted()

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

大厂面试题中断机制考点

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

  1. 通过volatile变量实现
   public static void main(String[] args) throws InterruptedException {
      // 创建并启动线程t1
      new Thread(()->{
         while (true){
            if(isStop){
                System.out.println("停止");
                break;
            }
            System.out.println("t1----hello volatile");
         }
      },"t1").start();
      
      // 线程休眠10秒
      TimeUnit.SECONDS.sleep(10);
      
      // 创建并启动线程t2
      new Thread(()->{
         // 将isStop标记设置为true
         isStop=true;
      },"t2").start();
   }

  1. 通过AtomicBoolean
   /**
    * 主线程入口
    *
    * @param args 参数数组
    * @throws InterruptedException 线程中断异常
    */
   public static void main(String[] args) throws InterruptedException {
      // 创建并启动线程t1
      new Thread(()->{
         while (true){
            if(isStop.get()){
                System.out.println("停止");
                break;
            }
            System.out.println("t1----hello volatile");
         }
      },"t1").start();

      // 线程休眠10秒
      TimeUnit.SECONDS.sleep(5);

      // 创建并启动线程t2
      new Thread(()->{
         // 将isStop标记设置为true
         isStop.set(true);
      },"t2").start();
   }

  1. 通过Thread类自带的中断api实现
   /**
    * 主线程入口
    *
    * @param args 参数数组
    * @throws InterruptedException 线程中断异常
    */
   public static void main(String[] args) throws InterruptedException {
       // 创建并启动线程t1
       Thread t1 = new Thread(() -> {
           while (true) {
               if (Thread.currentThread().isInterrupted()) {
                   System.out.println("停止");
                   break;
               }
               System.out.println("t1----hello interrupt api");
           }
       }, "t1");
       t1.start();

       TimeUnit.SECONDS.sleep(5);

       // 创建并启动线程t2
       new Thread(()->{
           t1.interrupt();
       },"t2").start();
   }

当前线程的中断标识被设置为true的化,是不是线程立刻停止?

只会设置中断标志位,程序线程会继续运行

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

   }
}

image.png 由此可见,调用interrupt后 标识变为true,线程并没有停止

当程序中调用sleep,wait等方法的情况

如果该线程阳毫的调用wait(),wait(long),或wait(Long,int)的方法0 bject:类,或的join(),join(Long),join(Long,int),sleep(long),或sleep(Long,int),这个类的方法,那么它的中断状态将被清除,并且将收到InterruptedException。

   public static void main(String[] args) throws InterruptedException {
      Thread r1=new Thread(()->{
         while (true){
            if(Thread.currentThread().isInterrupted()){
               System.out.println(Thread.currentThread().getName()+"线程被中断");
               break;
            }
            try {
               Thread.sleep(200);
            } catch (InterruptedException e) {

               e.printStackTrace();
            }
            System.out.println("----hello");
         }

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



      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         throw new RuntimeException(e);
      }
      new Thread(()->{
         r1.interrupt();
      },"t2").start();

   }

image.png 由于调用阻塞方法sleep () 清除了中断状态 线程会一直执行下去

public class InterruptDemo4 {
   public static void main(String[] args) throws InterruptedException {
      Thread r1=new Thread(()->{
         while (true){
            if(Thread.currentThread().isInterrupted()){
               System.out.println(Thread.currentThread().getName()+"线程被中断");
               break;
            }
            try {
               Thread.sleep(200);
            } catch (InterruptedException e) {
               Thread.currentThread().interrupt();//为什么要在异常处,再调用一次?
               e.printStackTrace();
            }
            System.out.println("----hello");
         }

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



      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         throw new RuntimeException(e);
      }
      new Thread(()->{
         r1.interrupt();
      },"t2").start();

   }
}

所以要在sleep方法 抛出异常的时候,重新给中断标志位赋值

Thread.interrupted()

判断线程是否被中断并清除当前中断状态。 这个方法做了两件事: 1返回当前线程的中断状态,例试当前线程是否已被中断 2将当前线程的中断状态清零并重新设为ase,清除线程的中断状态 此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回as,因为连续调用两次的结果可能不一样

   public static void main(String[] args) {
      System.out.println(Thread.currentThread().getName() +Thread.interrupted());
      System.out.println(Thread.currentThread().getName() +Thread.interrupted());
      System.out.println("-----1-----------");
      Thread.currentThread().interrupt();
      System.out.println("----2");
      System.out.println(Thread.currentThread().getName() + " is"+Thread.interrupted());
      System.out.println(Thread.currentThread().getName() + " is"+Thread.interrupted());
   }
mainfalse
mainfalse
-----1-----------
-----2
main istrue
main isfalse

LockSupport

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

  1. wait notify
public class LockSupportDemo1 {
   public static void main(String[] args) throws InterruptedException {
      Object objectLock = new Object();
      new Thread(()->{
         synchronized(objectLock){
            System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
            try {
               objectLock.wait();
            } catch (InterruptedException e) {
               throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "被唤醒");
         }
      },"t1").start();
      TimeUnit.SECONDS.sleep(1);

      new Thread(()->{
         synchronized(objectLock){

            objectLock.notify();
            System.out.println(Thread.currentThread().getName() + " 唤醒");
         }
      },"t2").start();
   }
}


t1 进入同步代码块
t2 唤醒
t1被唤醒

如果先notify 再 wait 还能否唤醒??

   public static void main(String[] args) throws InterruptedException {
      Object objectLock = new Object();
      new Thread(()->{
         try {
            TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
         synchronized(objectLock){

            System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
            try {
               objectLock.wait();
            } catch (InterruptedException e) {
               throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "被唤醒");
         }
      },"t1").start();
//      TimeUnit.SECONDS.sleep(1);

      new Thread(()->{
         synchronized(objectLock){

            objectLock.notify();
            System.out.println(Thread.currentThread().getName() + " 唤醒");
         }
      },"t2").start();
   }
}

image.png t1无法被唤醒

  1. conditon await()和 signal()方法
public class LockSupportDemo2 {
   public static void main(String[] args) throws InterruptedException {
      Object objectLock = new Object();
      Lock lock=new ReentrantLock();
      Condition condition = lock.newCondition();
      new Thread(()->{
         try {
            TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
         lock.lock();
            System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
            try {
              condition.await();
            } catch (InterruptedException e) {
               throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "被唤醒");
            lock.unlock();

      },"t1").start();
//      TimeUnit.SECONDS.sleep(1);

      new Thread(()->{
         lock.lock();
            condition.signal();
            System.out.println(Thread.currentThread().getName() + " 唤醒");
         lock.unlock();
      },"t2").start();
   }
}

image.png 同样不能唤醒t1

  1. LockSupport 类中的park等待和 unpark唤醒
public class LockSupportDemo3 {
   public static void main(String[] args) throws InterruptedException {
      Thread t1 = new Thread(() -> {
         try {
            TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
         System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
         LockSupport.park();
         System.out.println(Thread.currentThread().getName() + "被唤醒");

      }, "t1");
      t1.start();
//      TimeUnit.SECONDS.sleep(1);

      new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " 唤醒");
      },"t2").start();
   }
}

image.png 特点: 正常+无锁块要求 现在支持先唤醒,后等待

注意:许可证只能有一个 image.png image.png 唤醒不了了

:::info LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程 LockSupport和每个使用它的线程都有一个许可(permit)关联。 每个线程都有一个相关的permit,permiti最多只有一个,重复调用unpark也不会积累凭证。 形象的理解 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。 当调用par水方法时 *如果有凭证,则会直接消耗掉这个凭证然后正常退出: *如果无凭证,就必须阻塞等待凭证可用: 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。 :::

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

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

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

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证: 而调用两次pa却需要消费两个凭证,证不够,不能放行。