【Java】《2小时搞定多线程》个人笔记

简介: 【Java】《2小时搞定多线程》个人笔记

简介

基于慕课网站上的一个一元钱课程《2小时搞定多线程》的 个人笔记。

线程的起源

我们先来看看网络中关于线程起源的说明,理解线程的来龙去脉对于掌握多线程有一定帮助。

此部分内容整理自下面两篇网络博客:

线程的起源与计算机的发展息息相关。早期的计算机系统是单指令模式,资源利用效率低下。当批处理模式即计算机的多指令模式出现后,计算机资源利用率得到有效提升,但这种模式又经常导致 CPU 陷入等待状态,无法得到充分利用,于是进程出现了。

当用户对计算机发出一系列操作指令时,每个进程会将不同的操作储存起来,随时进行切换。但是进程的指令执行效率仍然不够快,无法在同一时刻执行多个任务。为了解决这一问题,技术人员又发明了线程。

有了线程以后,每个操作指令对应的任务都能够被划分为多个子任务,由每一个单独的线程负责,而不同的线程可以同时运行,这样计算机的运行效率便得到进一步提升。

我们可以把上面的几段内容做一个概括:

  1. 单指令模式(类似计算器)。
  2. 多指令模式(批处理)。
  3. 批处理存在CPU等待情况,进程诞生。
  4. 进程指令运行效率不满足需求,为了处理多任务线程诞生。

我们会发现这里存在一些不太清楚的概念,单指令是什么?多指令模式又是什么?为什么批处理存在CPU等待情况等等.....下面我们至上而下进行简单分析。

单指令模式(类似计算器)

在计算机诞生的最早期,计算机属于政府和一些大型公司才有的”昂贵“仪器,受制各种因素限制,当时计算机只能完成一些类似”1+1“的指令操作,并且要完成这样的操作需要用户把程序写入到打孔卡(可以看作最早期的存储设备)并由专门操作人员完成执行,整个过程非常繁琐。

受这样的工作模式限制,不管来多少个用户进行输入,也只能等待计算机管理人员拿到”指令“(物理意义上)完成执行。在”拿指令“和”执行“的间隙,整个计算机都是空闲不干活的,资源利用率极低。

多指令模式(批处理)

随着CPU的执行效率提升以及对CPU资源利用率的要求提升,计算机管理员逐渐成为执行瓶颈,由此诞生了多指令模式。 多指令模式类似饭店点餐一次性下多个指令批量完成。为此人们设计了对应的批处理操作系统,由它代替计算机管理员完成任务的执行切换工作,

批处理可以挨个执行多个指令,此时我们可以把整个计算机本身类比为”单进程“操作,所以批处理在某些情况下依然存在”闲置“的情况,比如4条指令中第3条需要访问IO设备,此时第4条指令哪怕和第3条没有任何关系也依然需要等待。

进程诞生

以上的工作模式被叫做”单道批处理操作系统“,后面为了解决等待问题,人们又设计了多道批处理操作系统(也叫多任务操作系统),它的改进优势如下:

  • 内存划分多个区域,每个区域存储一个程序。
  • 程序执行 I/O 操作时,操作系统会将 CPU 资源分配给其它等待执行的程序。

由此”进程“的基础概念便诞生了,进程就是执行中的应用程序,操作系统会为每个进程分配独立的内存、空间和所需要的资源(IO设备,文件等)。

线程诞生

但是随着计算机软硬件的发展,人们发现像进程这种”指挥军队“的粒度代价很高并且难以控制,后面又提出了线程的概念。

进程调度的一些问题:

  • 进程切换开销大。
  • 进程占用空间是独立的,实现进程通信难度很大。
  • 单个进程本身执行类似IO操作依然会出现等待情况。(只不过此时可以切换到其他进程)

至此我们简单梳理了单指令模式到线程诞生的全过程。

线程和进程的关系

线程包含于进程当中,进程是线程的集合

当操作系统运行时,至少有一个进程会启动,而这个进程中往往包含了多个线程。

线程和进程的区别

相同点如下:

两者都生命周期是由一样的,线程会随着进程结束而一起结束。

不同点如下:

  • 起源不同:先有进程后有线程,早期CPU为了跟上外部操作,后续出现线程的概念来提高效率。
  • 概念不同:线程是CPU调度的最小单位,而进程是操作系统调度程序的独立单位
  • 作用域不同:通常线程存在共享区域,但是在进程和进程之间内容不共享(除非使用类似IPC手段进行进程通信)。
  • 开销不同:进程之间通信需要内核辅助创建开销相对较大,而线程通信创建线程的开销很小。
  • 线程创建终止时间比进程短。
  • 同一个进程内线程切换时间比进程短。
  • 同一个进程内线程可以互相共享文件资源和内存,并且不依赖内核就可以完成。
  • 拥有资源不同,线程在拥有进程大部分基本资源之外还有独立的内容。
  • 数量不同:同一个进程通常只有一个,而每个进程至少有一个线程。

这里强调一下拥有资源不同的含义,线程共享内容包括:

  1. 进程代码段
  2. 进程公有资源(线程可以利用进程的共享资源简单通信)。
  3. 进程打开的文件描述符。
  4. 信号处理器。
  5. 进程当前目录。
  6. 进程ID进程组ID

线程独有内容包括:

  1. 寄存器的值
  2. 线程ID
  3. 线程名称
  4. 线程堆栈
  5. 错误返回号码
  6. 线程信号屏蔽码

Java 和 多线程

为了迎合时代需求,Java自诞生之初就天然支持多线程,Java的多线程实现是和内核线程一对一映射

Jvm 天然多线程验证

Jvm启动需要自动开启一些后台线程维持工作:

  • Finalize线程:处理部分对象的finalize操作。高版本jdk已经弃用此实现
  • Single signature:接收操作系统的信号量来进行一些操作,比如Kill的信号量接收强制关闭程序。
  • main 线程:也叫主线程,其他用户创建的线程都都叫做子线程
  • reference gc:垃圾回收线程,对象清理工作

为了证明上面的理论,我们可以通过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)个线程,线程之间相互协作、共同执行一个应用程序。

现代概念中把仅有单个线程工作的应用程序成为单线程程序。

多线程目的

  • 提高CPU处理效率。
  • 避免无效等待(IO过程可以别的事情)。
  • 提高用户体验,避免卡顿和缩短等待时间。
  • 并行处理:提高性能,多个线程接收http请求。
  • 安卓开发:主线程只能绘制界面。线程不允许io或者网络请求,避免卡顿影响体验。
  • 编程建模。
  • 摩尔定律失效之后,CPU的频率逐渐达到瓶颈,根本处理器越来越接近纳米工艺,再往下原子设计无法突破物理极限。导致单核的性能主频提升已经越过临界点。
  • CPU由单核转为多核多线程,多线程利用越发重要。
  • 阿姆达尔定律:处理器越多,处理效率越高,但是上限取决于串行部分的代码占比,占比越小性能越高。

阿姆达尔定律

在处理器运行单核速度放缓的今天,处理器开始追求多核心多线程,但是需要注意多线程的效率提升取决于代码能够用到多少并行性能

如果一个程序只能单核单线程串行运行,那么程序运行的时候多线程是没有任何意义的,如果代码支持一半并行一半串行,效率提升2倍,如果程序有95%支持并行,那就可以提升20倍性能。

通过下面这个图,可以很直观的看到并行带来质的提升。

image.png

多线程局限

多线程的引入不可避免的带来更为复杂的情况。

  • 异构化任务很难高效并行。
  • 性能安全问题。
  • 上下文切换。
  • 共享数据互相篡改幻读。
  • 缓存失效
  • 线程安全问题。
  • 编码设计逻辑漏洞等。
  • 活跃性问题(线程饥饿、死锁)。
  • 非原子操作问题(例如 i++)。

多线程的生活案例

这里列举生活中吃火锅的案例来理解多线程:

  1. 大火锅一个人吃:单进程单线程串行执行。
  2. 大火锅多人吃:单进程多线程。
  3. 每人小火锅:多进程多线程。
  4. 吃火锅底料:资源不足 。

并发和并行

并发和并行的前提

  • CPU的飞速发展,比如 i7 出现多核多线程。
  • 编程语言自身支持多线程,这一点很重要,比如Java天生具备多线程能力。
  • 一对一映射内核线程。
  • 充分利用操作系统资源。
  • 操作系统本身:操作系统对于多线程的利用也很关键,操作系统通过编程语言的逻辑进行多线程。调度是性能影响的关键。

理清两者概念

并发

image.png

实际上包含了两种概念,第一种:并发存在程序“并发性”,第二种:多个任务的执行状态是“并发”的。

这两种概念都有一个很好的比喻,也就是我们的大脑,我们大脑可以具备“并发性”,比如可以同时操作鼠标和键盘。另一个意义操作鼠标和键盘这两个动作是并发但不是并行的,同时画圆和画圈(需要经过一定的训练)可以看作是并行的。

并发存在程序“并发性”

  • 此时并发和并行的概念不在同一个维度。
  • 同一个时间可能有多个线程接替工作,给使用者的感受好像是在同时工作,比如边敲键盘,边操作鼠标,实际上是受到程序并发性的影响。
  • 不同的部分在无序或者同时执行,但是最终结果不影响。
  • 无论单核还是多核心,只要能得出正确结果,就具备并发性
  • 这时候和多个任务执行状态的概念是不在一个维度上的, 而是更高维度。

多个任务的执行状态是“并发”的

  • 这种情况下是逻辑上的“并行”,并不是真正的并行。
  • 重叠的时间段交替运行
  • 并发不一定并行
  • 并行一定并发
  • 并发,并发和并发的不同

并行

image.png

并行的例子就是两个动作物理上同时发生,比如边打游戏,边接电话这两个动作可以同时进行。

  • 并行是多个核心可以在同一个时间线物理上同时工作
  • 并行一定并发
  • 依托于现代处理的发展
  • 多核能力强化
  • 编程语言支持多线程

两者关系

image.png

上面的图可以得出几个概念

  • 并发不一定并行
  • 并行一定并发
  • 并行和并发是并行包含在并发的概念里面,所以并行的前提是并发

提问:并发程序一定是并行的么?

结论:并发程序不一定并行,但是并行程序一定是并发的。

不一定,因为单核处理器通过快速的上下文切换也可以达到类似并行的效果,实际上是利用抢占式的系统调用和分片式的系统调用完成的。

单核逻辑上同行运行叫做并发。上下文切换非常快,所以会认为是并行的。多核实现了物理上并行,核心和核心之间互相独立,可以真正意义上物理时间可以实现。

比喻:是一个人操作多条流水线,好像每一个流水线都可以处理任务。多个人在流水线上,一个流水线挂了可以由别的流水线接收。

并发继续拓展

  • 表面上多个任务执行状态。
  • 程序上的并发性也叫做并发。

高并发和多线程关系

多线程是解决高并发问题的解决方案之一,但是多线程不是高并发的唯一办法。

比如redis操作数据就是单线程实现的(保证原子性非常简单),因为没有上下文切换十分高效,它没有用到多线程却页解决了数据库高并发的问题,分担压力。

拓展:

  • 并行的程序执行效率取决于开发者的代码。
  • 取决于处理器的性能。
  • 操作系统的调度。

高并发的性能指标

  • QPS:每秒的查询数量,越高说明服务器可以承受的瞬间压力越大
  • 带宽:决定了例如视频网站的服务质量。
  • PV(Page View):也就是访问和点击量。
  • UV(Unique View):表示单个用户访问的次数,是对于PV的访问量和点击率
  • UV一定会小于PV
  • IP和UV:最大的区别是是否是同一个用户的操作决定,也就是Cookie
  • 两个不同的指标
  • 第一个指标 IP:拨号上网IP变动
  • IP是每个人单独IP,但是访问者的Cookie是一样的
  • IP+1,Uv不变
  • 第二个指标 UV:局域网同一个账户多个人使用
  • IP可能没有变但是Cookie在切换
  • IP没有变,UV+1
  • 并发连接数
  • 某个时刻同时接受请求数量
  • 服务器平均等待时间
  • 处理一个请求所需时间。

同步和异步/阻塞和非阻塞

同步和异步

区分关键点:被调用方的行为

同步

强调的是被调用者(服务器)行为,不是请求方的行为。没有得到结果之前,服务端不返回任何结果。

和阻塞的判断刚好相反。

再次强调是被调用者(服务器)行为,不是请求方的行为。

image.png

image.png

异步

调用之后服务端立刻返回结果(通常是一个通知)。

image.png

image.png


案例:烧水壶、买书

烧水壶:传统的铁壶需要等待水烧开才会有结果,电水壶只需要理解返回启动结果即可,因为后续水烧开之后会断电并且提醒。

买书:同步就是书店买书直到老板给出想要的书之前会一直被迫等待(被调用方的行为),网上买书下单之后直接通知结果过几天之后到货。

阻塞和非阻塞

关键:对于调用者而言的服务端状态

  • 站在线程状态角度
  • 站在线程发出请求之后请求方的角度

案例:烧水壶、买书

烧水壶:阻塞就是看着水一直到烧开,非阻塞就是烧水的时候间隔一段时间看一眼。

买书:阻塞就是拿到要买的书籍之前老老实实等待(调用方等待),非阻塞书店老板找书的过程中可以逛逛书店看看其他的书籍。

综合案例

综合案例用洗衣服的案例来理解。

同步阻塞

洗衣服丢到洗衣机,全程看着洗衣机洗完,洗好之后晾衣服。

同步非阻塞

把衣服丢到洗衣机洗,然后回客厅做其他事情,定时看看洗衣机是不是洗完了,洗好后再去晾衣服。(等待期间你可以做其他事情,比如用电脑刷剧看视频)。

异步阻塞

把衣服丢到洗衣机洗,然后看着洗衣机洗完,洗好后再去晾衣服(没这个情况,几乎没这个说法,可以忽略)。

异步非阻塞

把衣服丢到洗衣机洗,然后回客厅做其他事情,洗衣机洗好后会自动去晾衣服,晾完成后放个音乐告诉你洗好衣服并晾好了

常见问题汇总

线程和进程的相同与不同点

不同点:

  • 起源不同
  • 概念不同
  • 性能开销不同
  • 作用域不同
  • 拥有资源不同
  • 数量不同

相同点:生命周期

并发和并发

  • 并发和高并发是一个包含关系(并行包含并发),一个程序并行意味着一定是并发,但是并发可以模拟出并行的效果。
  • 并发有两种概念,如果是程序“并发性”,则并行和并发不在一个维度,可以任务无论单核或者多核只要结果正确就具备并发性。另外一个程序具备并发性也算是并发的说法。

多线程的弊端

  • 异构化任务无法用多线程完成的任务不如单线程高效。
  • 线程安全问题,比如共享变量互相覆盖。
  • 性能问题,比如上下文切换、缓存失效。

高并发是否意味着多线程

  • 多线程仅仅是高并发的解决方案之一,两者是两个不同的概念,不能混为一谈。
  • 多线程不是唯一办法,但确实是主要办法。

缓存、消息队列、锁是高并发的三架马车

同步、异步、阻塞、非阻塞

从并发编程的角度对着四个概念进行再次整理。

同步异步:和队列有关,事情能不能委托给其他人来办。 阻塞非阻塞:和锁的机制有关,做一个工作的时候能不能抽空干别的事情。

洗衣机洗衣服:

  • 同步阻塞:开启洗衣机,并且全程盯着洗衣机工作。
  • 同步非阻塞,开启洗衣机,虽然还是要隔几分钟看洗衣机是否完成工作,但是期间可以干别的事情。
  • 异步阻塞:委托给洗衣机自己洗衣服,但是要全程盯着取出衣服最后把衣服晾了。
  • 异步非阻塞:告诉洗衣机自己洗衣服,工作完成之后洗衣机自动把衣服晾了,最后告知结果。

单核CPU上多线程的意义

  • 开启多个线程可以让耗时的任务交给后台处理,利用其他线程提供服务。
  • 程序不知道运行在单核还是多核,单核CPU也可以充分利用多线程提高资源利用率。
相关文章
|
5天前
|
Java
Java基础—笔记—static篇
`static`关键字用于声明静态变量和方法,在类加载时初始化,只有一份共享内存。静态变量可通过类名或对象访问,但推荐使用类名。静态方法无`this`,不能访问实例成员,常用于工具类。静态代码块在类加载时执行一次,用于初始化静态成员。
9 0
|
5天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
13 0
|
6天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
7天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
20 3
|
6天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
4天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
2天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
3天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
13 1
探秘jstack:解决Java应用线程问题的利器
|
3天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
14 0
|
5天前
|
Java API
Java基础—笔记—内部类、枚举、泛型篇
本文介绍了Java编程中的内部类、枚举和泛型概念。匿名内部类用于简化类的创建,常作为方法参数,其原理是生成一个隐含的子类。枚举用于表示有限的固定数量的值,常用于系统配置或switch语句中。泛型则用来在编译时增强类型安全性,接收特定数据类型,包括泛型类、泛型接口和泛型方法。
9 0