从0开始深入理解并发、线程与等待通知机制(中)

news2024/9/20 8:37:41

一,深入学习 Java 的线程

        线程的状态/生命周期

        Java 中线程的状态分为 6 种:

        1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。

        2. 运行(RUNNABLE)Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

        线程对象创建后,其他线程( 比如 main 线程)调用了该对象的 start() 方法。 该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权,此时处于就绪状态(ready )。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running )。
        3. 阻塞(BLOCKED):表示线程阻塞于锁。
        4. 等待 (WAITING) :进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
        5. 超时等待 (TIMED_WAITING) :该状态不同于 WAITING ,它可以在指定的时间后自行返回。
        6. 终止 (TERMINATED) :表示该线程已经执行完毕。
        状态之间的变迁如下图所示
         掌握这些状态可以让我们在进行 Java 程序调优时可以提供很大的帮助。

         其他的线程相关方法

        yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也 不会释放锁资源。同时执行 yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。(运行态--->就绪态)

        比如,ConcurrentHashMap#initTable 方法中就使用了这个方法,

        这是因为 ConcurrentHashMap 中可能被多个线程同时初始化 table,但是其实这个时候只允许一个线程进行初始化操作,其他的线程就需要被阻塞或等待, 但是初始化操作其实很快,这里 Doug Lea 大师为了避免阻塞或者等待这些操作 引发的上下文切换等等开销,就让其他不执行初始化操作的线程干脆执行 yield()方法,以让出 CPU 执行权,让执行初始化操作的线程可以更快的执行完成。

        线程的优先级  

        在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

        设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

        线程的调度 

        线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种:

        协同式线程调度(Cooperative Threads-Scheduling):

        使用协同式线程调度的多线程系统,线程执行的时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。使用协同式线程调度的最大好处是实现简单,由于线程要把自己的事情做完后才会通知系统进行线程切换,所以没有线程同步的问题,但是坏处也很明显,如果一个线程出了问题,则程序就会一直阻塞。

        抢占式线程调度(Preemptive Threads-Scheduling)

        使用抢占式线程调度的多线程系统,每个线程执行的时间以及是否切换都由系统决定。在这种情况下,线程的执行时间不可控,所以不会有「一个线程导致整个进程阻塞」的问题出现。

        Java 线程调度就是抢占式调度,为什么?

        在 Java 中,Thread.yield()可以让出 CPU 执行时间,但是对于获取执行时间, 线程本身是没有办法的。对于获取 CPU 执行时间,线程唯一可以使用的手段是 设置线程优先级,Java 设置了 10 个级别的程序优先级,当两个线程同时处于Ready 状态时,优先级越高的线程越容易被系统选择执行。

        线程和协程 

        为什么 Java 线程调度是抢占式调度?这需要我们了解 Java 中线程的实现模式。

        我们已经知道线程其实是操作系统层面的实体,Java 中的线程怎么和操作系统层面对应起来呢?

        任何语言实现线程主要有三种方式:

        使用内核线程实现(1:1 实现)

        使用用户线程实现(1:N 实现),

        使用用户线程加轻量级进程混合实现(N:M 实现)。

        内核线程实现 

        使用内核线程实现的方式也被称为 1 1 实现。 内核线程( Kernel-LevelThread, KLT ) 就是直接由操作系统内核( Kernel , 下称内核) 支持的线程, 这种线程由内核来完成线程切换, 内核通过操纵调度器( Scheduler ) 对线程进行调度, 并负责将线程的任务映射到各个处理器上。
        由于内核线程的支持,每个线程都成为一个独立的调度单元,即使其中某一 个在系统调用中被阻塞了,也不会影响整个进程继续工作,相关的调度工作也不需要额外考虑,已经由操作系统处理了。
        局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高, 需要在用户态(User Mode)和内核态( Kernel Mode )中来回切换。其次,每个语言层面的线程都需要有一个内核线程的支持,因此要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持的线程数量是有限的。

        用户线程实现

        严格意义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。 如果程序实现得当, 这种线程不需要切换到内核态, 因此操作可以是非常快速且低消耗的, 也能够支持规模更大的线程数量, 部分高性能数据库中的多线程就是由用户线程实现的。

        用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难, 甚至有些是不可能实现的。 因为使用用户线程实现的程序通常都比较复杂,所以一般的应用程序都不倾向使用用户线程。Java 语言曾经使用过用户线程,最终又放弃了。 但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如 Golang

        混合实现

        线程除了依赖内核线程实现和完全由用户程序自己实现之外, 还有一种将内核线程与用户线程一起使用的实现方式, 被称为 NM 实现。 在这种混合实现下, 既存在用户线程, 也存在内核线程。

        用户线程还是完全建立在用户空间中, 因此用户线程的创建、 切换、 析构等操作依然廉价, 并且可以支持大规模的用户线程并发。

        同样又可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过内核线程来完成。在这种混合模式中, 用户线程与轻量级进程的数量比是不定的,是 NM 的关系。

        Java 线程的实现 

         Java 线程在早期的 Classic 虚拟机上(JDK 1.2 以前),是用户线程实现的,但从 JDK 1.3 起, 主流商用 Java 虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用 11 的线程模型。

        以 HotSpot 为例,它的每一个 Java 线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构, 所以 HotSpot 自己是不会去干涉线程调度的,全权交给底下的操作系统去处理。

        所以,这就是我们说 Java 线程调度是抢占式调度的原因。而且 Java 中的线程优先级是通过映射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,操作系统中线程的优先级有时并不能和 Java 中的一一对应,所以Java 优先级并不是特别靠谱。

        协程 (用户线程的一种实现)

        出现的原因

        随着互联网行业的发展,目前内核线程实现在很多场景已经有点不适宜了。比如,互联网服务架构在处理一次对外部业务请求的响应, 往往需要分布在不 同机器上的大量服务共同协作来实现,,也就是我们常说的微服务,这种服务细 分的架构在减少单个服务复杂度、 增加复用性的同时, 也不可避免地增加了服 务的数量, 缩短了留给每个服务的响应时间。这要求每一个服务都必须在极短 的时间内完成计算, 这样组合多个服务的总耗时才不会太长;也要求每一个服 务提供者都要能同时处理数量更庞大的请求, 这样才不会出现请求由于某个服务被阻塞而出现等待。

        Java 目前的并发编程机制就与上述架构趋势产生了一些矛盾,11 的内核线程模型是如今 Java 虚拟机线程实现的主流选择, 但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。 以前处理一个请求可以允许花费很长时间在单体应用中,具有这种线程切换的成本也是无伤大雅的, 但现在在每个请求本身的执行时间变得很短、 数量变得很多的前提下,用户本身的业务线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。

        另外我们常见的 Java Web 服务器,比如 Tomcat 的线程池的容量通常在几十个到两百之间, 当把数以百万计的请求往线程池里面灌时, 系统即使能处理得过来,但其中的切换损耗也是相当可观的。

        这样的话,对 Java 语言来说,用户线程的重新引入成为了解决上述问题一个非常可行的方案。

        其次,Go 语言等支持用户线程等新型语言给 Java 带来了巨大的压力,也使得 Java 引入用户线程成为了一个绕不开的话题。

        协程简介

        为什么用户线程又被称为协程呢?我们知道,内核线程的切换开销是来自于保护和恢复现场的成本, 那如果改为采用用户线程, 这部分开销就能够省略掉吗? 答案还是“不能”。 但是,一旦把保护、恢复现场及调度的工作从操作系统交到程序员手上,则可以通过很多手段来缩减这些开销。

        由于最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling)的,所以它有了一个别名——“协程”(Coroutine) 完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”(Stackfull Coroutine)。

        协程的主要优势是轻量, 无论是有栈协程还是无栈协程, 都要比传统内核线程要轻量得多。如果进行量化的话, 那么如果不显式设置,则在 64 Linux上 HotSpot 的线程栈容量默认是 1MB,此外内核数据结构(Kernel Data Structures)还会额外消耗 16KB 内存。与之相对的, 一个协程的栈通常在几百个字节到几KB 之间, 所以 Java 虚拟机里线程池容量达到两百就已经不算小了, 而很多支持协程的应用中, 同时并存的协程数量可数以十万计。

        协程当然也有它的局限, 需要在应用层面实现的内容(调用栈、 调度器这些)特别多,同时因为协程基本上是协同式调度,则协同式调度的缺点自然在协程上也存在。

        总的来说,协程机制适用于被阻塞的,且需要大量并发的场景(网络 io),不适合大量计算的场景,因为协程提供规模(更高的吞吐量),而不是速度(更低的延迟)

        守护线程

        Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。

        Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。

       二,线程间的通信和协调、协作 

        很多的时候,孤零零的一个线程工作并没有什么太多用处,更多的时候,我们是很多线程一起工作,而且是这些线程间进行通信,或者配合着完成某项工作,这就离不开线程间的通信和协调、协作。

        管道输入输出流

        我们已经知道,进程间有好几种通信机制,其中包括了管道,其实 Java 的线程里也有类似的管道机制,用于线程之间的数据传输,而传输的媒介为内存

        设想这么一个应用场景:通过 Java 应用生成文件,然后需要将文件上传到云端,比如:

        1、页面点击导出后,后台触发导出任务,然后将 mysql 中的数据根据导出条件查询出来,生成 Excel 文件,然后将文件上传到 oss,最后发步一个下载文件的链接。

        2、和银行以及金融机构对接时,从本地某个数据源查询数据后,上报 xml 格式的数据,给到指定的 ftp 、或是 oss 的某个目录下也是类似的。
        
        我们一般的做法是,先将文件写入到本地磁盘,然后从文件磁盘读出来上传到云盘,但是通过 Java 中的管道输入输出流一步到位,则可以避免写入磁盘这一步。
        
        Java 中的管道输入 / 输出流主要包括了如下 4 种具体实现:PipedOutputStream PipedInputStream PipedReader PipedWriter
前两种面向字节,而后两种面向字符。

        join 方法  

        面试题

        现在有 T1T2T3 三个线程,你怎样保证 T2 T1 执行完后执行,T3 T2执行完后执行?

        答:用 Thread#join 方法即可,在 T3 中调用 T2.join ,在 T2 中调用 T1.join
        join()

        把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B 剩下的代码。

        synchronized 内置锁 

        线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码 一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行, 那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作, 包括数据之间的共享,协同处理事情。这将会带来巨大的价值。

        Java 支持多个线程同时访问一个对象或者对象的成员变量,但是多个线程同 时访问同一个变量,会导致不可预料的结果。关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使多个线程访问同一个变量的结果正确,它又称为内置锁机制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1061640.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Endnote修改参考文献(References)的期刊全称为缩写

一、准备(下载)所需要的期刊缩写列表 (Term Lists) 我已经下载并上传了一份Trem Lists 链接: 在不列颠哥伦比亚大学图书馆网站导出所有期刊名和缩写,大概1W的期刊名字,期刊名字和缩写截至2021.12.03 哥伦…

【抢先体验】开通使用 ChatGPT 语音版功能保姆级教程

大家好,我是苍何,一个土木转码的非典型程序员,也是一名技术管理者,同时也是 AI 应用的探索者。今天在视频号上看到和 ChatGPT 语音对话的视频,其声音的真实感太让人震撼了,于是也想去抢先体验一下 ChatGPT …

学习记忆——宫殿篇——记忆宫殿——记忆桩——卧室——莫兰勋爵在地铁走失的案子

《神探夏洛克》第三季第一集中提到“思维殿堂”,其实指的就是记忆宫殿。讲述了一个名叫莫兰勋爵在地铁走失的案子,这里简单给大家罗列以下破案信息: 订阅报纸的男人、伦敦养狗的女人、穿着黑色运动的非裔女人、松木、云杉、雪松、新樟脑球、碳…

js——深拷贝和浅拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。对于基本数据类型,例如字符串、数字、布尔值等,由于它们是按值传递的,所以不存在深拷贝和浅拷贝的问题。 深拷贝 将对象从内存中完整拷贝出来,从堆内存中开辟一个新的…

mac电脑任务管理器 Things3 for Mac中文

Things 3是一款效率软件,可以帮助用户规划一天行程、管理项目,并使使用者按部就班地朝目标迈进。以下是Things 3的主要特点和功能: 待办事项:以“待办事项”为基本组成部分,每一则待办事项都是迈向大成就的一小步。用…

【每日一题】买卖股票的最佳时机含冷冻期

文章目录 Tag题目来源题目解读解题思路方法一:动态规划空间优化 写在最后 Tag 【动态规划】【数组】【2023-10-05】 题目来源 309. 买卖股票的最佳时机含冷冻期 题目解读 这是股票系列问题的第五篇了,要求求出买卖股票的最佳时期以获得最大的利润&…

基于spirngboot人事考勤管理信息系统

一:功能介绍 本系统前端采用vue框架以及Elemnt-UI,后端采用springboot、mysql、redis、mybatis等技术栈。 主要功能有登录、员工考勤、数据统计、薪资管理、权限管理、打卡管理、考勤审核、请假审批、薪资发放、报表统计、文件上传、文件下载、考勤设置、请假设置。…

Spring Boot注册Web组件

文章目录 什么是Web组件?注册Servlet注册Filter注册Listener总结 🎉欢迎来到架构设计专栏~Spring Boot注册Web组件 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒🍹✨博客主页:IT陈寒的博客🎈该系列文章专栏:架构设计&a…

安装matplotlib__pygame,以pycharm调入模块

安装pip 安装matplotlib 安装完毕,终端输入pip list检查 导入模块出现bug,发现不是matplotlib包的问题,pycharm版本貌似不兼容,用python编辑器可正常绘图,pygame也可正常导入。 ​​​​​​​ pycharm版本问题解决 终…

k8s集群-7 service

工作负载的应用是如何暴露出去的 解决访问问题 Service可以看作是一组提供相同服务的Pod对外的访问接口。借助Service,应用可以方便地实现服务发现和负载均衡。 service默认只支持4层负载均衡能力,没有7层功能。(可以通过Ingress实现) service的类型: C…

【每日一题】买卖股票的最佳时机含手续费

文章目录 Tag题目来源题目解读解题思路方法一:动态规划空间优化 写在最后 Tag 【动态规划】【数组】【2023-10-06】 题目来源 714. 买卖股票的最佳时机含手续费 题目解读 本题与 122. 买卖股票的最佳时机 II 如出一辙,仅仅是增加了交易的手续费用。 解…

人工智能:定义未来,揭开历史神秘面纱,展望无限可能!

🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是尘缘,一个在CSDN分享笔记的博主。📚📚 👉点击这里,就可以查看我的主页啦!👇&#x…

python读取vivo手机截图,将满屏图片文件移动别的路径

问题之初 python读取vivo手机截图, 将满屏图片文件移动别的路径好多这样的图片,占用手机大量的内存,食之无味弃之可惜!那么会复制粘贴👀代码的我们我们今天就把这些图片筛选清理掉。 这段代码 原有逻辑的基础上&…

MySQL中的 增 删 查 改(CRUD)

目录 新增 insert into 表名 value(数据,数据),.......; insert into 表名(列1,列2.....) value(数据,数据),.......; datatime 类型的数据如何插入? 查询 select * from 表名…

太阳 IT

太阳是我们太阳系的中心星体,也是地球上最重要的能源来源之一。太阳的直径约为109倍地球,质量也大约是地球的33万倍。它以极高的温度和压力维持着核聚变反应,将氢气转化为氦气,释放出巨大的能量和光辐射。 太阳对地球的影响非常广…

【Linux】进程探秘

文章目录 一、什么是进程?1.1写一个进程 二、操作系统是如何管理的进程的呢?2.1描述进程-PCB2.2组织进程 三、Linux下是怎么做的?3.1task_struct-PCB的一种3.2task_ struct内容分类3.3linux中组织进程3.4查看进程的相关属性 🍀小结…

【物联网】STM32的中断机制不清楚?看这篇文章就足够了

在嵌入式系统中,中断是一种重要的机制,用于处理来自外部设备的异步事件。STM32系列微控制器提供了强大的中断控制器,可以方便地处理各种外部中断和内部中断。本文将详细介绍STM32中断的结构和使用方法。 文章目录 1. 什么叫中断2. 中断优先级…

Spring Cloud Gateway:打造可扩展的微服务网关

文章目录 什么是微服务网关?Spring Cloud Gateway的优势使用Spring Cloud Gateway构建微服务网关步骤1:添加依赖步骤2:创建路由配置步骤3:创建过滤器步骤4:运行应用程序 进一步探索结论 🎉欢迎来到架构设计…

Ubuntu Server CLI专业提示

基础 网络 获取所有接口的IP地址 networkctl status 显示主机的所有IP地址 hostname -I 启用/禁用接口 ip link set <interface> up ip link set <interface> down 显示路线 ip route 将使用哪条路线到达主机 ip route get <IP> 安全 显示已登录的用户 w…