线程池的引入:
由于进程的创建和销毁的开销过大,所以我们引入了线程。但是,在线程的使用过程中,不断的创建和销毁线程,这个开销不断变大,所以我们需要一个方法来减少线程的创建和销毁的开销。
有两种办法来减少这个开销:
1.使用纤程/携程 ->更轻量化的线程
2.使用线程池
线程池是什么?
线程池是一种能够存储线程的数据结构,在用户使用完线程后不再销毁线程,而是将线程归还到线程池中,当用户需要使用线程,也由线程池进行分配。
为什么使用线程池比直接创建/销毁线程更快
线程池是用户态,也就是代码方面的,更加的可控,当代码执行时,就可以完成对线程的操作,开销更小。
而创建/销毁线程是内核态的,不可控,不知道什么时候内核会去创建/销毁这个线程,开销相比较大。
线程池最大的好处就是:减少创建/销毁线程的开销。
而代价就是会占用更多的资源。
Java标准库中的线程池
在Java标准库中,线程池的类为ThreadPoolExecutor,构造方法有四种:
以构造方法最多的举例:
corePoolSize:核心线程数,核心线程会一直存在,直到线程池被销毁。
maximumPoolSize:最大线程数,线程池可容纳的最多的线程数目。
keepAliveTime:非核心线程数可空闲的时间,当非核心线程空闲时间超过这个时间后,就会被线程池销毁。
unit:可空闲时间的单位,如:s,ms等
workQueue:用来存储要执行内容的阻塞队列。
threadFactory:线程工厂,用来创建线程,封装了创建线程的new操作。
handler:拒绝策略,如果任务量过多,超过了阻塞队列的容纳值,应该如何处理。
拒绝策略的四种:
AbortPolicy():超过负荷,直接抛出异常。(撂挑子不干了)
CallerRunsPolicy():调⽤者负责处理多出来的任务。(谁添加谁执行)
DiscardOldestPolicy():丢弃队列中最⽼的任务。(把老任务放弃,执行新的)
DiscardPolicy():丢弃新来的任务。(不执行新来的任务,假装不知道)
当一个任务被添加到线程池后,会有线程取走这个任务然后进行执行。
在Java标准库中,也对ThreadPoolExecutor进行了封装,给出了更简单化的线程池创建方法,ExecutorService类。
ExecutorService类在生成线程池时提供了四种方法:
newFixedThreadPool:创建固定线程数的线程池
newCachedThreadPool:创建线程数⽬动态增⻓的线程池
newSingleThreadExecutor:创建只包含单个线程的线程池
newScheduledThreadPool:设定延迟时间后执⾏命令,或者定期执⾏命令。是进阶版的Timer。
简单实现线程池
实现一个线程池,需要准备:
一个阻塞队列,用来传递和存储要执行的内容。
一个构造方法,在方法中创建线程
一个submit方法,能够添加执行内容
public class MyThreadPoolExecutor {
//阻塞队列,用来存放要执行的内容
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
//构造方法,指定创建多少线程
public MyThreadPoolExecutor(int n){
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
//执行的内容
while(true){
try {
//拿到要执行的内容,如果没有要执行的内容,那就就阻塞等待
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
//添加要执行的任务
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
for (int i = 0; i < 1000; i++) {
int k = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行第:"+ k +"个工作,执行者:"+ Thread.currentThread().getId());
}
});
}
}
}
执行这个代码可以发现,执行工作的顺序并不是严格按照放入顺序执行的。
因为,当一个线程取走这个工作后,他不一定会立刻执行这个工作(在上面的代码里表现为打印内容),那么在这个空档中,有其他线程执行工作,就会体现出这种非严格按照放入顺序执行的局面。
例如,当12拿到0后立刻执行,然后13拿到了1,但是13此时被调度走了,没有执行,紧接着的14就会拿到2,那么此时14执行,然后13才执行,就形成了0->2->1这种执行方式。