并发编程
Posted 东瓜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程相关的知识,希望对你有一定的参考价值。
并发编程
实践场景
怎么防重复提交
- 定义业务唯一ID
- 操作前使用唯一ID做key设置分布式锁
- 先查后插,做业务幂等控制
- 设置数据库唯一键
- 使用token机制,设置token一次有效
业务幂等怎么做
- 定义业务唯一ID
- 操作前使用唯一ID做key设置分布式锁
- 定义数据库唯一键
- 用唯一键先查,有则直接返回,无则处理。如商户下订单、发优惠券,使用商户订单id先查这边的订单
- 定义状态流转。如订单状态为已支付,则不能再发起支付
并发扣库存&秒杀场景
- 客户端限制按钮点击频率
- 服务端通过ng限制用户请求频率
缓存
- 热点数据缓存
- 缓存预热
异步
- 库存提前加载到redis,抢购成功后立即返回,通过消息队列异步处理后续步骤,扣减数据库库存、发送消息等
- NG用令牌桶算法对接口限流
- 服务端可以根据库存情况或数据库的抗压能力,对请求进行令牌桶限制,或使用队列进行削峰限速
- 一些非主要业务服务可以考虑先降级
- 动静分离,CDN加速
服务器响应慢的原因
外部资源
- 请求外部资源阻塞或响应变慢,数据库、redis、外部接口等
- 做活动等引起的流量突发,导致请求排队
自身
- 死锁、死循环导致应用cpu飙高
- 频繁fullgc导致应用响应慢
- 网络抖动
接口、服务器响应慢怎么定位
- pinpoint等链路追踪工具查看接口各阶段执行耗时可以很容易定位到接口
- 看错误日志
- 看线程情况
- 查看内存占用情况
- 查看应用gc日志,检查fullgc频率是否正常,gc异常可使用jmap打印堆转储dump文件,看哪些对象占用内存多
- 使用top-c命令查看进程cpu占用情况,cpu占用高考虑为死锁或死循环导致,定位到占用cpu的线程号
- 使用jstack命令打出线程堆栈,查看对应线程运行情况(线程状态、线程等待的锁等)
- 开一台实例的debug日志,使用awk命令筛选出耗时长的请求traceId,分析请求日志定位耗时操作
- 检查调用关联方接口的耗时情况
- 检查数据库、redis等运行情况,redisCPU飙高也会导致响应慢
- 检查网络情况(sar命令等,运维操作)
多线程
基础知识
线程与进程的区别
- 进程是操作系统资源分配的基本单位,一个进程就是一个程序,有自己独立的内存空间
- 线程是处理器任务调度和执行的基本单位
- 一个进程可以包含多个线程
并行与并发的区别
- 并发是一个CPU按分配的时间片轮流处理多个任务,从逻辑上看任务是同时执行的
- 并行是多个CPU同时处理多个任务,是真正意义上的同时进行
使用多线程的好处与弊端
好处:
- 充分利用CPU的计算能力
- 一条线程I/O或阻塞时,CPU可切换执行另外一条线程的计算工作
- 能提高程序的执行效率,提高程序的运行速度
弊端:
- 需要占用更多资源,每条线程都需要在栈内分配空间
- 需要考虑线程安全问题,死锁等情况
- 大量线程上下文切换,带来的性能损耗
多线程使用场景
- 异步处理任务。当接口需要处理一个耗时操作并且不需要立刻知道处理结果时,使用多线程异步处理减少接口响应时间。
- 并行处理。对一些耗时长的接口,通过多线程并行处理给主线程返回future的方式,加快接口处理速度。
- 处理后台任务。后台定时任务线程处理一些数据修改操作等。
java线程状态
- NEW,新建。刚创建出线程实例,一旦调用thread.start(),线程状态将会变成runnable。
- RUNNABLE,可运行状态。线程正在运行,或等待分配CPU,或等待IO事件完成。
- BLOCKED,阻塞。线程进入synchronized修饰的代码块前未获取到锁。
- WAITING,等待。在同步代码块中调用wait方法,LockSupport.park线程,Thread.join等待线程同步
- TIMED_WAITING,定时等待。同上,在等待方法中加入时间参数
- TERMINATED。终止状态,线程执行完成,或执行异常且没有进行捕获处理
操作系统线程状态(新建、就绪、运行、等待、结束)
- 初始状态。线程刚被创建,还不能分配CPU。
- 可运行状态。线程等待系统分配CPU,从而执行任务。
- 运行状态。操作系统将CPU分配给线程,线程执行任务。
- 休眠状态。线程调用阻塞API,如阻塞方式读取文件,休眠状态的线程将让出CPU。
- 终止状态。线程执行完,或执行过程中发生异常。
java线程状态与操作系统状态的异同
- java的RUNNABLE状态包含操作系统的可运行、运行、休眠状态
- java线程BLOCKED、WAITING,TIMED_WAITING对应操作系统休眠状态
线程间通信
- 在同步代码块中使用notify,notifyAll方法通信
- 通过LockSupport.park与unPark通信
- 通过共享变量通信
- 通过interrupt通信
线程上下文切换内容
- 线程上下文是指某一时间点CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停地切换线程执行。
- 具体包括CPU寄存器和程序计数器的内容、用户态与内核态之间的切换
减少线程上下文切换的方法
- 无锁并发编程:就是多线程竞争锁时,会引起上下文切换,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法:Java的Atomic包使用CAS算法来更新数据,就是它在没有锁的状态下,可以保证多个线程对一个值的更新。
- 使用最少线程:避免创建不需要的线程。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
创建线程
创建线程的四种方式
- 继承 Thread 类;
- 实现 Runnable 接口;
实现 Callable 接口;
- 可用FutureTask对象接收线程执行的结果
- 可以捕获线程执行的异常
- 使用 Executors 工具类创建线程池
run()和 start()的区别
- start()方法用于启动线程,run()方法用于执行线程的运行时代码,只是方法体中的一个普通函数
- start()方法会启动一个线程,并执行相应准备工作,然后调用run()方法,run()方法中执行真正的多线程业务逻辑
线程安全
并发编程三要素
线程的安全性问题体现在:
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因:
线程切换带来的原子性问题
缓存导致的可见性问题
编译优化带来的有序性问题
解决办法:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
关键字
volatile
volatile关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
synchronized关键字
描述
synchronized关键字是用来控制线程同步的,它的作用是在多线程环境下,保证被synchronized关键字修饰的代码在同一时间只能被一个线程执行
实现原理
synchronized的实现依赖虚拟机,通过获取和释放锁对象的管程(Monitor)对象实现
Monitor对象是一种同步工具,包含一个对象和多个队列,控制对象的获取和挂起线程
synchronized优化
锁消除、锁粗化
默认打开适用性自旋锁
锁升级
synchronized的执行过程(锁升级过程)
1.检测锁对象头的MarkWord里面是不是当前线程ID,如果是,表示当前线程处于偏向锁
2.如果不是,则使用CAS将MarkWord里面的线程ID替换为当前线程,成功则表示获取到偏向锁
3.失败则说明发生竞争,撤销偏向锁,升级为轻量级锁
4.当前线程使用CAS将对象头的MarkWord替换为锁记录指针,如果成功,当前线程获得锁
5.如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
6.如果自旋成功则依然处于轻量级状态
7.如果自旋失败,则升级为重量级锁
synchronized的锁升级性能优化在哪:
偏向锁:单线程无竞争情况访问同步块时,只需让锁对象的对象头记录当前线程ID,不需要触发同步的原生语意-管程机制,不需要获取Monitor对象
轻量级锁:通过自旋来减少挂起线程的操作,如果同步代码块的执行时间较短,线程自旋后可能获取锁
线程池
线程池的核心参数
线程池的扩张和回收策略
线程池线程数怎么设置
- 线程数的设置根据具体业务情况而定,我们会评估业务的一般并发情况,来设置线程池的coreSize参数,同时会将线程池的maxSize参数设置为coreSize的5到10倍
- 核心线程数设置看执行任务的类型,CPU密集型任务一般设置CPU核数加1,IO密集型任务线程数会适当增加,根据一个CPU计算耗时与IO耗时的比例公式设置(计算时间+IO时间)/计算时间*CPU核数
- 后续根据业务的实际运行情况进行调整
java锁
锁的分类
*悲观锁
*乐观锁*共享锁
*排他锁*可重入锁
*非可重入锁*公平锁
*非公平锁乐观锁的ABA问题
- 通过递增版本号解决,cas时比较原值与版本号
死锁
- 产生的原因
线程获取两个以上的互斥锁,线程A获取锁A等待锁B,线程B获取锁B等待锁A 解决办法
- 以确定的顺序获得锁。针对两个特定的锁,可以按照锁对象的hashCode值大小的顺序,分别获得两个锁
- 超时放弃。在获取锁超时以后,主动释放之前已经获得的所有的锁
- 产生的原因
AQS
AQS(AbstractQueuedSynchronizer)是RetrentLock与Java并发包工具的实现基础,其底层采用乐观锁,大量使用了CAS操作,并且在冲突时,采用自旋方式重试,以实现轻量级锁和高效的获取锁
CAS
CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用
其实现主要由状态、队列、CAS三部分组成:
状态:在AQS中,状态由state属性来表示,该属性的值表示了锁的状态,state为0表示锁没有被占用,state大于0表示已被占用(状态可以大于1,以实现可重入)
队列:在AQS中,队列的实现是一个双向链表,它表示所有等待锁的线程的集合,当线程获取锁失败时通过CAS操作将自己加入队列的末尾
CAS操作:操作系统层面提供的API,CAS是一条CPU原语,其包含三个操作数——内存位置、预期原值和新值,如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值
执行流程
- 线程尝试获取锁,将state的状态通过CAS操作由0改写成1
- 设置成功,将当前获取到锁的线程设置为自己
- 未获取到锁,则将自己封装成node,通过CAS操作加入队列尾部
- 如果前置节点是头节点,则会再次尝试获取锁
- 如果自旋获取锁失败,将前驱节点状态设置为signal后,通过LockSupport.park,挂起当前线程
- 拿到锁的线程执行完逻辑后,通过LockSupport.unPark唤起后置节点线程,后置节点线程则会再次尝试去获取锁
AQS超时机制
通过LockSupport的parkNanos实现
并发容器
ConcurentHashMap实现
jdk1.7
- 使用一个Segment数组和多个HashEntry组成,每个数组桶位下面挂一个HashEntry
- 当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,再通过ReentrantLock进行加锁后,将数据添加HashEntry中的对应位置
jdk1.8
- 与HashMap结构一致,底层是数组+链表或数组加红黑树结构实现
- 作流程是,对key进行hash运算定位到数组下标,如果下标位置链表为空则先初始化,再cas插入,如果有数据,则用同步锁Synchronized对数组桶位进行加锁后插入
这个改动说明了jdk团队认为经过优化后的Synchronized比ReentrantLock效率高
ThreadLocal
ThreadLocal是一个线程隔离变量
ThreadLocal.set方法相当于给当前线程的TheadLocalMap属性赋值,key就是TheadLocal自己,value是要set的值
使用场景:
- 多线程使用一些线程不安全的工具类时,ThreadLocal复用工具对象
- 用来传递上下文,可以避免对象的跨层传递。(定义一个工具类,类中定义一个TheadLocal静态属性,通过工具类调用TheadLocal的set和get方法)
- 具体场景:之前oracle迁移mysql数据库时,切换开关使用ThreadLocal保存至当前线程,避免开关值的上下文传递
内存泄露场景:
由于TheadLocalMap与线程生命周期一致,在使用线程池等线程生命周期长的场景使用TheadLocal时,如不手动执行remove操作,value便不会被回收,造成内存泄露
HashMap
- 当链表节点数大于8时会转换为红黑树,小于6转回链表
- 对key进行hash运算时进行了两次hash运算,并有扰流函数,减少hash冲突,可以均匀分布
- 位与操作与取模
由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
正整数对2的倍数取模,只要将数与2的倍数-1做按位与运算
对2的倍数取余,只要将数右移2的倍数位
以上是关于并发编程的主要内容,如果未能解决你的问题,请参考以下文章