《深入拆解Tomcat&Jetty》总结五:EndPoint+Processor+Executor

补充:

https://jiges.github.io/2018/02/07/%E4%BA%94%E5%A4%A7IO%E6%A8%A1%E5%9E%8B

7 EndPoint组件+Processor+Executor

I/O就是计算机内存与外部设备之间拷贝数据的过程。
CPU访问内存的速度远远高于外部设备,因此CPU是先把外部设备的数据读到内存里,然后再进行处理。
当程序通过CPU向外部设备发出一个读指令时,数据从外部设备拷贝到内存往往需要一段时间,这个时候CPU没事干了,程序是主动把CPU让给别人?还是让CPU不停地查:数据到了吗、数据到了吗…?

这就是I/O模型要解决的问题。

7.1 IO模型的区别

对于一个网络I/O通信过程,比如网络数据读取,会涉及两个对象,一个是调用这个I/O操作的用户线程,另外一个就是操作系统内核。

一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。

当用户线程发起I/O操作后,网络数据读取操作会经历两个步骤:

用户线程等待内核将数据从网卡拷贝到内核空间。
内核将数据从内核空间拷贝到用户空间。

各种I/O模型的区别就是:它们实现这两个步骤的方式是不一样的。

同步阻塞IO:
用户线程发起read调用后就阻塞了,让出CPU
内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒
在这里插入图片描述
同步非阻塞IO:
用户线程不断的发起read调用,数据没到内核空间时,每次都返回失败;
直到数据到了内核空间,这一次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒
在这里插入图片描述
IO多路复用:JavaNIO就是这种模型结合非阻塞io模型,即调用read后不阻塞,直接返回,但是不是反复调用read去查询,而是交给select去查询,此read对应的Channel数据准备就绪后会返回给select可读条件,select去触发后,再进行read:https://jiges.github.io/2018/02/07/%E4%BA%94%E5%A4%A7IO%E6%A8%A1%E5%9E%8B的图很直观/
用户线程的读取操作分成两步了,线程先发起select调用,目的是问内核数据准备好了没,等内核把数据准备好了,用户线程再发起read调用。
在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。
那为什么叫I/O多路复用呢?因为一次select调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用
在这里插入图片描述
异步IO:用户线程发起read调用的同时注册一个回调函数,read立即返回,等内核将数据准备好后,再调
用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。对应Java的AIO
在这里插入图片描述
阻塞或非阻塞是指应用程序在发起I/O操作时,是立即返回还是等待。

而同步和异步,是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起拷贝还是由应用程序来触发内核的拷贝。(应用程序不是直接从内核拷贝数据,因为应用程序不能访问内核空间)

7.2 NioEndPoint:Tomcat如何实现非阻塞IO?

NioEndPoint组件实现了I/O多路复用模型

7.2.1 总体工作流程

对于Java的多路复用器的使用分为两步:
1.创建一个Seletor,在它身上注册各种感兴趣的事件,然后调用select方法,等待感兴趣的事情发生。
2.感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从Channel中读数据。

这也是NioEndPoint的基本原理,它一共包含LimitLatch、Acceptor、Poller、SocketProcessor和Executor共5个组件,其工作过程如图所示:
在这里插入图片描述
LimitLatch是连接控制器,它负责控制最大连接数,NIO模式下默认是10000,达到这个阈值后,连接请求被拒绝。

public class LimitLatch {
    //AQS内部维护一个状态和一个线程队列,可以用来控制线程什么时候挂起,什么时候唤醒
    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected int tryAcquireShared() {
            long newCount = count.incrementAndGet();
            if (newCount > limit) {
                count.decrementAndGet();
                return -1;
            } else {
            	return 1;
            }
        }
        @Override
        protected boolean tryReleaseShared(int arg) {
            count.decrementAndGet();
            return true;
        }
    }
    private final Sync sync;
    private final AtomicLong count;
    private volatile long limit;
    //线程调⽤这个⽅法来获得接收新连接的许可,如果暂时无法获取,这个线程会被阻塞到AQS的队列中
    //会调用tryAcquireShared
    public void countUpOrAwait() throws InterruptedException {
    	sync.acquireSharedInterruptibly(1);
    }
    //调⽤这个⽅法来释放⼀个连接许可,那么前⾯阻塞的线程可能被唤醒
    //会调用tryReleaseShared
    public long countDown() {
        sync.releaseShared(0);
        long result = getCount();
        return result;
    }
}

Acceptor跑在一个单独的线程里(实现了Runnable),是Endpoint的内部类,因为是Endpoint的实现细节,外部组件无需知道;
它在一个死循环里调用accept方法来接收新连接(ServerSocketChannel),一旦有新的连接请求到来,accept方法返回一个SocketChannel对象,然后将SocketChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入Poller的Queue里,这是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过Queue通信。

ServerSocketChannel初始化代码:

serverSock = ServerSocketChannel.open();
//当应用层面的连接数到达最大值时,操作系统可以继续接收连接,那么操作系统能继续接收的最大连接数就是这个队列长度,可以通过acceptCount参数配置,默认是100。
serverSock.socket().bind(addr,getAcceptCount());
serverSock.configureBlocking(true);//阻塞模式接收连接,因为是单独的线程,所以不用非阻塞

Poller的本质是一个Selector,也跑在单独线程里。Poller在内部维护一个Channel数组,它在一个死循环里通过内部的Selector对象不断向内核查询Channel的数据就绪状态,一旦有Channel可读,就生成一个SocketProcessor任务对象扔给Executor去处理。
Poller的另一个重要任务是循环遍历检查自己所管理的SocketChannel是否已经超时,如果有超时就关闭这个SocketChannel。

维护了一个Queue:

private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();

SynchronizedQueue的方法比如offer、poll、size和clear方法,都使用了Synchronized关键字进行修饰,用
来保证同一时刻只有一个Acceptor线程对Queue进行读写。

同时有多个Poller线程在运行,每个Poller线程都有自己的Queue。

每个Poller线程可能同时被多个Acceptor线程调用来注册PollerEvent。

Poller的个数可以通过pollers参数配置。

SocketProcessor实现了Runnable接口,用来定义Executor中线程所执行的任务,主要就是调Http11Processor组件来处理请求。

Http11Processor读取Channel的数据来生成ServletRequest对象,但并不是直接读取Channel的。
因为Tomcat支持同步非阻塞I/O模型和异步I/O模型,在Java API中,相应的Channel类也是不一样的,比如有AsynchronousSocketChannel和SocketChannel,为了对Http11Processor屏蔽这些差异,Tomcat设计了一个包装类叫作SocketWrapper,Http11Processor只调用SocketWrapper的方法去读写数据。

Executor就是线程池,负责运行SocketProcessor的run,SocketProcessor的run方法会调用Http11Processor来读取和解析请求数据。

Http11Processor是应用层协议的封装,它会调用容器获得响应,即最终会调用到Servlet,最后再把响应通过Channel写出。

7.2.2 高并发思路

高并发就是能快速地处理大量的请求,需要合理设计线程模型让CPU忙起来,尽量不要让线程阻塞,因为一
阻塞,CPU就闲下来了。另外就是有多少任务,就用相应规模的线程数去处理。

NioEndpoint要完成三件事情:接收连接、检测I/O事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程去处理
比如用专门的线程组去跑Acceptor,并且Acceptor的个数可以配置;
用专门的线程组去跑Poller,Poller的个数也可以配置;
最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小。

7.3 Nio2EndPoint

7.3.1 同步/异步

同步的情况下,数据从内核空间拷贝到用户空间这段时间,应用程序还是阻塞的;
而使用异步,应用程序则始终不阻塞,所以效率高于同步

流程:

首先,应用程序在调用read API的同时告诉内核两件事情:
数据准备好了以后拷贝到哪个Buffer,以及调用哪个回调函数去处理这些数据。

之后,内核接到这个read指令后,等待网卡数据到达,数据到了后,产生硬件中断,内核在中断程序里把数据从网卡拷贝到内核空间,接着做TCP/IP协议层面的数据解包和重组,再把数据拷贝到应用程序指定的Buffer,最后调用应用程序指定的回调函数

对比:
在这里插入图片描述

7.3.2 Nio2EndPoint原理

组件:
在这里插入图片描述
LimitLatch是连接控制器,它负责控制最大连接数。

Nio2Acceptor扩展了Acceptor,用异步I/O的方式来接收连接,跑在一个单独的线程里,也是一个线程组。
Nio2Acceptor接收新的连接后,得到一个AsynchronousSocketChannel,Nio2Acceptor把
AsynchronousSocketChannel封装成一个Nio2SocketWrapper,并创建一个SocketProcessor任务类交给线
程池处理,并且SocketProcessor持有Nio2SocketWrapper对象。

Nio2Acceptor的监听连接的过程不是在一个死循环里不断的调accept方法,而是通过回调函数来完成的

serverSock.accept(null, this);

自己就是处理连接的回调类:

protected class Nio2Acceptor extends Acceptor<AsynchronousSocketChannel> implements CompletionHandler<AsynchronousSocketChannel, Void> {
    @Override
    public void completed(AsynchronousSocketChannel socket,Void attachment) {
        if (isRunning() && !isPaused()) {
            if (getMaxConnections() == -1) {
                //如果没有连接限制,继续接收新的连接
                serverSock.accept(null, this);
            } else {
                //如果有连接限制,就在线程池⾥跑Run⽅法去接收新的连接,Run⽅法会检查连接数
                //为什么要跑run方法呢,因为在run方法里会检查连接数,当连接达到最大数时,线程可能会被						LimitLatch阻塞。
				//为什么要放在线程池里跑呢?这是因为如果放在当前线程里执行,completed方法可能被阻塞,会导致这个回调方法一直不返回。(设计技巧)
                getExecutor().execute(this);
            }
        //处理请求
        if (!setSocketOptions(socket)) {
        	closeSocket(socket);
        }
    }
}

Nio2SocketWrapper的主要作用是封装Channel,并提供接口给Http11Processor读写数据。

问题:Http11Processor是不能阻塞等待数据的,按照异步I/O的流程,Http11Processor在调用
Nio2SocketWrapper的read方法时需要注册回调类,read调用会立即返回,问题是立即返回后
Http11Processor还没有读到数据, 这个请求的处理不就失败了吗?

为了解决这个问题,Http11Processor是通过2次read调用来完成数据读取操作的:(设计技巧)

第一次read调用:连接刚刚建立好后,Acceptor创建SocketProcessor任务类交给线程池去处理,
Http11Processor在处理请求的过程中,会调用Nio2SocketWrapper的read方法发出第一次读请求,同时注册了回调类readCompletionHandler,因为数据没读到,Http11Processor把当前的Nio2SocketWrapper标记为数据不完整。接着SocketProcessor线程被回收,Http11Processor并没有阻塞等待数据。
这里请注意,Http11Processor维护了一个Nio2SocketWrapper列表,也就是维护了连接的状态,等第二次read调用就可以来这个列表里面找到对应的Nio2SocketWrapper进行读写了

第二次read调用:当数据到达后,内核已经把数据拷贝到Http11Processor指定的Buffer里,同时回调类
readCompletionHandler被调用,在这个回调处理方法里会重新创建一个新的SocketProcessor任务来继
续处理这个连接,而这个新的SocketProcessor任务类持有原来那个Nio2SocketWrapper,这一次
Http11Processor可以通过Nio2SocketWrapper读取数据了,因为数据已经到了应用层的Buffer。

(数据没有拷贝两次,第一次read调用是读不到数据的,因为这个时候数据还没应用层的Buffer,只是注册
一个回调函数,当内核将数据拷贝到了应用层Buffer,调用回调函数,在回调函数里,HttpProccessor再
发起一次read,read方法首先会检查数据是不是已经到了Buffer,如果是,直接读取Buffer返回,这一次
并没有真正向内核发起read调用。)

这个回调类readCompletionHandler的源码如下,最关键的一点是,Nio2SocketWrapper是作为附件类来传
递的,这样在回调函数里能拿到所有的上下文。

this.readCompletionHandler = new CompletionHandler<Integer,SocketWrapperBase<Nio2Channel>>() {
    public void completed(Integer nBytes, SocketWrapperBase<Nio2Channel> attachment) {
        ...
        //通过附件类SocketWrapper拿到所有的上下⽂
        Nio2SocketWrapper.this.getEndpoint().processSocket(attachment, SocketEvent.OPEN_READ, false);
    }
    public void failed(Throwable exc, SocketWrapperBase<Nio2Channel> attachment) {
        ...
    }
}

Executor在执行SocketProcessor时,SocketProcessor的run方法会调用Http11Processor来处理请求,
Http11Processor会通过Nio2SocketWrapper读取和解析请求数据,请求经过容器处理后,再把响应通过
Nio2SocketWrapper写出。

Nio2Endpoint中没有Poller组件,也就是没有Selector。这是为什么呢?
因为在异步I/O模式下,Selector的工作交给内核来做了。

7.3.3 总结

由于NIO和NIO.2的API接口和使用方法完全不同,可以想象一个系统中如果已经支持同步I/O,要再支持异
步I/O,改动是比较大的,很有可能不得不重新设计组件之间的接口。
但是Tomcat通过充分的抽象,比如SocketWrapper对Channel的封装,再加上Http11Processor的两次read调用,巧妙地解决了这个问题,使得协议处理器Http11Processor和I/O通信处理器Endpoint之间的接口保持不变。

7.4 AprEndPoint

跟NioEndpoint一样,AprEndpoint也实现了非阻塞I/O
它们的区别是:NioEndpoint通过调用Java的NIO API来实现非阻塞I/O,而AprEndpoint是通过JNI调用APR本地库而实现非阻塞I/O的。

在某些场景下,比如需要频繁与操作系统进行交互,Socket网络通信就是这样一个场景,特别是如果Web应用使用了TLS来加密传输,而TLS协议在握手过程中有多次网络交互,在这种情况下Java跟C语言程序相比还是有一定的差距,而这正是APR的强项。

Tomcat本身是Java编写的,为了调用C语言编写的APR,需要通过JNI方式来调用。JNI(Java Native Interface) 是JDK提供的一个编程接口,它允许Java程序调用其他语言编写的程序或者代码库,JDK本身的实现也大量用到JNI技术来调用本地C程序库。

7.4.1 AprEndpoint工作过程

在这里插入图片描述
它跟NioEndpoint相比只有Acceptor和Poller的实现不同

7.4.1.1 Acceptor

Accpetor的功能就是监听连接,接收并建立连接。它的本质就是调用了四个操作系统API:socket、bind、listen和accept。

那Java语言如何直接调用C语言API呢?通过JNI,具体来说就是两步:
第一步先封装一个Java类,在里面定义一堆用native关键字修饰的方法

public class Socket {
    ...
    //⽤native修饰这个⽅法,表明这个函数是C语⾔实现
    public static native long create(int family, int type,
    int protocol, long cont)
    public static native int bind(long sock, long sa);
    public static native int listen(long sock, int backlog);
    public static native long accept(long sock)
}

第二步就是用C代码实现这些方法,如bind:

//注意函数的名字要符合JNI规范的要求
JNIEXPORT jint JNICALL
Java_org_apache_tomcat_jni_Socket_bind(JNIEnv *e, jlong sock,jlong sa)
{
    jint rv = APR_SUCCESS;
    tcn_socket_t *s = (tcn_socket_t *)sock;
    apr_sockaddr_t *a = (apr_sockaddr_t *) sa;
    //调⽤APR库⾃⼰实现的bind函数
    rv = (jint)apr_socket_bind(s->sock, a);
    return rv;
}
7.4.1.2 Poller

Acceptor接收到一个新的Socket连接后,按照NioEndpoint的实现,它会把这个Socket交给Poller去查询I/O事件。

AprEndpoint也是这样做的,不过AprEndpoint的Poller并不是调用Java NIO里的Selector来查询Socket的状态,而是通过JNI调用APR中的poll方法,而APR又是调用了操作系统的epoll API来实现的。

这里有个特别的地方是在AprEndpoint中可以配置一个叫deferAccept的参数,它对应的是TCP协议中的TCP_DEFER_ACCEPT,设置这个参数后,当TCP客户端有新的连接请求到达时,TCP服务端先不建立连接,而是再等等,直到客户端有请求数据发过来时再建立连接。这样的好处是服务端不需要用Selector去反复查询请求数据是否就绪。

这是一种TCP协议层的优化,不是每个操作系统内核都支持,因为Java作为一种跨平台语言,需要屏蔽各种操作系统的差异,因此并没有把这个参数提供给用户;但是对于APR来说,它的目的就是尽可能提升性能,因此它向用户暴露了这个参数。

7.4.2 APR提升性能的原因

除了APR本身是C程序库之外,还有:

7.4.2.1 使用本地内存

通过JNI调用C代码来实现Socket通信,C代码在运行过程中需要的内存通过本地内存来分配:

一个程序运行一个Java类时,这个程序会创建JVM来加载和运行你的Java类。操作系统会创建一个进程来执行这个java可执行程序,而每个进程都有自己的虚拟地址空间,JVM用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。
注意,JVM内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。从JVM的角度看,JVM内存之外的部分叫作本地内存,C程序代码在运行过程中用到的内存就是本地内存中分配的:
在这里插入图片描述
Tomcat的Endpoint组件在接收网络数据时需要预先分配好一块Buffer,所谓的Buffer就是字节数组byte[],Java通过JNI调用把这块Buffer的地址传给C代码,C代码通过操作系统API读取Socket并把数据填充到这块Buffer。
Java NIO API提供了两种Buffer来接收数据:HeapByteBuffer和DirectByteBuffer

HeapByteBuffer和DirectByteBuffer有什么区别呢?

HeapByteBuffer对象本身在JVM堆上分配,并且它持有的字节数组byte[]也是在JVM堆上分配。但是如果用HeapByteBuffer来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到JVM堆,而不是直接从内核拷贝到JVM堆上。这是为什么呢?
这是因为数据从内核拷贝到JVM堆的过程中,JVM可能会发生GC,GC过程中对象可能会被移动,也就是说JVM堆上的字节数组可能会被移动,这样的话Buffer地址就失效了。如果这中间经过本地内存中转,从本地内存到JVM堆的拷贝过程中JVM可以保证不做GC。

如果使用HeapByteBuffer,会发现JVM堆和内核之间多了一层中转,而DirectByteBuffer用来解决这个问
题;
DirectByteBuffer对象本身在JVM堆上,但是它持有的字节数组不是从JVM堆上分配的,而是从本地内存分配的。DirectByteBuffer对象中有个long类型字段address,记录着本地内存的地址,这样在接收数据的时候,直接把这个本地内存地址传递给C程序,C程序会将网络数据从内核拷贝到这个本地内存,JVM可以直接读取这个本地内存,这种方式比HeapByteBuffer少了一次拷贝,因此一般来说它的速度会比HeapByteBuffer快好几倍。

Tomcat中的AprEndpoint就是通过DirectByteBuffer来接收数据的,而NioEndpoint和Nio2Endpoint是通过
HeapByteBuffer来接收数据的。
NioEndpoint和Nio2Endpoint为什么不用DirectByteBuffer呢?因为本地内存不好管理,发生内存泄漏难以定位,从稳定性考虑,NioEndpoint和Nio2Endpoint没有去冒这个险。

7.4.2.2 sendfile

考虑另一个网络通信的场景,也就是静态文件的处理。

浏览器通过Tomcat来获取一个HTML文件,而Tomcat的处理逻辑为两步:

  1. 从磁盘读取HTML到内存。
  2. 将这段内存的内容通过Socket发送出去。

在传统方式下,有很多次的内存拷贝:

读取文件时,首先是内核把文件内容读取到内核缓冲区。
如果使用HeapByteBuffer,文件数据从内核到JVM堆内存需要经过本地内存中转。
同样在将文件内容推入网络时,从JVM堆到内核缓冲区需要经过本地内存中转。
最后还需要把文件从内核缓冲区拷贝到网卡缓冲区:

在这里插入图片描述>

这个过程有6次内存拷贝,并且read和write等系统调用将导致进程从用户态到内核态的切换,会耗费大量的CPU和内存资源。

Tomcat的AprEndpoint通过操作系统层面的sendfile特性解决了这个问题:

sendfile(socket, file, len);//两个关键参数:Socket和文件句柄

将文件从磁盘写入Socket的过程只有两步:
第一步:将文件内容读取到内核缓冲区。
第二步:数据并没有从内核缓冲区复制到Socket关联的缓冲区,只有记录数据位置和长度的描述符被添加到
Socket缓冲区中;接着把数据直接从内核缓冲区传递给网卡:
在这里插入图片描述

7.5 Executor::Tomcat如何扩展Java线程池?

定制ThreadPoolExecutor有两个关键参数:是否限制线程个数、是否限制队列长度,Tomcat定制如下:

//定制版的任务队列
taskqueue = new TaskQueue(maxQueueSize);
//定制版的线程⼯⼚
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
//定制版的线程池
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS

Tomcat有自己的定制版任务队列和线程工厂,并且可以限制任务队列的长度,它的最大长度是maxQueueSize。
Tomcat对线程数也有限制,设置了核心线程数(minSpareThreads)和最大线程池数(maxThreads)。

  • 定制版任务处理流程

除了资源限制以外,Tomcat线程池还定制自己的任务处理流程:通过重写execute方法实现了自己的任务处理逻辑:

  1. 前corePoolSize个任务时,来一个任务就创建一个新线程。(同)
  2. 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。(同)
  3. 如果总线程数达到maximumPoolSize,则继续尝试把任务添加到任务队列中去,如果队列也满了,插入失败,执行拒绝策略。

对比Java:

  1. 前corePoolSize个任务时,来一个任务就创建一个新线程。
  2. 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。
  3. 如果总线程数达到maximumPoolSize,执行拒绝策略。

其代码为:

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
...
    public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
        //调⽤Java原⽣线程池的execute去执⾏任务
        	super.execute(command);
        } catch (RejectedExecutionException rx) {
            //如果总线程数达到maximumPoolSize,Java原⽣线程池执⾏拒绝策略
            if (super.getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue)super.getQueue();
                try {
                //继续尝试把任务放到任务队列中去
                	if (!queue.force(command, timeout, unit)) {
                        //submittedCount这个变量是为了在任务队列的长度无限制的情况下,让线程池有机会创建新的线程。
                    	submittedCount.decrementAndGet();
                    	//如果缓冲队列也满了,插⼊失败,执⾏拒绝策略。
                    	throw new RejectedExecutionException("...");
                }
            }
    	}
	}
}
  • 定制版的任务队列:使得在任务队列长度无限制的情况下,线程池仍然有机会创建新的线程

Tomcat的任务队列TaskQueue扩展了Java中的LinkedBlockingQueue,传入capacity参数,默认是是Integer.MAX_VALUE,等于没有限制,这样就带来一个问题:当前线程数达到核心线程数之后,再来
任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会有机会创建新线程了。

解决:TaskQueue重写了LinkedBlockingQueue的offer方法,在当前线程数大于核心线程数、小于最大线程数,并且已提交的任务个数大于当前线程数时,也就是说线程不够用了,但是线程数又没达到极限,才会去创建新的线程时返回false,返回false表示任务添加失败,这时线程池会创建新的线程

public class TaskQueue extends LinkedBlockingQueue<Runnable> {
    ...
    @Override
    //线程池调⽤任务队列的⽅法时,当前线程数肯定已经⼤于核⼼线程数了
    public boolean offer(Runnable o) {
        //如果线程数已经到了最⼤值,不能创建新线程了,只能把任务添加到任务队列。
        if (parent.getPoolSize() == parent.getMaximumPoolSize())
        	return super.offer(o);
        //执⾏到这⾥,表明当前线程数⼤于核⼼线程数,并且⼩于最⼤线程数。
        //表明是可以创建新线程的,那到底要不要创建呢?分两种情况:
        //1. 如果已提交的任务数⼩于当前线程数,表⽰还有空闲线程,⽆需创建新线程
        if (parent.getSubmittedCount()<=(parent.getPoolSize()))
        	return super.offer(o);
        //2. 如果已提交的任务数⼤于当前线程数,线程不够⽤了,返回false去创建新线程
        if (parent.getPoolSize()<parent.getMaximumPoolSize())
        	return false;
        //默认情况下总是把任务添加到任务队列
        return super.offer(o);
    }
}

当然可以通过设置maxQueueSize参数来限制任务队列的长度,也就不存在这个问题了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值