一,线程池
1.为什么会有线程池?线程池和多线程的区别?
为了很好的解决高并发问题,提高计算机的运行效率,提出了多线程来取代多进程(因为一个线程的创创建、销毁和调度比进程更加“轻量”,所以线程也被称作“轻量级进程”),这就是线程存在的意义;
随着并发程度的提高,随着我们对于性能要求标准的提高,我们发现线程的创建也没有那么“轻量”,因为线程的创建,销毁和调度都源自于操作系统内核,频繁的对线程进行操作开销也会很大,所以线程池的概念也随之诞生。
线程池就是在多线程的基础上,减少了对线程频繁创建、销毁和调度的操作,来降低创建、销毁线程的开销,其核心思想:事先把需要使用的线程创建好放到“池”中,后面需要使用的时候,直接从池里获取,用完了还给“池”。
主要区别:
多线程:创建、销毁线程都是交由操作系统内核来完成
线程池:事先创建好线程,从“池子”里获取还给“池子”,都是由用户代码实现,不用交给内核操作
主要改进:
减少了在多线程环境下操作系统内核频繁创建、销毁线程的开销
2.线程池的创建
在Java标准库中,对于线程池的创建主要是通过Executors这个工厂类(工厂模式就是使用普通方法来代替构造方法创建对象=相当于是把new操作给隐藏到普通方法后面)来实现。
构造方法(只介绍常见的):
newFixedThreadPool | 创建指定数目线程的线程池 |
newCachedThreadPool | 线程数量是动态变化的,任务多了就多创建几个线程 |
newSingleThreadExecutor | 线程池内只有一个线程 |
newScheduledThreadPool | 类似于定时器,只是执行扫描任务的时候不是由扫描线程完成,而是线程池内的线程完成 |
submit方法:
submit方法是为了用来给线程池提交任务,传入的参数是Runnable类型的。
线程池代码示例:
/**
* 创建一个包含10个线程的线程池去执行1000个任务
* 每个任务被执行的时候打印哪个线程正在执行任务来知道当前被调度的线程
*/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo3 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
}
}
发现此时线程执行任务的顺序是随机的,因为每个线程执行完一个任务之后再立即取下一个任务,由于每个线程执行任务的时间不同,因此每个线程并不是按照一定的顺序来执行。而且我们发信这里的进程并没有结束,这点类似于之前说的Timer计时器类,所创建的线程都是前台线程会阻止进程的结束。
二,模拟实现线程池
简单的线程池需要实现两个功能:
阻塞队列,保存任务
若干个工作线程
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
//使用阻塞队列保存任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//此处n表示线程数量
public MyThreadPool(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) {
e.printStackTrace();
}
}
});
t.start();
}
}
//注册任务给线程池
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +"正在执行任务");
}
});
}
}
}
通过编译可以执行效果同Java标注库提供的submit方法一样。
三,ThreadPoolExecutor
上面构造方法中提到的那些线程池本质上都是通过包装ThreadPoolExecutor类来实现的,只是ThreadPoolExecutor这个线程池用起来更麻烦(所以Java标准库给我们提供了工厂类),因为其参数过多使其使用起来变得复杂,但是在面试中ThreadPoolExecutor类的参数的含义考的非常多,所以我们需要做一个简单的了解。
ThreadPoolExecutor的构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) |
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecytionHandler handler) |
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) |
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecytionHandler handler) |
这里我们只讲解第四种构造方法,因为第四种的构造方法最为复杂,包含了所有参数。
corePoolSize(核心线程数):
一般指最少包含的线程数量(类比公司的正式员工);
maximumPoolSize(最大线程数):
线程池所能容纳的最大线程数,当活跃线程数达到该数值后,后续的新任务将会阻塞(类比公司的实习生,正式员工+实习生 = 最大线程数);
keepAliveTime(线程闲置超时时长):
如果超过该时长,非核心线程就会被回收(描述了实习生可以偷懒的最大时间);
unit(时间单位):
指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分);
BlockingQueue<Runnable> workQueue(线程池的任务队列):
任务队列通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中,采用阻塞队列实现;
ThreadFactory threadFactory(线程工厂):
用于指定为线程池创建新线程的方式;
RejectedExecytionHandler handler(拒绝策略):
描述线程的“拒绝策略”,也是一个特殊的对象,描述了当线程朝任务队列满了,如果继续添加任务会有啥样的行为。
拒绝策略:
ThreadPoolExecutor.AbortPolicy | 如果队列满了,就直接抛出一个异常 |
ThreadPoolExecutor.CallerRunsPolicy | 如果队列满了,多出来的任务,谁加的谁负责 |
ThreadPoolExecutor.DiscardOldestPolicy | 如果队列满了,丢弃最早的任务 |
ThreadPoolExecutor.DiscardPolicy | 如果队列满了,丢弃最新的任务 |