注:本文主要是总结下多线程、并发编程、锁编程相关概念,内容相对简略。
注:如有侵权,请联系删除。
1、什么是多线程
线程是程序执行的最小单位,多个线程并发或并行执行就是多线程。多线程下需要保证执行结果的最终正确性。
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
2、为什么使用多线程
充分发挥多核性能优势
单一线程同步执行IO操作时,存在长时间的阻塞,无法充分使用CPU
3、多线程下可能导致什么问题
根本问题:最终结果与单线程执行结果不一致。
常见衍生问题:
死锁
内存泄漏
线程不安全
...
4、线程模型:用户线程和内核线程之间的关联方式
多对一、一对一、多对多
5、线程生命周期和状态:
初始状态
运行状态
阻塞状态
等待状态
超时等待状态
结束状态
6、线程间通信
管道流
等待/通知机制
共享内存
threadLocal
7、线程上下文切换是什么,什么场景下发生?
线程上下文切换是指在特定场景下,在CPU上执行的线程发生变化,需要记录上个线程的执行状态, 以及恢复新线程之前的执行状态。
线程主动让出cpu
线程CPU时间片用尽,系统调度防止其他线程饿死。
系统中断
线程任务执行完成
8、死锁四个必要条件:
1、互斥条件
2、不可剥夺条件
3、请求与保持条件
4、循环等待
9、JMM
指令重排序:JIT编译优化 + CPU指令重排序
happenBefore原则
线程安全三大特性:原子性、可见性、有序性
10、CAS(Compare-And-Swap, 比较并交换)
在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是Unsafe
。
CAS 算法存在以下问题
-
ABA 问题是 CAS 算法最常见的问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的
AtomicStampedReference
类就是用来解决 ABA 问题的,其中的compareAndSet()
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 -
循环时间长开销大
依照使用场景来选择是否使用CAS,写多读少,用锁,读多写少, 用CAS
-
只能保证一个共享变量的原子操作
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了
AtomicReference
类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用AtomicReference
来执行 CAS 操作。
11、synchronized 关键字
在 Java 6 之后, synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,实现核心原理是对象monitor的获取,对象头中有锁标志位。
12、ReentrantLock
ReentrantLock 比 synchronized 增加了一些高级功能
公平与非公平实现
可中断
可选择通知
13、ThreadLocal
ThreadLocal原理
最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
ThreadLocal 内存泄露问题
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法
14、线程池
池化技术
优点:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java创建线程池参数:核心线程数、最大线程数、空线程存活时间、时间单位、任务队列、丢弃策略、线程工厂。
线程池的拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
拓展:
如果服务器资源以达到可利用的极限,这就意味我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路,有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢?
这里提供的一种任务持久化的思路,这里所谓的任务持久化,包括但不限于:
设计一张任务表将任务存储到 MySQL 数据库中。
Redis 缓存任务。
将任务提交到消息队列中。
这里以方案一为例,简单介绍一下实现逻辑:
实现
RejectedExecutionHandler
接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。继承
BlockingQueue
实现一个混合式阻塞队列,该队列包含 JDK 自带的ArrayBlockingQueue
。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写take()
方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从ArrayBlockingQueue
中去取任务。
常用阻塞队列
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue
DelayedWorkedQueue
线程池中线程异常是复用还是销毁?
execute 销毁
submit 复用
核心线程数参数确定
CPU密集型 (N+1)
IO密集型(2N)
如果有资源的话 ,结合实际情况具体压测下。
动态线程池
https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A
15、CompletableFuture
引用:CompletableFuture从入门、踩坑、迷茫、到精通(全网看这一篇够了)_completablefuture.get()坑-CSDN博客
16、AQS
17、为什么 wait() 方法不定义在 Thread 中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
类似的问题:为什么 sleep()
方法定义在 Thread
中?
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
18、如何理解线程安全和不安全?
线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
19、如何预防死锁? 破坏死锁的产生的必要条件即可:
破坏请求与保持条件:一次性申请所有的资源。
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
这个让我想起了经典的哲学家进餐问题:
如上有三种解法:
1、设置临界区,一个人一次性申请左右两个叉子。
2、设置信号量,最多 4人同时持有叉子。
3、奇数位申请顺序,先左后右;偶数位先右后左。
关于多线程,多考量下业务场景
引用目录
CompletableFuture从入门、踩坑、迷茫、到精通(全网看这一篇够了)_completablefuture.get()坑-CSDN博客