「重磅开篇」形成完善的多线程世界观
? 尽人事,听天命。博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步
? 本文已收录于 「CS-Wiki」Gitee 官方推荐项目,现已累计 1.6k+ star,致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习
? 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 600+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中
关于这篇文章我很早就想写了,也一直不敢写,一方面是早先知识储备不足,另一方面主要是多线程这部分内容确实比较高深而且每个知识点之间比较零散,让人摸不着头脑,不知道该从哪里下手。而且对于我们学生群体来讲,很少有机会接触到高并发这方面的真实场景,平常自己敲代码也基本不会用到,所以也导致我们大部分同学都是面向面经学习,你问 synchronized,叭叭叭我能说一堆,你问 volatile,叭叭叭我也能说一堆,但总感觉差点意思,就是这些知识点是零散的,没有那么一根线把它们很好的串联起来。
所以今天我斗胆造一根线,站在小白的角度,讲讲多线程这部分我们到底要学啥,按照什么样的顺序去学,帮助各位建立一个比较完善的知识体系,形成正确的多线程世界观。后续的文章我也基本上会按照这根线写下来。
然后,我目前也没有踏入工作岗位,也没有实际的高并发经验,所以只是在纸上谈兵,学识尚浅,大佬们若觉得有问题恳请评论区或者私聊我指正,晚辈感激不尽(抱拳)。
炼气
首先,学习多线程,你肯定得知道线程是啥吧,包括线程的一些基础概念(比如上下文切换),那么说到线程,肯定离不开进程。OK,进程和线程这两个概念其实我们在操作系统这门课中都接触过,当然并行和并发、同步与异步等这种基本概念咱也默认你学过,那么你还需要去了解一下 Java 线程和操作系统的线程有啥区别。
另外,容易被大家忽视的一点是,一项技术的出现必定不是凭空捏造的,他一定是为了某个目的而来,在某个成熟的时机应运而生。因此,你需要知道我们为啥要使用多线程,多线程的出现解决了什么问题。
掌握上面这一步,我们称之为炼气,所谓炼精化气,起步阶段需一心一意、沉心静气。
筑基
现在我们已经知道线程是啥了,那在 Java 中如何创建线程呢?为此你会接触到三种创建线程(Thread)的方式:
- 直接使用 Thread
- Thread + Runnable
- Thread + Callable + FutureTask
学会了如何创建线程,我们去翻一翻 Thread 类的源码,你会发现其中定义了 Java 线程的六种状态,也就是所谓的生命周期,它和操作系统中线程的五态模型又有啥区别和联系呢?
既然都翻了 Thread 源码,岂有不深究的道理?我们接下来去学习一下 Thread 类给我们提供了哪些控制线程的方法,它们分别能干啥,怎样影响了线程的状态:
- start / run
- sleep / yield
- join / join(long n)
- interrupt
- setDaemon 守护线程
这一阶段的学习,也就是入门阶段后的第一步,我们称之为筑基。基础不牢,地动山摇。
金丹
诚然,一个程序顺序的运行多个线程本身是没有问题的,但是如果多个线程同时访问了某个共享资源,就可能会发生不可预知的现象,也就是我们常说的线程安全问题,要了解这些问题产生的根本原因,我们就需要去深刻的了解 Java 内存模型(Java Memory Model,JMM)。
为此,我们会学习到和线程安全息息相关的三大性质:
1)原子性:一个操作是不可中断的,要么全部执行成功要么全部执行失败(也可以说是提供互斥访问,同一时刻只能有一个线程对数据进行操作)
2)可见性:当一个线程修改了共享变量后,其他线程能够立即得知这个修改
3)有序性(或者说重排序):重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。由于重排序的存在,可能导致多线程环境下程序运行结果出错的问题。
那么编译器和处理器在重排序时会遵守什么原则呢?为此你会了解到数据依赖性和 as-if-serial,这里简单介绍一下这两个概念:
- 编译器和处理器在重排序时,会遵守数据依赖性,它们不会改变存在数据依赖性关系的两个操作的执行顺序
- as-if-serial 语义的意思是:不管怎么重排序,程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义
事实上,可见性和有序性其实是互相矛盾的两点。一方面,对于程序员来说,我们希望内存模型易于理解、易于编程,为此 JMM 的设计者要为程序员提供足够强的内存可见性保证,专业术语称之为 “强内存模型”。而另一方面,编译器和处理器则希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化(比如重排序)来提高性能,因此 JMM 的设计者对编译器和处理器的限制要尽可能地放松,专业术语称之为 “弱内存模型”。
当然,对于这个问题,JMM 的设计者找到了一个很好的平衡点,那就是 happens-before,这是 JMM 最核心的概念!理解 happens-before 是理解 JMM 的关键。
知其然而知其所以然,这一阶段,我们称为金丹。
渡劫
具体到 Java 语言层面,是怎么保证线程安全的呢?也就是如何保证原子性、可见性和有序性呢?(保证有序性上文已经说过了,就是使用 happens-before 原则)。
1)对于可见性,可以使用 volatile
关键字来保证。不仅如此,volatile
还能起到禁止指令重排的作用;
2)对于原子性,我们可以使用 锁 和 java.util.concurrent.atomic
包中的原子类来保证。(给萌新解释一下,java.util.concurrent,简称 J.U.C,就是一个包,也成为并发包。现在网上大部分博客都会直接说 JUC,对萌新不是很友好),我们可以看看 juc.atomic 中有哪些类:
当然, atomic 包下这些原子操作类保证原子性最关键的原因还是因为它们使用了 CAS 操作,于是,你需要先去深入学习一下 CAS,了解 CAS 存在的三个问题,然后再去挖一挖这些原子类的底层原理。
另外,上面我们提到的锁这个话题其实又是一个非常核心的知识点,在深入学习之前,你需要了解一下各种锁的概念:
- 悲观锁和乐观锁
- 重量级锁和轻量级锁
- 自旋锁
- 偏向锁
- 重入锁和不可重入锁
- 公平锁和非公平锁
- 共享锁和排他锁
另外,与锁相关的概念的还有临界区、竞态条件等,这些你都是要去了解的。
那么锁在 Java 中具体是怎么实现的呢?早先 Java 程序是靠 synchronized
关键字实现锁功能的,在我们掌握了 synchronized
的使用方式以及底层原理后,你还会接触到与 synchronized
配套的 wait/notify/notifyAll
方法。
在 Java SE 5 之后,并发包 JUC 中新增了 Lock
接口以及相关实现类(放在 java.util.concurrent.locks
包下)也可以用来实现锁功能。
为什么会新增这样一个 Lock
接口及其相关实现类呢?因为使用 synchronized
关键字会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。
例如,针对一个场景,手把手进行锁获取和释放,先获得锁 A,然后再获取锁 B,当锁 B 获得后,释放锁 A 同时获取锁 C,当锁 C 获得后,再释放 B 同时获取锁 D,以此类推。这种场景下,如果使用 synchronized
关键字就不那么容易实现了,而使用 Lock
却容易许多。
它提供了与 synchronized
关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized
关键字所不具备的同步特性。
另外,还有一点非常重要的是!我们可以去翻一翻实现了 Lock
接口的类,比如 ReentrantLock
(大部分文章都会直接把它翻译成重入锁),你会惊讶的发现它并没有多少代码,基本所有的方法都是调用了其静态内部类 Sync
中的方法,而 Sync
类继承了 AbstractQueuedSynchronizer
类(也就是大名鼎鼎的 AQS,译为队列同步器,简称同步器)。
可以把 AQS 理解为一个用来构建锁和同步器(工具类)的框架,locks 包中的各种锁以及接下来我们会学习的 JUC 中的工具类都是基于 AQS 来实现的。
OK,关于 AQS 这篇文章就不再多说了。上面我们提到了两个并发关键字,synchronized
和 volatile
,其实还有一个,那就是 final
,可能很多小伙伴都不知道,啥?final
和并发有啥关系?当然,这些,后续文章都会写的。
本阶段的知识非常重要,并且相对来说知识点比较多也比较难,因此我们称之为渡劫。
大乘
渡劫完毕,走到这一步各位对多线程基本的知识架构已经有了一定的认知,世界观已经初步形成,最后,就是补强的过程了,我们来看看 J.U.C 这个包还有什么东西(下图没有截全):
JUC 其实可以分为五大类:
- Lock 框架(locks 包)
- 原子类(atomic 包)
- 并发集合
- 线程池
- 工具类
后面三种正是我们在这一阶段需要学习的。并发集合和线程池就没啥好说的了,它们的知识点都比较集中,学习目标也很明确,网络上很容易就能找到一篇条理清晰的文章。
然后常用的工具类还是有必要学习下:
CountDownLatch
CyclicBarrier
Semaphore
Exchanger
所谓工具类嘛,那一定是封装了某些比较复杂的操作,使我们可以很简单的去完成这些操作。以 CountDownLatch 为例:在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用 Thread 类的 join 方法,让主线程等待被 join 的线程执行完之后,主线程才能继续往下执行。而 Java 并发工具类中为我们提供了这样一个类似 “倒计时” 的工具类 CountDownLatch,可以十分方便的完成这种业务场景。
另外,还有一个比较重要的类,我也不知道怎么给它分类,就是 ThreadLocal
,江湖人称线程隔离术,必问高阶考点。
OK,学完了本阶段,多线程世界观已完整形成,我们称之为大乘,忘我之境,全在己心。
? 关注公众号 | 飞天小牛肉,即时获取更新
- 博主东南大学硕士在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 基础和面试指南的相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。希望大家多多支持哦,和小牛肉一起成长 ?
- 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.6k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ ?
- 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 600+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中。
相关文章
- 在 Go 里用 CGO?这 7 个问题你要关注!
- 9款优秀的去中心化通讯软件 Matrix 的客户端
- 求职数据分析,项目经验该怎么写
- 在OKR中,我看到了数据驱动业务的未来
- 火山引擎云原生大数据在金融行业的实践
- OpenHarmony富设备移植指南(二)—从postmarketOS获取移植资源
- 《数据成熟度指数》报告:64%的企业领袖认为大多数员工“不懂数据”
- OpenHarmony 小型系统兼容性测试指南
- 肯睿中国(Cloudera):2023年企业数字战略三大趋势预测
- 适用于 Linux 的十大命令行游戏
- GNOME 截图工具的新旧截图方式
- System76 即将推出的 COSMIC 桌面正在酝酿大变化
- 2GB 内存 8GB 存储即可流畅运行,Windows 11 极致精简版系统 Tiny11 发布
- 迎接 ecode:一个即将推出的具有全新图形用户界面框架的现代、轻量级代码编辑器
- loongarch架构介绍(三)—地址翻译
- Go 语言怎么解决编译器错误“err is shadowed during return”?
- 敏捷:可能被开发人员遗忘的部分
- Denodo预测2023年数据管理和分析的未来
- 利用数据推动可持续发展
- 在 Vue3 中实现 React 原生 Hooks(useState、useEffect),深入理解 React Hooks 的