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

1,855 阅读5分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 46 篇原创文章

相关阅读:

Java并发编程(一)知识地图
Java并发编程(二)原子性
Java并发编程(三)可见性
Java并发编程(四)有序性
Java并发编程(五)创建线程方式概览
Java并发编程入门(六)synchronized用法
Java并发编程入门(七)轻松理解wait和notify以及使用场景
Java并发编程入门(八)线程生命周期
Java并发编程入门(九)死锁和死锁定位
Java并发编程入门(十)锁优化
Java并发编程入门(十一)限流场景和Spring限流器实现
Java并发编程入门(十二)生产者和消费者模式-代码模板
Java并发编程入门(十三)读写锁和缓存模板
Java并发编程入门(十四)CountDownLatch应用场景
Java并发编程入门(十五)CyclicBarrier应用场景
Java并发编程入门(十七)一图掌握线程常用类和接口
Java并发编程入门(十八)再论线程安全
Java并发编程入门(十九)异步任务调度工具CompleteFeature
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

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的其他用法也就不难理解了。

end.

<--阅过留痕,左边点赞!