初步了解线程-thread启动关闭详解

1,240 阅读8分钟

进程

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程

  • 进程和线程的关系
  1. 在多核CPU中,利用多线程可以实现真正意义上的并行 执行
  2. 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性
  3. 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快

线程的应用

  • 在 Java 中,有多种方式来实现多线程。继承 Thread 类、 实现 Runnable 接口、使用 ExecutorService、Callable、 Future实现带返回结果的多线程。

继承Thread类

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它会启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

实现 Runnable 接口创建线程

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口

实现Callable接口通过FutureTask包装器来创建Thread线程

有的时候,我们可能需要线程在执行完成以后,提供一个返回值给到当前的主线程,主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程了。

通过线程池

有的时候如果我们每增加一次请求就创建一个新的线程,当请求结束再把该线程关闭。这回造成CPU的性能浪费。所以我们就引入了线程池,每次获取线程从线程池拿。

多线程的实际应用场景

其实在工作中很少有场景能够应用多线程了,因为基于业务开发来说,很多使用异步的场景我们都通过分布式消息队列来做了。但并不是说多线程就不会被用到, 如果有看一些框架的源码,会发现线程的使用无处不在。

  • 以zookeeper中的责任链设计模式为例
  • 普通的责任链模式就是单线程,请求必须等到全部责任链执行完毕后才返回。

  • 如果我们使用线程优化

  • 我们每个职责都开启一个线程,只有当请求来的时候在执行,没有请求的话就一直阻塞。这就是各种消息中间件的雏形。

线程的生命周期

Java 线程既然能够创建,那么也势必会被销毁,所以线程是存在生命周期的,那么我们接下来从线程的生命周期开始去了解线程。

  • 线程一共有 6 种状态,源码中写的很清楚
  1. NEW:初始状态,线程被构建,但是还没有调用start方法。
  2. RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
  3. BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权。
  4. WAITING:等待状态。
  5. TIMED_WAITING:超时等待状态,超时以后自动返回。
  6. TERMINATED:终止状态,表示当前线程执行完毕 。
  • 可以大体总结一个图

  • 举一个例子来证明一下

  • 看一下线程当前状态(TIMED_WAITING)

通过上面的分析,我们了解到了线程的生命周期,现在在整个生命周期中并不是固定的处于某个状态,而是随着代码的执行在不同的状态之间进行切换

线程的启动

前面我们通过一些案例演示了线程的启动,也就是调用 start()方法去启动一个线程,当run方法中的代码执行完毕以后,线程的生命周期也将终止。调用start方法的语义是当前线程告诉JVM,启动调用start方法的线程。

线程的启动原理

  • 我们看到调用 start 方法实际上是调用一个 native 方法 start0()来启动一个线程。

没下OPENJDK源码的可以从下面的位置看native源码hg.openjdk.java.net/jdk8/jdk8/j…

从这段代码可以看出 ,start0(),实际会执行JVM_StartThread方法,这个方法是干嘛的呢? 从名字上来看,似乎是在JVM层面去启动一个线程,如果真的是这样,那么在 JVM 层面,一定会调用Java中定义的run方法。那接下来继续去找找答案。我们找到 jvm.cpp这个文件;这个文件需要下载hotspot的源码才能找到。

JVM_ENTRY 是用来定义 JVM_StartThread 函数的,在这个函数里面创建了一个真正和平台有关的本地线程. 本着打破砂锅查到底的原则,继续看看 newJavaThread做了什么事情,继续寻找JavaThread的定义在hotspot的源码中 thread.cpp文件中1558行的位置可以找到如下代码

这个方法有两个参数,第一个是函数名称,线程创建成功之后会根据这个函数名称调用对应的函数;第二个是当前进程内已经有的线程数量。最后我们重点关注与一下 os::create_thread,实际就是调用平台创建线程的方法来创建线程。 接下来就是线程的启动,会调用 Thread.cpp 文件中的 Thread::start(Thread* thread)方法。 start方法中有一个函数调用:os::start_thread(thread);调用平台启动线程的方法,最终会调用Thread.cpp文件中的JavaThread::run()方法。

线程的终止

线程的终止,并不是简单的调用stop命令去。虽然api仍然可以调用,但是和其他的线程控制方法如suspend、 resume 一样都是过期了的不建议使用,就拿 stop 来说,stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。要优雅的去中断一个线程,在线程中提供了一个 interrupt 方法。

interruptf方法

当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查资深是否被中断来进行相应,可以通过 isInterrupted()来判断是否被中断。

这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

interrupted方法

上面的案例中,通过 interrupt,设置了一个标识告诉线程可以终止了,线程中还提供了静态方法 Thread.interrupted()对设置中断标识的线程复位。比如在下面的案例中,外面的线程调用thread.interrupt来设置中断标识,而在线程里面,又通过Thread.interrupted 把线程的标识又进行了复位

Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号, 但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是false,这就是复位的原因。

InterruptedException异常

除了通过 Thread.interrupted 方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出 InterruptedException 异常的方法,在 InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出InterruptedException,这个时候如果调用isInterrupted方法,将会返回false。

  • 如果在阻塞状态发现Interrupted为true则会抛出InterruptedException异常

需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,

  1. 直接捕获异常不做任何处理
  2. 将异常往外抛出
  3. 停止当前线程,并打印异常信息
  • 运行测试代码

interrupted方法原理

通过下面的代码分析可以知道,thread.interrupt()方法实际就是设置一个interrupted 状态标识为 true、并且通过ParkEvent的unpark方法来唤醒线程。

  1. 对于synchronized阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被park
  2. 在调用ParkEvent的park方法之前,会先判断线程的中断状态,如果为true,会清除当前线程的中断标识
  3. Object.wait 、 Thread.sleep 、 Thread.join 会抛出 InterruptedException