高并发编程
2024-02-21 11:22:44 0 举报
AI智能生成
高并发编程
作者其他创作
大纲/内容
线程池
为什么要使用线程池
线程的创建和销毁的代价很大
有效控制线程数量,避免创建过多线程
内部组成
线程管理器(ThreadPool)
这个就是负责创建、销毁线程池的
工作线程(PoolWorker)
就是线程池中的一个线程
工作任务(Task)
就是线程池中某个线程的业务代码实现
任务队列(TaskQueue)
这个是扔到线程池里的任务需要排队的队列
常见的线程池
SingleThreadExecutor
单线程池队列
里面就一个线程,然后慢慢去消费。
FixedThreadExecutor
固定数量线程池
根据你设定的线程数量执行,多出来的进入队列排队等待
比如说,线程池里面固定就100个线程,超过这个线程数就到队列里面去排队等待
适用于负载比较均衡的情况
CachedThreadExecutor
自动回收线程池
无论多少任务,根据你的需要任意的创建线程,最短的时间满足你
适用于存在高峰的情况 | ps : 容易崩掉 ,4核8GB的100个线程就差不多了,cpu负载可能就 70%/80%了
高峰过去后,大量线程处于空闲状态,等待60s就会被销毁掉了
ScheduleThreadExecutor
定时任务线程池
线程数量无限制,定时调度执行任务
各种组件源码中常用,比如eureka、rocketmq的心跳等等
常用API
Executor
代表线程池的接口,有个execute()方法,扔进去一个Runnable类型对象,就可以分配一个线程给你执行
ExecutorService
这是Executor的子接口,相当于是一个线程池的接口,有销毁线程池等方法
Executors
线程池的辅助工具类,辅助入口类,可以根据Executors快速创建你需要的线程池
ThreadPoolExecutor
这是ExecutorService的实现类,这才是正儿八经代表一个线程池的类
一般在Executors里创建线程池的时候,内部都是直接创建一个ThreadPoolExecutor的实例对象返回的,然后同时给设置了各种默认参数
Executor
源码
分支主题
实现
分支主题
核心参数配置
corePoolSize
线程池里的核心线程数量
maximumPoolSize
线程池里允许的最大线程数量
keepAliveTime
等待时间,corePoolSize外的线程等待时间大于这个值,则会被清理掉
unit
keepAliveTime的单位
workQueue
工作队列,当前运行的线程数 > corePoolSizes时,多出来的线程进入queue中等待
threadFactory
如果有新的线程需要创建时,就是由这个线程池来进行创建的
handle
线程数超过maximumPoolSize并且queue满了的时候,仍有线程进来所执行的策略
默认直接报错
启动原理示意图
内存模型
主内存 data = 0
data = 0
工作内存
(cpu级别 data = 0)
分支主题
线程1
data ++
分支主题
分支主题
data = 0
工作内存
(cpu级别 data = 0)
分支主题
线程2
data ++
说说并发编程可能存在哪些问题吧
原子性
data++,必须是独立执行的,没有人影响我的,一定是我自己执行成功之后,别人才能来进行下一次data++的执行
Java原生支持int=0这种基本类型赋值是原子性的
可见性
有序性
具备有序性,不会发生指令重排导致我们的代码异常;不具备有序性,可能会发生一些指令重排,导致代码可能会出现一些问题
synchronized
浅谈synchronized
monitor
monitorenter
// 代码对应指令
monitorexit
每个类/对象都有一个关联的monitor,如果要对对象加锁,那么必须先这个对象获取关联monitor的lock锁
monitor里面有一个计数器,从0开始
如果一个线程要获取monitor的锁,那么就要先看这个计数器是不是0
如果是0,那么说明没人获取锁,他可以获取锁,然后对计数器 加 1
支持重入锁
PS :
synchronized(myObject){ // 类的class对象来走的
// 一大堆代码
synchronized(myObject){
// 一大堆代码
}
}
// 一大堆代码
synchronized(myObject){
// 一大堆代码
}
}
如果不是0,那么说明有其他线程获取到锁了,那么它就什么事也干不了,只能先等着获取锁
接着如果出了synchronized修饰的代码片段,会执行monitorexit指令
此时获取锁的线程就会对那个对象的monitor里的计数器减 1,如果有多次重入加锁,那就多次减 1 ,直至减为0
此时锁被释放,其他阻塞住的线程可以重新请求获取锁
只有一个线程能成功获取锁
可以保证原子性、有序性、可见性
原子性
加锁和释放锁,ObjectMonitor
可见性
加锁,在进入synchronized代码块时的读操作,都会强制执行reflush
Load内存屏障
释放锁,在出代码块时,代码块内所有的写操作,都会强制执行flush操作
Stroe内存屏障
有序性
通过加各种内存屏障,保证有序性
代码块内部不保证有序性
但是同步代码块内部的指令和外部的指令,是不能重排的
核心原理示意图
分支主题
很简单,JDk 1.6之后,对synchronized内的加锁机制做了大量的优化,这里就是优化为CAS加锁的
你在之前把ReentrantLock底层的源码都读懂了,AQS的机制都读懂了之后,那么synchronized底层的实现差不多的
synchronized的ObjectMonitor的地位就跟ReentrantLock里的AQS是差不多的
对比
synchronized和locks包的锁有什么不同
其实锁的实现原理都是一个目的,让所有线程看到某种标记
synchronized是通过在对象头设置一个标记。上面加一个mark word
是一种JVM原生的锁实现方式
ReentrantLock以及所有基于Lock接口的实现类,都是通过一个被volatile修饰的int型变量
并保证对所有线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架
如何选择
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现的
synchronized在发生异常时,会自动释放线程粘有的锁,因此不会导致死锁现象的发生
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此需要再finally块中释放锁
Lock可以让等待锁的线程响应中断,而synchronized却不行
使用synchronized时,等待的线程会一直等待下去,不能响应中断
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
Lock可以提高多个线程进行读操作的效率
从性能上来说,如果竞争资源不激烈,两者的性能是差不多的
而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
类似zk的羊群效应?Curator对zk锁的优化类似?
总结
类锁所有对象一把锁 对象锁一个对象一把锁,多个对象多把锁
类锁:
SynchronizedTest.init();
public synchronized static void init(){}
synchronized (MyService.class){}
对象锁:
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.init();
public synchronized void init() {}
synchronized (synchronizedTest){}
1.6以后的锁优化
锁消除
锁消除是JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象
是不是只可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令
锁粗化
这个意思就是,JIT编译器如果发现有代码里连续多次加锁释放锁的代码,会给合并为一个锁,就是锁粗化,把一个锁给搞粗了,避免频繁多次加锁释放锁
偏向锁
monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大,因此如果发现大概率只有一个线程会主要竞争一个锁,那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS
但是如果有偏好之外的线程来竞争锁,就要收回之前分配的偏好
J V M会利用C A S操作,在对象头上的M a r kW o r d部分设置线程I D,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
轻量级锁
如果偏向锁没能成功实现,就是因为不同线程竞争锁太频繁了,此时就会尝试采用轻量级锁的方式来加锁,就是将对象头的Mark Word里有一个轻量级锁指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁
如果是自己加的锁,那就执行代码就好了
如果不是自己加的锁,那就是加锁失败,说明有其他人加了锁,这个时候就是升级为重量级锁
自适应性锁
自旋锁
你这个锁里面的代码实际执行的非常快
当其他线程获取锁未成功时,不切换线程,自旋一会,等待你这个锁释放,减少上下文切换带来的性能消耗
CAS
即compareAndSet
多线程同时读取主内存中的数据,必然会导致并发安全问题
优点
CAS即基于底层硬件实现,给你保证一定是原子性的
即同一时间只有一个线程可以成功执行CAS
先比较再设置
其他线程执行CAS会失败
并发包下AtomicInteger等类天然支持CAS保证原子性
缺点
只能保证一个变量的原子操作
长时间自旋,开销大
存在ABA问题
AQS
AbstractQueueSynchronizer
抽象队列同步器
ReentrantLock
线程1
CAS更新 state = 1
分支主题
分支主题
分支主题
state = 0 - > state = 1
线程1
线程2
CAS更新 state = 1
分支主题
分支主题
分支主题
分支主题
等待队列
线程2
ReentrantLock源码分析
new ReentrantLock()
NonfairSync - > 非公平锁
lock()
直接尝试获取锁
未获取锁成功,再执行acquire()方法
tryAcquire()尝试获取锁
直接尝试获取锁
未获取到锁,看下当前拿到锁的线程是不是自己,是自己则state + 1重入
new ReentrantLock(true)
FairSync - > 公平锁
lock()
acquire()
tryAcquire()尝试获取锁
看看队列里是否有人排队
没人排队的话再尝试获取锁
未获取到锁,看下当前拿到锁的线程是不是自己,是自己则state + 1重入
volatile
用来解决可见性和有序性的
在有些罕见的条件下,可以保证原子性 (double / floot)
32位虚拟机中,对于这种64位的操作,可能会有高32位、低32位并发写的问题,volatile是能保证这种数据的原子性的
加了volatile关键字修饰的参数,在读写的时候会强制执行flush和reflush操作
然后通过总线嗅探机制,保证其他线程的可见性
内存屏障
Load
Store
Acquire
Release
等等,这个不用细扣。各个硬件底层的实现都是不一样的,没有统一的说法
happen-before原则
即规定了在某些条件下,不允许编译器、指令器对你写的代码进行指令重排,以此来保证有序性
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作,thread.start(),thread.interrupt()
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
ThreadLocal
为什么有内存泄漏问题?
底层代码ThreadLocalMap - > K-V,其中key是一个内部静态类继承了WeakReference
即弱引用,弱引用在gc的时候会被直接清理掉
导致有null-value这样的数据大量存在,占用内存空间导致内存泄漏
Java团队做了什么优化?
你在通过ThreadLocal , set、get、remove时,他会自动清理掉map里null为key的
确保不会有很多的null值引用了你的value造成内存的泄漏问题
平时使用ThreadLocal需要注意什么?
尽量避免在 ThradLocal长时间放入数据,不使用时最好及时进行remove,自己主动把数据删除
硬件级别MESI协议
重排
指定重排
javac静态编译器编译成.class文件时指令重排
JIT动态编译.class文件未机器码的时候指令重排
处理器执行指令时的无序处理,比如指令1 指令2 指令3执行顺序可能为1,3,2
内存重排序,当指令写入到硬件组件后(写缓存 高速缓存 无效队列)可能发生重排序
处理器的重排序
指令乱序放入到高速缓存中
猜测执行,比如if语句先不执行条件去执行满足条件里面的逻辑最后在执行条件
可能造成可见性问题的组件
寄存器
写缓冲器
高速缓存
处理可见性问题的操作
flush
将无效队列中invalid message刷新到高速缓存让数据无效强制从其他处理器的高速缓存/主内存中读取
reflush
强制将写缓冲区中数据刷新到高速缓存/主内存中
高速缓存底层的数据结构
拉链散列表多个bucket组成
index确定所在bucket
tag定位cache entry
offset当前缓存变量的偏移量
每个bucket挂多个cache entry
每个cache entry包含三部分
tag:当前缓存行指向主存中数据的地址值
cache line:缓存数据
flag:数据状态 s:共享 invalidate:无效 exclusive:独占式 Modify:修改
读写流程、原理
多个处理器高速缓存通过总线相连(存在问题:多个写操作阻塞 需要等待其他处理器ack)
给处理器01向总线发送read请求读取数据
总线从主存中读取数据给处理器01
如果数据被多个处理器共享则flag标识为s状态
当变量被修改时,处理器01会往总线发送一个invalidate message消息,等待其他处理器回复ack invalidate消息
所有处理器都返回ack后获取数据修改的独占锁,修改数据flag=exclusive,修改数据完成后为modify状态
此时其他处理器中数据为invalidate状态,其他处理器从处理器01的高速缓存或者主存中读取数据
原理图示意
分支主题
优化后
优化多个写操作阻塞:
写数据不等待invalidate ack直接写入到写缓冲区中
其他处理器收到invalidate message后直接写入到无效队列中返回ack
处理器01嗅探到invalidate ack消息后从写缓冲区刷新数据到高速缓存中
原理图示意
高速缓存
tag
cache line
flag
tag
cache line
flag
...
tag
cache line
flag
tag
cache line
flag
...
tag
cache line
flag
tag
cache line
flag
0 条评论
下一页