多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)

简介: 多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)

多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)+https://developer.aliyun.com/article/1413588

3.java标准库内部的线程池

1.基本概念

java标准库内部其实实现了线程池,线程池被封装成了一个类ThreadPoolExecutor

创建出一个线程池

// 向上转型
        ExecutorService service = Executors.newCachedThreadPool();

\ \ 此处线程池的创建并没有通过常规的new关键字创建,而是调用了Executors内部的一个方法来创建线程池对象,这种创建对象的方式我们称之为工厂模式,工厂模式也是设计模式的一种,工厂模式的存在主要是为了解决构造方法缺陷,有时候我们对一个类的实现希望其有多种方式,而实现需要通过构造方法来创建,由于构造方法的方法名只能是类名,这就带来了一些使用上的局限性,请看下图

同理,对于ThreadPoolExecutor的创建来说,他也有很多种实现方式,为了更好的调用,此处就采用工厂模式进行创建(可以将对象的创建方法总结为以下两种)

2.ThreadPoolExecutor的实现方式

ThreadPoolExecutor一般有四种实现方式:

前两种实现方式比较常用,后两种实现方式不常用,了解即可

3.ThreadPoolExecutor的核心方法

核心方法就两个:

  1. 构造方法
  2. 任务提交方法
1.任务提交

任务提交方法比较简单,创建好ThreadPoolExecutor对象之后使用submit方法进行任务的提交,交给线程池内部线程去执行提交的任务

service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务提交");
            }
        });

更重要的是ThreadPoolExecutor的构造方法,这也是面试常考的!!!

2.构造方法(重点)

进入到javase的标准文档,查看ThreadPoolExecutor的构造方法

一共有四个构造方法,但实际上前三种都是简化版本,省去了一些参数,第四种是最全的构造方法,这里重点掌握第四种方法

依次来看第四种构造方法的参数

**int corePoolSize, int maximumPoolSize**

corePoolSize:核心线程的数目

maximumPoolSize:线程池内部最多持有的线程数目

什么是核心线程呢?对于一个线程池来说,其内部存储的线程分为两类:

  1. 核心线程
  2. 非核心线程

核心线程是一个线程池内部始终持有的线程,无论任务有多少,核心线程的数目始终固定不变;非核心线程不是线程池始终持有的,可以根据要执行任务的多少添加或删除,当任务多时,就新建几个非核心线程去应对高任务量,任务少时就删除几个非核心线程.

可以把核心线程想象为一个公司的正式员工,而非核心线程就是实习生,对于正式员工来说,是不能随便删除的(因为劳动法~),而实习生是可以随便开除的,当任务多时,就多招几个实习生来帮我干活,任务少了,就开除这些实习生(老铁扎心了吧)

核心线程保证了低负载情况下任务的正常运行,非核心线程可以有效应对高负载的情况

**long keepAliveTime, TimeUnit unit**

keepAliveTime:非核心线程在空闲状态下的存活时间

unit:时间单位

对于非核心线程来说,如果在一定时间内处于空闲状态,没有执行任务,系统就会讲这些空闲的非核心线程销毁,节省系统资源,keepAliveTime就是规定最多空闲时间是多少,TimeUnit unit是空闲时间的单位,TimeUnit 是一个枚举类型,里面存放时间的的单位(秒/分/时)

比如:keepAliveTime是5,unit为TimeUnit.SECONDS,这意味着非核心线程在空闲5s之后就会被销毁

**BlockingQueue<Runnable> workQueue**

workQueue:用于存放要执行任务的阻塞队列,等待线程池中的线程从阻塞队列中获取相应的任务并执行,此时用户端就是生产者,执行任务的线程池就是消费者.队列中的元素就是要执行的任务Runnable

不同的阻塞队列的使用场景也不同,主要考虑容量限制和阻塞策略,可以根据不同队列的性质进行选择

**ThreadFactory threadFactory**

threadFactory:通过工厂模式创建出来的定制化的线程

ThreadFactory 是一个接口,只有一个方法,用于创建自定义的线程

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

也就是ThreadFactory threadFactory这个参数就是让我们为线程池提供自己创建的自定义的线程,以下是一个简单的使用例子

// 创建自定义的线程  先让其实现ThreadFactory接口
class MyCustomThread implements ThreadFactory {
    // 设置属性  自定义线程的前缀
    private final String threadNamePrefix = "MyCustomThread - ";
    // 自定义线程的编号
    private int threadCnt = 0;
    @Override
    public Thread newThread(Runnable r) {
        // 规定线程要执行的任务
        Thread t = new Thread(r);
        t.setName(threadNamePrefix + ++threadCnt);
        return t;
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyCustomThread myCustomThread = new MyCustomThread();
        // 利用自定义线程创建出线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5,myCustomThread);
        // 提交任务
        for (int i = 0; i < 5; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    // 打印当前正在执行任务的线程名称
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

打印结果:

注意:因为在多线程编程中,线程的调度是随机的,所以每次打印的结果也是不同的

**RejectedExecutionHandler handler**

handler:线程池的拒绝策略 对于一个线程池来说,其能容纳的线程数量是有限的,当超过最大的容量时,线程池会有一定的拒绝策略来阻止容量超过最大的限制,不同的拒绝策略有不同的效果,具体来说有以下四种拒绝策略:

  1. 直接抛出异常(我就是不让你超过限制,一超过限制就报错)这种策略是默认策略
  2. 丢弃当前新加的任务(添加进来新的任务就抛弃)
  3. 丢弃任务队列中最老的任务(老弱病残终究会被淘汰的)
  4. 添加的任务由添加任务的线程负责执行(这样做是为了尽量不丢失任务,添加任务的线程不是线程池中的线程,哪个线程往线程池中提交的任务就交给谁执行)

    以上就是ThreadPoolExecutor类构造方法所有参数的讲解,其中corePoolSizeRejectedExecutionHandler是面试中最常考的!!!

4.线程池的模拟实现

如何去模拟实现一个线程池呢?需要先清楚了解线程池的基本组成,线程池由以下三部分组成

  1. BlockingQueue:任务队列 用于存放线程池中线程要执行的任务
  2. 线程池:线程池的核心主体还是多个用于执行任务的线程
  3. submit方法:用于提交任务,用于连接任务队列和线程池

代码实现:

class MyThreadPool {
    // 创建一个任务队列  用于存放线程池要执行的任务  10代表次任务队列最多存放的任务数量是10  超过10就要阻塞等待
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
    // 创建提交方法  将任务提交到任务队列之中
    public void submit(Runnable runnable) throws InterruptedException {
        // 此处采用的拒绝策略就是使用阻塞队列  队列满 阻塞等待
        queue.put(runnable);
    }
    // 创建构造方法
    public MyThreadPool(int n) {
        // 创建n个线程  就相当于newFixedPool的效果 创建出制定容量的线程池
        for (int i = 0; i < n; i++) {
            // 线程池中的线程是要执行任务的  获取任务队列中的任务 执行
            Thread t = new Thread(() -> {
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 10; i++) {
            final int id = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程执行id" + id);
                }
            });
        }
    }
}

运行结果:

5.线程数量如何决定?

在使用线程池的时候,如何确定线程池内部的线程数量呢?在网上其实有很多种说法,假设cpu的逻辑核心数是N,线程的数目可以设置为N,N-1,N-2,2N,1.5N等等,其实这些说法都不准确,再你没有接触到实际的项目之前,线程的数目是不可能确定的

我们要执行的代码可以分为以下两类:

  1. cpu密集型:代码中大量存在需要进行算术运算和逻辑判断
  2. I/O密集型:代码涉及到I/O操作

假设一个代码中都是cpu密集型的代码(很吃cpu资源),cpu的逻辑核心数是N,那你设置的线程数目最多只能是N,一个线程的执行对应着一个cpu逻辑核心,如果创建的线程数目比逻辑核心N还多,就没有cpu来执行多出的线程了,反而造成了资源的浪费

假设一个代码中全是I/O密集型的代码,此时线程池中的线程数目是可以大于N的,因为一个cpu上可以调度执行这些操作~I/O操作不吃cpu

对于我们所写的代码来说,我们不知道有多少cpu密集型的,有多少I/O密集型的,也就无法直接确定设置的线程数目,正确方法应该是通过性能测试来确定线程的数量,即不断地更换线程数,看什么情况下能达到性能的最优化,通过测试找出应设置的线程数

补充:I/O操作是指计算机系统与外部链接设备之间的数据传输,常见的I/O操作包括文件读取,数据库连接,网络通信等等

6.线程池构造方法的进一步认识

1.newFixedThreadPool

源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

讲解

2.newCachedThreadPool

源码:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

讲解:

3.newSingleThreadExecutor

源码:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

讲解:

4.ScheduledThreadPoolExecutor

源码:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

讲解:

最后附上多线程设计模式的思维导图

创作不易!!!欢迎大家多多转发支持!

目录
相关文章
|
5月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
193 0
|
5月前
|
设计模式 运维 监控
并发设计模式实战系列(4):线程池
需要建立持续的性能剖析(Profiling)和调优机制。通过以上十二个维度的系统化扩展,构建了一个从。设置合理队列容量/拒绝策略。动态扩容/优化任务处理速度。检查线程栈定位热点代码。调整最大用户进程数限制。CPU占用率100%
336 0
|
8月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
136 26
|
8月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
139 17
|
10月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
600 2
|
10月前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
145 0
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
4月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
251 5
|
8月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
268 20