Java多线程基础-11:工厂模式及代码案例之线程池(二)

简介: 这篇内容介绍了Java多线程基础,特别是线程池中的定时器和拒绝策略。

Java多线程基础-10:代码案例之定时器(一) +https://developer.aliyun.com/article/1520548?spm=a2c6h.13148508.setting.14.75194f0ethWdBZ


4、标准库提供的4种拒绝策略⭐


下面是标准库提供的四种拒绝策略。



  • ThreadPoolExecutor.AbortPolicy 直接抛异常:如果线程池满了还在继续加任务,添加操作就直接抛出异常,新任务和老任务都执行不了了。(你一个礼拜周一到周日满课,天天早八到晚十,结果班长还要求你去打扫办公室。你一听这个消息,直接绷不住了,课也不上了,哇得一声嚎啕大哭。)


  • ThreadPoolExecutor.CallerRunsPolicy 添加任务的线程自己负责执行这个任务,即在哪个线程中写了submit()或execute()等向线程池中添加任务的方法,就由哪个线程来执行它要添加的任务。(你直接怼回去:我才不去呢,要去你自己去。)


  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老的任务,即阻塞队列队首元素,不执行了,直接删除。(你看了一下课表,决定把最早要上的这一节课鸽了,去打扫。)


  • ThreadPoolExecutor.DiscardPolicy 丢弃最新的任务,只做原来的任务。(你还是继续上你的课,打扫办公室这个任务就直接丢弃了。)


注意,这里线程池并没有依赖于阻塞队列的阻塞行为,而是通过额外实现其它逻辑来更好地处理这个场景的操作。就好比班长告诉你你要去打扫卫生,然后他就阻塞住了,他也干不了别的你也干不了别的……最好的情况应当是你立即给出答复。在线程池中,并不希望依赖“满了阻塞”,而更主要是利用“空了阻塞”。


关于各个拒绝策略的具体场景,可以参考这篇文章:


🔗线程池的拒绝策略


三、代码实现线程池


下面代码实现了一个固定线程数的简单的线程池:


import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
 
class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 
    //提交任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
 
    //实现一个固定线程数的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        //取出线程池中的一个任务
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;    //lambda表达式的变量捕获规则
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}


运行结果:




打印1000次


1、代码解析


核心的数据结构是BlockingQueue,它用于存放各个可执行的任务(runnable):


class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 
    //...
}


submit()就像生产者一样,给队列中添加任务:

1.
class MyThreadPool{
    //向线程池中提交任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
 
    //...
}


在线程池内部有一组总数为n的工作线程,它就像消费者一样,不停地从队列中取任务然后执行。


主线程中传入n为10,即线程数固定为10。在线程池构造方法中,通过for循环创建了10个工作线程。这10个工作线程是并发执行,无序调度的。每一个线程的任务都是不断从阻塞队列中获取任务并执行。因此这里为了保证工作线程的活跃,不会在执行完一个任务后立即终止线程,需要给取任务、执行任务的操作加上while(true)。如果没有while(true),线程执行完一个任务后就会终止,导致线程池中的线程数量不足,无法处理后续的任务(运行结果中只能打印10次)。


class MyThreadPool{
    //实现一个固定线程数的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        //取出线程池中的一个任务
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
 
    //...
}


主线程中,创建出线程池,并通过循环向其中添加1000个任务。这1000个任务先后被添加到阻塞队列中,工作线程从阻塞队列中获取任务并执行。注意,这里num的创建是由于lambda表达式(或匿名内部类)的变量捕获规则,它要求lambda表达式中捕获到的变量必须是final或实际final的(即不能被更改),由于变量 i 被更改了,因此重新创建一个变量来保存 i,代替 i 来使用:


public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}


2、实际开发中如何给线程池设置合适的线程数量


实际不同的程序中,线程需要干的活大不相同。一个线程池的线程数量设置成几是比较合适的?这需要结合具体的任务情况,测试而定。


CPU密集型任务,主要做一些计算工作,要在 CPU 上运行。假设一个极端情况,如果你的线程执行的全是使用CPU资源的任务,那么线程数就不应该超过CPU的核心数(指逻辑核心)。

IO密集型任务,主要是等待 IO 操作(等待读写硬盘,读写网卡等),不怎么消耗 CPU 资源。如果你的线程全是使用IO,线程数就可以设置很多,远远超出 CPU 核心数。

不过,实践中很少有这么极端的情况,具体要通过测试的方式来确定,选取一个性能上恰当且资源使用上也恰当的这样一个均衡的结果。


测试的大体思路是:运行程序,通过记录时间戳计算一下执行时间(平均值),同时监测资源使用状态。


四、总结:线程池的执行流程


1. 任务提交


当有任务需要执行时,将任务提交给线程池。


2. 队列处理


线程池会将提交的任务放入任务队列中。任务队列是一个缓冲区,用于存储等待执行的任务。不同类型的线程池可能使用不同的任务队列。


3. 线程调度


线程池中的线程会从任务队列中获取任务。线程池会根据配置的核心线程数、最大线程数、线程空闲时间等参数来决定是否创建新的线程,或者复用空闲的线程来执行任务。


4. 任务执行


线程池中的工作线程会执行从任务队列中获取的任务。每个线程执行一个任务后,会继续从任务队列中获取下一个任务,直到线程池关闭或者没有更多的任务可以执行。


5. 线程回收


如果线程池中的线程空闲时间超过设定的时间(线程空闲时间设置的过程中),则线程会被回收,以减少资源消耗。但是,核心线程不会被回收,线程池会保持至少核心线程数的线程处于运行状态。


6. 线程池关闭


当不再需要线程池时,应该显式地关闭线程池,释放相关资源。关闭线程池后,线程池将不再接受新的任务,并且会等待所有已提交的任务执行完毕。


总结来说,线程池的执行流程涉及任务提交、队列处理、线程调度、任务执行和线程回收等步骤。通过线程池的管理,我们可以更好地控制线程的创建和销毁,提高程序的性能和效率,并避免因为频繁创建和销毁线程而导致的资源浪费和性能下降。


相关文章
|
2天前
|
Java
Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
10 4
|
1天前
|
Java
Java多线程中notifyAll()方法用法总结
Java多线程中notifyAll()方法用法总结
|
19小时前
|
缓存 安全 Java
如何使用Java实现高效的多线程编程
如何使用Java实现高效的多线程编程
|
19小时前
|
安全 Java 机器人
Java中的多线程编程实用指南
Java中的多线程编程实用指南
|
1天前
|
Java
java使用多线程编写服务端与客户端文件上传程序
java使用多线程编写服务端与客户端文件上传程序
5 0
|
1天前
|
Java
java使用匿名内部类实现多线程
java使用匿名内部类实现多线程
6 0
|
1天前
|
安全 Java 开发者
Java中的多线程编程实用指南
Java中的多线程编程实用指南
|
29天前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
40 2
|
1月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
28 1
|
1月前
|
Java 调度
Java多线程:什么是线程池(ThreadPool)?
Java多线程:什么是线程池(ThreadPool)?
59 0