多线程异步编排
1.异步编程
1.业务背景
在商品详情中,数据需要不同微服务的提供的不同接口共同返回,如果此时,这些数据接口采用同
步调用的话,也即接口1返回数据之后接口2才执行类似于这种操作,他会有一个问题,一且接口1
查询慢或者有异常了,他之后的接口执行就会大受影响。为了解决和提高接口的执行效率,我们项
目组经过讨论,决定使用异步编编排。
2.异步编排是什么?如何使用?
经过技术讨论和同时的分享之后,我大致了解一下这个异步编排,他是jdk8 juc包下提供的并发工
具类,允许在不阻塞主线程的情况下执行多个任务。
主要目的通过多线程的方法提供接口数据的查询下来,此时的多线程技术考虑到线程复用,没有采
用频繁手动创建的方法,我们用的池化技术实现方案-线程池
他的用法比较固定一般创建异步任务分为两种类型,一种有返回值异步任务,另一种是无返回值的
异步任务,再对这些异步任务进行编排,编排官方提供了很多编排场景,我们项目中使用了三四种
左右好像有thenRun、thenAccept、thenapply
-
thenRun 方法: 只要上面的任务执行完成,就开始执行 thenRun,只要处理完任务后,执行 thenRun 的后续操作
-
thenAccept 方法: 消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
-
thenApply 方法: 当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
3.达到效果是过是什么?最好是量化
通过压测工具,我们也做了测试,没有使用异步编排的时候,接口查询时间再300s左右,使用了
异步编排之后,查询时间稳定了100MS以内
4.谈谈你对线程池理解
1 概念
线程池是一种用于管理线程的机制,核心思想是资源复用,避免频繁地创建和销毁线程所带来的性
能开销。
2 原理
线程池的原理是预先创建一定数量的线程,并将它们放入一个线程池中。当有任务需要执行时,从
线程池中取出一个空闲线程来执行该任务,如果所有线程都在忙,则任务会被放入队列中等待
5.自定义线程池参数以及每个参数设置了多少
JUC的工具类也会提供一些实例化线程池的方法,但是我们项目中并没有使用,因为都会有潜在的OOM风险,所以我们是对这些参数做了定制,具体是这样的
-
corePoolSize(核心线程数):
cpu+1 这个参数要根据具体的服务器配置来定
-
maximumPoolSize (救急线程):
cpu核数*2 这个参数我们考虑到能够对任务处理最大化,设置的都比较大
-
keepAliveTime (救急线程过期时间):
60
-
TimeUnit(时间单位):
一般是s
-
ThreadFactory(线程工厂) :
我们项目采用的默认提供的
-
BlockingWorkQueue (阻塞队列) :
这个参数首光要保证的有界,其次,再根据是否有对执行有效有更高要求的场景
如果有,采用ArrayBlockingQueue反之采用LinkedBlockingQueue
-
RejectedExecutionHandler(拒绝策略):
我们一般用AbortPolicy拒绝并抛出异常其实就是考虑到一旦日志中检测到了拒绝执行异常的时候,及时动态调整其他参数
6.线程池执行逻辑
-
线程池创建,准备好 corePoolSize(核心线程数量)的线程,准备接收任务。
-
新的任务进来,用core准备好的空闲线程执行
-
若核心线程满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行。
-
阻塞队列满了,就直接开新线程执行,最大只能开到maximumPoolSize最大核心线程数指定的数量
-
若最大线程数量满了,就交给拒绝策略去处理。
7.拒绝策略有几种?
-
AbortPolicy (终止策略):,丢弃任务,并抛出异常。(jdk默认策略)
-
DiscardPolicy (丢弃策略):丢弃任务,不抛出异常
-
DiscardOldestPolicy(弃老策略):丢弃队列最前面的任务,然后重新执行任务
-
CallerRunsPolicy (调用方策略): 既不丢弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它
3.创建线程的四种方式
-
继承 Thread 类并重写 run方法创建线程,实现简单但不可以继承其他类;
-
实现 Runnable 接口并重写 run 方法,避免了单继承局限性,编程更加灵活,实现解耦;
-
实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常;
-
使用线程池创建。
1 创建线程池的7种方式
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors
创建的,1 种是通过ThreadPoolExecutor
创建的):
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
单线程池的意义从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?
虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。
4.Thread中run()与start()的区别
-
run
方法:-
run
方法是在当前线程中直接调用的,不会创建新的线程。 -
当调用
run
方法时,任务会在当前线程中按顺序依次执行,而不会启动并发执行。 -
run
方法可以被重载和覆盖,允许子类根据需要定制线程的执行逻辑。
-
-
start
方法:-
start
方法用于启动一个新的线程来执行任务。 -
调用
start
方法后,会创建一个新的线程,并在该线程中并发执行任务。 -
start
方法会自动调用线程类中的run
方法,执行线程的主体逻辑。 -
每个线程只能调用一次
start
方法,否则会抛出RuntimeError
异常。
-
总的来说,`run`方法是在当前线程中执行任务,而`start`方法会创建一个新的线程来执行任务,实现多线程并发执行。在大部分情况下,我们会使用`start`方法来启动线程,以实现并发执行多个任务。但在某些特殊情况下,我们也可以直接调用`run`方法,以在当前线程中顺序执行任务。
5 线程状态(生命周期)
-
New(新建):当线程对象被创建时,它处于新建状态。在这个阶段,线程尚未启动。
-
Runnable(可运行):在新建状态之后,调用线程的
start()
方法将其置为可运行状态。此时,线程已经准备好运行,并且等待系统的调度。 -
Running(运行):线程进入运行状态时,它会执行其
run()
方法中的代码。线程可以由操作系统的调度器选择并分配CPU时间片来执行。 -
Blocked(阻塞):当线程被阻塞时,它暂停执行,并且不会占用CPU资源。线程可能被阻塞的原因包括等待输入/输出完成、等待获取锁或者等待其他条件满足。
-
Waiting(等待):线程在等待某个特定条件满足时进入等待状态。线程可以通过调用
wait()
方法使自己进入等待状态,并且只能通过其他线程的通知或者等待时间结束来唤醒。 -
Timed Waiting(计时等待):与等待状态类似,但是线程在等待一段特定的时间后会自动唤醒。
-
Terminated(终止):当线程的
run()
方法执行完毕或者出现了未捕获的异常时,线程进入终止状态。在终止状态下,线程不会再执行任何代码。
6 wait()与sleep()的区别
-
🌟锁性质不同
wait在等待的时候,会释放锁 sleep在睡眠的时候,是抱着锁睡眠,即不会是释放锁
-
所属类不同
-
wait Object 方法
-
sleep Thread基类方法
-
-
线程进入状态不同
-
调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态
-
调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态
-
-
唤醒时机不同
-
wait 必须要使用notifyAll() 手动唤醒
-
sleep有两种唤醒手段 第一种时间到 第二种Thread.interupt()
-
7 线程分类
-
普通线程 99% 也叫用户线程 也叫 业务线程
-
守护线程 1% 常见的垃圾回收线程
特点: 后台线程 不会单独运行,只要是普通线程执行结束,守护线程也会随之结束,jvm不会单独运行守护线程
8 多线程线程安全问题产生的原因和如何解决
1 原因
-
操作共享资源的代码有多行
-
操作共享资源的线程有多个
2 方法
上锁
9 ThreadLocal
1 简介
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
2 ThreadLocal与Synchronized的区别
ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。