美文网首页
Java基础之多线程篇(三)

Java基础之多线程篇(三)

作者: youzhihua | 来源:发表于2019-11-28 12:09 被阅读0次

    一、使用线程池的优点

    • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
    • 提高系统资源的利用率:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。

    二、线程池相关体系结构

    线程池相关类继承关系.png

    三、ThreadPoolExecutor

    • 构造方法

    ThreadPoolExecutor的构造方法有很多,这里我们挑选一个构造参数最多的。

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    
    • 参数解释

    corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量。
    maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。
    keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
    unit:keepAliveTime 参数的时间单位。
    workQueue:当线程数大于核心线程数时,会进入该队列中。
    threadFactory:创建线程使用的工厂。
    handler:当线程池内的线程数超过maximumPoolSize时,所进行的拒绝策略,总共有四种定义好的策略。分别为:DiscardOldestPolicy,AbortPolicy,DiscardPolicy,CallerRunsPolicy。

    • DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
    • AbortPolicy:默认的拒绝策略,抛出 RejectedExecutionException来拒绝新任务的处理。
    • DiscardPolicy:直接丢弃任务请求。
    • CallerRunsPolicy:新建线程处理任务,会降低对于新任务提交速度,影响程序的整体性能。

    3.1常见线程池

    3.1.1 FixedThreadPool

    • 创建方法:
        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
    • 执行流程
      1.如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
      2.当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
      3.线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

    • 缺点
      可以从构造方法看出,创建了一个固定大小的线程池,等待队列是一个无界队列,所以maximumPoolSize和keepAliveTime是无法生效的,若提交任务的速度大于CPU处理任务的速度,便会造成OOM。

    3.1.2 SingleThreadExecutor

    • 创建方法
        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    
    • 执行流程

    1.如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
    2.当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
    3.线程执行完当前的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

    与FixedThreadPool基本相同,只不过只能同时有一个线程工作。

    • 缺点

    与FixedThreadPool相同,等待队列是一个无界队列,所以maximumPoolSize和keepAliveTime是无法生效的,若提交任务的速度大于CPU处理任务的速度,便会造成OOM。

    3.1.2 CachedThreadPool

    • 创建方法
        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    
    • 执行流程

    1.首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 线程池中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成。
    2.当初始线程池为空,或者线程池中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

    • 缺点

    由于线程池的maximumPoolSize是Integer.MAX_VALUE,导致线程池近乎无界,若任务提交过快,也会造成和FixedThreadPool、SingleThreadExecutor一样的OOM。

    3.1.4 ScheduledThreadPoolExecutor

    主要用来启动定时任务,具体详情可以查看我的另一篇文章。SpringBoot基础教程(十) | 定时器篇

    四、线程池示例

    4.1 线程池+runnable

    /**
     * threadPoolExecutor + runnable
     */
    public class ThreadPoolDemo1 {
    
        public static void main(String[] args) {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,
                    20,
                    1L, 
                    TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<>(10),
                    new ThreadPoolExecutor.CallerRunsPolicy());
            for (int i = 0; i < 20; i++) {
                threadPoolExecutor.execute(new RunnaleDemo());
            }
            threadPoolExecutor.shutdown();
            while(!threadPoolExecutor.isTerminated()){
                System.out.println("正在终止");
            }
            System.out.println("终止完成");
        }
    }
    

    4.2 线程池+callable

    
    /**
     *  ThreadPoolExecutor+callable
     */
    public class ThreadPoolDemo2 {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
                    15,
                    1L,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100));
    
            List<Future<Integer>> futureTasks = new ArrayList<>();
    
            for (int i = 0; i < 20; i++) {
                futureTasks.add(threadPoolExecutor.submit(new CallableDemo()));
            }
    
            for (Future future:futureTasks) {
                System.out.println(future.get());
            }
    
            threadPoolExecutor.shutdown();
        }
    }
    

    五、线程池大小的制定规则

    • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
    • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

    相关文章

      网友评论

          本文标题:Java基础之多线程篇(三)

          本文链接:https://www.haomeiwen.com/subject/gymvwctx.html