多线程——线程池基础、原理及相关扩展知识

8 篇文章 0 订阅
5 篇文章 0 订阅

接上篇 多线程——基础(生命周期、常用方法、优先级等)

概述

上篇介绍了多线程的基础,其中提到,创建多线程有4种方式,其中的一种,就是这次要说到的线程池

线程池,可以看作是一个缓存空间,里面存放着一池的线程,需要的时候,就拿出来使用,不需要的时候就放回去。
这样就不用每次使用线程都要创建或者销毁,节省了系统的大量资源消耗。同时,线程都存放在线程池,也便于对线程的统一管理。同样的,有了线程池的限制,也可以控制系统的并发数。

1、线程池创建

线程池的顶级接口是Executor类,但我们不使用Executor创建线程池,而是使用ExecutorService来创建线程池。
借用阿里开发手册的一段话说明:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
  

1、OOM(Out Of Memory),即内存耗尽。
2、newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool 都是线程池的类型,后续会说明。

通过ThreadPoolExecutor来创建一个线程池:

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, threadFactory, handler);

参数解析:

  • corePoolSize:线程池的基本大小。

当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

  • maximumPoolSize:线程池最大大小。

线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

  • keepAliveTime:线程活动保持时间。
      	线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
    
  • TimeUnit:线程活动保持时间的单位。可选的单位有:
    • DAYS:天
    • HOURS:小时
    • MINUTES:分钟
    • MILLISECONDS:毫秒
    • MICROSECONDS:微秒(千分之一毫秒)
    • NANOSECONDS:毫微秒(千分之一微秒)
  • runnableTaskQueue任务队列

    用于保存等待执行的任务的阻塞队列

    插入一个概念,阻塞队列,除了在这里作为线程池中存放线程的队列,平时也可以用来在多线程数据共享时使用。典型的例子就是生产者-消费者情况,生产和消费的元素,就存放在阻塞队列里。
    在队列满的情况下插入元素会被阻塞,而在队列空的情况下取出元素,也会被阻塞。

    可以选择以下几个实现了BlockingQueue接口的常见类实现的阻塞队列。

    • ArrayBlockingQueue:有界列队
      是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
      可分为公平非公平访问队列。
      • 公平:按照阻塞的先后顺序访问队列,即先阻塞的先访问。为了保证公平性,会降低吞吐量。
      • 非公平:即不按照阻塞的先后顺序访问队列。
        使用例子
      	//创建一个公平的队列
      	ArrayBlockingQueue a = new ArrayBlockingQueue(1000, true);
      
    • LinkedBlockingQueue:有界列队
      一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。默认容量为:Integer.MAX_VALUE,约等于无界。
      静态工厂方法Executors.newFixedThreadPool()使用了这个队列(后面补充介绍)
      如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
    • SynchronousQueue:直接提交,不存储
      一个不存储元素的阻塞队列,接收到任务的时候,会直接提交给线程处理,而不保留它。
      每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列(后面补充介绍)
    • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
      一个具有优先级得无限阻塞队列。默认自然排序升序排列,也可以自定义实现compareTo(),或初始化时指定构造参数Comparator来指定排序规则。
    • DelayQueue:延迟无界队列
      队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
  • ThreadFactory:用于设置创建线程的工厂。

    可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。

  • RejectedExecutionHandler饱和策略

    当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常
      默认策略,抛出的是RejectedExecutionException异常。
      源码如下:(直接抛出异常,忽略了任务,即丢弃了)
      public static class AbortPolicy implements RejectedExecutionHandler {
          public AbortPolicy() { }
          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
              throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
          }
      }
      
    • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不抛出异常。
      源码如下:(如下可以看出,方法内什么都没做,即丢弃了任务,不做任何处理。)
      public static class DiscardPolicy implements RejectedExecutionHandler {
          public DiscardPolicy() { }
          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          }
      }
      
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
      源码如下:(抛出第一个任务,执行当前)
      public static class DiscardOldestPolicy implements RejectedExecutionHandler {
          public DiscardOldestPolicy() { }
          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
              if (!e.isShutdown()) {
                  e.getQueue().poll();
                  e.execute(r);
              }
          }
      }
      
    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
      调用线程即当前调用多线程处理的线程,一般为主线程。
      源码如下:(调用当前线程执行,可能导致当前线程阻塞。因为当前线程突然被用来执行新的任务了)
      public static class CallerRunsPolicy implements RejectedExecutionHandler {
          public CallerRunsPolicy() { }
          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
              if (!e.isShutdown()) {
                  r.run();
              }
          }
      }
      
    • 自定义拒绝策略:在线程超出容量时,自定义处理方式
      实现RejectedExecutionHandler接口,线程池在超出最大等待容量时,会自动调用RejectedExecutionHandler。可实现如记录日志或持久化不能处理的任务等。
      举个例子:在超出线程池容量时,打印日志。
      static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
          @Override
          public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
              System.out.println("线程超出容量!");
          }
      }
      

2、常用的线程池

ThreadPoolExecutor可以让我们自定义创建线程池,但java也为我们提供了几个配置好了的线程池,方便我们使用:

  • ScheduledThreadPool
    它的核心线程数量是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收。主要用于执行定时任务和具有固定周期的重复任务。支持定时的以及周期性的任务执行。
    例子
    //Executors的newScheduledThreadPool部分源码
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
              return new ScheduledThreadPoolExecutor(corePoolSize);
          }
    //ScheduledThreadPoolExecutor的构造方法源码
    public class ScheduledThreadPoolExecutor 
    	extends ThreadPoolExecutor 
    	implements ScheduledExecutorService {
    
    	public ScheduledThreadPoolExecutor(int corePoolSize) {
    	   super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue());
    	}
    	......
    }
    
    //从源码可以看出,newScheduledThreadPool其实就是调用ThreadPoolExecutor来构造了一个线程池,对外输入为:newScheduledThreadPool。
    //底层都是通过ThreadPoolExecutor构造的。
    
    //创建
    ExecutorService ee = Executors.newScheduledThreadPool(corePoolSize);
    
  • FixedThreadPool
    它是一种线程数量固定的线程池,当线程池处于空闲状态时,它们并不会被回收,除非线程池被关闭了。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来。
    例子
    //Executors内部创建源码
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 
        							0L, TimeUnit.MILLISECONDS,
        							new LinkedBlockingQueue<Runnable>());
    }
    
    //直接用newFixedThreadPool创建
    ExecutorService ee = Executors.newFixedThreadPool(nThreads);
    
  • SingleThreadExecutor
    这类线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。
    例子
    //Executors内部创建源码
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    //直接用newSingleThreadExecutor创建
    ExecutorService ee = Executors.newSingleThreadExecutor();
    
  • CachedThreadPool
    它是一种线程数量不定的线程池,它只有非核心线程,并且其最大线程数为Integer.MAX_VALUE
    当线程池中的线程都是处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务。
    线程池中的空闲线程都有超时机制,这个超时长为60秒,超过60秒闲置线程就会被回收。
    这类线程池比较适合执行大量的耗时较少的任务。当整个线程池都处于闲置状态时,线程池中的线程都会超时而被终止,这时CachedThreadPool中是没有任何线程的,几乎不占用任何系统资源。
    //Executors内部创建源码
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    //直接用newCachedThreadPool创建
    ExecutorService ee = Executors.newCachedThreadPool();
    

由以上4种线程池的实现源码可以发现,他们都是通过创建ThreadPoolExecutor,配置特定参数而成。
所以我们需要的是熟练使用ThreadPoolExecutor,熟悉各种配置参数,这样就能定制出我们需要的各种特定的线程池。

3、线程池执行相关方法

  • execute:
    我们可以使用execute提交任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

    ee.execute(new Runnable(){
    	public void run(){
    		//do something ...
    	}
    });
    
  • submit:
    我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功。
    通过futureget方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

  • 关闭

    • shutdown
      按过去执行已提交任务的顺序发起一个有序的关闭,但是不接受新任务。如果已经关闭,则调用没有其他作用。
      通常shutdown之后调用awaitTermination,作用是:后者会阻塞当前线程,等待剩余任务执行完,然后继续往下执行。如果不使用await,那么shutdown之后,很可能导致剩余任务得不到执行(整个程序退出),或是执行出现异常(某些资源被释放之类)。
      在调用了shutdown后,线程池不再接收新任务。但是正在运行中的任务不会受到影响,会运行到结束。而在等待执行的任务,会尝试取消。
    • shutdownNow
      尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。在从此方法返回的任务队列中排空(移除)这些任务。
  • 判断当前线程池是否真的结束(必须先shutdown):
    executorService.isTerminated()
    若关闭后所有任务都已完成,则返回true。注意除非首先调用shutdownshutdownNow,否则isTerminated永不为true
    但该方式并不能用于所有场景,因为有些时候线程任务断断续续,这样无法正确使用该方法。看情况调用。

  • 主线程等待线程池内线程执行完毕的方式

    • 调用线程池的shutdown后,调用awaitTermination(),设置超时时间。(必须设置超时时间!)
      当使用awaitTermination()时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行。
    //第一个参数指定的是时间,第二个参数指定的是时间单位(当前是秒)。
    boolean b = executorService.awaitTermination(3, TimeUnit.SECONDS);
    
      · 如果等待的时间超过指定的时间,但是线程池中的线程运行完毕,那么awaitTermination()返回true。执行分线程已结束。
      · 如果等待的时间超过指定的时间,但是线程池中的线程未运行完毕,那么awaitTermination()返回false。不执行分线程已结束。
      · 如果等待时间没有超过指定时间,等待!
      	 可以用awaitTermination()方法来判断线程池中是否有继续运行的线程。
    
    • 使用 java.util.concurrent.CountDownLatch
      在初始化时要设定一个count计数。线程调用await()方法后,等待。直到这个count计数为0,等待中的线程才会被释放。
      方法:

      • CountDownLatch(int count):初始化方法。指定计数的次数,只能被设置1次,即不能重置。
      • Long getCount():得到当前的计数。
      • void countDown():调用此方法则计数减1。直到计数为0时,调用了await()方法的等待中的线程,就都会被释放。且此后调用await(),线程也不会再等待了,因为此时计数已经为0,且不能被重置。
      • void await() :调用此方法会一直阻塞当前线程,直到计时器的值为0,除非线程被中断。
      • boolean await(long timeout, TimeUnit unit):调用此方法会一直阻塞当前线程,直到计时器的值为0,除非线程被中断或者计数器超时,返回false代表计数器超时。

      使用场景:一个线程[A],等待其他线程[B]、[c]等完成后,再继续执行。
      常用方式:线程[A]调用await(),线程[B]、[c]等调用countDown()减少计数。需要等待多少个其他线程结束,count计数就设置为多少。每个线程完成后调用一次countDown()将count计数减一,最后都结束了,count计数就为0,线程[A]就从等待中恢复回来继续执行了。

      int tasks = ...;  //设置任务量,作为count计数
      ExecutorService ee = ...;  //创建线程池
      CountDownLatch cDLatch = new CountDownLatch(tasks);  //创建对象
      for (int i = 0; i < tasks; i++) {
      	ee.submit(...);  //提交线程任务(在每个线程里面执行 countDown )
      }
      cDLatch .await();//主线程调用await方法
      
      //等待其他线程都结束后,在主线程里执行操作:关闭线程池
      ee.shutdown();
      

4、结果返回

进行多线程并发的时候,线程的结束是无序的,如果需要获取线程执行完成的结果,有以下几种方式可以参考:

  1. 使用Callable接口配合submit(而非Runnable接口),可以获取返回数据。
    如同Runnable实现run()方法一样,Callable实现call()方法。
    返回结果用 Future<?>接收,调用Future.get()返回获取结果。但是它会阻塞当前线程,直至返回结果。
    Future有以下几个方法:
    get():获取结果内容。
    get (long timeout, TimeUnit unit):同上,加入超时时间,如果超时,抛出异常。
    cancel (boolean):取消任务执行。参数true=立即中断,false=等待结束。
    isCancelled ():任务是否已取消。
    isDone ():任务是否已完成。(非正常情况下的结束,也返回true
    举个例子,加深印象
	//eg1:
	ExecutorService ee = new ThreadPoolExecutor(...);
	Future<Object> f = ee.submit(new Callable(){...});
			
	//eg2:	
	//调用方法:
	ExecutorService ee = Executors.newCachedThreadPool();
    ArrayList<Future<Object>> fList = new ArrayList<Future<Object>>();
    for(int i = 0; i < 10; i++){
        fList.add(ee.submit(new ThreadF(i)));
    }
    for(Future<Object> fs:fList){
        try {
			//Future获取方式1:进行轮询,判断是否结束,获取返回结果
			while(fs.isDone()){
				System.out.println(fs.get());
			}
			//Future获取方式2:直接通过阻塞获取返回
            //System.out.println(fs.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }finally{
            ee.shutdown();
        }
    }

    //Callable接口实现:
    class ThreadF implements Callable<Object>{
        private int num;
        public ThreadF(int num){
            this.num= num;
        }
        public Object call() throws Exception {
            return "num = "+num;//返回内容
        }
    }
  1. 还有一种方式,类似上面的Future。比较简陋,获取不太方便,也是只能通过阻塞获得:FutureTask
    FutureTask类实现了RunnableFuture接口,RunnableFuture接口的继承了RunnableFuture接口。
    所以FutureTask既可以直接作为一个RunnableThread执行,也可以作为Future来得到Callable结果。
    举个例子,加深印象
    //方式1:作为Future得到Callable结果
    Callable mc = new MyCallable();
    ExecutorService ee = Executors.newCachedThreadPool();
    FutureTask<String> ft = new FutureTask<String>(mc);
    ee.submit(ft);
    System.out.println(ft.get()); //获取返回值
    ee.shutdown();
    
    //方式2:作为Runnable被Thread执行
    FutureTask<String> ft = new FutureTask<>(() -> {
    		System.out.println("start");
    		sleep(1000); //休眠,测试阻塞效果
    		return "finish";
    	}); 
    // 使用Thread执行
    ExecutorService ee = Executors.newCachedThreadPool();
    ee.submit(ft);
    System.out.println(ft.get()); //获取返回值
    ee.shutdown();
    
  2. CompletableFuture 弥补了Future模式的缺点,不需阻塞,就可以获取到结果。
    CompletableFuture 是java8的新特性。
    它能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。
    CompletableFuture不需阻塞,在需要获取结果时,可以直接通过thenAcceptthenApplythenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

5、线程池状态监控

ThreadPoolExecutor有几个方法,可以帮助我们监控线程池的状态:

  • getTaskCount()
    返回曾计划执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值。
  • getCompletedTaskCount()
    返回已完成执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值,但是该值在整个连续调用过程中不会减少。
  • getLargestPoolSize()
    线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize()
    线程池的线程数量。
  • getActiveCount()
    返回主动执行任务的近似线程数。
  • isTerminated()
    若关闭后所有任务都已完成,则返回true。注意除非首先调用shutdown()shutdownNow(),否则isTerminated永不为true
  • 通过扩展线程池进行监控
    继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。

6、线程池工作过程

  1. 刚创建好的线程池,内部线程是空的,就算参数队列里面有线程,也不会执行;
  2. 调用了execute() / submit()方法,会添加任务进线程池:
    1. 如果当前运行的线程数小于corePoolSize,则马上创建线程运行这任务;
    2. 如果当前运行线程数大于等于corePoolSize,则将任务放入队列;
    3. 如果当前队列满了,且正在运行线程数小于maximumPoolSize,则创建非核心线程运行这个任务;
    4. 如果当前队列满了,且正在运行线程数大于等于maximumPlloSize,则抛出异常,拒绝执行此任务。
  3. 当一个线程完成任务时,它会从队列取下一个任务来执行;
  4. 当一个线程闲置了,超过一定时间(keepAliveTime)时,线程池会判断:
    1.当前线程运行数大于等于corePoolSize时,这个线程就会被销毁;
    2.当前线程运行数小于corePoolSize时,这个线程就继续限制,等待下一个需要执行的任务。
    所以线程池闲置一段时间后,最终还是会收缩到corePoolSize大小。

7、fork join框架

fork join框架是用来执行并发任务的java框架,下面简单介绍一下。
fork join框架将大任务分割成小任务,最后将小任务聚合起来得到结果。fork是分解的意思, join是收集的意思.

  • ForkJoinTask:
    • 我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。
      它提供在任务中执行fork()join()的操作机制,ForkJoinTask实现了Future接口,可以按照Future接口的方式来使用。
    • ForkJoinTask类中最重要的两个方法fork()join()
      • fork()方法用以一部方式启动任务的执行;
      • join()方法则等待任务完成并返回指向结果。
    • 在创建自己的任务时,最好不要直接继承自ForkJoinTask类,而要继承自ForkJoinTask类的子类RecurisiveTaskRecurisiveAction
      • RecursiveAction,用于没有返回结果的任务;
      • RecursiveTask,用于有返回值的任务。
  • ForkJoinPool
    task要通过ForkJoinPool来执行,分割的子任务也会添加到当前工作线程的双端队列中,进入队列的头部。当一个工作线程中没有任务时,会从其他工作线程的队列尾部获取一个任务。
    • 2个构造方法:
      • ForkJoinPool(int parallelism) 创建一个包含parallelism个并行线程的ForkJoinPool
      • ForkJoinPool()Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool
    • 3种方式启动:
      • 异步执行:execute(ForkJoinTask) ForkJoinTask.fork
      • 等待获取结果:invoke(ForkJoinTask) ForkJoinTask.invoke
      • 执行,获取Future:submit(ForkJoinTask) ForkJoinTask.fork(ForkJoinTask are Futures)

以上就是本章关于线程池的基础介绍。涉及到的相关扩展,这里只是给出一个概念,篇幅关系,就不细讲了。有需要的话可以自行了解。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值