• 多线程编程核心技术(十七)线程池


      创建一个线程只需要,New Thread();,就可以完成,但是在JVM里面是一个很重的操作,需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源。

      所以线程是一个重量级的对象,应该避免频繁创建和销毁。

      线程池是一种生产者-消费者模式,目前对于池化技术的实现一般都需要依赖容器来进行实现。下面是一个简单的伪代码,逻辑其实就是依赖一个队列,然后让队列中的线程循环执行传入的任务。

    //简化的线程池,仅用来说明工作原理
    class MyThreadPool{
      //利用阻塞队列实现生产者-消费者模式
      BlockingQueue<Runnable> workQueue;
      //保存内部工作线程
      List<WorkerThread> threads 
        = new ArrayList<>();
      // 构造方法
      MyThreadPool(int poolSize, 
        BlockingQueue<Runnable> workQueue){
        this.workQueue = workQueue;
        // 创建工作线程
        for(int idx=0; idx<poolSize; idx++){
          WorkerThread work = new WorkerThread();
          work.start();
          threads.add(work);
        }
      }
      // 提交任务
      void execute(Runnable command){
        workQueue.put(command);
      }
      // 工作线程负责消费任务,并执行任务
      class WorkerThread extends Thread{
        public void run() {
          //循环取任务并执行
          while(true){ ①
            Runnable task = workQueue.take();
            task.run();
          } 
        }
      }  
    }
    
    /** 下面是使用示例 **/
    // 创建有界阻塞队列
    BlockingQueue<Runnable> workQueue = 
      new LinkedBlockingQueue<>(2);
    // 创建线程池  
    MyThreadPool pool = new MyThreadPool(
      10, workQueue);
    // 提交任务  
    pool.execute(()->{
        System.out.println("hello");
    });

      Java 并发包里提供的线程池比上面的复杂的多,最核心的是 ThreadPoolExecutor,ThreadPoolExecutor 的构造函数非常复杂,如下面代码所示,这个最完备的构造函数有 7 个参数。

    ThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      RejectedExecutionHandler handler) 
    

      你可以把线程池类比为一个项目组,而线程就是项目组的成员。

    • corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。
    • maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
    • keepAliveTime 和 unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
    • workQueue:工作队列,和上面示例代码的工作队列同义。
    • threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
    • handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
      • CallerRunsPolicy:提交任务的线程自己去执行该任务。
      • AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
      • DiscardPolicy:直接丢弃任务,没有任何异常抛出。
      • DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

      Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走。这边要注意的是如果是自己实现一个线程池最好还是使用有界队列,因为无界队列很容易出现OOM,这也是无建议使用静态的Executors的原因。使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用

      submit提交的任务,出现runtime异常时异常会被吞掉。setUncaughtExceptionHandler也只能捕获execute提交的task抛出的异常。submit提交时,通过返回的Future的get捕获异常。所以最好还是按需处理

    try {
      //业务逻辑
    } catch (RuntimeException x) {
      //按需处理
    } catch (Throwable x) {
      //按需处理
    } 
    

      

  • 相关阅读:
    ASCII、GBK、Unicode、UTF-8、ISO-8859-1等常见字符编码介绍
    HTTP协议简介
    关于无知的一点思考
    Java 8 新特性之lambda表达式
    Java 8 新特性之新的日期时间库
    【java】<Jsoup>获取网页中的图片
    【数据结构】二叉树
    【转载】Android中UI线程与后台线程交互设计的5种方法
    【数据结构】广义表
    【c语言】数据结构(约瑟夫生者死者游戏的问题)
  • 原文地址:https://www.cnblogs.com/SmartCat994/p/14228745.html
Copyright © 2020-2023  润新知