Java线程池原理和使用

为什么要用线程池?

诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP、FTP 或 POP)、通过 JMS 队列或者可能通过轮询数据库。不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。

构建服务器应用程序的一个过于简单的模型应该是:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务。实际上,对于原型开发这种方法工作得很好,但如果试图部署以这种方式运行的服务器应用程序,那么这种方法的严重不足就很明显。每个请求对应一个线程(thread-per-request)方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。

除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

2、ThreadPoolExecutor类介绍

       Java中的线程池技术主要用的是ThreadPoolExecutor 这个类。先来看这个类的构造函数,

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,

BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 

    corePoolSize       线程池维护线程的最少数量

    maximumPoolSize    线程池维护线程的最大数量 

    keepAliveTime      线程池维护线程所允许的空闲时间  

    workQueue          任务队列,用来存放我们所定义的任务处理线程

    threadFactory      线程创建工厂

    handler            线程池对拒绝任务的处理策略

     ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize 设置的边界自动调整池大小。当新任务在方法

execute(Runnable) 中提交时, 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是

空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。 如果设置的

corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。

     ThreadPoolExecutor是Executors类的实现Executors类里面提供了一些静态工厂,生成一些常用的线程池,主

要有以下几个:

     newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行

所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任

务的提交顺序执行。  

     newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线

程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

     newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分

空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池

大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

      在实际的项目中,我们会使用得到比较多的是newFixedThreadPool,创建固定大小的线程池,但是这个方法在真实的线上

环境中还是会有很多问题,这个将会在下面一节中详细讲到。

      当任务源源不断的过来,而我们的系统又处理不过来的时候,我们要采取的策略是拒绝服务。RejectedExecutionHandler接

口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。

      1)CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

             if (!e.isShutdown()) {

                 r.run();

            }

        }

这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。

     2)AbortPolicy处理程序遭到拒绝将抛出运行时 RejectedExecutionException

         public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

              throw new RejectedExecutionException();

        }

 这种策略直接抛出异常,丢弃任务。

      3)DiscardPolicy不能执行的任务将被删除

          public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}

   这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。

     4)DiscardOldestPolicy如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,

则重复此过程)

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

            if (!e.isShutdown()) {

                e.getQueue().poll();

                e.execute(r);

            }

        }

      该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略

需要适当小心

3、  ThreadPoolExecutor无界队列使用

   public class ThreadPool {

        private final static String poolName = "mypool";

        static private ThreadPool threadFixedPool = new ThreadPool(2);

       private ExecutorService executor;

      static public ThreadPool getFixedInstance() {

           return threadFixedPool;

       }

    private ThreadPool(int num) {

           executor = Executors.newFixedThreadPool(num, new DaemonThreadFactory(poolName));

}

public void execute(Runnable r) {

           executor.execute(r);

}

public static void main(String[] params) {

           class MyRunnable implements Runnable {

                    public void run() {

                             System.out.println("OK!");

                             try {

                                       Thread.sleep(10);

                             } catch (InterruptedException e) {

                                       e.printStackTrace();

                             }

                    }

           }

           for (int i = 0; i < 10; i++) {

             ThreadPool.getFixedInstance().execute(new MyRunnable());

           }

           try {

                    Thread.sleep(2000);

                    System.out.println("Process end.");

           } catch (InterruptedException e) {

                    e.printStackTrace();

           }

}

}

    在这段代码中,我们发现我们用到了Executors.newFixedThreadPool()函数,这个函数的实现是这样子的:

return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); 

       它实际上是创建了一个无界队列的固定大小的线程池。执行这段代码,我们发现所有的任务都正常处理了。但是在真实的线上环

境中会存在这样的一个问题,前端的用户请求源源不断的过来,后端的处理线程如果处理时间变长,无法快速的将用户请求处理

完返回结果给前端,那么任务队列中将堵塞大量的请求。这些请求在前端都是有超时时间设置的,假设请求是通过套接字过来,

当我们的后端处理进程处理完一个请求后,从队列中拿下一个任务,发现这个任务的套接字已经无效了,这是因为在用户端已经

超时,将套接字建立的连接关闭了。这样一来我们这边的处理程序再去读取套接字时,就会发生I/0 Exception. 恶性循环,导致我

们所有的处理服务线程读的都是超时的套接字,所有的请求过来都抛I/O异常,这样等于我们整个系统都挂掉了,已经无法对外提供

正常的服务了。

     对于海量数据的处理,现在业界都是采用集群系统来进行处理,当请求的数量不断加大的时候,我们可以通过增加处理节点,反正现

在硬件设备相对便宜。但是要保证系统的可靠性和稳定性,在程序方面我们还是可以进一步的优化的,我们下一节要讲述的就是针对

线上出现的这类问题的一种处理策略。



4、ThreadPoolExecutor有界队列使用

public class ThreadPool {

         private final static String poolName = "mypool";

         static private ThreadPool threadFixedPool = null;

         public ArrayBlockingQueue queue = new ArrayBlockingQueue(2);

         private ExecutorService executor;

 

         static public ThreadPool getFixedInstance() {

                   return threadFixedPool;

         }

         private ThreadPool(int num) {

                   executor = new ThreadPoolExecutor(2, 4,60,TimeUnit.SECONDS, queue,new DaemonThreadFactory

(poolName), new ThreadPoolExecutor.AbortPolicy());

         }

         public void execute(Runnable r) {

                   executor.execute(r);

         }

        

         public static void main(String[] params) {

                   class MyRunnable implements Runnable {

                            public void run() {

                                     System.out.println("OK!");

                                     try {

                                               Thread.sleep(10);

                                     } catch (InterruptedException e) {

                                               e.printStackTrace();

                                     }

                            }

                   }

                   int count = 0;

                   for (int i = 0; i < 10; i++) {

                            try {

                                     ThreadPool.getFixedInstance().execute(new MyRunnable());

                            } catch (RejectedExecutionException e) {

                                     e.printStackTrace();

                                     count++;

                            }

                   }

                   try {

                            log.info("queue size:" + ThreadPool.getFixedInstance().queue.size());

                            Thread.sleep(2000);

                   } catch (InterruptedException e) {

                            e.printStackTrace();

                   }

                   System.out.println("Reject task: " + count);

         }

}

  首先我们来看下这段代码几个重要的参数,corePoolSize 为2,maximumPoolSize为4,任务队列大小为2,每个任务平

均处理时间为10ms,一共有10个并发任务。

      执行这段代码,我们会发现,有4个任务失败了。这里就验证了我们在上面提到有界队列时候线程池的执行顺序。当新任务在

方法 execute(Runnable) 中提交时, 如果运行的线程少于 corePoolSize,则创建新线程来处理请求。 如果运行的线程多于

corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程,如果此时线程数量达到maximumPoolSize,并且队

列已经满,就会拒绝继续进来的请求。

    现在我们调整一下代码中的几个参数,将并发任务数改为200,执行结果Reject task: 182,说明有18个任务成功了,线程处理

完一个请求后会接着去处理下一个过来的请求。在真实的线上环境中,会源源不断的有新的请求过来,当前的被拒绝了,但只要线

程池线程把当下的任务处理完之后还是可以处理下一个发送过来的请求。

     通过有界队列可以实现系统的过载保护,在高压的情况下,我们的系统处理能力不会变为0,还能正常对外进行服务。

5、举例分析

  • execute(Runnable command):履行Ruannable类型的任务
  • submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
  • invokeAll(collection of tasks):执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表.
  • shutdown():在完成已提交的任务后封闭办事,不再接管新任务
  • shutdownNow():停止所有正在履行的任务并封闭办事。
  • isTerminated():测试是否所有任务都履行完毕了。
  • isShutdown():测试是否该ExecutorService已被封闭

1、固定大小线程池

import java.util.concurrent.Executors;  
import java.util.concurrent.ExecutorService;

ExecutorService pool = Executors.newFixedThreadPool(2);

pool.execute(t1);

pool.shutdown();

2、单任务线程池

ExecutorService pool = Executors.newSingleThreadExecutor();

3、可变尺寸线程池

ExecutorService pool = Executors.newCachedThreadPool();

4、延迟连接池

import java.util.concurrent.Executors;  
import java.util.concurrent.ScheduledExecutorService;  
import java.util.concurrent.TimeUnit;

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

pool.schedule(t4, 10, TimeUnit.MILLISECONDS);

5、单任务延迟连接池

ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

例子分析:

  schedule(Runnable command, long delay, TimeUnit unit),schedule方法被用来延迟指定时间后执行某个指定任务。

public class Job implements Runnable { 
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 
public void run() { 
try { 
            Thread.sleep(5000); 
        } catch (InterruptedException ex) { 
            ex.printStackTrace(); 
        } 
        System.out.println("do something  at:" + sdf.format(new Date())); 
    } 
} 
public class ScheduledExecutorServiceTest { 
public static void main(String[] args) { 
        ScheduledExecutorService schedule = Executors.newScheduledThreadPool(5); 
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 
        System.out.println(" begin to do something at:" + sdf.format(new Date())); 
        schedule.schedule(new Job(),1, TimeUnit.SECONDS); 
    } 
} 

输出如下:

  1. begin to do something at:2012-08-03 09:31:36
  2. do something  at:2012-08-03 09:31:42

注:此时程序不会推出,若想让程序推出,需要加上schedule.shutdown();

ScheduledExecutorService 中两种最常用的调度方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。ScheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialDelay, initialDelay+period, initialDelay+2*period, …;ScheduleWithFixedDelay 每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay, initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay。由此可见,ScheduleAtFixedRate 是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。

 

2.scheduleWithFixedDelay 
         scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit) 
         创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟,如果任务的执行时间超过了廷迟时间(delay),下一个任务则会在 
(当前任务执行所需时间+delay)后执行。 

public class ScheduledExecutorServiceTest { 
public static void main(String[] args) { 
            ScheduledExecutorService schedule = Executors.newScheduledThreadPool(5); 
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 
            System.out.println(" begin to do something at:" + sdf.format(new Date())); 
            schedule.scheduleWithFixedDelay(new Job(), 1, 2, TimeUnit.SECONDS); 
        } 
    } 

   输出如下:

  1. begin to do something at:2012-08-03 09:36:53
  2. do something at:2012-08-03 09:36:59
  3. do something at:2012-08-03 09:37:06
  4. do something at:2012-08-03 09:37:13

3.scheduleAtFixedRate 
         scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnitunit) 
         创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。 如果任务的执行时间小于period,将会按上述规律执行。否则,则会按 任务的实际执行时间进行周期执行。 
    

public class ScheduledExecutorServiceTest { 
public static void main(String[] args) { 
        ScheduledExecutorService schedule = Executors.newScheduledThreadPool(2); 
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 
        System.out.println(" begin to do something at:" + sdf.format(new Date())); 
        schedule.scheduleAtFixedRate(new Job(), 1,2, TimeUnit.SECONDS); 
    } 

结果输出:

  1. begin to do something at:2012-08-04 08:53:30
  2. do something at:2012-08-04 08:53:36
  3. do something at:2012-08-04 08:53:41
  4. do something at:2012-08-04 08:53:46
  5. do something at:2012-08-04 08:53:51


你可能感兴趣的:(Java)