并发编程 || Java线程详解

Posted hill1126

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程 || Java线程详解相关的知识,希望对你有一定的参考价值。

  • 通用线程模型

在很多研发当中,实际应用是基于一个理论再进行优化的。所以,在了解JVM规范中的Java线程的生命周期之前,我们可以先了解通用的线程生命周期,这有助于我们后续对JVM线程生命周期的理解。

首先,通用的线程生命周期有五种,分别是:新建状态(NEW)、可运行状态(RUNNABLE)、运行状态(RUN)、休眠状态(SLEEP)、终止状态(TERMINATED)。生命流程如下图所示:

技术图片

  1. 新建状态(NEW)。线程在此状态,仅仅是在编程语言层面创建了此线程,而在真正的操作系统中是没有创建的。所以,它在这个状态下是无法获得CPU的执行的权限的。
  2. 可运行状态(RUNNABLE)。线程到达此状态,意味着它已经被操作系统创建,该线程获得被CPU执行的资格,但此时还没有被CPU执行相关操作。
  3. 运行状态(RUN)。线程获得CPU的执行权限,在一个特定的时间片内执行,线程仅在这个时间片内被称为运行状态。
  4. 休眠状态(SLEEP)。当线程调用了某个阻塞API或者等待IO操作的时候,它会释放当前CPU的执行权限,进入休眠状态。此时,线程没有获取CPU执行的资格,只有当该线程被唤醒时,线程才能进入RUNNABLE状态。
  5. 终止状态(TERMINATED)。当线程完成程序任务或者出现异常的时候,它就会进入终止状态。一个线程的使命就此结束。
  • JVM线程模型

JVM中的线程模型对于上面的通用线程模型进行了一些特有的分类和合并,它们的类别如下:

  1. 新建状态(NEW) 
  2. 可运行/运行状态(RUNNABLE)
  3. 阻塞状态(BLOCK)
  4. 等待状态(WAITING)
  5. 有限等待状态(TIMED_WATING)
  6. 终止状态(TERMINATED)

而JVM中的状态结合到通用状态中可以如下图所示理解:

技术图片

由上图可以看出,JVM讲运行中的线程和等待运行的线程归为一类,因为JVM不关心操作系统层面的调度,所以把这两个状态合并了。而Block、Wating、Timed_Wating三个状态在操作系统层面都为休眠状态没有区别。所以,这三种状态都没有获得CPU执行的资格

  • 线程状态的转换

从上面的JVM线程生命周期图分析,我来说说一个线程从新建到消亡的状态转变中,到底会发生什么事情。

  1. 从NEW到RUNNABLE

这个很简单,线程在被显式声明后,在调用start()方法前,这段时间都被称为NEW状态。如下代码所示

1   Runnable task = ()-> System.out.println("线程启动");
2         //创建一个线程,线程状态为NEW状态
3         Thread thread = new Thread(task);

 

  2.从RUNNABLE到BLOCK

从RUNNABLE到BLOCK状态的转变只有一种途径,那就是在有synchronized关键字的程序当中。当线程执行到此,没有获取到synchronized的隐式锁,线程就会从RUNNABLE被阻塞为BLOCK状态。当阻塞中的线程获取到synchronized隐式锁时,它又会转变为RUNNABLE状态。

问:当线程调用阻塞API时,它的状态会不会改变呢? 例如我们日常说的:ServerSockt的accept()、Scanner的next()方法等。

答案是:对于JVM层面来说,调用这些方法的线程依旧在RUNNABLE状态,因为JVM对于等待CPU资源或等待IO资源并不关心,所以把他们归为RUNNABLE状态。而对于操作系统层面来说,线程则属于休眠状态。(对于较真的同学,可以通过jstack指令查看调用阻塞API是的线程是什么状态

  3.RUNNABLE到WATING

其中有三种场景会使线程转换为WATING状态:

  1. synchronized的内部,调用wait()方法。
  2. 调用Thread.join()方法。该方法的意思是,当一个A线程调用了B线程的join方法,那么A线程就必须等待B线程执行完毕,此时,A线程就为WATING状态。需要注意的是,如果是B线程自己调用自己的join方法。那么就会造成自己等待自己的局面,从而使线程无限等待。
  3. 调用LockSupport.park()方法。这个方法看上去很陌生,但是其实jdk中的并发包中的锁都是由它实现的。例如:我们日常中用到的lock.lock()方法,condition.await()方法,其底层都是通过调用这个方法运行的。

 

  4.RUNNABLE到TIMED_WATING状态

其实从字面上就可以看出,TIMED_WATING状态与WATING状态的差别就是TIMED_WATING会在有限的时间内等待。所以,在WATING方法中的大多数方法,只要加上一个时间参数,就会触发TIMED_WATING这个状态。具体的有:

1         Thread.currentThread().join(millis);
2         Thread.sleep(millis);
3         Obj.wait(timeout);
4         LockSupport.parkNanos(Object blocker, long deadline);
5         LockSupport.parkUntil(long deadline);

 

  5.RUNNABLE到TERMINAL状态

当线程顺利的完成run()方法中的任务,就会进入TERMINAL状态。同时,当线程抛出没有处理异常的时候,线程同样会变为TERMINAL状态。那如果业务上需要我们主动的终止线程,那应该怎么做呢?

 

  • 终止线程的正确姿势

在以往的jdk中,它提供了一些诸如:stop()、suspend()、resume()方法,这些方法都会直接把线程关闭,不给线程任何处理的机会。这样做的风险可想而知,所以这些方法早就已经被标记为过时方法,不推荐使用,我也没有详细去了解。那么,我们现在想要终止一个线程,该怎么做呢?

答案就是:通过调用thread.interrupt();方法来达到终止线程的目的。当然,并不是调用interrupt()就会关闭线程,我们通过一个图来了解一下具体的流程是怎样的。

技术图片

如图所示,线程状态的不同,对于Interrupt方法的处理也不同。流程在图中已经比较清晰,我再列出几个重点:

  1. Interrupt方法仅仅是把线程是否被中断的标识设置为true
  2. 当抛出InterruptedException时会把中断标志清除
  3. 被中断的线程状态不同,做出的响应也会不同。运行时线程需要主动检测、等待时的异常会抛出异常(这里可以类比硬件中的中断,相当于一个信号)。
  • 总结

我们在日常开发中,一旦遇到多线程的bug,分析线程dump信息是一个非常重要的手段。而了解线程运行时的状态,有助于在分析信息时正确的判断线程的状况。同样,我们可以通过jstack命令或者Java VisualVM可视化工具来查看线程的具体信息。

 

以上是关于并发编程 || Java线程详解的主要内容,如果未能解决你的问题,请参考以下文章

java并发编程synchronized详解

Java并发编程Thread详解

Java 并发编程一文详解 Java 中有几种创建线程的方式

Java并发编程——ThreadLocalAQS线程安全集合类

Java并发编程——ThreadLocalAQS线程安全集合类

GitHub上260K Stars的P8架构师纯手写的Java高并发编程详解