Java Executor框架

1. Runnable接口

步骤:

  1. 定义类实现Runnable接口:覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中。
  2. 通过Thread 类建立线程对象:将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
  3. 调用Thread类的start方法开启线程。
public class example {  
      public static void main(String[] args){  
          MyThread myThread=new MyThread();  
          Thread thread=new Thread(myThread);  
          thread.start();  
          while(true)  
          {  
              System.out.println("Main方法在运行");  
          }  
      }  
}  

class MyThread implements Runnable{  
    public void run(){  
        while(true){  
            System.out.println("MyThread类的run()方法在运行");  
        }  
    }  
}  

2. Callable和Future接口

不论是继承Thread类还是实现Runnable接口,构造的线程都有一个弊端,那就是既没有返回值也不会抛出异常,这一点看run()方法返回值是void就知道。

(1)Callable接口

public interface Callable<V> {   
      V   call()   throws Exception;   
}

返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值

(2)Future接口

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。示例:

 interface ArchiveSearcher { String search(String target); }
 class App {
   ExecutorService executor = ...
   ArchiveSearcher searcher = ...
   void showSearch(final String target)
       throws InterruptedException {
     Future<String> future
       = executor.submit(new Callable<String>() {
         public String call() {
             return searcher.search(target);
         }});
     displayOtherThings(); // do other things while searching
     try {
       displayText(future.get()); // use future
     } catch (ExecutionException ex) { cleanup(); return; }
   }
 }

方法摘要:

  • V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
  • V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
  • boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
  • boolean isCancelled() :如果任务完成前被取消,则返回true。
  • boolean cancel(boolean mayInterruptRunning)
    • 如果任务还没开始,执行cancel(…)方法将返回false;
    • 如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
    • 当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;
    • 当任务已经完成,执行cancel(…)方法将返回false。
    • mayInterruptRunning参数表示是否中断执行中的线程。

(3)FutureTask类

public class FutureTask<V> extends Object implements RunnableFuture<V>

但是我们必须明白Future只是一个接口,我们无法直接创建对象,因此就需要其实现类FutureTask。FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到。

(1)FutureTask通过Thread封装,再调用start()方法启动:

public class CallableAndFuture {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get()); // 注意这里用get方法获取结果
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

(2)使用线程池(具体概念稍后介绍)

package com.zejian.Executor;  
import java.util.concurrent.Callable;  

public class CallableDemo implements Callable<Integer> {  
    private int sum;  
    @Override  
    public Integer call() throws Exception {  
        System.out.println("Callable子线程开始计算啦!");  
        Thread.sleep(2000);  

        for(int i=0 ;i<5000;i++){  
            sum=sum+i;  
        }  
        System.out.println("Callable子线程计算结束!");  
        return sum;  
    }  
}  
package com.zejian.Executor;

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

public class CallableTest {  

    public static void main(String[] args) {  
        //创建线程池  
        ExecutorService es = Executors.newSingleThreadExecutor();  
        //创建Callable对象任务  
        CallableDemo calTask=new CallableDemo();  
        //提交任务并获取执行结果  
        Future<Integer> future = es.submit(calTask);  // submit返回一个Future对象
        //关闭线程池  
        es.shutdown();  
        try {  
            Thread.sleep(2000);  
        System.out.println("主线程在执行其他任务");  

        if(future.get()!=null){  
            //输出获取到的结果  
            System.out.println("future.get()-->"+future.get());  
        }else{  
            //输出获取到的结果  
            System.out.println("future.get()未获取到结果");  
        }  

        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        System.out.println("主线程在执行完成");  
    }  
}  

3. ThreadPoolExecutor类

构造方法

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue);

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

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

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

下面解释下一下构造器中各个参数的含义:

(1)corePoolSize

核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中

(2)maximumPoolSize

线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。注意,如果workQueue使用了无限工作队列则这个参数没有意义。

(3)keepAliveTime

表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

(4)unit

参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

  • TimeUnit.DAYS; //天
  • TimeUnit.HOURS; //小时
  • TimeUnit.MINUTES; //分钟
  • TimeUnit.SECONDS; //秒
  • TimeUnit.MILLISECONDS; //毫秒
  • TimeUnit.MICROSECONDS; //微妙
  • TimeUnit.NANOSECONDS; //纳秒

(5)workQueue

一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

  • ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,必须指定容量,按FIFO排序元素。
  • LinkedBlockingQueue:一个基于链表的阻塞队列,可以有容量限制,也可以无限。吞吐量高于ArrayBlockingQueue。Executes.newFixedThreadPool()使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。它的用处是让优先级高的线程先执行,优先级由任务的Comparator决定。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,否则插入操作一直处于阻塞状态。Executes.newCachedThreadPool()使用了这个队列。属于无界队列

线程池的排队策略与BlockingQueue有关。

(6)threadFactory

线程工厂,主要用来创建线程.

(7)handler

表示当拒绝处理任务时的策略,有以下四种取值:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

(8)工作线程Worker

线程池在创建线程时会将其封装为工作线程Worker,Worker在执行完任务后,还会循环地获取工作队列里的任务来执行。


重要方法

execute
void execute(Runnable command) 

execute()方法实际上是Executor中声明的方法,用于提交不需要返回值的任务。在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

执行步骤:

(1)如果当前运行的线程数量 < corePoolSize,则创建新线程来执行任务。创建新线程需要给线程池加全局锁。

(2)如果当前运行的线程数量 >= corePoolSize,则加入阻塞队列BlockingQueue

(3)如果BlockingQueue已满,则创建新的线程来处理任务(需要全局锁)。

(4)如果创建新线程会使当前运行的线程超出maximumuPoolSize,则交给饱和策略去处理。

submit

<T> Future<T> submit(Callable<T> task) 
          提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。

Future<?>   submit(Runnable task) 
          提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。

<T> Future<T> submit(Runnable task, T result) 
          提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。

submit()方法是在ExecutorService中声明的方法,用于提交需要返回值的任务。在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果

shutdown和shutdownNow
  • shutdown()后线程池将变成shutdown状态,此时不接收新任务,但会处理完正在运行的 和 在阻塞队列中等待处理的任务。
  • shutdownNow()后线程池将变成stop状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。

三种常用线程池

FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {  
    return new ThreadPoolExecutor(nThreads, nThreads,  // corePoolSize 和maximumPoolSize 都是nThreads
                                  0L, TimeUnit.MILLISECONDS, //  keepAliveTime 0 
                                  new LinkedBlockingQueue<Runnable>());  // 使用无界阻塞队列
}  
  • 维护着固定数量的工作线程。
  • 由于corePoolSizemaximunPoolSize都为用户设定的线程数量nThreads,所以当线程数少于nThreads时新提交的任务都会触发工作线程的创建。
  • 当线程池已经满了之后,所有提交的任务都会塞到无界的阻塞队列,这意味着该线程池永远不会拒绝任务。
  • keepAliveTime参数在这里没有意义。
  • 弊端是在没有任何请求的时候也维持着nThreads的工作线程。
newCachedThreadPool
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // corePoolSize 0, maximumPoolSize 对于 SynchronousQueue 没有意义
                                      60L, TimeUnit.SECONDS, // keepAliveTime 60s
                                      new SynchronousQueue<Runnable>());
    }
  • newCachedThreadPool可以减少不必要的线程创建和销毁上的消耗,请求到来,如果有空闲未超过60s的线程则复用;如果没有空闲线程则立刻创建新的线程,因为SynchronousQueue的容量为0;如果线程空闲超过60s则被销毁。
  • 如果有批量的hello world过来,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程;如果保持每分钟(keepAliveTime 60s)有1000个hello world 请求,则一共会有1000个线程在,但是如果不用线程池,则每分钟创建1000个线程,每分钟关闭1000个线程。
  • 自动回收不使用的线程(终止并从缓存中移除那些已有 60 秒钟未被使用的线程),在无可用线程的情况下自动的为新来的task创建新线程。 在小任务量,任务时间执行短的场景下能提高性能。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
    return new ThreadPoolExecutor(1,1, // 维持一个工作线程
                                  0L,TimeUnit.MILLISECONDS, // keepAliveTime 参数无意义
                                  new LinkedBlockingQueue<Runnable>()); // 无界队列
}
  • keepAliveTime参数只有在 maximumPoolSize > corePoolSize 的情况下有效。
  • 可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

4. Java定时/周期任务

Timer

Timer是线程安全的,但是只能串行执行Task:

Timer类可以保证多个线程可以共享单个Timer对象而无需进行外部同步,所以Timer类是线程安全的。但是由于每一个Timer对象对应的是单个后台线程,用于顺序执行所有的计时器任务,一般情况下我们的线程任务执行所消耗的时间应该非常短,但是由于特殊情况导致某个定时器任务执行的时间太长,那么他就会“独占”计时器的任务执行线程,其后的所有线程都必须等待它执行完,这就会延迟后续任务的执行,使这些任务堆积在一起,具体情况我们后面分析。

核心方法(void schedule):

  • schedule(TimerTask task, Date time):安排在指定的时间执行指定的任务。
  • schedule(TimerTask task, Date firstTime, long period) :安排指定的任务在指定的时间开始进行重复的固定延迟执行。

  • schedule(TimerTask task, long delay) :安排在指定延迟后执行指定的任务。

  • schedule(TimerTask task, long delay, long period) :安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。同时也重载了scheduleAtFixedRate方法,scheduleAtFixedRate方法与schedule相同,只不过他们的侧重点不同,区别后面分析。

  • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):安排指定的任务在指定的时间开始进行重复的固定速率执行。

  • scheduleAtFixedRate(TimerTask task, long delay, long period):安排指定的任务在指定的延迟后开始进行重复的固定速率执行。


TimerTask

TimerTask类是一个抽象类,由Timer 安排为一次执行或重复执行的任务。它有一个抽象方法run()方法,该方法用于执行相应计时器任务要执行的操作。因此每一个具体的任务类都必须继承TimerTask,然后重写run()方法。

另外它还有两个非抽象的方法:

  • boolean cancel():取消此计时器任务。

  • long scheduledExecutionTime():返回此任务最近实际执行的安排执行时间。

public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        Task task = new Task();
        timer.schedule(task, new Date(), 1000);
    }
}

class Task extends TimerTask{

    @Override
    public void run() {
        System.out.println("Do work...");
    }
}

Timer的缺陷

(1)不能捕获异常

如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排但尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。

(2)单线程

一个Timer在后台对应一个线程,虽然它是线程安全的,可以schedule多个TimerTask,但也只能串行执行。如果一个Task耗时超过了两个Task的间隔,会阻塞下一个Task的执行。

对于Timer的缺陷,我们可以考虑 ScheduledThreadPoolExecutor 来替代。Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间;Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor是JDK1.5以后推出的类,用于实现定时、重复执行的功能,官方文档解释要优于Timer。

构造方法

(1) ScheduledThreadPoolExecutor(int corePoolSize) 使用给定核心池大小创建一个新定定时线程池 。

(2)ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) 使用给定的初始参数创建一个新对象,可提供线程创建工厂。

调度方法

(1)schedule(Callable callable, long delay, TimeUnit unit) 延迟delay时间后开始执行callable。

(2)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,当任务执行时间大于间隔时间时,之后的任务都会延迟,此时与Timer中的schedule方法类

(3)scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,这里的间隔时间delay是等上一个任务完全执行完毕才开始计算,与Timer中scheduleAtFixedRate情况不同。

示例
import java.util.Date;  
import java.util.concurrent.Callable;  
import java.util.concurrent.Executors;  
import java.util.concurrent.ScheduledThreadPoolExecutor;  
import java.util.concurrent.TimeUnit;  

public class Task implements Callable<String> {  

    private String name;  

    public Task(String name) {  
        super();  
        this.name = name;  
    }  

    @Override  
    public String call() throws Exception {  
        System.out.printf("%s: Starting at : %s\n",name,new Date());  
        return "hello world";  
    }  

    public static void main(String[] args) {  
        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);  
        System.out.printf("Main: Starting at: %s\n",new Date());  
        for(int i = 0; i < 5; i++) {  
            Task task = new Task("Task " + i);  
            executor.schedule(task, i, TimeUnit.SECONDS);  
        }  
        executor.shutdown();  

        try {  
            executor.awaitTermination(1, TimeUnit.DAYS);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.printf("Main: Ends at: %s\n",new Date());  
    }  
}  
DelayedWorkQueue

ScheduledThreadPoolExecutor是基于DelayedWorkQueue实现的,它是对它是对DelayQueue的优化。DelayQueue是Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。

该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。

当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值