Java并发编程学习系列四:线程池

简介: Java并发编程学习系列四:线程池

概念

为什么要使用多线程呢?


先从总体上来说:


  • 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。


  • 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。


再深入到计算机底层来探讨:



  • 单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。


  • 多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。


使用多线程可能带来什么问题?


并发编程的目的就是为了能提高程序的执行效率,进而提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题


线程池

池化技术



程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段。


通过学习线程池原理,明白所有池化技术的基本设计思路。遇到其他相似问题可以解决。


池化技术


前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?


池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。


在编程领域,比较典型的池化技术有:线程池、连接池、内存池、对象池等。

主要来介绍一下其中比较简单的线程池的实现原理,通过对线程池的理解,学习并掌握所有编程中池化技术的底层原理。


线程池


线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程:先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又置于睡眠状态,而不是销毁。


Java中线程池的实现


Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors,ExecutorService,ThreadPoolExecutor 这几个类。


1.jpg


Executors三大方法


Executors.newSingleThreadExecutor()

只有一个线程


public class PoolDemo01 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();//有且仅有一个固定的线程
        try {
            for (int i = 0; i < 10; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}
复制代码


执行结果为:


pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
复制代码


从结果可以看出只有一个线程。


Executors.newFixedThreadPool(int)


执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程。


public class PoolDemo01 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);//创建指定个数的线程
        try {
            for (int i = 0; i < 10; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}
复制代码


执行结果为:


pool-1-thread-2
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
复制代码


从结果可以看出,最高有5个线程在执行。


Executors.newCachedThreadPool()


执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。 可扩容,遇强则强。


public class PoolDemo01 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();//该线程池可扩容,遇强则强
        try {
            for (int i = 0; i < 100; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}
复制代码


在本机上最高有 41个线程在执行。


ThreadPoolExecutor七大参数


通过查看上述三大方法的源码,可以发现都是 new 了一个 ThreadPoolExecutor 对象,只是传入的参数有所不同,关于 ThreadPoolExecutor 的构造方法定义如下:


public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
复制代码


参数理解:

  • corePoolSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建  一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  • maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。
  • keepAliveTime:空闲的线程保留的时间
  • unit:空闲线程的保留时间单位
  • BlockingQueue:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。
  • ThreadFactory:线程工厂,用来创建线程,一般默认即可
  • RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值


ThreadPoolExecutor.AbortPolicy    //丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.CallerRunsPolicy    //由调用线程处理该任务
ThreadPoolExecutor.DiscardOldestPolicy    //丢弃队列最前面的任务,然后重新尝试执行任务
ThreadPoolExecutor.DiscardPolicy    //也是丢弃任务,但是不抛出异常。
复制代码


ThreadPoolExecutor底层原理


2.jpg


我们用个案例来进行解析:银行办理业务。比如说目前银行只有两个工作窗口对外开放,有三个空闲位置允许等待,当一下子来了5个人,其中两个人去办理业务,另外三个人去等待。如果人数大于5,则临时开放另外三个工作窗口来处理业务办理。那么该银行最多一次可以接收8个人,其中5个人办理业务,另外3个人等候。


流程图如下:


3.jpg


  1. 在创建了线程池后,开始等待请求。


  1. 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:


  1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务:


  1. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列:


  1. 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;


  1. 如果队列满了且正在运行的线程数量大 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。


  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。


  1. 当一个线程无事可做超过一定的时间(keepA1iveTime)时,线程会判断:
  • 如果当前运行的线程数大于 corePoolSize ,那么这个线程就被停掉。
  • 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。


代码实现为:


public class PoolDemo02 {
    static final int NUM = 8;
    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());//该拒绝策略会抛出异常信息
        try {
            //最大承载:Deque+max=3+5
            for (int i = 1; i <= NUM; i++) {
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}
复制代码


当 NUM 值不大于5时,执行结果为:


pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
复制代码


当 NUM 值大于5,不大于8时,执行结果为:


pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-4
pool-1-thread-5
复制代码


当 NUM 值大于8时,执行会报错,错误信息如下:


java.util.concurrent.RejectedExecutionException
复制代码


如何设置参数


我们在上面的学习中使用 Executors 来创建线程池,但是实际工作中我们并不会这样做,需要自定义来创建线程池,如阿里巴巴开发手册中所提到的:


4.jpg


虽然建议创建线程池不要使用 Executors,但是与它关联很紧密,我们入门阶段还是要从它下手。


线程池是否越大越好?


一个计算为主的程序(专业一点称为 CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu 核心,比如说4个核心的 cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。


但是如果线程远远超出 cpu 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 cpu 密集型的任务来说,线程数等于cpu数是最好的了。

如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。


此时 线程数等于 IO任务数是最佳的。



目录
相关文章
|
1天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
1天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
15 1
|
8天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2天前
|
Java API 数据库
Java 反射机制:动态编程的 “魔法钥匙”
Java反射机制是允许程序在运行时访问类、方法和字段信息的强大工具,被誉为动态编程的“魔法钥匙”。通过反射,开发者可以创建更加灵活、可扩展的应用程序。
|
6月前
|
Java C++
关于《Java并发编程之线程池十八问》的补充内容
【6月更文挑战第6天】关于《Java并发编程之线程池十八问》的补充内容
49 5
|
3月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
5月前
|
安全 Java 开发者
Java中的并发编程:深入理解线程池
在Java的并发编程中,线程池是管理资源和任务执行的核心。本文将揭示线程池的内部机制,探讨如何高效利用这一工具来优化程序的性能与响应速度。通过具体案例分析,我们将学习如何根据不同的应用场景选择合适的线程池类型及其参数配置,以及如何避免常见的并发陷阱。
57 1
|
5月前
|
监控 Java
Java并发编程:深入理解线程池
在Java并发编程领域,线程池是提升应用性能和资源管理效率的关键工具。本文将深入探讨线程池的工作原理、核心参数配置以及使用场景,通过具体案例展示如何有效利用线程池优化多线程应用的性能。
|
4月前
|
Java 数据库
Java中的并发编程:深入理解线程池
在Java的并发编程领域,线程池是提升性能和资源管理的关键工具。本文将通过具体实例和数据,探讨线程池的内部机制、优势以及如何在实际应用中有效利用线程池,同时提出一个开放性问题,引发读者对于未来线程池优化方向的思考。
43 0
|
6月前
|
监控 Java 调度
Java并发编程:深入理解线程池
【6月更文挑战第26天】在Java并发编程的世界中,线程池是提升应用性能、优化资源管理的关键组件。本文将深入探讨线程池的内部机制,从核心概念到实际应用,揭示如何有效利用线程池来处理并发任务,同时避免常见的陷阱和错误实践。通过实例分析,我们将了解线程池配置的策略和对性能的影响,以及如何监控和维护线程池的健康状况。
40 1
下一篇
无影云桌面