多线程的简单应用

目录

创建线程的4种方式

几种常见的线程池

使用CountDownLatch实现主线程与子线程的同步

主线程捕获子线程的异常

代码实现

线程池的标准创建方式

线程池的拒绝策略

创建线程的4种方式

        众所周知,创建线程由以下4种方式:继承Thread类创建线程,实现Runnable接口创建线程,使用CallBack和FutureTask创建线程,通过线程池创建线程。由于创建一个线程实例的时间成本及资源消耗都很高,在实际开发中我们一般使用线程池来创建线程。java提供了一个名为Executors的静态工厂类来创建不同的线程池,其中包含几种常见的线程池。

几种常见的线程池

  • FixedThreadPool:核心线程数和最大线程数一样,是一个固定长度的线程池
  • CachedThreadPool:可缓存线程池,线程数可以无限增加,当线程闲置时可以对线程进行回  收,该线程池的长度是不固定的。
  • ScheduledThreadPool:是一种支持定时或者周期性任务的线程池
  • SingleThreadExecutor:该线程池只有一个线程,如果线程在执行任务过程中发生了异常,会重新创建一个新线程继续后续任务的执行
  • SingleThreadScheduledExecutor:是ScheduledThreadPool的一种特例,其内部只有一个线程
  • ForkJoinPool:适合执行可以产生子任务的任务,有一个Task,它可以产生三个子任务,三个子任务并行执行完之后将结果汇总给Result

使用CountDownLatch实现主线程与子线程的同步

        有时候我们需要所有子线程任务执行完之后主线程再继续执行,例如:我们需要解析多个文件的内容,并将解析出来的内容保存到数据库。此时我们就可以使用多线程来实现,使用多个子线程去解析文件,等所有子线程解析完毕之后,主线程再将解析的结果批量保存。

         CountDownLatch相当于是一个计数器,有一个int类型的入参,代表了需要等待的线程数量,允许一个或者多个线程去等待其他线程执行完毕,主要有以下几个方法:

void await()

使当前线程到同步队列中等待,直到latch的值变为0或者当前线程被中断,

此时才会被唤醒

boolean await(long timeout, TimeUnit unit)
有超时时间的等待方法,超过该时间后当前线程也会被唤醒
void countDown()
使latch的值减1,当latch值变为0时,会唤醒所有等待该latch的线程
long getCount()
获取latch的值

主线程捕获子线程的异常

        由于线程的run()方法没有throws语句,所以并不会抛出checked异常,至于RuntimeException这样的unchecked异常,由于线程是由JVM进行调度的,主线程是不会捕获到子线程的运行时异常,所以在主线程中使用try-catch语句是无法实现捕获子线程异常的。

        此时如果主线程需要捕获子线程的方法,可以调用ThreadPoolExecutor.submit()方法,此方法可以获得一个线程执行结果的Future对象,使用Future.get()方法时可以捕获ExecutionException,从而知道子线程中发生了异常。

代码实现

      ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                         5, //核心线程数,即使空闲也不会被回收
                        10, //最大线程数
                         1, //线程最大空闲时长
          TimeUnit.MINUTES, //线程最大空闲时长类型
          new LinkedBlockingQueue<>(5),//阻塞队列的长度
          new CallerRunsPolicy() //拒绝策略,调用者执行策略
          );
      CountDownLatch latch = new CountDownLatch(list.size());
      List<Attach> vector = new Vector<>();
      Attach att = new Attach();
      for (File file : list) {
        Future future = threadPool.submit(() -> {
          att = attachService.identify(file);
          latch.countDown();
          vector.add(att);
        });
        //捕捉子线程异常,防止子线程报错导致主线程无法继续进行
        try {
          future.get(5,TimeUnit.SECONDS);
        }catch (ExecutionException e){
          log.error("1、捕捉到子线程异常:" , e);
          latch.countDown();
        } catch (InterruptedException e) {
          log.error("2、线程中断异常:" , e);
          latch.countDown();
        } catch (TimeoutException e) {
          log.error("3、子线程执行结果获取超时:" , e);
          latch.countDown();
        }
      }
      try {
        latch.await();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      threadPool.shutdown();
      //批量保存入库操作

线程池的标准创建方式

        大部分的企业都禁止使用Executors创建线程池,例如阿里巴巴开发手册就明文规定了禁止使用Executors创建线程池。要求通过标准构造器ThreadPoolExecutor去创建线程池,其中有一个较为重要的构造方法:

    public ThreadPoolExecutor(int corePoolSize,//核心线程数,即使线程空闲也不会回收
                              int maximumPoolSize,//最大线程数
                              long keepAliveTime,//线程最大空闲时间
                              TimeUnit unit,//空闲时间类型(毫秒、秒、分钟...)
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//新线程的产生方式
                              RejectedExecutionHandler handler //拒绝策略
                             )

注意事项:

        当前工作线程多于核心线程数,且小于最大线程数时,新的任务将会暂时保存到阻塞队列中,只有当阻塞队列满了之后才会创建新的线程去执行任务,所以如果核心线程数、阻塞队列、最大线程数等参数配置不当会出现任务不能被正常执行的问题。

      /**
       * 做一个极限测试:
       * 此时只有一个核心线程,往线程池提交了5个任务,而阻塞队列长度为100
       * 这时候由于阻塞队列没有满,所以不会创建一个新的线程去执行剩下的4个任务
       * 剩下的4个任务只有等第一个任务执行完才可以执行
       */
      ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
          1, //核心线程数,即使空闲也不会被回收
          100, //最大线程数
          100, //线程最大空闲时长
          TimeUnit.MINUTES, 
          new LinkedBlockingQueue<>(100)//阻塞队列的长度
      );
      for (int i = 0; i < 10; i++){
        threadPool.execute(()->{
          System.out.println(Thread.currentThread().getName());
          try {
            //工作线程睡眠无限长的时间
            Thread.sleep(Long.MAX_VALUE);
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        });
      }

线程池的拒绝策略

        在线程池的阻塞队列(LinkedBlockingQueue)为有界队列时,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:

  1. 线程池已经被关闭了
  2. maximumPoolSize(最大线程数)已满且LinkedBlockingQueue(阻塞队列)已满

当任务被拒绝时,线程池都会调用RejectedExecutionHandler实例的RejectedExecutionHandler方法。RejectedExecutionHandler是拒绝策略的接口,JUC提供了以下几种实现:

  • AbortPolicy:拒绝策略

线程池的默认拒绝策略,如果队列满了,新任务会被拒绝,且抛出异常:RejectedExecutionException,该异常是运行时异常,很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获该异常。

  • DiscardPolicy:抛弃策略

是AbortPolicy的安静版本,如果队列满了,新任务会被直接丢弃,且不会抛出异常

  • DispartOldestPolicy:抛弃最老任务策略

如果队列满了,将会抛弃最早进入队列的任务,从队列中腾出空间,再尝试加入队列。因为队列时先进先出,队头的任务是最老的,所以每次都是移除队头的任务后再尝试入队。

  • CallRunsPolicy:调用者执行策略

在新任务被添加到线程池是,如果添加失败,那么提交任务的线程就会自己去执行该任务,不会使用线程池中的线程去执行新的任务。

该策略有两个好处:

  1.  新任务不会被丢弃,不会造成业务的损失
  2. 负责提交任务的线程去执行任务,那么此时负责提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,这时候便不会再有新任务提交(该线程去执行任务了),相当于是一个缓冲,减缓了提交任务的速度,可以利用这段时间执行掉一些任务,腾出线程池的一部分空间,用于接收新的任务。
  • 自定义策略

如果以上拒绝策略都不符合业务需求,那么也可以自定义一个拒绝策略,实现RejectedExecutionHandler接口,重写rejected方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值