多线程篇( 并发编程 - 认识多线程)(持续更新迭代)

目录

一、如何阅读源码

1. 为什么要源码

2. 如何看源码

二、多线程基础概念

1. 为什么要学习并发编程

2. 什么是多线程并发编程

3. 并发 & 并行

并发

并行

4. 为什么要进行多线程并发编程

5. 多线程并发编程带来了什么优势

6. 多线程并发编程带来的风险

7. 线程无处不在

一、如何阅读源码

1. 为什么要源码

我们在做项目的时候一般会遇到下面的问题:

(1)不知道如何去设计。比如刚入职场时,来一个需求需做概要设计,不知如何下手,不得不去看

当前系统类似需求是如何设计的,然后仿照去设计。

(2)设计的时候,考虑问题不周全。相比职场新手,这类人对一个需求依靠自己的经验已经能够拿

出一个概要设计,但是设计中经常会遗漏一些异常细节,比如使用多线程有界队列执行任务,遇到机

器宕机了,如果队列里面的任务不存盘的话,那么机器下次启动的时候这些任务就丢失了。

对于这些问题,说到底主要还是因为经验不够,而经验主要从项目实践中积累,所以招聘单位一般都

会限定工作时间大于3年,因为这些人的项目经验相对较丰富,在项目中遇到的场景相对较多。工作

经验的积累来自于年限与实践,然而看源码可以扩展我们的思路,这是变相增加我们经验的不错方

法。虽然不能在短时间内通过时间积累经验,但是可以通过学习开源框架、开源项目来获取经验。

另外,进职场后一般都要先熟悉现有系统,如果有文档还好,没文档的话就得自己去翻代码研究。如

果之前对阅读源码有经验,那么在研究新系统的代码逻辑时就不会那么费劲了。

还有一点就是,当你使用框架或者工具做开发时,如果你对它的实现有所了解,就能最大化地减少出

故障的可能。比如并发队列 ArrayBlockingQueue 里面关于元素入队有个offer方法和 put 方法,虽然

某个时间点你知道使用offer 方法时,当队列满了就会丢弃要入队的元素,之后 offer 方法会返回

false,而不会阻塞当前线程;而使用 put 方法时,当队列满了,则会挂起当前线程,直到队列有空闲,元素

入队成功后才返回。但是人是善忘的,一段时间不使用,就会忘记它们的区别,当你再去使用时,需

进入 offer 和 put方法的内部,看它们的源码实现。进入 offer 方法一看,哦,原来队列满后直接返回

了false ;进入 put 方法一看,哦,原来队列满后,直接使用条件变量的 await 方法挂起了当前线程。

知道了它们的区别,你就可以根据自己的需求来选择了。

看源码最大的好处是可以开阔思维,提升架构设计能力。有些东西仅靠书本和自己思考是很难学到

的,必须通过看源码,看别人如何设计,然后思考为何这样设计才能领悟到。能力的提高不在于你写

了多少代码,做了多少项目,而在于给你一个业务场景时,你是否能拿出几种靠谱的解决方案,并且

说出各自的优缺点。

而如何才能拿出来,一来靠经验,二来靠归纳总结,而看源码可以快速增加你的经验。

2. 如何看源码

那么如何阅读源码呢?在你看某一个框架的源码前,先去 Google 查找这个开源框架的官方介绍,通

过资料了解该框架有几个模块,各个模块是做什么的,之间有什么联系,每个模块都有哪些核心类,

在阅读源码时可以着重看这些类。

然后对哪个模块感兴趣就去写个小demo,先了解一下这个模块的具体作用,然后再 debug 进入看具

体实现。在 debug 的过程中,第一遍是走马观花,简略看一下调用逻辑,都用了哪些类;第二遍需有

重点地debug,看看这些类担任了架构图里的哪些功能,使用了哪些设计模式。如果第二遍有感觉

了,便大致知道了整体代码的功能实现,但是对整体代码结构还不是很清晰,毕竟代码里面多个类来

回调用,很容易遗忘当前断点的来处;那么你可以进行第三遍 debug,这时候你最好把主要类的调用

时序图以及类图结构画出来,等画好后,再对着时序图分析调用流程,就可以清楚地知道类之间的调

用关系,而通过类图可以知道类的功能以及它们相互之间的依赖关系。

另外,开源框架里面每个功能类或者方法一般都有注释,这些注释是一手资料,比如JUC包里的一些

并发组件的注释,就已经说明了它们的设计原理和使用场景。

在阅读源码时,最好画出时序图和类图,因为人总是善忘的。如果隔一段时间你再去看之前看过的源

码,虽然有些印象,但当你想去看某个模块的逻辑时,又需根据 demo 再从头 debug 了。而如果有

了这俩图,就可以从这俩图里面直接找,并且看一眼时序图就知道整个模块的脉络了。

此外,查框架使用说明最好去官网查(这些信息是源头,是没有经过别人翻译的),虽然是英文,但是看

久了就好了,毕竟还有 Google 翻译呐!

当然研究代码时不一定非要 debug 三遍,其实这里说的是三种掌握程度,如果你 debug 一遍就能掌

握,那自然更好啦。

二、多线程基础概念

1. 为什么要学习并发编程

学习多线程并发编程可以帮助我们更好地利用计算机资源,提高软件开发的效率和性能,实现更复杂

的业务逻辑,提高应用程序的响应能力和用户体验。它的目的就是为了让程序运行得更快,但是,并

不是启动更多的线程就能让程序最大限度地并发执行。

在进行并发编程时,我们可以通过多线程执行任务让程序运行得更快,但是,也会面临非常多的挑

战。比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制。

2. 什么是多线程并发编程

多线程并发编程是指在一个程序中,同时运行多个线程,每个线程可独立执行不同的任务,并且它们

可以共享同样的内存空间和其他资源。这些线程将并行地执行,从而提高了程序的性能、效率和响应

能力。

在多线程并发编程中,与传统的单线程编程相比,可以使用更少的时间来完成更多的工作,不需要等

待某个任务完成后才能开始下一个任务,也可以更好地处理各种复杂的任务和业务逻辑。

多线程并发编程在实现上,需要考虑一些重要概念,如线程同步、线程通信、死锁、资源竞争等问

题,以确保多个线程可以安全地访问共享的数据结构和变量。如果实现不当,则可能会导致程序出现

各种异常和问题,如死锁、并发错误、数据一致性问题等。

总之,多线程并发编程是一种有效的方式,可以提高程序的性能,并实现更为复杂的任务和业务逻

辑。

3. 并发 & 并行

并发

并发和并行是计算机领域中经常被提到的两个概念,它们都涉及到在同一时间处理多个任务。

并发是指在同一个时间段内,多个任务或者线程同时执行。

在单 CPU 的情况下,并发任务通常采用时间片轮转的方式进行调度,由于 CPU 处理速度非常快,看

上去好像这些任务是在同时进行的。

并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个

任务在单位时间内不一定同时在执行。

在单 CPU 的时代多个任务都是并发执行的,这是因为单个CPU同时只能执行一个任务。

在单 CPU的时代多任务是共享一个 CPU 的,当一个任务占用 CPU 运行时,其他任务就会被挂起,

当占用CPU 的任务时间片用完后,会把 CPU 让给其他任务来使用,所以在单 CPU 时代多线程编程

是没有太大意义的,并且线程间频繁的上下文切换还会带来额外开销。

如图,在单个 CPU 上运行两个线程,线程 A 和线程 B 是轮流使用 CPU 进行任务处理的,也就是在

某个时间内单个 CPU 只执行一个线程上面的任务。当线程A的时间片用完后会进行线程上下文切换,

也就是保存当前线程A的执行上下文,然后切换到线程B来占用 CPU 运行任务。

如图所示,为双 CPU 配置,线程A和线程B各自在自己的 CPU上执行任务,实现了真正的并行运行。

在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程

并行编程。

并行

并行则指的是在多个 CPU 上,同时执行多个任务。每个任务都分配到一个或多个 CPU 核心上运行,

因此各个任务间有较少的干扰,可以达到更高的执行效率。

总之:虽然并发和并行都涉及到在同一时间处理多个任务,但是它们的本质区别在于是否有多个 CPU

参与执行。如果只有一个 CPU 在处理多个任务,则通常是通过时间片轮转等方式来实现并发;如果

有多个

CPU 参与执行,则通常是通过并行来实现。

需要注意的是:在实际应用中,并发和并行往往会共同使用,比如将一个大任务拆分成多个小任务并

发执行,每个小任务再分配到不同的 CPU 核心上并行执行,从而达到更高的效率和性能。

4. 为什么要进行多线程并发编程

多核 CPU 时代的到来打破了单核 CPU对多线程效能的限制。

多个 CPU 意味着每个线程可以使用自己的 CPU 运行,这减少了线程上下文切换的开销,但随着对应

用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切

的需求。

那么学习多线程并发编程主要有以下几点重要原因:

  • 提高程序执行效率:通过使用多线程,可以将一个应用程序分成独立的子任务,并以并行方式同时执行这些子任务,从而提高程序的执行速度。
  • 节省资源:多线程编程可以利用 CPU 的多核心以及多处理器的优势,充分利用系统资源,使得一个程序能更好地发挥出机器的性能。
  • 增加应用程序的响应能力:当一个应用程序需要同时处理多个用户请求时,使用多线程可以让程序同时处理多个请求,从而增加应用程序的响应能力。
  • 减少等待时间:在单线程程序中,如果一个任务执行时间很长,在这段时间内程序不会做其他事情。但是,通过多线程并发编程,可以将任务分解为多个子任务,这些子任务可以并行执行,从而减少等待时间和运行时间。
  • 实现复杂业务逻辑:多线程编程还可以实现复杂的业务逻辑,如死锁避免、任务同步和协作等方面。

补充:响应时间是指系统或者组件从接收到输入数据或请求后,到产生对应输出结果的时间间隔。在计

算机科学和信息技术领域,响应时间常常用来衡量系统的性能和效率。

5. 多线程并发编程带来了什么优势

1. 多核处理器

多核处理器的出现使得计算机能够同时执行多个任务。

如果不使用并发编程,单个线程只能在一个核心上执行,无法充分利用计算机的性能。

2. 提高程序响应速度

并发编程可以提高程序的响应速度。例如,当程序需要等待磁盘IO或网络IO时,如果使用并发编程,

可以让程序在等待IO的同时执行其他任务,从而提高了程序的响应速度。

3. 提高程序可扩展性

并发编程可以让程序具有更好的可扩展性。例如,在一个Web服务器中,如果每个请求都在一个单独

的线程中处理,当请求数量增加时,线程的数量也会增加。这种方式能够让程序具有更好的可扩展

性。

4. 避免阻塞

并发编程可以避免程序阻塞。在单线程程序中,如果一个任务阻塞了,整个程序都会被阻塞。

而在并发编程中,其他的任务可以继续执行,从而避免了整个程序的阻塞。

5. 更好的资源利用

并发编程可以让程序更好地利用计算机的资源。例如,在一个多线程程序中,一个线程可以在等待IO

的同时,让其他线程执行计算密集型的任务,从而更好地利用了计算机的资源。综上所述,学习并发

编程已经成为了必要的技能。通过学习并发编程,可以让程序更好地利用计算机的性能,提高程序的

响应速度和可扩展性,避免程序阻塞,更好地利用计算机的资源。

6. 发挥多处理器的强大能力

现在越来越多的芯片上放置多个处理器核,因为基本的调度单位是线程,如果只有一个线程的话,只

有一个处理器核被使用,其他的相当于被“浪费”,如果用多线程,则大大他搞了处理器的资源利用

率。

6. 多线程并发编程带来的风险

1. 安全性问题

在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。

线程间的通信主要是通过共享访问字段及其字段所引用的对象来实现的。

这种形式的通信是非常有效的,但可能导致两种错误:

线程干扰(Thread Interference)和内存一致性错误(Memory Consistency Errors)。

2. 活跃性问题

一个并行应用程序的及时执行能力被称为它的活跃度(Liveness)。

安全性的含义是“永远不发生糟糕的事情”,而活跃度则关注另外一个目标,即“某件正确的事情最终会

发生”。当某个操作无法继续执行下去,就会发生活跃度问题。在串行程序中,活跃度问题形式之一就

是无意中造成的无限循环(死循环)。

而在多线程程序中,常见的活跃度问题主要有死锁、饥饿以及活锁。

3. 性能问题

在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总是带来某种程度的运行

时开销。而这种开销主要是在线程调度器临时关闭活跃线程并转而运行另外一个线程的上下文切换操

作(Context Switch)上,因为执行上下文切换,需要保存和恢复执行上下文,丢失局部性,并且

CPU时间将更多地花在线程调度而不是在线程运行上。

当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的

数据无效,以及增加贡献内存总线的同步流量。所以这些因素都会带来额外的性能开销。

7. 线程无处不在

1. JVM启动

每个Java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务(例如,垃圾收集、终结操作

等)创建后台线程,并创建一个主线程来运行 main方法。

2. Servlet & JavaServer Page(JSP)

Servlet框架用于部署网页应用程序以及分发来自HTTP客户端的请求。到达服务器的请求可能会通过

一个过滤器链被分发到正确的Servlet或JSP。每个Servlet都表示一个程序逻辑组件,在高吞吐率的网

站中,多个客户端可能同时请求同一个Servlet的服务。

在Servlet规范中,Servlet 同样需要满足被多个线程同时调用,换句话说,Servlet 需要是线程安全

的。即使你可以确保每次只有一个线程调用某个Servlet,但在构建网页应用程序时仍然必须注意线程

安全性。

Servlet 通常会访问与其他Servlet共享的信息,例如应用程序中的对象(这些对象保存在

ServletContext中)或者会话中的对象(这些对象保存在每个客户端的 HttpSession中)。

当一个 Servlet 访问在多个Servlet 或者请求中共享的对象时,必须正确地协同对这些对象的访问,因

为多个请求可能在不同的线程中同时访问这些对象。Servlet和JSP,以及在ServletContext和

HttpSession等容器中保存的Servlet过滤器和对象等,都必须是线程安全的。

3. 远程方法调用(Remote Method Invocation,RMI)

RMI 使代码能够调用在其他JVM 中运行的对象。当通过RMI调用某个远程方法时,传递给方法的参数

必须被打包(也称为列集[Marshaled])到一个字节流中,通过网络传输给远程JVM,然后由远程VM 拆

包(或者称为散集[Unmarshaled])并传递给远程方法。

当 RMI 代码调用远程对象时,这个调用将在哪个线程中执行?你并不知道,但肯定不会在你创建的线

程中,而是将在一个由 RMI 管理的线程中调用对象。

RMI会创建多少个线程?

同一个远程对象上的同一个远程方法会不会在多个RMI线程中被同时调用

远程对象必须注意两个线程安全性问题:正确地协同在多个对象中共享的状态,以及对远程对象本身

状态的访问(由于同一个对象可能会在多个线程中被同时访问。与Servlet相同,RMI 对象应该做好被

多个线程同时调用的准备,并且必须确保它们自身的线程安全性。

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值