线程状态

  对于Java中线程状态,JVM有明确声明:虚拟机中的线程状态,不反应任何操作系统中的线程状态JVM在设计上有自己的一套规范,切勿与操作系统底层的线程状态混为一谈。

  Java线程状态使用了Thread的内部类State来表示,规定了如下的六种状态;

状态

含义

NEW

新创建了一个线程对象,但还没有调用start()方法

RUNNABLE

可运行状态,线程对象调用start()方法后,由NEW状态迁移到RUNNABLE状态

BLOCKED

阻塞状态,表示线程正在等待一个监视器锁(monitor lock),而监视器锁在Java代码中的体现就是synchronized关键字;也就是说:只有线程在等待进入synchronized修饰的代码块或方法时,线程才处于BLOCKED状态

WAITING

等待状态,表示线程在等待某些条件的到达

TIMED_WAITING

超时等待状态,与WAITING状态类似,并在其基础上,增加了超时的限制

TERMINATED

终止状态,包括线程正常执行完毕和异常终止

线程状态间的流转如下:

java查看jvm的线程 jvm线程状态_死锁

RUNNABLE

  • RUNNABLE状态表示正在运行或者准备运行,这种状态是相对于JVM来说的,而线程具体何时运行,这取决于操作系统底层的资源调度;注意在操作系统底层,正在运行和准备运行是两种不同的状态,但是JVM层面并不细分这两个
  • 当执行yield操作时,底层状态确实是有变化的,线程会让出当前执行权,让CPU可以去执行其他可运行的线程,这是底层的变化,但是JVM层面,原线程依旧还是RUNNABLE状态

BLOCKED 与 WAITING 状态的区别

  • BLOCKED状态表示正在等待一个监视器锁,即虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区
  • WAITING 状态的先决条件则是已经进入临界区,也就是说线程已经拿到锁了,但是因为其他原因,在此等待一下
  • 简单来说,线程只有在等待进入synchronized代码块或者方法时,才会进入BLOCKED状态;此外,注意对于Lock接口下实现的锁,由于其底层都是调用LockSupport中的方法,因此等待锁时进入的都是WAITING或者TIMED_WAITING

线程方法

线程等待(wait)

  • wait()方法位于Object类中,前面说了进入等待状态的前提是已经拿到对象的锁了,所以该方法必须在同步代码中才能使用
  • 调用该方法,当前线程进入WAITING状态,只有等待其他线程的通知,或者当前线程被中断才会返回
  • 调用wait()方法,线程会释放锁;因为如果不释放锁,那么其他线程就无法获取当前对象的锁,那么也就无法执行notify/notifyAll方法来唤醒挂起的线程,这就造成死锁了

线程睡眠(sleep)

  • sleep()方法位于Thread类中
  • sleep()方法不会释放当前对象的锁,只能等待时间自然醒来执行完,其他线程才能继续获取当前对象的锁

wait 与 sleep 的区别?

  1. 这两方法来自不同的类中,wait()作用于资源对象本身,sleep()方法作用于当前线程
  2. 最主要的就是wait()方法会释放锁,而sleep()不会释放锁
  3. 使用范围不同,wait()必须在同步代码中使用,sleep()可在任何地方使用(这也印证了第二点,sleep没有锁可以释放)

线程让步(yield)

  • yield 会使当前线程让出 CPU 执行时间片,即暂停一下,让系统的线程调度器重新选择个可运行的线程执行
  • 注意当前线程调用yield方法后,最终选择的结果可能还是当前线程继续执行;一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感

线程中断(interrupt)

  • 注意线程中断并不是立刻停止线程工作的意思,因为强制停止某个线程运行可能会造成无法预估的后果,但是某些场景是需要停止线程的,而线程中断就可满足这些使用场景;它的使用场景类似如下:在A线程中调用B线程的interupt()方法,即相当于跟B线程打了个招呼,并给B线程打上了中断标记,在B线程中可调用isInterrupted()或者interrupted()方法来判断是否有其他线程想中断自己,根据这个判断结果,B线程可以自行决定自己接下来要怎么做
  • 线程中断最重要的就是如下三个方法:
  • interupt():中断目标线程,给目标线程发一个中断信号,目标线程被打上中断标记
  • isInterrupted():判断线程是否被中断,不会清除中断标记
  • interrupted():判断线程是否被中断,会清除中断标记
  • 调用线程中断时,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,并且会清除打断标识;但是不能中断 I/O 阻塞和 synchronized 锁阻塞
  • 注意线程中断是线程之间的一种协作机制,如果不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程
public static void main(String[] args) throws InterruptedException {
  Thread A = new Thread(() -> {
    // 判断是否被中断
    while (!Thread.currentThread().isInterrupted()) {
      System.out.println("  A:学习java中...");
    }
    System.out.println("A:听说有人想中断我?学完js再说");
    // 同样返回中断标识, 并且会清除标识
    boolean interrupted = Thread.interrupted();
    System.out.println("A:当前中断标识:" + interrupted + ", 清除掉,忽略继续学习");
    boolean b = Thread.currentThread().isInterrupted();
    System.out.println("A:清除掉之后的标识:" + b + ", 可以安心学习了");
    // 判断是否被中断
    while (!Thread.currentThread().isInterrupted()) {
      System.out.println("  A:学习js中...");
    }
    System.out.println("A:又想中断我, 那休息下吧");
  }, "A");


  A.start();
  try {TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {}
  // 中断A线程, 不要沉迷学习了
  A.interrupt();
  try {TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {}
  // 继续中断
  A.interrupt();
}

执行结果:

A:学习java中...
  A:学习java中...
  A:学习java中...
  A:学习java中...
A:听说有人想中断我?学完js再说
A:当前中断标识:true, 清除掉,忽略继续学习
A:清除掉之后的标识:false, 可以安心学习了
  A:学习js中...
  A:学习js中...
  A:学习js中...
  ...
A:又想中断我, 那休息下吧

线程等待(join)

  • 使用场景如下:在A线程中调用B线程的join()方法,则A线程必须等待B线程执行完毕才继续执行
  • 注意调用B线程的join()方法时,是需要B线程已启动,否则无效的,这点从join()方法源码中很容易看出
public static void main(String[] args) throws InterruptedException {
  Thread a = new Thread(() -> {
    System.out.println("a开始执行");
    sleep(3);
    System.out.println("a执行完毕");
  });
  Thread b = new Thread(() -> {
    try {
      System.out.println("  b开始执行");
      a.join();
      System.out.println("  b执行完成");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  });
  a.start();
  sleep(1);
  b.start();
}

执行结果:

a开始执行
  b开始执行
a执行完毕
  b执行完成

线程唤醒(notify/notifyAll)

  • 通常与wait()方法搭配使用,用来唤醒等待状态的线程继续执行;该同样位于Object类中
  • 注意调用notify/notifyAll方法并不会释放对象锁

wait/notify搭配使用

大致流程如下:

java查看jvm的线程 jvm线程状态_线程状态_02

代码示例:

public static void main(String[] args) throws InterruptedException {
  Object obj = new Object();

  Thread A = new Thread(() -> {
    synchronized (obj) {
      System.out.println("A:先拿到锁, 玩两秒");
      sleep(2);
      try {
        System.out.println("A:进入等待队列");
        obj.wait();
        System.out.println("A:被唤醒, 拜拜");
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }, "A");

  Thread B = new Thread(() -> {
    System.out.println("  B:锁被A占据, 先等着");
    synchronized (obj) {
      System.out.println("  B:线程A等待去了,我拿到锁了, 玩两秒");
      sleep(2);
      System.out.println("  B:唤醒A");
      obj.notify();
      System.out.println("  B:虽然我唤醒A, 但我接着玩, A没拿到锁, 仍无法执行, A线程状态:" + A.getState());
      sleep(2);
      System.out.println("  B:玩完了, 拜拜");
    }
  }, "B");

  A.start();
  sleep(1);
  B.start();
}

private static void sleep(int seconds) {
  try {
    TimeUnit.SECONDS.sleep(seconds);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
}

执行结果:

A:先拿到锁, 玩两秒
  B:锁被A占据, 先等着
A:进入等待队列
  B:线程A等待去了,我拿到锁了, 玩两秒
  B:唤醒A
  B:虽然我唤醒A, 但我接着玩, A没拿到锁, 仍无法执行, A线程状态:BLOCKED
  B:玩完了, 拜拜
A:被唤醒, 拜拜

死锁

示例:

  死锁描述的是多个线程都因为想获取对象锁而进入阻塞状态,且这种状态无法自己恢复过来,无限期的阻塞;

如下图所示,线程A、B都因为想获取某个对象锁而阻塞住,但是却又无法获取到,仿若形成一个圈,陷入死循环

java查看jvm的线程 jvm线程状态_死锁_03

代码示例:

public static void main(String[] args) throws InterruptedException {
  Object obj1 = new Object();
  Object obj2 = new Object();

  Thread A = new Thread(() -> {
    synchronized (obj1) {
      System.out.println("A获取到 obj1 的锁");
      sleep(2);
      System.out.println("A想获取到 obj2 的锁");
      synchronized (obj2) {
        System.out.println("A已获取到 obj2 的锁");
      }
      System.out.println("A 不获取了");
    }
  }, "A");

  Thread B = new Thread(() -> {
    synchronized (obj2) {
      System.out.println("B 获取到 obj2 的锁");
      sleep(2);
      System.out.println("B 想获取到 obj1 的锁");
      synchronized (obj1) {
        System.out.println("B 已获取到 obj1 的锁");
      }
      System.out.println("B 不获取了");
    }
  }, "B");

  A.start();
  B.start();
}

运行结果:

A获取到 obj1 的锁
B 获取到 obj2 的锁
A想获取到 obj2 的锁
B 想获取到 obj1 的锁

此时会发现程序并未运行完,且一直不会结束,容易看出线程A卡在想获取obj2的锁上,线程B与之相反,这就是死锁了,无法自己恢复过来了

八股文之产生死锁的四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

八股文之避免死锁:

   那就是破坏产生死锁的四个条件之一即可;通俗来讲,可有如下做法:

  • 按照顺序来获取资源,既然线程A是先获取obj1,再想获取obj2,那么线程B也可以按照这个顺序来,那就不会死锁了
  • 主动释放资源,线程A可选择先尝试获取obj2的锁,若没有获取到,那么该咋样就咋样,别阻塞了,继而释放自己占据的obj1的锁
  • 一次性申请所有资源,线程A可以一开始就将obj1obj2的锁都获取到,最后再都释放掉,也不会死锁了