这是我参与更文挑战的第2天,活动详情查看: 更文挑战html
最近在实验室作相关工做时,一个小伙伴看见项目代码中出现了 new Thread(...)
,破口大骂之。看见这一场景,我默默地删掉了我在另外一个地方写的 new Thread(...)
看成无事发生(还好他没看见XD)。java
为了避免再犯这种错误,我写下这篇文章来记录一下Java线程究竟该怎么使用(才不会被骂),也是开了一个新坑!git
若有错误欢迎联系我指正!api
首先从我秉持的原则入手,“简洁优雅”。试想若是在一段代码中你须要建立不少线程,那么你就不停地调用 new Thread(...).start()
么?显然这样的代码一点也不简洁,也不优雅。初次以外这样的代码还有不少坏处:缓存
从这些坏处很容易能够看出解决方法,那就是弄一个监管者来统一的管理这些线程,并将它们存到一个集合(或者相似的数据结构)中,并且还要动态地分配它们的任务。固然Java已经给咱们提供好十分健全的东西来使用了,那就是线程池!markdown
Java提供了一个工厂类来构造咱们须要的线程池,这个工厂类就是 Executors 。这个类提供了不少方法,咱们这里主要讲它提供的4个建立线程池的方法,即数据结构
newCachedThreadPool()
newFixedThreadPool(int nThreads)
newScheduledThreadPool(int corePoolSize)
newSingleThreadExecutor()
这个方法正如它的名字同样,建立缓存线程池。缓存的意思就是这个线程池会根据须要建立新的线程,在有新任务的时候会优先使用先前建立出的线程。也就是说线程一旦建立了就一直在这个池子里面了,执行完任务后后续还有任务须要会重用这个线程,如果线程不够用了再去新建线程。多线程
以一段代码作个例子:并发
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 建立缓存线程池
for (int i = 0; i < 10; i++) {
final int index = i;
// 每次发布任务前等待一段时间,如1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行任务
cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}
复制代码
在这个例子里,我在每次调用线程执行任务以前都等待1秒,这使时间让线程池内的线程执行完上一个任务绰绰有余,因此你会发现输出里都是同一个线程在执行任务。oracle
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 建立缓存线程池
for (int i = 0; i < 10; i++) {
final int index = i;
// 每次发布任务前根据奇偶不一样等待一段时间,如1s
if (i % 2 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行任务
cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}
复制代码
这个例子中我在每次调用线程执行任务以前根据奇偶不一样控制其是否等待,这样就会在同一时间须要执行2个任务,因此线程池中按须要多建立了一个线程。你也能够把这个模数改大到三、四、5...来观察线程池是否按需建立了新线程。
注意这里的线程池是无限大的,咱们并无规定他的大小。(但其实在实际使用时不多是无限大的,我会在这个系列后面的文章再来探讨这个问题)
能够看到这个方法中带了一个参数,这个方法建立的线程池是定长的,这个参数就是线程池的大小。也就是说,在同一时间执行的线程数量只能是 nThreads 这么多,这个线程池能够有效的控制最大并发数从而防止占用过多资源。超出的线程会放在线程池的一个队列里等待其余线程执行完,这个队列也是值得咱们去好好研究的,它是一个无界队列,我会在这个系列后面的文章探讨它。
以一段代码作个例子:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); // 建立缓存线程池,大小为3
for (int i = 0; i < 10; i++) {
final int index = i;
// 执行任务
fixedThreadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + index);
// 模拟执行任务耗时1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
复制代码
这个例子里能够看到我建立了一个大小为3的线程池,也就是说它支持的最大并发线程数是3,运行后发现这些数确实是3个3个为一组输出的。
合理的设置定长线程池的大小是一个很重要的事情。
从 Scheduled 大概能够猜出这个线程池是为了解决上面说过的第3个坏处,也就是缺少定时执行功能。这个线程池也是定长的,参数 corePoolSize 就是线程池的大小,即在空闲状态下要保留在池中的线程数量。
而要实现调度须要使用这个线程池的 schedule()
方法 (注意这里要把新建线程池的返回类 ExecutorService 改为 ScheduledExecutorService 噢)
以一段代码作个例子:
// 注意!这里把 ExecutorService 改为了 ScheduledExecutorService ,不然没有定时功能
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); // 建立缓存线程池
// 执行任务
scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + ": 我会在3秒后执行。"),
3, TimeUnit.SECONDS);
复制代码
这个例子会在3秒后输出结果。固然你能够根据不一样的需求设置不一样的定时,甚至还能实现按期执行功能,详细能够查看[官方api]
这个线程池就比较简单了,他是一个单线程池,只使用一个线程来执行任务。可是它与 newFixedThreadPool(1, threadFactory)
不一样,它会保证建立的这个线程池不会被从新配置为使用其余的线程,也就是说这个线程池里的线程始终如一。
以一段代码作个例子:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 建立单线程池
for (int i = 0; i < 10; i++) {
final int index = i;
// 执行任务
singleThreadExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + index);
// 模拟执行任务耗时1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
复制代码
能够看到输出里他只会一秒一秒地打印内容,只有一个线程在执行任务。
若是你运行了我上面的示例,你会发现程序一直都没有结束,这是由于我上面的示例代码并无关闭线程池。线程池自己提供了两个关闭的方法:
shutdown()
: 将线程池状态置成 SHUTDOWN
,此时再也不接受新的任务,等待线程池中已有任务执行完成后结束;shutdownNow()
: 将线程池状态置成 SHUTDOWN
,将线程池中全部线程中断(调用线程的 interrupt()
操做),清空队列,并返回正在等待执行的任务列表。而且它还提供了查看线程池是否关闭和是否终止的方法,分别为 isShutdown()
和 isTerminated()
。
那么根据须要使用以上四种线程池就足够应对平时的需求了,别再使用 new Thread(...)
这种方法啦!
固然,线程池只能隐式的控制线程变量,若是有业务需求须要对线程进行定制化的监控控制,那也请绝不吝啬的使用new Thread(...)
。