补充:
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的处理逻辑为两步:
- 从磁盘读取HTML到内存。
- 将这段内存的内容通过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方法实现了自己的任务处理逻辑:
- 前corePoolSize个任务时,来一个任务就创建一个新线程。(同)
- 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。(同)
- 如果总线程数达到maximumPoolSize,则继续尝试把任务添加到任务队列中去,如果队列也满了,插入失败,执行拒绝策略。
对比Java:
- 前corePoolSize个任务时,来一个任务就创建一个新线程。
- 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。
- 如果总线程数达到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参数来限制任务队列的长度,也就不存在这个问题了