JAVA高并发核心知识
2023-02-10 14:39:58 1 举报
AI智能生成
JAVA高并发知识点大杂烩
作者其他创作
大纲/内容
Runnable (interface)
Thread (implements Runnable)
重写RUN方法
new Thrad(Runnable a)
join() 用主线程阻塞办法确保多线程顺序
Thread源码
静态代码块,执行本地方法,注册系统资源registerNatives
init
调用本地方法,获取当前线程为父线程
获取系统安全管理,获取管理器的threadGroup,如果没有,从父线程获取
检查线程组权限
当前线程集成父线程相关属性
run
直接调用Runnable对象的run方法,因此不会执行任务。因为继承Runable必须实现run方法
synchronized void start()
添加当前启动的线程到线程组
调用本地方法启动线程
变量 ThreadLocal.ThreadLocalMap threadLocals
关于ThreadLocal
set()
获取当前线程 Thread t = Thread.currentThread()
以当前线程为Key,获取ThreadLocalMap对象 ThreadLocalMap map = getMap(t)
设置value, map.set(this, value);或创建ThreadLoaclMap对象
get()
获取当前线程 Thread t = Thread.currentThread()
以当前线程为Key,获取ThreadLocalMap对象 ThreadLocalMap map = getMap(t)
返回Thread 对象属性ThreadLocal.ThreadLocalMap threadLocals
获取的threadLocals变量不为空,返回本地变量对应的值
获取的threadLocals变量为空,返回setInitialValue()的值
setInitialValue()
调用初始化方法T value = initialValue();
protected T initialValue(),空方法,由子类覆盖
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
判断map,设置值或创建map
返回value
remove()
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) m.remove(this);
注意:本地变量不适用一定要删除,否则会内存溢出
内部类
ThreadLoaclMap
内部类Entry extends WeakReference<ThreadLocal<?>>
Object value
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
super(k);
value = v;
}
super(k), 即key为ThreadLocal弱引用对象
变量,Entry[] table
变量 ThreadLocal.ThreadLocalMap inheritableThreadLocals
ThreadLoacl 不能值传递,其子类InheritableThreadLocal 可以
InheritableThreadLocal
重写父类ThreadLocal childValue()
new Thread时候执行init 方法,inheritableThreadLocals属性为空且父线程Thread属性
inheritableThreadLocals不为空,则创建Thread 对象属性ThreadLocal.ThreadLocalMap inheritableThreadLocals。
创建方法里调用了 重写方法childValue, 把父类的变量copy到子线程inheritableThreadLocals
inheritableThreadLocals不为空,则创建Thread 对象属性ThreadLocal.ThreadLocalMap inheritableThreadLocals。
创建方法里调用了 重写方法childValue, 把父类的变量copy到子线程inheritableThreadLocals
重写父类ThreadLocal getMap()
返回Thread 对象属性ThreadLocal.ThreadLocalMap inheritableThreadLocals
重写父类ThreadLocal createMap()
stop 和 interrupt 的区别
stop 直接杀死线程,已不推荐使用
interrupt 通知线程中断。线程抛异常或主动检查
Subtopic
Callable (interface)
TaskCallable
RunnableAdapter
DownloadTask
Future(interface)
RunnableFuture (interface)extends Runnable, Future
FutureTask
new FutureTask(Runnable a)
new FutureTask(Callable a)
子主题
ForkJoinTask implements Future
RecursiveTask
RecursiveAction
SimpleDateFormat线程安全
原因:继承DateFormat,DateFormat有一个全局变量的Calendar变量,Calendar对象不支持多线程
解决:
局部变量:高并发时会创建大量对象,影响性能
synchronized :高并发时影响性能
lock:高并发时影响性能
ThreadLocal: 放在线程副本,适合高并发
线程池 Executor (interface)
ExecutorService (interface)
AbstractExecutorService (abstract)
ThreadPoolExecutor( 重要,线程池本质)
构造方法参数
corePoolSize 核心线程数
线程数小于corePoolSize,直接创建线程,无论线程池的线程是否空闲
想降低系统资源,例如CPU使用率、corePoolSize设置小一点,队列容量大一点。降低线程池任务处理的吞吐量
相反利用系统资源,提升cpu使用率,可调大corePoolSize,缩小队列容量。但如果线程数量过大,会带来线程池调度问题,反而会降低任务执行的吞吐量
maxmumPoolSize 最大线程数
线程数大于corePoolSize,小于maxmumPoolSize,当workeQueue满队时,才能创建线程
当corePoolSize==maxmumPoolSize,workQueue未满,则放入workQueue,等待空闲线程出现后取出执行
workQueue 阻塞队列,非常重要,对线程池有重大影响
wokeQueue已满,没有可执行线程,执行拒绝策略
直接切换用常用队列是SynchronousQueue
无边界阻塞队列 LinkedBlockingQueue
要小于maximumPoolSize才会创建新线程,所以maximumPoolSize约等于不起作用
严重时会OOM
有边界阻塞队列 ArrayBlockingQueue
队列容量有限,可以降低资源损耗,但让线程调度变得困难
keepAliveTime 空闲线程等待时间,超过则销毁,直接线程数了==corePoolSize
unit
keepAliveTime的时间单位
threadFactory
DefaultThreadFactory(静态内部类)
PrivilegedThreadFactory(静态内部类)
rejectHandler
AbortPolicy 直接抛出异常,这也是默认的策略
CallerRunsPolicy 用调用者所在的线程来执行任务
DiscardOldestPolicy 丢弃队列中最靠前的任务并执行当前任务
DiscardPolicy 直接丢弃当前任务。
自定义策略 继承RejectedExecutionHandler
重要属性
ctl
AtomicInteger类,保存线程数量和线程池的状态
mainLock
ReentrantLock类,可重入锁
重要内部类
Worker
extends AbstractQueuedSynchronizer implements Runnable
集成AQS(AbstractQueuedSynchronizer类)
关于AQS
概念:锁和同步器的框架 (抽象队列同步器)
原理:共享占用资源。当前线程使用资源时锁定资源,其他线程被阻塞等待,等待机制时队列
原理图
常用类
ReentrantLock
Semaphore
SynchronousQueue
FutureTask
资源被定义两种方式
独占
公平锁(先到先得),按照线程在队列排队方式,发现空闲资源,使用一次CAS抢锁,抢不到加入队列等待
非公平锁(抢锁),不判断空闲资源,直接CAS抢锁,抢到后再一次CAS锁定资源,两次抢不到则加入队列等待。ReentrantLock得默认方式
优劣:非公平锁吞吐量大,但由于获取锁是靠抢,有可能让线程长时间出于饥饿状态
共享
概念:多个线程可以同时访问资源
Semaphore(信号量)
概念:Semaphore相当于颁发线程获取许可证,许可证是共享得。Semaphore 经常用于限制获取某种资源的线程数量。
公平模式: 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO;
非公平模式: 抢占式的。
CountDownLatch(倒计时器)
概念:允许count个线程阻塞再某处,直至所有线程执行完毕。
原理: 默认构造AQS的stat 为count,每个线程执行完毕后CAS操作减少stat
典型场景1: 启动服时,主线程等待其它组件加载
典型场景2: 并行,多个线程在同一时间起跑
CountDownLatch.countDown() count指减1
CountDownLatch.await()方法,阻塞主线程,直到count为1
问题
使用不当容易死循环,例如count值为结束循环指标
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次
CyclicBarrier(循环栅栏)
概念:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。简单说,拦截一堆线程,囤够了再一起工作
功能与CountDownLatch,但更复杂更强大
典型场景:多线程计算数据,最后合并结果
属性 final Thread thread;
调用外部ThreadPoolExecutor类的runWorker方法执行任务
四种拒绝策略也是内部类
常用方法
execute()
execute(Runnable)
通过ctl.get()获取线程数量,和corePoolSize等参数进行一系列逻辑处理
或通过workQueue增加核心线程任务。addWorker(Runnable, boolean)
1在标记双循环里,首先检查特定条件是否允许加入线程
2通过1,进入内循环,通过cas方式(ctl.compareAndSet)增加线程
CAS操作成功,break retry; 结束双层循环
CAS操作失败,continue retry,从外循环开始,从新CAS操作
3通过2,取得全局独占锁ReentrantLock,把wokers 增加到wokers集合
或向阻塞队列增加等待任务
或执行拒绝策略
submit()
shutdown()
shutdownNow()
监控类方法
getTaskCount()
getCompletedTaskCount()
getPoolSize()
getCorePoolSize()
getActiveCount() 当前线程池中正在执行任务的线程数量
runWorker(Worker w)
1.获取woker和当前线程,获取woker任务,并且释放woker的锁
2.在while循环体中,判断任务不为空或者当前任务队列不为空,执行循环。否则执行processWorkerExit退出woker工作线程
3.循环中,获取woker独占锁,进行一系列逻辑判断是否需要中断当前线程的执行
4. 通过判断,执行beforeExecute(Thread t, Runnable r),可重写覆盖
5. 执行Runnable 的 run 方法
6. 现在执行完毕后,执行afterExecute(Runnable r, Throwable t) 执行线程执行后的逻辑
7. 设置完成任务为空,完成任务数+1,释放woker独占锁
8.执行processWorkerExit退出woker工作线程
ForkJoinPool(WorkStealingPool的本质,使用工作窃取算法)
与ThreadPoolExecutor功能类似,结合ForkJoinTask类一起使用;ForkJoinTask.fork()执行任务,ForkJoinTask.join()返回结果汇总。ForkJoinTask需要实现方法exec(),进行任务拆解。常用RecursiveTask和RecursiveAction ,均属于ForkJoinTask的子类
ScheduleExecutorService
ScheduledThreadPoolExecutor
和Timer 的区别
Timer是单线程模式,ScheduledThreadPoolExecutor是多线程模式
Timer调度是基于操作系统的绝对时间的,ScheduledThreadPoolExecutor调度是基于相对时间的
Timer不会捕获TimerTask抛出的异常,加上Timer又是单线程的。一旦某个调度任务出现异常,则整个线程就会终止,其他需
要调度的任务也不再执行
要调度的任务也不再执行
Timer中执行的TimerTask任务整体上没有优先级的概念,不支持对任务的排序
Timer中执行的TimerTask类只是实现了java.lang.Runnable接口,不能返回结果
method:
submit(Runnable a)
submit(Callable c)
工具类Executors
常用类
Executors.newCachedThreadPool
Executors.newFixedThreadPool
Executors.newScheduleThreadPool
Executors.newSingleThreadPool
Executors.newSingleScheduleThreadPool
Executors.newWorkStealingPool
工作窃取算法(WorkStealing) 类似MapReduce思想
线程池实例状态
Running 运行状态,能接收新提交的任务,并且也能处理阻塞队列中的任务
Shutdown 关闭状态,不能再接收新提交的任务,但是可以处理阻塞队列中已经保存的任务,当线程池处于Running状态时,调用shutdown()方法会使线程池进入该状态
stop 不能接收新任务,也不能处理阻塞队列中已经保存的任务,会中断正在处理任务的线程,如果线程池处于Running或Shutdown状态,调用shutdownNow()方法,会使线程池进入该状态
tidying 如果所有的任务都已经终止,有效线程数为0(阻塞队列为空,线程池中的工作线程数量为0),线程池就会进入该状
态。
态。
terminated 处于Tidying状态的线程池调用terminated()方法,会使用线程池进入该状态
线程数量配置建议
CPU密集型 CPU数量+1
IO密集型 CPU数量*2
并发问题
可见性问题
一个线程修改了共享变量,另一个线程立刻可见,否则有可见性问题
单核CPU因为只有一个CPU缓存,所以不存在可见性问题。
归根结底:可见性的问题是多核CPU的缓存导致的
原子性问题
多线程执行时候,CPU时间片段不断切换。一个或多个操作在CPU执行过程中出现中断
归根结底:原子性的问题是CPU对任务切换机制导致的
多核 32位CPU,多线程同时对Long类型写数据,会出现异常
有序性问题(指令重排)
解决可见性问题和指令重排
Happens-Before
程序次序规则
volatile变量规则
对一个使用了volatile变量的写操作,先行发生于后面对这个变量的读操作
使用内存屏障
传递规则
程序次序规则+volatile变量规则
锁定规则
进入synchronized代码块之前,会自动加锁,在代码块执行完毕后,会自动释放锁。
线程启动规则
线程A启动线程B之后,线程B能够看到线程A在启动线程B之前的操作
线程终结规则
线程A等待线程B完成,则线程A能够访问到线程B对共享变量的操作(Thread.join();)
线程中断规则
在线程A中中断线程B之前,将共享变量x的值修改为100,则当线程B检测到中断事件时,访问到的x变量的
值为100。
值为100。
对象终结原则
一个对象的初始化完成Happens-Before于它的finalize()方法的开始。
JVM禁用缓存和编译优化
synchronized
synchronized 使用了monitorenter 和monitorexit两条JVM指令锁定资源
synchronized 是非公平锁, 可重入锁
隐藏规则
当使用synchronized关键字修饰代码块时,锁定的是实际传入的对象
当使用synchronized关键字修饰非静态方法时,锁定的是当前【实例对象】this
当使用synchronized关键字修饰静态方法时,锁定的是当前类的【Class对象】
常见问题
为什么有synchronized又要提供lock
synchronized有局限性
阻塞
不支持响应中断
不支持超时
Redis实现分布式锁
如何实现加锁和解锁的归一化
加锁和解锁操
作由同一个线程来完成
作由同一个线程来完成
分布式锁的基本要求
支持互斥性
相同key
支持锁超时
设置key时候添加缓存有效期
支持可重入
可重入方案(synchronized,lock支持可重入)
使用计数器来解决可重入问题
支持阻塞和非阻塞特性
非阻塞体验不友好,例如拿不到锁直接返回错误给前端
用自旋锁实现阻塞(死循环访问是否可以拿到锁)
支持高可用
支持锁失效
加超时锁,任务没有执行完成,锁失效了
用异步续命解决
Redisson实现分布式锁
可重入锁(Reentrant Lock)
概念:1 同一个线程下,外层方法上锁之后,内层调用的方法也能正常获取锁。
2 可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码
2 可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码
RLock lock = redisson.getLock("anyLock");
lock.lock(10, TimeUnit.SECONDS);
lock.tryLock(3, 10, TimeUnit.SECONDS);
公平锁(Fair Lock)
RLock fairLock = redisson.getFairLock("anyLock");
联锁(MultiLock)
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
lock.tryLock(100, 10, TimeUnit.SECONDS);
红锁(RedLock)
概念 :红锁是过半节点锁成功了,就算成功。解决连锁单点失效问题
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
可重入读写锁(ReadWriteLock)
概念: 读读共享,读写互斥
RReadWriteLock rwlock = redisson.getLock("anyRWLock");
rwlock.readLock().lock(10, TimeUnit.SECONDS);
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
信号量(Semaphore)
概念 :Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
semaphore.acquire(23);
semaphore.tryAcquire();
可过期性信号量(PermitExpirableSemaphore)
redisson.getPermitExpirableSemaphore("mySemaphore");
闭锁(CountDownLatch)
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
限流方案
限流算法
令牌桶
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌
允许一定程度的突发请求
漏桶
一个固定容量的漏桶,按照常量固定速率流出水滴
平滑流入,固定流出
计数器
单机限流
com.google.common.util.concurrent.RateLimiter rateLimiter = RateLimiter.create(5); rateLimiter.tryAcquire
Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现
分布式限流
redis+lua实现的lua脚本
sentinel框架
秒杀业务
秒杀三阶段
准备阶段
数据预热
如电商业务预防超卖,上传秒杀商品数据到redis
redis+lua 用lua脚本封装redis原子操作
Redis使用同一个Lua解释器来执行所有命令,同时,Redis保证以一种原子性的方式来执行脚本:当lua脚本在执行的时候,不会有其他脚本和命令同时执行,这种语义类似于 MULTI/EXEC。从别的客户端的视角来看,一个lua脚本要么不可见,要么已经执行完。
然而这也意味着,执行一个较慢的lua脚本是不建议的,由于脚本的开销非常低,构造一个快速执行的脚本并非难事。但是你要注意到,当你正在执行一个比较慢的脚本时,所以其他的客户端都无法执行命令。
然而这也意味着,执行一个较慢的lua脚本是不建议的,由于脚本的开销非常低,构造一个快速执行的脚本并非难事。但是你要注意到,当你正在执行一个比较慢的脚本时,所以其他的客户端都无法执行命令。
redisson
redisson底层是使用lua脚本
秒杀阶段
做好系统保护
例如:限流
例如:负载均衡层直接访问lua脚本,例如商品没有库存,直接返回已售完,不经过应用层
结算阶段
处理一致性问题,业务补偿处理
死锁问题
事例代码
线程A持有账户A的锁并等待线程B释放账户B的锁,线程B持有账户B的锁并等待线程A释放账户A的锁,死锁发生了
死锁四个必要条件
互斥条件
一时间内资源只能一个线程等待,其它线程只能等待
不可剥夺条件
线程拥有的资源只能自己主动释放
请求与保持条件
保存自己的资源,同时对别的资源请求。别的资源在占用,自己的资源也不释放
循环等待条件
预防死锁,破坏四个必要条件
破坏互斥条件
无法破坏,使用锁的目的就是为了互斥
破坏不可剥夺条件
破坏不可剥夺的条件的核心就是让当前线程自己主动释放占有的资源,可以使用java.util.concurrent包下的Lock
破坏请求与保持条件
可以一次性申请所需要的所有资源
破坏循环等待条件
可以通过对资源排序,按照一定的顺序来申请资源,然后按照顺序来锁定资源,可以有效的避免死锁
例如转账操作中,每个账户都会有一个唯一的id值,我们在锁定账户资源时,可以按照id值从小到大的顺序来申请账
户资源,并按照id从小到大的顺序来锁定账户,此时,程序就不会再进行循环等待了。
户资源,并按照id从小到大的顺序来锁定账户,此时,程序就不会再进行循环等待了。
缓存
堆内存
读取最快,容量受限,影响GC。一般用弱/软引用做缓存对象,使用存热点数据
框架: Guava Cache、Ehcache 3.x、 MapDB
堆外内存
减少GC,容量相对较多,读取数据时需要序列化/反序列化
框架: Ehcache 3.x、 MapDB
磁盘缓存
可持久化
框架: Ehcache 3.x、 MapDB
分布式缓存
Java进程间分布式缓存
框架: Memcached、Redis
分布式缓存问题
缓存穿透
缓存命中低(没有缓存),查询直接打到数据库
解决方案
缓存空数据
布隆过滤
缓存击穿
缓存时间失效,请求大量打到数据库
解决方案
热点数据缓存永不失效
批量数据设置不同的缓存失效时间
同一数据的数据库库访问设置并发锁
缓存雪崩
缓存数据集中失效;缓存服务器故障;请求大量打到数据库
解决方案
Redis 高可用集群,必要时异地多活
同一数据的数据库库访问设置并发锁
限流
缓存回收策略
基于空间
存储空间达到多少清理,如10M
基于容量
储存容量达到多少条清理,如100条
基于时间
存活期(TTL),空闲期(TTI)
基于对象引用
只有在没有其他强引用对象引用弱引用/软引用对象时,垃圾回收时才回收该引用。即如果有一个对象(不是弱引用/软引用对
象)引用了弱引用/软引用对象,那么垃圾回收时不会回收该弱引用/软引用对象。
象)引用了弱引用/软引用对象,那么垃圾回收时不会回收该弱引用/软引用对象。
回收算法
FIFO
LRU
LFU
优化指标
性能指标
QPS
TPS
HPS
响应时间
并发量
正确性
0 条评论
下一页