文章目录
- 一、线程池的基本情况
- 1.1、使用线程池的必要性
- 1.2、线程池为什么比直接在系统中创建线程更高效?
- 1.2.1、纯内核态操作
- 1.2.2、纯用户态操作
- 1.3、那为什么用户态操作比内核态操作更高效?
- 二、如何在Java中使用线程池
- 2.1、ExecutorService
- 2.1、ThreadPoolExecutor[重点]
- 2.1.1、谈谈Java标准库里的 线程池 构造方法的参数和含义?[经典面试题]
- 三、自我实现一个线程池
- 3.1、输入线程数目时如何界定?
- 3.1.1、CPU密集型
- 3.1.2、IO密集型
一、线程池的基本情况
1.1、使用线程池的必要性
虽然线程是 “轻量级进程” ,但当我们频繁的创建/销毁 线程时,其所产生的成本不可忽视。因此可以使用 线程池,提前创建好一些线程放在线程池中,后续需要使用线程时,直接从线程池中随取随用即可,当线程不再使用时,就放回池子里。那么此时效率就会大幅度提升。
1.2、线程池为什么比直接在系统中创建线程更高效?
1.2.1、纯内核态操作
但是为什么从线程池中取线程,就比在操作系统里创建线程,更高效呢?
1、如果是从系统处来创建线程,就需要调用系统API,进一步的由系统内核来完成创建线程的操作,譬如说:创建一个PCB,然后再将PCB加入到链表中…(纯内核态操作)
1.2.2、纯用户态操作
2、如果是直接从线程池处获取线程,上述内核态中进行的操作,都已经提前完成了,从线程池中取线程的过程,纯粹是由用户代码完成。(纯用户态)
1.3、那为什么用户态操作比内核态操作更高效?
举个例子:
比如说我们需要去银行办业务,柜台是非工作人员无法进入,此时柜台处就相当于内核态,大众可以在银行大厅随意溜达,此时银行大厅就相当于是用户态。假如A要去柜台让工作人员帮他解冻银行卡,工作人员需要A提供一下身份证复印件来办理业务,但是A没有带复印件,此时工作人员就给出了2个建议:1、工作人员帮他复印。2、银行大厅有复印机,A可以自行进行复印。
如果A让工作人员帮其进行复印,有可能工作人员在帮A复印时,被领导交代了别的比较急的业务(因为内核态是给所有进程提供服务的,工作人员也一样,他也会为很多用户提供服务 ),此时工作人员就会放下手里的复印的工作,先去完成领导交代的业务,过了一会后才继续帮A进行复印。那么此时A就不知道自己什么时候才能拿到复印件,到底是多久,这是不可控的,取决于工作人员的效率,A无法控制。
如果A选择自行到银行大厅进行复印,此时什么时候能复印完,是可控的。
那此时就是A自己复印,比让工作人员帮忙复印,效率会更高更快。
二、如何在Java中使用线程池
2.1、ExecutorService
Java标准库中,提供了现成的线程池类——ExecutorService
ExecutorService 类中提供4个用来 创建线程池对象的过程 的工厂方法:
1、Executors.newFixedThreadPool(int x)
创建一个固定线程数量的线程池
2、Executors.newCachedThreadPool()
创建一个线程数目动态变化的线程池:譬如说当首次使用线程池时,线程池可能会没有线程,此时因为要执行任务,就会创建出一个线程,当一个线程不够用时,会动态的创建出其他线程,创建出的线程就不用回收了,以便下次使用。
3、Executors.newSingleThreadPool()
创建一个只有单个线程的线程池
4、Executors.newScheduleThreadPool()
包含单个线程的线程池,类似于定时器的效果。添加一些任务,任务都在后续某个时刻再执行,被执行的时候不是只有一个扫描线程来执行任务,可能是由多个线程共同执行所有任务。
Executors 称为 “工厂类”。
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
for(int i = 0;i<1000;i++){
/**
* 线程池对象创建好了之后,就可以使用 submit() 把任务添加到线程池中
*
* 但由于线程池不止要执行一个任务,因此光一个 submit() 还远远不够,
* 所以可以在 submit() 外加一个 for循环,1000 表示 有 1000 个任务,
* 线程池中的4个线程需要共同执行/分担这1000个任务
**/
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("Runnable 里的 run() 里,就是我们具体要执行的任务!");
}
});
}
}
运行结果:(1000个任务,所以执行了1000下,打印了1000个输出)
2.1、ThreadPoolExecutor[重点]
Java标准库除了提供上述的线程池标准类外,还提供了一个接口更丰富的线程池类—— ThreadPoolExecutor
ThreadPoolExecutor类有4个构造方法:
2.1.1、谈谈Java标准库里的 线程池 构造方法的参数和含义?[经典面试题]
由于该线程池类 ThreadPoolExecutor 中的线程数并不是一成不变的,而是根据任务的情况动态变化(自适应),如果任务多,该线程池中的线程数就多一些(创建出来),任务少,该线程池中的线程数就少一些(多余的就销毁),但是此处的动态变化也并不是没有限制,因此ThreadPoolExecutor类的构造方法提供了几个不同含义的参数,来对线程池的动态变化产生一定限制。(如:corePoolSize、maximumPoolSize)
1、介绍构造方法中的参数:
int corePoolSize
核心线程数(线程池里最少也得有这些数量的线程,哪怕线程池里一点任务也没有)
2、int maximumPoolSize
最大线程数(最多不能超过这些线程。哪怕线程池由于需要执行很多任务忙疯了,动态变化的线程数也不能比这个数目更多了)
举个例子理解上述两个参数:
这样设定之后,公司繁忙时,就可以在含有正式员工的情况下,招收一些实习生帮忙;在公司不忙的时候,可以将多余的实习生裁掉。
对于线程池来说也是一样,这样的设定,既能保证繁忙的时候高效的处理任务,又能在空闲的时候不会浪费资源。
3、long keepAliveTime
允许摸鱼的最大时间数(当公司业务不繁忙时,实习生就都空闲下来了,那么公司会立即裁掉实习生吗??不是,是当实习生线程空闲超过指定的时间阈值后,就会被销毁。)
4、TimeUnit unit
时间单位(譬如 ms、s…)
5、 BlockingQueue workQueue
线程池内部会有很多任务要线程去执行,这些任务,可以使用阻塞队列来管理起来
6、ThreadFactory threadFactory
这是一个工厂类,主要用于创建线程的
7、RejectedExecutionHandler handler [考察的重点参数]
。拒绝方式/拒绝策略。 ThreadPoolExecutor 的线程池,有一个阻塞队列,当阻塞队列中的任务满了之后,继续添加任务,该如何应对??阻塞?不太合适。因此就有了4种拒绝策略:
1)、ThreadPoolExecutor.AbortPolicy
直接抛出异常,线程池直接不干活了
2)、ThreadPoolExcutor.CallerRunsPolicy
谁是添加这个新任务的线程,那么这个任务就由谁来执行
3)、ThreadPoolExecutor.DiscardOldestPolicy
丢弃前边最早的任务,执行新的任务(晚来的任务)。
4)、ThreadPoolExecutor.DiscardPolicy
直接把最新的任务丢弃了
三、自我实现一个线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 自我实现一个固定数量的线程池
*/
class MyExecutor {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
/**
* 线程数
*
* @param n
*/
public MyExecutor(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 class testMyExecutor {
public static void main(String[] args) throws InterruptedException {
MyExecutor myExecutor = new MyExecutor(4);
for (int i = 0; i < 1000; i++) {
myExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"hello");
}
});
}
}
}
3.1、输入线程数目时如何界定?
那么创建线程池时,使用创建固定线程数目的线程池的方法时,这个线程池的线程数目究竟是该填几呢?4?8?12?
都不准确。没有什么固定数量!不同项目里,线程要做的工作都是不一样的。有的线程进行的工作是:“CPU密集型”(该线程做的工作大多/全是运算),有的线程的工作是:“IO密集型”(该线程进行的工作可能是:读写文件、等待用户进行输入、网络通信…)。
3.1.1、CPU密集型
"CPU密集型"大部分工作都是要在CPU上执行的,CPU得给线程安排cpu核心去完成工作才行。因此在这个情况下,如果CPU是N个核心,当线程数量也是N时,理想情况下,即每个核心上一个线程,如果有更多线程数,线程只能进行排队等待CPU核心,不会有什么新的进展(譬如说效率提升之类的,不会),甚至于可能物极必反,太多线程数导致线程调度开销变大,影响效率。
3.1.2、IO密集型
“IO密集型”大部分的线程需要进行大量的等待时间,(譬如等待用户进行输入),等的过程中,并没有使用CPU,此时就算线程数就算多一些,也不会给CPU造成太大负担。比如CPU为16个核心,写个32个线程,由于这些线程在进行着“IO密集型”工作,这里大部分的线程都在等,并不消耗CPU,反而CPU的被占用情况还比较低。
但上述描述的是一种 “理想状态”,在实际开发中,既会有部分线程进行 “CPU密集型”工作,又会有部分线程进行“IO密集型”工作。
因此最好的做法就是通过实验(对程序进行性能测试,测试时尝试不同线程数,找到性能和系统资源开销比较均衡的数值)的方式,来找到线程池中合适的线程数目。