提起线程,你不了解的那些事

  对
MESI 缓存一致性协议,有了初步的了解后,本文来介绍偏内容性的一个话题:
线程。 Java 开发者对多线程一定不会陌生,那么线程到底是啥?Java 又是如何利用多线程来调度/使用 CPU 来完成操作的呢。

1.什么是线程

  线程(英语:thread)是操作系统调度CPU最小单位。它被包含在进程之中,是进程中的实际运作单位,进程是系统分配资源的基本单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。[BaiDu:线程]

  每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)

  JVM 本身是没有调度 CPU 的能力。JVM 需要安装在操作系统上,JVM 中的 Thread 线程是依附于操作系统的(Java的线程在JDK1.2版本之前,是由用户自己去创建、维护、调度线程的;1.2版本后,JVM 线程依赖于底层的操作系统)

线程又分为两种:

  1. 内核级线程(Kernel-Level Thread)  简称:KLT
  2. 用户级线程(User-Level Thread)  简称:ULT

1.用户级线程

  用户线程,指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心。应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。

  我们常说的线程,指的就是用户级线程。

线程模型

  一个线程阻塞将使得整个进程(包括它的所有线程)阻塞?这个说法也是不全面的,回答这个问题,就需要简单了解一下线程模型,线程模型分为:

  1. 多对一用户级线程模型
  2. 1对1内核级线程模型
  3. 多对多两级线程模型

1.多对一用户级线程模型

  • 线程的创建、调度、同步,由所属进程的用户空间线程库实现
  • 用户态线程,对内核几乎是透明的(许多操作不需要内核接管)
  • 但线程总要有一些操作经过内核,比如系统调用
  • 不需要频繁的内核态/用户态切换,处理速度非常快
  • 该模式下,当进程的某个线程,系统调用(比如I/O)阻塞时,该进程也会阻塞
  • 原因:该模式下,进程的所有线程,都对应一个内核调度实体(KES),并且内核不知道这个进程有哪些线程。KES无法将其他线程,调度到其他处理器上。该进程(所有的线程)被阻塞,直到本次系统调用(比如I/O)结束

2.一对一内核级线程模型

  • 每个用户线程都对应一个的内核调度实体
  • 内核会对每个线程进行调度,可以调度到其他处理器上
  • 线程每次操作会在用户态和内核态切换
  • 线程数量过多时,对系统性能有影响

3.多对多两级线程模型

  • 每个用户线程拥有多个内核调度实体
  • 多个用户线程也可以对应一个内核调度实体
  • 实现该模型非常复杂

目前(linux)基本上都采用一对一模型

总结

  • 线程系统调用阻塞时,在多对1用户级线程模型下,会导致所属进程阻塞。
  • 在1对1或多对多模型下,不会导致该问题的发生。
  • 如果是单进程单线程的话,不管哪个模型,都会阻塞的。

2.内核级线程

  内核线程,由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。

3.系统空间

  系统空间,分为内核空间用户空间两种。

  我们日常安装的 exe ,都属于进程,它是没有资格操作底层的内核空间的。只能通过内核空间提供的交互接口,去操作内核空间。内核空间类似一个封闭的空间,我们不能随便瞎操作,否则很容易造成系统瘫痪。就类似于 Windows 系统的 C 盘重要文件,我们不能随便删除一样。

  比如现在我们通过 new Thread 的方式创建一个线程,运行在 JVM 上,这属于一个用户级线程。此时就需要 JVM 进程去调用内核空间提供交互接口,对CPU底层进行操作。

  在 CPU 中,以 Intel x86为例,x86处理器是通过Ring级别来进行访问控制的。级别共分4层,RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3RING0层拥有最高的权限RING3层拥有最低的权限。按照Intel原有的构想,应用程序工作在RING3层,只能访问RING3层的数据,操作系统工作在RING0层,可以访问所有层的数据,而其他驱动程序位于RING1、RING2层,每一层只能访问本层以及权限更低层的数据。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。 [本段内容,摘自:BaiDu:Ring0]

  用户级线程调度 CPU 资源过程,如图所示:
在这里插入图片描述
  用户级线程,由进程来管理所有的线程,然后通过交互接口来调用内核空间,去使用CPU资源。JDK 采用的是哪种线程方式( JDK 1.2版本前,使用的是 ULT;JDK 1.2版本之后,使用的是KLT)。如何创建用户级线程,在Java中就是通过 new Thread() 的方式

2.Java 线程与内核线程的关系

  Java 线程和内核线程,是1:1的映射关系。

  此处以 Linux 服务器为例,我们通过 new Thread() 创建的用户线程,然后调用 Linux为我们提供的工具类库(pThread类库)来调用交互接口,来调度 Linux 操作系统,由内核空间来创建一个内核栈,去管理线程,最终去调用CPU底层资源。(以 Linux 系统为例,图中的库调度器,指的就是 pThread 类库)
在这里插入图片描述

3.Java为什么用并发

  并发编程的本质其实就是利用多线程技术,在现代多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。除此之外,面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
  即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

  并发不等于并行并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

并发优点:

  1. 充分利用多核CPU的计算能力
  2. 方便进行业务拆分,提升应用性能

并发产生的问题:

  1. 高并发场景下,导致频繁的上下文切换
  2. 线程安全问题,开发考虑不全面,很容易造成死锁

1.什么是线程上下文切换

  线程在运行过程中,所有的运行信息都是保存在寄存器CPU中。线程 t1 和 t2 进行切换时,线程会先申请 CPU 时间片,切换过程中,线程 t1 失去使用CPU时间片,线程此时会处于暂停状态线程 t1 计算的中间结果都是存储在我们的缓存中(CPU缓存),这时候要从线程 t1 切换到线程 t2 。

  如果此时线程 t1 还没有执行完,就切换到线程 t2,就需要将线程 t1 执行的中间状态信息(包括指令集、运行到哪行指令、中间变量数据等),都需要通过 CPU 总线,将这些数据回写到主内存中去。【写到主内存的内核空间中的TSS任务状态段中去(以Linux为例)】

  线程 t2 执行完后,线程 t1 此时获得了CPU使用权,线程 t2 向线程 t1 切换时,会去主内存中查询线程 t1 对应的 TSS 任务状态段信息,再次加载到CPU缓存、寄存器,再去接着往下执行

这就叫做线程上下文切换

2.死锁检测方法

  可通过 jps -l 查看当前程序的 pid,然后使用 jstack pid 查看是否死锁
  在这里插入图片描述

4.Java线程生命周期状态

文末为大家附上 Java 线程的六大状态,以及状态之间如何进行切换

  1. 初始化(NEW)
  2. 运行(RUNNABLE)
  3. 等待(WAITING)
  4. 超时等待(TIMED_WAITING)
  5. 阻塞(BLOCKED)
  6. 终止(TERMINATED)
    在这里插入图片描述

更多生命周期状态内容,可以参考以下两篇文章:

  1. Java多线程的六种状态
  2. 线程的生命周期包括哪几个阶段

参考文章:

  1. 线程系统调用阻塞是否导致进程阻塞的问题

  2021-11-16,《提起线程,你不了解的那些事》已更新,接下来涉及到的 JMM 内存模型、MESI 协议在 JMM 内存模型中的8种交互操作等内容,如有需要,请持续关注《并发编程》板块!!!


博主写作不易,加个关注呗

求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙

我不能保证所写的内容都正确,但是可以保证不复制、不粘贴。保证每一句话、每一行代码都是亲手敲过的,错误也请指出,望轻喷 Thanks♪(・ω・)ノ

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>