多线程小抄集(新编三)

多线程小抄集(新编三)

终止正在运行的线程的三种方法

使用退出标志,是线程正常退出,也就是当run方法完成后线程终止;
使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样都是作废过期的方法,使用它们可能产生不可预料的结果;
使用interrupt()方法中断线程;

线程中断

interrupted()方法:返回对应线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用interrupted(),第一次返回的结果为true,第二次一般就是false (除非同时又发生了一次中断)。

isInterrupted()方法:返回对应线程的中断标志位是否为true。

这两个方法的定义如下:

public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);

interrupt()方法:中断线程。

  • 如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。
  • 如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。
  • 如果线程处于WAITING/TIMED_WAITING在这些状态时,对线程对象调用interrupt()会使得该线程抛出InterruptedException。需要注意的是,方法在抛出InterruptedException之前,JVM会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
  • 如果线程在运行(Runnable)中,interrupt()只是会设置线程的中断标志位,没有任何其它作用。

interrupt方法不一定会真正”中断”线程,它只是一种协作机制,如果不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程。对于以线程提供服务的程序模块而言,它应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者。Java并发库的一些代码就提供了单独的取消/关闭方法,比如说,Future接口提供了如下方法以取消任务:boolean cancel(boolean mayInterruptIfRunning)。再比如ExecutorService提供的两个关闭方法:void shutdown()和 List shutdownNow()。

处理不可中断的阻塞

对于以下几种情况,中断请求只能设置线程的中断状态,除此之外没有其他任何作用:

  • Java.io包中的同步Socket I/O:虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。
  • Java.io包中的同步I/O:当中断一个在InterruptibleChannel上等待的线程时会抛出ClosedByInterrptException并关闭链路。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
  • Selector的异步I/O:如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  • 获得某个锁:如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求,但是在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

线程池

线程池的作用:利用线程池管理并复用线程、控制最大并发数等;实现任务线程队列缓存策略和饱和策略;实现某些与时间相关的功能,如定时执行、周期执行等;隔离线程环境。可以通过ThreadPoolExecutor来创建一个线程池:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  1. corePoolSize 表示常驻核心线程数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁(除非allowCoreThreadTimeOut设置为true)。向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。

  2. maximumPoolSize 表示线程池能够容纳同时执行的最大线程数(>0)。线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,如果当前的线程数小于maximumPoolSize,则会新建线程来执行任务。注意,如果使用的是无界队列,该参数也就没有什么效果了。如果maximumPoolSize与coolPoolSize相等,即是固定大小线程池。

  3. keepAliveTime 表示线程池中的线程空闲时间,当空闲时间到达keepAliveTime值时,线程会被销毁,直到只剩下corePoolSize个线程为止。在默认情况下,当线程池的线程数大于corePoolSize时,这个参数才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量设置为true时,核心线程超时后也会被回收,可以通过ThreadPoolExecutor.allowCoreThreadTimeOut(boolean value)设置。

  4. unit 表示时间单位。keepAliveTime的时间单位通常是TimeUnit.SECONDS。

  5. workQueue 表示缓存队列。对于无界队列,可以忽略该参数。

    1. 如果运行的线程数少于 corePoolSize,则Executor 始终首选添加新的线程,而不进行排队。
    2. 如果运行的线程数等于或多于 corePoolSize,则Executor 始终首选将请求加入队列,而不添加新的线程。
    3. 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
    • ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
    • LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,FIFO。
    • SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作,反之亦然。
    • PriorityBlockingQueue:具有优先级的无界阻塞队列。
  6. threadFactory 用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)

  7. handler 表示执行饱和策略的对象。当超过workQueue的任务缓存区上限的时候,就可以通过该策略处理请求。可以实现自己的拒绝策略,例如记录日志等等,实现RejectedExecutionHandler接口即可。可以拒绝策略有4种:
    a. AbortPolicy:直接抛出异常RejectedExecutionException,默认策略
    b. CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
    c. DiscardPolicy:直接丢弃任务
    d. DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个对象可以判断任务是否执行成功。如Future future = executor.submit(task);

利用线程池提供的参数进行监控,参数如下:

  • getTaskCount():线程池需要执行的任务数量。
  • getCompletedTaskCount():线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  • getLargestPoolSize():线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize():线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount():获取活动的线程数。

shutdown和shutdownNow

可以调用线程池的shutdown或者shutdownNow方法来关闭线程池。shutdown和shutdownNow的区别:1. 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。2. 执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,shutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

扩展ThreadPoolExecutor

可以通过继承线程池来自定义线程池,重写线程池的beforeExecute, afterExecute和terminated方法。在执行任务的线程中将调用beforeExecute和afterExecute等方法,在这些方法中还可以添加日志、计时、监视或者统计信息收集的功能。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。如果任务在完成后带有一个Error,那么就不会调用afterExecute。如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。在线程池完成关闭时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后,terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者手机finalize统计等操作。

合理地配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。

  • 对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数;
  • 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率;
  • 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为Ncpu+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费)对于包含IO操作或者其它组she操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间和计算时间的比值。

Ncpu = number of CPUs
Ucpu = target CPU utilization, 0<=Ucpu<=1
W/C = ratio of wait time to compute time

要使处理器达到期望的使用率,线程池的最优大小等于

Nthreads = Ncpu * Ucpu * (1+W/C)

Amdahl定律

在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个取决于程序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高加速比为

Speedup<= 1/(F+(1-F)/N)

提交任务

ThreadPoolExecutor中可以使用两个方法向线程池提交任务,分别是execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这个时候又可能任务还没有执行完。

Future & FutureTask

FutureTask表示的计算是通过Callable来实现的,相当于一种可生产结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。运行表示计算的所有可能结束方式,包括正常结束、由于取消而结束和由于异常而结束等。当FutureTask进入完成状态后,它会永远停止在这个状态上。Future.get的行为取决于任务的状态,如果任务已经完成,那么get会立刻返回结果,否则get将阻塞直到任务进入完成状态,然后返回结果或者异常。FutureTask的使用方式如下:

public class Preloader
{
//method1
private final static FutureTask<Object> future = new FutureTask<Object>(new Callable<Object>(){
@Override
public Object call() throws Exception
{
return "yes";
}
});

//method2
static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static final Future<Object> futureExecutor = executor.submit(new Callable<Object>(){
@Override
public Object call() throws Exception
{
return "no";
}
});

public static void main(String[] args) throws InterruptedException, ExecutionException
{
executor.shutdown();
future.run();
System.out.println(future.get());
System.out.println(futureExecutor.get());
}
}

运行结果:yes no 。Callable表示的任务可以抛出受检查或未受检查的异常,并且任何代码都可能抛出一个Error.无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在Future.get中被重新抛出。

通过Future来实现取消

ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning。如果mayInterruptIfRunning为true并且任务当前正在某个线程运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没启动,就不要运行它”(取消还没有开始的任务),这种方式应该用于那些不处理中断的任务中。当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。

Executors

newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。(LinkedBlockingQueue)

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

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

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

SynchronousQueue是一个没有元素的阻塞队列,加上corePool = 0 ,maximumPoolSize = Integer.MAX_VALUE,这样就会存在一个问题,如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地创建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。

newSingleThreadExecutor:是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。能确保一组任务在队列中的顺序来串行执行。(LinkedBlockingQueue)

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

newWorkStealingPool:使用ForkJoin实现的线程池。

public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
newScheduledThreadPool:创建了一个固定长度的线程池,而且以延迟或者定时的方式来执行任务,类似于Timer。

ScheduledThreadPoolExecutor

public class ScheduledThreadPoolExecutor  extends ThreadPoolExecutor  implements ScheduledExecutorService

在ScheduledThreadPoolExecutor的构造函数中,我们发现它都是利用ThreadLocalExecutor来构造的,唯一变动的地方就在于它所使用的阻塞队列变成了DelayedWorkQueue,而不是ThreadPoolExecutor的LinkedBlockingQueue(通过Executors产生ThreadPoolExecutor对象)。DelayedWorkQueue为ScheduledThreadPoolExecutor中的内部类,它其实和阻塞队列DelayQueue有点儿类似。DelayQueue是可以提供延迟的阻塞队列,它只有在延迟期满时才能从中提取元素,其列头是延迟期满后保存时间最长的Delayed元素。如果延迟都还没有期满,则队列没有头部,并且poll 将返回null。DelayedWorkQueue中的任务必然是按照延迟时间从短到长来进行排序的。

ScheduledThreadPoolExecutor提供了如下四个方法,也就是四个调度器:

  1. schedule(Callable callable, long delay, TimeUnit unit) :创建并执行在给定延迟后启用的 ScheduledFuture。
  2. schedule(Runnable command, long delay, TimeUnit unit) :创建并执行在给定延迟后启用的一次性操作。
  3. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) :创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期。是以上一个任务开始的时间计时,period时间过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。
  4. scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) :创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。是以上一个任务结束时开始计时,period时间过去后,立即执行。

Timer的使用

JDK中的Timer类主要负责计划任务的功能,也就是在指定时间开始执行某一任务。Timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类(public abstract class TimerTask extends Object implements Runnable)。可以通过new Timer(true)设置为后台线程。有以下几个方法:

  • void schedule(TimerTask task, Date time):在指定的日期执行某一次任务。如果执行任务的时间早于当前时间则立刻执行。
  • void schedule(TimerTask task, Date firstTime, long period):在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。如果执行任务的时间早于当前时间则立刻执行。
  • void schedule(TimerTask task, long delay):以当前时间为参考时间,在此基础上延迟指定的毫秒数后执行一次TimerTask任务。
  • void schedule(TimerTask task, long delay, long period):以当前时间为参考时间,在此基础上延迟指定的毫秒数,再以某一间隔无限次数地执行某一任务。
  • void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):下次执行任务时间参考上次任务的结束时间,且具有“追赶性”。

TimerTask是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行时间也会被延迟。TimerTask类中的cancel方法的作用是将自身从任务队列中清除。Timer类中的cancel方法的作用是将任务队列中的全部任务清空,并且进程被销毁。

Timer的缺陷:Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor只支持相对时间的调度。Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不波或异常,因此当TimerTask抛出为检测的异常时将终止定时线程。JDK5或者更高的JDK中已经很少使用Timer.


欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×