任务执行
大多数并发应用程序都围绕“任务执行(Task Execution)”来构造的:任务通常是一些抽象切离散的单元。
串行的执行任务:在应用程序中可以通过多种策略来执行一个任务,通常串行任务是最简单的。
public static void httpSocket() throws IOException {
ServerSocket serverSocket=new ServerSocket(8080);
while (true){
Socket connection=serverSocket.accept();
perseRequest(connection);
}
}
httpSocket方法将监听8080端口,这种处理方式是完全正确的,但是在实际的应用中很糟糕,他每次只能处理一个请求。假如服务器完成这次操作需要大量的IO操作,这些操作往往会产生阻塞,那么程序将无法处理其他用户的请求。同时CPU的利用率很低。
显示的为任务创建线程
通过为每个请求创建一个线程来处理一个服务,从而实现更高的响应性。
public static void httpSocket() throws IOException {
ServerSocket serverSocket=new ServerSocket(8080);
while (true){
final Socket connection=serverSocket.accept();
perseRequest(connection);
new Thread(new Runnable() {
public void run() {
perseRequest(connection);
}
}).start();
}
}
(实际中可以使用NIO,AIO,此处只是为了了解线程池,对网络编程不深究)与前面的单线程版本类似,主线程仍然不断的接收外部请求,分发请求等操作。我们暂且称分发的线程为work线程,每次请求都会在work线程来完成用户的工作,而不是在主线程的循环中去处理。由此可以得出三个结论:
- 任务处理过程在主线程中分离出来使得主线程能够重新等待下一个连接的到来。
- 任务可以并行的处理,即使因为IO原因引发阻塞,此时阻塞的是work线程。
- 在这种情况下,任务的处理代码必须是安全的。
无限制创建线程的不足
在生产环境中,每个任务分配一个线程,这种方法存在着一些缺陷,尤其是当需要创建大量线程时。
线程的生命周期开销非常高。线程的创建与销毁并不是没有代价的,每个线程都会有独立的线程ID,栈,栈指针,程序计数器,条件吗和通用目的的寄存器值。java上的线程是操作系统线程的映射,所以这些都需要JVM与操作系统的交互。
资源消耗。假如是cpu密集型的任务,对于四核处理器来说,创建100个线程会有96个线程让费,占用大量的空间。同时每个线程不停的争夺cpu时间片段,系统会不停的切换上下文,会有性能的损耗。
稳定性。可创建的线程的数量存在一个限制,jvm对线程总数的限制,操作系统对线程总数的限制。
线程池 Thread Pool
想到Thread Pool不得不说Work Queue ,其中 Work Queue中保存了等待执行的任务,工作者线程(Worker Thread)的任务很简单:从Work Queue中获取一个任务,执行任务。
在java中提供了一种线程池的实现,Executor接口。
ThreadPoolExecutor为一些Executor提供了基本的实现,他是一个灵活的稳定的线程池,允许进行各种定制。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
......
}
这是线程池的基本构造参数,只有对这些参数理解,才能构造出适合自己的线程池。
corePoolSize:线程池的基本大小,基本大小是线程池的目标大小(注意在创建ThreadPoolExecutor的初期,没有任务时线程不会启动,除非调用preStartAllCoreThreads方法,只有任务队列满了的情况下,才回超出这个数量的线程)
MaximumPoolSize:表示同时活动线程数的上限。
keepAliveTime:线程的存活时间,当线程一段时间没有任务时,并且线程数量大于基本大小时,会进行线程的销毁。
管理任务的队列
在有限的线程池中会限制可并发执行的任务数量,就像前面所讲的,如果无限制的创建线程会导致程序不稳定,可以通过固定大小的线程池来解决这个问题。当然这个方案在高并发下依然会耗尽资源,当请求速度超过服务器的处理速度时,新来的请求将堆积起来。在线程池中,这些请求会在Executor管理的Runnable队列中等待,而不会像线程那样去竞争cpu的资源。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种,有界队列,无界队列,和同步移交(Synchronous Handoff),队列的选择与其他参数有关。
一种妥善的资源管理策略是使用有界队列,比如ArrayBlockingQueue 有界的LinkBlockingQueue,PriorityBlockingQueue等。有界队列有助于避免资源耗尽的情况发生。但是带来的问题是我们必须关注使用的饱和策略,即当队列满时应该怎么处理新的任务。
对于非常大或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者(会在另一篇文章中详细讲述)。
队列不只在我们自己的程序中存在,对越linux系统来说也有自己的TCP队列,当用户请求过多时是由内到外蔓延的,最终拒绝连接。
饱和策略:
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改,这里需要注意的是,当一个任务提交到一个已经被关闭的Executor时,也会用到饱和策略。JDK提供了几种不通的RejectedExecutionHandler实现,每种实现包含有不同的饱和策略:AbortPolicy,CallerRunsPllicy,DiscardPolicy,DiscardOldestPolicy。
Abort 是默认饱和策略,会抛出RejectedExecution-Exception异常。调用者可以捕获这个异常,根据需求编写自己的处理代码。
Discard 会悄悄抛弃该任务。Discard-Oldest会抛弃马上就要执行的任务。
Caller-Runs:由调用者直接运行该任务。
定制自己的线程池:
往往我们需要让线程的名称更有意义,便于在日志中查看,比如大家查看日志时长可以看到 http-nio-7000-exec-12 ,看到这个名字时很容易知道这个线程是tomcat(假如你用的tomcat)里的Woker线程池里的线程打印的(第12个创建的线程)。同时有些场景需要记录时间,线程池提供方法获取活跃的线程数,自定义处理异常等,自定义工作队列等。这些都需要定义自己的线程池。接下来定义自己的线城池。
首先自定义线程基类,自定义异常处理器,自定义线程名(自定义的异常处理器用excute方法提交线程才有效,用submit方法提交的线程,异常会封装在Future对象中返还给调用者)。
class MyAppThread extends Thread{
private static final String DEFAULT_NAME="TomDog";
private static volatile boolean debugLifeCycle = true;
private static final AtomicInteger created=new AtomicInteger();
private static final AtomicInteger alive=new AtomicInteger();
private static final Logger log=Logger.getAnonymousLogger();
public MyAppThread(Runnable runnable){
this(runnable,DEFAULT_NAME);
}
public MyAppThread(Runnable runnable,String name){
super(runnable,name+"-"+created.incrementAndGet());
setUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler(){
public void uncaughtException(Thread t, Throwable e) {
System.out.println("UNCAUGHT in thread "+t.getName());
}
}
);
}
@Override
public void run(){
boolean debug=debugLifeCycle;
if (debug){
System.out.println("Created "+ getName());
}
try {
alive.decrementAndGet();
super.run();
}finally {
alive.decrementAndGet();
if (debug){
System.out.println("Exiting "+getName());
}
}
}
public static int getCreated() {
return created.get();
}
public static int getAlive() {
return alive.get();
}
public static void setDebug(boolean debugLifeCycle) {
MyAppThread.debugLifeCycle = debugLifeCycle;
}
}
有了线程基类接下来创建自己的线程工场,线程工场顾名思义就是生产线程的工场。
class MyThreadFactory implements ThreadFactory{
private final String threadName;
MyThreadFactory(String threadName){
this.threadName=threadName;
}
public Thread newThread(Runnable r) {
return new MyAppThread(r,threadName);
}
}
接下来创建自定义线程池,来实现记录任务的执行时间,可以自定义重写ThreadPoolExecutor的beforeExecute,afterExecute, terminated方法来实现记录每次任务的执行时间,任务的平均执行时间。
class MyTimingThreadPool extends ThreadPoolExecutor{
private final ThreadLocal<Long> startTime
=new ThreadLocal<Long>();
private final AtomicLong numTasks=new AtomicLong();
private final AtomicLong totalTime=new AtomicLong();
public MyTimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
protected void beforeExecute(Thread thread,Runnable runnable){
super.beforeExecute(thread,runnable);
System.out.println(String.format("Thread %s:start %s",thread,runnable));
startTime.set(System.nanoTime());
}
@Override
protected void afterExecute(Runnable runnable,Throwable throwable){
try {
long endTime=System.nanoTime();
long taskTime=endTime-startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
System.out.println(String.format(" Thread %s :end %s, time=%dns",throwable,runnable,taskTime));
}finally {
super.afterExecute(runnable,throwable);
}
}
@Override
protected void terminated(){
try {
System.out.println(String.format("Terminated : avg time=%dns",totalTime.get()/numTasks.get()));
}finally {
super.terminated();
}
}
}
根据ThreadPoolExecutor的构造参数可知,我们还需要为其指定任务队列,饱和策略,核心线程数,最大线程数,线程活跃时间,这些根据自己的业务场景来配置就可以了。
对于cpu密集型的任务,可以指定线程的个数为cpu的核数+1 .在java中可以通过RunTime.getRunTime().availableProcessors()来获取。
接下来new一个自定义的线程池:
ThreadPoolExecutor executor=new MyTimingThreadPool(CPU_COUNT,maxPoolSize,keepAliveTime,timeUnit,blockingQueue,
threadFactory ,rejectedExecutionHandler);
此时完成了一个定制的线程池。
我们实际开发的系统中,tomcat有几个线程池,dubbo有几个线程池,kafka client中有几个线程池,zookeeper client中存在着几个线程池.......完成一个请求数据是怎么在多个线程之间传递的,这些都需要我们对感兴趣的去了解清除,才能更明确的了解线程池的应用场景。
---------------我是分割线,接下来是废话---------------
好久没写博客了,如果有错误欢迎指正。