logo头像
书院的十三先生

Java并发编程入门(十六)正确理解两类线程池

一、例子一

网站的请求要记录调用耗时日志,此日志并不是非常重要,不希望影响核心业务流程,要在主流程之外独立处理,因此可以在请求调用完成后由独立线程来记录,处理方式有如下几种:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.


站点: http://javashizhan.com/


微信公众号: