前言
“如何做一个合格的多线程开发者? 你真的懂多线程么?” 作为编程初学者被问的最多的问题, 本文就这个问题. 详细的讲讲对方究竟为什么要问这个问题, 并且回答问题的主要思路框架.
PS: 本文主体背景为Java语言. 其他语言应当为同理.
问题 - 单线程问题
-
什么是进程?什么是线程? 什么是协程?
-
如何创建一个线程? 线程有哪些状态? 其如何变化?
-
如何停止一个线程? 如何优雅的停止一个线程?
问题 - 线程池问题
-
什么是线程池?
-
请描述下线程池的工作原理?
-
线程池的核心参数有哪些?
-
实战问题1: 一般如何设计这些参数?
-
实战问题2: 开发中都使用哪些线程池? 都是如何使用的?
问题 - 锁 - 线程竞争
-
什么是锁?
-
锁的种类有哪些?
-
锁升级过程?
-
[Java] synchronized锁的实现原理?
-
[Java] RentrankLock锁的实现原理?
-
[Java] 乐观锁的实现原理? 缺点? 如何解决缺点?
-
如何防止超买问题? 如何防止超卖问题?
问题 - 多线程集合工具
-
线程安全的集合工具有哪些?
-
为什么说ArrayList是非线程安全的?
-
如何处理ArrayList的非线程安全问题? 有什么代替的线程安全结构? 实现原理?
-
为什么说HashMap是非线程安全的?
-
如何处理HashMap的非线程安全问题? 有什么代替的线程安全结构? 实现原理?
-
实战问题1: 开发过程中, 都使用哪些线程安全的集合工具?
问题 - 多线程并发工具
-
现在有A和B, 2个线程? 如何使A线程先执行, B线程后执行?
-
现在有A和B, 2个线程? 如何使A线程执行2次, B线程执行1次?
-
现在又ABCDE, 5个线程? 如何使AB先并发执行, CDE随后并发执行?
-
现在又ABCDEF, 6个线程? 如何保证同时只有5个线程同时执行?
问题 - 实战
-
实战问题1: 开发过程中都会遇到哪些多线程问题? 都是如何解决的?
-
实战问题2: 单例模式时候的多线程问题?
回答 & 目的
本文的主旨是探讨多线程问题设计的目标, 此处应当不涉及具体问题的解答. 为梳理多线程相关的知识点, 为考察自己做一个简单的疏漏分析, 也为检查他人对于多线程的掌握做一个评判标准.
问题 - 单线程问题
在询问单线程的时候, 应当需要考察读者对于多线程的理解. 这与将军带兵打仗时一个道理, 如何合理的带兵打仗一样? 什么是士兵? 应当如何合理的派遣士兵.
-
线程的宏观认知
首先, 进程应当是操作系统内执行程序的基本单位. 但是进程之间的数据是不共享的. 为了方便操作, 引出线程和协程的概念, 其主要有2点优势. 1. 相比进程, 其声明和销毁更具有轻量化的特征. 2. 进程之间资源共享. -
线程的生命周期 & 状态转换
随后, 当对于线程有一个宏观的了解后, 应当考察对于线程的创建, 线程的生命周期, 线程的管理. 等几个问题. 就其本质都是线程的生命周期问题. 线程的生命周期问题通常有3态模型和5态模型. 如下所示:
此处关于线程3态, 5态, 7态模型的问题, 先以3张图示掠过, 后期会进行详细叙述. 我们工作中常用的模型即是线程的5态模型. 3态模型较为简单, 7态模型较为复杂.
- 线程关键问题1 - 如何创建线程
如何创建线程. 对于Java程序来说, 一般是包含4种方式, 其实都是一种创建的方式. 即Thread newThread = new Thread();
Thread newThread = new Thread();
Thread newThread = new Thread( new Runnable() { xxx});
即重写Runnable接口. 其本质还是通过声明Thread对象.- 通过Future接口和Callable接口. 其本质也是通过
Thread newThread = new Thread();
来进行声明的. - 通过线程池. 线程池其本质是一个享元模型的设计模式, 其支持Runnbale, Future, Callable接口的提交, 其本质也是维护了一个FutureTask对象, 其也是一个new Thread的操作.
详细可见[]
- 线程关键问题2 - 如何销毁线程
如何销毁线程. 我们可以通过3种方式:
- 强制关闭. 前后通过try–catch的方式进行异常捕获, 此处会立即关闭线程. 不推荐, 因为可能部分线程内的任务并没有结束, 会导致异常. 举个例子, 比如处理10条消息存储, 当存储到5条消息时候, 手动stop, 此时会导致后面5条消息无法处理. 当然我们还可以通过异常捕获或者数据库事务的方式进行问题解决. 此处为非线程相关的问题.
- 通过isInterrupted()方法, 进行标志位退出.
- 通过其他标志位退出. 此方法和isInterrupted()类似.
- 线程关键问题3 - 线程的start()和run()方法有什么区别?
- start()方法. 调用后, 启动调用的线程, 随后其线程由创建状态转变为就绪态, 等待进行执行. 其过程会执行
start()-run()-stop()
顺序执行线程. - run()方法. 调用后, 执行的线程为当前线程, 在main方法内通常为主线程, 单纯的方法调用, 线程的状态未发生任何变化.
小结: 上述5个问题, 可以对于单线程问题有一个全面的了解. 从浅入深, 非常适合.
问题 - 线程池问题
线程池问题, 也是Java内对于线程的考察必问的问题之一. 我们在真实的工作场景中, 对于线程而言, 线程都不是单打独斗的, 也不是自己管理的, 我们通常使用线程池进行处理. 即便Java上层抽象出了Executors和ExecuteService等上层抽象, 我们对于线程池还是需要一定量的了解.
首先, 我们通常问的问题也是线程池的声明, 并且包含其哪些关键参数. 其次, 需要考察, 线程池的种类, 线程池的运行原理. 如果你想考察其是否有真实的线程池经理, 可以问线程池遇到的问题, 线程池是否需要OOM问题, 多个线程池应当如何管理, 如何监控线程池的运行状态等等. 如下给出一些简单的问题的简要回答.
-
宏观 - 什么是线程池?
线程池是用于线程管理的线程集合, 其用于管理多个线程的使用. 其中通常包含核心线程和临时线程, 因为线程的声明和销毁通常较为消耗服务器性能, 所以, 通常使用享元模型的线程池进行管理. 其通常包括 核心线程, 临时线程, 消息队列, 消息工厂等常见元素. -
请描述下线程池的工作原理?
线程池通常包括 核心线程, 临时线程, 消息队列, 消息工厂等几个常见组成部分. 当新的任务提交至线程池时:
- 线程池会判断当前核心线程是否达最大值. 若没有, 则创建一个新的核心线程, 并将新的任务分配给新的核心线程. 若达到最大值, 则进行下一步.
- 如果核心线程数目已经满了. 线程池会将查看当前线程池的消息队列, 若消息队列未满, 其会将新的任务放入消息队列, 若满, 则进行下一步.
- 如果消息队列已满. 线程池会查看当前临时线程是否达到最大值, 若未满, 其会创建临时线程, 并将新的任务分配给临时线程, 且临时线程在执行结束后, 会经过规定时间的等待, 随后进行销毁. 若临时线程也达到最大值, 则进行下一步.
- 如果临时线程数已满, 线程池会根据设置的拒绝策略进行拒绝任务的执行, 通常有抛出异常等.
-
线程池的核心参数有哪些?
其主要包括. 核心线程数, 总线程数目, 临时线程存活时间, 临时线程存活时间单位, 消息队列, 线程创建工厂, 拒绝策略. 这7个核心参数. 其常见的类型具体可以看. -
实战问题1: 一般如何设计这些参数?e
截至JDK1.8, 线程池通常包含, 单线程池SIngleThreadPool, 常量线程池FixedThreadPool, 变量线程池CacheableThreadPool, 定时线程池ShceduleTheadPool, ForkJoinThreadPool 这几种类型.
- 核心线程数, 总线程数目. 通常设置成一样的. 防止频繁的生成和销毁线程产生开销, 缺点是, 线程可能声明太多, 占用内存空间.
- 消息队列. 通常使用规定长度的队列, ArrayListBlockingQueue. 防止内存溢出, CacheableThreadPool使用的是无限长度的队列, 可能导致OOM问题.
- 核心线程数, 总线程数目. 其有时候根据CPU密集型和IO密集型进行区分, CPU密集型通常为可用CPU核心数目的1倍, IO密集型通常为
可用CPU核心数目的2倍. 但是具体的数值可以进行性能压测得出最完美的数值.
- 实战问题2: 开发中都使用哪些线程池? 都是如何使用的?
开发过程中, 通常使用FixedThreadPool, ForkJoinThreadPool这2种.
- 使用时需要手动声明线程池, 而非直接放入JDK的内置的Common线程池内.
- 通常推荐使用ForkJoinThreadPool, 对于任务较为频繁的线程池, 对于任务不频繁的使用FixedThreadPool.
- 且注意使用额定长度的消息队列和合适的拒绝策略.
问题 - 线程池问题
当多个线程之间出现竞争的时候, 通常使用锁来处理, 让后获取的一方进行等待或者直接抛出异常. 从初期的Thread.yeild()和Thread.join()方法的线程通信, 后期衍生出synchronized和violatile关键字, 到最后的ReentanckLock. 其经历了一些列的衍生过程. 对于Java内部锁的考察通常就考察这一些列的使用和原理. 当然有时还会考察操作系统种, “如何产生死锁问题?” 和 "如何避免死锁问题?’ “如何解决死锁问题?”这3个问题.
-
宏观 - 什么是锁?
Java内的锁. 即是指定一个Java的内容空间, 防止其他线程进行访问. 宏观意义上, 锁可以是任何资源. -
锁的种类有哪些?
- 从锁的抽象来说. 乐观锁和悲观锁.
- 从锁的使用. 读锁, 写锁, 读写锁.
- 从Java的使用, synchronized锁, ReentranckLock锁.
- 从Java锁升级的过程. 偏向锁, 排他锁.
- Mysql内, 页锁, 表锁, 行锁等.
-
锁升级过程?
略. -
[Java] synchronized锁的实现原理?
class文件上设置当前持有的线程线程号. -
[Java] RentrankLock锁的实现原理?
同上? -
[Java] 乐观锁的实现原理? 缺点? 如何解决缺点?
- 乐观锁. 设置一个值, 开始和结束进行比较, 如果出现变化则锁失效.
- CAS. ABA问题.
- 设置版本号和值. 双重判断.
- 如何防止超买问题? 如何防止超卖问题?
- update where count > 1
- 加锁.
- “如何产生死锁问题?” / "如何避免死锁问题?’ /“如何解决死锁问题?
- 死锁的产生. 各个线程 or 进程都持有资源, 不肯释放. 剩余的资源又无法满足任何一个线程或者进程.
- "如何避免死锁问题. 每次分配资源时, 进行检测.
- 如何解决死锁问题? 当死锁已经产生, 那么只有让当前持有资源的线程和进程放弃手中的资源. 可以选择1个1个的释放进程, 也可以全部清空.
问题 - 多线程集合工具
多线程集合. 我们常见的集合为List, Set, HashMap. 其对于多线程问题, 通常出现的问题就是写覆盖, 即A线程写入, B线程覆盖的问题. 这样就衍生出了一些列的多线程集合, LinkedBlockingQueue, ConcurrentHashMap等.
-
线程安全的集合工具有哪些?
LinkedBlockingQueue, ConcurrentHashMap. -
为什么说ArrayList是非线程安全的?
A线程取值, B线程取值, A写入, B写入. B取的是错误的值, 并且会覆盖A的值. -
如何处理ArrayList的非线程安全问题? 有什么代替的线程安全结构? 实现原理?、
LinkedBlockingQueue. -
为什么说HashMap是非线程安全的?
写覆盖. -
如何处理HashMap的非线程安全问题? 有什么代替的线程安全结构? 实现原理?
ConcurrentHashMap. Segment, or 锁当前Hash槽. -
实战问题1: 开发过程中, 都使用哪些线程安全的集合工具?
LinkedBlockingQueue和 ConcurrentHashMap 较多.
问题 - 多线程并发工具
多线程并发工具通常有 CountdownLatch, CycleBarrier 和 Sephore 3种. 前2种主要用于设置线程之前的前后关系, Sephore 用于处理线程的并发量, 这个用的不是特别多.
-
现在有A和B, 2个线程? 如何使A线程先执行, B线程后执行?
CountdownLatch CycleBarrier -
现在有A和B, 2个线程? 如何使A线程执行2次, B线程执行1次?
-
现在又ABCDE, 5个线程? 如何使AB先并发执行, CDE随后并发执行?
CountdownLatch CycleBarrier -
现在又ABCDEF, 6个线程? 如何保证同时只有5个线程同时执行?
Sephore
问题 - 实战
- 实战问题1: 开发过程中都会遇到哪些多线程问题? 都是如何解决的?
- 声明ForkJoinPool线程池.
- 监控线程池使用和内存使用. 合理设置线程池大小.
- 合理使用消息队列和拒绝策略. 其实就是微服务中的 限流和降级.
- 实战问题2: 单例模式时候的多线程问题?
public class PersonService {
private static volatile boolean singleton= null;
public PersonService getPersonService(){
if(null == singleton) {
synchronized(PersonService.class){
if(null == singleton) {
singleton = new PersonService ();
}
}
}
return singleton;
}
}
- 双重锁判断.
- 使用synchronized进行排他锁.
- 使用二次判断. 防止第一个线程已经创建了对象, 随后第二个线程也经过第一重判断, 未进行第二重判断进行的重复问题.
- 此处为懒汉模型. 开发中还可以使用饿汉模式, 和 枚举方式 进行单例模式.
经过上述的问题梳理, 个人认为应当对于Java的多线程应当属于一个中级掌握的阶段.