先从总体上来说:
再深入到计算机底层来探讨:
并发编程的目的就是为了能提高程序的执行效率,进而提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如: 内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题 。
程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段。
通过学习线程池原理,明白所有池化技术的基本设计思路。遇到其他相似问题可以解决。
前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?
池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。
在编程领域,比较典型的池化技术有: 线程池、连接池、内存池、对象池等。
主要来介绍一下其中比较简单的线程池的实现原理,通过对线程池的理解,学习并掌握所有编程中池化技术的底层原理。
线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程:先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又置于睡眠状态,而不是销毁。
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors,ExecutorService,ThreadPoolExecutor 这几个类。
只有一个线程
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 复制代码
从结果可以看出只有一个线程。
执行长期任务性能好,创建一个线程池,一池有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个线程在执行。
执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。 可扩容,遇强则强。
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个线程在执行。
通过查看上述三大方法的源码,可以发现都是 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; } 复制代码
参数理解:
ThreadPoolExecutor.AbortPolicy //丢弃任务并抛出RejectedExecutionException异常 ThreadPoolExecutor.CallerRunsPolicy //由调用线程处理该任务 ThreadPoolExecutor.DiscardOldestPolicy //丢弃队列最前面的任务,然后重新尝试执行任务 ThreadPoolExecutor.DiscardPolicy //也是丢弃任务,但是不抛出异常。 复制代码
我们用个案例来进行解析:银行办理业务。比如说目前银行只有两个工作窗口对外开放,有三个空闲位置允许等待,当一下子来了5个人,其中两个人去办理业务,另外三个人去等待。如果人数大于5,则临时开放另外三个工作窗口来处理业务办理。那么该银行最多一次可以接收8个人,其中5个人办理业务,另外3个人等候。
流程图如下:
在创建了线程池后,开始等待请求。
当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务:
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列:
如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了且正在运行的线程数量大 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做超过一定的时间(keepA1iveTime)时,线程会判断:
代码实现为:
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 来创建线程池,但是实际工作中我们并不会这样做,需要自定义来创建线程池,如阿里巴巴开发手册中所提到的:
虽然建议创建线程池不要使用 Executors,但是与它关联很紧密,我们入门阶段还是要从它下手。
一个计算为主的程序(专业一点称为 CPU密集型程序 )。多线程跑的时候,可以充分利用起所有的cpu 核心,比如说4个核心的 cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
但是如果线程远远超出 cpu 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 cpu 密集型的任务来说, 线程数等于cpu数 是最好的了。
如果是一个磁盘或网络为主的程序( IO密集型 )。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。
此时 线程数等于 IO任务数 是最佳的。