一、💛
线程池的基本介绍
内存池,进程池,连接池,常量池,这些池子概念上都是一样的~~
如果我们需要频繁的创建销毁线程,此时创建销毁的成本就不能忽视了,因此就可以使用线程池。
提前创建好一波线程,后续需要使用线程,就直接从池子里面拿一个即可,当线程不再使用,就放回池子里面。(本来是需要创建线程/销毁线程,现在是从池子里面获取到现成的线程,并且把线程归还到池子里面
那么为啥从池子里面拿就比系统里面创建线程更加高效呢?
不用线程池:如果是系统这里创建线程,需要调用系统API,进一步的由操作系统内核完成,完成线程的创建过程(内核是给所有进程提供服务的,这样你想干的事情,就需要等一等,等多长时间我们是未知的,是不可以控制的)
使用线程池:上述的内核中进行的操作都是提前做好了的,现在的取线程过程,纯粹的是用户使用代码完成的(纯用户态)-是可控制的。
二、💙
工厂模式:去生产的功能(字面意思),用于生产对象,一般情况下我们创建对象都是new,通过构造方法,但是构造方法有时候存在巨大的缺陷(构造方法是固定就是类名,有的类需要使用多种不同构造方式->(方法重载仅要求参数的个数和类型有区别)
~比如说:表示坐标->这种无法构成重载
public class Circle { public Circle(double x,double y){ //笛卡尔坐标 }; public Circle(double r,double a){ //极坐标 }; } 所以上面的代码也不对,构不成方法重载
使用工厂模式来解决上述问题,不使用构造方法来,(我刚开始也在想为什么是静态,直到我自己去试一下,明白了也就是说不用构造方法创建对象,假如不是静态方法,那么该怎么调用他呢,不是静态的,可是只能用对象.方法才可以调用)用普通方法来构造对象,这样方法就可以任意的了,普通方法内部去new对象~由于普通方法的目的是创建对象,然后调用方法来设置属性,所以方法一般都是静态的。(类名.方法)
三、💜
//Executors:工厂类,后面的是工厂方法。这句话是创建一个固定线程数量的线程池
//线程池对象:ExecutorService service
ExecutorService service= Executors.newFixedThreadPool(4);
//创建一个线程数组,动态变化的线程池。
ExecutorService service2= Executors.newCachedThreadPool();
//包含单个线程(比原生创建API更简单一些指Thread)
ExecutorService service3= Executors.newSingleThreadExecutor();
//类似于定时器效果,添加一些任务,执行,被执行的时候,不是只有有一个扫描线程来执行,可能是有多个共同执行
ExecutorService service4= Executors.newScheduledThreadPool();
}
四、❤️
面试题:谈谈java库中的线程池构成方法的参数和含义
这个方法最复杂,而且别的参数这个参数都有,所以就只解释这个方法就行。
int corePoolSize:核心线程数
int maximunPoolSize:最大线程数
ThreadPoolExcutor:里面的线程个数,并
非固定不变的,会根据当前任务的情况动态变化(自适应)
corePoolSize:至少要这些线程,哪怕你的线程都没任务也要这些个线程(如同公司里面的正式员工)
maximumPoolSize:最多不超过这些线程,哪怕干冒烟了,也不能比这个更多了(正式员工+实习生)
long keepAliveTime, TimeUnit unit:实习生线程,空闲时间超过指定阈值(允许实习生摸鱼的最大时间),就可以销毁了
BlockingQueue<Runnable>workQueue:线程池内部有很多很多,任务可以使用阻塞队列管理,线程池可以内置阻塞队列,也可以手动一个。
RejectedExecutionHandler handler:线程池考的重点,拒绝方式/拒绝策略,线程池有一个阻塞队列,当队列满了,继续加任务如何处理。
1.ThreadPoolExecutors.AbortPolicy:直接抛出异常,线程池就不干活了,(小王喊我打球,我在学习,我直接崩溃了,我哇哇大哭)
2.ThreadPoolExecutor.callerRunsPolicy:谁说添加这个任务的线程,谁就去执行这个任务
,我会直接说:我没空,自己投去吧)
3.ThreadPoolExecutor.DiscardPolicy:把新的任务丢弃,(不打球了,我接着学习)
4.ThreadPoolExecutor.DiscardPolicy:丢弃最早的任务,执行新的任务(放弃学习,去打球)
有的线程公司会推荐使用这个。
五、💚
具体实现一个线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//通过这个方法把任务添加到线程池中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//n表示一个线程池里面有几个字段,创建了一个固定数量的线程池
public MyThreadPool(int n) throws InterruptedException {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true) {
Runnable runnable = null;
try {
runnable = queue.take(); //从队列中提取这个任务
} catch (InterruptedException e) {
e.printStackTrace();
}
runnable.run(); //运行这个任务
}
});
t.start(); //开启这个线程
}
}
}
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
//获取当前引用实例
MyThreadPool myThreadPool = new MyThreadPool(4);
for (int i = 0; i < 1000; i++) { //循环很关键哈,这个循环是添加1000次任务,但是假如你放到run里面,就会变成一个任务,至少内容是执行1000遍,线程是执行多个任务,但是不能一个任务还分担
myThreadPool.submit(new Runnable() { //(安排任务,安排一个任务)
@Override //这个任务进行的工作
public void run() {
System.out.println(Thread.currentThread().getName() + " love");
}
});
}
}
}
面试问题2号:创建线程池的时候,线程个数怎么数的:
网路上查资料很多:假设cpu逻辑数是N,线程的个数:N,N+1,2N···
准确的说都不准确,因为不同的项目要做的工作是不同的:
cpu密集型线程工作:全是运算大部分工作在cpu上完成,cpu给他安排核心,才能概括,假如cpu N个核心,线程数量最好也是为N,如果多了,线程也只能是排队等待,没有新的进展
Io密集型线程工作:涉及大量等待时间,等待的过程,不要cpu,所以这里线程多,也会给cpu造成负担,cpu16核,整个32个线程,不犯毛病(不耗cpu,甚至cpu占用很低)———
实际上,一部分cpu密集,一部分Io密集,是我们工作中的常态,此时一个线程多少在cpu上执行,多少等待IO,说不好,更好的做法:自己去性能测试一下,找到性能和开销比较均衡的数值。
六、💔
多线程进阶开启:
常见的锁策略
如果工作中,真正要实现一把锁,需要理解锁策略
1.乐观锁VS悲观锁
乐观锁:预测,不太会出现锁冲突的情况
悲观锁:预测,这个场景非常容易锁冲突
2.重量级锁VS轻量级锁
重量级锁:加锁开销比较大,花的时间多,占有系统资源,一个悲观锁,很可能是重量级锁
轻量级锁:花的时间少,占有资源少,加锁的开销比较小的,很可能是乐观锁
悲观,乐观是加锁之前堆冲突概率的预测,决定工作的多少,重量,轻量,是加锁后,考虑实际的锁开销
3.自旋锁VS刮起等待锁
自旋锁是轻量级的一种典型实现,在用户态通过自旋的方式(while循环),实现类似加锁的操作(一直在疯狂的舔,这种锁会耗一定的cpu,但是是最快速度拿到锁的)
挂起等待锁:通过内核态,借助系统提供的锁机制,当出现锁冲突,会牵扯到内核对线程的调度,是冲突的线程出现挂起(阻塞等待)重量级锁的一种典型体现,发现锁被占用后,自己该干啥干啥,偶尔听到了消息,又去找这个锁,耗费cpu少,但无法第一时间拿到锁(小摆烂)
4.读写锁VS互斥锁
读写锁:把读操作,写操作分开了
假如两个线程 一个读加锁,另一个还是读加锁,那么两个不会有锁竞争(目的:就是把这种情况处理,这样多线程的效率会更高)
一个读,一个写,两个都是写都会有锁竞争,但是两个读没事,在开发中读操作会比写操作更加频繁
互斥锁:写了就不能读,读了就不可以写,
5.公平锁VS非公平锁
公平锁是遵循先来后到这个规则的
非公平锁相当于超市促销,都来抢位置,不遵守顺序
操作系统自带锁(pthread-mutex)是非公平锁,要实现公平锁,就需要一些额外的数据结构来支持(比如需要有办法记录每个线程的阻塞等待时间)