导图社区 Java并发编程技术总结
关于Java并发编程技术总结的思维导图,包含多线程基础知识:1.并发编程的优缺点、2.线程的状态与基本操作;并发理论(JMM):1.JMM内存模型、2.指令重排序、3.happens-before先发生原则等。
编辑于2021-12-02 16:55:56Java并发编程
多线程基础知识
1.并发编程的优缺点
1.进程和线程的区别
进程是程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位
线程是进程内部的调度单元,多个线程会共享进程的资源空间,如IO、CPU以及内存资源等
2.为什么要用到多线程(优点)
充分利用多核CPU的运算能力提升系统整体运行效率
方便业务上进行拆分
3.多线程并发需要考虑的问题(缺点)
频繁的上下文切换
多线程竞争可能会导致的死锁或者脏数据等问题 即并发安全问题
4.易混淆的概念
同步VS异步
并发VS并行
阻塞VS非阻塞
并发VS串行
2.线程的状态与基本操作
1.如何新建线程
继承Thread类 实现run方法
实现runnable接口 实现run方法
实现callable接口 实现call方法
2.如何停止线程
使用退出标识 使线程正常退出 run方法执行完毕线程正常终止
暴力解决:使用stop方法 但是会造成不可预料的结果 不建议使用
使用interrupt方法 异常法 中断线程
3.如何暂停线程
暂停线程:suspend方法暂时挂起
恢复线程:resume方法恢复线程
缺点:独占以及不同步问题
4.线程状态转换
NEW 新建
RUNNABLE 运行
WAITING 等待
TIMED_WAITING 等待超时
TERMINATED 终止
BLOCKED 阻塞
5.线程基本操作
currentThread()方法 返回代码段正在被哪个线程所调用的信息
isAlive()方法判断当前的线程是否处于存活状态
sleep()方法让当前正在执行的线程休眠
getId()方法返回当前线程的唯一标识
yield()方法 放弃当前cpu资源 让给其他的任务 放弃时间不确定
interrupt
会抛出interruptedException异常清除中断标志位
interrupt() interrupted() isInterrupt()
6.守护线程
setDaemon(true)
只要当前存在用户线程再工作 守护线程就不会退出 只有当非守护线程退出时 才和JVM一同退出工作
并发理论(JMM)
1.JMM内存模型
1.哪些是共享数据
实例域
静态域
数组
2.抽象结构模型
共享变量先放于主存
线程会有自己的工作内存 并将主存数据拷贝到工作内存 今后的读写操作均基于工作内存中的变量副本,并在某一个时刻将该数据刷新到主存
通过这种方式实现线程之间的隐式通信
2.指令重排序
1.重排序的原因
在不改变程序执行结果的前提下 尽可能地提升并行度 编译器和处理器对指令进行重排序
2.重排序的种类
1.编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
2.指令级并行的重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
3.内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的
3.重排序的特点
针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序
针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序
4.as-if-serial原则
遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的
3.happens-before先发生原则
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁;先解锁再加锁
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生
对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始
4.并发中的三大性质
原子性
一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
Java语言天然支持
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
可以使用volatile关键字保证内存可见
有序性
即程序执行的顺序按照代码的先后顺序执行
happens-before原则
使用volatile禁止指令重排序
并发关键字
1.synchronized关键字
1.如何使用
实例方法(锁类的实例对象)
静态方法(锁类对象)
实例对象(锁类的实例对象)
class对象(类对象)
任意实例对象Object(任意对象object)
2.monitor机制
在字节码中添加monitorenter以及monitorexit指令 代表进入和退出
支持锁的重入 同一个锁程不需要再次获取锁
实质:每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一
3.synchronized的happens-before关系
对同一个监视器的解锁,happens-before于对该监视器的加锁
4.synchronized的内存含义
加锁->执行临界区的代码->释放锁
将共享变量刷新到主内存 线程每次都会从主内存读取最新的变量值到工作内存
5.锁优化
1.锁状态
无锁状态
偏向锁
轻量级锁
重量级锁
2.CAS操作
含义和特点
乐观锁机制 假设访问共享资源不会造成冲突 有冲突则重试 直到没冲突为止
操作过程
CAS(V,O,N) 比较和交换 底层依赖于1.5JDK中的CMPXCHG指令
存在的问题
ABA问题
自旋时间过长问题
只能保证一个共享变量的原子操作
3.Java对象头
对象hashcode
对象分代年龄
锁标志位
是否是偏向锁的标志位
6.锁升级策略
轻量级锁
重量级锁
各种锁的比较
2.volatile关键字
1.作用
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值(前提是真的修改过了),这新值对其他线程来说是立即可见的
禁止进行指令重排序
volatile不能保证操作的原子性 需要控制并发访问加锁的方式(Synchronized 或者 ReentrantLock)或者Atomic原子操作类
2.实现原理
写volatile变量在前面加lock锁指令
缓存一致性原理
3.happens-before关系的推导
4.内存语义
写volatile变量会重新刷新到主存 其他线程读volatile变量会从主存中读取最新的值
5.内存语义的实现
在特定的位置插入内存屏障来实现禁止指令的重排序
3.final关键字
1.如何使用
1.变量
基本类型
类变量
只能在申明时赋值或者在静态代码块中赋值
实例变量
申明时赋值 构造器或者非静态代码块中赋值
局部变量
仅有一次赋值的机会
引用类型
对象的地址不可变更 但是对象中的属性值可以发生变化
2.方法
final修饰的方法不能被子类重写 但是可以进行方法的重载
3.类
final修饰的类不能被子类所继承
2.final的重排序规则
1.final域为基本数据类型
禁止对final域的写重排序到构造函数之外
禁止读对象的引用和读该对象包含的final域的重排序
2.final域为引用类型
对一个final修饰的对象的成员域的写入与随后在构造函数之外把这个被构造的对象的引用赋值给一个引用变量 这两个操作不能被重排序
3.final的实现原理
4.final引用不能从构造函数中溢出
Lock锁体系结构
1.Lock锁与Synchronized关键字的比较
锁存在层次
一个同步类vsJvm内存的层面
锁的释放时机
finally释放锁vs正常退出释放锁、异常释放锁
锁的获取方式
支持超时获取锁vs阻塞等待 必须等第一个线程执行完毕
锁的状态是否可判断
可判断vs不可判断
锁是否可重入
都可重入
适合的场景
大量同步vs少量同步
2.AQS设计介绍
1.设计意图
屏蔽了同步状态管理、线程排队的底层实现
模板方法的设计模式 将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法
2.如何使用AQS实现自定义同步组件
重写protected方法 告诉AQS判断当前的同步状态
同步组件调用AQS的模板方法实现同步语义 模板方法又会调用被重写的方法
实现自定义组件时 推荐使用AQS的静态内部类
3.可重写的方法
tryAcquire()方法
4.AQS提供的模板方法
acquire()方法
3.ReentrantLock锁
1.重入锁的实现原理
如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作
释放锁则将同步状态进行减一操作 直到值减到0 代表成功释放锁
2.公平锁
如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO
3.非公平锁
如果一个锁是非公平的,那么锁的获取顺序就随机
4.ReentrantReadWriteLock读写锁
含义:
读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量是非公平的优于公平性的
重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁
锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
应用场景:读多写少的场景 比如缓存的设计
5.Condition机制
1.与Object的wait/notify方法的机制对比
Condition能够支持不响应中断
Lock可以支持多个Condition等待 Object只能等待一个 也就是等待的对象个数限制
await可以支持设置超时响应时间 wait不行
2.与Object的wait/notify方法的对比
在Object中的wait方法 对应lock的Condition的await方法、awaitNanos方法
在Object中的notify方法、notifyAll方法 对应lock的Condition的signal方法、signalAll方法
3.底层的数据结构
复用AQS的NODE类 由不带头结点的链表组成的队列
4.await的实现原理
当当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理
5.signal/signalAll的实现原理
调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock
必知必会的并发容器
ConcurrentHashMap
1.关键属性
table: 装载Node的数组 采用懒加载的方式 大小总是为2的幂次方
nextTable:只在扩容时使用 平时为NULL
sizeCtl:控制数组的大小
unsafe u:提供对桶数组的CAS的操作
2.重要的内部类
Node : 实现Node.entry接口 存放key以及value
TreeNode:继承Node 会被封装成TreeBin
TreeBin:会被进一步封装成TreeNode 在链表过长时 转换为红黑树使用
ForwardingNode:在扩容时出现的特殊的节点
3.构造方法
数组长度总是会保证为2的幂次方
4.put一个元素的流程
1.如果当前的数组没有初始化过 则先进行初始化
2.spread重hash 将高16位与低16位进行异或操作 将hash值与数组长度进行与运算 确定插入的索引i
3.当前hash桶的位置i处为null 直接插入
4.i处的节点不为空hash值大于0 i处为链表头节点 遍历链表 如果Key相同 则覆盖其value的值 如果遍历完毕没找到 则插入链表的尾节点
5.i处不为空并且节点为MOVED 说明正在扩容 帮助其进行扩容
6.i处的节点不为空并且为TreeBin 说明为红黑树 则以红黑树的方式插入节点
7. 插入新节点 检查下链表的长度是否大于8 如果大于8了 则转换为红黑树
8.检测数组的长度 若超过临界值 则进行扩容操作
5.get一个元素的流程
1.当前的hash桶数组节点即table[i]是否为查找的节点,若是则直接返回
2.若不是,则继续再看当前是不是树节点?通过看节点的hash值是否为小于0,如果小于0则为树节点。如果是树节点在红黑树中查找节点
3.如果不是树节点,那就只剩下为链表的形式的一种可能性了,就向后遍历查找节点,若查找到则返回节点的value即可,若没有找到就返回null
6.扩容机制
7.1.8版本的与之前版本的比较
1.减少锁的粒度
2.采用synchronized而不是lock 并大量使用了CAS操作保证并发安全
CopyOnWriteArrayList
1.实现原理
读写分离的思路 写线程在写入数据时 会复制出一个新的容器 在新容器中写入值 当写入完成后 会将旧容器再指向新容器 这样对旧容器的读就是不阻塞的
读读线程互不阻塞 牺牲了数据的实时性但是保证了数据的最终一致性
2.COW和ReentrantReadWriteLock的区别
相同点:
都采用了读写分离的思想 读读线程互不影响
不同点:
当写线程再写数据时 前者不会阻塞数据实时差了 后者的读线程会阻塞
3.应用场景
适用于读多写少的场景 白名单或者黑名单
4.为什么具有弱一致性
COW的实现是基于数组 数组的引用采用的是volatile修饰 只能保证数组的地址是内存可见的 但是数组中的值的修改确实随时可以发生的
5.COW的缺点
写数据时会有容器的复制 导致内存的飙升 会发生major GC以及minor GC
只具有数据的最终一致性 但是不具有数据的实时性
ConcurrentLinkedQueue
1.实现原理
2.数据结构
3.核心方法
4.HOPS的设计意图
BlockingQueue
1.BlockingQueue的基本操作
2.常用的BlockingQueue
ArrayBlockingQueue:数组实现的有界阻塞队列
LinkedBlockingQueue:由链表实现的有界阻塞队列
PriorityBlockingQueue:支持优先级的无界阻塞队列
SynchronousBlockingQueue:不存储任何元素的阻塞队列
LinkedTransferQueue:由链表实现的无界阻塞队列
LinkedBlockingDeque:由链表实现的无界阻塞队列
DelayDeque:存放实现了Delayed接口的无界阻塞队列
ThreadLocal
1.实现思想
空间换时间的思想 每个线程都有变量的副本 变量在多个线程之间进行隔离 保证数据的并发安全
2.set方法原理
数据存放在由当前线程Thread所维护的ThreadLocalMap中 ThreadLocal实例作为key 传入的value值 构成key-value键值对
3.get方法原理
以当前的ThreaLocal为键 从当前的线程所维护的ThreadLocalMap中获取value
4.remove方法原理
移除当前线程所维护的ThreadLocalMap中的键为ThreadLocal实例的键值对
5.ThreadLocalMap原理
1.底层数据结构
键为ThreadLocal实例 值为value的Entry数组
数组大小为2的幂次方
键ThreadLocal为弱引用
2.set方法原理
计算ThreadLocal的hashcode值
计算带插入的索引i
如何解决hash冲突 进行线性探测的原因:冲突的概率不大
加载因子 初始容量为16 加载因子为2/3
扩容resize 大小为原数组大小的2倍
3.getEntry方法原理
根据ThreadLocal的hashcode值进行寻找 如果定位的Entry的key和需要的key一致 那么就返回该Entry 否则继续向后进行探测
4.remove方法原理
先找到对应Entry 将key置空为null 之后再进行清理
6.ThreadLocal内存泄漏问题
1.造成内存泄漏的原因
如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,很显然在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏
2.如何解决内存泄漏
cleanSomeSlots方法
expungeStaleEntry方法
replaceStaleEntry方法
3.使用强引用结果
设threadLocal使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉threadLocal实例的目的,但是因为threadLocalMap的Entry强引用threadLocal,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误
4.使用弱引用的结果
假设Entry弱引用threadLocal,尽管会出现内存泄漏的问题,但是在threadLocal的生命周期里(set,getEntry,remove)里,都会针对key为null的脏entry进行处理。 使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。
7.ThreadLocal最佳实践
使用完ThreadLocal后要记得移除掉
8.应用场景
解决对象不能被多个线程所共享的问题
线程池Executors体系结构
Executor接口
与线程池相关的大部分类都实现了该接口
可以使用Executors来创建线程池对象
ThreadPoolExecutor线程池
1.为何要使用线程池
降低资源消耗 通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗
提升系统响应速度 通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度
提高线程的可管理性 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程
2.线程池的工作原理
核心线程池corePool 阻塞队列BlockQueue 最大线程池MaxPool配合地进行工作
3.创建线程池的构造函数的参数含义
corePoolSize 核心线程池的大小
maximumSize 最大线程池容量
keepAliveTime 空闲线程的存活时间
unit 时间单位
workQueue 工作队列
threadFactory 创建线程的工作类
handler 饱和策略
AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常
CallerRunsPolicy:只用调用者所在的线程来执行任务
DiscardPolicy:不处理直接丢弃掉任务
DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
4.如何关闭线程池
shutdown 正在执行任务的线程将线程执行完毕 空闲线程以中断的方式关闭 将线程池的状态设置为shutdown
shutdownNow 停止所有的线程 包括正在执行任务的线程 返回空闲的任务列表 将线程池的状态设置为STOP
使用isTerninated判断线程池是否关闭
5.如何合理配置线程池的参数
CPU密集型
N+1
IO密集型
2N+1
Timer定时器
负责计划任务的功能,也就是在指定的时间开始执行某一个任务
cancel取消任务执行方法
schedule调度任务方法
ScheduledThreadPoolExecutor任务调度器
1.组成结构
继承了ThreadPoolExecutor,也就是说ScheduledThreadPoolExecutor拥有execute()和submit()提交异步任务的基础功能,实现了ScheduledExecutorService,该接口定义了ScheduledThreadPoolExecutor能够延时执行任务和周期执行任务的功能
DelayedWorkQueue
DelayedWorkQueue实现了BlockingQueue接口,也就是一个阻塞队列
ScheduledFutureTask
ScheduledFutureTask则是继承了FutureTask类,也表示该类用于返回异步任务的结果
2.延迟执行任务涉及的方法
3.周期执行任务涉及到的方法
FutureTask
涉及到的状态
未启动 未执行run方法
已启动 已经执行run方法
退出 执行完毕、发生异常而退出、被取消
get方法
未启动和已启动的状态 阻塞线程的执行等待返回结果
cancel方法
未启动状态:调用后线程将永远不会再执行
已启动状态:根据参数看是否要中断线程的执行
已关闭状态:调用后将返回false
应用场景
当一个线程等到另外一个线程执行完毕后才去继续执行
并发工具
1.倒计时器CountDownLatch
当CountDownLatch维护的计数器的值减为0时,调用await方法的线程 才会继续往下执行 否则会等待
倒计时的根据类 需要等待其他非同类的线程的情况
调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行
2.循环栅栏CyclicBarrier
同一组的线程等待 当同组的线程都达到这个“临界点”后 所有的线程才会携手并进 一起往下走
调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行
3.资源访问控制器Semaphore
Semaphore可以理解为信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待。当执行完业务功能后,需要通过release()方法将许可证归还,以便其他线程能够获得许可证继续执行。
4.线程数据交换器Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间数据能够交换。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。具体交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据
原子操作类
1.实现原理
采用乐观锁去更新数据 底层是在做CAS操作
2.原子更新基本类型
AtomicInteger AtomicLong AtomicBoolean addAndGet(int delta)等方法
3.原子更新数组类型
AtomicIntegerArray AtomicLongArray AtomicReferenceArray addAndGet(int i, int delta)等方法
4.原子更新引用类型
AtomicReference AtomicReferenceFieldUpdater AtomicMarkableReference
5.原子更新字段类型
AtomicIntegeFieldUpdater AtomicLongFieldUpdater AtomicStampedReference
并发实践
生产者和消费者问题解决思路
使用Object的wait、notify方法构造
使用lock的Condition的await、signalAll方法构造
使用BlockingQueue阻塞队列实现