并发编程学习——5 线程和线程池

显式的为任务创建线程

new Thread对于很多人来说很长一段时间都是他们接触多线程的起点。

显式的为任务创建线程

我们通过new Thread创建线程去拆分任务会有什么好处呢?

  • 任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接。
  • 任务可以并行处理,从而能够同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,程序的吞吐量将得到提高

当然任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

无限制创建线程的问题

上面说到的好处与其说是创建线程的好处,更贴切的说是多线程的好处。有句古话:物极必反。当线程的创建不受限制带来的问题是巨大的

  • 线程声明周期的开销非常高
  • 资源消耗,激活的线程会消耗系统资源
  • 稳定性,一定范围内,增加线程可以提高系统的吞吐率,但是如果超过了这个范围,再创建更多的线程只会降低程序的执行顺序。

更好的线程创建方式——线程池

字面意思是指管理一组同构工作线程的资源池。通过重用现有线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销

Executor

Executor是线程池的基类,但是Executor有一个问题,它并没有为线程池提供完整的生命周期管理,所以使用ExecutorService扩展了Executor接口

public interface ExecutorService extends Executor {

    /**
     */
    void shutdown();

    /**
     */
    List<Runnable> shutdownNow();

    /**
     */
    boolean isShutdown();

    /**
     */
    boolean isTerminated();

    /**
     */
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    /**
     */
    <T> Future<T> submit(Callable<T> task);

    /**
     */
    <T> Future<T> submit(Runnable task, T result);

    /**
     */
    Future<?> submit(Runnable task);

    /**
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    /**
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    /**
     */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    /**
     */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ExecutorService 有三种状态

  • 创建时处于运行状态
  • shutdown方法将执行平缓的关闭过程,不再接受新的任务,同时等待已经提交的任务完成。
  • shutdownNow方法将执行粗暴的关闭过程,它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始的任务。

线程池的创建

JAVA提供了Executors工具来进行线程池的创建。常见的主要有下面几种。

ThreadPoolExecutor

在查看Executors提供的方法创建线程池之前最好先对ThreadPoolExecutor的参数有个了解。下面几种线程池的创建最终都是创建ThreadPoolExecutor的不同参数配置。而了解ThreadPoolExecutor的参数含义也可以了解上面几种方法创建出来的线程池的作用

构造函数
    /**
     * 
     * @param corePoolSize      线程池里最小线程数
     * @param maximumPoolSize   线程池里最大线程数量,超过最大线程时候会使用RejectedExecutionHandler
     * @param keepAliveTime     线程最大的存活时间
     * @param unit              线程最大的存活时间的单位
     * @param workQueue         缓存异步任务的队列
     * @param threadFactory     用来构造线程池里的worker线程
     * @param handler           由于达到线程边界和队列容量而阻止执行时要使用的处理程序
     */
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // ……
    }

常见的的线程池

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1)

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

使用的构造函数

new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>())

含义:nThreads为传入参数。维持线程任务的队列使用LinkedBlockingQueue

单线程的线程池

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

创建一个单线程的线程池,任务将会以顺序执行任务。当因为异常导致线程关闭的时候,线程池会创建一个新的线程。

使用的构造函数

new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));

含义:线程池的基础线程和最大线程都被设置为1,同样使用LinkedBlockingQueue维持任务队列

可缓存的线程池

ExecutorService executorService = Executors.newCachedThreadPool();

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

使用的构造函数

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>())

含义:线程数被设置为最大,但是设置了线程寿命。使用SynchronousQueue维持任务队列

执行定时及周期性任务的线程池

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);

创建一个定长线程池,支持定时及周期性任务执行

使用的构造函数

new ScheduledThreadPoolExecutor(corePoolSize);

super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
      new DelayedWorkQueue());
      
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

含义:和之前不同之处主要使用DelayedWorkQueue作为任务队列。

关于任务队列后续会在队列、容器里面再介绍

ForkJoinPool

1.8的时候JAVA提供了一个新的方法newWorkStealingPool,而这个方法创建出来的并不是ThreadPoolExecutor而是ForkJoinPool

和之前的线程池不同的是,ForkJoinPool内部维护一个名为WorkQueue的双端队列,使用工作密取来获取其他线程的任务。

volatile WorkQueue[] workQueues;     // main registry

双端密取的好处就是当一个线程任务队列执行完成的时候它可以检测其他线程的任务队列,并从其他线程任务队列的尾部获取任务,即充分利用了资源,又减少了从头部获取任务而产生的资源竞争。

WorkQueue

WorkQueue是ForkJoinPool的内容类,根据注释说明

支持工作密取和外部任务的队列
ForkJoinTask

ForkJoinTask是ForkJoinPool任务的接口,主要有两种RecursiveTask(有返回值的),RecursiveAction(无返回值的)

使用类中compute的方法,可以实现子任务的的拆分,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。

简单的使用
@AllArgsConstructor
@NoArgsConstructor
public class JoinTask extends RecursiveTask<Integer> {

    private static final int THRESHOLD = 10; //每个小任务 最多只累加20个数
    private int array[];
    private int start;
    private int end;
    
    
    /**
     * 执行计算的规则
     * The main computation performed by this task.
     * @return the result of the computation
     */
    @Override 
    protected Integer compute() {
        int sum =0;
        // 任务已经拆分足够细了
        if (end - start < THRESHOLD){
            for(int i = start; i < end; i++){
                sum += array[i];
            }
            System.out.println(Thread.currentThread().getName() + "线程开始执行计算任务");
            return sum;
        } else { // 需要继续拆分任务
            int middle = (start + end) / 2;
            JoinTask left = new JoinTask(array, start, middle);
            JoinTask right = new JoinTask(array, middle, end);
            // 并行执行两个小任务
            left.fork();
            right.fork();
            //把两个小任务累加的结果合并起来
            return left.join() + right.join();
        }
    }
}
public class ForkJoinPoolTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int[] arr = new int[100];
        int total = 0;
        //初始化100个数组元素
        for(int i= 0,len = arr.length;i<len;i++){
            //对数组元素赋值,并将数组元素的值添加到sum总和中
            arr[i] = 100;
            total = total + 100;
        }
        ForkJoinPool pool = null;
        try {
            System.out.println("初始化数据总和:"+total);
            JoinTask task = new JoinTask(arr,0,arr.length);
            // 创建一个通用池,这个是jdk1.8提供的功能
            pool = ForkJoinPool.commonPool();
            Future<Integer> future = pool.submit(task);
            // 提交分解的SumTask 任务
            System.out.println("多线程执行结果:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (pool != null) {
                pool.shutdown();
            }
        }
    }
}

自定义线程池

假如开发中你使用了阿里的规约应会发现,在阿里的规约中其实是不建议使用Executors创建线程池。它建议开发者自己去定义线程池的参数,这样有助于开发人员真正理解线程池的作用。

线程上限的异常处理

    class MyRejectedExecutionHandler implements RejectedExecutionHandler {

        /**
         * 由于达到线程边界和队列容量而阻止执行时要使用的处理程序
         */
        @Override 
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // do some thing
        }
    }

创建策略

    public ThreadPoolExecutor myExecutor2() {
        return new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),
                new MyRejectedExecutionHandler());
    }
线程数量的设置

这一块因为本人实际经验真的不算太多就使用了慕课网中的一个计算公式了(http://www.imooc.com/article/5887)

如何来设置

  1. 需要根据几个值来决定
    1. tasks :每秒的任务数,假设为500~1000
    2. taskcost:每个任务花费时间,假设为0.1s
    3. responsetime:系统允许容忍的最大响应时间,假设为1s
  2. 做几个计算
    1. corePoolSize = 每秒需要多少个线程处理?
      1. threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
      2. 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
    2. queueCapacity = (coreSizePool/taskcost)*responsetime
      1. 计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
      2. 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
    3. maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
      1. 计算可得 maxPoolSize = (1000-80)/10 = 92
      2. (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
    4. rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
    5. keepAliveTime和allowCoreThreadTimeout采用默认通常能满足

线程池的关闭

线程池提供了两种关闭方法

  • 使用shutdown正常关闭,
  • 使用shutdownNow强行关闭。

强行关闭的速度更快,但风险也更大,因为任务很可能在执行到一半时被结束,而正常关闭虽然速度慢,却更安全,因为线程池会一直等到队列中所有任务都执行完成后才关闭

shutdownNow的问题

使用shutdownNow的时候,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务。然而我们无法通过正常方法找出哪些任务已经开始。但是我们可以使用一些特殊办法保存这些数据.

public class TrackingExecutor extends AbstractExecutorService {

    private final ExecutorService executorService;

    private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<>());

    public TrackingExecutor(ExecutorService executorService) {
        this.executorService = executorService;
    }

    public List<Runnable> getCancelledTasks () {
        if (!executorService.isTerminated()) {
            throw new RuntimeException();
        }
        return new ArrayList<>(tasksCancelledAtShutdown);
    }

    @Override
    public void execute(Runnable command) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    command.run();
                } finally {
                    if (isShutdown() && Thread.currentThread().isInterrupted()) {
                        tasksCancelledAtShutdown.add(command);
                    }
                }
            }
        });
    }
    
    // 方法委托executorService执行
}

一种特殊的线程池关闭策略——“毒丸”

可以将某个类型对象标识为"毒丸",当系统得到这个对象的时候立刻停止线程。

总结

本篇主要介绍的是线程和线程池的内容,线程池的确在一定范围内降低了我们使用多线程的难度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值