通俗易懂地聊 Java 线程同步(二)interrupt、wait、notify、join该怎么用?

如何停止线程

如何启动线程相信大家都很清楚,

java 复制代码
Thread thread = new Thread(() -> {
  for (int i = 0; i < 10000; i++) {
    System.out.println("index:" + i);
  }
});
thread.start();

调用Thread#start方法就可以启动一个线程,很简单。那么如何停止一个线程呢?我们可以看到Thread类有一个stop方法,我们来试一下。

java 复制代码
try {
  Thread.sleep(10);
} catch (InterruptedException e) {
  throw new RuntimeException(e);
}
thread.stop();

运行之后看结果:

vbnet 复制代码
index:508
index:509
index:510
index:511
Process finished with exit code 0
=======================================
index:410
index:411
index:412
index:413
Process finished with exit code 0

线程确实在 10ms 之后停止了,不过有个问题,每次执行的结果不一样呀。这是问题么?当然是问题,一个线程在执行过程中有可能会处理任何事情,比如在处理处理数据库,或者在拷贝文件,如果我们此时直接中断,那就会有可能导致状态不对。Thread#stop 就相当于是直接把线程给「拉闸」了,线程来不及做任何反应就结束了。所以这个方法很早就被标记为废弃了,因为它是不安全的,而替代它的则是interrupt方法。

interupt()

我们将thread.stop()改为thread.interrupt(),然后再运行下:

java 复制代码
index:9996
index:9997
index:9998
index:9999

Process finished with exit code 0

运行后发现,线程不会被中断,线程咋没停啊?其实interrupt只是一个状态,将线程标记为中断。调用此方法后线程不会像stop方法一样让线程「断电」,它是将线程标记为中断状态,开发者在线程运行过程中可以去判断线程是不是处于中断状态,然后处理相应的逻辑,比如上述逻辑可以这样处理:

java 复制代码
public static void main(String[] args) {
  Thread thread = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
      System.out.println("index:" + i);
      if(Thread.currentThread().isInterrupted()){
        if(i == 5000) {
          // release(); // 释放资源
          break;
        }
      }
    }
  });
  thread.start();
  try {
    Thread.sleep(10);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  thread.interrupt();
}

这段代码会在线程内判断当前的isInterrupted 状态,如果是true则表示此线程需要中断了,那便需要做「善后」逻辑,保证业务逻辑状态正常,这便是Thread#interrupt的作用。Thread#stop则做不到这一点,开发者无法善后,对于程序状态来说会比较危险,所以此方法被标记为废弃,此方法一般不建议使用。

除了Thread#isInterrupted可以判断线程中断外,还有个方法Thread#interrupted也是可以的。它们的区别是interrupted方法在调用之后会重置「中断」状态。就是说连续调用两次interupted方法,第二次将返回false。本文不做详细阐述了,感兴趣的可以自己了解下。

InterruptedException

我们在调用Thread#sleep时,会强制捕获这个异常,这个方法使用的比较多,大家应该都不陌生。其实不仅仅是sleep方法会抛这个异常,任何能让线程阻塞的方法都会抛这个异常,如 waitjoin都会。

为什么会抛异常,从上文中讲到的Thread#interrupt逻辑看,就比较容易理解了。比如我用 Thread.sleep(1000)阻塞当前线程 1 秒,但是我在 500 毫秒时调用了 Thread.interrupt(),那当前线程就不能继续睡了。你线程都需要中断了还睡着那线程是不是就中断不了了,所以之前睡着的线程会被立刻唤醒,你需要「善后」了,对吧?waitjoin 都是一样的逻辑,如果当前线程被阻塞,在被interrupt之后会被立刻唤醒,让你处理线程停止之前的逻辑。

诶,终于说到 waitnotify 了,很多同学不敢用 wait,就怕用了之后线程有可能永远被阻塞,导致异常,下面我们就来讲 wait 应该怎么用。

wait()/notify()/notifyAll()

先来看一段代码:

java 复制代码
public class ThreadTest5 {
  private String xManager;
  void init(){
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    xManager = "xManager";
    System.out.println("init finish.");
  }
  void execute(){
    while (xManager == null){
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    System.out.println(xManager);
  }
  public static void main(String[] args) {
    ThreadTest5 test5 = new ThreadTest5();
    new Thread(() -> test5.init()).start();
    new Thread(() -> test5.execute()).start();
  }
}

这是一个经典的「手动」线程同步逻辑,一个对象初始化在一个线程,使用在另一个线程,所以我们需要保证初始化完成之后,再去使用。这段代码有问题么?对于运行结果来说可能没有问题,对于效率来说可能会有一点问题,有可能代码已经初始化完成,但没立刻执行,是吧。而类似这样的代码,其实也没那么少见。如果你对线程同步方式不太了解,就很有可能写出这样的代码。那么怎样写会更好呢?我们来改造一下。

java 复制代码
public class ThreadTest5 {
  private String xManager;

  synchronized void init() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    xManager = "xManager";
    System.out.println("init finish.");
    notify();
  }

  synchronized void execute() {
    System.out.println("execute");
    try {
      if(xManager == null)
        wait();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    System.out.println(xManager);
  }

  public static void main(String[] args) {
    ThreadTest5 test5 = new ThreadTest5();
    new Thread(() -> test5.execute()).start();
    new Thread(() -> test5.init()).start();
  }
}

分析一下代码,先看一下 wait() 的注释:

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).

wait()会让当前线程等待,直到其他线程执行 notify()notifyAll()。只要不 notify() 线程就永远不会被唤醒么?是这样的,这也是很多人不愿意用 wait()的原因,感觉比较危险。

再说回这段代码,在方法 execute() 内会判断 xManager是否为空,如果是则 wait(),此时线程会进入阻塞状态。另一个线程执行 init() 最后再调用 notify(),此前阻塞的线程便会恢复,执行相应的逻辑。分析之后就不难发现,这种方式一定比「轮询」的那种逻辑效率要高,因为它能保证在条件达到之后会立刻执行此前的等待操作,是没有时间损耗的,对吧?

用哪个对象的 wait()/notify()?

wait()notify()Object 的方法,并不是线程的,每个对象都有这些方法,所以我应该用哪个对象呢?很简单,看 synchronized 代码块用的是哪个 monitor,比如此处是修饰方法,所以monitor就是this,所以调用的是this.wait()。同时我们也得出一个结论,wait()notify()/noitfyAll() 都必须在 synchronized代码块内执行,因为只有synchronized才会产生monitor,同时还需要是同一个 monitor

比如,我们自己定义一个monitor再看下:

java 复制代码
public class ThreadTest5 {

  private Object lock = new Object();
  private String xManager;
  void init() {
    synchronized (lock) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      xManager = "xManager";
      System.out.println("init finish.");
      lock.notify();
    }
  }
  void execute() {
    synchronized (lock) {
      System.out.println("execute");
      try {
        if (xManager == null)
          lock.wait();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      System.out.println(xManager);
    }
  }

  public static void main(String[] args) {
    ThreadTest5 test5 = new ThreadTest5();
    new Thread(() -> test5.execute()).start();
    new Thread(() -> test5.init()).start();
  }
}

这边我们定义了lock这个monitor,然后再使用 lock.wait()lock.notify(),这样改造之后跟之前的逻辑是完全一样的,没有区别。那如果这边我们调用 this.wait() 再运行下,就不行了,会抛出IllegalMonitorStateException异常。

所以我们应该怎么用 wait()notify()? 很简单,一句话就能描述完。代码块使用哪个monitor就使用它的 wait()notify(),并且它们一定是相同的monitor

不了解 monitor 的同学,可以参考通俗易懂地聊 Java 线程同步(一) - 掘金 (juejin.cn) 这篇文章。

notify()/notifyAll()

notify() 实际场景使用会比较少,因为它只会唤醒一个线程,并且不确定唤醒哪个。如果你的代码只有两个线程,这样使用也 OK,不过通常还是建议使用 noitfyAll(),它会唤醒所有等待这个锁的线程。

上面代码的 wait 逻辑我们使用 if 来判断的,这在实际场景也有可能不安全,因为有可能线程被唤醒之后,这个条件还有可能为false,所以在被唤醒后还需要在此判断条件是否符合。所以我们可以参考 Object#wait 注释提供的参考代码:

java 复制代码
synchronized (obj) {
          while (<condition does not hold>)
              obj.wait();
          ... // Perform action appropriate to condition
      }

这边我就不再代码演示了,大家可以自己尝试下。

Thread#join()

wait()notify()notifyAll() 方法都是 Object 类的方法,不过 join()Thread类的。 方法注释:

Waits for this thread to die.

翻译下就是,如果调用此方法,当前执行的线程则会被阻塞,直到此线程执行完毕才会恢复。

上代码:

java 复制代码
public static void main(String[] args) {
  Thread thread = new Thread(() -> {
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    System.out.println("This is thread 1.");
  });
  thread.start();
  try {
    thread.join();
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  System.out.println("Main thread is finished.");
}
=================================================
运行结果:
This is thread 1.
Main thread is finished.

如果没有thread.join()这行代码,先打印的一定是Main thread is finished.。不过在此之前因为有了thread.join(),所以主线程会被阻塞,直到thread这个线程执行完毕之后才会恢复。

这个方法其实比较简单,看以上这个示例代码基本都会理解。

结束

本篇文章内容就这些,欢迎交流互动,下面一篇我会跟大家分下几种线程同步的工具类。

相关推荐
黑胡子大叔的小屋11 分钟前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark14 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
_Shirley27 分钟前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
雷神乐乐1 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
V+zmm101342 小时前
基于小程序宿舍报修系统的设计与实现ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·ssm
测试19982 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
文大。2 小时前
2024年广西职工职业技能大赛-Spring
java·spring·网络安全
一只小小翠2 小时前
EasyExcel 模板+公式填充
java·easyexcel
Aphasia3113 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试