java多线程已经写了好几篇了,博客中使用所有demo,线程都是通过Thread
类的start()
方法启动的,这样的话每次都会创建一个新的线程,创建线程时会耗费一些操作系统的资源,当线程比较多时对性能有较大的影响。为了解决这个问题,java提供了好几种类型的线程池来提高运行时效率。
ExecutorService接口
绝大部分(如果不是全部的话)的线程池都实现了ExecutorService
接口,ExecutorService
又实现了Executor
接口,总之,ExecutorService
接口包含以下常用的方法
void execute(Runnable command);
上面这个方法会启动一个线程。第一,它有可能会创建一个新的线程来执行,也有可能复用线程池中已有的线程来执行。第二,该方法返回时并不保证该线程马上回被执行,有可能会排队。
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
上面这三个方法和上面的execute()
方法类似,只是上面这三个方法返回一个Future
对象,它的get()
方法会挂起调用线程,直到Runable
线程或Callable
线程运行结束。下一篇博客会重点讨论Future
和Callable
。
上面第一个方法,如果返回的Future
对象的get()
方法为null
,则表示线程执行成功。
上面第二个方法,如果返回的Future
对象的get()
方法得到的是result
对象,则表示线程执行成功。
上面第三个方法,当线程成功执行后,通过返回的Future
对象的get()
方法可以获取Callable
线程的运行结果。
void shutdown();
该方法关闭线程池。如果没有调用它的话,进程将不会结束。
除了ExecutorService接口之外,java还提供了ScheduledExecutorService接口,该接口继承自ExecutorService,并加入了几个版本的schedule方法,例如
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
它可以延迟启动一个线程,其他方面和ExecutorService
的submit
方法一样。
多种类型的线程池
java提供了好几种类型的线程池,通过java.util.concurrent.Executors
类的静态方法来创建。例如:
CachedThreadPool
Executors.newCachedThreadPool()
上面的方法创建了一个具有缓存功能的线程池。当它需要启动一个线程时,如果当前线程池内有可用的线程,则会复用,否则,会创建新的线程。闲置时间超过1分钟的线程将会被自动销毁并移除当前线程池。
FixedThreadPool
Executors.newFixedThreadPool(int nThreads);
上面的方法创建了一个具有固定数量(nThreads个)线程的线程池,在任何时间,最多有nThreads个线程可以同时运行。当新的任务被提交到线程池时,如果有闲置的线程,就立刻复用该线程来运行该任务,如果没有闲置的线程,则该任务会在队列中等候闲置的线程来运行自己。顾名思义,FixedThreadPool
拥有固定数量线程,不会动态创建新线程,也不会销毁闲置的线程。
SingleThreadExecutor
Executors.newSingleThreadExecutor()
上面的方法创建了一个只包括一个工作者线程的线程池,类似Executors.newFixedThreadPool(1)
。
ScheduledExecutor
Executors.newScheduledThreadPool(int corePoolSize)
上面的方法具有corePoolSize
个常驻线程的线程池,它可以用来延迟启动进程,或周期性地运行线程。
效率对比
为了说明线程池的性能优势,下面来一个例子对比一下。有一个工作者线程,计算从1到10000000的和,并且一次启动100000个工作者线程。分别使用三种方式启动线程
- Thread.start()
- CachedThreadPool
- FixedThreadPool
先来看工作者线程
import java.util.concurrent.CountDownLatch;
public class MyWorker implements Runnable {
String name;
CountDownLatch countDownLatch;
public MyWorker(String name, CountDownLatch countDownLatch) {
this.name = name;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
long sum = 0;
for (int i = 1; i <= 10000000; i++) {
sum += i;
}
countDownLatch.countDown();
}
}
下面以直接启动线程的方式来一次启动100000个工作者线程
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadStartDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
final int threadCount = 100000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
long from = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
String threadName = "t" + i;
Runnable runnable = new MyWorker(threadName, countDownLatch);
new Thread(runnable).start();
}
countDownLatch.await();
long to = System.currentTimeMillis();
long period = to - from;
System.out.println("use " + period + " milliseconds");
service.shutdown();
}
}
运行上面的代码运行完平均需要8000毫秒。
下面使用CachedThreadPool来启动线程
mport java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
final int threadCount = 100000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
long from = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
String threadName = "t" + i;
service.execute(new MyWorker(threadName, countDownLatch));
}
countDownLatch.await();
long to = System.currentTimeMillis();
long period = to - from;
System.out.println("use " + period + " milliseconds");
service.shutdown();
}
}
上面的代码运行完大概只需要800毫秒,正好差一个数量级。下面再来看一下通过FixedThreadPool启动线程的例子
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(4);
final int threadCount = 100000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
long from = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
String threadName = "t" + i;
service.execute(new MyWorker(threadName, countDownLatch));
}
countDownLatch.await();
long to = System.currentTimeMillis();
long period = to - from;
System.out.println("use " + period + " milliseconds");
service.shutdown();
}
}
我通过Executors.newFixedThreadPool(4)
创建了一个包含4个线程的线程池,用这种方式运行的话,平均只需要170毫秒。
由此可见:
1. 线程池的引入,大大减少了直接创建线程带来的性能开销
2. 求和属于CPU密集型操作,这种情况并不是创建的线程越多越好,因为线程太多的话(超过了CPU的核心数),线程间会频繁的切换,也会消耗一定的性能。Executors.newFixedThreadPool(4)
的情况要明显好于Executors.newCachedThreadPool()
的情况。我运行上面代码的电脑是四核的处理器,所以创建一个包含四个线程的线程池,性能是最佳的。
3. 如果工作者线程包含大量的IO操作的话,例如读写磁盘文件、读写数据库、请求网络接口等,newCachedThreadPool
的效率有可能会优于newFixedThreadPool(4)
,因为做IO操作时,CPU会闲置。这种情况下,更多的线程更有利于压榨CPU。