提起线程,你不了解的那些事
Posted 扛麻袋的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提起线程,你不了解的那些事相关的知识,希望对你有一定的参考价值。
对 MESI 缓存一致性协议,有了初步的了解后,本文来介绍偏内容性的一个话题:
线程
。 Java 开发者对多线程一定不会陌生,那么线程到底是啥?Java 又是如何利用多线程来调度/使用 CPU 来完成操作的呢。
1.什么是线程
线程
(英语:thread)是操作系统调度CPU
的最小单位
。它被包含在进程之中,是进程中的实际运作单位,进程是系统分配资源的基本单位
。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
。[BaiDu:线程]
每个线程都有一个程序计数器
(记录要执行的下一条指令),一组寄存器
(保存当前线程的工作变量),堆栈
(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)
JVM 本身是没有调度 CPU 的能力。JVM 需要安装在操作系统上,JVM 中的 Thread 线程是依附于操作系统的
(Java的线程在JDK1.2版本之前,是由用户自己去创建、维护、调度线程的;1.2版本后,JVM 线程依赖于底层的操作系统)
线程又分为两种:
- 内核级线程(Kernel-Level Thread) 简称:
KLT
- 用户级线程(User-Level Thread) 简称:
ULT
1.用户级线程
用户线程,指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心
。应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞
。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
我们常说的线程,指的就是用户级线程。
线程模型
一个线程阻塞将使得整个进程(包括它的所有线程)阻塞?这个说法也是不全面的,回答这个问题,就需要简单了解一下线程模型
,线程模型分为:
多对一用户级
线程模型1对1内核级
线程模型多对多两级
线程模型
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和RING3
。RING0层拥有最高的权限
,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的系统中。
并发优点:
- 充分利用多核CPU的计算能力
- 方便进行业务拆分,提升应用性能
并发产生的问题:
- 高并发场景下,导致频繁的
上下文切换
; - 线程安全问题,开发考虑不全面,很
容易造成死锁
;
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 线程的六大状态,以及状态之间如何进行切换
初始化
(NEW)运行
(RUNNABLE)等待
(WAITING)超时等待
(TIMED_WAITING)阻塞
(BLOCKED)终止
(TERMINATED)
更多生命周期状态内容,可以参考以下两篇文章:
参考文章:
2021-11-16,《提起线程,你不了解的那些事》已更新,接下来涉及到的 JMM 内存模型、MESI 协议在 JMM 内存模型中的8种交互操作等内容,如有需要,请持续关注《并发编程》板块!!!
博主写作不易,加个关注呗
求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙
我不能保证所写的内容都正确,但是可以保证不复制、不粘贴。保证每一句话、每一行代码都是亲手敲过的,错误也请指出,望轻喷 Thanks♪(・ω・)ノ
以上是关于提起线程,你不了解的那些事的主要内容,如果未能解决你的问题,请参考以下文章