1.大屏异常|JUC调优
有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。
接口 A 通过 HttpClient 访问服务 2,响应 100ms 后返回;接口 B 访问服务 3,耗时 2 秒。HttpClient 本身是有一个最大连接数限制的,如果服务 3 迟迟不返回,就会造成 HttpClient 的连接数达到上限,概括来讲,就是同一服务,由于一个耗时非常长的接口,进而引起了整体的服务不可用
这个时候,通过 jstack 打印栈信息,会发现大多数竟然阻塞在了接口 A 上,而不是耗时更长的接口 B,这个现象起初十分具有迷惑性,不过经过分析后,我们猜想其实是因为接口 A 的速度比较快,在问题发生点进入了更多的请求,它们全部都阻塞住的同时被打印出来了。
为了验证这个问题,我搭建了一个demo 工程,模拟了两个使用同一个 HttpClient 的接口。fast 接口用来访问百度,很快就能返回;slow 接口访问谷歌,由于众所周知的原因,会阻塞直到超时,大约 10 s。 利用ab对两个接口进行压测,同时使用 jstack 工具 dump 堆栈。首先使用 jps 命令找到进程号,然后把结果重定向到文件(可以参考 10271.jstack 文件)。
过滤一下 nio 关键字,可以查看 tomcat 相关的线程,足足有 200 个,这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是,有大多数线程,都处于 BLOCKED 状态,说明线程等待资源超时。通过grep fast | wc -l 分析,确实200个中有150个都是blocked的fast的进程。
问题找到了,解决方式就顺利成章了。
①fast和slow争抢连接资源,通过线程池限流或者熔断处理
②有时候slow的线程也不是一直slow,所以就得加入监控
③使用带countdownLaunch对线程的执行顺序逻辑进行控制
2.接口延迟|SWAP调优
解决方式:关闭 SWAP 分区。
swap 是很多性能场景的万恶之源,建议禁用。在高并发 SWAP 绝对能让你体验到它魔鬼性的一面:进程倒是死不了了,但 GC 时间长的却让人无法忍受。
3.内存溢出|Cache调优
内存溢出是一个结果,而内存泄漏是一个原因。内存溢出的原因有内存空间不足、配置错误等因素。一些错误的编程方式,不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系,这就是内存泄漏。
for example:
①有团队使用了 HashMap 做缓存,但是并没有设置超时时间或者 LRU 策略,造成了放入 Map 对象的数据越来越多,而产生了内存泄漏。
②代码如下,由于没有重写 Key 类的 hashCode 和 equals 方法,造成了放入 HashMap 的所有对象都无法被取出来,它们和外界失联了。所以下面的代码结果是 null。
//leak example
import java.util.HashMap;
import java.util.Map;
public class HashMapLeakDemo {
public static class Key {
String title;
public Key(String title) {
this.title = title;
}
}
public static void main(String[] args) {
Map<Key, Integer> map = new HashMap<>();
map.put(new Key("1"), 1);
map.put(new Key("2"), 2);
map.put(new Key("3"), 2);
Integer integer = map.get(new Key("2"));
System.out.println(integer);
}
}
即使提供了 equals 方法和 hashCode 方法,也要非常小心,尽量避免使用自定义的对象作为 Key。
③关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,close 方法又没有放在 finally 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。
4.线程状态
线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈
线程状态:创建、就绪、运行、阻塞、死亡
5.线程状态切换
start ★作用:启动线程,由虚拟机自动调度执行run()方法
★区别:线程处于就绪状态
run ★作用:线程逻辑代码块处理,JVM调度执行
★区别:线程处于运行状态
sleep ★作用:让当前正在执行的线程休眠(暂停执行)
★区别:不释放锁
wait ★作用:使得当前线程等待
★区别:释放同步锁
notify ★作用:唤醒在此对象监视器上等待的单个线程
★区别:唤醒单个线程
notifyAll ★作用:唤醒在此对象监视器上等待的所有线程
★区别:唤醒多个线程
yiled ★作用:停止当前线程,让同等优先权的线程进行
★区别:用Thread类调用
join ★左右:使当前线程停下来等待,直至另一个调用join方法的线程终止
★区别:用线程对象调用
6.阻塞过程唤醒
阻塞
导致线程阻塞的原因有①等待同步资源(等待锁资源,等待条件变量)②I/O操作(文件读取/写入,网络请求)③线程休眠(主动休眠)④等待外部事件(等待用户输入,等待其他线程完成特定任务)
唤醒
线程将会从等待队列中移除,重新成为可调度线程。它会与其他线程以常规的方式竞争对象同步请求。一旦它重新获得对象的同步请求,所有之前的请求状态都会恢复,也就是线程调用wait的地方的状态。线程将会在之前调用wait的地方继续运行下去。
为什么要出现在同步代码块中:
由于wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait() 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。
7.wait与sleep的区别
①wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
②wait 方法会主动释放 monitor 锁,在同步代码中执行 sleep 方法时,并不会释放 monitor 锁。
③wait 方法意味着永久等待,直到被中断或被唤醒才能恢复,不会主动恢复,sleep 方法中会定义一个时间,时间到期后会主动恢复。
④wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
8.创建线程方式
★实现 Runnable 接口(优先使用)
public class RunnableThread implements Runnable {
@Override
public void run() {System.out.println('用实现Runnable接口实现线程');}
}
★实现Callable接口(有返回值可抛出异常)
class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception { return new Random().nextInt();}
}
★继承Thread类(java不支持多继承)
public class ExtendsThread extends Thread {
@Override
public void run() {System.out.println('用Thread类实现线程');}
}
★使用线程池(底层都是实现run方法)
static class DefaultThreadFactory implements ThreadFactory {
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "pool-" + poolNumber.getAndIncrement() +"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);
if (t.isDaemon()) t.setDaemon(false); //是否守护线程
if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); //线程优先级
return t;
}
}
9.线程池构造函数
/**
* 线程池构造函数7大参数
*/
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
★线程池优点
通过复用已创建的线程,降低资源损耗、线程可以直接处理队列中的任务加快响应速度、同时便于统一监控和管理。
★参数介绍
corePoolSize ✦作用:核心线程池大小
maximumPoolSize ✦作用:最大线程池大小
keepAliveTime ✦作用:线程池中超过 corePoolSize 数目的空闲线程最大存活时间;
TimeUnit ✦作用:KeepAliveTime时间单位
workQueue ✦作用:阻塞任务队列
threadFactory ✦作用:新建线程工厂
RejectedExecutionHandler ✦作用:拒绝策略。当提交任务数超过 maxmumPoolSize+workQueue 之和时,任务会交给RejectedExecutionHandler 来处理
10.线程处理任务过程
✧当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
✧当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。
✧当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。
✧当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。
✧当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。
11.线程拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
☄AbortPolicy:直接抛出异常,阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略。
☄CallerRunsPolicy :只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
☄不会造成任务丢失,同时减缓提交任务的速度,给执行任务缓冲时间。
☄DiscardOldestPolicy :丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
☄DiscardPolicy :该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
12.Execuors类实现线程池
✡newSingleThreadExecutor():只有一个线程的线程池,任务是顺序执行,适用于一个一个任务执行的场景
✡newCachedThreadPool():线程池里有很多线程需要同时执行,60s内复用,适用执行很多短期异步的小程序或者负载较轻的服务
✡newFixedThreadPool():拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务。
✡newScheduledThreadPool():用来调度即将执行的任务的线程池
✡newWorkStealingPool():底层采用forkjoin的Deque,采用独立的任务队列可以减少竞争同时加快任务处理
因为以上方式都存在弊端:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为 Integer.MAX_VALUE,会导致OOM。 CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,会导致OOM。
手动创建的线程池底层使用的是ArrayBlockingQueue可以防止OOM。
13.线程池大小设置
★CPU 密集型(n+1)
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。
CPU 密集型任务尽可能的少的线程数量,一般为 CPU 核数 + 1 个线程的线程池。
★IO 密集型(2*n)
由于 IO 密集型任务线程并不是一直在执行任务,可以多分配一点线程数,如 CPU * 2
也可以使用公式:CPU 核心数 *(1+平均等待时间/平均工作时间)。
好好生活✦
830

被折叠的 条评论
为什么被折叠?



