线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
为什么要使用线程池?
因为频繁的开启线程或者停止,线程需要重新被cpu从就绪到运行状态调度,效率非常低。所以采用线程池直接实现复用,创建好一个线程之后,不会立马毁掉,而是一直复用,从而直接从运行状态调度,提高效率(无需CPU调度切换,直接运行方法)
这样一说我们就已经了解到了他的优点和缺点了。
优点
- 效率比较高
- 统一管理线程
- 线程可以直接复用
缺点也很明显。
缺点
主要就是被复用的线程一直在运行状态,导致非常的耗费CPU资源。
但是,优点和缺点的相互对比下来,其实优点大于缺点。所以线程池是很好的了。
线程池的作用
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
那么线程池的创建有几种形式?JDK自带的实现一共有四种,分别是
- Executors.newCachedThreadPool(); 可缓存线程池
- Executors.newFixedThreadPool();可定长度
- Executors.newScheduledThreadPool() ; 可定时
- Executors.newSingleThreadExecutor(); 单例
但是在阿里巴巴的java开发手册中,有着这样一个强制要求。
那还是自定义最好了,其实可以发现上面四种创建线程池的方式底层都是通过new ThreadPoolExecutor()的形式实例化的。所以我们直接使用new ThreadPoolExecutor()实例化即可。
那为什么不建议使用呢?其实在上面的这个实例化可以看到使用的是new LinkedBlockingQueue<Runnable>()
,那么这个是什么呢?其实是他就是缓存队列,后面会说。如果缓存队列过长,那么最大线程数则会失效,并且非常消耗内存。
有界队列和无界队列的区别
创建的时候是否自定义初始值
那么要怎样是实例化线程池才会合理要求?上面提到过,最不合乎要求的地方是,缓存队列没有固定的大小。
那么缓存队列使用阻塞队列就可以达到效果,那么阻塞队列又是什么?
阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
非阻塞队列
无论入队还是出队,都是在死循环中进行的,也就是说,当一个线程调用了入队、出队操作时,会尝试获取链表的tail、head结点进行插入和删除操作,而插入和删除是通过CAS操作实现的,而CAS具有原子性。故此,如果有其他任何一个线程成功执行了插入、删除都会改变tail/head结点,那么当前线程的插入和删除操作就会失败,则通过循环再次定位tail、head结点位置进行插入、删除,直到成功为止。
我们在这里可以看出,两者的主要差别就在于线程是否释放执行权。
ThreadPoolExecutor的核心参数
- corePoolSize(核心线程数量): 一直正在保持运行的线程
- maximumPoolSize(最大线程数):线程池允许创建的最大线程数(核心线程数+非核心线程数)。生效条件:缓存队列满的情况
- keepAliveTime(超时时间):在一定时间内没有使用的非核心线程的情况下,则销毁。
- unit:keepAliveTime的时间单位。
- workQueue(缓存线程队列):用于保存待执行的任务。
- threadFactory:线程池内部创建线程所用的工厂。
- handler:任务无法执行时的处理器。
这里可以看到当核心线程数不满足用户线程的时候,应该启用非核心线程来解决多出的线程。
了解这个之前,我们先了解怎么解决内存溢出问题。其实就是将缓存队列设定大小即可。
案例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author 龙小虬
* @date 2021/4/30 12:06
*/
public class MyExecutor {
// 存放核心线程数
private List<TaskThread> taskThreads;
//缓存队列
private BlockingQueue<Runnable> blockingQueues;
public MyExecutor(int codeThreads, int threadQueues) {
taskThreads = new ArrayList<TaskThread>(codeThreads);
this.blockingQueues = new LinkedBlockingQueue<Runnable>(threadQueues);
for (int i = 0; i < codeThreads; i++) {
TaskThread taskThread = new TaskThread();
taskThread.start();
taskThreads.add(taskThread);
}
}
class TaskThread extends Thread {
@Override
public void run() {
while (true) {
Runnable runnable = blockingQueues.poll();
if (runnable != null) {
runnable.run();
}
}
}
}
public boolean executor(Runnable runnable) {
return blockingQueues.offer(runnable);
}
public static void main(String[] args) {
MyExecutor myExecutor = new MyExecutor(2, 2);
for (int i = 0; i < 10; i++) {
int finalI = i;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean executor = myExecutor.executor(() -> {
System.out.println(Thread.currentThread().getName() + "," + finalI);
});
}
}
}
运行之后,会发现,项目一直不会停止,那要怎么去停止项目?其实很简单,项目一直不停止是因为while(true)
的原因,所以我们只需要给出一个临界条件。
代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author 龙小虬
* @date 2021/4/30 12:06
*/
public class MyExecutor {
static boolean flag = false;
// 存放核心线程数
private List<TaskThread> taskThreads;
//缓存队列
private BlockingQueue<Runnable> blockingQueues;
public MyExecutor(int codeThreads, int threadQueues) {
taskThreads = new ArrayList<TaskThread>(codeThreads);
this.blockingQueues = new LinkedBlockingQueue<Runnable>(threadQueues);
for (int i = 0; i < codeThreads; i++) {
TaskThread taskThread = new TaskThread();
taskThread.start();
taskThreads.add(taskThread);
}
}
class TaskThread extends Thread {
@Override
public void run() {
while (blockingQueues.size() != 0 || !flag) {
Runnable runnable = blockingQueues.poll();
if (runnable != null) {
runnable.run();
}
}
}
}
public boolean executor(Runnable runnable) {
return blockingQueues.offer(runnable);
}
public static void main(String[] args) {
MyExecutor myExecutor = new MyExecutor(2, 2);
for (int i = 0; i < 10; i++) {
int finalI = i;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean executor = myExecutor.executor(() -> {
System.out.println(Thread.currentThread().getName() + "," + finalI);
});
}
flag = true;
}
}
其实如果要把最大线程数使用起来,那么只需要自己去实例化ThreadPoolExecutor就行了。比如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author 龙小虬
* @date 2021/4/30 12:27
*/
public class MyThreadPoolExecutor {
public static ExecutorService newFixedThreadPool(int corePoolSize, int maximumPoolSize, int blockingQueue) {
return new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
60L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(blockingQueue));
}
}
上面还提到了拒绝策略。
这里提一下。在线程数超过了最大线程数会报错。
所以需要对报错之后的线程进行处理
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认是AbortPolicy,会抛出异常。
ThreadPoolExecutor类有几个内部实现类来处理拒绝任务:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
- 实现RejectedExecutionHandler接口,可自定义处理器