JUC并发编程
1. JUC线程池
-
概述和架构
- 通过线程池可以创建线程
- 线程池就是控制多个线程,将要执行的任务放到任务队列中,然后找空闲的线程去执行这些任务,如果线程数量超过了最大数量,则会等待其他任务执行结果后再来执行队列中的任务
- 减少每次创建和销毁线程的开销,而是复用一个线程;线程池维护着多个线程,从阻塞队列任务队列中取出任务由空闲线程进行执行
- 通过重复使用已经创建的线程来降低创建和销毁线程的开销;且可以提高任务响应速度,无需创建线程就可以使用已经创建好的线程立即执行
- Java线程池通过Executor接口来实现,有多个实现类,且还有一个Executors工具类来创建单一、定长和可变的线程池,但不建议使用,因为阻塞队列的长度均为MAX,此时容易造成请求堆叠,从而导致OOS错误
-
使用方式
- 一池N线程,创建线程池时固定线程数量:Exectors.newFixedThreadPool(int)
- 一池一线程,一个任务一个任务依次执行:Executors.newSingleThreadExecutor()
- 根据需求创建线程,可以扩容:Executors.newCachedThreadPool()
- 根据具体的应用场景来通过Executors工具类创建具体的线程池ExecutorService对象
- 通过线程池对象.execute(Runnable)方法来申请线程执行操作,要传入一个Runnable实现类对象重写run()指定线程的操作,只有execute()时才会创建线程,此时会从核心线程中申请一个线程,当没有时会放入阻塞队列,当阻塞队列满时,就会判断是否可以扩容,如果不可以则拒绝策略
- 当使用完线程后,通过shutdown()方法将线程放回线程池
- 先创建一个线程池对象,通过execute(Runnable)传入一个要执行的线程操作来申请线程执行,使用完之后使用 shutdown() 操作将申请的线程返回到线程池中
- 可扩容的线程池会根据当前申请线程的任务数量进行自动扩容,且会自动缩容
-
底层原理
- 使用Exceutors创建的线程池底层都是ThreadPoolExecutor对象,传入七个不同的参数来创建线程池,但此时创建的阻塞队列都是MAX,故会堆积请求,故不允许使用Exceutors来创建线程,而是通过创建ThreadPoolExecutor对象指定参数来创建线程池
- 固定数量线程池和单个线程池和自动扩容线程池底层都是ThreadPoolExecutor对象,只是传入不同的参数来实现不同的功能
- ThreadPoolExecutor对象的7个参数说明
- corePoolSize:线程池中核心线程的数量,即常驻线程数量,即不会销毁的线程数量
- maximumPoolSize:线程池中可以创建的最大线程池数量,当大于核心线程数时说明可以进行线程扩容,且扩容的线程会被销毁,有一个存活时间
- keepAliveTime:线程存活时间,当线程超过存活时间内未使用,则会销毁该线程(核心线程不会销毁)
- unit:线程存活时间的单位
- workQueue:工作队列,是阻塞队列类型,内部存放申请线程的任务,当常驻现场用完时,此时再次请求线程时任务就会进入阻塞队列中,等待线程结束或者创建新的线程来取出执行;而线程空闲时会从阻塞队列中取任务,如果没有任务则线程阻塞
- ThreadFactory:创建线程的工厂类对象
- handler:拒绝策略,当线程均无空闲时对任务进行拒绝的策略,当阻塞队列满时且无法创建新线程时就会拒绝
-
底层工作流程
-
当执行execute()时线程池才会在底层创建线程
-
先去常驻核心线程中申请线程,如果没有则将任务放入阻塞队列,当核心线程均被占用时,此时新的任务会进入阻塞队列中,当阻塞队列满时会进行线程扩容,如果不能扩容则直接根据拒绝策略拒绝
-
然后线程池会根据阻塞队列中任务的数量来创建别的非核心线程进行处理(只有运行扩容的才会创建),且当空闲超过存活时间时,会将该线程销毁,扩容的线程有存活时间
-
当阻塞队列也满时,就会对当前的请求进行拒绝策略,
-
创建线程池后不会立即创建线程,只有执行execute()后才会创建线程,此时会申请线程执行操作
-
当核心线程池满了会进入阻塞队列,且阻塞队列中的任务等待核心线程的执行,当阻塞队列也满了时会根据能否扩容进行创建新线程,如果不可以扩容,则会执行拒绝策略
-
JDK内置的拒绝策略,默认直接抛出异常,也可以抛弃阻塞队列中等待最久的任务,将当前任务加入;还可以不做处理且不抛出异常
-
-
自定义线程池(实际使用)
- 通过Executors创建的线程池,有单一、定长和可变三种类型,但都存在一定问题,因为此时创建的线程池的阻塞队列默认都是Integer.MAX_VALUE长度,即阻塞队列会很长,导致任一个线程任务均会进行等待,可能会堆积大量的请求,造成OOM异常
- 不允许使用Executors工具类去创建线程池,必须使用ThreadPoolExecutor方式去创建自定义线程池
- 必须使用ThreadPoolExecutor类自定义创建线程池
- 可以自己创建ThreadPoolExecutor类来传入7个参数自定义线程池
- 通过Executors工具类创建的线程对象底层都是创建ThreadPoolExecutor对象,传入不同的七个参数来实现不同类型的线程池
- 此时我们可以自己创建一个ThreadPoolExecutor对象,根据功能传入不同的参数来实现不同的功能
- 当创建线程池后不会立即创建线程,只有调用execute()才会创建线程,且会先申请核心线程,当核心线程满了则直接加入阻塞队列,当阻塞队列满了则会去申请扩容线程,如果不可以扩容则会直接拒绝策略(直接抛出异常、找调用者、从阻塞队列中选择时间最长的删除加入阻塞队列、不做处理也不抛出异常)
- 且只有最大线程数量大于核心线程数量时才会进行扩容,否则不可以扩容,阻塞队列满了才会去创建新的线程,且扩容的线程有存活时间,当超过存活时间时就会被销毁
- 通过内部的线程工厂来创建新线程
- 线程池的拒绝策略有四种:直接抛出异常、去找调用者、删除阻塞队列中等待时间最长的线程再加入、不做处理也不抛出异常
2. Fork/Join分支合并框架
- 概述
- 分支合并框架是将一个大任务拆分Fork为多个子任务并行处理,然后将结果合并Join,此时可以提高任务执行效率
- 分支合并框架可以将一个大任务拆分为多个子任务并行处理,最后将子任务结果合并成最后的计算结果并进行输出
- 将一个大任务拆分为多个子任务并行处理,最后将子任务合并成最后的计算结果并输出
- Fork就是拆分,Join就是合并
- 将一个大任务Fork拆分为多个小任务,并行处理后再合并Join结果
- 可以通过Executor接口的实现类,ForkJoinPool实现类来进行实现分支合并
- 还可以通过Future的实现类ForkJoinTask来进行实现
- 分支合并框架就是将一个大任务Fork为多个子任务并行处理,最后Join合并结果
- 案例实现
- 创建一个类继承RecursiveTask<>类,并重写compute()方法来实现,此时在compute()内实现拆分合并计算操作,如果要拆分则重新创建一个当前类对象,然后调用fork()操作进行拆分,最后使用join()函数获得拆分函数的结果进行合并
- compute()函数返回值就是拆分函数返回的结果
- 在使用时先创建一个ForkJoinPool对象,然后调用submit()传入创建的类对象,此时通过get()就可以获得分支合并任务的结果
3. CompletableFuture异步回调
- CompletableFuture<>类实现了Future接口,泛型中指定返回值的类型,如果没有返回值则指定为Void
- 有返回值的异步方法supplyAsync(Runnable),此时会返回CompletableFuture<>,通过泛型指定返回值类型,且传入Runnable函数实现类中的run()要return返回值,且此时不可以直接get()执行,而是通过whenComplete()方法,传入函数式接口对象(使用lambda表达式创建一个两个参数(一个是返回值v,一个是异常u)的对象),当完成后再使用get()处理对结果进行处理
- 没有返回值的异步方法runAsync(Runnable),此时返回的是CompletableFuture< Void >对象,通过返回对象的get()方法来执行异步任务,且方法中要传入一个Runnable接口的实现类