java多线程
目录
一、获取多线程的4种方式(Runnable/Callable/Thread/线程池)
1.1 生产环境中如何配置corePoolSize和maximumPoolSize?
2.1 为何用?(线程复用 / 控制最大并发数 / 管理线程)
2.2 用的好处?(降低资源消耗/提高响应速度/提高线程的可管理性)
1、线程不安全的ArrayList(Vector, Collections.synchronized(), JUC方法)
本文通过学习:周阳老师-尚硅谷Java大厂面试题第二季 总结的多线程相关的笔记
一、获取多线程的4种方式(Runnable/Callable/Thread/线程池)
- 实现Runnable接口
- 实现Callable接口
- 实例化Thread类
- 使用线程池获取
Runnable | class MyThread implements Runnable { @Override public void run() { System.out.println("Runnable"); } } | |
Callable | class MyThread2 implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("Callable"); return 1234; } } | |
Thread | for(int i=0; i<5; i++) { new Thread(()->{ System.out.println("Thread"); }, String.valueOf(i)).start(); } | |
默认线程池 | //step1.创建 ExecutorService threadPool = Executors.newFixedThreadPool(5);//5个 ExecutorService threadPool = Executors.newSingleThreadExecutor();//1个 ExecutorService threadPool = Executors.newCacheThreadPool();//可扩展 //step2.执行 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()); }); //step3.销毁 threadPool.shutdown(); | |
自定义线程池 | ExecutorService executorService = new ThreadPoolExecutor( corePoolSize,//核心线程数 maximumPoolSize,//最大线程数 keepAliveTime,//多余的空闲线程存活时间 TimeUnit.SECONDS,//keepAliveTime的单位 new LinkedBlockingQueue<>(3),//任务队列,SynchronousBlockingQueue Executors.defaultThreadFactory(),//线程工厂,生成工作线程 new ThreadPoolExecutor.AbortPolicy());//拒绝策略 |
二、相关细节
1、自定义线程池的相关参数
1.1 生产环境中如何配置corePoolSize和maximumPoolSize?
CPU密集型 (需要大量的运算,而没有阻塞,CPU一直全速运行) | CPU核数 + 1个线程数 |
IO密集型 (需要大量的IO操作,即大量的阻塞) | CPU核数 / (1 - 阻塞系数) = CPU核数*5 或 CPU核数*10 阻塞系数在0.8 ~ 0.9左右 |
1.2 四种拒绝策略
当队列满了+工作线程大于最大线程数,执行拒绝策略,有4种:
AbortPolicy | 默认,直接抛出RejectedExcutionException异常,阻止系统正常运行 |
DiscardPolicy | 直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案 |
CallerRunsPolicy | 该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者 |
DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 |
2、为何使用线程池?使用线程池的好处?为何不用默认线程池?
2.1 为何用?(线程复用 / 控制最大并发数 / 管理线程)
线程池的3大特点:线程复用、控制最大并发数、管理线程。
线程池主要就是控制运行的线程的数量,处理过程中,将线程任务放入到阻塞队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
2.2 用的好处?(降低资源消耗/提高响应速度/提高线程的可管理性)
降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行。
提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2.3 线程池的底层原理?
-
在创建了线程池后,等待提交过来的任务请求
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断
-
如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
-
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
-
如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
-
如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
-
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
-
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
-
所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
-
以顾客去银行办理业务为例
-
最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理
-
后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
-
后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
-
假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
-
临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数
三、线程不安全
1、线程不安全的ArrayList(Vector, Collections.synchronized(), JUC方法)
1.1 线程不安全的原因
因为在进行写操作的时候,方法上为了保证并发性,是没有添加synchronized修饰,所以并发写的时候,就会出现问题。源码如下:
1.2 解决办法
(1)Vector
Vector v = new Vector(3, 2);//初始3个,每次加2个
v.capacity());//当前容量
v.addElement(new Integer(1));//插入元素
(2)Collections.synchronized()
List<String> list = Collections.synchronizedList(new ArrayList<>());
(3)采用JUC里面的方法(COW)
CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想。
写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的度,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。就是写的时候,把ArrayList扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList的引用指向扩容后的。
public boolean add(E e) {
//step1.加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//step2.在末尾扩容一个单位
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
//step3.把扩容后的空间,填写上需要add的内容
newElements[len] = e;
//step4.把内容set到Array中
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2、线程不安全的HashSet
2.1 线程不安全的原因
hashset在执行add方法的时候,存储一个值进入map中,只是作为key进行存储,而value存储的是一个Object类型的常量,也就是说HashSet只关心key,而不关心value
2.2 解决办法
Set<String> mySet = new CopyOnWriteArraySet<String>();
mySet.add("hiha");
3、线程不安全的HashMap
3.1 线程不安全的原因
3.2 解决办法
(1)使用Collections.synchronizedMap(new HashMap<>());
Map<Long, String> list = Collections.synchronizedMap(new HashMap<Long, String>());
(2)使用 ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
四、核心线程数、最大线程数、阻塞队列长度的计算
线程执行优先级: 核心线程 > 阻塞队列 > 扩容线程 > 拒绝策略(注意 先阻塞队列,后扩容线程)
线程池的底层实现,基本都是ThreadPoolExecutor,可用jvisualvm工具来预估这两个时间
1、核心线程数
【常规-核心线程数的计算】
CPU密集型: N+1
I/O密集型: 2N+1 或 N/(1-阻尼系数)
其中,N+1的1是为了应对线程执行过程发生缺页中断,或其它异常导致线程阻塞请求。
【max-核心线程数的计算】
视频教程 https://www.bilibili.com/video/BV1PL411w7oQ
【每天】8个外卖员,8辆车,早8晚8送外卖。但是外卖员得吃饭和休息,中午和晚上分别吃饭1h,休息1小时。如何在歇人不歇车,需要请多少外卖员?
答案:(总时间*车数量)/工作时间=(12*8)/8=12人
外卖员对应多线程,车对应cpu, 工作时间=8小时,休息时间=因此极限线程数量= (线程平均工作时间+线程平均等待时间)*cpu数/线程平均工作时间
但是不能一个接口任务把cpu全耗尽,cpu还需要处理监控和日志程序。需要乘以cpu利用率(70%~80%)。因此,
最大核心线程数 = (线程平均工作时间+线程平均等待时间)*cpu数/线程平均工作时间 * cpu利用率
其中,工作时间 = cpu计算的耗时, 等待时间 = IO请求时间(mysql,文件,调三方接口)
(1)如果接口压力比较均衡,就用以上的计算公式
(2)如果接口压力波动较大,则在(1)基础上下调整
【实际-核心线程数的计算】
public void test() {//cpu是4核
createActivity();//IO,30ms
calculateLogic();//cpu,10ms
}
【1次请求时间(40ms)】: 可以开4个线程(自己执行10ms, 另外30ms的空闲时间可以再开3个线程执行),4核=4*4=16个线程,*(70%~80%)=12个
【1s时间】: 可以处理(1000ms/40ms) * 12 = 300个请求。
如果每秒200个请求,那么核心线程 = 8
1s 12个线程=300个请求
1s ?个线程=200个请求
2、最大线程数(我自己总结的)
若比较平稳,则在核心线程数基础上,稍加一些
若波动较大,则=max核心线程数
3、阻塞队列
单位时间内,队列长度=等待时间/处理时间*cpu数
【1次请求时间(40ms)】: 30ms/20ms*4=6
【1s时间】: (1000ms/40ms) * 6 = 150