基于慕课网站上的一个一元钱课程《2小时搞定多线程》的 个人笔记。
我们先来看看网络中关于线程起源的说明,理解线程的来龙去脉对于掌握多线程有一定帮助。
此部分内容整理自下面两篇网络博客:
线程的起源与计算机的发展息息相关。早期的计算机系统是单指令模式,资源利用效率低下。当批处理模式即计算机的多指令模式出现后,计算机资源利用率得到有效提升,但这种模式又经常导致 CPU 陷入等待状态,无法得到充分利用,于是进程出现了。
当用户对计算机发出一系列操作指令时,每个进程会将不同的操作储存起来,随时进行切换。但是进程的指令执行效率仍然不够快,无法在同一时刻执行多个任务。为了解决这一问题,技术人员又发明了线程。
有了线程以后,每个操作指令对应的任务都能够被划分为多个子任务,由每一个单独的线程负责,而不同的线程可以同时运行,这样计算机的运行效率便得到进一步提升。
我们可以把上面的几段内容做一个概括:
我们会发现这里存在一些不太清楚的概念,单指令是什么?多指令模式又是什么?为什么批处理存在CPU等待情况等等.....下面我们至上而下进行简单分析。
在计算机诞生的最早期,计算机属于政府和一些大型公司才有的”昂贵“仪器,受制各种因素限制,当时计算机只能完成一些类似”1+1“的指令操作,并且要完成这样的操作需要用户把程序写入到打孔卡(可以看作最早期的存储设备)并由专门操作人员完成执行,整个过程非常繁琐。
受这样的工作模式限制,不管来多少个用户进行输入,也只能等待计算机管理人员拿到”指令“(物理意义上)完成执行。在”拿指令“和”执行“的间隙,整个计算机都是空闲不干活的,资源利用率极低。
随着CPU的执行效率提升以及对CPU资源利用率的要求提升,计算机管理员逐渐成为执行瓶颈,由此诞生了多指令模式。多指令模式类似饭店点餐一次性下多个指令批量完成。为此人们设计了对应的批处理操作系统,由它代替计算机管理员完成任务的执行切换工作,
批处理可以挨个执行多个指令,此时我们可以把整个计算机本身类比为”单进程“操作,所以批处理在某些情况下依然存在”闲置“的情况,比如4条指令中第3条需要访问IO设备,此时第4条指令哪怕和第3条没有任何关系也依然需要等待。
以上的工作模式被叫做”单道批处理操作系统“,后面为了解决等待问题,人们又设计了多道批处理操作系统(也叫多任务操作系统),它的改进优势如下:
由此”进程“的基础概念便诞生了,进程就是执行中的应用程序,操作系统会为每个进程分配独立的内存、空间和所需要的资源(IO设备,文件等)。
但是随着计算机软硬件的发展,人们发现像进程这种”指挥军队“的粒度代价很高并且难以控制,后面又提出了线程的概念。
进程调度的一些问题:
至此我们简单梳理了单指令模式到线程诞生的全过程。
线程包含于进程当中,进程是线程的集合。
当操作系统运行时,至少有一个进程会启动,而这个进程中往往包含了多个线程。
相同点如下:
两者都生命周期是由一样的,线程会随着进程结束而一起结束。
不同点如下:
这里强调一下拥有资源不同的含义,线程共享内容包括:
线程独有内容包括:
为了迎合时代需求,Java自诞生之初就天然支持多线程,Java的多线程实现是和内核线程一对一映射。
Jvm启动需要自动开启一些后台线程维持工作:
为了证明上面的理论,我们可以通过IDEA进行调试来验证答案。
首先我们通过IDEA编写一个HelloWorld程序,当然为了方便这里个人直接拿了SpringBoot的Main启动代码进行验证。
public class InterviewApplication {
public static void main(String[] args) {
SpringApplication.run(InterviewApplication.class, args);
}
}
我们把Debug断点打在代码的第一行,然后Idea中直接Debug运行。
image.png
通过下面的筛选功能,我们可以Debug中切换到其他的线程进行观察多线程执行情况。
image.png
下面j结果使用为JDK11运行。
image.png
除了上面这种观察方式之外,我们可以通过“Threads“视图界面观察所有线程的运行情况。以IDEA2022版本为例,打开”Threads“视图只需要在右上角点击小方块然后勾选“Threads”即可。
下面结果使用JDK8运行。
image.png
个人更喜欢上面的展现方式,平铺直叙告诉开发者当前断点内的线程运行情况。
如果想要多线程Debug,可以鼠标右击断点,接着会出现相关提示切换到“Thread”,,在调试多线程代码的时候,这个操作会非常方便好用。
image.png
通过上面的简单讲解可以证明Java天生就是多线程程序(哪怕只有一行代码)。
一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序。
现代概念中把仅有单个线程工作的应用程序成为单线程程序。
在处理器运行单核速度放缓的今天,处理器开始追求多核心多线程,但是需要注意多线程的效率提升取决于代码能够用到多少并行性能。
如果一个程序只能单核单线程串行运行,那么程序运行的时候多线程是没有任何意义的,如果代码支持一半并行一半串行,效率提升2倍,如果程序有95%支持并行,那就可以提升20倍性能。
通过下面这个图,可以很直观的看到并行带来质的提升。
image.png
多线程的引入不可避免的带来更为复杂的情况。
这里列举生活中吃火锅的案例来理解多线程:
利用超线程技术模拟多核并发
实际上包含了两种概念,第一种:并发存在程序“并发性”,第二种:多个任务的执行状态是“并发”的。
这两种概念都有一个很好的比喻,也就是我们的大脑,我们大脑可以具备“并发性”,比如可以同时操作鼠标和键盘。另一个意义操作鼠标和键盘这两个动作是并发但不是并行的,同时画圆和画圈(需要经过一定的训练)可以看作是并行的。
并发存在程序“并发性”:
多个任务的执行状态是“并发”的:
image.png
并行的例子就是两个动作物理上同时发生,比如边打游戏,边接电话这两个动作可以同时进行。
image.png
上面的图可以得出几个概念
结论:并发程序不一定并行,但是并行程序一定是并发的。
不一定,因为单核处理器通过快速的上下文切换也可以达到类似并行的效果,实际上是利用抢占式的系统调用和分片式的系统调用完成的。
单核逻辑上同行运行叫做并发。上下文切换非常快,所以会认为是并行的。多核实现了物理上并行,核心和核心之间互相独立,可以真正意义上物理时间可以实现。
比喻:是一个人操作多条流水线,好像每一个流水线都可以处理任务。多个人在流水线上,一个流水线挂了可以由别的流水线接收。
并发继续拓展
多线程是解决高并发问题的解决方案之一,但是多线程不是高并发的唯一办法。
比如redis操作数据就是单线程实现的(保证原子性非常简单),因为没有上下文切换十分高效,它没有用到多线程却页解决了数据库高并发的问题,分担压力。
拓展:
区分关键点:被调用方的行为
强调的是被调用者(服务器)行为,不是请求方的行为。没有得到结果之前,服务端不返回任何结果。
和阻塞的判断刚好相反。
再次强调是被调用者(服务器)行为,不是请求方的行为。
image.png
image.png
调用之后服务端立刻返回结果(通常是一个通知)。
image.png
image.png
烧水壶:传统的铁壶需要等待水烧开才会有结果,电水壶只需要理解返回启动结果即可,因为后续水烧开之后会断电并且提醒。
买书:同步就是书店买书直到老板给出想要的书之前会一直被迫等待(被调用方的行为),网上买书下单之后直接通知结果过几天之后到货。
关键:对于调用者而言的服务端状态
案例:烧水壶、买书
烧水壶:阻塞就是看着水一直到烧开,非阻塞就是烧水的时候间隔一段时间看一眼。
买书:阻塞就是拿到要买的书籍之前老老实实等待(调用方等待),非阻塞书店老板找书的过程中可以逛逛书店看看其他的书籍。
综合案例用洗衣服的案例来理解。
洗衣服丢到洗衣机,全程看着洗衣机洗完,洗好之后晾衣服。
把衣服丢到洗衣机洗,然后回客厅做其他事情,定时看看洗衣机是不是洗完了,洗好后再去晾衣服。(等待期间你可以做其他事情,比如用电脑刷剧看视频)。
把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(没这个情况,几乎没这个说法,可以忽略)。
把衣服丢到洗衣机洗,然后回客厅做其他事情,洗衣机洗好后会自动去晾衣服,晾完成后放个音乐告诉你洗好衣服并晾好了。
不同点:
相同点:生命周期
缓存、消息队列、锁是高并发的三架马车
从并发编程的角度对着四个概念进行再次整理。
同步异步:和队列有关,事情能不能委托给其他人来办。阻塞非阻塞:和锁的机制有关,做一个工作的时候能不能抽空干别的事情。
洗衣机洗衣服: