自从踏入程序猿这条不归路,便摆脱不了(进程)线程这只粘人的小妖精,尤其在硬件资源“过剩”的今天

不论你在使用c、C++、.Net,还是Java、Python、Golang,都免不了要踏过这一关,即使使用以“单线程”著称的Node.js,也要借助pm2类似的进程管理工具fork一批进程,来榨干机器资源

早些年使用c编写多线程时,需要使用宏定义来兼容多平台下不同库的函数,而Java从一开始便宣称的"Write Once, Run Anywhere"从虚拟机层面帮我们屏蔽了众多平台差异,那,Java线程与OS线程间有什么关系?

1. 系统架构

以*nix类系统为例,其系统体系架构主要分为用户态(user context)和内核态(kernel context)

内核,本质上讲是一种较为底层的控制计算机硬件资源的软件

用户态,即上层应用程序的活动空间,应用程序的执行依托于内核提供的资源,为了使上层资源访问内核资源,内核提供系统调用接口以供上层应用访问

系统调用,可以看作是操作系统的最小功能单元,一种不能再简化的操作,而函数库则是对一组系统调用的封装,以降低应用程序调用内核的复杂度

1.1 用户态与内核态切换

在*nix类系统中,为了有效减少内核资源的访问及冲突,对不同的操作赋予了不同的执行等级,越是与系统相关的关键操作,越是需要高特权来执行

linux操作系统中主要采用了0和3两个特权等级,分别对应于内核态及用户态,运行于用户态的进程可以执行的操作及访问的资源会受到很大的限制,而运行在内核态的进程则可以执行任何操作,并且在资源的访问上也不会受到任何限制

一般应用程序一开始运行时都会处于用户态,当一些操作需要在内核权限下才能执行时,则会涉及一次从用户态到内核态的切换过程,当该操作执行完毕后,又会涉及一次从内核态到用户态的切换过程

2 线程模型

回过头来,从系统层面聊一聊线程的实现模型

2.1 用户线程 v.s. 内核线程

简单来讲

  • 用户线程

    • 由应用程序创建、调度、撤销,不需要内核的支持(内核不感知)
    • 由于不需要内核的支持,便不涉及用户态/内核态的切换,消耗的资源较少,速度也较快
    • 由于需要应用程序控制线程的轮换调度,当有一个用户线程被阻塞时,整个所属进程便会被阻塞,同时在多核处理器下只能在一个核内分时复用,不能充分利用多核优势
  • 内核线程
    • 由内核创建、调用、撤销,并由内核维护线程的上下文信息及线程切换
    • 由于内核线程由内核进行维护,当一个内核线程被阻塞时,不会影响其他线程的正常运行,并且多核处理器下,一个进程内的多个线程可以充分利用多核的优势同时执行
    • 由于需要内核进行维护,在线程创建、切换过程中便会涉及用户态/内核态的切换,增加系统消耗

2.2 轻量级进程 LWP

在linux操作系统中,往往都是通过fork函数创建一个子进程来代表内核中的线程,在fork完一个子进程后,还需要将父进程中大部分的上下文信息复制到子进程中,消耗大量cpu时间用来初始化内存空间,产生大量冗余数据

为了避免上述情况,轻量级进程(Light Weight Process, LWP)便出现了,其使用clone系统调用创建子进程,过程中只将部分父进程数据进行复制,没有被复制的资源可以通过指针进行数据共享,这样一来LWP的运行单元更小、运行速度更快

LWP与内核线程一一映射,每个LWP都由一个内核线程支持

2.3 1:1 线程模型

1:1 模型,即每一个用户线程都对应一个内核线程,每个线程的创建、调度、销毁都需要内核的支持,每次线程的创建、切换都会设计用户状态/内核状态的切换,性能开销比较大,并且单个进程能够创建的LWP的数量是有限的,但能够充分里用多核的优势

2.4 N:1 线程模型

N:1模型,即所有的用户线程都会对应到一个内核线程中,该模型可以在用户空间完成线程的创建、调度、销毁,不需要内核的支持,同样也就不涉及用户状态/内核状态的切换,线程的操作较快且消耗较低,并且线程数量不受操作系统的限制,但不能发挥多核的优势,只能在一个核中分时复用,并且由于内核不能感知用户态的线程,在某一线程被阻塞时,会导致整个所属进程阻塞

2.5 N:M 线程模型

N:M 模型是基于以上两种模型的一种混合实现,多个用户线程对应于多个内核线程,即解决了1:1模型中性能开销及线程数量的问题,也解决了N:1模型中阻塞问题,同时也能充分利用CPU的多核优势,这也是大部分协程实现的基础

Java在1.2之前基于用户线程实现(N:1线程模型),在1.2之后windows及linux平台下采用1:1线程模型,在solaris平台使用1:1或N:M线程模型实现(可配置)

3 线程状态

以下以linux平台为例

linux平台下,JVM采用1:1的线程模型,那Java中的线程状态与OS的线程状态是否也是一一对应的?

3.1 系统线程状态&生命周期

linux系统的线程状态及生命周期如上图,每种状态的详细解释不再一一赘述,这里简单介绍下RUNNABLERUNNING

  • RUNNABLE

线程处于可运行的状态,但还没有被系统调度器选中,即还没有分配到CPU时间片

  • RUNNING

线程处于运行状态,即线程分配到了时间片,正在执行机器指令

3.2 Java线程状态&生命周期

Java中的线程状态并没有使用系统线程状态一一对应的方式,而是提供了与之不同的6种状态

以下,linux系统线程状态会使用 下划线 加以区分

linux系统中的RUNNABLERUNNING被Java合并成了RUNNABLE一种状态,而linux系统中的BLOCKED被Java细化成了WAITINGTIMED_WAITINGBLOCKED三种状态

Java中的线程状态与系统中的线程状态大体相似,但又略有不同,最明显的一点是,如果由于I/O阻塞会使Java线程进入BLOCKED状态么?NO!I/O阻塞在系统层面会使线程进入BLOCKED状态,但在Java里线程状态依然是RUNNABLE

系统中的RUNNABLE表示线程正在等待CPU资源,在在Java中被认为同样是在运行中,只是在排队等待而已,故Java中将系统的RUNNABLERUNNING合并成了RUNNABLE一种状态

而对于系统中I/O阻塞引起的BLOCKED状态,在Java中被认为同样是在等待一种资源,故也认为是RUNNABLE的一种情况

Java线程的状态在Thread.State枚举中可以查看,其每种状态的释义写的非常清楚,这里不再一一解释

  • NEW
    Thread state for a thread which has not yet started.
  • RUNNABLE
    Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
  • BLOCKED
    Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait.
  • WAITING
    Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:

    • Object.wait with no timeout
    • Thread.join with no timeout
    • LockSupport.park

A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

  • TIMED_WAITING
    Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:

    • Thread.sleep
    • Object.wait with timeout
    • Thread.join with timeout
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  • TERMINATED
    Thread state for a terminated thread. The thread has completed execution.

4. 上下文切换与调优

上下文切换涉及到进程间上下文切换与线程间上下文切换

用户态与内核态的每一次切换都会导致进程间上限文的切换,比如java中在使用重量级锁的时候会依赖系统底层的mutex lock,而该系统操作会导致用户态/内核态的切换,进而引起进程间的上下文切换

这里重点讨论下线程间的上下文切换

4.1 什么情况会触发线程间上下文切换

一个线程由RUNNING转为BLOCKED时(线程暂停),系统会保存线程的上下文信息

当该线程由BLOCKED转为RUNNABLE时(线程唤醒),系统会获取上次的上下文信息以保证线程能够继续执行

以上的一个过程线程上下文的一次切换过程

同样,一个线程由RUNNING转为RUNNABLE,再由RUNNABLE转为RUNNING时也会发生线程间的上下文切换

即,多线程的上下文切换实际上就是由多线程两个运行状态的互相切换导致的

那,什么情况下会触发 RUNNINGBLOCKEDRUNNABLE (对应Java中 RUNNABLEBLOCKED/WAITING/TIMED_WAITINGRUNNABLE) 的状态转变呢?

一种为程序本身触发,一种为操作系统或虚拟机触发

程序本身触发很容易理解,所有会导致 RUNNABLEBLOCKED/WAITING/TIMED_WAITING 的逻辑均会触发线程间上下文切换,如synchronized wait join park sleep

操作系统触发,最常见的比如线程时间片的分配

虚拟机触发,最常见的在于进行垃圾回收时的 'stop the world'

4.2 如何优化

既然所有会导致 RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING 的逻辑均会触发线程间上下文切换,那便从诱因入手

4.2.1 锁竞争

锁其实并不是性能开销的根源,竞争锁才是

  1. 减少锁的持有时间

锁的持有时间越长,就意味着可能有越多的线程在等待锁的释放,如果是同步锁,除了会造成线程间上下文切换外,还会有进程间的上下文切换 (mutex lock)

优化方法有很多,比如将synchronized关键字从方法修饰移到方法体内,将synchronized修饰的代码块中无关的逻辑移到synchronized代码块外,等等

2. 降低锁的粒度
- 锁分离
对于读操作大于写操作的逻辑,可以将传统的同步锁拆分为读写锁,即读锁与写锁,在多线程中,只有读写与写写是互斥的,避免读读情况下锁的竞争
- 锁分段
对于大集合或者大对象的锁操作,可以考虑将锁进一步分离,将大集合或者大对象分隔成多个段,对每一个段分别上锁,以避免对不同段进行操作时锁的竞争,如ConcurrentHashMap中对锁的实现

3. 非阻塞乐观锁代替竞争锁
- 使用volatile
volatile 的读写操作不会导致上下文切换,开销较小,但volatile只保证可见性,不保证原子性
- 使用CAS
CAS 是一个原子的 if-then-act 操作,可以在我外部锁的情况下来保证读写操作的一致性,如Atomic包中的算法

4. 其它非阻塞乐观锁

4.2.2 wait/notify优化

  • 使用notify()代替notifyAll()
    众所周知,notifyAll会唤醒所有相关的线程,而notify则会唤醒指定线程,以减少过多不相关线程的上下文切换
  • 使用Lock+Condition组合的方式替代wait/notify
    synchronized是基于系统层面实现的,而Lock则是应用程序层面实现的,不会造成用户态/内核态的切换
    Condition会避免类似notifyAll提前唤醒过多无关线程的问题

4.2.3 合理设置线程池大小

线程池数量不宜设置过大,线程池数量设置过大容易导致大量线程处于等待CPU时间片的状态(RUNNABLE),同时也会导致过多的上下文切换

4.2.4 使用协程实现非阻塞等待

协程可以看做是一种轻量级线程

前文介绍到,Java线程使用1:1线程模型,每个用户线程都会映射到一个系统线程,线程由内核来管理

协程则使用N:M线程模型,协程完全由应用程序来管理,避免了众多的上下文切换

(协程不等于没有系统线程,只是会大大减少系统线程上下文切换的次数)

5. 总结

  • 操作系统体系架构主要分为用户态(user context)和内核态(kernel context)
  • 由于系统操作分不同的执行等级,应用程序在执行一些高等级操作时会发生用户态/内核态的切换
  • 用户线程由应用程序创建、调度、撤销,不需要内核的支持
  • 内核线程由内核创建、调用、撤销,并由内核维护线程的上下文信息及线程切换
  • 线程模型分为1:1N:1N:M三种,Java在window及linux上采用1:1线程模型,即每个用户线程都会对应一个内核线程
  • Java中的线程状态并没有使用系统线程状态一一对应的方式,而是使用NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态
  • 用户态/内核态的切换会导致进程间上下文切换
  • 多线程两个运行状态的互相切换会导致线程间的上下文切换,诸如synchronized wait join park sleep 等常见操作均会引起线程间的上下文切换
  • 理解线程上下文切换的原因,合理优化程序,减少上下文切换,减轻系统负担

用户级别线程的切换切换到内核线程_【修炼内功】[JVM] 细说线程相关推荐

  1. 将线程pid转成16进制_如何使用jstack分析线程状态

    背景 记得前段时间,同事说他们测试环境的服务器cpu使用率一直处于100%,本地又没有什么接口调用,为什么会这样?cpu使用率居高不下,自然是有某些线程一直占用着cpu资源,那又如何查看占用cpu较高 ...

  2. 线程间通信的几种方法_并发编程中的线程间通信

    线程通信的目标是使线程间能够互相发送信号.另一方面,线程通信使线程能够等待其他线程的信号. 线程通信常用的方式有: wait/notify 等待 Volatile 内存共享 CountDownLatc ...

  3. 从0到1写RT-Thread内核——线程定义及切换的实现

    从0写RT-Thread内核之线程定义及切换的实现具体可以分为以下六步来实现 一:分别定义线程栈.线程函数.线程控制块: ALIGN(RT_ALIGN_SIZE)//设置4字节对齐 /* 定义线程栈 ...

  4. 华为用户级别切换认证配置举例

    用户级别切换认证配置举例 用户级别切换认证配置举例 关键词:用户级别切换认证,RADIUS,HWTACACS 摘  要:本文结合不同的登录认证方式,详细介绍了三种用户级别切换认证的配置思路和配置过程. ...

  5. 微软宣布 Edge 浏览器将切换至 Chromium 内核

    简述 据微软官方 blog的消息,windows 的默认浏览器 Edge将切换内核至 Chromium,并且微软将秉承开源精神,在未来更多的为 Chromium项目贡献代码. 微软具体说了什么? 原文 ...

  6. 线程的主动切换——KiSwapThreadKiSwapContext逆向

    线程的切换分为主动切换和被动切换两种方式 主动切换主要是通过KiSwapThread进行的. KiSwapThread逆向 .text:004050BF 8B FF mov edi, edi .tex ...

  7. 什么是线程上下文的切换?

    什么是线程上下文的切换 对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类 ...

  8. 【Linux 内核】进程管理 ( 内核线程概念 | 内核线程、普通进程、用户线程 | 内核线程与普通进程区别 | 内核线程主要用途 | 内核线程创建函数 kernel_thread 源码 )

    文章目录 一.内核线程概念 二.内核线程.普通进程.用户线程 三.内核线程.普通进程区别 四.内核线程主要用途 五.内核线程创建函数 kernel_thread 源码 一.内核线程概念 直接 由 Li ...

  9. java 切换主线程_Java线程状态及切换、关闭线程的正确姿势分享

    前言 在讲线程之前有必要讨论一下进程的定义:进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位.进程实体由程序段, 数据段 PCB(进程控制块)组成.线程又是什么?线程可以 ...

最新文章

  1. 关于ASP.NET 中站点地图sitemap 的使用【转xugang】
  2. Could not calculate build plan: Plugin org.apache.maven.plugins:maven-war-plugin:2.4
  3. wchar_t * 与 char * 互相转换小记
  4. 数制转换itoa atoi int转字符串 字符串转int string转int int转string
  5. mysql 归类函数_mysql常用的函数归类
  6. c#winform演练 ktv项目 MediaPlayer控件播放音乐
  7. php 企业微信指令回调借款_php处理微信支付回调改变订单支付状态的代码
  8. Web前端工作笔记013---拦截所有的ajax请求,设置出错信息
  9. centos删除文件不释放_Linux运维知识之linux centos下彻底删除文件 解决删除文件文件夹硬盘空间不释放不减少...
  10. 升级nodejs的方法(3)
  11. 张正友标定法matlab,张正友标定法(相机标定)
  12. 谱尼测试凭借现代化的测试平台
  13. android和夜神模拟器哪个好,天天模拟器和夜神安卓模拟器哪个好 两者功能对比...
  14. android课程设计健身,健身软件课程设计-毕业论文.doc
  15. 广电网络宽带电视网关简介与优化设置
  16. win7cmd闪退_Win7运行bat批处理闪退怎么解决?
  17. java 获取回车字符_java回车键的字符
  18. 夏季哪些蔬菜不适合生吃凉拌,食用会发生危险
  19. 这应该是最全的软件测试工程师必读书籍
  20. 解铃还须系铃人—大数据时代的安全交给大数据

热门文章

  1. 【概念集锦】之 浅拷贝与深拷贝
  2. 【Java】从键盘中输入一个值,在数组中查找该值的索引并输出
  3. 前端使用js发起http请求的几种方法
  4. 计算机编程课程顺序_620多个免费的在线编程和计算机科学课程,您可以在三月开始
  5. pytorch 全局变量_Pytorch如何通过深度学习展现全局
  6. Python小技巧:如何批量更新已安装的库?
  7. REST与RESTful
  8. 伪静态、静态和动态的区别
  9. 小程序如何实现tab切换,一部到位
  10. Shiro实战hello