java.util.concurrent——ExecutorService

概述

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的比较

RunnableCallable
返回值重写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();
        }

    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值