Java线程池解惑

首先本文还是为实习生同学解惑用的。某天有个实习生表示对线程池不理解为何要用线程池代替每次产生线程的方式,简单说了几句后,想想干脆整理篇文章好了。

首先按照实习生的想法,来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(80);
while (true) {
final Socket socket = ss.accept();
Runnable task = ()-> {
handlRequest(socket);
};
new Thread(task).start();
}
}
}

这段代码为每个连接创建一个线程来处理请求,线程在run方法执行完后终止。在正常负载的情况下,为每个任务创建一个线程能提升串行执行的性能。
但是是否存在问题,当需要创建大量线程的时候:

  • 线程生命周期的开销非常高
  • 资源消耗,活跃线程会消耗系统资源尤其是内存,当线程多于可用处理器的数量,空闲线程会占用许多线程,所以如果CPU已经处于忙碌状态,再创建线程反而会降低性能。
  • 稳定性,在一个平台上创建的线程数是有限的,如果无限创建会突破这些限制则会抛出异常。

这个时候我们的线程池就应运而生了。
先官方定义下线程池:处理一组同构工作线程的资源池。
那什么叫同构工作呢,因为只有任务是相同类型,并且相互独立的时线程池的性能才能达到最佳,如果不同运行时长的任务放一起可能会造成拥塞,
如果互相之间有依赖的任务放一起则有可能造成死锁。
线程池与工作队列work queue(后面会讲到)密切相关,线程池中线程的任务是从工作队列中取出任务,执行任务,然后然会线程池。
现在参照上面那个例子的几个缺点,列出线程池到底有什么好处:

  • 通过重用线程,减少线程创建和销毁过程中产生的开销
  • 减少创建线程而造成的延时
  • 防止过多线程相互竞争资源

当然线程池的大小设置也是门技术活。
过大了线程会竞争cpu和内存资源,还有线程切换的开销
过小了浪费处理器,吞吐率变低。
那么线程池应该怎么设置,参考资料里第二条的沈剑老师的文章可以看看。
Java中为我们提供四种静态工厂方法来创建一个线程池

  • newFixedThreadPool
  • newCachedThreadPool
  • newScheduledThreadPool
  • newSingleThreadExecuPool

如果觉得上述的线程池不适合还可以自己配置ThreadPoolExecutor来定制一个线程池。
ThreadPoolExecutor的构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{

if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

其中参数一次为线程池基本大小,最大大小,线程存活时间,,工作队列,RejectedExecutionHandler 表示当执行被阻塞之后要做的操作。
线程存活时间的作用是:如果某个线程的空闲时间超过了存活时间,那么它将会被标记为可回收的,并且当线程池的当前大小超过基本大小后,将会被终止。
ThreadFactory,线程工厂,线程池创建线程都是通过这个方法进行的。

1
2
3
4
5
6
7
8
9
10
11
12
public interface ThreadFactory {

/**
* Constructs a new {@code Thread}. Implementations may also initialize
* priority, name, daemon status, {@code ThreadGroup}, etc.
*
* @param r a runnable to be executed by new thread instance
* @return constructed thread, or {@code null} if the request to

* create a thread is rejected
*/
Thread newThread(Runnable r);
}

但是线程池也并非是万能的,如果客户提交给服务器请求的速率超过了服务器的处理速率,仍然可能会资源耗尽。
ThreadPoolExecutor提供了工作队列(workqueue)来保存等待执行的任务,任务排列方法有

  • 无界队列
  • 有界队列
  • 同步移交

newFixedThreadPool和newSingleThreadPool默认使用一个无界的LinkedBlockingQueue。
无界队列如果任务处理一直跟不上任务到达的速度将会造成资源耗尽的情况,采用有界队列将会避免这一情况的发生。
如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU使用率,同事还可以减少上下文切换,但是付出的代价就是限制吞吐量。
如果任务之间存在依赖,那么使用有界队列将有可能出现死锁的的问题,这个时候才应该使用无界队列。

好了文章就讲到这里,为何需要线程池,如何使用线程池。

参考资料