线程池是一种非常实用的并发处理机制,它可以帮助我们更有效地管理和控制线程。线程池的主要优点是减少了线程创建和销毁的开销,提高了系统性能。
线程池应用场景
线程池在许多并发编程的场景中都有广泛的应用,以下是一些常见的线程池应用场景:
- Web服务器:线程池可以用于处理传入的HTTP请求。每当有请求到达时,线程池中的线程可以负责处理请求并生成响应,从而提高服务器的并发处理能力。
- 数据库连接池:在数据库访问过程中,连接的建立和销毁通常是开销较大的操作。通过使用线程池,可以事先创建和管理一组数据库连接,通过复用连接来提高数据库访问的效率。
- 图片处理:当需要对大量的图片进行处理时,可以将每个图片处理任务添加到线程池的任务队列中。线程池中的线程可以并发地处理这些任务,加快图片处理的速度。
- 并行计算:一些计算密集型任务,如数值计算、数据分析等,可以使用线程池实现并行计算。将任务分割成小块,每个块分配给线程池中的线程进行计算,最后将结果合并。
- 定时任务:线程池可以用于执行定时任务,例如定时的数据备份、定时的日志清理等。通过线程池管理定时任务,可以有效地控制任务的执行时间和并发度。
这些只是线程池应用的一些示例,线程池在需要并发处理大量任务和资源管理的场景中非常有用。它可以提高程序的性能、稳定性和资源利用率,减少线程创建和销毁的开销。
自定义一个简单的线程池
任务类
定义一个用于执行任务的类,并设置一个任务编号。
package com.caogl.threadPool;
/**
* 任务类:包含任务编号
*/
public class MyTask implements Runnable {
private int id;
public MyTask (int id) {
this.id = id;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("线程:" + name + "即将执行任务:" + id);
try {
Thread.sleep(200); // 这里模拟执行任务耗费2毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + name + "完成了任务" + id);
}
@Override
public String toString() {
return "MyTask{" +
"id=" + id +
'}';
}
}
线程类
定义一个线程类,用于保存线程的名称和所有任务的集合
package com.caogl.threadPool;
import java.util.List;
/**
* 线程类,用于保存线程的名称,和保存所有任务集合
*/
public class MyWork extends Thread {
private List<Runnable> tasks;
public MyWork(String name, List<Runnable> tasks) {
super(name);
this.tasks = tasks;
}
@Override
public void run() {
// 判断集合中是否有任务,有就一直执行
while (tasks.size() > 0) {
Runnable r = tasks.remove(0); // 将第一个任务取出
r.run(); // 执行
}
}
}
线程池类
定义一个线程池类,用于提交任务和执行任务
package com.caogl.threadPool;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* 自定义线程池类,用于提交任务和执行任务
*/
public class MyThreadPool {
// 任务队列,需要控制线程安全问题
private List<Runnable> tasks = Collections.synchronizedList(new LinkedList<>());
// 当前的线程数量
private int num;
// 核心线程数量
private int corePoolSize;
// 最大线程数量
private int maximumPoolSize;
// 队列的长度
private int workSize;
public MyThreadPool(int corePoolSize, int maximumPoolSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workSize = workSize;
}
/**
* 提交任务
* @param r 任务
*/
public void submit(Runnable r) {
// 判断当前集合中的任务数量是否超出了最大任务数量
if (tasks.size() >= workSize) {
System.out.println("任务:" + r + "被丢弃了...");
} else {
tasks.add(r);
// 执行任务
execTask(r);
}
}
/**
* 执行任务
* @param r 任务
*/
private void execTask(Runnable r) {
// 判断当前线程池中的线程总数量,是否超出了核心线程
if (num < corePoolSize) {
new MyWork("核心线程:" + num, tasks).start();
num ++;
} else if (num < maximumPoolSize) {
new MyWork("非核心线程:" + num, tasks).start();
num ++;
} else {
System.out.println("任务:" + r + "被缓存了...");
}
}
}
测试类
定义一个测试类,创建自定义的线程池类并设置对应参数大小,使用for循环提交任务到线程池中执行。
package com.caogl.threadPool;
/**
* 测试类
*/
public class MyTest {
public static void main(String[] args) {
// 设定核心线程2,最大线程4,队列大小20
MyThreadPool myThreadPool = new MyThreadPool(2, 4, 20);
for (int i = 0; i < 20; i++) {
MyTask myTask = new MyTask(i);
myThreadPool.submit(myTask);
}
}
}
执行主方法,可以看到当任务数量大于最大线程数量时,会被放到队列当中。
尝试调整任务数量为30看看,是否会执行丢弃任务。
可以看到,设置的队列长度为20,最大线程为4,当队列无法容纳新的任务时,就会执行拒绝策略。
实际应用场景中,可以根据以下参数设计分析来设置线程池的线程数量和队列大小。
- 核心线程数(corePoolSize)
- 核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来决定。
- 例如:系统百分之 80 的时间每秒都会产生 100 个任务,那么想要在 1 秒内处理完这 100 个任务,就需要10 个线程,此时我们就可以设计核心线程数为 10 ,当然实际情况不可能这么平均,所以一般按照 8020 的原则设计即可,既然按照百分之 80 的情况设计核心线程数,那么剩下的百分之 20 可以利用最大线程数量来处理。
- 任务队列长度(workQueue)
- 任务队列的长度一般设计为:核心线程数量/单个任务的执行时间 * 2
- 例如:根据上面的场景,核心线程为10,单个任务的执行时间为0.1秒,那么队列的长度可以设计为 10/0.1 * 2 = 200
- 最大线程数(maximumPoolSize)
- 最大线程数的设计除了参照核心线程数的条件,还需要参照系统每秒产生的最大任务数量决定。
- 例如:根据上面的场景,如果系统每秒产生最大的任务是 1000 个,那么最大线程数 = (最大任务数 - 队列长度) * 单个任务的执行时间,(1000 - 200)* 0.1 = 80