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

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

多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)+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());
    }

讲解:

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

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

目录
相关文章
|
1天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
4天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
3天前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
8天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
14天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
25天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
3月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
27天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
20天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
37 1
|
2月前
|
设计模式 Java Kotlin
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。对于快速学习Kotlin语法,推荐查看“简洁”系列教程。本文重点介绍了构建者模式在Kotlin中的应用与改良,包括如何使用具名可选参数简化复杂对象的创建过程,以及如何在初始化代码块中对参数进行约束和校验。
25 3