网站的请求要记录调用耗时日志,此日志并不是非常重要,不希望影响核心业务流程,要在主流程之外独立处理,因此可以在请求调用完成后由独立线程来记录,处理方式有如下几种:
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就是客人了。
下面看代码理解一下。
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 + "."); } } 复制代码
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的其他用法也就不难理解了。
end.
相关阅读:
Java并发编程(一)知识地图
Java并发编程(二)原子性
Java并发编程(三)可见性
Java并发编程(四)有序性
Java并发编程(五)创建线程方式概览
Java并发编程入门(六)synchronized用法
Java并发编程入门(七)轻松理解wait和notify以及使用场景
Java并发编程入门(八)线程生命周期
Java并发编程入门(九)死锁和死锁定位
Java并发编程入门(十)锁优化
Java并发编程入门(十一)限流场景和Spring限流器实现
Java并发编程入门(十二)生产者和消费者模式-代码模板
Java并发编程入门(十三)读写锁和缓存模板
Java并发编程入门(十四)CountDownLatch应用场景
Java并发编程入门(十五)CyclicBarrier应用场景