9.23学习

1.大屏异常|JUC调优

有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。

 
 
2c7246bbf6484fc2b7431c4d53e89622.jpg

 

 

接口 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任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈

线程状态:创建、就绪、运行、阻塞、死亡

 
 
c87e47fe6ac841728da5fc1dc355c86f.png

 

 

5.线程状态切换

start ★作用:启动线程,由虚拟机自动调度执行run()方法

★区别:线程处于就绪状态

 

run ★作用:线程逻辑代码块处理,JVM调度执行

★区别:线程处于运行状态

 

sleep ★作用:让当前正在执行的线程休眠(暂停执行)

★区别:不释放锁

 

wait ★作用:使得当前线程等待

★区别:释放同步锁

 

notify ★作用:唤醒在此对象监视器上等待的单个线程

★区别:唤醒单个线程

 

notifyAll ★作用:唤醒在此对象监视器上等待的所有线程

★区别:唤醒多个线程

 

yiled ★作用:停止当前线程,让同等优先权的线程进行

★区别:用Thread类调用

 

join ★左右:使当前线程停下来等待,直至另一个调用join方法的线程终止

★区别:用线程对象调用

 
 
4511e719fc934deda96041a629f01086.png

 

 

 

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.线程处理任务过程

 
 
35d859720f1649bd8916a350240c3991.png

 

 

✧当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

✧当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。

✧当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。

✧当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。

✧当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。

11.线程拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下:

☄AbortPolicy:直接抛出异常,阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略。

☄CallerRunsPolicy :只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。

☄不会造成任务丢失,同时减缓提交任务的速度,给执行任务缓冲时间。

☄DiscardOldestPolicy :丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

☄DiscardPolicy :该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

12.Execuors类实现线程池

 
 
f598f4c84aed44d896362a70230ebbab.png

 

 

✡newSingleThreadExecutor():只有一个线程的线程池,任务是顺序执行,适用于一个一个任务执行的场景

✡newCachedThreadPool():线程池里有很多线程需要同时执行,60s内复用,适用执行很多短期异步的小程序或者负载较轻的服务

✡newFixedThreadPool():拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务。

✡newScheduledThreadPool():用来调度即将执行的任务的线程池

✡newWorkStealingPool():底层采用forkjoin的Deque,采用独立的任务队列可以减少竞争同时加快任务处理

 
 
609ca99cb79f4876bb25055c2d9f511e.png

 

 

因为以上方式都存在弊端:

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+平均等待时间/平均工作时间)。

 

好好生活✦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值