并发编程的基石-多线程概念建立
什么是线程、进程?
操作系统、进程、线程的包含关系
- 操作系统是包含多个进程的容器,而每个进程又都是容纳多个线程的容器。
Oracle 文档的官方定义
- 进程: 使用fork(2)系统调用创建的UNIX环境(例如文件描述符,用户ID等),它被设置为运行程序。
- 线程:在进程上下文中执行的一系列指令。
什么是进程?
-
进程的英文是Process,指的是程序的一次执行,在用户下达运行程序的命令后,就会产生进程。
-
进程也可以看为对代码的一种实例化。每一次的运行进程都是不完全一样的。
-
任务管理器图示:
-
总结: 进程是程序(这里可以理解为我们写的代码)的真正运行实例,是资源分配的基本单位。
什么是线程?
-
靠谱定义是“线程是CPU的基本调度单位,每个线程执行的都是进程代码的某个片段”。
-
实例演示
-
代码示例:
-
启动前:
-
启动后:
-
运行结束后:
-
用房子作比喻
-
房子是一个容器,拥有某些属性(例如建筑面积,卧室数量等),但是房子本身并没有主动做任何事情。
-
做事情的是住在里面的人(比喻线程)。人在房子里工作、睡觉、看电视,这些比喻线程在执行各种功能的代码。
-
进程是线程的容器,包含存储器等资源,而线程利用这些资源来执行代码,最终产生结果。
进程和线程的不同
-
起源不同 先有进程,后有线程。由于处理器速度比外设等设备执行速度要快,为了提高 CPU 的利用率,线程就出来了。
-
概念不同 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是 CPU 调度单位。
- 内存共享方式不同 默认情况下,一个进程的内存无法与其他进程共享。
线程可以共享由操作系统分配给其父进程的相同内存块。
- 拥有资源不同 进程拥有独立内存,而线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程共享的内容: (1) 进程代码段。
(2) 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)。
(3) 进程打开的文件描述符。
(4) 信号的处理器。
(5) 进程的当前目录。
(6) 进程用户 ID与进程组 ID。
线程独有的内容:
(1) 线程 ID。
(2) 寄存器组的值。
(3) 线程的堆栈。
(4) 错误返回码。
(5) 线程的信号屏蔽码。
-
数量不同 一个程序至少有一个进程,一个进程至少有一个线程。
-
开销不同 线程的创建、终止时间比进程短。
同一进程内的线程切换时间比进程切换短。
同一进程的各个线程间共享内存和文件资源,可以不通过内核进行通信。
- 相似点: 生命周期 都有就绪、等待、运行等状态。
Java 语言和多线程的渊源和关系
-
Java 设计之初,就支持多线程,相比当时的语言是一个显著的优势,语言排名高。
-
Java 线程会一对一映射到操作系统。
-
JVM 会自动启动一些线程
-
代码示例
-
debugger 观察
-
线程信息
-
Signal Dispatcher 线程(负责把操作系统发来的信号分发给适当的处理程序,即用来连接操作系统和应用程序的 )
-
Finalizer 线程(负责对象的 finalize() 方法)
-
Reference Handler 线程(和GC、引用相关的线程,会把对象的引用记录起来配合 GC 进行垃圾回收 )
-
main 线程(主线程,用户程序的入口)
什么是多线程?
-
多线程是指在单个进程中运行多个线程。如果一个程序允许允许两个或以上的线程,那么它就是多线程程序。
-
比喻
-
合租室友的比喻
- 客厅: 公共空间,即进程的内容空间,对每一个线程都敞开,每个进程都可以随时到进程的内存中获取资源并且进行通信。
- 厕所: 锁,比如只有一个厕所,一个人上厕所另一个人就不能使用厕所。
- 独立房间: 线程独享空间,每个人各司其职拥有自己的一些资源,互相之间也不影响。
- 花园浇花: 线程合作。
-
火锅的比喻
- 大火锅一个人吃,就是单进程单线程。
- 大火锅多人吃,就是单进程多线程。
- 分开吃小火锅,就是多进程多线程。
- 我吃火锅,别人吃火锅底料,那就是我拥有锁,别人拿不到锁。
-
实例: 抢火车票
-
独立任务和共享任务图示
为什么需要多线程?
- 最主要的目的就是提高 CPU 利用率。
- 提高处理速度。
- 避免无效等待(IO 的时候可以做其他事)。
- 提高用户体验: 避免卡顿、缩小等待时间。
- 并行处理,提高性能,通常是服务器领域(例如 Tomcat),用多个线程去接收进来的 HTTP 请求,而不是排队等待单一的线程处理。
- 在 Android 开发中,主线程的重要任务之一是绘制屏幕界面,该线程中不允许进行 IO 操作或网络请求,目的就是避免卡顿,影响用户的交互。
- 便于编程建模
- 把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任 务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
- 计算机性能定律:摩尔定律失效,阿姆达尔定律登上舞台。
- 摩尔定律: 当价格不变时,集成电路上可容纳的元器件的数目,约每隔 18-24 个月便会增加一倍,性能也将提升一倍。
- 阿姆达尔定律: 一般情况下,处理器越多,程序执行的速度就会越快,但是会有一个上限,上限取决于程序中串行部分的比例,并行的比例越高,多处理器的效果越明显。
- 串行任务: 比如鱼塘中的鱼从小长大和鱼塘的数量无关。
- 图示
什么场景需要多线程?有什么局限?
-
什么时候需要新开线程? 通常在需要进行耗时任务的时候,例如执行磁盘 IO 读写,或者从网络获取信息的时候。
-
常见场景
-
为了同时做多件不同的事情。
- 开网页同时听音乐。
- 后台线程: 比如执行定时任务 quartz。
-
为了提高工作效率、处理能力。
- tomcat: 每次有一个新的请求过来的时候,tomcat 会把这个请求交给一个新的线程去处理。
- 多线程后台并行下载文件。
- NIO 和 AIO。
-
需要同时有很大并发量的时候。
- 例如压测。
-
多线程的局限
-
性能方面: 上下文切换带来的消耗。
- 线程间切换时会有上下文切换, 它会保存一些 CPU 所需要的数据, 比如说当前运行到哪一行代码了, 那么这个切换保存的都会带来一定的损耗。
-
异构化任务(任务结构不一样)很难高效并行。没办法总结为一个流水线。
-
带来线程安全问题。
- 数据安全问题,例如 i++ 总数不一致。
- 活跃性问题, 例如线程饥饿和死锁。
- 死锁: 这个线程和另个线程互相拥有需要的资源,自己又不肯放手,陷入一个无穷等待。
- 线程饥饿: 一个线程很想执行一些任务, 但是有太多线程都抢先于它,优先于它运行,导致这个线程始终得不到上台、始终得不到发挥。
串行、并行和并发
串行和并行
- 串行是大家排队一个个来, 并行是大家一起来。
并行和并发
- 并行的概念
- 真正的同时运行, 在同一时刻, 有多个任务同时执行。
- 例如,在多核处理器上,有两个线程同时执行同一段代码。
- 可见,单核处理器是无法实现并行的,因为单核处理器无法在同一时刻执行多个任务。
- 并发的概念(通常有两种)
- 当并发指的是第一种概念(多个任务的执行状态)的时候。
- 两个或多个任务可以在重叠的时间段内启动、运行和完成。
- 并行(两个线程同时执行)一定是并发。但是并不意味着并发一定要求是并行。
-
当并发指的是第二种概念(程序的不同部分具有可以同时执行的性质,即对并发性的简称)的时候。
-
“并发性”是一种程序的性质。如果一个程序具有并发性,说明这个程序的不同的部分可以无序或同时执行,且不影响最终的执行结果。
-
图示
串并行和并发的实际例子
例子 1
- 打游戏时,女朋友来电话了。
- 同时打游戏和接电话。
- 并发的体现,大脑在两个事情之间快速切换,来做到表面上的同时执行,事实上没有办法做到并行。
- 关掉游戏,专心接电话。
- 串行的体现。
例子 2
- 用单核 CPU 和多核 CPU 来辨析各概念。
- 程序具备并发性,但不并行执行。(单核并行程序)
- 程序具备并发性,且并行执行。(多核)
- 既不并行执行,也不具备并发性。(单核串行)
- 并行执行,但程序不具备并发性。(不可能发生,如果可以并行执行,那么就代表一定有并发性)
是什么让并行和并发成为可能?
- CPU 的升级。
- 操作系统的升级。
- 编程语言的升级。
什么是高并发?
- 高并发在业界通常理解为同时有很多个请求发送给服务器系统,因此服务器就会并行处理。
- 常见场景
- 双 11
- 例如每秒钟的订单创建数很高。
- 春晚
- 例如春晚上的抽奖活动和红包活动等,比如微信、阿里等承包。
- 12306
- 每天的 PV(网站访问量)一天可以达到 297 亿次, 并且成功售出了超过 1000 万张票。
- 平时开发中对于并发的架构设计不应该过度设计也不应该欠缺设计,设计为当前被访问的实际请求的 10 倍左右是一个不错的设计标准。
高并发和多线程的联系和不同
- 多线程和高并发是什么?
- 解决方案、状态。
- 高并发可以认为是一种状态,指的是大量的请求同时到达服务器所带来的一种结果,这个结果就是服务器需要同时去处理很多请求,所以我们的系统需要去应对高并发这种状态所带来的后果,带来的后果如果处理不当,会导致请求响应速度慢、无响应、甚至是服务器死机。
- 多线程实际上并不意味着高并发,多线程是一种编程方式、解决方案,它所解决的恰恰是防止高并发所带来的那些线程安全问题或者性能问题。
- 并发量很高,需要多线程处理,很可能有线程安全问题,此时不应该轻易的使用 HashMap,可考虑使用线程安全的 ConcurrentHashMap。
- 多线程可以提高硬件的利用率,比如 Tomcat 为了同时处理多个请求,内部采用了多线程的模型,这恰恰是为了解决高并发这种状态所带来的问题。
- 多线程和高并发的关系
- 多线程编程是我们解决高并发或者应对高并发这种场景的其中一种非常重要的解决方案。
- 除了多线程之外,还有其他解决方案。比如为了减小数据库的压力,可以在数据库上面加一个 redis 缓存层,此时都会优先访问 redis,把大部分压力给扛下来。并且如果只使用 redis,很少会带来多线程的这种线程安全问题,因为相对而言 redis 不会造成返回结果的错误,只可能会有一些相对而言没那么严重的错误,比如缓存不一致。
- 综上,可以得出高并发并不意味着是多线程。例如上面的 redis,它的底层是使用单线程来处理的,并且 redis 的吞吐量也相当大,并发也很高,性能在峰值可以达到几万或者是十几万、几十万,在这种情况就可以认为它是一种高并发,但它的底层却是单线程处理的。
高并发有哪些重要指标?
- QPS: 每秒请求数。
- 带宽: 峰值流量和页面的平均大小。
- PV: 页面浏览量、点击量。
- UV: 用户访问量,同个用户访问只会记为 1。
- IP 和 UV 的区别
- 第一种情况: IP 换了,这个人没变,进入网站 IP 统计次数会加 1,但是 UV 没变。
- 第二种情况: 都在同一个局域网内,对外的 IP 是一样的,进入网站 IP 只有一个,但这个 IP 中可能有不同的 UV。
- 吞吐率(Requests per second)。
- 并发连接数(The number of concurrent connections): 某个时刻服务器所接受的请求的数目。
- 服务器平均请求等待时间(Time per request: across all concurrent requests)。
同步与异步、阻塞与非阻塞
同步与异步
- 同步与异步: 被调用者是否主动告诉调用者结果。
- 总结
- 同步
- 同步指的是,客户端发出一个请求后,在没有得到结果之前,服务端就不返回任何结果。但是一旦服务端返回,那返回的就是客户端想要的结果(而不是告诉客户端再等等之类的内容)。
- 被调用者不主动告诉调用者结果,由调用者主动等待调用的结果。
- 异步
- 不实时处理。
- 调用在发出之后,服务端会立刻返回,告诉调用方“我收到你的请求了,我会处理的”。
- 调用发出后,等服务端计算完毕后,服务端会通过发消息等途径来通知调用者,或通过回调函数处理这个调用。
- 故事示例
- 烧水壶
- 同步:盯着水壶。
- 异步:煮开后声音提醒。
- 买书时打电话问老板有没有某本书
- 同步:打电话时没挂电话等老板现场查询。
- 异步:先挂电话,老板去查,一段时间后回电通知。
- 烧水壶
阻塞与非阻塞
- 阻塞与非阻塞: 我是调用者,我调用一个东西后,结果返回前,是否还能做别的事。
- 站在线程状态的角度
- 线程当前不能进行执行了,需要等待一段时间或等待其他人唤醒它。
- 站在线程发出请求(通常是 HTTP 请求)的角度(调用者角度)
- 例子:
- 烧水壶
- 阻塞:坐在水壶面前不能动。
- 非阻塞:等待的时候去看电视。
- 打电话买书时和老板说要一本书
- 阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟 check 一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你无关。
- 烧水壶
- 同步异步、阻塞非阻塞的综合例子
- 同步阻塞:坐在普通水壶前等待检查水是否烧开,水壶不会主动通知。
- 同步非阻塞:使用普通水壶烧水,水烧开之前我们先去客厅看电视了,但是水壶不会主动通知我们,需要我们时不时的去厨房看一下水有没有烧开。
- 异步阻塞:带有提醒功能的水壶烧水,我们坐着等。
- 异步非阻塞:带有提醒功能的水壶烧水。在水烧发出提醒之前我们先去客厅看电视了,等水壶发出声音提醒我们。
常见面试问题
- 线程和进程的相同和不同?
- 并行和并发的异同?
- 高并发就是多线程吗? 有什么反例?
- 反例:redis。内部单线程,支持高并发。
- 什么是同步,什么是异步,什么是阻塞,什么是非阻塞?
- 同步阻塞和异步非阻塞的关系?
- 多线程可以提高程序执行效率,你知不知道有有哪些弊端?
- 在单核 CPU 上运行多线程程序有意义吗? 有意义。虽然在单核 CPU 上执行真正的并行是不可能的,因为它只有一个处理器;但是对于我们应用程序而言, 我们不知道这个程序未来是运行在单核 CPU 还是多核 CPU,所以在编写时以多核 CPU 为准,编写一个多线程的程序;其次假设是多线程的,当其中一个线程执行缓慢或者被阻塞的时候,其他的线程可以利用这些时间去做其他事情,让这个程序保持高效运转。比如有一个整理磁盘的程序,它要对磁盘进行很多的读写操作,如果只有一个线程,而不采用多线程编程的话,当这个线程进行磁盘读写时,程序界面可能会卡死不能动。而如果采用多线程,开一个子线程进行磁盘读写的话,就不会影响界面或者其他操作,直到文件整理好了再来把这个线程的内容进行进一步的处理。
脑图地址
http://naotu.baidu.com/file/6c1069164a776869728a1f7796847a24?token=aed687763c7a1d6e