34. 正确理解两类线程池
一、例子一
网站的请求要记录调用耗时日志,此日志并不是非常重要,不希望影响核心业务流程,要在主流程之外独立处理,因此可以在请求调用完成后由独立线程来记录,处理方式有如下几种:
1.每个请求先将数据放入队列中,由已运行的线程从队列中获取后入库。
2.每个请求启动一个新线程,将数据传给新线程处理入库。
由于线程的创建和切换比较耗资源,因此第2种方式下每个请求都要启动一个新的线程来处理无疑不可行。
在这个例子中,使用方式1处理就可以了,方式一中事先运行的几个线程也可以理解为是线程池,这些线程做的事情都是相同的。
二、例子二
商场开了一个DIY陶器店,每个顾客可以自己制作陶器,根据测算,店里置办了5套制陶工具,十一假期人满为患,制陶工具不够用了,店主就到陶具店临时租了两套,假期结束后,客人少了又还回去,可以看到临时租陶具很耗资源。这个例子可以对应到代码中的线程池,如下:
1.Thread创建和销毁都很耗资源(租陶具和还陶具),因此不能频繁创建和销毁,而是事先准备好几个,这几个就是线程池的初始线程。
2.Thread(陶具)只是一个工具,至于Runnable(顾客)要做什么它不知道。
3.Thread(陶具)可以复用,每个Runnable(顾客)具体要做啥,那是Runnable(顾客)的事情。每个Runnable(顾客)可以做相同的事情(陶器),也可以做不同的事情(陶器),没有限制。
4.如果Thread(陶具)不够,Runnable(顾客)要排队等待。
Java中对应的类结构如下:
1.Executors可以理解为陶器店,通过newFixedThreadPool(nThreads:int)方法置办陶具。 2.ExecutorService刚才例子里没有,这里理解为店小二,负责安排客人。 3.Runnable就是客人了。
下面看代码理解一下。
三、Show me code
I、Customer.java
public class Customer implements Runnable {
//顾客名
private final String customerName;
//制作陶器名
private final String porcelainName;
public Customer(String customerName, String porcelainName) {
this.customerName = customerName;
this.porcelainName = porcelainName;
}
public void run() {
makePorcelain();
}
//制作陶器
private void makePorcelain() {
System.out.println(Thread.currentThread().getName() + ": " + customerName + " made a " + porcelainName + ".");
}
}
II、ThreadPoolTest.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName ThreadPoolTest
* @Description 线程池测试
* @Author 铿然一叶
* @Date 2019/10/9 23:29
* @Version 1.0
* javashizhan.com
**/
public class ThreadPoolTest {
public static void main(String[] args) {
//店铺开张置办了3套陶具,交给店小二打理
ExecutorService executorService = Executors.newFixedThreadPool(3);
//店小二安排顾客制作陶具,陶具不够排队等待
executorService.execute(new Customer("甲", "vase"));
executorService.execute(new Customer("乙", "bowl"));
executorService.execute(new Customer("丙", "plate"));
executorService.execute(new Customer("丁", "teapot"));
//店小二关门打烊
executorService.shutdown();
}
}
输出日志:
pool-1-thread-2: 乙 made a bowl.
pool-1-thread-3: 丙 made a plate.
pool-1-thread-1: 甲 made a vase.
pool-1-thread-2: 丁 made a teapot.
Process finished with exit code 0
1.通过线程名pool-1-thread-X可以看到线程池(店里)只有3个Thread(陶具)。
2.由于陶具不够,顾客丁要等待顾客乙制作完后才能使用陶具。
四、总结
1.两个例子都可以说用到了线程池,在谈论的时候要先理解对方说的是哪一种线程池。
2.例子一线程池里的线程可以理解为全包,你要做啥,线程全帮你做。
3.例子二线程池里的线程就是半包,线程池提供基础设施(工具),要做啥你自己做。
4.如果所有线程要做的事情是固定的,那么采用例子一中的方式基本都能搞定,当每个线程要做的事情不确定时,要使用Executors。当然,也没有限制即使要做的事情是固定的就不能用Executors。
5.有大厂不建议使用Executors来创建线程,原因是可能由于要执行的Runnable(顾客)太多而导致内存溢出,但在例子一中使用数据队列方式处理时,如果数据太多也一样可能溢出,所以,实际使用还要看业务场景,不要因噎废食。
本篇的主要目的是理解线程池概念,这个理解了,Executors的其他用法也就不难理解了。