JAVA线程池全解

来源:互联网 发布:迈远短信平台网页源码 编辑:程序博客网 时间:2024/06/04 20:13

我们都知道线程池有很多好处:
通过重复利用已经创建好的线程,可以减少创建线程时的资源消耗。
如果不限制线程的数量,不仅会大量消耗系统内存,还会照成系统的不稳定。
使用线程池,可以控制线程的数量,还可以对所有线程进行统一的管理,好处不言而喻。

一、BlockingQueue

先看一下阻塞队列的代码关系:interface BlockingQueue extends Queue
我们发现这是一个继承了Queue的接口。我们发现这是一个继承了Queue的接口。首先Queue接口有如下方法:

Tables Throws Exception Special Value 插入类 add(o) offer(o) 删除类 remove(o) poll(o) 获取类 element(o) peek(o) 意义 如果这个操作不能立即执行,那么抛出异常 如果这个操作不能立即执行,那么相应返回(true/false)

我们再看看BlockingQueue接口的方法
BlockingQueue有四种类型的插入,删除,和获取元素方法,归类如下:

Tables Throws Exception Special Value Blocks Times Out 插入类 add(o) offer(o) put(o) offer(o, timeout, timeunit) 删除类 remove(o) poll(o) take(o) poll(timeout, timeunit) 插入类 add(o) offer(o) put(o) offer(o, timeout, timeunit) 获取类 element(o) peek(o) 返回意义 若不能立即执行,那么抛出异常 若不能立即执行,那么相应返回(true/false) 若不能立即执行,那么操作阻塞等待直到可以执行 若不能立即执行,那么操作阻塞等待,但是有限定时间,如果超过时间还没有完成,返回错误信息

我们可以很明显的看到BlockingQueue加入了阻塞等待的操作,可以理解成如果队列满了,插入任务就在门口等着,不抛出错误信息,直到有元素从队列中取出,队列有空位了,再进行插入操作,相应的你还可以加入等待超时机制,如果过时了,就不等了。

public class BlockingQueueTest {    public static void main(String[] args) {        //初始化队列长度只有3的队列        final BlockingQueue<String> blockingque = new ArrayBlockingQueue<String>(3);        Thread Putter = new Thread(new Runnable(){            @Override            public void run() {                for(int i=1;i<10;i++){                    try {                        blockingque.put("货物"+i);                        System.out.println("成功往队列中放入货物"+i);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        });        Putter.start();        Thread taker = new Thread(new Runnable(){            @Override            public void run() {                while(true){                    try {                        //取得时间延长,模拟取得时间远大于放入时间                        Thread.sleep(3000);                        String cargo = blockingque.take();                        System.out.println("取出货物: "+cargo);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        });        taker.start();      }}

输出结果为:

成功往队列中放入货物1成功往队列中放入货物2成功往队列中放入货物3取出货物: 货物1成功往队列中放入货物4取出货物: 货物2成功往队列中放入货物5取出货物: 货物3成功往队列中放入货物6取出货物: 货物4成功往队列中放入货物7取出货物: 货物5成功往队列中放入货物8取出货物: 货物6成功往队列中放入货物9取出货物: 货物7取出货物: 货物8取出货物: 货物9

我们可以看到队列长度为3,队列放满3个后,put()方法就处于blocking的状态等待队列有位子
过了3秒后,getter开始从队列中取货物,一有空位,put()方法就得以继续执行。

二、实现一个简单的线程池

首先我们关注一个 “生产者消费者” 的情景。
生产者:不断产生新的任务,比如查询数据库,执行某些业务逻辑等。
消费者:完成任务。

那么把这两者连接起来的就要用到我们的BlockingQueue了。
生产者将不断产生的任务放入到队列中,如果队列满了,生产者等待。
消费者不断的从队列中取出任务解决,当队列空了,消费者等待新任务到来。
首先BlockQueue的长度我们要限制,不然如果解决者的解决能力跟不上生产者的,这个任务队列就会越来越多。
接着我们还需要限定消费者的个数,就是我们所谓的线程池中能同时运行的最多的线程数,如果线程数太多的话会严重影响系统的稳定性。
那么我们根据这两个参数写一个简单的线程池

线程池:

public class ThreadPool {    //用blockingQueue创建一个任务队列,初始化长度为5    private BlockingQueue<Runnable> tasksQueue = new ArrayBlockingQueue<Runnable>(5);    //定义线程池中消费者最大数量    private int consumers = 3;    //这个方法提供给所有的任务生产者,产生新的任务插入    public void insertTask(Runnable task) throws InterruptedException{        tasksQueue.put(task);    }    //线程池的初始化    ThreadPool(){        //激活消费者,等待问题到来        for(int i=1;i<=consumers;i++){            Solver consumer = new Solver(tasksQueue,i);            consumer.start();        }    }}

接下来定义 消费者 逻辑:

public class Solver extends Thread{    //引用线程池的任务队列,消费者不断的从里面取得任务去解决    private BlockingQueue<Runnable> taskQueue = null;    String name;    Solver(BlockingQueue<Runnable> tasks,int name){        this.taskQueue = tasks;        this.name = String.valueOf(name);    }    public void run(){        try {            while(true){                //从队列中取出任务执行,注意这里用了take方法,所以如果队列空了,那么线程会等待,直到有任务来了,继续执行                Runnable task = taskQueue.take();                System.out.println("消费者"+name+"接收了一个任务");                task.run();                System.out.println("消费者"+name+"解决了一个任务");            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

我们在上面的例子可以看到。
这个线程池中最大的线程数是3,就是最多只能同时有3个消费者线程执行,消费者会监视线程池的任务队列,只要队列中有任务,就会取出来执行。

接下来我们定义 生产者 的逻辑:

public class ProblemCreater {    public static void main(String[] args) throws Exception {        //初始化线程池        ThreadPool threadPool = new ThreadPool();        //生成者不断产生任务        for(int i=1;i<10;i++){            //定义一个新的任务            Runnable task = new Runnable(){                public void run(){                    Random random = new Random();                    //随机一个数字模拟需要解决的时间                    int randomTime = Math.abs(random.nextInt())%20;                    //System.out.println("这个任务需要解决时间为:"+randomTime);                    try {                        Thread.sleep(randomTime*1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            };            //将问题插入到线程池任务队列中            threadPool.insertTask(task);            System.out.println("插入新的任务"+i);        }    }}

到此我们已经实现好了一个非常简单的线程池,将线程的创建与执行过程分离开,而不是将线程的生命周期管理和任务的执行过程绑定在一起,如果只是想简单的丢一个任务进去执行,我们只需要将任务的执行过程封装到一个Runnable接口中就可以了。而对于那些需要返回结果的任务,我们可以将其封装到Callable接口里面。

三、ExecutorService接口以及ThreadPoolExecutor

我们来了解一下java为我们提供的线程池实现—— ExecutorService接口
它位于jdk的java.util.concurrent包下。
JDK提供了这么两个类来实现这个接口:

  1. ThreadPoolExecutor
  2. ScheduledThreadPoolExecutor

我们这篇文章只介绍一下ThreadPoolExecutor类(ScheduledThreadPoolExecutor类类似,多加入了计划任务功能。
我们首先看看怎么用ThreadPoolExecutor类初始化一个线程池:

//初始化一个线程池  //核心线程数  int  corePoolSize  = 5;  //最大线程数  int  maxPoolSize   = 10;  //空闲线程最大存活时间  long keepAliveTime = 5000;  //任务队列  BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(5);  ExecutorService threadPoolExecutor =          new ThreadPoolExecutor(                  corePoolSize,                  maxPoolSize,                  keepAliveTime,                  TimeUnit.MILLISECONDS,                  queue          );  

这个类完全实现了一个类似于我们上一篇文章中实现的线程池,它包含以下几个属性:
1. corePoolSize
核心线程数:即使没有任何任务过来,线程池里面也会有保持的最基本线程数。
2. maximumPoolSize
最大线程数(即使任务特别多,线程池里的线程数也不会超过它)
3. keepAliveTime
空闲线程最大存活时间
4. blockingQueue
任务队列,用来存放待处理的任务。可以选择以下几个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
那么整个类的结构就如下图所示:

要使用这个线程池的话,可以使用它提供给我们的如下方法:

  1. execute(Runnable)
  2. submit(Runnable)
  3. submit(Callable)
  4. invokeAny(…)
  5. invokeAll(…)

execute()和submit()可以向这个线程池提交单个任务。他们的区别是:
使用execute提交任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。
使用submit 方法来提交任务,它会返回一个future对象,那么我们可以通过这个future对象来判断任务是否执行成功
invokeAll可以直接把一个List类型的任务列表一次性的提交给线程池执行。


那么接下来我们就用 ThreadPoolExecutor 来创建一个线程池,改写一下我们上一篇文章的例子:

public class ProblemCreater {      public static void main(String[] args) throws Exception {          //初始化线程池          //核心线程数          int  corePoolSize  = 5;          //最大线程数          int  maxPoolSize   = 10;          long keepAliveTime = 5000;          ExecutorService threadPoolExecutor =                  new ThreadPoolExecutor(                          corePoolSize,                          maxPoolSize,                          keepAliveTime,                          TimeUnit.MILLISECONDS,                          new ArrayBlockingQueue<Runnable>(5)                  );          //生成者不断产生任务          for(int i=1;i<10;i++){              //定义一个新的任务              Runnable task = new Runnable(){                  public void run(){                      Random random = new Random();                      //随机一个数字模拟需要解决的时间                      int randomTime = Math.abs(random.nextInt())%20;                      try {                          Thread.sleep(randomTime*1000);                          System.out.println("任务完成,花费时间为:"+randomTime);                      } catch (InterruptedException e) {                          e.printStackTrace();                      }                  }              };              //将问题插入到线程池任务队列中              threadPoolExecutor.execute(task);              System.out.println("插入新的任务"+i);          }      }  }  

我们再回头看看这个 ThreadPoolExecutor 的初始化,我们需要给它传递5个参数。核心线程数,最大线程数,线程存活时间,时间单位,还有阻塞队列的类型。
这个过程还是比较繁琐的。其实Java帮我们简化了这个过程,我们可以根据不同的情景,直接用一行代码创建一个合适的线程池。
实现这个功能的就是 java.util.concurrent.Executors类。

四、Executors类

上一章我们介绍了ExecutorService接口,以及它的实现类ThreadPoolExecutor。
那么这里我们将介绍Executors类,它可以更进一步的简化我们的工作,直接创建一些预定义过的线程池

这个类也在java.util.concurrent包下。它有如下的几个比较常用的创建线程池的方法:

一:newFixedThreadPool

创建一个线程可重用的并且线程数固定的线程池。

二:newCachedThreadPool

创建一个可根据实际情况动态维持线程数的线程池,当任务到来时,如果有已经构造好的空闲线程将重用它们,不创建新的线程。
如果没有可用的空闲线程,则创建一个新线程并添加到池中。并且会终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

三:newSingleThreadExecutor

创建一个使用单个线程的 ExecutorService,以无界队列方式来运行该线程。
关于这3类的线程池我引用一下《thinking in java》对它们的描述:

使用FixedThreadPool,你可以一次性的预先执行代价高昂的线程分配,因而也就可以限制线程数的数量,这可以节省时间,因为你不用为每一个任务都固定的付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如你所愿地尽快得到服务。你不会滥用可获得的资源,因为FixedThreadPool使用的Thread对象的数量是有界的。

对于CachedThreadPool,它在程序的执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的ExecutorService的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。

SingleThreadExecutor就是线程数量为1的FixedThreadPool。这对于你希望在另一个线程中连续运行的任何事物(长期存活的任务)来说,都是非常有用的,例如监听进入套接字连接的任务


我们再来看一下它们的源码,比如FixedThreadPool:

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

所以在本质上,就是创建了一个我们上一篇文章介绍过的ThreadPoolExecutor类。


接下来我们写一个完整的,使用它们的例子:
下面给出了一个网络服务的简单结构,这里线程池中的线程作为传入的请求。它使用了预先配置的 Executors.newFixedThreadPool(int) 方法创建线程池:

class NetworkService implements Runnable {      private final ServerSocket serverSocket;      private final ExecutorService pool;      public NetworkService(int port, int poolSize)          throws IOException {        serverSocket = new ServerSocket(port);        pool = Executors.newFixedThreadPool(poolSize);      }      public void run() { // run the service        try {          for (;;) {            pool.execute(new Handler(serverSocket.accept()));          }        } catch (IOException ex) {          pool.shutdown();        }      }  }  class Handler implements Runnable {      private final Socket socket;      Handler(Socket socket) { this.socket = socket; }      public void run() {        // read and service request on socket      }  }  

五、ThreadPoolExecutor源码分析

通过前面的章节,我们学习了可以通过ThreadPoolExecutor来创建一个线程池。
那么接下来我们分析一下ThreadPoolExecutor的源码,看看它具体是如何工作的。
我们看一下使用execute(Runnable task)执行一个任务的时候,到底发生了什么(代码进过简化):
先简单描述一下当我们提交一个任务到线程池中后发生了什么:
这里写图片描述

    public void execute(Runnable task) {        //取出当前线程池活跃的线程数。        //ctl是一个原子类型的对象(final AtomicInteger ctl),用来保存当前线程池的线程数以及线程池的状态。        int c = ctl.get();        //如果当前的活跃线程数小于核心线程数,即使现在有空闲线程,也创建一个新的线程,去执行这个任务        if (workerCountOf(c) < corePoolSize) {            //创建一个新的线程,去执行这个任务。            if (addWorker(task, true))                return;            //如果执行到这一句说明任务没有分配成功。            //所以获得当前线程池状态值,为后面的检查做准备。            c = ctl.get();        }        //如果大于核心线程数,检查一下线程池是否还处于运行状态,并尝试把任务放入到blockingQueue任务队列中。        if (isRunning(c) && workQueue.offer(task)) {            //这里再次检查一下线程池的状态            int recheck = ctl.get();            if (! isRunning(recheck) && remove(task))                //如果线程池不处于运行状态的话,就把我们刚才添加进任务队列中的任务移出,并拒绝这个任务。                reject(task);            //检查如果当前线程池中的线程数,如果为0了,就为线程池创建新线程(因为有可能之前存活的线程在上一次检查过后死亡了)            else if (workerCountOf(recheck) == 0)                addWorker(null, false);        }        //执行到这一句,说明队列满了。这时,如果当前线程池中的线程数还没有超过最大线程数,就创建一个新的线程去执行这个任务,如果失败就拒绝这个任务。        else if (!addWorker(task, false))            reject(task);    }

六、可以返回结果的“Runnable”

我们知道ExecutorService框架使用Runnble作为其基本的任务表示形式。但是它有很大的局限性就是它不能返回一个值,或者抛出一个受检查的异常
实际上许多需要知道执行结果的任务都需要一定的执行时间的,比如执行数据库的查询,或者从网络上获取一些资源,更或者进行一些比较复杂的计算。对于这些任务Callable是一种更好的抽象。你可以把它当成有返回值的“Runnable”,它的call()方法就相当于Runnable的run()方法。但关键是它的call()方法将返回一个值,并可能抛出一个异常。
那么怎么在ExecutorService框架中很好的使用Callable呢。这里就需要使用到Feture接口。
ExecutorService中的所有submit方法都将返回一个Future对象,从而可以将Callable提交给ExecutorService,并得到一个Future用来获得任务的执行结果或者取消任务。

public class Test {      private final ExecutorService executor = Executors.newFixedThreadPool(3);      public void runTheTask(){          Future<String> future = executor.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  Thread.sleep(3000);                  return "result";              }        });          try {              System.out.println( future.get());          } catch (InterruptedException | ExecutionException e) {          }      }     public static void main(String args[]){          Test t = new Test();          t.runTheTask();      }  }  

运行后在future.get()步会阻塞 直到3秒后 返回结果“result”
上面的线程只执行了一个Callable任务。
但有某些情景下需要我们执行好几个Callable任务,并且要获得它们的返回结果,代码就变得很不好控制了

public class Test {      private final ExecutorService executor = Executors.newFixedThreadPool(3);      public void runTheTask(){          Future<String> future1 = executor.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  Thread.sleep(3000);                  return "result1";              }          });          Future<String> future2 = executor.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  Thread.sleep(3000);                  return "result2";              }          });          Future<String> future3 = executor.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  Thread.sleep(3000);                  return "result3";              }          });          try {              System.out.println( future1.get());              System.out.println( future2.get());              System.out.println( future3.get());          } catch (InterruptedException | ExecutionException e) {          }      }      public static void main(String args[]){          Test t = new Test();          t.runTheTask();      }  }  

可以看到 上面的代码非常难看而且不好控制。
幸运的是,在这种情况下我们可以使用CompletionService来实现
CompletionService将ExecutorService和BlockingQueue功能融合在了一起。
你可以将Callable任务提交给它来执行,它执行完返回的Future结果会放进BlockingQueue中。
你再用类似于队列操作的take和poll方法来获得已完成的结果。

public class Test2 {      private final ExecutorService executor = Executors.newFixedThreadPool(3);      public void runTasks(){          CompletionService<String> completionService = new ExecutorCompletionService<String>(executor);          completionService.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  return "result1";              }          });          completionService.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  return "result2";              }          });          completionService.submit(new Callable<String>(){              @Override              public String call() throws Exception {                  return "result3";              }          });          for(int i=0;i<3;i++){              try {                  Future<String> f = completionService.take();                  System.out.println(f.get());              } catch (InterruptedException e) {              } catch (ExecutionException e) {              }          }      }      public static void main(String[] args) {          Test2 t = new Test2();          t.runTasks();      }  }  

我们可以看到我们使用了一个ExecutorService作为参数来初始化CompletionService。多个CompletionService可以共享一个ExecutorService。因此可以创建一个对于特定计算私有,又能共享一个ExecutorService的应用。

七、线程池的关闭——shutdown()和shutdownNow()方法。

我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。对shutdown()方法的调用可以防止新的任务被提交给这个线程池,当前线程将继续运行在shutdown()被调用之前提交的所有任务

shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

我们加入shutdown()和shutdownNow()方法来完善一下上面这个例子:

void shutdownAndAwaitTermination(ExecutorService pool) {     pool.shutdown(); // 防止新的任务被提交上来     try {       // 等待当前已经存在的任务执行完       if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {         pool.shutdownNow(); // 如果过了指定时间还有任务没有完成,立马停止它们         // 等待任务响应取消命令         if (!pool.awaitTermination(60, TimeUnit.SECONDS))             System.err.println("Pool did not terminate");       }     } catch (InterruptedException ie) {       // (Re-)Cancel if current thread also interrupted       pool.shutdownNow();       // Preserve interrupt status       Thread.currentThread().interrupt();     }   }  

八、线程池的合理配置

一、确定线程数

在工作中,为了加快程序的处理速度,我们需要将问题分解成若干个并发执行的任务。接着我们将这些任务委派给线程,以便使它们可以并发的执行。但是需要注意的是,由于资源所限,我们不能创建过多的线程。
这就涉及到一个 确定创建多少线程数才是合理 的问题。
《java虚拟机并发编程》一书中,对这个问题有详尽的解答,本人在此摘取归纳如下:

1.我们可以先获取到系统可用的处理器核心数:

Runtime.getRuntime().availableProcessors()

2.确定任务的类型:

如果所有任务都是计算密集型的,则创建处理器可用核心数那么多的线程数就可以了。
在这种情况下,创建更多的线程对程序的性能而言反而是不利的。因为当有多个任务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。
如果任务都是IO密集型的,那么我们需要开更多的线程来提高性能。
当一个任务执行IO操作时,其线程被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器可用核心数那么多线程的话,则即使有待执行的任务也无法处理,因为我们已经拿不出更多的线程供处理器调度了。

3.计算出程序所需的线程数:

首先我们要明白一个概念叫 阻塞系数
如果任务有50%的时间处于阻塞状态,则阻塞系数为0.5。则程序所需的线程数为处理器可用核心数的两倍。如果任务被阻塞的时间少于50%,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应该低于处理器的核心数。如果任务被阻塞的时间大于执行时间,即该任务是IO密集型的,我们就需要创建比处理器核心数大几倍数量的线程。
我们可以计算出程序所需线程的总数,总结如下:

线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。

计算密集型人物的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。

二、线程池的监控:

我们可以通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
taskCount:线程池需要执行的任务数量。
completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:

protected void beforeExecute(Thread t, Runnable r) { }  
0 0
原创粉丝点击