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

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

多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)+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
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
74 0
|
1天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
3天前
|
安全 算法 Java
JavaSE&多线程&线程池
JavaSE&多线程&线程池
17 7
|
28天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解08-阻塞队列之LinkedBlockingDeque
**摘要:** 本文分析了Java中的LinkedBlockingDeque,它是一个基于链表实现的双端阻塞队列,具有并发安全性。LinkedBlockingDeque可以作为有界队列使用,容量由构造函数指定,默认为Integer.MAX_VALUE。队列操作包括在头部和尾部的插入与删除,这些操作由锁和Condition来保证线程安全。例如,`linkFirst()`和`linkLast()`用于在队首和队尾插入元素,而`unlinkFirst()`和`unlinkLast()`则用于删除首尾元素。队列的插入和删除方法根据队列是否满或空,可能会阻塞或唤醒等待的线程,这些操作通过`notFul
44 5
|
28天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解07-阻塞队列之LinkedTransferQueue
`LinkedTransferQueue`是一个基于链表结构的无界并发队列,实现了`TransferQueue`接口,它使用预占模式来协调生产者和消费者的交互。队列中的元素分为数据节点(isData为true)和请求节点(isData为false)。在不同情况下,队列提供四种操作模式:NOW(立即返回,不阻塞),ASYNC(异步,不阻塞,但后续线程可能阻塞),SYNC(同步,阻塞直到匹配),TIMED(超时等待,可能返回)。 `xfer`方法是队列的核心,它处理元素的转移过程。方法内部通过循环和CAS(Compare And Swap)操作来确保线程安全,同时避免锁的使用以提高性能。当找到匹
35 5
|
28天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解04-阻塞队列之PriorityBlockingQueue原理及扩容机制详解
1. **继承实现图关系**: - `PriorityBlockingQueue`实现了`BlockingQueue`接口,提供了线程安全的队列操作。 - 内部基于优先级堆(小顶堆或大顶堆)的数据结构实现,可以保证元素按照优先级顺序出队。 2. **底层数据存储结构**: - 默认容量是11,存储数据的数组会在需要时动态扩容。 - 数组长度总是2的幂,以满足堆的性质。 3. **构造器**: - 无参构造器创建一个默认容量的队列,元素需要实现`Comparable`接口。 - 指定容量构造器允许设置初始容量,但不指定排序规则。 - 可指定容量和比较
42 2
|
28天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解03-阻塞队列之LinkedBlockingQueue
LinkedBlockingQueue 和 ArrayBlockingQueue 是 Java 中的两种阻塞队列实现,它们的主要区别在于: 1. **数据结构**:ArrayBlockingQueue 采用固定大小的数组实现,而 LinkedBlockingQueue 则使用链表实现。 2. **容量**:ArrayBlockingQueue 在创建时必须指定容量,而 LinkedBlockingQueue 可以在创建时不指定容量,默认容量为 Integer.MAX_VALUE。 总结起来,如果需要高效并发且内存不是主要考虑因素,LinkedBlockingQueue 通常是更好的选择;
33 1
|
28天前
|
Java 测试技术 Python
Python开启线程和线程池的方法
Python开启线程和线程池的方法
17 0
Python开启线程和线程池的方法
|
18天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
30天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3