Current并发包(三) - ExecutorService

ExecutorService - 执行器服务


ExecutorService概述

ExecutorService本质上是一个线程池

线程池的意义:做到线程的复用,减少线程的创建和销毁

当线程创建好后,里面暂时没有任何线程,此时这个池子是空的

每过来一个请求的时候,就会在线程池中创建一个线程(核心线程)来处理

核心线程在使用完成之后不会被销毁

在核心线程达到指定的数量之前,即使有核心线程空闲,新来的请求依然会创建一个核心线程来处理,直到核心线程达到指定的数量

如果所有的核心线程都被占用,则新来的请求会被放入工作队列中。工作队列本质上是一个阻塞式队列

如果工作队列也被占满,则新来的请求会被交给临时线程来处理

临时线程在处理完请求之后不会被立即销毁,而是会等待指定的一段时间再销毁。而如果在指定的时间内又接受了新的请求,则临时线程会继续存活

如果临时线程被全部占用,再来的请求会交给RejectedExcutionHandler来进行拒绝

ExecutorServiceDemo.java
package com.wiscom.pool;
​
public class ExecutorServiceDemo {
​
    public static void main(String[] args) {
​
        // ExecutorService es = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue)
        // 创建一个执行器服务
        // corePoolSize - 核心线程数量
        // maximumPoolSize - 最大线程数量 = 核心线程数 + 临时线程数
        // keepAliveTime - 临时线程存活时间
        // unit - 时间单位
        // workQueue - 工作队列
        // handler - 拒绝执行处理器
        
        
        ExecutorService es = new ThreadPoolExecutor(5, // 5个核心线程
                10, // 5个核心+5个临时
                5, TimeUnit.SECONDS, // 临时线程在使用完成之后要存活5s
                new ArrayBlockingQueue<Runnable>(5), // 工作队列中能够存储5个请求
                new RejectedExecutionHandler() {
​
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("拒绝执行" + r);
                    }
                });
​
        // 把任务提交给线程池来执行
        for (int i = 0; i < 18; i++)
            es.execute(new ESRunnable());
​
        // 关闭线程池
        es.shutdown();
​
    }
​
}
​
class ESRunnable implements Runnable {
​
    @Override
    public void run() {
        try {
            System.out.println("hello~~~");
            Thread.sleep(3000);
            System.out.println("finished~~~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
​
}

 

Callable

Callbale是JDK1.5提供的一个定义线程的类

Callable和Runnable的区别

返回值

Runnable没有返回值

Callable允许返回效果

启动方式

Runnable可以通过Thread或者线程来启动

Callable只能通过线程池来启动

容错机制

Runnable不允许抛出异常,导致无法利用全局方式(如:Spring中的异常通知)来处理异常

Callable允许抛出异常,可以利用全局方式进行处理

ExecutorServiceDemo2.java
package com.wiscom.pool;
​
public class ExecutorServiceDemo2 {
​
    public static void main(String[] args) throws InterruptedException, ExecutionException {
​
        // 特点:
        // 1. 没有核心线程全部都是临时线程
        // 2. 临时线程的数量是Integer.MAX_VALUE。考虑实际开发中
        // 单台服务器能处理的线程数量是有限的
        // 导致可以认为这个线程池能够处理无限多的请求
        // 3. 临时线程的存活时间是1min
        // 4. 工作队列是一个同步队列,只能存储1个元素
        // 大池子小队列
        // 适应场景:适用于短任务高并发场景
        // ExecutorService es = Executors.newCachedThreadPool();
// 特点:
        // 1. 没有临时线程全部都是核心线程
        // 2. 工作队列是一个LinkedBlockingQueue
        // 并且这个队列在创建的时候没有容量
        // 那么意味着这个队列的容量是Integer.MAX_VALUE
        // 可以认为这个队列的容量是无限的
        // 也就意味着这个线程池可以存储无限多的任务
        // 小池子大队列
        // 适应场景:适用于长任务场景 - 百度云盘下载
        ExecutorService es = Executors.newFixedThreadPool(5);
​
        // Callable线程不能通过Thread启动,只能通过线程池启动
        // submit既可以执行Runnable也可以执行Callable
        Future<String> f = es.submit(new CallableDemo());
        // 关闭线程池
        es.shutdown();
        // 从Future中将实际结果来解析出来
        System.out.println(f.get());
    }
​
}
​
// 泛型规定的是返回结果的类型
class CallableDemo implements Callable<String> {
​
    @Override
    public String call() throws Exception {
        return "SUCCESS";
    }
}

 

ScheduledExecutorService - 定时执行器

这个类能提供定时的效果

ScheduledExecutorServiceDemo.java
package com.wiscom.pool;
​
public class ScheduledExecutorServiceDemo {
​
    public static void main(String[] args) {
​
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
​
        // 这个方法能起到定时的效果
        // command - 提交的任务
        // delay - 延时时间
        // unit - 时间单位
        // 延迟5s再执行提交的任务
        // ses.schedule(new ScheduledRunnable(), 5, TimeUnit.SECONDS);
// 每隔5s执行一次这个任务
        // 从上一次的开始来计算下一次的启动时间
        // 如果线程执行时间大于间隔时间
        // 则两次任务的间隔时间以线程执行时间为准
        ses.scheduleAtFixedRate(
                new ScheduledRunnable(), 0, 
                5, TimeUnit.SECONDS);
        
        // 从上一次的结束来计算下一次的启动时间
        ses.scheduleWithFixedDelay(
                new ScheduledRunnable(), 0, 
                5, TimeUnit.SECONDS);
        
    }
​
}
​
class ScheduledRunnable implements Runnable {
​
    @Override
    public void run() {
        try {
            System.out.println("running~~~");
            Thread.sleep(8000);
            System.out.println("finished~~~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
​
}

 

ForkJoinPool

分叉

将一个大的任务拆分成多个小任务交给多个线程来执行

合并

将小任务的执行结果进行汇总

分叉合并的目的

提高cpu的利用率,提高效率

如果数据量较大,则适合使用分叉合并

如果数据量较小,此时循环的效率反而更高

分叉合并过程

分叉合并在给每一个核分配任务的时候,尽量保证每一个核的任务数量基本相等。

在分叉合并中,为了防止“慢任务”导致整体的效率降低,所以采取了work-stealing(工作窃取)策略来解决这个问题。

工作窃取策略

是指当前核上的任务执行完成后,这个核并不会空闲下来,而是继续扫面其他的核,随机的从一个核的任务队列的尾端 一个任务回来执行

ForkJoinPoolDemo.java
package com.wiscom.pool;
​
public class ForkJoinPoolDemo {
​
    public static void main(String[] args) throws InterruptedException, ExecutionException {
​
        long begin = System.currentTimeMillis();
         long sum = 0;
         for (long i = 0; i <= 100000000000L; i++) {
         sum += i;
         }
         System.out.println(sum);
​
        // 创建分叉合并池
//      ForkJoinPool pool = new ForkJoinPool();
//      Future<Long> f = pool.submit(new Sum(1, 100000000000L));
//      System.out.println(f.get());
//      pool.shutdown();
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }
​
}
​
@SuppressWarnings("serial")
class Sum extends RecursiveTask<Long> {
​
    private long start;
    private long end;
​
    public Sum(long start, long end) {
        this.start = start;
        this.end = end;
    }
​
    @Override
    public Long compute() {
​
        if (end - start <= 10000) {
​
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
​
            long mid = (start + end) / 2;
            Sum left = new Sum(start, mid);
            Sum right = new Sum(mid + 1, end);
            // 分叉
            left.fork();
            right.fork();
            // 合并
            return left.join() + right.join();
​
        }
​
    }
​
}
posted @ 2020-08-25 14:39  minnersun  阅读(207)  评论(0编辑  收藏  举报