4.2 JDK多任务执行框架
为了给并行程序开发提供更好的支持,Java不仅提供了Thread类,Runnable接口等简单的多线程支持工具,为了改善并发程序的性能,在JDK中还提供了用于多线程管理的线程池。本节将主要讨论线程池及其在Java中的实现与使用。
4.2.1无限制线程的缺陷
多线程的软件设计方法确实可以最大限度地发挥多核处理器的计算能力,提高生产系统的吞吐量和性能。但是,若不加控制和管理地随意使用线程,对系统的性能反而会产生不利的影响。
new Thread(new Runnable() {
@Override
public void run() {
// do sth;
}
}).start();
以上代码创建了一个线程,并在run方法结束后,自动回收该线程。在简单的应用系统中,没有太大问题。但是在真实的生存环境中,系统由于真实环境的需要,可以会开启很多线程来支撑应用。而当线程数量过大时,反而会耗尽CPU和内存资源。
首先,虽然与进程相比,线程是一种轻量级的工具,但其创建和关闭依然需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间,反而会得不偿失。
其次,线程本身也是要占用内存空间的,大量的线程会抢宝贵的内存资源,如果处理不当,可能会导致内存溢出。即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。
因此,对线程的使用必须掌握一个度,在有限的范围内,增加线程的数量可以明显提高系统的吞吐量,但是一旦超过了这个范围,大量的想吃只会拖垮应用系统。因此,在生产环境中使用线程,必须对其加以控制和管理。
4.2.2 简单的线程池实现
为了节省系统在多线程并发时不断创建和销毁线程所带来的额外开销,就需要引入线程池。线程池的基本功能就是进行线程的复用。当系统接受一个提交的任务,需要一个线程时,并不急着立即去创建线程,而是先去线程池查找是否有空闲的线程,若有,则直接使用线程池中的线程,若没有,再去创建新的线程。待任务结束后,也不简单的销毁线程,而是将线程放回线程池的空闲队列,等下下次使用。这样,在线程频繁调度的场合,可以节约不少系统开销(指创建和销毁线程的开销)。
下例中, 给出一个最简单的线程池的实现。
public class PThread extends Thread{
private ThreadPool pool;// 线程池
private Runnable target;// 任务
private boolean isShutDown = false;// 是否关闭
private boolean isIdle = false;// 是否空闲
public PThread(Runnable target, String name, ThreadPool pool) {
super(name);
this.pool = pool;
this.target = target;
}
@Override
public void run() {
// 只要没有关闭,则一直不结束该线程
while(!isShutDown){
isIdle = false;
if(null != target) {
target.run();
}
// 任务结束了,到闲置状态
isIdle = true;
try {
// 该任务结束后,不关闭线程,而是放入线程池空闲队列
pool.repool(this);
synchronized (this) {
wait();// 线程空闲,等待新的任务到来
}
} catch (InterruptedException e) {
}
isIdle = false;
}
}
public Runnable getTarget() {
return target;
}
public synchronized void setTarget(Runnable target) {
this.target = target;
// 设置了任务之后,通知run方法,开始执行这个任务
notifyAll();
}
public boolean isIdle() {
return isIdle;
}
// 关闭线程
public synchronized void shutDown() {
isShutDown = true;
notifyAll();
}
}
public class ThreadPool {
private static ThreadPool instance = null;
private List<PThread> idleThreads;// 空闲的线程队列
private int threadCounter;// 已有的线程总数
private boolean isShutDown = false;
public ThreadPool() {
this.idleThreads = new Vector<>(5);
threadCounter = 0;
}
public synchronized static ThreadPool getInstance() {
if(null == instance) {
instance = new ThreadPool();
}
return instance;
}
public int getThreadCounter() {
return threadCounter;
}
// 将线程放入池中
protected synchronized void repool(PThread repoolingThread) {
if(!isShutDown) {
idleThreads.add(repoolingThread);
}else{
repoolingThread.shutDown();
}
}
public synchronized void shutDown() {
isShutDown = true;
for(int threadIndex = 0; threadIndex < idleThreads.size(); threadIndex++) {
PThread thread = (PThread)idleThreads.get(threadIndex);
thread.shutDown();
}
}
// 执行任务
public synchronized void start(Runnable target) {
PThread thread = null;
// 如果有空闲线程,则直接使用
if(idleThreads.size()>0) {
int lastIndex = idleThreads.size()-1;
thread = idleThreads.get(lastIndex);
idleThreads.remove(lastIndex);
// 立即执行这个任务
thread.setTarget(target);
}else{
threadCounter++;
thread = new PThread(target, "PThread #" + threadCounter, this);
thread.start();
}
}
}
class MyThread implements Runnable{
protected String name;
public MyThread(String name) {
this.name = name;
}
public MyThread() {
}
@Override
public void run() {
try {
Thread.sleep(100);// 使用sleep方法代替一个具体功能的执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
long begin = System.currentTimeMillis();
new FrameTest().testNoPool();// 300+
// new FrameTest().testBYPool();// 200+
System.out.println("total time is " + (System.currentTimeMillis() - begin));
}
public void testNoPool(){
for (int i=0; i<5000; i++) {
new Thread(new MyThread("testNoPool")+Integer.toString(i)).start();
}
}
public void testByPool(){
for (int i=0; i<5000; i++) {
ThreadPool.getInstance().start(new MyThread("TestByPool"+Integer.toString(i)));
}
}
结果表明,未实现线程池的调度,明显比使用了线程池调度的要慢。需要注意的是,这里花费的时间并不是指所有任务执行完成的时间,而是仅包括完成任务调度所花费的时间。
4.2.3 Executor框架
为了更好地控制多线程,JDK提供了一套Executor框架,帮助开发人员有效进行线程控制。
以上成员均在java.util.concurrent包中,是JDK并发包的核心类。其中ThreadPool-Executor表示一个线程池,当然它的实现比稳重的ThreadPool要复杂许多。Executor类则扮演着线程池工厂的角色,通过Executor可以取得一个特定功能的线程池。
public void testByExecutor() {
// 得到一个可复用线程的线程池
ExecutorService exe = Executors.newCachedThreadPool();
for (int i=0; i<5000; i++) {
exe.execute(new MyThread(("testJDKTreadPool"+Integer.toString(i))));
}
}
4.2.4 自定义线程池
由于ScheduledExecutorService接口与本书的核心内容没有太大关联,故本书不做太多深入的讨论,仅对其它几个线程池进行说明。无论是newFixedThreadPool()方法,newSingleThreadPool还是newCacheThreadPool方法,其内部实现均使用了ThreadPoolExecutor,所以它们都是ThreadPoolExecutor类的封装。为何ThreadPoolExecutor有如此强大的功能呢,来看一下ThreadPoolExecutor最重要的构造函数:
/**
*
* @param corePoolSize 指定了线程池中的线程数量
* @param maximumPoolSize 指定了线程中的最大的线程数量
* @param keepAliveTime 当线程池线程数量超过了corePoolSIze时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程,在多长时间内会被销毁
* @param unit keepAliveTime的单位
* @param workQueue 任务队列,被提交尚未被执行的任务
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
4.2.5 优化线程池大小
线程池的大小对系统的性能有一定的影响。过大或者过小的线程数量都无法发挥最优的系统性能,但是线程对大小的确定也不需要做的非常精确,因为只要避免极大 和极小两种情况,线程池的大小对系统的性能并不会影响太大。一般来说,确定线程池的大小需要考虑CPU的数量,内存大小,JDBC的连接等因素。在<java并发编程实践>一书中给出了一个估算线程池大小的经验公式:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0Ucpu1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的线程池的大小等于
Nthreads = Ncpu * Ucpu * (1 + W/C)
在java中,可以通过
System.out.println(Runtime.getRuntime().availableProcessors());
获取CPU数量.
4.2.6 扩展ThreadPoolExecutor
ThreadPoolExecutor也是一个可以扩展的线程池,它提供了beforeExecutor(),afterExecutor()和terminated()3个接口对线程池进行控制。