背景
之前虽然在书里学过 Java 并发编程的基础,但一直没有在实践中用到过。直到上周,终于在项目看到了一个活生生的例子。
这个例子也非常基础,需求大概是这样的:
- 我们基于 Lucene 做站内搜索引擎,每天都需要重新构建一次索引,因为每天都会产生新内容(之所以彻底重新构建一遍,是因为内容量也没有那么大,与其在原有索引后面追加新索引,不如重新构建,因为这样查询起来更快);
- 在构建索引时,需要请求 CMS 数据库,拿到全部内容的相关数据,例如标题、关键字等等,基于这些字段构建索引;
- 由于请求 CMS 数据库涉及网络 I/O,所以程序大量时间都在等待网络请求结果。这种情况下,如果能利用多个线程并发操作(线程数 > CPU 核数),就应该能获得较大的性能提升;
实现起来是这样:
- 使用固定的线程数 20(我也不知道是怎么得到这个数字的,有空可以研究下);
- 把需要索引的内容均分成 20 份,每个线程负责为自己的那一份内容写入索引;
具体到 Java 层面:
- 使用
Executors.newFixedThreadPool(20)
创建一个线程数为 20 的固定大小线程池; - 声明一个 worker class 来 implement
Runnable
,在其run()
方法下写每个线程要执行的逻辑; - 创建 20 个 worker 实例,都使用
executor.execute()
来执行;
下面简单来看一看。
Runnable
首先来看一看 Runnable
这个东西。这大概是 Java 里使用线程最基础、最直白的方式了吧!
在我们这个例子里,每个线程之间是相互独立的,而且执行的操作逻辑是一样的,只不过参数不同(每个线程都是请求内容、写入索引,只不过每个线程所负责的内容不同,比如线程 1 负责 0-99 号内容,线程 2 负责 100-199 号,等等)。在这种情况下,使用 Runnable
就可以轻松达到目的:
@AllArgsConstructor
class MyWorker implements Runnable {
private int someParam;
private int someOtherParam;
// ...其他参数
@Override
public void run() {
// 这里可以基于 someParam 和 someOtherParam 等参数,写每个线程执行的逻辑
}
Executors.newFixedThreadPool
那么该如何执行这些线程呢?
这个例子中的需求也比较简单,只需要固定的线程数:
// 创建一个大小固定为 20 的线程池
ExecutorService executor = Executors.newFixedThreadPool(20);
// 依次启动 20 个线程
for (int i = 0; i < 20; i++) {
try{
// 创建 worker,这里可以加很多参数,作为示例只写了两个
Runnable worker = new MyWorker(i, someOtherParam);
// 启动!
executor.execute(worker);
} catch (Exception e) {
log.error("worker failed", e);
}
}
// 关闭 executor,不再接受新任务(不过上面 20 个任务此时一定还在执行)
executor.shutdown();
其实平时用到 Executors.newFixedThreadPool()
的概率应该不大,因为它有个缺陷:如果线程池没有空闲线程,而新任务又源源不断被创建,那么这些任务会被放在一个等待队列中,但这个等待队列没有数量上限,可能会越来越大,直到内存不够用。所以正常来说是应该设定等待队列上限的。
不过这里就没有这个问题,因为任务并不是动态创建的,而是固定只有 20 个。在这种情况下使用 Executors.newFixedThreadPool()
就非常合理了!
小结
这次遇到的是一个非常简单的 Java 并发编程例子,作为入门很合适。之后有机会再学习更高级的技巧吧!
参考链接
“implements Runnable” vs “extends Thread” in Java
Reason for calling shutdown() on ExecutorService
Executor Thread Pool - limit queue size and dequeue oldest