线程池

Callable接口

  • 多线程中获得多线程的方式

    1. 继承Thread类

      class MyThread2 extends Thread{
          public void run(){
      
          }
      }
      
    2. 实现runnable接口

      class MyThread implements Runnable
      {
          @Override
          public void run() {
      
          }
      }
      
    3. 实现Callable接口通过FutureTask包装器来创建Thread线程

      • 是一种执行完,可带来返回结果
      • FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
      • 用Thread进行实例化,传入实现Runnabnle接口的FutureTask的类
      • futureTask.get() 获取返回值
      • 注意:
        • 计算完成就要去强求,会导致阻塞,直到计算完成
        • 多个线程执行 一个FutureTask的时候,只会计算一次
      class MyThread3 implements Callable<Integer>{
          public Integer call() throws Exception{
              System.out.println(Thread.currentThread().getName()+"****** come in callable");
              try{
                  TimeUnit.SECONDS.sleep(3);
              }catch (InterruptedException e){
                  e.printStackTrace();
              }
              return 1024;
          }
      }
      public class CallableDemo {
          public static void main(String[] args)throws InterruptedException , ExecutionException {
              //两个线程,main,AA
              //MyThread3(): callable
              FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
              //FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread3());
              new Thread(futureTask, "AA").start();
              //多个线程一同个futureTask,只算一次。
              new Thread(futureTask, "BB").start();
              int result02 = futureTask.get();
              System.out.println(Thread.currentThread().getName()+"***********");
              int result01 = 100;
              //等待
              //while (!futureTask.isDone()){}
      
              //如果result02没有计算完成,就会堵塞,直到计算完成
              System.out.println("****result"+(result01 + result02));
          }
      }
      

  1. 线程池获取

线程池

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行

主要特点:

线程复用;控制最大并发数;管理线程

线程池的优势:

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

Java Executor 框架

用于克服Java底层线程问题的类和接口的框架

1.Exeutor 接口

Runnable 接口,Callable 接口都是对任务处理逻辑的抽象,这种抽象使得我们无须关系任务的具体处理逻辑

java.util.concurrent.Excutor 则是对任务的执行进行的抽象

Excutor 接口仅定义了方法: void execute(Runnable command)

command 则代表需要执行的任务。Excutor 接口使得任务的提交方只需要知道它调用 Excutor.execute 方法便可以使指定的任务被执行,而无需关系任务的具体细节

解耦提交操作

2.ExecutorServise 接口

继承自Executor 接口,克服了Executor接口的局限性

ThreadPoolExecutor 是其默认实现类

  1. ScheduledExecutorService接口:

    继承自接口ExecutorService,它代表了一种能够让你调度任务运行一次或者在指定延迟之后周期性执行的Executor

  2. Executors

Executors 是实用工具类

它提供了能够返回ExcutorService实例的快捷方法

这些ExecutorService 实例 往往使我们在不必手动创建 ThreadPoolExecutor 实例的情况下使用线程池

线程池的工作原理

  1. 线程池的拒绝策略:

    • 等待队列排满了,塞不下新任务了
    • 线程池的max线程也达到了,无法继续为新任务服务
  2. 拒接策略:

    以上内置策略均实现了RejectedExecutionHandeler接口,若仍无法满足,可以自己扩展接口

    • AbortPolicy(默认): 直接抛出RejectedExecutionExceptiom异常阻止系统正常运行

    • CallerRunsPolicy: “调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量

    • DiscardOldestPolicy: 抛弃任务中等待最久的任务,然后把当前任务加入队列中再次尝试提交当前任务

    • DiscardPolicy: 直接丢弃任务,不予任何处理也不抛异常。如果任务允许丢失这是最好的方案

创建线程池

  1. Executors.newFixedThreadPool(int i) :

    创建一个拥有 i 个线程的线程池

    使用了LinkedBlockingQueue任务队列

    当任务提交的非常频繁的时候,该队列可能迅速膨胀,从而耗尽资源

    由于该方法返回的线程池的核心线程大小等于其最大线程池大小,因此该线程池中的工作线程永远不会超时。我们必须手动关闭

  2. Executors.newSingleThreadExecutor :

    创建一个只有一个线程的线程池

    适合生产者-消费者模式

  3. Executors.newCacheThreadPool() :

    创建一个可扩容的线程池

    执行很多短期异步的小程序或者负载教轻的服务器

    SynchronousQueue 队列,而它又是直接提交的任务队列,总会迫使线程池增加新的线程执行任务。

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

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory);
    }
    
  4. Executors.newScheduledThreadPool(int corePoolSize) :

    线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池

  5. newWorkStealingPool:

    创建一个拥有多个任务队列(以便减少连接数)的线程池

  6. newSingleThreadScheduledExecutor

    创建只有一条线程的线程池,他可以在指定延迟后执行线程任务

  7. 通过ThreadPoolExecutor来创建

public static void threadPoolInit(){
    System.out.println(Runtime.getRuntime().availableProcessors());
    //一池5个处理线程,执行长期任务,性能好很多
    //ExecutorService threadPool = Executors.newFixedThreadPool(5);
    //一池1线程,一个任务一个任务执行的场景
    //ExecutorService threadPool = Executors.newSingleThreadExecutor();
    //一池N线程 执行很多短期异步的小程序或者负载较轻的服务器
    ExecutorService threadPool = Executors.newCachedThreadPool();
    try{
        for(int i = 1; i <= 10; i++){
            final int tempInt = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
            });
        }
    }catch(Exception e){
        e.getStackTrace();
    }finally {
        threadPool.shutdown();
    }

}

线程池的几个重要参数介绍

public ThreadPoolExecutor(int corePoolSize, 
                          int maximumPoolSize, 
                          long keepAliveTime, 
                          TimeUnit unit, 
                          BlockingQueue<Runnable> workQueue, 
                          ThreadFactory threadFactory, 
                          RejectedExecutionHandler handler){}
  1. corePoolSize: 线程池中的常驻核心线程数

  2. maximumPoolSize: 线程池能够容纳同时执行的最大线程数,此值必须大于等于1

  3. keepAliveTime: 多余的空闲线程存活时间

    • 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁知道只剩下corePoolSize为止
  4. unit:keepAliveTime的单位

  5. workQueue:任务队列,被提交但尚未被执行的任务

    它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。

    • 直接提交的队列:SynchronousQueue
    • 有界的任务队列: ArrayBlockingQueue
    • 无界的任务队列: LinkedBlockingQueue
    • 优先任务队列: PriorityBlockingQueue
  6. threadFactory: 表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可

    ThreadFactory 是一个接口,他只有一个用来创建线程的方法Thread newThread(Runnable r);当线程需要线程的时候就会调用这个方法

  7. handler: 拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程(maximumPoolSize)时如何来

public static void main(String[] args){
  //  threadPoolInit();

    ExecutorService threadPool = new ThreadPoolExecutor(
            2,
            5,
            1L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(3),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());
    try{
        for(int i = 0; i < 10; i++){
            final int tempInt = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
            });
        }
    }catch(Exception e){
        e.printStackTrace();
    }finally {
        threadPool.shutdown();
    }

}

扩展线程池

beforeExecute(),afterExecute(),和terminated()三个方法,记录任务的开始,结束和退出

为什么不用默认创建的线程池

根据阿里巴巴手册:并发控制这章

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
    • 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
  • 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
    • Executors返回的线程池对象弊端如下:
      • FixedThreadPool和SingleThreadPool:
        • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
      • CacheThreadPool和ScheduledThreadPool
        • 运行的请求队列长度为:Integer.MAX_VALUE,线程数上限太大导致oom

线程池的合理参数

  • 生产环境中如何配置 corePoolSize 和 maximumPoolSize

  • 这个是根据具体业务来配置的,分为CPU密集型和IO密集型

  1. CPU密集型

    意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行

    • CPU密集任务只有在真正的多核CPU上,通过多线程才可能得到加速
    • 在单核CPU上,无论开几个模拟线程该任务都不可能得到加速,因为CPU总的运算能力就那些,CPU密集型任务配置尽可能少的线程数量
    • CPU核数 + 1 线程
  2. IO密集型

    即该任务需要大量的的IO操作,即大量阻塞

    • 在单线程上运行IO密集的任务会导致浪费大量的CPU运算能力花费在等待上
    • 所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
    • 由于IO密集型任务线程并不是一直在执行任务,则可能多的线程,如CPU核数 * 2
    • 公式: CPU核数 / (1 - 阻塞系数),阻塞系数在0.8~0.9左右

Future 模式

Future 模式是多线程开发中常见的一种设计模式,它的核心思想是异步调用。

参与者 作用
Main 系统启动,调用Client发出请求
Client 返回Data对象,立即返回FutureData,并开启ClientThread线程装配RealData
Data 返回数据的接口
FutureData Future数据构造很快,但是是一个虚拟的数据,需要装配RealData
RealDate 真实数据,其构造是比较慢的

JDK中的Future模式

  1. Future接口
  2. RunnableFuture 继承了 Future接口 和 Runnable 接口
  3. 实现类FutureTask
  4. FutureTask类中内部类Sync
  5. Sync会调用Callable接口
posted @ 2022-02-20 16:32  ftfty  阅读(50)  评论(0编辑  收藏  举报