线程池

一、线程池简介

线程池可以看做是线程的集合。当请求到来,线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁),实现了线程的重用。

没有线程池 ——> 为每个请求都新开一个线程 缺点:
- 线程生命周期的开销非常高(创建和销毁都是需要时间和资源的)
- 程序的稳定性和健壮性会下降。

线程池的优点:

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性:线程池允许我们开启多个任务而不用为每个线程设置属性值(便于管理);线程池根据当前在系统中运行的进程来优化线程时间片(调优);线程池可以限制创建线程数量,提高系统稳定性。
  • 线程池工作原理:

    如果核心线程池的线程执行完当前任务,那么立刻从阻塞队列头取任务来执行。
    如果队列为空,且当前线程超过了存活时间(keepAliveTime),那么判断当前线程数是否大于核心线程池的最大数?
    如果是,那么销毁当前线程,如果不是,则保留当前线程。
    因此当创建的线程数大于核心线程池的最大数,在所有任务执行完毕后,线程池最终线程数量会回到corePoolSize。

二、jdk提供的线程池API:Executor框架:将任务提交与任务执行分离开的机制(解耦)

  • Executor接口:基础 execute(Runnable) : void
  • ExecutorService接口:继承Executor接口,提供了线程池生命周期管理的方法。
  • AbstractExecutorService方法:实现ExecutorService,提供默认实现
  • ScheduledExecutorService接口:继承ExecutorService接口。
  • ThreadPoolExecutor方法:最常用的线程池。
  • ScheduledThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService:延后与定期执行。
    还涉及到其他的类如下:
  • java线程池有必要吗 java线程池的优缺点_任务队列

三、Executor接口:

就一个void execute(Runnable command)方法,代表提交一个任务

我们经常这样启动一个线程

new Thread(new Runnable(){
    //do something
}).start()

用了线程池Executor就可以这样使用

Executor executor = anExecutor;
executor.execute(new RunnableTask1());

如果我们希望线程池同步执行每一个任务可以这样实现这个接口

class DirectExecutor implements Executor{
    public void executor(Runnable r){
        r.run();//没有new Thread().start(),也就没有启动任何一个线程,也就是同步的
    }
}

如果希望每个任务提交后,直接启动一个新的线程来执行这个任务,可以这样实现

class ThreadPerTaskExecutor implements Executor{
    public void execute(Runnable r){
        new Thread(r).start();//每个任务一个线程
    }
}

组合两个 Executor 来使用,下面这个实现是将所有的任务都加到一个 queue 中,然后从 queue 中取任务,交给真正的执行器执行,这里采用 synchronized 进行并发控制:

class SerialExecutor implements Executor {
    // 任务队列
    final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
    // 这个才是真正的执行器
    final Executor executor;
    // 当前正在执行的任务
    Runnable active;
 
    // 初始化的时候,指定执行器
    SerialExecutor(Executor executor) {
      this.executor = executor;
    }
 
    // 添加任务到线程池: 将任务添加到任务队列,scheduleNext 触发执行器去任务队列取任务
  public synchronized void execute(final Runnable r) {
        tasks.offer(new Runnable() {
            public void run() {
              try {
                    r.run();
              } finally {
                    scheduleNext();
              }
            }
        });
        if (active == null) {
            scheduleNext();
        }
    }
 
    protected synchronized void scheduleNext() {
      if ((active = tasks.poll()) != null) {
            // 具体的执行转给真正的执行器 executor
          executor.execute(active);
        }
    }
}

Executor接口只有提价任务的功能,接下来ExecutorService提供了比较丰富的功能。

四、ExecutorService接口:

一般定义一个线程池,往往使用这个接口,提供了比较丰富的功能。

ExecutorService executor = Executors.newFixedThreadPool();

方法:

public interface ExecutorService extends Executor {
 
    // 关闭线程池,已提交的任务继续执行,不接受继续提交新任务
    void shutdown();
 
    // 关闭线程池,尝试停止正在执行的所有任务,不接受继续提交新任务
    // 它和前面的方法相比,加了一个单词“now”,区别在于它会去停止当前正在进行的任务
    List<Runnable> shutdownNow();
 
    // 线程池是否已关闭
    boolean isShutdown();
 
    // 如果调用了 shutdown() 或 shutdownNow() 方法后,所有任务结束了,那么返回true
  // 这个方法必须在调用shutdown或shutdownNow方法之后调用才会返回true
    boolean isTerminated();

    // 等待所有任务完成,并设置超时时间
  // 我们这么理解,实际应用中是,先调用 shutdown 或 shutdownNow,
    // 然后再调这个方法等待所有的线程真正地完成,返回值意味着有没有超时
    boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException;
 
    // 提交一个 Callable 任务
    <T> Future<T> submit(Callable<T> task);
 
  // 提交一个 Runnable 任务,第二个参数将会放到 Future 中,作为返回值,
    // 因为 Runnable 的 run 方法本身并不返回任何东西
  <T> Future<T> submit(Runnable task, T result);
 
    // 提交一个 Runnable 任务
    Future<?> submit(Runnable task);

    // 执行所有任务,返回 Future 类型的一个 list
  <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;
 
  // 同上一个方法,只有其中的一个任务结束了,就可以返回,返回执行完的那个任务的结果,
    // 不过这个带超时,超过指定的时间,抛出 TimeoutException 异常
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

在继续往下层介绍 ExecutorService 的实现类之前,我们先来说说相关的类 FutureTask。

FutureTask

Future --> RunnableFuture --> FutureTask

Runnable --> RunnableFuture

FutureTask通过Runnable间接实现了Runnable接口,

所以每个Runnable通常都先包装成FutureTask,

然后调用executor.execute(Runnable command)将其提交给线程池

我们知道Runnable的run方法是没有返回值的,如果需要会在submit中指定第二个参数作为返回值:

Future submit(Runnable task, T result);

//需要获取结果用submit,不需要可以用execute方法

到时候会通过这两个参数将其包装成Callable,Callable的call方法有返回值,同时如果运行出现异常,call方法会抛出异常

public interface Callable{

V call() throws Exception;

}

五、AbstractExecutorService

抽象类派生自ExecutorService接口,实现了invokeAny方法和invokeAll方法

六、ThreadPoolExecutor详解

这个类实现了一个线程池需要的各个方法,它实现了任务提交、线程管理、监控等等方法。

先看一下任务提交的几个方法:

public Future<?> submit(Runnable task) {
    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) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

提交任务的submit方法中,参数是Runnable/Callable类型,这个参数不是用于new Thread(runnable).start()中的,此处的参数不是用于启动线程的,这里指的是任务,任务要做的事是run方法里面定义的或call方法定义的。

线程池中一些主要的构件:

java线程池有必要吗 java线程池的优缺点_Java_02

ps:我们经常会使用Executors这个工具类来快速构造线程池,最终重点也是构造方法

public ThreadPoolExecutor(…)

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
          maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
          throw new IllegalArgumentException();
        // 这几个参数都是必须要有的
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
 
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

构造方法

构造方法可以让我们自定义(扩展)线程池

1. 指定核心线程数量 corePoolSize
2. 指定最大线程数量 maximumPoolSize
3. 允许线程空闲时间 keepAliveTime:空闲线程的保活时间,如果某线程的空闲时间超过这个值没有任务给他去做,他就可以被关闭了。但是如果线程池中的线程数少于核心线程数,那么这些线程不会因为空闲太长时间而被关闭。
4. 时间对象 unit
5. 阻塞队列 workQueue
6. 线程工厂 threadFactory:用于生成线程
7. 任务拒绝策略 handle

除了构造方法中的主要属性,还存在其他重要的属性:

  • 内部状态:
    变量ctl定义为AtomicInteger,**记录了线程池中的 任务数量 和 线程池的状态 **两个信息。(其中高三位表示线程池状态、低29位表示线程池中的任务数量)
  • 线程池状态runState:
  • RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理
  • SHUTDOWN:不可以接受新任务,但是可以对已添加的任务进行处理
  • STOP:不接受新任务,不处理已添加任务,并且会中断正在处理的任务
  • TIDYING:所有任务已终止,会变成TIDYING状态,线程池会执行钩子函数terminated(),terminated()在ThreadPoolExecutor是空的,若用户想在线程池变为TIDYING时进行处理,可以通过重载钩子函数terminated()来实现。
  • TERMINATED:线程池彻底终止的状态,terminated方法结束后,状态就会变成这个。
  • 状态转换过程
  • RUNNING -> SHUTDOWN:当调用了 shutdown() 后,会发生这个状态转换,这也是最重要的
  • (RUNNING or SHUTDOWN) -> STOP:当调用 shutdownNow() 后,会发生这个状态转换,这下要清楚 shutDown() 和 shutDownNow() 的区别了
  • SHUTDOWN -> TIDYING:当任务队列和线程池都清空后,会由 SHUTDOWN 转换为 TIDYING
  • STOP -> TIDYING:当任务队列清空后,发生这个转换
  • TIDYING -> TERMINATED:这个前面说了,当 terminated() 方法结束后

另外,还需要看一下内部类Worker

ps:Worker是一个内部类,就是线程池中做任务的线程。任务Runnable(内部叫task或者command),线程Worker

Worker类使用了AQS

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    private static final long serialVersionUID = 6138294804551838833L;
 
    // 这个是真正的线程,任务靠你啦
    final Thread thread;
 
    // 前面说了,这里的 Runnable 是任务。为什么叫 firstTask?因为在创建线程的时候,如果同时指定了
    // 这个线程起来以后需要执行的第一个任务,那么第一个任务就是存放在这里的(线程可不止执行这一个任务)
    // 当然了,也可以为 null,这样线程起来了,自己到任务队列(BlockingQueue)中取任务(getTask 方法)就行了
    Runnable firstTask;
 
    // 用于存放此线程完全的任务数,注意了,这里用了 volatile,保证可见性
    volatile long completedTasks;
 
    // Worker 只有这一个构造方法,传入 firstTask,也可以传 null
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // 调用 ThreadFactory 来创建一个新的线程
        this.thread = getThreadFactory().newThread(this);
    }
 
    // 这里调用了外部类的 runWorker 方法
    public void run() {
        runWorker(this);
    }
 
    ...// 其他几个方法没什么好看的,就是用 AQS 操作,来获取这个线程的执行权,用了独占锁
}

接下来看最重要的方法:

execute执行方法

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        // 如果线程池中运行的线程数量 < corePoolSize,
    	// 则创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务	(firstTask),即使其他辅助线程是空闲的。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        // 到这里说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败了
    	
    	// 如果线程池处于 RUNNING 状态,把这个任务添加到任务队列 workQueue 中
        if (isRunning(c) && workQueue.offer(command)) {
            /* 这里面说的是,如果任务进入了 workQueue,我们是否需要开启新的线程
             * 因为线程数在 [0, corePoolSize) 是无条件开启新的线程
             * 如果线程数已经大于等于 corePoolSize,那么将任务添加到队列中,然后进到这里
             */
            int recheck = ctl.get();
            // 如果线程池已不处于 RUNNING 状态,
            // 那么移除已经入队的这个任务,并且执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 如果线程池还是 RUNNING 的,并且线程数为 0,那么开启新的线程
        	// 到这里这块代码的真正意图是:担心任务提交到队列中了,但是线程都关闭了
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 如果 workQueue 队列满了,那么进入到这个分支
        // 以 maximumPoolSize 为界创建新的 worker,
        // 如果失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

再总结一遍这些参数的要点:

线程数量要点:

  • 如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
  • 如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
  • 如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池
  • 如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量

线程空闲时间要点:

  • 当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁。

排队策略要点:

  • 同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
  • 无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数
  • 有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量
    当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了:

拒绝任务策略:

final void reject(Runnable command) {
    // 执行拒绝策略
    handler.rejectedExecution(command, this);
}

此处的handler需要在构造线程池的时候就传入,他是RejectExecutionHandler的实例,其中由四个类可以直接用。

// 只要线程池没有被关闭,那么由提交任务的线程自己来执行这个任务。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public CallerRunsPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}
 
// 不管怎样,直接抛出 RejectedExecutionException 异常
// 这个是默认的策略,如果我们构造线程池的时候不传相应的 handler 的话,那就会指定使用这个
public static class AbortPolicy implements RejectedExecutionHandler {
    public AbortPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}
 
// 不做任何处理,直接忽略掉这个任务
public static class DiscardPolicy implements RejectedExecutionHandler {
    public DiscardPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}
 
// 这个相对霸道一点,如果线程池没有被关闭的话,
// 把队列队头的任务(也就是等待了最长时间的)直接扔掉,然后提交这个任务到等待队列中
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public DiscardOldestPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}
  • 直接抛出异常
  • 使用调用者的线程来处理
  • 直接丢掉这个任务
  • 丢掉最老的任务
  • 关闭线程池的相关方法
    tryTerminate()
    当线程池涉及到要移除worker时候都会调用tryTerminate(),该方法主要用于判断线程池中的线程是否已经全部移除了,如果是的话则关闭线程池。
    在关闭线程池的过程中,如果线程池处于STOP状态或者处于SHUDOWN状态且阻塞队列为null,则线程池会调用interruptIdleWorkers()方法中断所有线程,注意ONLY_ONE== true,表示仅中断一个线程。
    interruptIdleWorkers
    onlyOne==true仅终止一个线程,否则终止所有线程。
  • 线程池关闭
  • 线程池ThreadPoolExecutor提供了shutdown()和shutDownNow()用于关闭线程池。
    shutdown():按过去执行已提交任务的顺序发起一个有序的关闭,但是不接受新任务。
    shutdownNow() :尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。

七、Executors

Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务需求。其中所有的方法都是static的。

  1. FixedThreadPool
    创建固定长度的线程池,每次提交任务创建一个线程,直到达到线程池的最大数量,线程池的大小不再变化。这个线程池可以创建固定线程数的线程池。特点就是可以重用固定数量线程的线程池。它的构造源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) { 
        return new ThreadPoolExecutor(nThreads, nThreads, 0L,
                                      TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue<Runnable>()); 
}
  • FixedThreadPool的corePoolSize和maxiumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。
  • 0L则表示当线程池中的线程数量操作核心线程的数量时,多余的线程将被立即停止
  • 最后一个参数表示FixedThreadPool使用了无界队列LinkedBlockingQueue作为线程池的工作队列,由于是无界的,当线程池的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池的线程数量不会超过corePoolSize,同时maxiumPoolSize也就变成了一个无效的参数,并且运行中的线程池并不会拒绝任务。
  • FixedThreadPool运行图如下
  • 执行过程如下:
  1. 如果当前工作中的线程数量少于corePool的数量,就创建新的线程来执行任务。
  2. 当线程池的工作中的线程数量达到了corePool,则将任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务后会从队列中去任务。

注意LinkedBlockingQueue是无界队列,所以可以一直添加新任务到线程池。

  1. SingleThreadExecutor
    SingleThreadExecutor是使用单个worker线程的Executor。特点是使用单个工作线程执行任务。它的构造源码如下:
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

SingleThreadExecutor的corePoolSize和maxiumPoolSize都被设置1。

其他参数均与FixedThreadPool相同,其运行图如下:

java线程池有必要吗 java线程池的优缺点_并发_03

执行过程如下:

  1. 如果当前工作中的线程数量少于corePool的数量,就创建一个新的线程来执行任务。
  2. 当线程池的工作中的线程数量达到了corePool,则将任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务后会从队列中去任务。

注意:由于在线程池中只有一个工作线程,所以任务可以按照添加顺序执行。

  1. CachedThreadPool
    CachedThreadPool是一个”无限“容量的线程池,它会根据需要创建新线程。特点是可以根据需要来创建新的线程执行任务,没有特定的corePool。下面是它的构造方法:

CachedThreadPool的corePoolSize被设置为0,即corePool为空;

maximumPoolSize被设置为Integer.MAX_VALUE,即maximum是无界的。

这里keepAliveTime设置为60秒,意味着空闲的线程最多可以等待任务60秒,否则将被回收。

CachedThreadPool使用没有容量的SynchronousQueue作为主线程池的工作队列,它是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作。这意味着,如果主线程提交任务的速度高于线程池中处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源。其运行图如下:

java线程池有必要吗 java线程池的优缺点_线程池_04

执行过程如下:

  1. 首先执行SynchronousQueue.offer(Runnable task)。如果在当前的线程池中有空闲的线程正在执行SynchronousQueue.poll(),那么主线程执行的offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行。,execute()方法执行成功,否则执行步骤2
  2. 当线程池为空(初始maximumPool为空)或没有空闲线程时,配对失败,将没有线程执行SynchronousQueue.poll操作。这种情况下,线程池会创建一个新的线程执行任务。
  3. 在创建完新的线程以后,将会执行poll操作。当步骤2的线程执行完成后,将等待60秒,如果此时主线程提交了一个新任务,那么这个空闲线程将执行新任务,否则被回收。因此长时间不提交任务的CachedThreadPool不会占用系统资源。

SynchronousQueue是一个不存储元素阻塞队列,每次要进行offer操作时必须等待poll操作,否则不能继续添加元素。

具体的使用实例

  1. . newCachedThreadPool :
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
    	Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
    	e.printStackTrace();
	}
  	//线程池的执行方法
	cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println(index);
        }
    });
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

  1. . newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
        }
    });
}

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。

定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。

  1. newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
    	System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);

表示延迟3秒进行

定期执行代码如下

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
    	System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);
表示延迟1秒后每3秒执行一次。
  1. newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

结果依次输出,相当于顺序执行各个任务。

现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

总结

  1. java 线程池有哪些关键属性?

corePoolSize,maximumPoolSize,workQueue,keepAliveTime,rejectedExecutionHandler

  1. corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))。
    workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。
    keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作
    rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常。
  2. 说说线程池中的线程创建时机?
  • 如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务;
  • 如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务;
  • 如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。
  1. 任务执行过程中发生异常怎么处理?
    如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。
  2. 什么时候会执行拒绝策略?
  • workers 的数量达到了 corePoolSize(任务此时需要进入任务队列),任务入队成功,与此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。
  • workers 的数量大于等于 corePoolSize,将任务加入到任务队列,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略。