多线程-线程池
线程池的作用是提前创建好线程.提高程序在面对大量并发任务时的效率和性能
为什么要使用线程池
只有一个原因:创建一个线程的开销非常大
创建一个线程时,需要进行很多操作:
- 分配内存
- 与操作系统互动
- 线程销毁也需要资源
但当我们使用线程池时:
线程池会预先创建好一批线程,当有任务时,直接取出一个线程执行,而完成后不销毁线程,直接将线程重新放入线程池中,当再有类型的线程需求时,可以直接取出
这样的好处很明显: - 提升性能:减少创建/销毁线程的开销
理所应当的,毕竟线程池是创建几个线程后就不断复用这些线程 - 限制并发量:线程池可以规定最大线程数.这样可以防止线程创建过多导致的系统崩溃
另外java中还有进程池,但在普通编程环境下线程池的效果远不如进程池好.因此我们此处忽略进程池的相关知识
java中的线程池
java的线程池由java.util.concurrent包中的Executor框架提供
- Executor:任务执行器(接口)
- ExecutorService:增强版的Executor
- Executors:工具类,用来创建线程池
- ThreadPoolExecutor:真正的线程池实现
ThreadPollExecutor关键参数解释
java
ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
BlockingQueue<Runnable> blockingQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
| 参数 | 意义 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程存活时间 |
| blockingQueue | 任务阻塞队列 |
| threadFactory创建线程工厂 | |
| handler | 队列满时的拒绝逻辑 |
corePoolSize|核心线程数 和 maximumPoolSize|最大线程数
首先,需要知道什么是核心线程和非核心线程
- 核心线程:不会随便释放掉的线程(类似于公司中的正式员工)
- 非核心线程:在任务比较少的时候会自动释放的线程(类似与公司中的实习生)
因此
corePoolSize这个参数就是:你需要多少个核心线程
maximumPoolSize这个参数是:你一共需要多少个核心线程和非核心线程(这个值不能小于核心线程数)
那么如何设置核心线程数和最大核心线程数呢?
对于不同类型的程序,我们需要灵活设置 - 对于CPU密集型:算术运算,逻辑判断等
由于对CPU消耗很大,需要更大的核心线程数 - 对于I/O密集型:文件操作,网络操作等
由于对CPU消耗很小,故不需要大量的核心线程
最合理的做法是:
通过一次次性能测试,找到一个合适的值
keepAliveTime |非核心线程存活时间
如果一个非核心线程的空闲时间超过了设定的存活时间,那么这个非核心线程将会被销毁
例如:如果设定时间是100.存在一个非核心线程在100的时间内仍然没有工作,那么这个线程就会销毁
blockingQueue|任务阻塞队列
由于一个线程池需要处理多个任务,因此需要存放运行的任务
submit的功能就是把要执行的任务(Runnable对象)给放到队列中
threadFactory | 线程工厂
通过线程工厂,可以做到不使用'new'或者构造方法来初始化对象
通过工厂方法最大的好处就是没有重载限制:比如使用相同的构造名但可以使用不同的参数
java
public class Point {
// //无法重载,会报错
// public Point(double x, double y) {//使用坐标构建点
//
// }
// public Point(double r, double a) {//使用极坐标构建点
//
// }
//使用工厂方法
public static Point buildPointByXY(double x, double y) {//使用坐标构建点
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point buildPointByRA(double r, double a) {//使用极坐标构建点
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
}
handler|队列满时的拒绝逻辑
在阻塞队列中,队列满时的拒绝逻辑就是阻塞.但在线程池中,队列满时,阻塞并不一定是最好的应对方法.因此,我们需要有其它方式来处理
线程池中hander是RejectedExecutionHandler.它是一个接口,用来定义队列满时的拒绝逻辑.通过使用这个接口,就能做到自定义线程池的拒绝逻辑
- 线程池自带的拒绝逻辑
- ThreadPoolExecutor.AbortPolicy
当队列满时直接抛出异常
适用于需要立即知道任务被拒绝访问的场景 - ThreadPoolExecutor.CallerRunsPolicy
当队列满时让调用者来执行
适用于对任务处理实时性能要求不高的场景 - ThreadPoolExecutor.DiscardPolicy
当队列满时直接丢弃任务中最新的任务 - ThreadPoolExecutor.DiscardOldesPolicy
当队列满时直接丢弃任务中最老的任务
- ThreadPoolExecutor.AbortPolicy
简单线程池的模拟实现:只实现定义线程数
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MyThreadPoolExecutor {
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();//定义一个阻塞队列
public MyThreadPoolExecutor(int nThread) {//根据参数n创建n个线程
for (int i = 0; i < nThread; i++) {
Thread t = new Thread(() -> {
while(true) {
Runnable runnable = null;
try {
runnable = queue.take();//如果没有使用到的线程会自动阻塞
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
runnable.run();//运行runnable内容
}
});
t.start();//开始执行线程
}
}
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(5);
for (int i = 0; i < 1000; i++) {//假如有100个任务
int id = i;
myThreadPoolExecutor.submit(() -> {
System.out.println("线程:" + Thread.currentThread().getName() + "正在执行第" + id + "个任务");
});
}
}
}
观看这个模拟的线程池可以发现线程池使用了阻塞队列.在一个线程中通过不断传入runnable接口来执行特定的任务
运行这个模拟程序后,我们会发现返回的结果如下
bash
线程:Thread-0正在执行第979个任务
线程:Thread-0正在执行第997个任务
线程:Thread-4正在执行第976个任务
线程:Thread-4正在执行第999个任务
线程:Thread-0正在执行第998个任务
线程:Thread-1正在执行第996个任务
线程:Thread-2正在执行第992个任务
线程:Thread-3正在执行第989个任务
观察这个结果我们发现线程的执行是不是线程0,1,2,3这样依次执行的,而且最后一个任务也不一定是最后一个完成的
定时器
在执行多线程任务,我们肯定会有一个需求.就是我希望某些任务先执行,某些任务后执行.那么应该怎么做呢?总不能在每个线程执行前加个sleep吧
此时,定时器这个东西就可以解决这个问题
- Timer.schedule
Timer类中有一个schedule方法.这个方法可以做到多线程中定时执行任务
语法格式public void schedule(TimerTask task, long delay);.
其中TimerTask 可以简单理解为一个专为Timer类写的一个Runnable接口.它和runnable的唯一区别是TimerTsak多了有关时间控制的内容.
long delay 就是你要过多久才执行这个线程执行这个线程.
实例展示
java
import java.util.Timer;
import java.util.TimerTask;
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1");
}
},1000);
System.out.println("end");
//timer.cancel();//关闭所有进程
}
}
运行后结果
bash
end
hello 1
hello 2
可以发现打印"end"的进程在代码中明明是排在最后的,但执行时代码却是最先执行完的.而"hello 1"在其次,"hello 2"在最后
实际上是在线程执行的一开始end就被打印了.1000毫秒后,hello 1 打印. 2000毫秒后, hello 2 打印.
注意到代码中有一个cancel()方法.可能很容易被理解为在执行完所有线程后关闭这个总的进程.但你如果把这个方法的注释去掉,会发现执行后的结果变成了这样
bashend 进程已结束,退出代码0会发现只打印了一个end.这是因为方法的逻辑是立即关闭所有线程.不会等延时线程执行完再关闭进程
定时器原理--手写一个MyTimer
定时器最大的作用是能在多个任务中,指定每个任务延迟多久后再开始运行.因此需要做到让时间最小的任务先进行运行
为了找到任务中最先执行的任务,我们可以使用小根堆
java
import java.util.PriorityQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyTimerTask implements Comparable<MyTimerTask> {
private Runnable runnable;//要执行的任务
private long time;//执行时间
public long getTime() {
return time;
}
public void run() {
runnable.run();
}
public MyTimerTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
}
public class MyTimer {
Lock locker = new ReentrantLock();
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public MyTimer() {
Thread t = new Thread(()-> {
try {
while (true) {
synchronized (locker) {
while (queue.isEmpty()) {
locker.wait();
}
MyTimerTask task = queue.peek();
long currentTime = System.currentTimeMillis();//系统时间
if (task.getTime() <= currentTime) {//判断时间
task.run();
queue.poll();
} else {
//超时等待
locker.wait(task.getTime() - currentTime);
}
}
}
}catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
public void schedule(Runnable task, Long delay) {
synchronized (locker) {
queue.offer(new MyTimerTask(task, delay));
locker.notify();
}
}
}