目录
一.池的概念
池是一种资源复用和管理技术,预先创建并维护一组可以重复利用的资源,减少重复创建和销毁的开销,达到节省资源的目的
- 资源复用:重复利用已分配的资源,减少重复创建和销毁的开销
- 资源管控:限制资源总量(如果资源太多,容易发生线程安全问题)
二.线程池的概念
在最开始使用进程的时候,由于进程的创建和销毁开销太大,于是引入了线程的概念(进程的更精细划分),虽然线程的开销很小,但是在频繁的创建和释放情况下,线程的开销也不得不重视起来,针对这个问题进行了优化。
- 协程 / 纤程:轻量化线程,对线程的更精细划分
一个代码中,创建出上千个线程,可能会卡死,如果是上千个协程,只能说还不够
- 线程池
将要使用的线程创建好,使用完后,将线程放入线程池中,下一次使用的时候,直接从线程池中取,不需要创建和销毁
并且在池子中获取线程,属于纯用户态操作(可控),由系统来创建,属于内核态操作(不可控),所以从线程池中获取线程比从系统中申请线程更高效
三.标准库中的线程池
在java中,线程池的实现基于 java.util.concurrent 包中的 ThreadPoolExecutor
类
ThreadPoolExecutor类
maximumPoolSize (最大线程数)=核心线程数+空闲线程数
BlockingQueue<Runnable> workQueue (工作队列):这里使用Runnable来描述任务,我们也可以改进,使用PriorityBlockingQueue,带有优先级的阻塞队列,控制任务的执行顺序
ThreadFactory threadFactory (线程工程):线程池提供定制化线程的能力
拒绝策略!!!
线程池中,有一个阻塞队列,能够容纳的元素有上限,如果队列满了,还要继续添加任务,线程池会有四种解决办法
策略 | 行为 | 适用场景 |
---|---|---|
AbortPolicy(默认策略) |
抛异常 | 关键任务,需明确失败反馈 |
CallerRunsPolicy |
提交者线程执行任务 | 缓解临时负载压力 |
DiscardPolicy |
静默丢弃 | 非关键任务(如日志) |
DiscardOldestPolicy |
丢弃队列最旧任务并重试提交 | 实时性优先 |
AbortPolicy(默认策略)
- 抛出异常,导致新任务和旧任务都不执行
- 需要手动处理异常
CallerRunsPolicy
- 新的任务由添加任务的线程去执行(新的任务会执行,不是由线程池中的线程去执行,而是由调用者执行)
- 减缓任务提交速度,避免线程池过载,但是可能导致阻塞提交任务的线程。
DiscardPolicy
- 丢弃最新的任务(新的任务直接销毁了,调用的线程和线程池都不执行)
- 无异常处理负担,但是任务丢失不易察觉。
DiscardOldestPolicy
- 丢弃最老的任务,去执行新的任务
- 可能丢弃重要旧任务。
在我们的日常使用中,由于ThreadPoolExecutor类使用起来太复杂,标准库中提供了另一个Executors工厂类对ThreadPoolExecutor类进行封装并且设计好了不同的参数
下面是常用的方法:
方法 | 说明 |
newScheduleThreadExecutor() | 创建定时器线程,延时执行任务 |
newSingleThreadExecutor() | 只包含单个线程的线程池 |
newCachedThreadExecutor() | 线程数目能够动态扩容 |
newFixedThreadExecutor(int nThreads) | 创建固定大小的线程池 |
无论是ThreadPoolExecutor类还是Executor类都是使用submit方法添加任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo_7 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("111");
}
});
}
}
在创建线程池的时候,会牵扯一个问题:线程池中线程的数量设置多少合适?
没有固定的答案,不同的程序,能够设定的线程数量是不同的
- 如果在一个进程中,所有的线程都是cpu密集型,那么每个线程的工作都在cpu上面,那么建议线程数目不要超过N(cpu核心数)
- 如果在一个进程中,所有的线程都是IO密集型,那么每个线程的工作都在等待IO,那么建议线程数目远远超过超过N(cpu核心数)
由于在实际开发中,一部分是cpu,另一部分IO,最好的办法是根据试验和测试找出合适的线程数
在不同的线程数目下,参考在总的时间的开销和系统资源的开销中,找到合适的量
四.模拟线程池
实现一个线程池,我们要明确以下步骤:
- 为了保证创建线程池就可以执行任务,将线程创建在构造方法中(只要创建出实例就会启动线程,执行任务)
- 在构造方法中指定线程池中使用多少个线程
- 使用阻塞队列创建任务队列,当队列中没有任务的时候,可以实现阻塞的功能
- 提供一个submit方法,可以将任务添加进入任务队列中
主要步骤:
- 在构造方法中,使用for循环创建出指定的线程数,每个线程都进入while死循环,一直执行任务,如果队列中没有任务,则会进入take()方法(带有阻塞功能)
- 使用submit()方法将任务存放进入队列中
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPoolExecutor{
// 存放线程
List<Thread> list = new ArrayList<>();
// 存放任务
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
MyThreadPoolExecutor(int n){
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
//线程具体的工作
while(true){
try {
//取出任务
//使用take方法,可以进入阻塞
Runnable x = queue.take();
//执行任务
x.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//将每一个线程都启动起来
t.start();
list.add(t);
}
}
//添加任务
public void submit(Runnable runnable) throws InterruptedException {
//会进行唤醒操作
queue.put(runnable);
}
}
public class Demo_8 {
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(9);
for (int i = 0; i < 1000; i++) {
//此处i是变量不能打印出,需要变成常量
int n = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName()+"打印:"+n);
}
});
}
}
}
点赞的宝子今晚自动触发「躺赢锦鲤」buff!