线程池 —— 使用介绍

引入线程池的原因

通常我们需要使用线程去完成某项任务的时候都会去创建一个线程,一般都会这么写:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // TODO
    }
});
thread.start();

这样操作直接且简单,当然是没有错的,但是却存在这一些问题。在应付一些线程并发不多的情况时是完全够用的,但是如果并发的线程数量很多,就会造成系统的效率降低。主要会造成如下影响:

  • 频繁创建和销毁线程占用大量不必要的系统处理时间,影响性能。
  • 频繁创建和销毁线程容易导致 GC 的频繁执行,造成内存抖动,导致移动设备出现卡顿。
  • 大量的线程并发非常消耗内存,容易造成 OOM 问题。
  • 不利于扩展,如定时执行、周期执行、线程中断。

而解决上面问题的方法,就是引入线程池的概念。线程池使得线程可以重复利用,执行完任务后并不会销毁线程,而是继续执行其他的任务。这样可以有效的减少并控制创建线程的数量,防止并发线程过多,内存消耗过度,从而提高系统的性能。

同时线程池还可以很方便的控制线程的并发数量,线程的定时任务,单线程顺序执行等等。

ExecutorService 接口

ExecutorService 就是一般所说的线程池接口,它继承 Executor 接口,同时还提供了一些管理终止的方法,以及可以跟踪一个或者多个异步任务线程执行状况并生成 Future 的方法。

而真正意义上的线程池是 ThreadPoolExecutor,它实现了 ExecutorService 接口,并且封装了一系列接口使其具有线程池的特性。

线程池:ThreadPoolExecutor

查看源码后我们发现 ThreadPoolExecutor 有四个构造方法,都是调用其中一个构造方法进行初始化操作。具体代码如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) { ... }
参数 作用
corePoolSize 线程池中的核心线程数量
maximumPoolSize 线程池中的最大线程数量
keepAliveTime 超过核心线程数量的多余空闲线程,在超过 keepAliveTime 时间内没有任务,则被销毁
unit keepAliveTime 的时间单位
workQueue 任务队列,用来存储已经提交但没有被执行的任务,不同的线程池采取的排队策略各不相同
threadFactory 线程工厂,用来创建线程池中的线程
handler 当最大线程数和任务队列已经饱和,而导致无法接受新的任务时,用来拒绝接受任务的策略

五种不同功能的线程池

可以看出想要创建一个 ThreadPoolExecutor 对象并不容易,所以一般推荐使用工厂类 Executors 的工厂方法来创建线程池对象。Executors 主要提供了下面五种不同功能的线程池:

1. 固定型线程池 newFixedThreadPool

创建一个固定线程数量的线程池。每次提交一个任务就创建一个线程,直到达到设定的线程数量,之后线程池中的线程数量不再变化。当有新任务提交时,如果有空闲的线程,则由该空闲线程处理任务,否则将任务存至任务队列中,一旦有线程空闲下来,则按照 FIFO 的方式处理队列中的任务。该线程池适合一些稳定的正规并发线程,多用于服务器。

定义:

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

运行实例:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

运行结果:

02-24 08:15:20 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:15:20 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 08:15:20 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 08:15:23 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:15:23 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:15:23 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 08:15:26 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 08:15:26 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:15:26 I/System.out: Thread: pool-1-thread-3, running Task9
02-24 08:15:29 I/System.out: Thread: pool-1-thread-1, running Task10

观察线程名发现一共只创建了 3 个线程处理任务,当所有线程都处于运行状态时,再提交的任务则会进入等待,3 个线程处理完之前任务后会被等待队列中的任务复用,所以观察时间发现,每次都是 3 个任务同时运行,间隔 3 秒后再运行后面 3 个任务,任务执行的顺序即提交的顺序。

2. 缓存型线程池 newCachedThreadPool

创建一个可以根据实际情况调整线程数量的线程池。线程池中的线程数量不确定,当需要时会创建新的线程,当有线程空闲时复用空闲线程。通常对执行大量短暂异步任务的程序,可以提升其效率。

当然,线程池中的线程并不会越来越多,每个线程都有一个参数用来设置保持活动的时间,一旦线程空闲时间超过了该时间,则立即销毁该线程,默认的保持活动时间为 60 秒,我们可以在其定义中看到这个默认参数。

定义:

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

运行实例:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    final int index = i;
    cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(index * 500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

运行结果:

02-24 08:25:29 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:25:30 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 08:25:31 I/System.out: Thread: pool-1-thread-2, running Task3
02-24 08:25:32 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:25:33 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:25:34 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 08:25:35 I/System.out: Thread: pool-1-thread-3, running Task7
02-24 08:25:36 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:25:37 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 08:25:38 I/System.out: Thread: pool-1-thread-4, running Task10

我们让任务间隔 1 秒提交一次,并且每个任务的时间逐渐增长,从结果可以发现,每隔 1 秒就会有一个任务被执行,刚开始一个线程可以处理过来,但随着任务时间的增长,线程池创建了新的线程来处理那些提交的任务,当有之前的线程处理完后,也会被赋予任务执行。最后一共创建了 4 个线程。

3. 单例线程 newSingleThreadExecutor

创建一个只含有一个线程的线程池。每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,当这个线程空闲时,再按照 FIFO 的方式顺序执行队列中的任务。

定义:

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

运行实例:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

运行结果:

02-24 09:00:18 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:00:19 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 09:00:20 I/System.out: Thread: pool-1-thread-1, running Task3
02-24 09:00:21 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 09:00:22 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:00:23 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 09:00:24 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:00:25 I/System.out: Thread: pool-1-thread-1, running Task8
02-24 09:00:26 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 09:00:27 I/System.out: Thread: pool-1-thread-1, running Task10

结果显而易见,从始至终只有一个线程在执行,该线程顺序执行已提交的线程。

4. 调度型线程池 newScheduledThreadPool

创建一个可指定大小的,可以调度线程根据 schedule 延迟执行,或者周期执行的线程池。

运行实例:

System.out.println("Start Task");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 1; i <= 10; i++) {
    final int index = i;
    scheduledThreadPool.schedule(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, 3, TimeUnit.SECONDS);
}

运行结果:

02-24 09:47:45 I/System.out: Start Task
02-24 09:47:48 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:47:48 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 09:47:48 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 09:47:50 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:47:50 I/System.out: Thread: pool-1-thread-2, running Task4
02-24 09:47:50 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 09:47:52 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:47:52 I/System.out: Thread: pool-1-thread-3, running Task8
02-24 09:47:52 I/System.out: Thread: pool-1-thread-2, running Task9
02-24 09:47:54 I/System.out: Thread: pool-1-thread-1, running Task10

运行情况基本和 newFixedThreadPool 类似,但是在开始运行时,有 3 秒钟的延迟。

5. 调度型单例线程 newSingleThreadScheduledExecutor

创建一个只含有一个线程,可以调度线程根据 schedule 延迟执行,或者周期执行的线程池。

运行实例:

System.out.println("Start Task");
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("Thread: " + threadName + ", running Task1");
    }
}, 1, 2, TimeUnit.SECONDS);

运行结果:

02-24 10:01:36 I/System.out: Start Task
02-24 10:01:37 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:39 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:41 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:43 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:45 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:47 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:49 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:51 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:53 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:55 I/System.out: Thread: pool-1-thread-1, running Task

运行情况基本和 newSingleThreadExecutor 类似,但是在开始运行时,有 1 秒钟的延迟,并且每隔 2 秒周期性的执行一次任务。

自定义线程池

如果仔细观察上面五种线程池的定义就会有所发现,其实线程池的功能不同,和其内部的 BlockingQueue 不同有关。如果我们想要实现一些不同功能的自定义线程池,可以从其中的 BlockingQueue 着手。

比如现在有一种 BlockingQueue 的实现类是 PriorityBlockingQueue,他可以实现队列按照优先级排序,那我们就可以利用它实现一个按任务的优先级来处理任务的线程池。

首先创建一个实现了 Runnable 与 Comparable 接口的抽象类 PriorityRunnable,用来存放任务。实现了 Comparable 接口就可以进行优先级的比较。

public abstract class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
    private int mPriority;

    public PriorityRunnable(int priority) {
        if (priority < 0 ) {
            throw new IllegalArgumentException();
        }
        mPriority = priority;
    }

    @Override
    public int compareTo(PriorityRunnable runnable) {
        int otherPriority = runnable.getPriority();
        if (mPriority < otherPriority) {
            return 1;
        } else if (mPriority > otherPriority) {
            return -1;
        } else {
            return 0;
        }
    }

    public int getPriority() {
        return mPriority;
    }
}

接着就可以创建一个基于 PriorityBlockingQueue 实现的线程池,其核心数量定义为 3,方便测试结果。然后创建 10 个优先级依次增加的 PriorityRunnable 任务,提交给线程池执行。

ExecutorService threadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
for (int i = 1; i <= 10; i++) {
    final int priority = i;
    threadPool.execute(new PriorityRunnable(priority) {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println("Thread: " + name + ", running PriorityTask" + priority);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

运行结果:

02-25 11:34:45 I/System.out: Thread: pool-1-thread-1, running PriorityTask1
02-25 11:34:45 I/System.out: Thread: pool-1-thread-2, running PriorityTask2
02-25 11:34:45 I/System.out: Thread: pool-1-thread-3, running PriorityTask3
02-25 11:34:47 I/System.out: Thread: pool-1-thread-1, running PriorityTask10
02-25 11:34:47 I/System.out: Thread: pool-1-thread-2, running PriorityTask9
02-25 11:34:47 I/System.out: Thread: pool-1-thread-3, running PriorityTask8
02-25 11:34:49 I/System.out: Thread: pool-1-thread-1, running PriorityTask7
02-25 11:34:49 I/System.out: Thread: pool-1-thread-2, running PriorityTask6
02-25 11:34:49 I/System.out: Thread: pool-1-thread-3, running PriorityTask5
02-25 11:34:51 I/System.out: Thread: pool-1-thread-1, running PriorityTask4

可以看到,运行结果和 newFixedThreadPool 非常相似,唯一不同就在于等待任务不是按照 FIFO 的方式执行,而是优先级较高的任务先执行。

线程池的优化

线程池可以自定义内部线程的数量,定义的数量影响着系统的性能表现,如果定义过大会浪费资源,定义过小线程并发量太低,处理速度也变低了,所以合理的设置线程数量是很重要的。通常需要考虑 CPU 的核心数、内存的大小、并发请求的数量等因素。

通常核心线程数量可以设为 CPU 核心数 + 1,最大线程数量可以设为 CPU 核心数 * 2 + 1。

获取 CPU 核心数的方法为:

Runtime.getRuntime().availableProcessors();

线程池的调用方式

以下方法都可以调用线程池执行,但是效果不同,可以参考文档或源码了解:

  • void execute(Runnable command);
  • Future<?> submit(Runnable task);
  • <T> Future<T> submit(Runnable task, T result);
  • <T> Future<T> submit(Callable<T> task);
  • <T> T invokeAny(Collection<? extends Callable<T>> tasks);
  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

线程池的关闭

void shutdown();

该方法不再接受新的任务提交,在终止线程池之前,允许执行完以前提交的任务。

List<Runnable> shutdownNow();

该方法不再接受新的任务提交,并且把任务队列中的任务直接移除,尝试停止正在执行的任务。返回等待的任务列表。

posted @ 2017-02-25 21:15  银色子弹  阅读(448)  评论(0编辑  收藏  举报