Java线程池使用与原理详解

线程池是什么?

我们可以利用java很容易创建一个新线程,同时操作系统创建一个线程也是一笔不小的开销。所以基于线程的复用,就提出了线程池的概念,我们使用线程池创建出若干个线程,执行完一个任务后,该线程会存在一段时间(用户可以设定空闲线程的存活时间,后面会介绍),等到新任务来的时候就直接复用这个空闲线程,这样就省去了创建、销毁线程损耗。当然空闲线程也会是一种资源的浪费(所有才有空闲线程存活时间的限制),但总比频繁的创建销毁线程好太多。
下面是我的测试代码

  /*
   * @TODO 线程池测试
   */
  @Test
  public void threadPool(){

    /*java提供的统计线程运行数,一开始设置其值为50000,每一个线程任务执行完
     * 调用CountDownLatch#coutDown()方法(其实就是自减1)
     * 当所有的线程都执行完其值就为0
    */
    CountDownLatch count = new CountDownLatch(50000);
    long start = System.currentTimeMillis();
    Executor pool = Executors.newFixedThreadPool(10);//开启线程池最多会创建10个线程
    for(int i=0;i<50000;i++){
      pool.execute(new Runnable() {
        @Override
        public void run() {
          System.out.println("hello");
          count.countDown();
        }
      });
    }

    while(count.getCount()!=0){//堵塞等待5w个线程运行完毕

    }
    long end = System.currentTimeMillis();
    System.out.println("50个线程都执行完了,共用时:"+(end-start)+"ms");
  }

  /**
   *@TODO 手动创建线程测试
   */
  @Test
  public void thread(){
    CountDownLatch count = new CountDownLatch(50000);
    long start = System.currentTimeMillis();
    for(int i=0;i<50000;i++){
      Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
          System.out.println("hello");
          count.countDown();
        }
      });
      thread.start();
    }

    while(count.getCount()!=0){//堵塞等待5w个线程运行完毕

    }
    long end = System.currentTimeMillis();
    System.out.println("50000个线程都执行完了,共用时:"+(end-start)+"ms");

  }

使用线程池5w线程运行完大约为400ms,不使用线程池运行大约为4350ms左右,其效率可见一斑(读者可以自行测试,不过由于电脑配置不一样,跑出来的数据会有差别,但使用线程池绝对是比创建线程要快的)。

java如何使用线程池?

上面的测试代码中已经使用了线程池,下面正式介绍一下。

java所有的线程池最顶层是一个Executor接口,其只有一个execute方法,用于执行所有的任务,java又提供了ExecutorService接口继承自Executor并且扩充了一下方法,在往下就是AbstractExecutorService这个抽象类,其实现了ExecutorService,最后就是ThreadPoolExecutor其继承自上面的抽象类,我们常使用的java线程池就是创建的这个类的实例。

而上面我们使用Executors是一个工具类,它就是一个语法糖,为我们把各种不同的业务的线程池参数进行封装,进行new操作。

 public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                   0L, TimeUnit.MILLISECONDS,
                   new LinkedBlockingQueue<Runnable>());
  }

上面就是Executors.newFixedThreadPool(10)的源码。

下面重点来了,说一说ThreadPoolExecutor构造方法各参数的意思。

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

上面这个构造方法是最全的。

下面我们根据源码来解释部分参数意思,这样更有说服力。

下面是ThreadPoolExecutor#execute方法,就是我们上面接口调用的execute实际执行者。

 public void execute(Runnable command) {
    if (command == null)
      throw new NullPointerException();

    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
      if (addWorker(command, true))
        return;
      c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
      int recheck = ctl.get();
      if (! isRunning(recheck) && remove(command))
        reject(command);
      else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
    }
    else if (!addWorker(command, false))
      reject(command);
  }

ctl是一个AtomicInteger实例,是一个提供了原子语句的CAS操作的类,它用来记录线程池中当前运行的线程数量加上-2^29,workCountOf方法就取得其绝对值(可以去看源码如何实现),当其小于corePoolSize时,会调用addWorker方法(是用来创建一个新Workder,Workder会创建一个Thread,所以就是创建线程的方法),addWorkd创建线程过程中会跟corePoolSize或者maxnumPoolSize的值比较(当传入true会根corePoolSize比较,false会根据maxnumPoolSize比较,大于等于其值会创建失败)。可见如何当前运行中的线程数量小于corePoolSize就是创建并且也会创建成功(
只简单的讨论线程池Running状态下)。

如果当运行中线程数大于等于corePoolSize时,进入第二个if,isRunning是跟SHUTDOWN(其值=0)比较,之前说过c等于当前运行的线程数量加上-2^29,如果当前当前运行的线程数据达到2^29时其值就=0,isRunning返回false,else中在执行addWorkd也会返回false(addWorkd也对其进行了检验),所以这表示线程池最多能支持2^29个线程同时运行(足够用了)。

workQueue.offer(command)就是将runnable加入等待队列,加入等待队列后runWorker方法会从队列中获取任务执行的。如果当前队列采用的是有界队列(ArrayBlockingQueue)当队列满了offer就会返回false,这是就进入else if,看!这里传入了false,说明这里要跟maxnumPoolSize比较了,如果这里运行的线程数大于等于maxnumPoolSize,那么这个线程任务就要被线程池拒绝了,执行reject(command),拒绝方法中使用了我们ThreadPoolExecutor构造方法中的RejectedExecutionHandler(拒绝策略),后面再详细解释。

经过上面的结合源码的介绍,下面对们ThreadPoolExecutor的参数介绍就好理解了。

线程池中线程创建和拒绝策略

corePoolSize,maxnumPoolSize,BlockingQueue这三个要一块说

当线程池运行的线程小于corePoolSize时,来一个新线程任务总是会新建一个线程来执行;当大于corePoolSize就会把任务加入到等待队列blockingQueue中,如果你传入的BlockingQueue是一个无界队列(LinkedBlockingQueue)这是队列可以存放“无穷多”的任务,所有总是会加入队列成功,跟maxnumPoolSize就没关系了,这也表示线程池中线程数最多为corePoolSize个;但是如果你传入的是有界队列(ArrayBlockingQueue,SynchronousQueue),当队列满时,并且线程数小于maxmunPoolSize就是创建新的线程直至线程数大于maxnumPoolSize;如果当线程数量大于maxnumPoolSize时,在加入任务就会被线程池拒绝。

RejectedExecutionHandler拒绝策略java给实现了4个AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy用户也可以自己实现该接口实现自己的拒绝策略;第一个就是直接抛出异常,我们可以进行trycatch处理;第二个就是该新任务直接运行;第三个是取消队列中最老的;第四个是取消当前任务。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } } ).start(); 那你就out太多了,new Thread的弊端如下: a. 每次new Thread新建对象性能差. b. 线程缺乏统一管理,可能无限

  • Java Socket编程实例(三)- TCP服务端线程池

    一.服务端回传服务类: import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.logging.Level; import java.util.logging.Logger; public class EchoProtocol implements Runnable { private static f

  • Java 线程池详解及实例代码

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁.如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因. 例如Android中常见到的很多通用组件一般都离不开"池"的概念,如各种图片

  • 详谈Java几种线程池类型介绍及使用方法

    一.线程池使用场景 •单个任务处理时间短 •将需处理的任务数量大 二.使用Java线程池好处 1.使用new Thread()创建线程的弊端: •每次通过new Thread()创建对象性能不佳. •线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom. •缺乏更多功能,如定时执行.定期执行.线程中断. 2.使用Java线程池的好处: •重用存在的线程,减少对象创建.消亡的开销,提升性能. •可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞

  • 深入java线程池的使用详解

    在Java 5.0之前启动一个任务是通过调用Thread类的start()方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制 同时执行的线程数量就需要额外编写代码来完成.5.0里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接 池的线程池来执行任务.这个架构主要有三个接口和其相应的具体类组成.这三个接口是Executor, ExecutorService.ScheduledExecutorService,让我们先用一个图

  • Java 线程池ExecutorService详解及实例代码

    Java 线程池ExecutorService 1.线程池 1.1什么情况下使用线程池 单个任务处理的时间比较短. 将需处理的任务的数量大. 1.2使用线程池的好处 减少在创建和销毁线程上所花的时间以及系统资源的开销. 如果不使用线程池,有可能造成系统创建大量线程而导致消耗系统内存以及"过度切换"; 2.ExecutorService和Executors 2.1简介 ExecutorService是一个接口,继承了Executor, public interface ExecutorS

  • java中通用的线程池实例代码

    复制代码 代码如下: package com.smart.frame.task.autoTask; import java.util.Collection;import java.util.Vector; /** * 任务分发器 */public class TaskManage extends Thread{    protected Vector<Runnable> tasks = new Vector<Runnable>();    protected boolean run

  • Java线程池的几种实现方法和区别介绍

    Java线程池的几种实现方法和区别介绍 import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.E

  • Java代码构建一个线程池

    在现代的操作系统中,有一个很重要的概念――线程,几乎所有目前流行的操作系统都支持线程,线程来源于操作系统中进程的概念,进程有自己的虚拟地址空间以及正文段.数据段及堆栈,而且各自占有不同的系统资源(例如文件.环境变量等等).与此不同,线程不能单独存在,它依附于进程,只能由进程派生.如果一个进程派生出了两个线程,那这两个线程共享此进程的全局变量和代码段,但每个线程各拥有各自的堆栈,因此它们拥有各自的局部变量,线程在UNIX系统中还被进一步分为用户级线程(由进程自已来管理)和系统级线程(由操作系统的调

  • 支持生产阻塞的Java线程池

    通常来说,生产任务的速度要大于消费的速度.一个细节问题是,队列长度,以及如何匹配生产和消费的速度. 一个典型的生产者-消费者模型如下:   在并发环境下利用J.U.C提供的Queue实现可以很方便地保证生产和消费过程中的线程安全.这里需要注意的是,Queue必须设置初始容量,防止生产者生产过快导致队列长度暴涨,最终触发OutOfMemory. 对于一般的生产快于消费的情况.当队列已满时,我们并不希望有任何任务被忽略或得不到执行,此时生产者可以等待片刻再提交任务,更好的做法是,把生产者阻塞在提交任

随机推荐