-
第一部分:概述
早期的应用程序大多是单线程串行执行的,虽然程序的任务边界清晰有序,但是执行的效率却很低,尤其是执行花费时间较长的操作,会导致大量的等待和堆积。为了提高程序的执行效率和吞吐量,我们很自然的会想到多线程,即为每个任务都新建一个独立的线程,这样就极大地提高了程序的执行效率。但事实上多线程也会带来很多问题。比如大量的创建线程,这本身就会消耗很多的资源,尤其是内存,当创建的线程数量超过服务器能够承受的极限时,内存溢出是在所难免的;比如还有其他稳定性问题,以及多线程造成的程序调用和管理的混乱。所以,综上所述,我们需要一个介于两者之间的工具,既可以创建大量的线程来提高程序的并发性和吞吐量,同时又可以有序的管理这些线程,能够可控。而Executor接口和线程池技术就是在这种背景下应运而生的。下面分别将这两种常用的并发技术做以介绍和总结。
-
第二部分:Executor和ExecutorService接口介绍
java.util.coucurrent包下面为我们提供了丰富的并发工具,Executor和ExecutorService接口就是其中比较重要的两个。
-
Executor接口介绍
public interface Executor{
void execute(Runnable command);
}
以上是Executor接口的代码,可以看出这个借口非常简单,就只有一个execute()方法。但他却为强大的异步任务执行提供了基础,它支持不同类型的任务执行策略。Executor框架基于生产者消费者模型,它提供了一个标准的方法将任务的提交和任务的执行过程解耦,并用Runnable来表示任务。下面来看一个基于Executor的简单服务器实现:
public class ExecutorWebServer {
private static final int LIMIT = 50;
private static final Executor exe = Executors.newFixedThreadPool(LIMIT);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
ServerSocket server = new ServerSocket(80);
while(true){
Socket socket = server.accept();
Runnable task = new Runnable(){
public void run(){
doSomeThing(socket);
}
};
exe.execute(task);
}
}
}
通过上面列子我们就很好的把任务的提交和任务的执行分开来,这就是Executor框架最大的优势。
-
ExecutorService接口介绍
上面我们讲了如何创建一个Executor,但是并没有讲如何关闭它。既然Executor是为应用程序服务的,因而他们应该是可关闭的,并将关闭操作中受影响的任务状态反馈给应用程序。为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口,增加了管理生命周期的方法:
public interface ExecutorService Extends Executor{
void shutdown();
boolean isShutDown();
boolean isTerminated();
……
}
ExecutorService的生命周期分为三种,运行,关闭和已终止。下面看这个简单的实例:
class NetworkService implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(int port, int poolSize)
throws IOException {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
}
public void run() { // run the service
try {
for (;;) {
pool.execute(new Handler(serverSocket.accept()));
}
} catch (IOException ex) {
pool.shutdown();
}
}
}
class Handler implements Runnable {
private final Socket socket;
Handler(Socket socket) { this.socket = socket; }
public void run() {
// read and service request on socket
}
}
-
第三部分:线程池介绍及使用
-
几种类型的线程池
可以通过Executors类的几个静态方法来创建线程池。包含以下几类线程池:
newFixedThreadPool创建固定长度的线程池。每提交一个任务时就新建一个线程,直到达到线程池的最大数量。
newCachedThreadPool创建一个可缓存的线程池,线程的数量不受限制,但是在以前线程可用时将重用他们。
newSingleThreadExecutor创建一个使用单个 worker 线程的 ,以无界队列方式来运行该线程。
newScheduleThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。类似于一个定时器。
-
设置线程池大小
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中最好不要固定线程池的大小,而要通过某种灵活机制来配置。线程池大小的配置只要避免“过大”和”过小“两种极端情况即可。线程池过大会导致大量的线程竞争CPU和内存,最终导致资源耗尽;线程池过小时又会导致资源浪费,所以设置一个合适的线程池大小非常重要。通常有一个简单的设置线程池大小的公式供我们参考使用:
N(Threads) = N(cpu) * U(cpu) * (1 + w/c)
N(cpu)代表cpu的个数,U(cpu)代表cpu利用率, w/c表示等待时间与计算时间的比值
-
配置线程池
ThreadPoolExecutor为Executor和ExecutorService接口提供了基本实现。下面我们来看一下ThreadPoolExecutor类的构造函数:该类一共有四个构造函数,其中最基础的一个构造函数如下:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
corePoolSize表示的是核心池的大小,第二个参数表示线程池最大的线程数,第三个参数表示存活时间,第四个参数表示给定单元粒度的时间段,第五个参数表示的是工作队列。
-
扩展线程池
ThreadPoolExecutor是可以扩展的,它提供了几个可以在子类中改写的方法:beforeExecutor,afterExecutor,terminated,这些方法可用于扩展ThreadPoolExecutor的行为。