Java并发编程实战之 基础模块构建、任务执行

本文深入探讨了Java并发编程的关键概念和技术,包括线程安全类、并发容器如ConcurrentHashMap和CopyOnWriteArrayList,以及高级同步工具如闭锁、信号量和栅栏。详细介绍了Executor框架在任务执行中的应用,线程池的管理和错误处理策略。
摘要由CSDN通过智能技术生成

基础构建模块

委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可。

本章主要介绍一些比较有用的并发构建模块,特别是在 Java 5.0 和 Java 6.0 中引入的一些新模块,以及在使用这些模块来构造应用程序时的一些常用模式。

同步容器类

最早出现的同步容器类是VectorHashtable,在 JDK 1.2 及之后,又提供了一些功能类似的封装器类,这些同步容器类是由 Collections.synchronizedXxx 等工厂方法创建的。

其实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题

同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护符合操作。

容器上常见的复合操作包括:迭代、跳转以及条件运算。在同步容器类中,这些复合操作在没有客户端加锁的情况下是线程安全的,但是在其他线程并发的修改容器时,它们可能会表现出意料之外的行为。

Vector 上可能导致混乱结果的复合操作

 public static Object getLast(Vector list){
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
 }

 public static void deleteLast(Vector list){
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
 }

假设线程A在包含10个元素的Vector上调用 getLast,同时线程B在同一个Vector上调用 deleteLast,这些操作交替执行如下图:

img

那么当执行到get操作时,会报出异常。

为了解决这个问题,我们可以采用客户端加锁的方法来保证复合操作的线程安全性:

public static Object getLast(Vector list){
    synchronized(list){
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
}

public static void deleteLast(Vector list){
    synchronized(list){
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}
  • 迭代问题:如下为一个简单的Vector的遍历,看起来这段遍历代码并没有什么问题,但实际上在多线程情况下,假设线程A在迭代遍历,同时线程B在修改Vector,比如删除操作,那么就可能会抛出异常。
for (int i = 0; i < vector.size(); i ++)
    dosomething(vector.get(i));

为了解决这个问题,我们可以对其进行客户端加锁操作:

synchronized(vector){
    for (int i = 0; i < vector.size(); i ++)
        dosomething(vector.get(i));
迭代器与 ConcurrentModificationException

在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是“及时失败(fail-fast)”的。

这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException异常。

  • 注意:这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此,只能作为并发问题的预警指示器。
  • 解决方法:
    • 在迭代期间对容器加锁,某些线程必须在等待迭代过程结束才可以进行访问或修改,如果容器的规模很大,或者每个元素执行操作的时间很长,那么这些线程将长时间等待。我们知道持有锁的时间越长,那么在锁上的竞争就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和CPU的利用率。
    • ”克隆“容器,并在副本上进行迭代。由于副本被封闭在线程内,因此其他线程无法在迭代期间对其进行修改。但克隆容器时存在显著的性能开销。
隐藏迭代器

虽然加锁可以避免迭代器抛出 ConcurrentModificationException,但必须要记住在所有共享容器进行迭代的地方都要加锁。实际情况可能更加复杂,因为在某些情况下,迭代器会隐藏起来。

隐藏在字符串连接中的迭代操作

public class HiddenIterator{    // 不要这么做
    private final Set<Integer> set = new Hashset<Integer>();

    public synchronized void add(Integer i){ set.add(i); }
    public synchronized void remove(Integer i){ set.remove(i); }

    public void addTenThings(){
        Random r = new Random();
        for (itn i = 0; i < 10; i ++)
            add(r.nextInt());
		// !!注意下面一行
        System.out.println("DEBUG: added ten elements to " + set);   //进行了隐式地迭代
    }
}

set 的 toString 方法也进行了迭代操作!

除此之外,常见的隐式迭代器还有hashCode和equals,containsAll,removeAll 等方法。

并发容器

在 Java 5.0 中增加了 ConcurrentHashMap,用来代替 Map 及 CopyOnWriteArrayList。

通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险

Java 5.0 中新增了两种容器:Queue、BlockingQueue。Queue 提供了几种实现,包括:ConcurrentLinkedQueue——一个传统的先进先出队列,PriorityQueue——一个(非并发的)优先队列。

BlockingQueue 扩展了 Queue,增加了可阻塞的插入和获取等操作。

ConcurrentHashMap
  • 同步容器类在执行每个操作期间都持有一个锁。
  • 在基于散列的容器中,如果 hashCode 不能很均匀地分布散列值,那么容器中的元素就不会均匀地分布在整个容器中。
  • ConcurrentHashMap 并不是将每个方法都在同一个锁上同步并使得只有一个线程能访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制被称为分段锁(Lock Striping,后面会提到)
  • ConcurrentHashMap 在并发访问环境下将实现更高吞吐量,而在单线程环境中之损失非常小的性能
  • ConcurrentHashMap 与其它并发容器提供的迭代器不会抛出 ConcurrentModificationException
  • ConcurrentHashMap 返回的迭代器具有弱一致性(Weakly Consistent),而并非及时失败。可以(但是不保证)迭代器被构造后将修改操作反映给容器
  • ConcurrentHashMap 对于一些需要在整个 Map 上计算的方法,例如 size 和 isEmpty ,它们返回的可能不是一个精确值,它们在并发环境下用处很小
  • ConcurrentHashMap 中没有对 Map 加锁以提供独占访问。在 HashTable 和 synchronizedMap 中,获得Map 的锁能防止其它线程访问这个Map
  • ConcurrentHashMap 有着更多的优势以及更少的劣势,只有当应用程序需要加锁 Map 以独占访问时,才应该放弃使用 ConcurrentHashMap
CopyOnWriteArrayList
  • CopyOnWriteArrayList 用于替代同步 List
  • CopyOnWrite 机制
  • “写入时复制(Copy-On-Write)”容器的安全性在于,只要正确的发布了一个事实不可变对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可见性。
  • CopyOnWrite 容器不会抛出 ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致。
  • 仅当迭代操作次数远远多于修改操作时,才应该使用 CopyOnWrite容器

阻塞队列和生产者-消费者模式

  • 阻塞队列提供了可阻塞的 put 和 take 方法,以及支持定时的 offer 和 poll 方法

  • 生产者-消费者模式将“找出需要完成的工作”与“执行工作”这两个过程分离开,并把工作放入一个“待完成”的列表中一遍在随后处理。

    在基于阻塞队列的…模式中,当数据生成时,生产者把数据放入队列,而当消费者准备处理数据,将冲队列中获取数据。

    为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下(引自:生产者/消费者模式的理解及实现(整理)

    1、你把信写好——相当于生产者制造数据

    2、你把信放入邮筒——相当于生产者把数据放入缓冲区

    3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区

    4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据

  • 常见的阻塞队列(BlockingQueue)的实现

    • LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,二者分别与LinkedList和 ArrayList类似,但比同步list有更好的并发性。
    • PriorityBlockingQueue是一个按优先级排序的队列。它既可以根据元素的自然顺序来比较元素,也可以使用Comparator方法来比较。
    • SynchronousQueue并不是一个真正的队列,因为它不会为队列中元素维护存储空间,它维护的是一组线程,这些线程在等待把元素加入或移除。因为SynchronousQueue没有存储功能,因此put和take方法会被阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并总有一个消费者是准备好交付的工作时,才适合使用同步队列。
串行线程封闭

不是很理解!

通过将多个并发的任务存入队列实现任务的串行化,并未这些串行化的任务创建唯一的一个工作进程处理。
本质:使用一个开销更小的锁(队列锁)去代替另一个可能开销更大的锁(非线程安全对象引用的锁)。

  • 适用场景:
    • 需要使用非线程安全对象,但又不希望引入锁。
    • 任务的执行涉及I/O操作,不希望过多的I/O线程增加上下文切换。
双端队列与工作密取

Deque是一个双端队列,实现了队列在队列头和队列尾的高效插入和移除。具体实现包括ArrayQueue和LinkedBlockingQueue。

在工作密取(Work Stealing)中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。

工作密取模式比传统的生产者-消费者模式具有更高的可伸缩性。因为在大多数情况下,它们都只是访问自己的双端队列,从而极大地减少了竞争,并且当它需要访问另一个队列时,它会从尾部获取工作。

阻塞方法和中断方法
  • 线程可能会阻塞或者暂停执行,原因有多种:等待 I/O 操作结束,等待获得一个锁,等待从 Thread.sleep 中醒过来,或是等待另一个线程的计算结果
  • 当线程阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED、WAITING 或 TIMED_WAITING)。
  • Thread 提供了 interrupt 方法,用于中断线程或者查询线程是否已经被中断。
  • 中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他操作
  • 传递 InterruptException

同步工具类

同步工具类可以是任何一个对象,只要它根据其自身状态来协调线程的控制流。

阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore),栅栏(Barrier)以及闭锁(Latch)。

闭锁 CountDownLatch
  • 闭锁是一种同步工具类,可以延迟进程的进度直到其到达终止状态。
  • 闭锁的作用相当于一扇门:在闭锁达到结束状态之前,这扇门一直是关闭状态的,并且没有任何线程能通过,当达到结束状态时,这扇门会打开并允许所有的线程通过。

例如:

  • 确保某个计算 在其需要的所有资源都被初始化之后才可以继续执行
  • 确保某个服务载器以来的所有其他服务都已经启动之后才启动
  • 等待直到某个操作的所有参与者都就绪再执行

CountDownLatch 是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。比锁状态包括一个计数器,该线程被初始化为一个正数,表示需要等待的事件数量。

countDown 方法表示对计数器递减,表示有一个事件发生了,而 await 方法会等待计数达到 0

在计时测试中使用 CountDownLatch 来启动和停止线程

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

  public static long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
    final CountDownLatch startGate = new CountDownLatch(1);
    final CountDownLatch endGate = new CountDownLatch(nThreads);

    long start = System.currentTimeMillis();

    for (int i = 0; i < nThreads; i++) {
      int finalI = i;
      Thread t = new Thread() {
        @Override
        public void run() {
          System.out.println(String.format("线程%d启动!", finalI));
          try {
            startGate.await();
            try {
              System.out.println(String.format("第%d个线程开始运行", finalI));
              task.run();
            } finally {
              endGate.countDown();
            }
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      };
      t.start();
      Thread.sleep(100);
    }
    startGate.countDown();
    endGate.await();
    long end = System.currentTimeMillis();

    return end - start;
  }

  public static void main(String[] args) throws InterruptedException {

    Runnable r = new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };
    System.out.println(timeTasks(20, r));
  }
}
FutureTask
  • java.util.concurrent.Callable

    Callable 接口 与 Runnable 接口类似,不同的是它们的唯一的 run 方法:

    1、Callable 有返回值,Runnable 没有。
    Callable 的 run() 方法使用了 泛型类,可以返回任意类型的值。

    2、Callable 抛出异常 ,Runnable 没有抛出。

    同时 java.util.concurrent.Executors 提供了许多方法,可以操控 Callable 在线程池中运行。

  • FutureTask也可以用作闭锁,FutureTask 表示的计算是通过 Callable 来实现的,相当于可生成结果的 Runnable,并且处于以下三种状态:等待运行(Waiting to run)、正在运行(Running) 和运行完成(Completed)

  • FutureTask 的闭锁体现在它的get方法,如果任务完成,那么get会立即返回结果,否则get会阻塞直到任务完成进入完成状态。

  • FutureTask 将计算结果从执行计算的线程传递到获取这个结果的线程,且能保证传递过程能实现安全发布。

使用 FutureTask 来提前加载稍后需要的数据

public class Preloader{
    //通过call方法返回执行的结果
    private final FutrueTask<ProductInfo> futrue = 
        new FutrueTask<ProductInfo>(
        	new Callable<ProductInfo>(){
                public ProductInfo call() throws DataLoadException{
                    return loadProductInfo();
                }
        });
    private final Thread thread = new Thread(futrue);

    public void start(){ thread.start(); }

    public ProductInfo get()
            throws DataLoadException,InterruptedException{
        try{
            return futrue.get();
        } catch (ExecutionException ex){
            Throwable cause = e.getCause();
            if (cause instanceof DataLoadException) {
                throw (DataLoadException) cause;
            } else {
                throw launderThrowable(cause);
            }
        }   
    }
}

注意:在Preloader中,当get方法抛出ExecutionException时,可能是这三种情况:Callable抛出的异常,RuntimeExcetpion,以及Error。Preloader会首先检查已知的受检查类型异常,并重新抛出给它们,剩下的就是未检查异常交给了launder Throwable来处理。

public static RuntimeException launderThrowable(Throwable t){
    if (t instanceof RuntimeException) 
        return (RuntimeException) t;
    else if (t instanceof Error)
        return (Error) t;
    else 
        throw new IllegalStateException("Not unchecked",t);
}
信号量 Semaphore
  • 计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量,还可以用来实现某种资源池。
  • 一般 Semaphore 只有 3 种操作:初始化、增加(release)、减少(acquire)
  • 在初始化中会给定一个许可(permit)的数量,在执行操作时首先要 acquire 获取许可(如果有剩余的情况下),若没有 acquire 将阻塞直到有许可(或者被中断或超时)。
  • 在执行完操作后,通过 release 释放许可,此时可用的许可数会加1

使用 Semaphore 为容器设置边界

public class BoundedHashSet<T>{
    private final Set<T> set;
    private final Semaphore sem;

    public BoundedHashSet(int bound){
        this.set = Collections.synchronizedSet(new Hashset<T>());
        sem = new Semaphore(bound);   // 初始化 bound 为许可数量
    }

    public boolean add(T o) throws InterruptedException{
        sem.acquire();   // 获得一个许可
        boolean wasAdded = false;
        try{
            wasAdded = set.add(o);
            return wasAdded;
        } finally{
            if (!wasAdded)
                sem.release();   // 如果添加失败则释放一个许可
        }
    }

    public boolean remove(Object o){
        boolean wasRemoved = set.remove(o);
        if (wasRemoved)
            sem.release();   // 删除操作,释放一个许可
        return wasRemoved;
    }
}
栅栏 Barrier
  • 闭锁是一次性对象,一旦进入终止状态,就不能被重置。

  • 栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。

  • 栅栏与闭锁的关键区别在于:所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

  • 当线程达到栅栏时将调用await方法,这个方法将阻塞一直到所有的线程都达到这个栅栏位置。

public class BarrierTest implements Runnable {
  private final CyclicBarrier cyclicBarrier;
  private final String name;

  private final Random r = new Random();

  public BarrierTest(CyclicBarrier cyclicBarrier, String name){
    this.cyclicBarrier = cyclicBarrier;
    this.name = name;
  }

  @Override
  public void run() {
    System.out.println(name + "正在打桩....");
    try {
      Thread.sleep(r.nextInt(5000));
      System.out.println(name + "完成打桩。");
      // 阻塞线程,直到所有线程都到达栅栏
      cyclicBarrier.await();
    } catch (InterruptedException | BrokenBarrierException e) {
      e.printStackTrace();
    }
    System.out.println(name + ": 其他人都打完桩了,开始搭桥了。");
  }

  public static void main(String[] args){
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    //定义栅栏
    CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
    BarrierTest work1 = new BarrierTest(cyclicBarrier, "张三");
    BarrierTest work2 = new BarrierTest(cyclicBarrier, "李四");
    BarrierTest work3 = new BarrierTest(cyclicBarrier, "王五");

    executorService.execute(work1);
    executorService.execute(work2);
    executorService.execute(work3);

    executorService.shutdown();
  }
}

Exchanger是一种双方栅栏,各方在栅栏位置上交换数据。

Exchanger可以在两个线程之间交换数据,只能是2个线程,不支持更多的线程之间交换数据。

当线程A调用Exchange对象的exchange方法之后,它会陷入阻塞状态,直到线程B也调用exchange方法,然后以线程安全的方式交换数据,之后线程A和B继续运行。

举个例子:我们模拟将钱和商品进行交换,线程A持有钱,线程B持有商品,两者之间进行交换。

import java.util.Random;
import java.util.concurrent.Exchanger;

public class ExchangeTest {

  private static final Random r = new Random();

  public static void main(String[] args) {
    //定义Exchanger
    Exchanger<String> exchanger = new Exchanger<>();
    new Money(exchanger).start();
    new Product(exchanger).start();
  }

  static class Money extends Thread {
    private String data;
    private Exchanger<String> exchanger = null;

    Money(Exchanger<String> exchanger) {
      this.exchanger = exchanger;
      data = "钱";
    }

    @Override
    public void run() {
      System.out.println("线程" + Thread.currentThread().getName() + "正在把数据<" + data + ">交换出去");
      try {
        Thread.sleep(r.nextInt(3000));
        //进行交换数据
        data = exchanger.exchange(data);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("线程" + Thread.currentThread().getName() + "换回来的数据为<" + data + ">");
    }
  }

  static class Product extends Thread {
    private String data;
    private Exchanger<String> exchanger = null;

    Product(Exchanger<String> exchanger) {
      this.exchanger = exchanger;
      data = "商品";
    }

    @Override
    public void run() {
      System.out.println("线程" + Thread.currentThread().getName() + "正在把数据<" + data + ">交换出去");
      try {
        Thread.sleep(r.nextInt(3000));
        //进行交换数据
        data = exchanger.exchange(data);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("线程" + Thread.currentThread().getName() + "换回来的数据为<" + data + ">");
    }
  }
}

任务执行

大多数并发应用程序都是围绕"任务执行(Task Execution)"来构造的。理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态、结果或边界效应。

当负荷过载时,应用程序的性能应该是逐渐降低,而不是直接失败。所以应该选择清晰的任务边界以及明确的任务执行策略。

Executor 框架

  • 每当看到下面这种形式的代码时: new Thread(runnable).start() 。并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread.

  • Executor:执行的任务有4个生命周期阶段:创建、提交、开始和完成。

  • 由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时,才能取消。取消一个已经完成的任务不会有任何影响。

示例:基于 Executor 的 Web 服务

标准的 Executor 实现,即一个固定长度的线程池,可用容纳 100 个线程。

public class TaskExecutionWebServer {
    private static final int NTHREADS = 100;
    private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

我们可以很容易地将 TaskExecutionWebServer 修改为类似 ThreadPerTaskWebServer 的行为 只需使用一个为每个请求都创建新线程的Executor

为每个请求启动一个新线程的Executor

public class ThreadPerTaskExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    };
}
线程池

线程池从字面上理解是之管理一组同构工作线程的资源池。线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程(Work Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

在线程池中执行任务比为每个任务分配一个线程优势更大。通过重用现有的线程而不是新建线程,可用处理在多个请求时分摊在线程创建和销毁过程中产生的巨大开销。

通过适当调整线程池大小,可以创建足够的的线程以便使处理器保持忙碌状态,同时还可以防止过多现场相互竞争资源而耗尽内存或失败。

可以使用 Executors 中的静态工厂方法来创建一个线程池

  • newFixedThreadPool:创建一个固定长度的线程池,提交一个任务就创建一个线程,直到达到线程池最大容量,此时线程池规模不再变化(如果某个线程出现未预期的 Exception 而结束,那么线程池会补充一个新的线程)
  • newCacheThreadPool:创建一个可缓存的线程池,如果当前规模超过处理需求,那么将回收空闲线程,对线程池规模不存在任何限制
  • newSingleThreadExecutor:一个单线程的 Executor,它创建单个工作者线程来执行任务。能确保线程池中任务在按队列的顺序串行执行
  • newScheduledThreadPool:创建一个固定长度的线程池,以延迟或定时的方式来执行任务,类似于 Timer
Executor 的生命周期

Executor 的实现通常会创建线程来执行任务,但 JVM 只要在除守护线程外的其他线程全部终止后才会退出。因此,如果无法正确地关闭 Executor,那么 JVM 将无法结束。

当关闭应用程序时,可能采用最平缓的方式关闭——等待所有任务完成且期间不接受任何新的任务,也可能采用粗暴的方式关闭——直接拉电闸:)。

为了解决 Executor 的生命周期问题,Executor 扩展了 ExecutorService 接口,添加了一些用于生命周期管理的方法(还有一些用于任务提交的方法)。

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);
  //其它用于任务提交的便利方法
    ......
}

shutdown 执行平缓的关闭过程:不再接受新任务,等待已经提交的任务完成——包括那些还未开始的任务。

shutdownNow 执行粗暴的关闭过程:尝试取消所有运行中的任务,并且不在启动队列中未开始执行的任务。

在 ExecutorService 关闭后提交的任务将由 “拒绝执行处理器(Rejected Execution Handler)”来处理,会抛弃任务或使得 execute 方法抛出一个未检查的 RejectedExecutionException 。

等待所有任务完成后 ExecutorService 将进入终止状态。可以调用 awaitTermination 来等待 ExecutorService 到达终止状态。

支持关闭操作的 Web 服务

public class LifecycleWebServer {
    
    private final ExecutorService exec = Executors.newCachedThreadPool();
    
    public void start() throws IOException {
        ServerSocket socket = new ServerSocket(80);
        //线程池的关闭即意味着Web服务器关闭,Web服务器逻辑不再执行
        while(!exec.isShutdown()) {
            try {
                final Socket conn = socket.accept();
                exec.execute(new Runnable() {
                    
                    @Override
                    public void run() {
                        handleRequest(conn);
                    }
                });
            } catch(RejectedExecutionException e) {
                if(!exec.isShutdown()) {
                    log("task submition rejected ",e);
                }
            }
        }
    }
    /**关闭服务器*/
    public void stop() {exec.shutdown();//关闭线程池}
    
    /**处理请求*/
    void handleRequest(Socket connection) {
        Request req = readRequest(connection);
        if(isShutdownRequest(connection))
            stop();
         else 
            dispatchRequest(req);
    }
}
延迟任务与周期任务

ScheduledThreadPoolExecutor 能正确处理Timer表现出错误行为的任务。

Timer 在执行所有定时任务时是单线程的,且 TImerTask 抛出一个未受查异常,那么 Timer 将表现出一个糟糕的行为(线程泄漏)。

如果要构建自己的调度服务,可以使用DelayQueue,他实现了BlockingQueue,并为ScheduledThreadPoolExecutor 提供调度功能。DelayQueue管理着一组Delayed对象。每个Delayed对象都有一个相应的延迟时间:在 DelayedQueue 中,只有某个元素逾期后,才能从 DelayQueue 中执行 take 操作。从DelayQueue 中返回的对象将根据他们的延迟时间进行排序。

错误的 Timer 行为

public class OutOfTime {
    public static void main(String[] args) throws Exception {
        Timer timer = new Timer();
        timer.schedule(new ThrowTask(), 1);
        SECONDS.sleep(1);
        timer.schedule(new ThrowTask(), 1);
        SECONDS.sleep(5);
    }

    static class ThrowTask extends TimerTask {
        public void run() {
            throw new RuntimeException();
        }
    }
}

找出可利用的并行性

Executor框架帮助指定执行策略,但如果使用Executor,必须将任务表述成一个Runnable。大多数服务器应用程序都存在一个明显的 任务边界:单个客户请求。
但有时候任务边界是模糊的,即使是服务器,单个客户请求仍然有可待发掘的并行性,例如DB服务器。

Executor 可以输入 Runnable 和 Callable。对于 Callable 对象有返回值,要使用 submit 方法,其返回值是一个 Future 对象。而 Runnable 则可以使用 execute 和 submit。

// AbstractExecutorService 中的三种 submit 实现
public <T> Future<T> submit(Callable<T> task) {   // Callable
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

public Future<?> submit(Runnable task) {   // 不带参数的 Runnable
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Runnable task, T result) {   // 带参数的 Runnable
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}


// AbstractExecutorService 中的 newTaskFor 实现,返回一个 RunnableFuture 对象
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

// RunnableFuture 接口实现
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

// 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;
}

FutureTask 源码

/**
 * FutureTask是Future以及Runnable的共同实现,
 * 并在Runnable的run中封装了Callable逻辑的调用,以及对其返回结果进行描述的其它操作
  * 这使得线程的调用可以有返回结果
 */
public class FutureTask<V> implements RunnableFuture<V> {
 
    private Callable<V> callable;  //正在的业务逻辑
     
   private Object outcome; // 业务逻辑返回的结果封装对象
 
    private volatile Thread runner;
     
    //构造函数1 Runnable实现类中引进 业务逻辑
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    //构造函数2 :封装了业务逻辑的Runnable实现类,转化为可返回结果的Callable!!
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
 
    /*设置返回结果*/
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
 
    /*
     * Runnable实现类的真正业务逻辑运行处,此处封装返回结果,释放资源
     * 真正的逻辑 来着从构造函数引进来的Callable接口实现的调用
     */
    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();   //业务逻辑真正被调用的地方
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);   //设置返回结果
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
 
   /*获取业务逻辑返回值,返回类型是Callable的泛型*/
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
 
    /*真正的返回值是类成员变量:outcome*/
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
}

AbstractExecutorServiceThreadPoolExecutor的基类,其中定义了newTaskFor接口,默认逻辑就是生成FutureTask对象的工厂;和定义submit接口,用来提交Callable的任务,内部用FutureTask来进行中转。

Executor 框架的成员及关系

实现
Future接口
RunnableFuture接口
Runnable接口
FutureTask类
Executor接口
ExecutorService接口
AbstractExecutorService抽象类
ScheduledExecutorService接口
ThreadPoolExecutor类
ScheduledThreadPoolExecutor类
Callable接口

Executor 框架的使用

创建
创建
execute或submit
submit
return
get或cancel
主线程
Runnable对象
Callable对象
ExecutorService实现类
FutureTask对象

管理一系列的Future对象,可以使用ExecutorService.invokeAll接口,一次性等待所有任务都完成;也可以使用内部包裹了BlockingQueueExcectorCompletionService,其管理的FutureTask在完成后会将自身加到阻塞队列中,而外部调用程序通过take队列数据可实现每得到一个数据就处理的逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值