Java并发编程----线程池技术(一)

目录

串行执行任务

一任务一线程

固定线程数量

Executor框架

类库中的线程池

返回任务结果的Callable与Future


---------------------------------笔记来自于《Java并发编程实战》

串行执行任务

    以客户端请求服务器为例,同一个时期有多个客户端请求服务器时,单线程中一次只能处理一个客户端的请求,当客户向一个已经被其他客户端占用的服务器时,虽然可以连接到服务器,但是服务器不会立即对客户端做出响应,新连接的客户端必须等到服务器处理完上一个客户端请求。服务器按照顺序处理客户端的请求,就是所谓的串行执行

串行执行的缺点,首先一次只能处理一个请求,其次如果在服务器的处理中包含一些阻塞操作,例如I/O操作,这会推迟当前请求的完成时间。

public class SingleThreadHandler {
  public static void main(String[] args) {
    ServerSocket socket = new ServerSocket(80);
    //单线程处理请求,本次请求完成,才能继续处理下一个。
    while (true) {
      Socket connection = socket.accept();
      handleRequest(connection);
    }
  }
}

一任务一线程

    为了解决串行执行出现的问题,通常我们会为每一个任务创建一个单独的线程来执行。在正常的负载情况下,为每个任务分配一个线程能提升执行的性能,只要请求的到达速率不会超过服务器的请求处理能力,这种“为每个任务创建一个线程”方式能带来更快的响应性和刚高的吞吐率,但一线程一任务的又会引发新的问题。

1.线程的创建与销毁的开销非常高。

2.资源消耗,活跃的线程会消耗系统资源,尤其是内存。

3.稳定性,在可创建线程的数量上存在一个限制,超出这个限制,创建更多的线程反而会将降低程序的执行速度,并且如果过多的创建一个线程,应用程序可能出现崩溃。

public class ThreadPerTaskHandler {
  public static void main(String[] args) throws IOException {
    ServerSocket socket = new ServerSocket(80);
    while(true) {
      Socket connection = socket.accept();
      Runnable task = new Runnable(){
        public void run() {
          handleRequest(connection);
        }
      }    
    //为每个请求创建一个线程处理
      new Thread(task).start();
    }
  }
}

固定线程数量

     正如前面所说,线程的创建会消耗系统的资源,占用CPU的周期和内存。线程阻塞时,JVM将保存该线程状态,然后会选择另外一个线程运行,并在上下文转换时恢复阻塞线程的状态。随着线程数量的增加,将会消耗越来越多的资源。将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务,加入一个额外的线程实际上可能增加客户端总服务的时间。

解决此问题的方法是固定线程的大小,重复利用之前创建的线程,每当线程执行完任务后,会重新回到循环接收客户端的连接。

public class FixedThreadHandler {
  public static void main(String[] args) throws IOException {
    final int NTHREAD = 5;
    ServerSocket socket = new ServerSocket(80);
    for (int i = 0; i < NTHREAD; i++) {
      Thread t = new Thread(new Runnable() {
        public void run() {
          while (true) {
            Socket connection = socket.accept();
            handleRequest(connection);
          }
        }
      });
      t.start();
    }
  }
}

Executor框架

前面提到的几种用线程来执行任务的策略,即把所有的任务放在单个线程中串行执行、为每个任务创建一个线程以及固定数量线程来执行任务。几类方法各有优缺:串行执行的问题在于糟糕的响应性和吞吐量,而“为每个任务分配一个线程”的问题在于资源管理的复杂性,固定数量的线程问题是无法动态调整线程的数量和难以管理线程生命周期。

在jdk5.0后java.util.concurrent包下面的Executor框架提供了更加灵活的线程池实现。框架支持多种不同种类的任务执行策略,并且将任务的提交和执行解耦开来。Executor接口如下:

public interface Exectuor{
  void executor(Runnable command);
}

基于Executor框架的客户端的请求程序比较容易实现,下面使用的是固定的线程池的数量Executors.newFixedThreadPool(),这比显式创建线程的数量去执行任务更加方便。如下:

public class TaskExecutionHandler {
  public static void main(String[] args) throws IOException {
    //使用Executor框架,创建一个线程数量为10的线程池来处理所有的请求
    ExecutorService exec = Executors.newFixedThreadPool(10);
    ServerSocket socket = new ServerSocket(80);
    while(true) {
      Socket connection = socket.accept();
      Runnable task = new Runnable() {
        public void run() {
          handleRequest(connection);
        }
      };
      exec.execute(task);
    }
  }
}

Executors类中提供了创建Executor子类的静态方法,上面使用的Executors.newFixedThreadPool(10),就是创建了10个线程去执行所有的任务,很容易实现其他策略的线程池技术,只需要改变Executors后面的方法。

当然我们可以通过实现Executor接口自定义任务的执行策略,如上面提到的“为每个任务创建一个线程”,只需要实现Executor接口,在execute方法中创建一个新线程来执行任务。

public class ThreadPerTaskExecutor implements Executor{
  @Override
  public void execute(Runnable task) {
    //为任务创建新线程执行
    new Thread(task).start();
  }
}

同样可以编写一个类似单线程的行为,以同步的方式(串行执行)执行所有任务。

public class SingleThreadExecutor implements Executor {
  @Override
  public void execute(Runnable task) {
    task.run();
  }
}

类库中的线程池

     Executor框架中不同任务执行策略的线程池,线程池从字面含义看就是管理一组同构工作线程的资源池。线程池是与工作队列密切相关的,其中工作队列中保存了所有等待执行的任务。工作者线程的任务很简单,从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。合理使用线程的好处有以下节点:

1.降低了资源消耗:重复利用已创建的线程降低了线程的创建销毁所造成的资源消耗。

2.提高了响应的速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

3.提高了线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,而且会降低系统的稳定性,使用线程池可以进行同一个分配,调优和监控。

Executors类中提供了静态工厂方法来创建不同执行策略的线程池

方法描述
ExecutorService newFixedThreadPool创建一个固定长度的线程池
ExecutorService newSingleThreadExecutor创建的是单线程的Executor
ExecutorService newCachedThreadPool创建一个可缓存的线程池
SchduledExecutorService newScheduledThreadPool创建了一个固定长度的线程池,而且延迟或定时的方式来执行任务

1.newFixedThreadPool 

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

    newFixedThreadPool中的核心线程数和最大线程数都是指定的值(固定长度线程池),每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池里面线程数量不会再增加,此后提交任务都会被提交到阻塞队列LinkedBlockingQueue,阻塞队列的容量是Integer.MAX_VALUE。

    此外如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程。

2.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
  return new FinalizableDelegatedExecutorService
      (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
          new LinkedBlockingQueue<Runnable>()));
}

    从源码不难发现newSingleThreadPool相当于线程数量为1的newFixedThreadPool,创建的是一个单线程的线程池来执行任务,向newSingleThreadPool提交多个任务时,这些任务按照提交任务的顺序放到LinkedBlockingQueue,所有任务都使用相同的线程执行,也就是说每个任务在下一个任务开始执行前结束。

    如果这个线程异常结束,会创建另一个线程来替代,能确保依照任务在队列中的顺序来串行执行。

    SingleThreadExecutor对于单独线程中连续运行任何事物比价有用(长期存活的任务),例如监听进入套接字连接的任务,在线程中运行短任务也很方便,例如更新本地或远程日志的小任务,或者事件分发线程。任务按照提交顺序执行。

 3.newCachedThreadPool

    newCachedThreadPool创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何的限制。

public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>());
}

4.newScheduledThreadPool

    newScheduledThreadPool创建的是一个固定长度的线程池,而且以延迟或者定时的方式来执行任务。

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

与前面三个方法不同的是,Executors.newScheduledThreadPool返回的是ScheduledExecutorService类型,SchduledExecutorService接口继承了ExecutorService,提供了以下几个方法:

方法修饰返回值类型方法
publicScheduledFuture<?>schedule(Runnable command,long delay, TimeUnit unit)
public<V> ScheduledFuture<V>schedule(Callable<V> callable,long delay, TimeUnit unit)
publicScheduledFuture<?>scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
publicScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)

a.scheduler()方法中表示的是延迟指定时间后执行任务。其中command/callable都表示的是要执行的任务,delay表示的是从现在开始延迟执行的时间,unit表示的是延迟参数的时间单位。

b.scheduleAtFixedRate()方法表示的是在指定的延迟时间后首次开始执行一次任务,随后每隔period周期后执行一次任务。如果任务的任何执行遇到异常,将禁止后续执行。如果任务的任何执行时间长于其周期period,则后续的执行可能会延迟开始。command表示的是要执行的任务,initialDelay表示的是首次执行延迟的事件,period表示的是连续执行的时间周期,unit表示的是时间单位。

说明:scheduleAtFixedRate()方法执行是以上一个任务开始执行为计时,在周期period后检测上一个人任务是否执行结束,如果上一个任务执行结束,则当前任务立即执行。如果上一个任务没有执行结束,则等待上一个任务执行结束后当前任务立即执行

c.scheduleWithFixedDelay()方法表示的是在执行延迟时间后首次开始执行一次任务,随后在任务执行结束后计时,间隔周期period后开始执行下一次任务。command表示的是要执行的任务,initialDelay表示的是首次执行延迟的事件,period表示的是连续执行的时间周期,unit表示的是时间单位。

public class TestScheduleThreadPool {
  
  public static void main(String[] args) {
    ScheduledExecutorService exec = Executors.newScheduledThreadPool(5);
    //testScheduleAtFixedRate(exec);
    testScheduleWithFixedDelay(exec);
  }
  /**
   * scheduleAtFixedRate以任务开始执行为计时,在period时间后,上一个任务已执行完,立即执行当前执行。
   * 上一个任务未执行完,则会等待上一个任务执行完成后立即执行当前任务。
   * 测试里面,period=1s后任务还未执行完成,则会等待任务执行完后立即下个任务。
   */
  public static void testScheduleAtFixedRate(ScheduledExecutorService exec) {
    exec.scheduleAtFixedRate(new Runnable() {
      public void run() {
        try {
          System.out.println("Start Execution!!!");
          TimeUnit.SECONDS.sleep(10);
          System.out.println("Execution completed!!!");
        }catch(InterruptedException ex) {
          ex.printStackTrace();
        }
      }
    }, 3000,1000, TimeUnit.MILLISECONDS);
  }
  
  /**
   * scheduleWithFixedDelay以任务执行结束后开始计时,在period时间后,立即执行任务。
   * 测试里面,等任务“Execution completed”执行完成后计时,period=10s后,执行下一次任务。
   */
  public static void testScheduleWithFixedDelay(ScheduledExecutorService exec) {
    exec.scheduleWithFixedDelay(new Runnable() {
      public void run() {
        try {
          System.out.println("Start Execution!!!");
          TimeUnit.SECONDS.sleep(10);
          System.out.println("Execution completed!!!");
        }catch(Exception ex) {
          ex.printStackTrace();
        }
      }
    }, 3000, 10000, TimeUnit.MILLISECONDS);
  }
}   

返回任务结果的Callable与Future

    Executor框架中以Runnable作为任务的基本表现形式,但是Runnable有很大的局限性,不能返回任务的运行结果或者是抛出一个未检查的异常,许多任务实际上存在延迟的计算,比如:执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable是更好的抽象,像Runnable中执行任务的方法是run()一样,Callable执行任务的方法是call(),call()有返回值,并可能会抛出一个异常。

    线程池的类库中提供的四种创建线程池的方法,返回值类型都是ExecutorService,其中Executors.newScheduledThreadPool(n)返回值类型是ScheduledExecutorService(继承了接口ExecutorService),而ExecutorService继承了接口Executor,在ExecutorService中提供单个和多个任务的提交方法。

public interface ExecutorService extends Executor {
  void shutdown();
  List<Runnable> shutdownNow();
  boolean isShutdown();
  boolean isTerminated();
  boolean awaitTermination(long timeout, TimeUnit unit)throws InterruptedException;
  //任务提交方法
  <T> Future<T> submit(Callable<T> task);
  <T> Future<T> submit(Runnable task, T result);
  Future<?> submit(Runnable task);
  //execute继承自Executor接口
  //void execute(Runnable command);
  //多个任务提交
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException;
  <T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException;
  <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}

   Runnable和Callable描述的都是抽象的计算任务,这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束,Executor执行的任务的4个生命周期:创建、提交、开始、完成。有些任务执行时间比较长,通常希望能够取消这些任务。在Executor框架中,已经提交的任务但还未开始的任务可以取消,但是对于那些已经开始执行的任务,只有它们再响应中断时,才能取消,取消已经执行完成的任务不会有任何影响。任务的生命周期只能前进,不能后退,当某个任务完成后,它就永远地停留在了“完成”状态上。

public class TestCallAndFuture {
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    CallableTask task = new CallableTask();
    Future<Integer> future = exec.submit(task);
    try {
      //future.get()方法阻塞,直到任务执行完成返回结果
      System.out.println(future.get());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
class CallableTask implements Callable<Integer>{
  @Override
  public Integer call() throws Exception {
    TimeUnit.SECONDS.sleep(5);
    int sum = 0;
    for(int i =0 ; i < 50;i++) {
      sum+=i;
    }
    return sum;
  }
}

在接口ExecutorService中可以发现,单个任务提交后返回类型是Future,Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果或者取消任务等。

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);
  boolean isCancelled();
  boolean isDone();
  V get() throws InterruptedException, ExecutionException;
  V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

对于接口Future里面几个方法的具体解释:

返回值类型方法方法描述
booleancancel(boolean mapInterruptIfRunnning)试图取消执行此任务,如果任务已启动,mapInterruptedIfRunning确定执行此任务的线程是否在尝试停止任务时中断。
booleanisCancelled()如果任务在执行完成前取消,则返回true。
booleanisDone()如果任务已经完成,则返回true。可能由于正常终止、异常或者取消情况下,返回也返回true。
V

get() throws InterruptedException,

ExecutionException

获取任务的执行结果,任务未完成,将会阻塞直到完成
V

get(long timeout,TimeUnit unit)

throws interruptedException

ExecutionException ,TimeoutException

最多等待给定时间完成任务,超时将会抛出TimeoutException异常

可以看到像线程池提交任务的方式有两种:

1.execute()方法用于提交不需要返回值的任务。这种情况是无法判断任务是否执行成功,方法的参数是Runnable。

 2.submit()方法用于提交需要返回值的任务。ExcutorService接口有提供了三种submit()重载方法。任务执行完成后,调用Future中get()方法可以获取执行结果,对于Runnable类型的任务,ExecutorService提供了传入参数的方法获取任务执行结果,get()方法的行为取决于任务的状态(尚未开始,正在运行, 已完成)。

    如果任务已经完成,那么get()方法会立即返回或者抛出异常,如果任务没有完成,那么get()方法将阻塞直到任务执行完成。如果任务抛出了异常,那么get()将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get()将抛出CancellationException。如果get()抛出了ExecutionException,那么可以通过getClause来获取封装的初始异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值