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