并发编程-线程池

一、任务执行

1.1 在线程中执行任务

    围绕“任务执行”来设计应用程序结构时,第一步是要找出清晰的任务边界。在理想的情况下,各个任务之间是相互独立的:任务并不依赖其他任务的状态、结果或边界效应。而且,当负荷过载时,应用程序的性能应该是逐渐降低,而不是直接失败。要实现上述目标,应该选择清晰的任务边界以及明确的任务执行策略。

1.2 Executor框架

       java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分,任务的执行主要抽象不是Thread而是Executor

public interface Executor{
    void execute(Runnable command)
}

      虽然Executor是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能够支持多种不同类型的任务执行策略。它提供了一种标准方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

      Executor基于生产者-消费者。提交任务的相当于生产者,执行任务的线程则相当于消费者。只需要改变Executor的实现,就可以改变服务器行为,而这种改变所带来的影响要远远小于改变任务提交方式带来的影响。

1.2.2 执行策略

      将任务的提交与执行解耦开来,从而可以简单的为某种类型的任务指定和修改执行策略。在执行策略中定义了任务执行的“What、Where、When、How”等方面,包括:

  • 在什么(What)线程中执行任务?
  • 任务按照什么(Waht)顺序执行(FIFO、LIFO、优先级)?
  • 有多少个(How Many) 任务能并发执行?
  • 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程有任务拒绝?
  • 在执行一个任务之前或之后,应该进行哪些(What)动作?

     各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。通过限制并发任务的数量,可以确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源上发送竞争而严重影响性能。通过将任务的提交与任务的执行策略分离开来,有助于部署阶段选择与硬件资源最匹配的执行策略。

1.2.3 线程池

       线程池的好处是,

  • 降低资源的消耗。降低线程创建和销毁的资源消耗;
  • 提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间
  • 提高线程的可管’’理性。

通过调用Executors的静态工厂方法可以创建线程池:

  • newFixedThreadPool。创建固定长度线程池,如果某个线程由于发送未知错误而结束,线程池会补充一个新的线程。
  • newCachedThreadPool。 创建一个可缓存的线程池。如果线程数量超过处理需求,那么将回收线程。当需求增加,以添加新的线程,线程数量不受任何限制。
  • newSingleThreadExecutor。它创建单个工作线程来执行任务,如果这个线程异常结束,会创建另外一个线程代替,能确保依照任务在队列中的顺序来串行执行。单线程的Executor还提供了大量的内部同步机制,从而确保了任务执行的任何内存写入操作对于后续来说都是可见的,这意味着,即时这个线程被替代,但对象总是可以安全地封闭在“任务线程”中。
  • newScheduledThreadPool。创建一个固定长度的线程池,而以延迟或者定时的方式来执行任务。

        newFiedThreadPool和newCachedThreadPool这两个工厂方法返回通用的ThreadPoolExecutor实例,这些实例可以直接用来构造专门用途的Executor。“为每个任务分配一个线程”策略变成了基于线程从策略,将对应用程序的稳定性产生重大的影响,尽管服务器不会因为创建了过多的线程而失败,但是在足够长的时间内,如果任务到达的速度总是超过任务执行速度,那么等待执行的Runnable队列将不断增长,导致服务器耗尽内存。通过使用Executor,可以实现各种调优、管理、监视、记录日志、报错和其他功能。

1.2.4 Executor的生命周期

       JVM只有在所有线程全部终止后才会退出,因此如果无法正确关闭Executor,那么JVM将无法结束。由于Executor以异步方式来执行任务,因此在任何时刻,之前条件任务的状态不是立即可见的。当关闭应用程序时,可以采用最平缓的方式(完成所有已经启动的任务,并且不在接受任何新的任务),也可能采用最粗暴的关闭形式。

       为了解决执行服务声明周期的问题,Executor扩展了ExecutorService接口,添加一些用于生命周期的方法(同时还有一些用于任务提交的便利方法)。

         

public interface ExecutorService extends Executor {

    
    void shutdown();

    
    List<Runnable> shutdownNow();

   
    boolean isShutdown();

    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);

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

  ExecutorService的生命周期有3种状态:运行、关闭和已终止。ExecutorService关闭后提交的任务将由“拒绝执行处理器”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。通常在调用awaitTermination之后会立即调用shutdown,从而产生同步地关闭ExecutorService的效果。

       可以通过增加生命周期支持来扩展Web服务器的功能,比如关闭的时候记录日志,或者做一些任务抛弃处理。

1.2.5 延迟任务与周期任务

   Timer类负责关联延迟任务("在100ms后执行该任务")以及周期任务("每10ms执行一次该任务")。Timer的缺陷

  • 在于支持绝对时间,因此任务的执行对系统时钟的变化很敏感。而ScheduledThredPoolExecutor只支持基于相对时间的调度。
  • Timer在执行所有定时任务时只会创建一个线程。如果某个任务执行时间过长,那么将破坏其他TimerTask的定时精确性。例如某个周期TimeTask需要每10ms执行一次,而另外一个TimerTask需要执行40ms。那么这个周期任务将在40ms任务执行完成后快速连续地调用四次,或者彻底“丢失”四次调用(取决于基于固定速率,还是固定延时)。
  • 如果TimerTask抛出了一个未检查的异常,那么Timer并不捕获异常,而是将终止定时线程。已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度,这个过程称之为“线程泄漏”。

    

public class OutOfTime {

	public static void main(String[] args) throws InterruptedException {
	Timer timer=new Timer();
	timer.schedule(new ThrowTask(), 1);
	Thread.sleep(1000);
	timer.schedule(new ThrowTask(), 1);
	Thread.sleep(5000);
}
	static class ThrowTask extends TimerTask {

		@Override
		public void run() {
			throw new RuntimeException();

		}

	}
}
报错如下:
Exception in thread "Timer-0" java.lang.RuntimeException
	at Concurrent.test.OutOfTime$ThrowTask.run(OutOfTime.java:27)
	at java.util.TimerThread.mainLoop(Timer.java:555)
	at java.util.TimerThread.run(Timer.java:505)
Exception in thread "main" java.lang.IllegalStateException: Timer already cancelled.
	at java.util.Timer.sched(Timer.java:397)
	at java.util.Timer.schedule(Timer.java:193)
	at Concurrent.test.OutOfTime.main(OutOfTime.java:19)

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

1.3 找出可利用的并行性

1.3.1 携带结果任务Callable与Future

       Executor框架使用Runnable作为最基本的任务表现形式。Runnable时一种有很大局限的抽象,它不能返回一个值或者抛出一个受检查的异常。Callable是一种更好的抽象:它任务主入口点(即call)将返回一个值,并可能抛出一个异常(可以用Callable<Void>来表示无返回值的任务)。在Executor中包含一些辅助办法能将其他类型的任务封装为一个Callable,例如Runnable和java.security.PrivilegedAction。

        Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。在Executor框架中,已提交但尚未开始的任务可以取消,但是对于已开始的任务,只有它们能响应中断,才能取消。取消一个已经完成的任务不会有任何影响。

        Future表示一个任务的生命周期,并提供相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的意义是,任务的声明周期只能前进,不能后退。

         get方法的行为取决于任务的状态(尚未开始、正在运行、已完成)。如果任务已经完成,那么get会立即返回或者抛出一个Exception,如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将该异常封装为ExecutionException并且重新抛出。如果任务被取消,那么get将抛出CancellationException。如果get抛出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。

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

public interface Future<V> {
	boolean cancel(boolean mayInterruptIfRunning);

	boolean isCancelled();

	boolean isDone();

	V get() throws InterruptedException, ExecutionException, CancellationException;

	V get(long timeout, TimeUnit unit)
			throws InterruptedException, ExecutionException, CancellationException, TimeoutException;
}

  

ExecutorService中所有submit方法都将返回一个Future,从而将一个Runnable或者Callable提交给Executor,并得到一个Future用来获取任务的执行结果或者取消任务。

  public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
}

还可以显示地为某个指定的Runnable或者Callable实例化一个FutureTask(由于FutureTask实现了Runnable,因此还可以将它提交给Executor来执行,或者直接调用它的run方法)。

public class Test {
	public static class CallableThread implements Callable<Integer> {
		public Integer call() throws Exception {
			return sum();
		}
	}

	public static void main(String[] args) {
		CallableThread caThread = new CallableThread();
		FutureTask<Integer> result = new FutureTask<Integer>(caThread);
		Thread callThread = new Thread(result);
		callThread.start();
		Integer sum = result.get();
		System.out.println("得到的计算结果是:" + sum);
	}

}

从 Java6 开始,ExecutorService实现可以改写AbstractExecutorService中的newTaskFor方法,从而根据已提交的Runnable或Callable来控制实例化过程。

public abstract class AbstractExecutorService implements ExecutorService {
	protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
		return new FutureTask<T>(runnable, value);
	}

	protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
		return new FutureTask<T>(callable);
	}
.............................
}

在将Runnable或Callable提交到Executor的过程中,包含了一个安全发布过程,即将Runnable或Callable从提交线程发布到最终执行任务的线程。在设置Future结果的过程中也包含了一个安全发布,即将这个结果从计算它的线程发布到任何通过get获得它的线程。

get方法拥有“状态依赖”的内在特性,因而调用者不需要知道任务的状态,此外在任务提交和获得结果中包含的安全发布属性也确保了这个方法是线程安全的。Future.get的异常处理代码将处理两个可能的问题:任务遇到一个Exception,或者调用get的线程在获得结果之前被中断。

public class FutureTest1 {
	public static java.util.concurrent.ExecutorService ex = Executors.newFixedThreadPool(100);

	public static class CallableThread implements Callable<Integer> {
		public Integer call() throws Exception {
			 throw new RuntimeException(); 
		}
	}

	public static void main(String[] args) {
		Future<Integer> submit = ex.submit(new CallableThread());
		Thread.currentThread().interrupt();
		try {
			System.out.println(submit.get());
		} catch (InterruptedException e) {
			System.out.println("遇到中断了");
			Thread.currentThread().interrupt();
		} catch (ExecutionException e) {
			System.out.println("任务遇到异常了"+e.getMessage());
		}

	}
}
测试结果:
任务遇到异常了java.lang.RuntimeException
——-----------------------------------------------------------------
public class FutureTest1 {
	public static java.util.concurrent.ExecutorService ex = Executors.newFixedThreadPool(100);

	public static class CallableThread implements Callable<Integer> {
		public Integer call() throws Exception {
			Thread.sleep(2000);
			return 1;
		}
	}

	public static void main(String[] args) {
		Future<Integer> submit = ex.submit(new CallableThread());
		Thread.currentThread().interrupt();
		try {
			System.out.println(submit.get());
		} catch (InterruptedException e) {
			System.out.println("遇到中断了");
			
		} catch (ExecutionException e) {
			System.out.println("任务遇到异常了" + e.getMessage());
		}

	}
}
测试结果:
遇到中断了
————————————————————————————————————————————————————————————————————————————————————————
public class FutureTest2 {
	public static java.util.concurrent.ExecutorService ex = Executors.newFixedThreadPool(100);

	public static class CallableThread implements Callable<Integer> {
		public Integer call() throws Exception {
			Thread.sleep(2000);
			return 1;
		}
	}

	public static void main(String[] args) {
		Future<Integer> submit = ex.submit(new CallableThread());
		// Thread.currentThread().interrupt();
		try {
			submit.cancel(true);
			System.out.println(submit.get());

		} catch (InterruptedException e) {
			System.out.println("遇到中断了");

		} catch (ExecutionException e) {
			System.out.println("任务遇到异常了" + e.getMessage());
		} catch (CancellationException e) {
			System.out.println("任务取消了" + e.getLocalizedMessage());
		}

	}
}

测试结果:
任务取消了null


结论:如果任务抛出了异常,那么get将异常封装为ExecutionException并重新抛出。如果调用get的线程(这里是主线程)在获得结果之前被中断,就会抛出InterruptedException。如果任务被取消,那么get将抛出CancellationException。

1.3.2 在异构任务中存在的局限

 比如在渲染一个HTML页面时,把页面渲染分为文字渲染和页面渲染两种任务时,如果页面渲染的执行时间是文字渲染的10倍,那么整个过程也只加速了9%,然后在多个线程中分解任务,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行实现的提升。

 只有大量的相互独立且同构的任务可以并发处理时,才能体现出将程序的工作负载分配到多个任务中带来真正的性能提升。

1.3.3 CompletionServcie:Executor与BlockingQueue

    虽然Future可以通过轮询get来判断任务是否完成,但是这种方法非常繁琐,CompletionService(完成服务)可以更好的做到这种功能。CompletionService将Executor和BlockingQueue的功能融合在一起。可以将Callable任务提交给它来执行,然后通过类似于队列操作的take和poll等方法获得已经完成的结果,而这些结果会在完成时被封装为Future。ExecutorCompletionSevice实现了CompletionService并将计算部分委托给了一个Executor。

     ExecutorCompletionService的实现非常简单,在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用Future-Task中的done方法。当提交某个任务时,该任务将首先被包装为一个QueueingFuture,这是FutureTask的一个子类,然后再改写done方法,并且将结果存入BlockingQueue中。

public interface CompletionService<V> {
 
    Future<V> submit(Callable<V> task);
   
    Future<V> submit(Runnable task, V result);
   
    Future<V> take() throws InterruptedException;
    
    Future<V> poll();

    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}



public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    private final BlockingQueue<Future<V>> completionQueue;

    /**
     * FutureTask extension to enqueue upon completion
     */
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }

    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
            return new FutureTask<V>(task, result);
        else
            return aes.newTaskFor(task, result);
    }

    /**
     * Creates an ExecutorCompletionService using the supplied
     * executor for base task execution and a
     * {@link LinkedBlockingQueue} as a completion queue.
     *
     * @param executor the executor to use
     * @throws NullPointerException if executor is {@code null}
     */
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }

    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }

    public Future<V> submit(Runnable task, V result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task, result);
        executor.execute(new QueueingFuture(f));
        return f;
    }

    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

    public Future<V> poll() {
        return completionQueue.poll();
    }

    public Future<V> poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }

}

1.3.4 为任务设置时限

future,可以指定超时时间,如果超过则取消任务。

public class FutureTest1 {
	public static java.util.concurrent.ExecutorService ex = Executors.newFixedThreadPool(100);

	public static class CallableThread implements Callable<Integer> {
		public Integer call() throws Exception {
			Thread.sleep(2000);
			return 1;
		}
	}

	public static void main(String[] args) {
		Future<Integer> submit = ex.submit(new CallableThread());
		// Thread.currentThread().interrupt();
		try {
			submit.get(1, TimeUnit.SECONDS);
		} catch (TimeoutException e) {
			System.out.println("任务超时");
			submit.cancel(true);
		} catch (ExecutionException e) {
			System.out.println("任务遇到异常了" + e.getMessage());
		} catch (InterruptedException e) {
			System.out.println("任务中断了");
		}

	}
}

测试结果:
任务超时

1.3.5 InvokeAll

      “预定时间”方法可以很容易地扩展到任意数量的任务上。架设一个旅行预定门户网址:用户输入旅行的日期和其他要求,门户网址可以显示多条航线。在获取不同航线公司报价过程中,不应该让页面的响应受限于最慢的响应时间,而应该值显示在指定时间内收到的信息。

        从一个公司获得报价的过程与其他公司无关,因此可以将获得报价的过程当成一个任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到一个线程池,保留n个future,并使用限时的get方法通过future串行地获取每一个结果,这一切都很简单,但是还有一个更简单的办法——invokeAll。

        InvokeAll方法参数为一组任务,并且返回一组Future。这两个结合有着相同的结构。invokeAl会按照迭代顺序将所有的future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。当超过指定时限后,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户单代码可以用get或isCancelled来判断究竟是何种情况。

二、线程中任务的取消与关闭

要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事,Java没有提供任何机制来安全的终止线程(Thread.stop和suspend等方法提供了,但是有严重的缺陷),但是它提供了中断,这是一种协作机制,能够使一个线程终止另外一个线程的当前工作。这种协作方式是必要的,我们很少希望某个任务、线程或者服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。

2.1 任务取消

      取消某个操作或任务的原因有很多:用户取消,有时间限制的操作(超时),应用程序事件(比如其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消),错误(比如资源被消耗尽,所有任务需要被取消),关闭(程序或服务关闭时,必须对正在处理和等待工作执行操作进行取消)。

      Java中没有一种安全的抢占方法来停止线程,因此也就没有安全的抢占式方法来停止任务,只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。

      其中一种协作机制能设置某个“已请求取消”标志,而任务将定期查看该标志,标志位通过volatile修饰。

2.1.1 中断

      定期查看标志的取消机制,在退出过程中需要花费一定的时间,如果使用这种机制的任务调用了一个阻塞方法,例如BlockingQueue.put那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束。

      一些特殊的阻塞库的方法支持中断,线程中断是一种协作机制,线程可以通过这种机制来通知另外一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。

      如果在取消之外的其他操作中使用中断,那么都是不合适的,而且很难支撑起更大的应用。

       每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态被设置为true。

public class Thread{
	public void interrupt(){...}// 中断目标线程
	public boolean isInterrupted(){...}//返回目标中断状态
	public static boolean interrupted(){...}//清除中断状态的唯一方法,并返回它之前的值
	.........
}

      阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但是在实际情况中响应速度还是非常快的。中断是实现取消的最合理方式。

2.1.2 中断策略

     正如任务中应该包含取消策略一样,线程同样应该包含中断策略。中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

      最合理的中断策略是某种形式的线程级取消操作或服务级的取消操作:尽快退出,必要时进行清理,通知某个所有者该线程已经退出。此外还有其他中断策略,例如暂停服务或者重新开始服务。

       区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接受者——中断线程池中的某个工作者线程,同时意味着“取消当前任务”和“关闭工作者线程”。

        任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心保存中断状态,这样拥有线程的代码才能对中断做出响应。这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应。它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中上层代码可以采取进一步的操作。

        任务不应该对执行该任务的线程的中断策略做出任何假设(在中断线程之前,应该了解它的中断策略),除非该任务被专门设计为在服务中运行,并且在这些服务中包含了特定的中断状态。如果除了将InterruptedException传递给调用者外还需要执行其他操作,应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();

         正如任务代码不应该对其执行所在的线程中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,如关闭方法。

         由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

          Java的中断功能,没有提供抢占式中断机制,而且还强迫开发人员必须处理InterruptedException,然而,通过推出中断请求的处理,开发人员能指定更灵活的策略,从而使应用程序在响应性和健壮性之间实现合理的平衡

2.1.3 响应中断

          当调用可中断的阻塞函数时,例如Thread.sleep或者BlockingQueue.put等,有两种使用策略可用于处理InterruptedException:

  •    传递异常(可能 在执行某个特定任务的清除操作之后),从而使你的方法也成为可以中断的阻塞方法。
  •     恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。

       如果不想或者无法传递InterruptedException(比如通过Runnable来定义任务),那么需要另外一种方式来保存中断请求,比如再次调用interrupt来恢复中断状态。不能随意屏蔽InterruptedException(catch了却不处理),除非你在代码中实现了线程的中断策略。

       对于一些不支持取消但是仍可以调用可中断阻塞方法的操作,他们必须在循环中调用这些方法,并在发现中断后重新尝试。

2.1.4 通过Future实现取消

       ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel(boolean mayInterruptIfRunning),这表示任务是否能够接受中断,而不是表示任务能检查并且处理中断。如果mayInterruptRunning为true并且任务当前正在某个线程中运行,那么这个线程能够被中断。如果这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于哪些不处理中断的任务中。

       除非你清除线程的中断策略,否则就不要中断线程。那么在什么情况下调用cancel可以将参数指定为true?执行任务的线程是有标准的Executor创建的,它实现了一个中断策略使得任务可以通过中断被取消。如果任务在标准的Executor中运行,并通过他们的Future来取消任务,那么可以设置mayInterruptIfRuning。当尝试取消某个任务时,不应该直接中断线程池,而是通过任务的Future来实现取消。

2.1.6 处理不可中断的阻塞

       在Java库中,许多可阻塞的方法是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。

       对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止线程,但是这些要求我们必须知道线程阻塞的原因。

        Java.io包中的同步Socket I/O。最常见的阻塞I/O的形式就是对套接字进行读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个socketExcetion。

         Java.io包中的同步I/O. 当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(这还使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路上操作阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibleChanel.

         Selector的异步I/O。如果一个线程在调用Selector.select方法(在java.nio.channels中)时阻塞了,那么调用close或wakeup方法会是线程抛出ClosedSelectorException并提前返回。

         获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定能获得锁,所以将不会理会中断请求。但是,在Lock类中提供给了lockInterruptibly方法,该方法运行等待一个锁的同时仍然能响应中断。

          下面ReaderThread给出了Socket I/O,read的阻塞不能响应中断,但是经过改造后,ReaderThread无论是在read方法中阻塞还是某个可中断的阻塞方法中阻塞,都可以被中断并停止执行当前的工作。

             

public class ReaderThread extends Thread {

	private final Socket socket;
	private final InputStream in;

	public ReaderThread(Socket socket) throws IOException {
		this.socket = socket;
		this.in = socket.getInputStream();
	}

	public void interrupt() {
		try {
			socket.close();
		} catch (IOException ignored) {
		} finally {
			super.interrupt();
		}
	}
	public void run(){
		try{
			byte[] buf=new byte[1024];
			while(true){
				int count=in.read(buf);
				if(count<0) {
					break;
				}else if(count >0){
					//useBuf(buf,count)
				}
			}
		}catch(IOException e){
			//允许线程退出
		}
	}
}


本来 in.read是阻塞且不可响应中断的方法,但是在ReaderThread中调用了interrupt方法后,会关闭socket,在关闭socket后,in.read会报错IOException导致线程退出以响应中断。

2.1.7 采用newTaskFor来封装非标准的取消

 我们可以通过newTaskFor方法来进一步优化ReaderThread中封装非标准取消的技术,这是在ThreadPoolExcutor中新增的功能。当把一个Callable提交给ExecutorService时,submit方法会返回一个Future,我们可以通过Future.cancel来取消任务。

      在Java6的ThreadPoolExecutor中新的的功能,newTaskFor是一个工厂方法,它创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,RunnableFuture继承Future和Runnable(并由FutureTask实现)。

       通过CancellingExecutor扩展的ThreadPoolExecutor,改写newTaskFor使得CancellableTask可以创建自己的Future,然后在调用Future.cancel时,关闭socket。如果SocketUsingTask通过对自己的Future来取消,那么底层的套接字将被关闭并且线程将被中断。因此它提高了对取消操作的响应性:不仅能够在调用可中断方法的同时确保响应取消操作,还能调用可阻调的套接字I/O方法。

public interface CancellableTask<T> extends Callable<T> {
	void cancel();

	RunnableFuture<T> newTask();
}

public class CancellingExecutor<T> extends ThreadPoolExecutor {

	public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
			BlockingQueue<Runnable> workQueue) {
		super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
		// TODO Auto-generated constructor stub
	}

	protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
		if (callable instanceof CancellableTask) {
			return ((CancellableTask<T>) callable).newTask();
		} else {
			return super.newTaskFor(callable);
		}
	}


}


public class SocketUsingTask<T> implements CancellableTask<T> {
	private Socket socket;

	protected synchronized void setSocket(Socket s) {
		socket = s;
	}

	public T call() throws Exception {
		// TODO Auto-generated method stub
		return null;
	}

	public synchronized void cancel() {
		try {
			if (socket != null) {
				socket.close();
			}
		} catch (IOException ignored) {

		}

	}

	public RunnableFuture<T> newTask() {
		FutureTask<T> futureTask = new FutureTask<T>(this) {
			public boolean cancel(boolean mayInterruptIfRunning) {
				try {
					SocketUsingTask.this.cancel();
				} finally {
					return super.cancel(mayInterruptIfRunning);
				}

			}
		};

		return futureTask;
	}

}

2.2 停止基于线程的服

       正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如,中断线程或者修改线程的优先级。因此线程池是其工作者的线程所有者,如果要中断这些线程,那么应该使用线程池。

        与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作线程,但是应用程序不可能拥有工作线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法来关闭它自己以及它所拥有的的线程。这样,当应用程序关闭该服务时,服务就可以关闭所有的线程。在ExecutorService中提供了shudown和shutdownNow等方法。

2.2.1 “毒丸”现象

        一种关闭生产者-消费者的服务方式就是使用“毒丸对象”,“毒丸”是指一个放在队列上的对象,其含义是:“当得到这个对象时,立即停止”。在FIFO(先进先出)队列中,“毒丸”对象将确保消费者在关闭之前首先完成队列的所有工作,在提交“毒丸”对象之前所有工作都会被清理,而生产者在提交“毒丸对象”后,将不会再提交任务工作。

         

public class IndexingService<T> {
	private static final File POISON = new File("");
	private final IndexerThread consumer = new IndexerThread();

	private final BlockingQueue<File> queue;
	private final FileFilter fileFilter;
	private final File root;

	class CrawlerThread extends Thread {
		public void run() {
			try {
				crawl(root);
			} catch (InterruptedException e) {

			} finally {
				while (true) {
					try {
						queue.put(POISON);
						break;
					} catch (InterruptedException e1) {
						// 忽略报错,以重新尝试
					}
				}
			}
		}

		private void crawl(File root) throws InterruptedException {

		}
	}

	public class IndexerThread extends Thread {
		public void run() {
			try {
				while (true) {

					File file = queue.take();
					if (file == POISON) {
						break;
					} else {
						indexFile(file);
					}
				}
			} catch (InterruptedException consumed) {

			}
		}

		private void indexFile(File file) throws InterruptedException {

		}
	}
}

       只有在生产者和消费者数量都已知的情况下,才可以使用“毒丸”对象。在IndexingService中采用的解决方案可以扩展到多个生产者:只需要每个生产者都向队列中放入一个毒丸,并且在消费者接收到producers个“毒丸对象”才停止。这种方法也可以扩展到多个消费者的情况,只需生产者将consumers个“毒丸对象”放入队列。

2.2.2 示例:只执行一次的服务

      如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一个私有的Executor来简化服务的生命周期管理,其中该Executor的生命周期由这个方法来管理。

private void indexFile(List<String> hosts ) throws InterruptedException {		
			try {
				for (final String host : hosts) {
					exec.execute(new Runnable() {
						public void run() {
						}
					});
				}
			} finally {
				exec.shutdown();
				exec.awitTermination(timeout, unit);
			}
		}

2.2.3 shutdownNow的局限性

       通过shutDownNow强行关闭Executor时,它会尝试取消正在执行的任务,并返回所有已提交但是尚未开始的任务,不会返回正在执行的任务。如果想要知道关闭的时候哪些任务正在执行,可以在execute的时候记录哪些任务是在关闭后取消的。

public class TrackingExecutor<T> extends AbstractExecutorService {
	private final ExecutorService exec;
	private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedCollection(new HashSet<Runnable>());

	public void execute(final Runnable command) {
		exec.execute(new Runnable() {

			public void run() {
				try {
					command.run();
				} finally {
					if (isShutdown() && Thread.currentThread().isInterrupted()) {
						tasksCancelledAtShutdown.add(command);
					}
				}

			}

		});

	}
.............................

}

2.3 处理非正常的线程终止

       当单线程的控制台程序由于反生了一个未捕获的异常而终止时,应用程序将停止运行,并且产生与程序正常输出非常不同的栈追踪信息。然而并发程序中,某个线程发送故障时,应用程序看起来仍然在工作,所以这个失败很可能被忽略。

       导致线程提前死亡的最主要原因是RuntimeException。如果不是受检查的异常,它们通常没有显示捕获,没有在调用栈中逐层传递,而是默认地在控制台中输出栈追踪信息,并终止线程。线程非正常退出的后果可能是良性的,也可能是恶性的。比如50个线程的线程池上运行良好,丢失一个线程影响不大。然而如果在GUI程序中丢失了事件分派线程,那么造成应用程序将停止处理事件,并且GUI因此失去响应。Timer的timerTask如果抛出RuntimeException会导致整个Timer服务无法使用。

      在任务处理线程的声明周期中,通过某些抽象机制调用许多未知代码。需要对这些代码是否会抛出不可预期的异常保持怀疑。下面代码给出了如何在线程池内部构建一个工作者线程。如果任务抛出了一个未检查异常,那么它将使线程中介,但会首先通知框架该线程已经终结。

	public void run() {
				Throwable thrown=null;
				try{
					while(!isInterrupted()){
						//to do
					}
				}catch(Throwable e){
					thrown=e;
				}finally{
					threadExited(this,thrown);
				}
			}

          上面通过主动方法来解决未检查异常。在Thread API中同样提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获异常而终结的情况。当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有对线程提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err。在Java5.0之前,控制UncaughtExceptionHandler的唯一方法就是对ThreadGroup进行子类化。在Java5.0以及之后,可以通过Thread.setUncaughtExceptionHandler为每个线程设置一个unCaughtException。在这些异常处理器中,只有一个将被调用——JVM首先搜索每个线程的异常处理器,然后在搜索ThreadGroup的异常处理器。ThreadGroup的默认异常处理器实现将异常处理工作逐层委托它的上层ThreadGroup,直到某个ThreadGroup的异常处理器能够处理该未捕获的异常,否则将一直传递到顶层。顶层ThreadGroup的异常处理器委托给默认的系统处理器,否则将把栈追踪信息输出到控制台。

         

public interface UncaughtExceptionHandler{
void uncaughtException(Thread t,Throwable e);
}

我们最常见的响应方式是将一个错误信息记录到日志,或者尝试重新启动线程,关闭应用程序等。

public class UEHLogger implements Thread.UncaughtExceptionHandler{
      public void uncaughtException(Thread t, Throwable e){
            Logger logger= Logger.getAnonymousLogger();
            logger.log(Level.SEVER,"Thread terminated with exception"+t.getname,e);
      }
}

要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。(与所有的线程操控一样,只有线程所有者能够改变线程的UncaughtExceptionHandler。)下面代码是Executors新建一个FixedThreadPool,ThreadFactory的默认实现中拥有ThreadGroup实现了UncaughtExceptionHandler,默认是将栈追综信息输出到System.err。

public class Executors {

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


static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }



public  class ThreadGroup implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

...
}

如果希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。

   只有通过execute提交的任务,才能将它抛出的异常交给为未捕获异常处理器,而通过submit提交的任务,无论是抛出未检查异常还是已检查异常,都是被认为是任务返回状态的一部分。如果是一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。

2.4 JVM关闭

     jvm既可以正常关闭,也可以强行关闭。正常关闭的触发方式有很多:当最后一个“正常(非守护)线程结束时”,或者当调用了SYstem.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-C)。虽然可以通过这些标准方法关闭JVM,但是也可以通过调用Runtime.halt或者再操作系统中Kill jvm进程。

 2.4.1 关闭钩子

      关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。在正常关闭中,JVM首先会调用已注册的关闭钩子。关闭钩子必须尽快推出,因为它们会延迟JVM的结束事件,而用户可能希望JVM能尽快停止。关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。

      由于关闭钩子是并发执行,因此在关闭日志文件时可能导致其他需要日志服务的关闭钩子产生问题。为了防止这种情况,关闭钩子不应该依赖哪些可能被应用程序或其他关闭钩子关闭的服务。实现这种功能的一种方式是对所有服务使用同一个关闭钩子。

	public void start() {
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				try {
					LogService.this.stop;
				} catch (InterruptedException ignored) {

				}
			}
		});
	}

2.4.2 守护线程

       线程可以分为两种:普通线程和守护线程。在JVM启动时创建的所有线程,除了主线程以外,其余的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈。我们应该尽可能少的使用守护线程——很少有操作在进行清理的情况下被安全的抛出,守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。守护线程不能用来替代应用程序管理程序中各个服务的声明周期。

2.4.3 终结器

       当不需要内存资源时,可以通过垃圾回收器来回收他们,但对于其他一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显示地交换给操作系统。垃圾回收器对那些定义了finalize方法的对象会进行特殊处理:在回收期释放它们后,调用它们的finalize方法,从而保证资源被释放。

       由于终结器可以在某个由JVM管理的线程中运行,因此终结器访问状态能被多个线程访问,这样就必须对其访问操作进行同步。终结器并不能保证它们将在合适运行甚至是否会运行,并且复杂的终结器还会在对象上产生巨大的性能开销,在大多数情况通过try-finally代码块和显示的close方法。唯一的例外在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。我们要尽量避免使用包含终结器的类。

三、线程池的使用

3.1 在任务与执行策略之间的隐性耦合

Executor框架可以将任务的提交与任务的执行策略解耦开来,为制定和修改执行策略都提供了相当大的灵活性,但是并非所有的任务都能适用所有的执行策略。有些任务需要明确的指定执行策略:

  • 依赖性任务。如果提交给线程池的任务需要依赖其他任务,那么就隐含地给执行策略带来约束,此时必须小心地维护执行策略以避免产生活跃性问题(比如活锁)。
  • 使用线程封闭机制的任务。
  • 对时间敏感的任务。如果将一个运行时间长的任务提交给单线程的Executor中,或者将多个运行时间较长的任务提交给只包含少量线程的线程池中,需要指定明确的执行策略
  • 使用ThreadLocal任务。ThreadLocal使每个线程都可以拥有某个变量的私有“版本”,只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池中的线程中不应该使用ThreadLocal在任务间传递值。

只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则很有可能造成“拥塞”。如果提交的任务依赖于其他任务,除非线程池无限大,否则将可能造成死锁。

3.1.1 运行时间较长的任务

如果任务阻塞的时间过长,那么即时不出现死锁,线程池的响应性也会变得糟糕。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本。例如Thread.join,BlockingQueue.put、CountDownLatch以及Selector.select.

3.2 设置线程池的大小

        代码中通常不会固定线程池大小,而应该通过某种机制来提供,比如根据Runtime.availabelProcessors来动态计算。线程池大小不要出现过大或者过小这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的Cpu和内存资源上竞争,这会导致更高的内存使用量,而且还可能耗尽资源。如果线程池过小,那么将导致许多空闲处理器无法执行工作,从而降低吞吐率。

        计算密集型的任务,在拥有N个cpu的处理器系统上,线程池大小设置为N+1,通常能实现最优利用率。(即时当计算密集型的线程偶尔由于故障或者其他原因时,这个“额外”的线程也能确保Cpu的时钟周期不会被浪费)。

        对于I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确的设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值(记做 W/C)。CPU的数量记做N(可通过Runtime.getRuntime().availableProcessors()得到),CPU利用率记做U,要使处理器达到期望使用率,线程池的最优大小等于:

                    线程池大小=N*U*(1+W/C);

        当然CPu的周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。比如数据库连接受限,线程数量也会跟着受限。同样线程池中的任务是数据库的唯一使用者,那么线程池的大小又将限制连接池的大小。计算这些资源对线程池的约束条件是更容易的:计算每个任务对该资源的需求量R,然后用该资源的可用总量S,可以得到线程池大小的上限R/S。

3.3 配置ThreadPoolExecutor

        ThreadPoolExecutor为一些Executor提供了基本实现,Executors的newCachedThreadPool、newFixedThreadPool等工厂方法都是通过新建ThreadPoolExecutor对象,并且通过不同的参数(线程数量,活跃时间等)返回的不同的Executor。ThreadPoolExecutor是一个灵活的、稳定的线程池,允许各种定制。

          如果默认的执行策略不能满足需求,那么可以通过ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制。

          

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

3.3.1 线程的创建与销毁

       线程池的基本大小就是corePoolSize,即在没有任务执行时线程池的大小(在创建ThreadPoolExecutor初期,线程并不会立即启动,而是等到有任务提交时才会启动)。只有在工作队列满了的情况下才会创建超出这个数量的线程,然后一直持续创建线程到最大线程数量。如果某个线程的空闲时间超过了keepAliveTime,那么将被标记为可回收的,并且当线程池的当前大小超过corePoolSize时,这个线程将被终止。

       newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并且将超时设置为1分钟,这种方法创建出来的线程池会无线扩展,并且当需求降低时会自动收缩。

       如果开发人员将线程池的corePoolSize大小设置为零,如果在线程池中没有使用SynchronousQueue作为其工作队列(例如newCachedThreadPool就是synchronousQueue作为工作队列),当把任务提交给该线程池时,只有当线程池的工作队列被填满后,才会开始执行任务。因为线程池中的线程数量等于corePoolSize时,线程工作队列已经满了的情况下,ThreadPoolExecutor才会创建新的线程,所以如果corePoolSize等于零,那么在线程工作队列已经满了才会创建线程,才会执行任务。 如果希望线程池在没有任务的情况下销毁所有线程,可以在在设置corePoolSize=0后,通过allowCoreThreadTimeOute(Java6才有)来使所有线程超时,然后销毁所有线程。

3.3.2 管理队列任务

      ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本任务排队方法有3种:无界队列、有界队列和同步移交(Synchronous HandOff)。

      newFixedThreadPool和newSignleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。如果任务持续的快速达到,并且超过线程池处理它们的速度,那么队列将无限制地增加,耗尽资源。

       一种更稳健的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PrioriteBlockingQueue。有界队列被填满后,可以执行饱和策略。使用有界的工作队列时,队列的大小与线程池的大小必须是一起条件。如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文的切换,但是付出的代价是限制吞吐率。

       对于非常大或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者已经给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程直接进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另外一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小大于最大值,就会根据饱和策略,这个任务会被拒绝。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。

       LinkedBlockingQueue或ArrayBlockingQueue这样的FiFO,任务的执行顺序与它们到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。任务的优先级是通过自然顺序或者Comparator(如果任务实现了Comparable)来定义的。

         对于Executor,newCachedThreadPool是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能(由于使用了SynchronousQueue而不是linkedBlockingQueue)。当需要限制当前任务数量以满足资源管理需要,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载的问题。

        只有当任务互相独立时,为线程池或工作队列设置接线才是合理的。如果任务直接存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。

3.3.3 饱和策略

     当有界队列被填满后,饱和策略开始发挥作用(如果某个任务被提交到一个已被关闭的Executor时,也会用到包含策略)。JDK提供了不同的RejectedExectionHandler实现:

  • AbortPolicy中止策略。是默认的饱和策略,该策略会抛出未检查的RejectedExecutionException。调用这可以捕获这个异常,然后编写自己的处理代码。
  • DiscardPolicy 抛弃策略。悄悄抛弃该任务。
  • DicardOldestPolicy抛弃最旧的策略。抛弃下一个将被执行的任务,然后尝试提交新的任务。如果工作队列是一个优先队列,那么这个策略将导致抛弃优先级最高的任务,最后不要把这个策略和优先级队列放在一起使用。
  • CallerRunsPolicy 调用者运行策略。不会抛弃任务,在一个调用了execute的线程中执行任务。由于执行任务需要一定时间,因此执行execute的线程一段时间不会提交任何任务,从而使得工作者线程来完成正在执行的任务。比如WebServer,在这期间主线程不会调用accept,因此到达的请求将被保存到TCP层的队列而不是应用程序的队列中,如果持续过载,那么TCP层队列最终被填满,因此同样开始抛弃请求。当服务器过载时,这种过载情况会逐渐向外蔓延——从线程池到工作队列到应用程序再到TCP,最终到达客户端,导致服务器在高负载下实现一种平缓的性能降低。

如果没有预定义饱和策略,可以通过Semaphore来限制任务的到达率。

public class BoundedExecutor<T> {

	private final Executor exec;
	private final Semaphore semaphore;

	public void submitTask(final Runnable command) throws InterruptedException {
		semaphore.acquire();
		try {
			exec.execute(new Runnable() {

				public void run() {

					try {
						command.run();
					} finally {
						semaphore.release();
					}
				}
			});
		} catch (RejectedExecutionException e) {
			semaphore.release();
		}
	}

}

3.3.4 线程工厂

        每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。通过指定一个线程工厂方法,可以定制线程池的配置信息。可以在创建线程池Executor时,指定线程工厂,从而达到实现新建线程的额外配置,比如为每个线程设置名称,指定UncaughtExceptionHandler,在线程执行启动时记录日志等等。

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }


public interface ThreadFactory {

    Thread newThread(Runnable r);
}

3.3.5 在调用构造函数后再定制ThreadPoolExecutor

     在调用完ThreadPoolExecutor的构造函数后,仍然可以通过设置函数(Setter)来修改大多数参数(corepoolsize,maxPoolSzie,keepaliveTime,ThreadFactory,RejectedHandler)等等。如果Executor时通过Executors中的某个工厂方法创建,那么可以将结果的类型转化为ThreadPoolExecutor来访问设置器来改变参数(newSingleThreadExecutor除外)。

	ExecutorService exec = Executors.newCachedThreadPool();
		if(exec instanceof ThreadPoolExecutor){
			((ThreadPoolExecutor)exec).setCorePoolSize(10);
		}else{
			//...
		}

    Executors中包含一个unconfigurableExecutorService工厂方法,该方法对ExecutorService进行包装,使其只暴露ExecutorService方法,因此不能修改配置。newSingleThreadExecutor返回按这种方式封装的ExecutorService,而不是最初的ThreadPoolExecutor。因为如果在代码中增加单线程Executor的线程池大小,那么将会破坏它的执行语义。

3.4 扩展 ThreadPoolExecutor

       ThreqdExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。(如果任务完成后带有一个Error,那么就不会调用afterExecute)。如果BeforeExecute抛出一个RuntimeException,那么任务将不会执行,也不会被调用。  

3.5 手写一个线程池

   新建一个实现了Runnable的任务

  // 任务类
    static class MyTask implements Runnable {

        private String name;
        private Random r = new Random();

        public MyTask(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public void run() {// 执行任务
            try {
                Thread.sleep(r.nextInt(1000)+2000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
                        +Thread.currentThread().isInterrupted());
            }
            System.out.println("任务 " + name + " 完成");
        }
    }

   新建一个简单的线程池

public class MyThreadPool2 {
    // 线程池中默认线程的个数为5
    private static int WORK_NUM = 5;
    // 队列默认任务个数为100
    private static int TASK_COUNT = 100;  
    
    // 工作线程组
    private WorkThread[] workThreads;

    // 任务队列,作为一个缓冲
    private final BlockingQueue<Runnable> taskQueue;
    private final int worker_num;//用户在构造这个池,希望的启动的线程数

    // 创建具有默认线程个数的线程池
    public MyThreadPool2() {
        this(WORK_NUM,TASK_COUNT);
    }

    // 创建线程池,worker_num为线程池中工作线程的个数
    public MyThreadPool2(int worker_num,int taskCount) {
    	if (worker_num<=0) worker_num = WORK_NUM;
    	if(taskCount<=0) taskCount = TASK_COUNT;
        this.worker_num = worker_num;
        taskQueue = new ArrayBlockingQueue<>(taskCount);
        workThreads = new WorkThread[worker_num];
        for(int i=0;i<worker_num;i++) {
        	workThreads[i] = new WorkThread();
        	workThreads[i].start();
        }
    }


    // 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
    public void execute(Runnable task) {
    	try {
			taskQueue.put(task);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

    }


    // 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
    public void destroy() {
        // 工作线程停止工作,且置为null
        System.out.println("ready close pool.....");
        for(int i=0;i<worker_num;i++) {
        	workThreads[i].stopWorker();
        	workThreads[i] = null;//help gc
        }
        taskQueue.clear();// 清空任务队列
    }

    // 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
    @Override
    public String toString() {
        return "WorkThread number:" + worker_num
                + "  wait task number:" + taskQueue.size();
    }

    /**
     * 内部类,工作线程
     */
    private class WorkThread extends Thread{
    	
    	@Override
    	public void run(){
    		Runnable r = null;
    		try {
				while (!isInterrupted()) {
					r = taskQueue.take();
					if(r!=null) {
						System.out.println(getId()+" ready exec :"+r);
						r.run();
					}
					r = null;//help gc;
				} 
			} catch (Exception e) {
				// TODO: handle exception
			}
    	}
    	
    	public void stopWorker() {
    		interrupt();
    	}
    	
    }
}

新建测试类

public class TestMyThreadPool {
    public static void main(String[] args) throws InterruptedException {
        // 创建3个线程的线程池
        MyThreadPool2 t = new MyThreadPool2(3,0);
        t.execute(new MyTask("testA"));
        t.execute(new MyTask("testB"));
        t.execute(new MyTask("testC"));
        t.execute(new MyTask("testD"));
        t.execute(new MyTask("testE"));
        System.out.println(t);
        Thread.sleep(10000);
        t.destroy();// 所有线程都执行完成才destory
        System.out.println(t);
    }
}

测试结果:
10 ready exec :com.xiangxue.ch6.mypool.TestMyThreadPool$MyTask@2ea4c8fe
WorkThread number:3  wait task number:2
11 ready exec :com.xiangxue.ch6.mypool.TestMyThreadPool$MyTask@614b201a
12 ready exec :com.xiangxue.ch6.mypool.TestMyThreadPool$MyTask@32e40053
任务 testB 完成
12 ready exec :com.xiangxue.ch6.mypool.TestMyThreadPool$MyTask@36db82d7
任务 testA 完成
10 ready exec :com.xiangxue.ch6.mypool.TestMyThreadPool$MyTask@2063335a
任务 testC 完成
任务 testD 完成
任务 testE 完成
ready close pool.....
WorkThread number:3  wait task number:0

线程池的实现逻辑:

1、在new线程池对象pool的时候,就传入线程池中线程数量worker_num和阻塞队列的大小taskCount。在初始化线程池的时候,初始化了阻塞队列,和新建了worker_num数量的线程并且启动这些线程。线程的执行内容,就是通过无限循环从阻塞队列中得到任务,并且执行。

2、在pool.execute的方法中只是将需要执行的任务放入阻塞队列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值