0. Thread线程的状态转换以及相关API

先来个图再说明
线程就六种状态转换关系上面的图也画出来了.

如果是JDK开发环境可以使用在bin目录下的jstack命令查看相关的线程状态.

命令jstack pid 后面是笔者使用了过滤只查看状态, 可以看到图中有RUNNABLE, WAITING, TIEMD_WAITING.

  1. 几种状态解释:
    1. NEW: Thread对象的实例刚刚创建出来的状态属于NEW.
    2. RUNNABLE: 这个状态属于可运行的状态, 但是RUNNABLE并不表示一定在运行, 当前CPU时间片正在运行A线程, 这时候正在运行, 如果系统调度这个CPU去执行其它任务了, 那么这个A线程就不处于运行状态了, 但它依旧处于RUNNABLE状态等待被系统调度回来继续执行. 所以在RUNNABLE内部还有READY和RUNNING两个状态.
      RUNNABLE状态可以由别的状态转换而来. 就如图所示, 在以下几种情况会转换到RUNNABLE. ①当处于BLOCKED阻塞状态的线程获取到了所需资源的时候;②当处于WAITING状态的线程被各种各样的方式唤醒,又或者WAITING状态线程被中断后获取到了资源.
    3. TERMINATED: 终止, Thread的run方法执行完毕或者在其他状态中出现了异常都会走到这个环节, 这个环节是不可逆的,到这里就意味着线程结束了(在线程池技术中, 执行完任务后线程并不一定会结束).
    4. BLOCKED: 阻塞. 阻塞总结一句话就是, 程序因获取不到资源而已停在那个地方等待资源就绪. 上面说的锁, 其实锁也是可以理解为一种资源的.synchronized相关操作或者IO读取相关操作都可能使线程进入这个状态.
    5. WAITING/TIME_WAITING: 等待/超时等待, 这两个一起说, 从字面就很好理解, 前者会一直等待下去直到被某种机制唤醒, 后者是带有超时机制的等待, 等待到一定时长未被唤醒, 就会自行继续转换到RUNNABLE状态等待CPU调度继续运行.WAITING其实一种挂起机制.
  2. 阻塞和挂起
    上面分析到阻塞, 而WAITING实际是一种挂起, 阻塞和挂起有什么异同点呢?
    1. 阻塞是由于当前线程获取不到资源, 程序无法继续执行, 系统把CPU分配给另外处于RUNNABLE状态的线程去执行, 而当前线程现在所处的暂停状态就是阻塞状态. 阻塞是一种被动的状态, 一般情况都是系统决定的, 当它获取到资源的时候就会自动转为RUNNABLE状态.
    2. 挂起是一种系统或者程序的主动行为, 当进入挂起状态后, 程序继续恢复运行需要等待它被唤醒, 或者设置的超时时间到达.
  3. Thread相关API以及对应的状态转换分析.
    1. sleep以及相应的重载方法.
      sleep需要传入时间参数, 会挂起当前线程使线程由RUNNABLE->TIMEWAITINNG,直到到达相应的时间, 转为RUNNABLE等待等待系统调度执行. 有两点需要注意的是:①sleep方法并不会释放拥有的资源比如锁.②sleep方法会响应中断, 对sleeping线程调用中断, 线程会立即抛出InterruptedException异常.
      public class Main {
          public static Object lock = new Object();
          public static void main(String[] args) throws InterruptedException {
              Thread t1 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          System.out.println("t1执行");
                          TimeUnit.MILLISECONDS.sleep(300);
                          System.out.println("t1执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              Thread t2 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          System.out.println("t2执行");
                          TimeUnit.MILLISECONDS.sleep(300);
                          System.out.println("t2执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              t1.start();
              // 保证t1先启动获取到锁.
              TimeUnit.MILLISECONDS.sleep(100);
              t2.start();
              // t1.interrupt();
              while (true);
         }
      }  // t1执行->t1执行完毕->t2执行->t2执行完毕
      
      以上代码, t1先获取到了lock, 然后调用了sleep方法, 这个时候t2线程会因为获取不到锁继续处于BLOCKED状态而不执行,这也印证了sleep并不会释放锁的说法. 另外上面如果把t1.interrupt()注释打开会发现, t1线程并不会睡预期的时间, 在调用interrupt之后会立即恢复到RUNNABLE状态. 所以, sleep方法不会释放资源, 并且会立即响应中断.
    2. join方法
      join方法是当前线程等待调用join方法那个对象的线程结束, join方法同样也会立即响应对主线程的中断. 底层其实是Object对象的wait()方法, 稍后会将这个方法, 调用了join方法当前线程会从RUNNABLE->WAITING/TIMED_WAITING状态. join也有几个带参数的重载方法, 为了支持超时机制.
      public class Main {
          public static Object lock = new Object();
          public static void main(String[] args) throws InterruptedException {
              Thread mainThread= Thread.currentThread();
              Thread t1 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          System.out.println("t1执行");
                          TimeUnit.MILLISECONDS.sleep(100);
                          // 输出TIME_WAITING, 
                          System.out.println(mainThread.getState()); 
                          // 对主线程进行中断会立即响应
                          // mainThread.interrupt();
                          System.out.println("t1执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              t1.start();
              t1.join(500);
              System.out.println("2222");
          }
      }
      
      一开始的时候其实有些迷惑,为什么调用的是t1.join反而导致了main主线程阻塞以及上面那句"当前线程等待调用join方法那个对象的线程结束", 先解释后者, 在主线程里面main线程是当前线程, 当前线程中调用了t1.join, 所以就是等待t1线程运行结束, 这里解释清楚了? 那么为什么调用t1.join为什么会使调用线程挂起. 先看看join的源码.
      /*
      	就在这里面解释了, 我们发现这是一个synchronized修饰的同步方法
      	同步方法持有的对象就是当前对象t1
      	在方法内部调用了从Object对象继承而来的wait(),
      	提前预告wait方法也会挂起线程, 把当前线程转入WAITING/TIME_WAITING
      	t1.join是在主线程执行的, 所以挂起的自然是主线程, 而不是t1.start线程
      	这里要把t1当做一个普通的类, 而不是线程类理解
      */
      public final synchronized void join(long millis)
          throws InterruptedException {
              long base = System.currentTimeMillis();
              long now = 0;
      
          if (millis < 0) {
              throw new IllegalArgumentException("timeout value is negative");
          }
      
          if (millis == 0) {
              while (isAlive()) {
                  wait(0);
              }
          } else {
              while (isAlive()) {
                  long delay = millis - now;
                  if (delay <= 0) {
                      break;
                  }
                  wait(delay);
                  now = System.currentTimeMillis() - base;
              }
      	}
       }		
      
    3. yield的方法, 这是一个静态方法
      这个方法我在实际工作中真的真的很少用状态转换图中也画了这个方法.倒是在源码中看过, 在ConcurrentHashMap的initTable方法中当其它线程在初始化Map的时候当前线程让出时间片.调用这个方法的线程会让出当前的CPU时间片, 可注意了它只会从RUNNING->READY状态, 并不会进入其它状态. 当线程执行了yield方法可能没什么区别继续执行, 也可能在READY状态等待系统调度. 现在资源调度大都是抢占式的, yield当前线程让出CPU时间并不是谦让友好的把机会让给其它线程执行, 让出之后它将和其它READY状态一起抢占CPU执行权限. **所以yield方法只是让出然后大家再一起抢.**另外值得注意的是yield并不会放弃持有的资源, 比如锁. 这个例子就不是很好举例了
      /*
       当加synchronized 锁.
       多启动几个线程多尝试几次你会发现, 每一个线程的执行和执行完毕都是成对出现
       去掉锁的代码后, 你会发现执行和执行完毕不是成对出现的.
       所以yield并不会释放锁.
      */
      public class Main {
      	public static Object lock = new Object();
      	public static void main(String[] args) throws InterruptedException {
              Thread currentThread = Thread.currentThread();
              Thread t1 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          Thread.yield();
                          System.out.println("t1执行");
                          TimeUnit.MILLISECONDS.sleep(100);
                          System.out.println("t1执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              Thread t2 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          Thread.yield();
                          System.out.println("t2执行");
                          TimeUnit.MILLISECONDS.sleep(100);
                          System.out.println("t2执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              Thread t3 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          Thread.yield();
                          System.out.println("t3执行");
                          TimeUnit.MILLISECONDS.sleep(100);
                          System.out.println("t3执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              t1.start();
              t2.start();
              t3.start();
              while (true);
          }
      }
      
      
    4. 至于其它像stop, resume这种方法就不讲了, 都是被遗弃的方法.不建议使用, 要听过来人的建议少吃亏. Thread类本身常用的API就这几个.
  4. 其它API导致的Thread状态转换
    1. Object类的wait方法以及重载方法.
      Object的wait方法最终调用的是由native关键字修饰的wait方法, 看过以前java调用dll或者.so库–JNI那篇文章的就知道是怎么回事了, 这是一个本地方法.这里就不打开虚拟机源码一探究竟了, 就从应用层面讲起.
      wait方法会使当前线程从RUNNABLE状态进入TIME_WAITING/WAITING状态, wait方法也是有超时机制的. wait方法的重点:
      wait方法必须使用在同步方法或者同步代码块中. 原因嘛这里还有个知识点, 就是Lost Wake-Up. 直译丢失唤醒.
      // Lost Wake-Up问题
      // 有个知识前提, 等下会说, 先讲一下, wait进入TIME_WAITING之后
      // 可以使用notify唤醒. wait睡觉->notify叫醒他
      // 以伪代码方式演示Lost Wake-Up
      while {
      	A 穷人查看银行卡 没钱 {
      		没钱睡觉
      	} 有钱 {
      		有钱吃饭
      		没钱
      	}
      }
      while(true){
      	B 老板查看A银行卡 没钱 {
      		打钱给A
      		去梦中叫A起床吃饭
      	} 有钱 {
      		不管A的死活
      	}
      }
      
      上面的代码正常情况下, A发现没钱就睡觉, B发现A没钱就打钱叫他起来吃饭, 一直循环, 岁月静好, 但是在一种特殊的情况下A会饿死. 第一步: A查看没钱, A正准备睡觉的, 系统调度把CPU执行权限给B了, 这个时候, B老板查看A银行卡没钱, 打钱给A, 去梦中叫醒A(这个时候A才第一步发现没钱还没开始睡觉就被切换了), A没睡叫醒失败. 接下来切换回A, A刚才还没睡根本没做梦,所以收不到老板的叫醒服务, 以后老板每次查询A都有钱就不管A, 最后A死了. 这就是Lost wake-up问题, 由于竞态条件的存在我们无法保证睡觉和叫醒之间的顺序, 所以导致了叫醒在睡觉之前而丢失, 导致永远无法唤醒. 在Java里解决这种问题的方式也很简单, 把wait和notify放在持有同一把锁的同步代码块内部, 这样就严格保证了顺序. 另外Java做了限制在无锁的环境下使用wait会抛出异常IllegalMonitorStateException.
      ②wait方法和sleep方法区别不大? 不是的, wait方法会释放锁, 而sleep方法不会.
      /*
      	t1执行
      	t2执行
      	t2执行完毕
      	t1执行完毕
      	观看执行结果发现, 在t1执行之后调用了lock.wait(), t2线程执行了
      	而在同步代码块中t1, t2都持有同一把对象锁, 所以wait的确会释放锁.
      */
      public class Main {
          public static Object lock = new Object();
          public static void main(String[] args) throws InterruptedException {
              Thread t1 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          System.out.println("t1执行");
                          lock.wait();
                          TimeUnit.MILLISECONDS.sleep(100);
                          System.out.println("t1执行完毕");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              Thread t2 = new Thread(() -> {
                  synchronized (lock) {
                      try {
                          System.out.println("t2执行");
                          TimeUnit.MILLISECONDS.sleep(100);
                          System.out.println("t2执行完毕");
                          lock.notify();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              });
              t1.start();
              TimeUnit.MILLISECONDS.sleep(100);
              t2.start();
              while (true);
          }
      }
      
      ③wait方法醒来和sleep略有不同, sleep不会释放锁资源, 所以sleep不会有重新获取锁的过程, 而wait会释放掉锁资源, 再次执行的时候需从新获取锁资源. 当获取到锁的时候转入RUNNABLE, 没有获取到锁就进入了BLOCKED状态.
      wait同样也会响应中断状态, 但是不一定会像sleep一样立即抛出InterruptedException异常. 原因和第三点一样, 当wait线程被中断的时候, 它醒来可能并没有获取到锁, 会转入BLOCKED状态, 直到获取到了锁了才会转入RUNNABLE抛出中断异常.
    2. Object类的notify/notifyAll方法
      这两个方法的主要作用就是唤醒使用了wait而进入WAITING/TIME_WAITING的线程, notify只唤醒一个线程, 而notifyAll会唤醒所有需要当前锁的资源. 这里其实为什么要设置两个方法呢? 就算唤醒了所有线程, 由于是synchronized同步代码最终也只有一个线程获取了锁拥有执行权限.
      这样是有道理的, 可以避免死锁, wait会将线程状态转换为WAITING/TIMEHU_WAITING, 处于这状态的线程是不会尝试去获取锁的. notify只唤醒了一个线程获取锁转入了RUNNABLE状态, 唤醒线程一旦因为某些原因没有继续唤醒其它线程, 其它线程也就再也没机会获取到锁了. notifyAll会将所有需要当前锁的线程唤醒, 然后他们去竞争锁, 获取到锁的转入RUNNABLE, 没有获取到锁的转入BLOCKED, 而BLOCKED状态当有资源释放的时候是可以尝试再次去获取锁.
      这里wait/notify讲完了回头说说Lost wake-up的原因, wait和notify有严格的顺序, 必须先wait再notify, 如果先notify,notify会丢失, 再wait就很容易导致死锁.
    3. LockSupport的 park/unpark静态方法方法
      java.util.concurrent并发包下的LockSupport类的线程挂起恢复也可以改变线程的状态, 这个两个方法是基于Unsafe类的对应实现的.
      1. park方法不会释放锁, 中断不会抛出InterruptedException异常, 但在可以通过Thread.isInterrupted()判断是否被中断过. park方法会将线程由RUNNABLE转为WAITING状态. 还有一个值得注意的问题, 如果线程在调用park方法之前被中断过就算你清除了中断标志, 线程也还是不会park挂起的.
      2. unpark方法需要传入线程对象的引用, unpark方法会唤醒被挂起的线程. 对比Object的notify方法随机唤醒一个线程, unpark方法可以指定被唤醒的线程. unpark/park方法并没有严格的顺序, 先调用unpark方法再调用park方法会直接执行不会进入WAITING状态, 并且在park之前多次调用unpark方法也只会在第一次park的时候直接执行.需要注意的是, unpark虽然可以在park之前调用, 但是也得在线程调用start()方法之后调用, 在线程创建之前调用unpark是没什么用处的.

1. Thread其它API

  1. isInterrupted()这个方法返回线程是否被中断过.
  2. interrupted()这个方法也这个线程是否被中断过, 和isInterrupted()底层都是调用的带参的isInterrupted(boolean ClearInterrupted), isInterrupted()方法中的ClearInterrupted为false, 而interrupted()中的ClearInterrupted为true, 这个参数的区别也就是这个两个方法的区别. 中断实际上是就是设置一个标志位的值, interrupted获取标志位并且清除中断标志恢复到未中断状态, 而isInterrupted()则只读取中断标志, 不清除.
  3. 上文说到关于Thread的stop等方法被废止, 那么现在如何优雅的通知退出一个线程呢, 可以用中断的方式.
    public class Main {
    	public static void Main(String ... args){
    		Thread t1 = new Thread( ()-> {
    			while(!Thread.currentThread().isInterrupted()) {
    				System.out.println("running...")
    			}
    		}).start();
    		// 只需要在外层调用中断就可以退出线程了.
    		t1.interrupted()
    	}
    }
    
    关于中断的详细内容就不在这里展开了.
  4. Thread.currentThread()获取当前线程.
  5. Thread.interrupt()中断线程, 会让不少处于阻塞挂起的线程醒过来.上面已经介绍过了.
  6. start()方法和run()方法
    开启新线程必须使用start()方法, .run()方法只会直接调用, 而不采用多线程执行.

2. 总结

以上只是从Java应用层面描述了Thread相关的概念常见API并没有深入底层探究, 其实关于线程的很多东西特性 还是需要深入JDK源码才会得到解释, 这里很多地方只描述了现象没有原因. 说几个问题:

  • 上面一点的Thread.start()为什么最终会执行run()方法, 在跟源码的时候最终跟到了一个叫start0()的方法, 也没有发现调用了run()方法. 不知各位在学习的时候是否有这样的疑惑.其实在Jvm源码层面是有调用的.下图最后一句thread->run()就是调用run()方法.
    在这里插入图片描述

  • 为什么interrupt()中断线程后park不会挂起当前线程.

上面就抛砖引玉了, 如果哪天有心情有时间可能会写一篇JDK源码是如何实现上面的各种接口函数.
我一直都认为兴趣和好奇, 能让我们走得更远.

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐