线程池
概述
1、ExecutorService本质上是一个线程池(线程的池化技术)
2、线程池的意义:减少线程的创建和销毁,减少开销,做到线程的复用。
3、线程池刚创建的时候,里面是没有任何线程的
4、当过来一个新任务的时候,线程池会创建一个核心线程(core Thread)来处理这个任务。在创建线程池的时候需要给定核心线程的数量。
5、核心线程执行完任务之后不会销毁,而是等待下一个任务
6、当核心线程数还没有达到指定的核心线程数的时候,不管有没有空闲的核心线程,每来一个任务都会创建一个新的核心线程,这样可以使得线程数量快速的达到核心线程数量。
7、当核心线程数已满并且都被占用的时候,提交的任务将会进行到阻塞式队列中,工作队列(work queue)中
8、当工作队列也别占满,再次提交任务时,创建一个临时线程(temporary Thread)来执行任务。临时线程的数量也需要在创建线程池的时候就指定。
9、当临时线程的任务执行完毕,会存活一段时间(keepAlivetime),临时线程不会去工作队列中拿去任务,如果在等待的这段时间里没有任务提交到临时线程的话,时间到了之后临时线程会被销毁
10、即使临时线程空闲,也不会处理工作队列中的任务,这样可以尽快的销毁掉临时线程,保证核心线程的复用
11、当核心线程已满,工作队列已满,临时线程已满时,再次提交任务,线程池就是拒绝执行该任务并且执行拒绝策略。
如图:
拒绝策略中的Runnable 参数,就代表被线程池拒绝执行的线程,可以在这里记录日志,或者一些其他的操作。
创建线程池
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, //核心线程数+临时线程数
long keepAliveTime,//临时线程存活时间
TimeUnit unit,//存活时间单位
BlockingQueue<Runnable> workQueue,//阻塞式队列,工作队列
ThreadFactory threadFactory,//线程工厂,使用默认的就行Executors.defaultThreadFactory()
RejectedExecutionHandler handler) //拒绝策略
演示代码
package executorservice;
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService es = new ThreadPoolExecutor(
7, // 7个核心线程
15, // 7个核心线程 + 8个临时线程
5, TimeUnit.SECONDS, // 表示临时线程用完之后存活5s,如果超过5s没有接到新的任务就会销毁
new ArrayBlockingQueue<Runnable>(5), // 工作队列
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 实际过程中,拒绝会根据不同场景有对应的流程的
System.out.println("拒绝执行" + r);
}
}
);
// 可以将这个任务交给线程池执行
for (int i = 0; i < 22; i++) {
es.execute(new ESThread());
}
// 关闭线程池
// 实际开发中,线程池应该是开了之后就不再关闭
es.shutdown();
}
}
class ESThread implements Runnable {
@Override
public void run() {
try {
System.out.println("开始执行");
TimeUnit.SECONDS.sleep(3);
System.out.println("执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Executors线程池工具类
创建线程池时,不需要每次都指定多个参数才能够创建线程池,没必要怎么麻烦,java已经提供好了一些方法能够快速的创建线程池
newCachedThreadPool()
特点:
1、没有核心线程,全部都是临时线程
2、临时线程的数量是Integer.MAX_VALUE,即2^31-1,等价于临时线程的数量无穷大,一台服务器能够并发的线程数量是有限的,大概在15W到20W之间
3、临时线程的存活时间是60s
4、work Queue是SynchronousQueue同步队列,容量是1,生产中一般会预先放置一个任务吧这个队列占满
工作机制:
由于这种队列的特点,说明无论来多少个请求,都会创建临时线程用于任务处理,而且是没有等待的。并且临时线程的存活60s之后会被销毁。
适用于:
大池子小队列
这种线程池适合于短时的高并发处理,每个任务的处理时长很短,但是又具有高并发的特点。例如 短时通讯
不适用于长时任务,因为你的线程数量没做限制,如果线程长时间被占用,很可能会创建出很多很多的线程导致服务器崩溃。
编码:
"同时提交了100个线程,打印结果100个同时出现"
@Test
public void testCacheThreadPool() throws IOException {
ExecutorService pool = Executors.newCachedThreadPool();
for(int i =0;i<100;i++){
pool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(3000);
System.out.println("任务接受");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
System.in.read();//防止主线程退出
}
newFixedThreadPool()
线程数固定线程池。
特点:
1、没有临时线程,全部都是核心线程
2、工作队列是LinkedListBlockingQueue,长度等价无穷大,可以缓存很多任务。
工作机制:
因为没有临时线程只有核心线程,所以所有的任务都将交给核心线程进行处理,处理不过来的全部放入工作队列当中。
这种线程池可以控制同时工作的线程数,控制并发数量。
适用于:
小池子大队列
适用于那些处理时间比较长的,并发比较低的业务场景,例如:百度云盘下载,迅雷下载任务等,添加多个下载任务时,也只能开始几个任务
不适用于高并发短任务
编码
"任务3个3个的执行完毕"
@Test
public void testFixedPool() throws IOException {
ExecutorService serv = Executors.newFixedThreadPool(3);
for(int i =0;i<6;i++){
serv.execute(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(3000);
System.out.println("线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
System.in.read();
}
ScheduledExecutorService
可以执行 定时任务的线程池
能够起到定时执行的效果,很多定时机制的底层都是利用这个线程池来实现的
常用提交方法
- schedule(Runnable,延迟时间,时间单位)
例如ses.schedule(new ScheduleThread(), 5, TimeUnit.SECONDS);表示5秒后执行任务。 - scheduleAtFixedRate(Runnable, 延迟几秒执行, 间隔时间, TimeUnit.SECONDS);
ses.scheduleAtFixedRate(new ScheduleThread(), 0, 5, TimeUnit.SECONDS); 0表示立即开始执行任务,并且上一次任务启动之后,间隔5秒再次启动,不会考虑任务的执行时间;如果任务本身执行需要花费3s,那么任务的执行时间分别是0,5,10,15;如果任务本身的执行时间为8s,间隔时间为5s,将会以线程的执行时间为准 - scheduleWithFixedDelay(Runnable,延迟几秒执行, 间隔时间, TimeUnit.SECONDS);
ses.scheduleWithFixedDelay(new ScheduleThread(), 0, 5, TimeUnit.SECONDS); 表示立即执行任务,并且上一次任务执行完毕后间隔5秒再次启动任务。如果任务本身执行需要花费3s,那么任务的执行时间分别是0,8,16,24
测试代码
package executorservice;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
// 推迟5s执行线程
// ses.schedule(new ScheduleThread(), 5, TimeUnit.SECONDS);
// 从上一次启动开始,来计算下一次的启动时间
// 如果线程的执行时间超过间隔时间,则以线程的执行时间为准
//ses.scheduleAtFixedRate(new ScheduleThread(), 0, 5, TimeUnit.SECONDS);
// 从上一次结束开始,来计算下一次的启动时间
ses.scheduleWithFixedDelay(new ScheduleThread(), 0, 5, TimeUnit.SECONDS);
}
}
class ScheduleThread implements Runnable {
@Override
public void run() {
try {
System.out.println("hello");
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Callable
创建线程有三种方式:
1、继承Thread类
2、实现Runnable接口
3、实现Callable接口
而且,Callable接口的方式可以有返回值,可以抛出异常。
如果需要一个线程执行完成之后 有返回结果,那么这个线程就需要实现Callable接口(这个是阻塞式的调用)
Callable和Runnable的比较
Runnable | Callable | |
---|---|---|
返回值 | 重写run方法,不能有返回值 | 重写call方法,通过泛型指定返回值 |
启动方式 | new Thread().start()方式启动,线程池execute或者submit方式启动 | 只能通过提交线程池启动,线程池.submit方法 |
异常处理 | 不允许抛出异常,会自己捕捉;无法利用全局方式( 例如Spring中的异常通知)处理 | 允许抛出异常,可以用全局方式来处理异常 |
"通过继承Callable可以返回结果值,返回的结果会被封装到Future对象中,可以通过get方法获得任务的返回值"
class CDemo implements Callable<String> {// 泛型表示的是返回值的类型
@Override
public String call() throws Exception {
return "SUCCESS";
}
}
public class ExecutorServiceDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(5);
// 将任务交给线程池处理
// 将结果封装成了Future对象
Future<String> f = es.submit(new CDemo());
System.out.println(f.get());
// 关闭线程池
es.shutdown();
}
分叉合并池(fork/join)
将一个大任务分成多个小任务,然后对结果进行汇总。
linux的fork()函数可以克隆一个一模一样的线程出来,执行和原来的线程一模一样的任务。
java的分叉合并池简化了这种操作。不用去管怎么分叉怎么合并的,不用去控制线程数,全部交给分叉合并池进行管理。把任务往里面丢就可以了。
为什么会用分叉合并池?
1、分叉:将一个大的任务拆分成了多个小的任务交给不同的线程来执行。
2、将拆分出去的小任务的计算结果进行汇总整合
3、分叉合并池能够有效的 提高CPU的利用率,甚至能达到CPU利用率的100%(每个核上几乎都是满负荷的状态),所以实际开发中,分叉合并一般是放在凌晨执行 (分叉合并本质上就是将大任务拆分成小任务,分配给多个线程,让多个线程落地到不同的CPU核上来提高CPU的利用率,使用分叉合并池我们只需要关心任务的计算逻辑,以及任务的分叉逻辑,不需要关心线程是怎么拆分的,怎么复制的,怎么合并的 )
4、如果数据量偏大,分叉合并的效率要远远高于循环;如果数据量偏小,循环的效率反而高于分叉合并。(因为线程的分叉以及合并明显都是需要有额外的开销的)
5、任务的均衡以及工作窃取(work stealing)。分叉合并在向每个核上分配任务的时候,考虑任务的 均衡问题:核上任务多的少分,任务少的多分。由于CPU核在执行任务的时候,有快有慢,所以先执行完所有任务的CPU核并不会闲下来,而是会随机的扫描一个其他的核,从被扫描的核的任务队列 尾端"偷取"一个任务回来执行,这种策略称之为work-stealing(工作窃取)策略。这种机制是针对一些 慢任务设置的,有些任务确实执行得比较慢。
如果把一个任务只交给一个线程来计算,那么一个4核的机器,其他三核就只会看着。无论怎么提高CPU的执行效率,CPU的利用率也只能到25%。最好的办法就是将任务分拆,每个CPU核上都运行一个任务。
代码演示
"任务需要继承RecursiveTask"
"任务的返回值是一个Future类型"
package executorservice;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class ForkJoinPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long begin = System.currentTimeMillis();
// 求1-100000000000L
// long sum = 0;
// for (long i = 1; i <= 100000000000L; i++) {
// sum += i;
// }
// System.out.println(sum);
ForkJoinPool pool = new ForkJoinPool();
Future<Long> f = pool.submit(new Sum(1, 100000000000L));
pool.shutdown();
System.out.println(f.get());
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
}
class Sum extends RecursiveTask<Long> {
private long start;
private long end;
public Sum(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
// 如果start-end范围内数字比较少,那么可以将这个范围内的数字求和
if (end - start <= 10000) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 如果start-end数字依然比较多,那么就继续拆分
long mid = (start + end) / 2;
Sum left = new Sum(start, mid);
Sum right = new Sum(mid + 1, end);
// 拆分成了2个子任务
left.fork();
right.fork();
// 需要将2个子任务的结果来进行汇总才是这个范围内的最终结果
return left.join() + right.join();
}
}
}