本文是个人的学习笔记,主要参考以下资料:
Java核心技术 卷一,Cay S.Horstmann著,林琪、苏钰涵等译,机械工业出版社出版
马士兵教育
构造一个线程的代价巨大,因为涉及与操作系统的交互。所以,如果一个程序中有大量生命期很短的线程,那势必会影响都系统的运行。
Java中每个Runnable我们都可以看成是一个任务,我们可以在线程执行完一个Runnable的run方法之后不去销毁这个线程,而是让这个线程继续去执行其他Runnable的run方法。这样就可以用少量线程执行多个任务。
1、代表线程池的类:ExecutorService
ExecutorService
可以看成是代表线程池的类,其他各种类型的线程池都是它的子类。
线程池有各种类型,比如只有一个线程的线程池,固定大小的线程池和可扩充大小的线程池等等。Java中构建这些线程池都可以通过Executors
的静态方法获取。
1.1、ExecutorService的常用方法
1.1.1、常规方法,提交任务,关闭线程池
- 向线程池提交任务
我们有三个方法向线程池提交任务
Future<T> submit(Callable<T> task);
Future<T> submit(Runnable task);
Future<T> submit(Runnable task, T result);
void execute(Runnable command);
上面的三个方法都会返回Future的对象。返回值即可一定程度代表参数中的任务,我们可以通过Future来了解任务的在线程池中的一些状态,或者一定程度的控制任务的执行。 其中,前三个方法最后都是调用 execute方法来执行线程,这个方法来源是Executor。
- 关闭线程,关闭线程池
1.
shutdown()
:关闭线程池。停止接收任务,但是会执行完已提交的任务。等所有任务都执行完再关闭线程池。
2.shutdownNow
:立刻强制关闭线程池。
1.1.2、定时线程池的方法(Scheduled)
这类方法都定义在ScheduledExecutorService
中,方法比较少,主要是在时间上控制任务的执行。比如提交任务后延迟一段时间再执行任务等。
1.
ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit)
:提交一个任务,但是即使有空闲线程,任务也不会立刻占有线程,而是会等待time
个unit
时间单位后才开始占用线程。我做过实验,创建一个ScheduledExecutorService
线程池,使用该方法延迟提交一个任务,然后立刻使用普通方法提交一个任务,系统会先执行第二个任务。等第二个任务执行完,且第一个任务定时已过,那第一个任务会开始执行。
2.ScheduledFuture<V> schedule(Runnable<V> task, Long time, TimeUnit unit)
:同上一个一样。
3.ScheduledFuture<V> scheduleAtFixedRate(Runnable<V> task, Long time, Long period, TimeUnit unit)
:提交一个任务,该任务在time
个unit
时间单位后开始尝试占用线程。之后会每隔period
个unit
时间单位会执行一次任务,执行次数理论无上限。该间隔是指任务从获得线程的那一刻开始,到下一次获得线程的时刻,与任务执行时间无关。任务提交后,尽管线程池会不断地执行这个任务,但这个任务并没有一直占用着一个线程,若任务执行结束且处于间隔期间,那一定是没有占用线程的。
4.scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit)
:与上一个类似,不同点是,上一个方法的间隔是上一次任务开始执行时间到下一次任务开始的时间。该方法的间隔是上一次任务执行完成时间到下一次任务的开始时间。
1.1.3、任务组批量执行
ExecutorService中还定义了一些批量执行任务组的方法。
1.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
:一次性执行集合中的所有任务,返回代表这些任务的Future列表,Future的顺序与参数集合中的顺序相同。
2.<T> T invokeAny(Collection<? extends Callable<T>> tasks)
:一次性执行集合中所有任务,但是只返回其中一个任务的返回值,一般来说是第一个结束的任务的返回值。
1.2、生成线程池的方法
有两个方法可以创建线程池。
- 一是通过
ExcecutorService
的实现类的构造方法创建线程池。不过一般要输入多个参数比较麻烦。
比如ThreadPoolExecutor
实现了ExecutorServer
,它又有多个构造方法,基本每个都需要五六个参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
- 另外就是通过
Executors
静态方法生成的线程池。基本不需要参数就能创建出我们需要的线程池。
比如新建一个单线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
下面是Executors
可以创建的线程池。
1.2.1、Executors提供的线程池
方法 | 描述 |
---|---|
newCachedThreadPool | 优先使用已创建的线程执行任务,如果线程不够,再创建线程,可同时存在无限多的线程数。当一个线程的空闲时间超过60s时,这个线程会被销毁。该线程池可大量提高数量多,短周期的任务场景。 |
newFixedThreadPool | 创建一个固定大小的线程池。如果某一时间任务过多,没有足够的线程,那这些任务会被放到一个队列中等待,直到获取到空闲线程去执行任务。这些线程创建以后不会被销毁,除非线程池关闭或者发生未知错误。如果线程是因为后者被销毁,并且这一时间也有任务需要线程去执行,那会有一个新的线程被创建出来去执行任务。 |
newWorkStealingPool | 这个比较特殊,其他的线程池都是基于ThreadPoolExecutor ,而这个是基于ForkJoinPool 。之后会详细介绍两者的区别。 |
newSingleThreadExecutor | 单线程池,通常是用来测试性能。 |
newScheduledThreadPool | 该线程池提供了一些延时和定时的线程。可以实现任务第一次提交到线程池后,延时执行,并且每隔一段时间再执行一次。原理是基于DelayQueue 来完成延迟执行,周期执行是在完成之后重新扔回阻塞队列。 |
newSingleThreadScheduledExecutor | 和newScheduledThreadPool 一样,但是是单线程池。 |
1.3、ThreadPoolExecutor和ForkJoinPool的区别
ThreadPoolExecutor
只有一个阻塞队列,所有的线程都从这个队列中获取任务。
ForkJoinPool
可以让线程的使用率更高,但是从写法上也比较麻烦。
ForkJoinPool
里提交的任务必须是ForkJoinTask
,它继承Future
,但是提出了更多的要求。
ForkJoinTask
可以将任务拆分成更小的任务,存放在一个队列中。
所以ForkJoinPool
里每个线程都有一个自己的阻塞队列。线程接到一个任务后,因为任务都是ForkJoinTask
,所以这个任务就被分成了更多的小任务。线程就会先处理其中的一个,然后将剩下的放到属于这个线程的阻塞队列中。
关键点来了,有线程空闲时它是可以去处理其它线程的阻塞队列中还没来得及处理的任务。这就是ForkJoinPoin
中线程利用率更高的原因。
比如现在有三个线程,来了一个大任务,大任务可以被拆成三个小任务。
线程1接到这个大任务,开始处理其中的part1。part2和part3放到线程1的阻塞队列中。
因为线程2和3是空闲的,于是他们就去线程1的阻塞队列中拿part2和part3执行。这样线程的利用率就更高了。
缺点就是写法比较复杂,我们在覆写ForkJoinTask
时需要自己写如何对大任务拆分,拆分成多少份,这让逻辑变得更复杂了。