JUC并发编程02——线程原理(运行机制,线程调度,未来优化)

1.线程原理

1.运行机制

Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程

  • 线程的 CPU 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park 等方法

程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的

当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

JVM 规范并没有限定线程模型,以 HotSopot 为例:

  • Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
  • 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能

Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程


1.线程调度

线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择)

协同式线程调度:线程的执行时间由线程本身控制

  • 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题
  • 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃

抢占式线程调度:线程的执行时间由系统分配

  • 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用
  • 缺点:无法主动为某个线程多分配时间

Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用

说明:并不能通过优先级来判断线程执行的先后顺序


2.未来优化

内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多个用户线程映射到一个内核级线程),被设计为协同式调度,所以叫协程

  • 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程
  • 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限

有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器:

  • 执行过程:用于维护执行现场,保护、恢复上下文状态
  • 调度器:负责编排所有要执行的代码顺序

2.线程状态

进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态

线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征
Runnable(可运行)线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法)
Blocked(阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒
Timed Waiting (限期等待)有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait
Teminated(结束)run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡

  • NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE

  • RUNNABLE <–> WAITING:

    • 调用 obj.wait() 方法时

      调用 obj.notify()、obj.notifyAll()、t.interrupt():

      • 竞争锁成功,t 线程从 WAITING → RUNNABLE
      • 竞争锁失败,t 线程从 WAITING → BLOCKED
    • 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待

    • 当前线程调用 LockSupport.park() 方法

  • RUNNABLE <–> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)

  • RUNNABLE <–> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败


3.查看线程

Windows:

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

Linux:

  • ps -ef 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java:

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)