在RxJava中有5种不同的调度程序可供选择:
- immediate():创建并返回一个在当前线程上立即执行工作的Scheduler。
- trampoline():创建并返回一个Scheduler ,该Scheduler 所在线程并不会立即工作,而是要等待我们所设定的等待时间结束后才可以执行(默认为0),当然,这个延时设置是要设定在Runnable实现内部。还有一点就是所有任务要添加到一个队列中,然后依次执行即可。
- newThread():创建并返回一个Scheduler,为每个任务创建一个新的Thread。
- computation():创建并返回用于计算工作的Scheduler。它可以用于事件循环,处理回调和其他计算工作。注意不要使用该Scheduler执行IO类型的工作,对此,我们可以使用io() 代替。
- io() :创建并返回一个用于IO类型工作的Scheduler。该实现维护了一个Executor线程池,该线程池可根据需要增长。该Scheduler可用于异步执行阻塞IO。不要使用该Scheduler执行计算任务。
前3个Scheduler解释的非常到位,对computation() 和 io() 有点困惑。
java.io
)和files(java.nio.files
)吗?适用于数据库查询吗?适用于下载文件还是访问REST API?很棒的问题,我认为文档可以提供更多细节。
io()
由无限制线程数量的线程池支持,用于执行非计算密集型任务,这些任务不会对CPU造成太大负担(比如主板上的南北桥芯片,南桥芯片主要负责软驱、硬盘、键盘以及附加卡的数据交换)。因此,与文件系统的交互,与不同主机上的数据库或服务的交互就是很好的适用场景。computation()
由有限数量的线程池支持,其大小等于可用处理器的数量。如果你试图在可用处理器之外并行安排cpu密集型工作(比如使用newThread()
),那么当线程争夺处理器时,你就会面临线程创建开销和上下文切换开销,并且它可能会受到很大的性能影响。computation()
CPU密集型工作,否则你将无法获得良好的CPU利用率。io()
根据2中所讲,在进行计算任务时使用io()
是很不好的,如果你io()
并行安排了一千个计算任务,那么这千个任务中的每一个都将拥有自己的线程并争夺CPU产生的上下文切换成本。作为一个乐于分享的人,我希望通过一些成熟优秀的代码库,来向大家展示读源码思路以及阐述编程方面的技巧,也希望大家从中思考并得到属于自己的一套编程方法论。
半年以来,已进行72小时时长的源码解读分享视频录制,额外分享时间未计,虽有诸多不足,依然欢迎进行技术交流,也希望可以影响到更多人参与到分享中来,通过分享交到更多朋友,获取快乐,共同成长。
相关书籍已出版:https://item.m.jd.com/product/12615848.html
书籍封面:
bilibili:https://www.bilibili.com/video/av34537840
油管:https://www.youtube.com/playlist?list=PL95Ey4rht798MMCusPzIW7VYD1xaKJVjc
相关文章待书出版
B站:https://www.bilibili.com/video/av35326911
油管:https://www.youtube.com/playlist?list=PL95Ey4rht7980EH8yr7SLBvj9XSE1ggdy
备注:相关博文与视频会和Spring Webflux一起进行分享
视频分享(本系列视频以0.8.5+ 版本为主):
B站:https://www.bilibili.com/video/av45556406/
油管:https://www.youtube.com/watch?v=6qLh2L75KdM&list=PL95Ey4rht79-ISlb_Yr9ToaEI0K8ARmH6
0.7.x版本相关博文(0.8.5+ 版本相关文章待书出版):
Java编程方法论-Spring WebFlux篇 01 为什么需要Spring WebFlux 上
Java编程方法论-Spring WebFlux篇 01 为什么需要Spring WebFlux 下
Java编程方法论-Spring WebFlux篇 Reactor-Netty下HttpServer 的封装
Java编程方法论-Spring WebFlux篇 Reactor-Netty下TcpServer的功能实现 1
B站:https://www.bilibili.com/video/av43230997
油管:https://www.youtube.com/watch?v=ZZnCI8xaTRo&list=PL95Ey4rht799NVLgQiSV9skTqY6VuspIk
相关博文:
BIO到NIO源码的一些事儿之NIO 下 之 Selector
BIO到NIO源码的一些事儿之NIO 下 Buffer解读 上
BIO到NIO源码的一些事儿之NIO 下 Buffer解读 下
bilibili :https://www.bilibili.com/video/av50169264
youtube : https://www.youtube.com/watch?v=AHNW9YCF9aI&list=PL95Ey4rht798WiqkvGYChWdUtHie0j-IU
bilibili :https://www.bilibili.com/video/av51324899
youtube : 待上传
https://muyinchen.github.io/tags/Spring/
其中Spring Reactor 分享视频获得了Spring 开发者的认可并被Spring官方推特转发:
https://juejin.im/user/59c7640851882578e00ddf90
交流QQ群: 523409180
]]>此系列文章会详细解读NIO的功能逐步丰满的路程,为Reactor-Netty 库的讲解铺平道路。
关于Java编程方法论-Reactor与Webflux的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
本系列源码解读基于JDK11 api细节可能与其他版本有所差别,请自行解决jdk版本问题。
本系列前几篇:
BIO到NIO源码的一些事儿之NIO 下 之 Selector
在Java BIO中,通过BIO到NIO源码的一些事儿之BIO开篇的Demo可知,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接。我们来拿一个杯子做例子,我们不讲它的材质,只说它的使用属性,一个杯子在使用过程中会首先看其最大容量,然后加水,这里给一个限制,即加到杯子中的水量为杯子最大容量的一半,然后喝水,我们最多也只能喝杯子里所盛水量。由这个例子,我们思考下,杯子是不是可以看作是一个缓冲区,对于杯子倒水的节奏我们是不是可以轻易的控制,从而带来诸多方便,那是不是可以将之前BIO
中的缓冲区也加入一些特性,使之变的和我们使用杯子一样便捷。
于是,我们给buffer
添加几个属性,对比杯子的最大容量,我们设计添加一个capacity
属性,对比加上的容量限制,我们设计添加一个limit
属性,对于加水加到杯中的当前位置,我们设计添加一个position
属性,有时候我们还想在杯子上自己做个标记,比如喝茶,我自己的习惯就是喝到杯里剩三分之一水的时候再加水加到一半,针对这个情况,设计添加一个mark
属性。由此,我们来总结下这几个属性的关系,limit
不可能比capacity
大的,position
又不会大于limit
,mark
可以理解为一个标签,其也不会大于position
,也就是mark <= position <= limit <= capacity
。
结合以上概念,我们来对buffer中这几个属性使用时的行为进行下描述:
capacity
也就是缓冲区的容量大小。我们只能往里面写
capacity
个byte
、long
、char
等类型。一旦Buffer
满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
(1)当我们写数据到
Buffer
中时,position
表示当前的位置。初始的position
值为0.当一个byte
、long
、char
等数据写到Buffer
后,position
会向前移动到下一个可插入数据的Buffer
位置。position
最大可为capacity – 1
。(2)当读取数据时,也是从某个特定位置读。当将
Buffer
从写模式切换到读模式,position
会被重置为0
. 当从Buffer
的position
处读取数据时,position
向前移动到下一个可读的位置。
limit
(1)在写模式下,
Buffer
的limit
表示你最多能往Buffer
里写多少数据。 写模式下,limit
等于Buffer
的capacity
。(2)读模式时,
limit
表示你最多能读到多少数据。因此,当切换Buffer
到读模式时,limit
会被设置成写模式下的position
值。换句话说,你能读到之前写入的所有数据(limit
被设置成已写数据的数量,这个值在写模式下就是position
)
mark
类似于喝茶喝到剩余三分之一谁加水一样,当buffer调用它的reset方法时,当前的位置
position
会指向mark
所在位置,同样,这个也根据个人喜好,有些人就喜欢将水喝完再添加的,所以mark
不一定总会被设定,但当它被设定值之后,那设定的这个值不能为负数,同时也不能大于position
。还有一种情况,就是我喝水喝不下了,在最后将水一口喝完,则对照的此处的话,即如果对mark
设定了值(并非初始值-1),则在将position
或limit
调整为小于mark
的值的时候将mark
丢弃掉。如果并未对mark
重新设定值(即还是初始值-1),那么在调用reset
方法会抛出InvalidMarkException
异常。
可见,经过包装的Buffer是Java NIO中对于缓冲区的抽象。在Java有8中基本类型:byte、short、int、long、float、double、char、boolean
,除了boolean
类型外,其他的类型都有对应的Buffer
具体实现,可见,Buffer
是一个用于存储特定基本数据类型的容器。再加上数据时有序存储的,而且Buffer
有大小限制,所以,Buffer
可以说是特定基本数据类型的线性存储有限的序列。
接着,我们通过下面这幅图来展示下上面几个属性的关系,方便大家更好理解:
先来看一个Demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
我们抛去前两行,来总结下buffer的使用步骤:
1 | //java.nio.ByteBuffer#allocate |
ByteBuffer
是一个抽象类,具体的实现有HeapByteBuffer
和DirectByteBuffer
。分别对应Java
堆缓冲区与堆外内存缓冲区。Java堆缓冲区本质上就是byte数组(由之前分析的,我们只是在字节数组上面加点属性,辅以逻辑,实现一些更复杂的功能),所以实现会比较简单。而堆外内存涉及到JNI代码实现,较为复杂,所以我们先来分析HeapByteBuffer
的相关操作,随后再专门分析DirectByteBuffer
。
我们来看HeapByteBuffer
相关构造器源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//java.nio.HeapByteBuffer#HeapByteBuffer(int, int)
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
this.address = ARRAY_BASE_OFFSET;
}
//java.nio.ByteBuffer#ByteBuffer(int, int, int, int, byte[], int)
ByteBuffer(int mark, int pos, int lim, int cap,
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
//java.nio.Buffer#Buffer
Buffer(int mark, int pos, int lim, int cap) {
if (cap < 0)
throw createCapacityException(cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
由上,HeapByteBuffer
通过初始化字节数组hd
,在虚拟机堆上申请内存空间。
因在ByteBuffer
中定义有hb
这个字段,它是一个byte[]
类型,为了获取这个字段相对于当前这个ByteBuffer
对象所在内存地址,通过private static final long ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class)
中这个UNSAFE
操作来获取这个数组第一个元素位置与该对象所在地址的相对长度,这个对象的地址代表你的头所在的位置,将这个数组看作你的鼻子,而这里返回的是你的鼻子距离头位置的那个长度,即数组第一个位置距离这个对象开始地址所在位置,这个是在class字节码加载到jvm里的时候就已经确定了。
如果ARRAY_INDEX_SCALE = UNSAFE.arrayIndexScale(byte[].class)
为返回非零值,则可以使用该比例因子以及此基本偏移量(ARRAY_BASE_OFFSET)来形成新的偏移量,以访问这个类的数组元素。知道这些,在ByteBuffer
的slice
duplicate
之类的方法,就能理解其操作了,就是计算数组中每一个元素所占空间长度得到ARRAY_INDEX_SCALE
,然后当我确定我从数组第5个位置作为该数组的开始位置操作时,我就可以使用this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE
。
我们再通过下面的源码对上述内容对比消化下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45//java.nio.HeapByteBuffer
protected HeapByteBuffer(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{
super(mark, pos, lim, cap, buf, off);
/*
hb = buf;
offset = off;
*/
this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE;
}
public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
ByteBuffer slice(int pos, int lim) {
assert (pos >= 0);
assert (pos <= lim);
int rem = lim - pos;
return new HeapByteBuffer(hb,
-1,
0,
rem,
rem,
pos + offset);
}
public ByteBuffer duplicate() {
return new HeapByteBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
每个buffer
都是可读的,但不是每个buffer
都是可写的。这里,当buffer
有内容变动的时候,会首先调用buffer
的isReadOnly
判断此buffer
是否只读,只读buffer
是不允许更改其内容的,但mark
、position
和 limit
的值是可变的,这是我们人为给其额外的定义,方便我们增加功能逻辑的。当在只读buffer
上调用修改时,则会抛出ReadOnlyBufferException
异常。我们来看buffer
的put
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//java.nio.ByteBuffer#put(java.nio.ByteBuffer)
public ByteBuffer put(ByteBuffer src) {
if (src == this)
throw createSameBufferException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
//java.nio.Buffer#remaining
public final int remaining() {
return limit - position;
}
上面remaining
方法表示还剩多少数据未读,上面的源码讲的是,如果src
这个ByteBuffer
的src.remaining()
的数量大于要存放的目标Buffer
的还剩的空间,直接抛溢出的异常。然后通过一个for循环,将src
剩余的数据,依次写入目标Buffer
中。接下来,我们通过src.get()
来探索下Buffer
的读操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//java.nio.HeapByteBuffer#get()
public byte get() {
return hb[ix(nextGetIndex())];
}
public byte get(int i) {
return hb[ix(checkIndex(i))];
}
//java.nio.HeapByteBuffer#ix
protected int ix(int i) {
return i + offset;
}
//java.nio.Buffer#nextGetIndex()
final int nextGetIndex() {
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
这里,为了依次读取数组中的数据,这里使用nextGetIndex()
来获取要读位置,即先返回当前要获取的位置值,然后position自己再加1。以此在前面ByteBuffer#put(java.nio.ByteBuffer)
所示源码中的for
循环中依次对剩余数据的读取。上述get(int i)
不过是从指定位置获取数据,实现也比较简单HeapByteBuffer#ix
也只是确定所要获取此数组对象指定位置数据,其中的offset
表示第一个可读字节在该字节数组中的位置(就好比我喝茶杯底三分之一水是不喝的,每次都从三分之一水量开始位置计算喝了多少或者加入多少水)。
接下来看下单个字节存储到指定字节数组的操作,与获取字节数组单个位置数据相对应,代码比较简单:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//java.nio.HeapByteBuffer#put(byte)
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
public ByteBuffer put(int i, byte x) {
hb[ix(checkIndex(i))] = x;
return this;
}
//java.nio.Buffer#nextPutIndex()
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
前面的都是单个字节的,下面来讲下批量操作字节数组是如何进行的,因过程知识点重复,这里只讲get,先看源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//java.nio.ByteBuffer#get(byte[])
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
//java.nio.ByteBuffer#get(byte[], int, int)
public ByteBuffer get(byte[] dst, int offset, int length) {
// 检查参数是否越界
checkBounds(offset, length, dst.length);
// 检查要获取的长度是否大于Buffer中剩余的数据长度
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
//java.nio.Buffer#checkBounds
static void checkBounds(int off, int len, int size) { // package-private
if ((off | len | (off + len) | (size - (off + len))) < 0)
throw new IndexOutOfBoundsException();
}
通过这个方法将这个buffer中的字节数据读到我们给定的目标数组dst中,由checkBounds可知,当要写入目标字节数组的可写长度小于将要写入数据的长度的时候,会产生边界异常。当要获取的长度是大于Buffer中剩余的数据长度时抛出BufferUnderflowException
异常,当验证通过后,接着就从目标数组的offset
位置开始,从buffer
获取并写入offset + length
长度的数据。
可以看出,HeapByteBuffer
是封装了对byte数组的简单操作。对缓冲区的写入和读取本质上是对数组的写入和读取。使用HeapByteBuffer
的好处是我们不用做各种参数校验,也不需要另外维护数组当前读写位置的变量了。
同时我们可以看到,Buffer
中对position
的操作没有使用锁保护,所以Buffer
不是线程安全的。如果我们操作的这个buffer
会有多个线程使用,则针对该buffer
的访问应通过适当的同步控制机制来进行保护。
jdk本身是没这个说法的,只是按照我们自己的操作习惯,我们将Buffer
分为两种工作模式,一种是接收数据模式,一种是输出数据模式。我们可以通过Buffer
提供的flip
等操作来切换Buffer
的工作模式。
我们来新建一个容量为10的ByteBuffer
:1
ByteBuffer.allocate(10);
由前面所学的HeapByteBuffer
的构造器中的相关代码可知,这里的position
被设置为0,而且 capacity
和limit
设置为 10,mark
设置为-1,offset
设定为0。
可参考下图展示:
新建的Buffer
处于接收数据的模式,可以向Buffer
放入数据,在放入一个对应基本类型的数据后(此处假如放入一个char类型数据),position加一,参考我们上面所示源码,如果position已经等于limit了还进行put
操作,则会抛出BufferOverflowException
异常。
我们向所操作的buffer中put 5个char类型的数据进去:1
buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');
会得到如下结果视图:
由之前源码分析可知,Buffer的读写的位置变量都是基于position
来做的,其他的变量都是围绕着它进行辅助管理的,所以如果从Buffer
中读取数据,要将Buffer
切换到输出数据模式(也就是读模式)。此时,我们就可以使用Buffer
提供了flip方法。1
2
3
4
5
6
7//java.nio.Buffer#flip
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
我们知道,在put的时候,会进行java.nio.Buffer#nextPutIndex()
的调用,里面会进行position >= limit
,所以,此时再进行写操作的话,会从第0个位置开始进行覆盖,而且只能写到flip
操作之后limit
的位置。1
2
3
4
5
6//java.nio.Buffer#nextPutIndex()
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
在做完put
操作后,position
会自增一下,所以,flip
操作示意图如下:
也是因为position
为0了,所以我们可以很方便的从Buffer中第0个位置开始读取数据,不需要别的附加操作。由之前解读可知,每次读取一个元素,position
就会加一,如果position
已经等于limit
还进行读取,则会抛出BufferUnderflowException
异常。
我们通过flip
方法把Buffer
从接收写模式切换到输出读模式,如果要从输出模式切换到接收模式,可以使用compact
或者clear
方法,如果数据已经读取完毕或者数据不要了,使用clear
方法,如果只想从缓冲区中释放一部分数据,而不是全部(即释放已读数据,保留未读数据),然后重新填充,使用compact
方法。
对于clear
方法,我们先来看它的源码:1
2
3
4
5
6
7//java.nio.Buffer#clear
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
我们可以看到,它的clear
方法内并没有做清理工作,只是修改位置变量,重置为初始化时的状态,等待下一次将数据写入缓冲数组。
接着,来看compact
操作的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//java.nio.HeapByteBuffer#compact
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
//java.nio.ByteBuffer#position
ByteBuffer position(int newPosition) {
super.position(newPosition);
return this;
}
//java.nio.Buffer#position(int)
public Buffer position(int newPosition) {
if (newPosition > limit | newPosition < 0)
throw createPositionException(newPosition);
position = newPosition;
if (mark > position) mark = -1;
return this;
}
//java.nio.ByteBuffer#limit
ByteBuffer limit(int newLimit) {
super.limit(newLimit);
return this;
}
//java.nio.Buffer#limit(int)
public Buffer limit(int newLimit) {
if (newLimit > capacity | newLimit < 0)
throw createLimitException(newLimit);
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
//java.nio.Buffer#discardMark
final void discardMark() {
mark = -1;
}
这里使用了数组的拷贝操作,将未读元素转移到该字节数组从0开始的位置,由于remaining()
返回的是limit - position
,假如在flip
操作的时候填入的元素有5个,那么limit
为5,此时读到了第三个元素,也就是在调用compact
时position
的数值为2,那remaining()
的值就为3,也就是此时position
为3,compact
操作后,limit
会回归到和初始化数组容量大小一样,并将mark值置为 -1。
我们来看示意图,在进行buffer.compact()
调用前:
%E8%B0%83%E7%94%A8%E5%89%8D.png?raw=true)
buffer.compact()
调用后:
%E8%B0%83%E7%94%A8%E5%90%8E.png?raw=true)
接下来,我们再接触一些ByteBuffer
的其他方法,方便在适当的条件下进行使用。
首先来看它的源码:1
2
3
4
5
6//java.nio.Buffer#rewind
public Buffer rewind() {
position = 0;
mark = -1;
return this;
}
这里就是将position
设定为0,mark
设定为-1,其他设定的管理属性(capacity
,limit
)不变。结合前面的知识,在字节数组写入数据后,它的clear
方法也只是重置我们在Buffer
中设定的那几个增强管理属性(capacity
、position
、limit
、mark
),此处的英文表达的意思也很明显:倒带,也就是可以回头重新写,或者重新读。但是我们要注意一个前提,我们要确保已经恰当的设置了limit
。这个方法可以在Channel
的读或者写之前调用,如:1
2
3out.write(buf); // Write remaining data
buf.rewind(); // Rewind buffer
buf.get(array); // Copy data into array
我们通过下图来进行展示执行rewind
操作后的结果:
在JDK9版本中,新增了这个方法。用来创建一个与原始Buffer
一样的新Buffer
。新Buffer
的内容和原始Buffer
一样。改变新Buffer
内的数据,同样会体现在原始Buffer
上,反之亦然。两个Buffer
都拥有自己独立的 position
,limit
和mark
属性。
刚创建的新Buffer
的position
,limit
和mark
属性与原始Buffer
对应属性的值相同。
还有一点需要注意的是,如果原始Buffer
是只读的(即HeapByteBufferR
),那么新Buffer
也是只读的。如果原始Buffer
是DirectByteBuffer
,那新Buffer
也是DirectByteBuffer
。
我们来看相关源码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//java.nio.HeapByteBuffer#duplicate
public ByteBuffer duplicate() {
return new HeapByteBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
//java.nio.HeapByteBufferR#duplicate
public ByteBuffer duplicate() {
return new HeapByteBufferR(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
//java.nio.DirectByteBuffer#duplicate
public ByteBuffer duplicate() {
return new DirectByteBuffer(this,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
0);
}
基本类型的参数传递都是值传递,所以由上面源码可知每个新缓冲区都拥有自己的 position
、limit
和 mark
属性,而且他们的初始值使用了原始Buffer
此时的值。
但是,从HeapByteBuffer
角度来说,对于hb 作为一个数组对象,属于对象引用传递,即新老Buffer
共用了同一个字节数组对象。无论谁操作,都会改变另一个。
从DirectByteBuffer
角度来说,直接内存看重的是地址操作,所以,其在创建这个新Buffer
的时候传入的是原始Buffer
的引用,进而可以获取到相关地址。
可以使用 asReadOnlyBuffer()
方法来生成一个只读的缓冲区。这与duplicate()
实现有些相同,除了这个新的缓冲区不允许使用put()
,并且其isReadOnly()
函数
将会返回true 。 对这一只读缓冲区调用put()
操作,会导致ReadOnlyBufferException
异常。
我们来看相关源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62//java.nio.ByteBuffer#put(java.nio.ByteBuffer)
public ByteBuffer put(ByteBuffer src) {
if (src == this)
throw createSameBufferException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
//java.nio.HeapByteBuffer#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {
return new HeapByteBufferR(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
//java.nio.HeapByteBufferR#asReadOnlyBuffer
//HeapByteBufferR下直接调用其duplicate方法即可,其本来就是只读的
public ByteBuffer asReadOnlyBuffer() {
return duplicate();
}
//java.nio.DirectByteBuffer#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {
return new DirectByteBufferR(this,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
0);
}
//java.nio.DirectByteBufferR#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {
return duplicate();
}
//java.nio.HeapByteBufferR#HeapByteBufferR
protected HeapByteBufferR(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{
super(buf, mark, pos, lim, cap, off);
this.isReadOnly = true;
}
//java.nio.DirectByteBufferR#DirectByteBufferR
DirectByteBufferR(DirectBuffer db,
int mark, int pos, int lim, int cap,
int off)
{
super(db, mark, pos, lim, cap, off);
this.isReadOnly = true;
}
可以看到,ByteBuffer
的只读实现,在构造器里首先将isReadOnly
属性设定为true
。接着,HeapByteBufferR
继承了HeapByteBuffer
类(DirectByteBufferR
也是类似实现,就不重复了),并重写了所有可对buffer修改的方法。把所有能修改buffer
的方法都直接抛出ReadOnlyBufferException来保证只读。来看DirectByteBufferR
相关源码,其他对应实现一样:1
2
3
4//java.nio.DirectByteBufferR#put(byte)
public ByteBuffer put(byte x) {
throw new ReadOnlyBufferException();
}
slice
从字面意思来看,就是切片,用在这里,就是分割ByteBuffer
。即创建一个从原始ByteBuffer
的当前位置(position
)开始的新ByteBuffer
,并且其容量是原始ByteBuffer
的剩余消费元素数量( limit-position
)。这个新ByteBuffer
与原始ByteBuffer
共享一段数据元素子序列,也就是设定一个offset值,这样就可以将一个相对数组第三个位置的元素看作是起点元素,此时新ByteBuffer
的position
就是0,读取的还是所传入这个offset
的所在值。分割出来的ByteBuffer
也会继承只读和直接属性。
我们来看相关源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//java.nio.HeapByteBuffer#slice()
public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
protected HeapByteBuffer(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{
super(mark, pos, lim, cap, buf, off);
/*
hb = buf;
offset = off;
*/
this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE;
}
由源码可知,新ByteBuffer
和原始ByteBuffer
共有了一个数组,新ByteBuffer
的mark
值为-1,position
值为0,limit
和capacity
都为原始Buffer
中limit-position
的值。
于是,我们可以通过下面两幅图来展示slice
方法前后的对比。
原始ByteBuffer
:
调用slice
方法分割后得到的新ByteBuffer
:
本篇到此为止,在下一篇中,我会着重讲下DirectByteBuffer
的实现细节。
此系列文章会详细解读NIO的功能逐步丰满的路程,为Reactor-Netty 库的讲解铺平道路。
关于Java编程方法论-Reactor与Webflux的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
本系列源码解读基于JDK11 api细节可能与其他版本有所差别,请自行解决jdk版本问题。
本系列前几篇:
如我们在前面内容所讲,在学生确定之后,我们就要对其状态进行设定,然后再交由Selector
进行管理,其状态的设定我们就通过SelectionKey
来进行。
那这里我们先通过之前在Channel
中并未仔细讲解的SelectableChannel
下的register
方法。我们前面有提到过, SelectableChannel
将channel
打造成可以通过Selector
来进行多路复用。作为管理者,channel
想要实现复用,就必须在管理者这里进行注册登记。所以,SelectableChannel
下的register
方法也就是我们值得二次关注的核心了,也是对接我们接下来内容的切入点,对于register
方法的解读,请看我们之前的文章BIO到NIO源码的一些事儿之NIO 上 中赋予Channel可被多路复用的能力这一节的内容。
这里要记住的是SelectableChannel
是对接channel
特征(即SelectionKey
)的关键所在,这有点类似于表设计,原本可以将特征什么的设定在一张表内,但为了操作更加具有针对性,即为了让代码功能更易于管理,就进行抽取并设计了第二张表,这个就有点像人体器官,整体上大家共同协作完成一件事,但器官内部自己专注于自己的主要特定功能,偶尔也具备其他器官的一些小功能。
由此,我们也就可以知道,SelectionKey
表示一个SelectableChannel
与Selector
关联的标记,可以简单理解为一个token
。就好比是我们做权限管理系统用户登录后前台会从后台拿到的一个token
一样,用户可以凭借此token
来访问操作相应的资源信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//java.nio.channels.spi.AbstractSelectableChannel#register
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{ ...
synchronized (regLock) {
...
synchronized (keyLock) {
...
SelectionKey k = findKey(sel);
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
return k;
}
}
}
结合上下两段源码,在每次Selector
使用register
方法注册channel
时,都会创建并返回一个SelectionKey
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//sun.nio.ch.SelectorImpl#register
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
// register (if needed) before adding to key set
implRegister(k);
// add to the selector's key set, removing it immediately if the selector
// is closed. The key is not in the channel's key set at this point but
// it may be observed by a thread iterating over the selector's key set.
keys.add(k);
try {
k.interestOps(ops);
} catch (ClosedSelectorException e) {
assert ch.keyFor(this) == null;
keys.remove(k);
k.cancel();
throw e;
}
return k;
}
我们在BIO到NIO源码的一些事儿之NIO 上 中赋予Channel可被多路复用的能力这一节的内容知道,一旦注册到Selector
上,Channel
将一直保持注册直到其被解除注册。在解除注册的时候会解除Selector
分配给Channel
的所有资源。
也就是SelectionKey
在其调用SelectionKey#channel
方法,或这个key所代表的channel
关闭,抑或此key所关联的Selector
关闭之前,都是有效。我们在前面的文章分析中也知道,取消一个SelectionKey
,不会立刻从Selector
移除,它将被添加到Selector
的cancelledKeys
这个Set
集合中,以便在下一次选择操作期间删除,我们可以通过java.nio.channels.SelectionKey#isValid
判断一个SelectionKey
是否有效。
SelectionKey包含四个操作集,每个操作集用一个Int来表示,int值中的低四位的bit 用于表示channel
支持的可选操作种类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Operation-set bit for read operations.
*/
public static final int OP_READ = 1 << 0;
/**
* Operation-set bit for write operations.
*/
public static final int OP_WRITE = 1 << 2;
/**
* Operation-set bit for socket-connect operations.
*/
public static final int OP_CONNECT = 1 << 3;
/**
* Operation-set bit for socket-accept operations.
*/
public static final int OP_ACCEPT = 1 << 4;
通过interestOps
来确定了selector
在下一个选择操作的过程中将测试哪些操作类别的准备情况,操作事件是否是channel
关注的。interestOps
在SelectionKey
创建时,初始化为注册Selector
时的ops值,这个值可通过sun.nio.ch.SelectionKeyImpl#interestOps(int)
来改变,这点我们在SelectorImpl#register
可以清楚的看到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 //sun.nio.ch.SelectionKeyImpl
public final class SelectionKeyImpl
extends AbstractSelectionKey
{
private static final VarHandle INTERESTOPS =
ConstantBootstraps.fieldVarHandle(
MethodHandles.lookup(),
"interestOps",
VarHandle.class,
SelectionKeyImpl.class, int.class);
private final SelChImpl channel;
private final SelectorImpl selector;
private volatile int interestOps;
private volatile int readyOps;
// registered events in kernel, used by some Selector implementations
private int registeredEvents;
// index of key in pollfd array, used by some Selector implementations
private int index;
SelectionKeyImpl(SelChImpl ch, SelectorImpl sel) {
channel = ch;
selector = sel;
}
...
}
readyOps
表示通过Selector
检测到channel
已经准备就绪的操作事件。在SelectionKey
创建时(即上面源码所示),readyOps
值为0,在Selector
的select
操作中可能会更新,但是需要注意的是我们不能直接调用来更新。
SelectionKey
的readyOps
表示一个channel
已经为某些操作准备就绪,但不能保证在针对这个就绪事件类型的操作过程中不会发生阻塞,即该操作所在线程有可能会发生阻塞。在完成select
操作后,大部分情况下会立即对readyOps
更新,此时readyOps
值最准确,如果外部的事件或在该channel
有IO操作,readyOps
可能不准确。所以,我们有看到其是volatile
类型。
SelectionKey
定义了所有的操作事件,但是具体channel
支持的操作事件依赖于具体的channel
,即具体问题具体分析。
所有可选择的channel
(即SelectableChannel
的子类)都可以通过SelectableChannel#validOps
方法,判断一个操作事件是否被channel
所支持,即每个子类都会有对validOps
的实现,返回一个数字,仅标识channel
支持的哪些操作。尝试设置或测试一个不被channel
所支持的操作设定,将会抛出相关的运行时异常。
不同应用场景下,其所支持的Ops
是不同的,摘取部分如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//java.nio.channels.SocketChannel#validOps
public final int validOps() {
//即1|4|8 1101
return (SelectionKey.OP_READ
| SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT);
}
//java.nio.channels.ServerSocketChannel#validOps
public final int validOps() {
// 16
return SelectionKey.OP_ACCEPT;
}
//java.nio.channels.DatagramChannel#validOps
public final int validOps() {
// 1|4
return (SelectionKey.OP_READ
| SelectionKey.OP_WRITE);
}
如果需要经常关联一些我们程序中指定数据到SelectionKey
,比如一个我们使用一个object表示上层的一种高级协议的状态,object用于通知实现协议处理器。所以,SelectionKey支持通过attach
方法将一个对象附加到SelectionKey
的attachment
上。attachment
可以通过java.nio.channels.SelectionKey#attachment
方法进行访问。如果要取消该对象,则可以通过该种方式:selectionKey.attach(null)
。
需要注意的是如果附加的对象不再使用,一定要人为清除,如果没有,假如此SelectionKey
一直存在,由于此处属于强引用,那么垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。
SelectionKey在由多线程并发使用时,是线程安全的。我们只需要知道,Selector
的select
操作会一直使用在调用该操作开始时当前的interestOps
所设定的值。
到现在为止,我们已经多多少少接触了Selector
,其是一个什么样的角色,想必都很清楚了,那我们就在我们已经接触到的来进一步深入探究Selector
的设计运行机制。
从命名上就可以知道 SelectableChannel
对象是依靠Selector
来实现多路复用的。
我们可以通过调用java.nio.channels.Selector#open
来创建一个selector
对象:1
2
3
4//java.nio.channels.Selector#open
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
关于这个SelectorProvider.provider()
,其使用了根据所在系统的默认实现,我这里是windows系统,那么其默认实现为sun.nio.ch.WindowsSelectorProvider
,这样,就可以调用基于相应系统的具体实现了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//java.nio.channels.spi.SelectorProvider#provider
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
//sun.nio.ch.DefaultSelectorProvider
public class DefaultSelectorProvider {
/**
* Prevent instantiation.
*/
private DefaultSelectorProvider() { }
/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
return new sun.nio.ch.WindowsSelectorProvider();
}
}
基于windows来讲,selector这里最终会使用sun.nio.ch.WindowsSelectorImpl
来做一些核心的逻辑。1
2
3
4
5
6public class WindowsSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}
这里,我们需要来看一下WindowsSelectorImpl
的构造函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14//sun.nio.ch.WindowsSelectorImpl#WindowsSelectorImpl
WindowsSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
pollWrapper = new PollArrayWrapper(INIT_CAP);
wakeupPipe = Pipe.open();
wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
// Disable the Nagle algorithm so that the wakeup is more immediate
SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
(sink.sc).socket().setTcpNoDelay(true);
wakeupSinkFd = ((SelChImpl)sink).getFDVal();
pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}
我们由Pipe.open()
就可知道selector
会保持打开的状态,直到其调用它的close
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49//java.nio.channels.spi.AbstractSelector#close
public final void close() throws IOException {
boolean open = selectorOpen.getAndSet(false);
if (!open)
return;
implCloseSelector();
}
//sun.nio.ch.SelectorImpl#implCloseSelector
public final void implCloseSelector() throws IOException {
wakeup();
synchronized (this) {
implClose();
synchronized (publicSelectedKeys) {
// Deregister channels
Iterator<SelectionKey> i = keys.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
deregister(ski);
SelectableChannel selch = ski.channel();
if (!selch.isOpen() && !selch.isRegistered())
((SelChImpl)selch).kill();
selectedKeys.remove(ski);
i.remove();
}
assert selectedKeys.isEmpty() && keys.isEmpty();
}
}
}
//sun.nio.ch.WindowsSelectorImpl#implClose
protected void implClose() throws IOException {
assert !isOpen();
assert Thread.holdsLock(this);
// prevent further wakeup
synchronized (interruptLock) {
interruptTriggered = true;
}
wakeupPipe.sink().close();
wakeupPipe.source().close();
pollWrapper.free();
// Make all remaining helper threads exit
for (SelectThread t: threads)
t.makeZombie();
startLock.startThreads();
}
可以看到,前面的wakeupPipe
在close方法中关闭掉了。这里的close方法中又涉及了wakeupPipe.sink()
与wakeupPipe.source()
的关闭与pollWrapper.free()
的释放,此处也是我们本篇的难点所在,这里,我们来看看它们到底是什么样的存在。
首先,我们对WindowsSelectorImpl(SelectorProvider sp)
这个构造函数做下梳理:
PollArrayWrapper
对象(pollWrapper
);Pipe.open()
打开一个管道;wakeupSourceFd
和wakeupSinkFd
两个文件描述符;wakeupSourceFd
)放到pollWrapper
里;我们来看Pipe.open()
源码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53//java.nio.channels.Pipe#open
public static Pipe open() throws IOException {
return SelectorProvider.provider().openPipe();
}
//sun.nio.ch.SelectorProviderImpl#openPipe
public Pipe openPipe() throws IOException {
return new PipeImpl(this);
}
//sun.nio.ch.PipeImpl#PipeImpl
PipeImpl(final SelectorProvider sp) throws IOException {
try {
AccessController.doPrivileged(new Initializer(sp));
} catch (PrivilegedActionException x) {
throw (IOException)x.getCause();
}
}
private class Initializer
implements PrivilegedExceptionAction<Void>
{
private final SelectorProvider sp;
private IOException ioe = null;
private Initializer(SelectorProvider sp) {
this.sp = sp;
}
public Void run() throws IOException {
LoopbackConnector connector = new LoopbackConnector();
connector.run();
if (ioe instanceof ClosedByInterruptException) {
ioe = null;
Thread connThread = new Thread(connector) {
public void interrupt() {}
};
connThread.start();
for (;;) {
try {
connThread.join();
break;
} catch (InterruptedException ex) {}
}
Thread.currentThread().interrupt();
}
if (ioe != null)
throw new IOException("Unable to establish loopback connection", ioe);
return null;
}
从上述源码我们可以知道,创建了一个PipeImpl
对象, 在PipeImpl
的构造函数里会执行AccessController.doPrivileged
,在它调用后紧接着会执行Initializer
的run
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70//sun.nio.ch.PipeImpl.Initializer.LoopbackConnector
private class LoopbackConnector implements Runnable {
public void run() {
ServerSocketChannel ssc = null;
SocketChannel sc1 = null;
SocketChannel sc2 = null;
try {
// Create secret with a backing array.
ByteBuffer secret = ByteBuffer.allocate(NUM_SECRET_BYTES);
ByteBuffer bb = ByteBuffer.allocate(NUM_SECRET_BYTES);
// Loopback address
InetAddress lb = InetAddress.getLoopbackAddress();
assert(lb.isLoopbackAddress());
InetSocketAddress sa = null;
for(;;) {
// Bind ServerSocketChannel to a port on the loopback
// address
if (ssc == null || !ssc.isOpen()) {
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(lb, 0));
sa = new InetSocketAddress(lb, ssc.socket().getLocalPort());
}
// Establish connection (assume connections are eagerly
// accepted)
sc1 = SocketChannel.open(sa);
RANDOM_NUMBER_GENERATOR.nextBytes(secret.array());
do {
sc1.write(secret);
} while (secret.hasRemaining());
secret.rewind();
// Get a connection and verify it is legitimate
sc2 = ssc.accept();
do {
sc2.read(bb);
} while (bb.hasRemaining());
bb.rewind();
if (bb.equals(secret))
break;
sc2.close();
sc1.close();
}
// Create source and sink channels
source = new SourceChannelImpl(sp, sc1);
sink = new SinkChannelImpl(sp, sc2);
} catch (IOException e) {
try {
if (sc1 != null)
sc1.close();
if (sc2 != null)
sc2.close();
} catch (IOException e2) {}
ioe = e;
} finally {
try {
if (ssc != null)
ssc.close();
} catch (IOException e2) {}
}
}
}
}
这里即为创建pipe
的过程,windows
下的实现是创建两个本地的socketChannel
,然后连接(连接的过程通过写一个随机数据做两个socket的连接校验),两个socketChannel
分别实现了管道pipe
的source
与sink
端。
而我们依然不清楚这个pipe
到底干什么用的,
假如大家熟悉系统调用的C/C++
的话,就可以知道,一个阻塞在select
上的线程有以下三种方式可以被唤醒:
time out
。non-block
的信号。可由kill
或pthread_kill
发出。所以,Selector.wakeup()
要唤醒阻塞的select
,那么也只能通过这三种方法,其中:
select
一旦阻塞,无法修改其time out
时间。Linux
上实现,Windows
上没有这种信号通知的机制。看来只有第一种方法了。假如我们多次调用Selector.open()
,那么在Windows
上会每调用一次,就会建立一对自己和自己的loopback
的TCP
连接;在Linux上的话,每调用一次,会开一对pipe
(pipe在Linux下一般都成对打开),到这里,估计我们能够猜得出来——那就是如果想要唤醒select
,只需要朝着自己的这个loopback
连接发点数据过去,于是,就可以唤醒阻塞在select
上的线程了。
我们对上面所述做下总结:在Windows
下,Java
虚拟机在Selector.open()
时会自己和自己建立loopback
的TCP
连接;在Linux
下,Selector
会创建pipe
。这主要是为了Selector.wakeup()
可以方便唤醒阻塞在select()
系统调用上的线程(通过向自己所建立的TCP
链接和管道上随便写点什么就可以唤醒阻塞线程)。
在WindowsSelectorImpl
构造器最后,我们看到这一句代码:pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
,即把pipe内Source端的文件描述符(wakeupSourceFd
)放到pollWrapper
里。pollWrapper
作为PollArrayWrapper
的实例,它到底是什么,这一节,我们就来对其探索一番。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37class PollArrayWrapper {
private AllocatedNativeObject pollArray; // The fd array
long pollArrayAddress; // pollArrayAddress
private static final short FD_OFFSET = 0; // fd offset in pollfd
private static final short EVENT_OFFSET = 4; // events offset in pollfd
static short SIZE_POLLFD = 8; // sizeof pollfd struct
private int size; // Size of the pollArray
PollArrayWrapper(int newSize) {
int allocationSize = newSize * SIZE_POLLFD;
pollArray = new AllocatedNativeObject(allocationSize, true);
pollArrayAddress = pollArray.address();
this.size = newSize;
}
...
// Access methods for fd structures
void putDescriptor(int i, int fd) {
pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);
}
void putEventOps(int i, int event) {
pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);
}
...
// Adds Windows wakeup socket at a given index.
void addWakeupSocket(int fdVal, int index) {
putDescriptor(index, fdVal);
putEventOps(index, Net.POLLIN);
}
}
这里将wakeupSourceFd
的POLLIN
事件标识为pollArray
的EventOps
的对应的值,这里使用的是unsafe直接操作的内存,也就是相对于这个pollArray
所在内存地址的偏移量SIZE_POLLFD * i + EVENT_OFFSET
这个位置上写入Net.POLLIN
所代表的值,即参考下面本地方法相关源码所展示的值。putDescriptor
同样是这种类似操作。当sink端
有数据写入时,source
对应的文件描述符wakeupSourceFd
就会处于就绪状态。1
2
3
4
5
6
7
8
9
10
11//java.base/windows/native/libnio/ch/nio_util.h
/* WSAPoll()/WSAPOLLFD and the corresponding constants are only defined */
/* in Windows Vista / Windows Server 2008 and later. If we are on an */
/* older release we just use the Solaris constants as this was previously */
/* done in PollArrayWrapper.java. */
#define POLLIN 0x0001
#define POLLOUT 0x0004
#define POLLERR 0x0008
#define POLLHUP 0x0010
#define POLLNVAL 0x0020
#define POLLCONN 0x0002
AllocatedNativeObject
这个类的父类有大量的unsafe
类的操作,这些都是直接基于内存级别的操作。从其父类的构造器中,我们能也清楚的看到pollArray
是通过unsafe.allocateMemory(size + ps)
分配的一块系统内存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class AllocatedNativeObject // package-private
extends NativeObject
{
/**
* Allocates a memory area of at least {@code size} bytes outside of the
* Java heap and creates a native object for that area.
*/
AllocatedNativeObject(int size, boolean pageAligned) {
super(size, pageAligned);
}
/**
* Frees the native memory area associated with this object.
*/
synchronized void free() {
if (allocationAddress != 0) {
unsafe.freeMemory(allocationAddress);
allocationAddress = 0;
}
}
}
//sun.nio.ch.NativeObject#NativeObject(int, boolean)
protected NativeObject(int size, boolean pageAligned) {
if (!pageAligned) {
this.allocationAddress = unsafe.allocateMemory(size);
this.address = this.allocationAddress;
} else {
int ps = pageSize();
long a = unsafe.allocateMemory(size + ps);
this.allocationAddress = a;
this.address = a + ps - (a & (ps - 1));
}
}
至此,我们算是完成了对Selector.open()
的解读,其主要任务就是完成建立Pipe
,并把pipe
source
端的wakeupSourceFd
放入pollArray
中,这个pollArray
是Selector
完成其角色任务的枢纽。本篇主要围绕Windows的实现来进行分析,即在windows下通过两个连接的socketChannel
实现了Pipe
,linux
下则直接使用系统的pipe
即可。
所谓的注册,其实就是将一个对象放到注册地对象内的一个容器字段上,这个字段可以是数组,队列,也可以是一个set集合,也可以是一个list。这里,同样是这样,只不过,其需要有个返回值,那么把这个要放入集合的对象返回即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36//sun.nio.ch.SelectorImpl#register
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
// register (if needed) before adding to key set
implRegister(k);
// add to the selector's key set, removing it immediately if the selector
// is closed. The key is not in the channel's key set at this point but
// it may be observed by a thread iterating over the selector's key set.
keys.add(k);
try {
k.interestOps(ops);
} catch (ClosedSelectorException e) {
assert ch.keyFor(this) == null;
keys.remove(k);
k.cancel();
throw e;
}
return k;
}
//sun.nio.ch.WindowsSelectorImpl#implRegister
protected void implRegister(SelectionKeyImpl ski) {
ensureOpen();
synchronized (updateLock) {
newKeys.addLast(ski);
}
}
这段代码我们之前已经有看过,这里我们再次温习下。
首先会新建一个SelectionKeyImpl
对象,这个对象就是对Channel
的包装,不仅如此,还顺带把当前这个Selector
对象给收了进去,这样,我们也可以通过SelectionKey
的对象来拿到其对应的Selector
对象。
接着,基于windows
平台实现的implRegister
,先通过ensureOpen()
来确保该Selector
是打开的。接着将这个SelectionKeyImpl
加入到WindowsSelectorImpl
内针对于新注册SelectionKey进行管理的newKeys
之中,newKeys
是一个ArrayDeque
对象。对于ArrayDeque
有不懂的,可以参考Java 容器源码分析之 Deque 与 ArrayDeque这篇文章。
然后再将此这个SelectionKeyImpl
加入到sun.nio.ch.SelectorImpl#keys
中去,这个Set<SelectionKey>
集合代表那些已经注册到当前这个Selector
对象上的SelectionKey
集合。我们来看sun.nio.ch.SelectorImpl
的构造函数:1
2
3
4
5
6
7
8//sun.nio.ch.SelectorImpl#SelectorImpl
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = ConcurrentHashMap.newKeySet();
selectedKeys = new HashSet<>();
publicKeys = Collections.unmodifiableSet(keys);
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
也就是说,这里的publicKeys
就来源于keys
,只是publicKeys
属于只读的,我们想要知道当前Selector
对象上所注册的keys
,就可以调用sun.nio.ch.SelectorImpl#keys
来得到:1
2
3
4
5
6//sun.nio.ch.SelectorImpl#keys
public final Set<SelectionKey> keys() {
ensureOpen();
return publicKeys;
}
再回到这个构造函数中,selectedKeys
,顾名思义,其属于已选择Keys,即前一次操作期间,已经准备就绪的Channel
所对应的SelectionKey
。此集合为keys
的子集。通过selector.selectedKeys()
获取。1
2
3
4
5
6//sun.nio.ch.SelectorImpl#selectedKeys
public final Set<SelectionKey> selectedKeys() {
ensureOpen();
return publicSelectedKeys;
}
我们看到其返回的是publicSelectedKeys
,针对这个字段里的元素操作可以做删除,但不能做增加。
在前面的内容中,我们有涉及到SelectionKey
的取消,所以,我们在java.nio.channels.spi.AbstractSelector
方法内,是有定义cancelledKeys
的,也是一个HashSet
对象。其代表已经被取消但尚未取消注册(deregister)的SelectionKey
。此Set集合无法直接访问,同样,它也是keys()的子集。
对于新的Selector
实例,上面几个集合均为空。由上面展示的源码可知,通过channel.register
将SelectionKey
添加keys
中,此为key的来源。
如果某个selectionKey.cancel()
被调用,那么此key将会被添加到cancelledKeys
这个集合中,然后在下一次调用selector select
方法期间,此时canceldKeys
不为空,将会触发此SelectionKey
的deregister
操作(释放资源,并从keys
中移除)。无论通过channel.close()
还是通过selectionKey.cancel()
,都会导致SelectionKey
被加入到cannceldKey
中.
每次选择操作(select)期间,都可以将key添加到selectedKeys
中或者将从cancelledKeys
中移除。
了解了上面的这些,我们来进入到select
方法中,观察下它的细节。由Selector
的api可知,select
操作有两种形式,一种为
select(),selectNow(),select(long timeout);另一种为select(Consumer<SelectionKey> action, long timeout)
,select(Consumer<SelectionKey> action)
,selectNow(Consumer<SelectionKey> action)
。后者为JDK11新加入的api,主要针对那些准备好进行I/O操作的channels在select过程中对相应的key进行的一个字的自定义的一个操作。
需要注意的是,有Consumer<SelectionKey> action
参数的select操作是阻塞的,只有在选择了至少一个Channel的情况下,才会调用此Selector
实例的wakeup
方法来唤醒,同样,其所在线程被打断也可以。
1 | //sun.nio.ch.SelectorImpl |
我们可以观察,无论哪种,它们最后都落在了lockAndDoSelect
这个方法上,最终会执行特定系统上的doSelect(action, timeout)
实现。
这里我们以sun.nio.ch.WindowsSelectorImpl#doSelect
为例来讲述其操作执行的步骤:
1 | // sun.nio.ch.WindowsSelectorImpl#doSelect |
首先通过相应操作系统实现类(此处是WindowsSelectorImpl)的具体实现我们可以知道,通过<1>
处的 processUpdateQueue()
获得关于每个剩余Channel
(有些Channel取消了)的在此刻的interestOps
,这里包括新注册的和updateKeys
,并对其进行pollWrapper
的管理操作。
即对于新注册的
SelectionKeyImpl
,我们在相对于这个pollArray
所在内存地址的偏移量SIZE_POLLFD * totalChannels + FD_OFFSET
与SIZE_POLLFD * totalChannels + EVENT_OFFSET
分别存入SelectionKeyImpl
的文件描述符fd
与其对应的EventOps
(初始为0)。对
updateKeys
,因为是其之前已经在pollArray
的某个相对位置上存储过,这里我们还需要对拿到的key的有效性进行判断,如果有效,只需要将正在操作的这个SelectionKeyImpl
对象的interestOps
写入到在pollWrapper
中的存放它的EventOps
位置上。注意: 在对
newKeys
进行key的有效性判断之后,如果有效,会调用growIfNeeded()
方法,这里首先会判断channelArray.length == totalChannels
,此为一个SelectionKeyImpl
的数组,初始容量大小为8。channelArray
其实就是方便Selector
管理在册SelectionKeyImpl
数量的一个数组而已,通过判断它的数组长度大小,如果和totalChannels
(初始值为1)相等,不仅仅是为了channelArray
扩容,更重要的是为了辅助pollWrapper
,让pollWrapper
扩容才是这里的目的所在。
而当totalChannels % MAX_SELECTABLE_FDS == 0
时,则多开一个线程处理selector
。windows
上select
系统调用有最大文件描述符限制,一次只能轮询1024
个文件描述符,如果多于1024个,需要多线程进行轮询。通过ski.setIndex(totalChannels)
选择键记录下在数组中的索引位置SelectionKeyImpl
选择键的映射关系,以待后续使用。同时调用pollWrapper.addWakeupSocket(wakeupSourceFd, totalChannels)
在相对于这个pollArray
所在内存地址的偏移量SIZE_POLLFD * totalChannels + FD_OFFSET
这个位置上写入wakeupSourceFd
所代表的fdVal
值。这样在新起的线程就可以通过MAX_SELECTABLE_FDS
来确定这个用来监控的wakeupSourceFd
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 /**
* sun.nio.ch.WindowsSelectorImpl#processUpdateQueue
* Process new registrations and changes to the interest ops.
*/
private void processUpdateQueue() {
assert Thread.holdsLock(this);
synchronized (updateLock) {
SelectionKeyImpl ski;
// new registrations
while ((ski = newKeys.pollFirst()) != null) {
if (ski.isValid()) {
growIfNeeded();
channelArray[totalChannels] = ski;
ski.setIndex(totalChannels);
pollWrapper.putEntry(totalChannels, ski);
totalChannels++;
MapEntry previous = fdMap.put(ski);
assert previous == null;
}
}
// changes to interest ops
while ((ski = updateKeys.pollFirst()) != null) {
int events = ski.translateInterestOps();
int fd = ski.getFDVal();
if (ski.isValid() && fdMap.containsKey(fd)) {
int index = ski.getIndex();
assert index >= 0 && index < totalChannels;
pollWrapper.putEventOps(index, events);
}
}
}
}
//sun.nio.ch.PollArrayWrapper#putEntry
// Prepare another pollfd struct for use.
void putEntry(int index, SelectionKeyImpl ski) {
putDescriptor(index, ski.getFDVal());
putEventOps(index, 0);
}
//sun.nio.ch.WindowsSelectorImpl#growIfNeeded
private void growIfNeeded() {
if (channelArray.length == totalChannels) {
int newSize = totalChannels * 2; // Make a larger array
SelectionKeyImpl temp[] = new SelectionKeyImpl[newSize];
System.arraycopy(channelArray, 1, temp, 1, totalChannels - 1);
channelArray = temp;
pollWrapper.grow(newSize);
}
if (totalChannels % MAX_SELECTABLE_FDS == 0) { // more threads needed
pollWrapper.addWakeupSocket(wakeupSourceFd, totalChannels);
totalChannels++;
threadsCount++;
}
}
// Initial capacity of the poll array
private final int INIT_CAP = 8;
// Maximum number of sockets for select().
// Should be INIT_CAP times a power of 2
private static final int MAX_SELECTABLE_FDS = 1024;
// The list of SelectableChannels serviced by this Selector. Every mod
// MAX_SELECTABLE_FDS entry is bogus, to align this array with the poll
// array, where the corresponding entry is occupied by the wakeupSocket
private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[INIT_CAP];
// The number of valid entries in poll array, including entries occupied
// by wakeup socket handle.
private int totalChannels = 1;
//sun.nio.ch.PollArrayWrapper#grow
// Grows the pollfd array to new size
void grow(int newSize) {
PollArrayWrapper temp = new PollArrayWrapper(newSize);
for (int i = 0; i < size; i++)
replaceEntry(this, i, temp, i);
pollArray.free();
pollArray = temp.pollArray;
this.size = temp.size;
pollArrayAddress = pollArray.address();
}
// Maps file descriptors to their indices in pollArray
private static final class FdMap extends HashMap<Integer, MapEntry> {
static final long serialVersionUID = 0L;
private MapEntry get(int desc) {
return get(Integer.valueOf(desc));
}
private MapEntry put(SelectionKeyImpl ski) {
return put(Integer.valueOf(ski.getFDVal()), new MapEntry(ski));
}
private MapEntry remove(SelectionKeyImpl ski) {
Integer fd = Integer.valueOf(ski.getFDVal());
MapEntry x = get(fd);
if ((x != null) && (x.ski.channel() == ski.channel()))
return remove(fd);
return null;
}
}
// class for fdMap entries
private static final class MapEntry {
final SelectionKeyImpl ski;
long updateCount = 0;
MapEntry(SelectionKeyImpl ski) {
this.ski = ski;
}
}
private final FdMap fdMap = new FdMap();
上面WindowsSelectorImpl#doSelect展示源码中<2>
处的 processDeregisterQueue()
。cancelledKeys
进行清除,遍历cancelledKeys
,并对每个key
进行deregister
操作,然后从cancelledKeys
集合中删除,从keys
集合与selectedKeys
中删除,以此来释放引用,方便gc回收,implDereg
方法,将会从channelArray
中移除对应的Channel
代表的SelectionKeyImpl
,调整totalChannels
和线程数,从map
和keys
中移除SelectionKeyImpl
,移除Channel
上的SelectionKeyImpl
并关闭Channel
。processDeregisterQueue()
方法在调用poll
方法前后都进行调用,这是确保能够正确处理在调用poll
方法阻塞的这一段时间之内取消的键能被及时清理。cancelledKey
所代表的channel
是否打开和解除注册,如果关闭并解除注册,则应该将相应的文件描述符对应占用的资源给关闭掉。1 | /** |
上面WindowsSelectorImpl#doSelect
展示源码中adjustThreadsCount()
方法的调用。totalChannels % MAX_SELECTABLE_FDS == 0
,则多开一个线程处理selector
。这里就是根据分配的线程数量值来增加或减少线程,其实就是针对操作系统的最大select
操作的文件描述符限制对线程个数进行调整。SelectThread
的run
方法实现。通过观察其源码可以看到它首先是while (true)
,通过startLock.waitForStart(this)
来控制该线程是否运行还是等待,运行状态的话,会进而调用subSelector.poll(index)
(这个我们后面内容详细解读),poll
结束,而且相对于当前主线程假如有多条SelectThread
子线程的话,当前这条SelectThread
线程第一个结束poll
的话,就调用finishLock.threadFinished()
来通知主线程。在刚新建这个线程并调用其run
方法的时候,此时lastRun = 0
,在第一次启动的时候sun.nio.ch.WindowsSelectorImpl.StartLock#runsCounter
同样为0,所以会调用startLock.wait()
进而进入等待状态。 注意:
sun.nio.ch.WindowsSelectorImpl.StartLock
同样会判断当前其所检测的线程是否废弃,废弃的话就返回true
,这样被检测线程也就能跳出其内run方法的while
循环从而结束线程运行。- 在调整线程的时候(调用
adjustThreadsCount
方法)与Selector
调用close
方法会间接调用到sun.nio.ch.WindowsSelectorImpl#implClose
,这两个方法都会涉及到Selector
线程的释放,即调用sun.nio.ch.WindowsSelectorImpl.SelectThread#makeZombie
。finishLock.threadFinished()
会调用wakeup()
方法来通知主线程,这里,我们可以学到一个细节,如果线程正阻塞在select
方法上,就可以调用wakeup
方法会使阻塞的选择操作立即返回,通过Windows
的相关实现,原理其实是向pipe
的sink
端写入了一个字节,source
文件描述符就会处于就绪状态,poll
方法会返回,从而导致select
方法返回。而在其他solaris或者linux系统上其实采用系统调用pipe
来完成管道的创建,相当于直接用了系统的管道。通过wakeup()
相关实现还可以看出,调用wakeup
会设置interruptTriggered
的标志位,所以连续多次调用wakeup
的效果等同于一次调用,不会引起无所谓的bug出现。
1 | //sun.nio.ch.WindowsSelectorImpl#adjustThreadsCount |
subSelector.poll()
是select的核心,由native
函数poll0
实现,并把pollWrapper.pollArrayAddress
作为参数传给poll0
,readFds
、writeFds
和exceptFds
数组用来保存底层select
的结果,数组的第一个位置都是存放发生事件的socket
的总数,其余位置存放发生事件的socket
句柄fd
。poll0()
会监听pollWrapper
中的FD
有没有数据进出,这里会造成IO
阻塞,直到有数据读写事件发生。由于pollWrapper
中保存的也有ServerSocketChannel
的FD
,所以只要ClientSocket
发一份数据到ServerSocket
,那么poll0()
就会返回;又由于pollWrapper
中保存的也有pipe
的write
端的FD
,所以只要pipe
的write
端向FD
发一份数据,也会造成poll0()
返回;如果这两种情况都没有发生,那么poll0()
就一直阻塞,也就是selector.select()
会一直阻塞;如果有任何一种情况发生,那么selector.select()
就会返回,所有在SelectThread
的run()
里要用while (true) {}
,这样就可以保证在selector
接收到数据并处理完后继续监听poll()
;可以看出,NIO依然是阻塞式的IO,那么它和BIO的区别究竟在哪呢。
其实它的区别在于阻塞的位置不同,BIO
是阻塞在read
方法(recvfrom),而NIO
阻塞在select
方法。那么这样做有什么好处呢。如果单纯的改变阻塞的位置,自然是没有什么变化的,但epoll等
的实现的巧妙之处就在于,它利用回调机制,让监听能够只需要知晓哪些socket
上的数据已经准备好了,只需要处理这些线程上面的数据就行了。采用BIO
,假设有1000
个连接,需要开1000
个线程,然后有1000
个read
的位置在阻塞(我们在讲解BIO部分已经通过Demo体现),采用NIO
编程,只需要1个线程,它利用select
的轮询策略配合epoll
的事件机制及红黑树数据结构,降低了其内部轮询的开销,同时极大的减小了线程上下文切换的开销。
1 | //sun.nio.ch.WindowsSelectorImpl.SubSelector |
上面WindowsSelectorImpl#doSelect展示源码中<5>
处的 updateSelectedKeys(action)
来处理每个channel
的 准备就绪的信息。key
尚未在selectedKeys
中存在,则将其添加到该集合中。key
已经存在selectedKeys
中,即这个channel
存在所支持的ReadyOps
就绪操作中必须包含一个这种操作(由(ski.nioReadyOps() & ski.nioInterestOps()) != 0
来确定),此时修改其ReadyOps
为当前所要进行的操作。而我们之前看到的Consumer<SelectionKey>
这个动作也是在此处进行。而由下面源码可知,先前记录在ReadyOps
中的任何就绪信息在调用此action
之前被丢弃掉,直接进行设定。 1 | //sun.nio.ch.WindowsSelectorImpl#updateSelectedKeys |
至此,关于Selector的内容就暂时告一段落,在下一篇中,我会针对Java NIO Buffer进行相关解读。
]]>此系列文章会详细解读NIO的功能逐步丰满的路程,为Reactor-Netty 库的讲解铺平道路。
关于Java编程方法论-Reactor与Webflux的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
本系列源码解读基于JDK11 api细节可能与其他版本有所差别,请自行解决jdk版本问题。
我们最初的目的就是为了增强Socket,基于这个基本需求,没有条件创造条件,于是为了让Channel拥有网络socket的能力,这里定义了一个java.nio.channels.NetworkChannel
接口。花不多说,我们来看这个接口的定义:1
2
3
4
5
6
7
8
9
10
11
12public interface NetworkChannel extends Channel
{
NetworkChannel bind(SocketAddress local) throws IOException;
SocketAddress getLocalAddress() throws IOException;
<T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException;
<T> T getOption(SocketOption<T> name) throws IOException;
Set<SocketOption<?>> supportedOptions();
}
通过bind(SocketAddress)
方法将socket
绑定到本地 SocketAddress
上,通过getLocalAddress()方法返回socket
绑定的地址,
通过 setOption(SocketOption,Object)
和getOption(SocketOption)
方法设置和查询socket
支持的配置选项。
接下来我们来看 java.nio.channels.ServerSocketChannel
抽象类及其实现类sun.nio.ch.ServerSocketChannelImpl
对之实现的细节。
首先我们来看其对于bind的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//sun.nio.ch.ServerSocketChannelImpl#bind
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
synchronized (stateLock) {
ensureOpen();
//通过localAddress判断是否已经调用过bind
if (localAddress != null)
throw new AlreadyBoundException();
//InetSocketAddress(0)表示绑定到本机的所有地址,由操作系统选择合适的端口
InetSocketAddress isa = (local == null)
? new InetSocketAddress(0)
: Net.checkAddress(local);
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkListen(isa.getPort());
NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort());
Net.bind(fd, isa.getAddress(), isa.getPort());
//开启监听,s如果参数backlog小于1,默认接受50个连接
Net.listen(fd, backlog < 1 ? 50 : backlog);
localAddress = Net.localAddress(fd);
}
return this;
}
下面我们来看看Net中的bind和listen方法是如何实现的。
1 | //sun.nio.ch.Net#bind(java.io.FileDescriptor, java.net.InetAddress, int) |
bind0为native方法实现:
1 | JNIEXPORT void JNICALL |
socket是用户程序与内核交互信息的枢纽,它自身没有网络协议地址和端口号等信息,在进行网络通信的时候,必须把一个socket与一个地址相关联。
很多时候内核会我们自动绑定一个地址,然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要;
最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。
对于客户端,很多时候并不需要调用bind方法,而是由内核自动绑定;
这里要注意,绑定归绑定,在有连接过来的时候会创建一个新的Socket,然后服务端操作这个新的Socket即可。这里就可以关注accept方法了。由sun.nio.ch.ServerSocketChannelImpl#bind
最后,我们知道其通过Net.listen(fd, backlog < 1 ? 50 : backlog)
开启监听,如果参数backlog小于1,默认接受50个连接。由此,我们来关注下Net.listen
方法细节。
1 | //sun.nio.ch.Net#listen |
可以知道,Net.listen
是native
方法,源码如下:1
2
3
4
5
6JNIEXPORT void JNICALL
Java_sun_nio_ch_Net_listen(JNIEnv *env, jclass cl, jobject fdo, jint backlog)
{
if (listen(fdval(env, fdo), backlog) < 0)
handleSocketError(env, errno);
}
可以看到底层是调用listen
实现的,listen
函数在一般在调用bind
之后到调用accept
之前调用,它的函数原型是:int listen(int sockfd, int backlog)
返回值:0表示成功, -1表示失败
我们再来关注下bind操作中的其他细节,最开始时的ensureOpen()
方法判断:1
2
3
4
5
6
7
8
9
10//sun.nio.ch.ServerSocketChannelImpl#ensureOpen
// @throws ClosedChannelException if channel is closed
private void ensureOpen() throws ClosedChannelException {
if (!isOpen())
throw new ClosedChannelException();
}
//java.nio.channels.spi.AbstractInterruptibleChannel#isOpen
public final boolean isOpen() {
return !closed;
}
如果socket
关闭,则抛出ClosedChannelException
。
我们再来看下Net#checkAddress
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//sun.nio.ch.Net#checkAddress(java.net.SocketAddress)
public static InetSocketAddress checkAddress(SocketAddress sa) {
if (sa == null)//地址为空
throw new NullPointerException();
//非InetSocketAddress类型地址
if (!(sa instanceof InetSocketAddress))
throw new UnsupportedAddressTypeException(); // ## needs arg
InetSocketAddress isa = (InetSocketAddress)sa;
//地址不可识别
if (isa.isUnresolved())
throw new UnresolvedAddressException(); // ## needs arg
InetAddress addr = isa.getAddress();
//非ip4和ip6地址
if (!(addr instanceof Inet4Address || addr instanceof Inet6Address))
throw new IllegalArgumentException("Invalid address type");
return isa;
}
从上面可以看出,bind首先检查ServerSocket
是否关闭,是否绑定地址, 如果既没有绑定也没关闭,则检查绑定的socketaddress
是否正确或合法; 然后通过Net工具类的bind
和listen
,完成实际的ServerSocket
地址绑定和开启监听,如果绑定是开启的参数小于1
,则默认接受50
个连接。
对照我们之前在第一篇中接触的BIO,我们来看些accept()
方法的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48//sun.nio.ch.ServerSocketChannelImpl#accept()
public SocketChannel accept() throws IOException {
acceptLock.lock();
try {
int n = 0;
FileDescriptor newfd = new FileDescriptor();
InetSocketAddress[] isaa = new InetSocketAddress[1];
boolean blocking = isBlocking();
try {
begin(blocking);
do {
n = accept(this.fd, newfd, isaa);
} while (n == IOStatus.INTERRUPTED && isOpen());
} finally {
end(blocking, n > 0);
assert IOStatus.check(n);
}
if (n < 1)
return null;
//针对接受连接的处理通道socketchannelimpl,默认为阻塞模式
// newly accepted socket is initially in blocking mode
IOUtil.configureBlocking(newfd, true);
InetSocketAddress isa = isaa[0];
//构建SocketChannelImpl,这个具体在SocketChannelImpl再说
SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);
// check permitted to accept connections from the remote address
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
try {
//检查地址和port权限
sm.checkAccept(isa.getAddress().getHostAddress(), isa.getPort());
} catch (SecurityException x) {
sc.close();
throw x;
}
}
//返回socketchannelimpl
return sc;
} finally {
acceptLock.unlock();
}
}
对于accept(this.fd, newfd, isaa)
,调用accept接收socket中已建立的连接,我们之前有在BIO中了解过,函数最终会调用:int accept(int sockfd,struct sockaddr addr, socklen_t addrlen);
这里begin(blocking);
与 end(blocking, n > 0);
的合作模式我们在InterruptibleChannel 与可中断 IO这一篇文章中已经涉及过,这里再次提一下,让大家看到其应用,此处专注的是等待连接这个过程,期间可以出现异常打断,这个过程正常结束的话,就会正常往下执行逻辑,不要搞的好像这个Channel要结束了一样,end(blocking, n > 0)
的第二个参数completed也只是在判断这个等待过程是否结束而已,不要功能范围扩大化。
我们再来看下NetworkChannel
的其他方法实现,首先来看supportedOptions
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//sun.nio.ch.ServerSocketChannelImpl#supportedOptions
public final Set<SocketOption<?>> supportedOptions() {
return DefaultOptionsHolder.defaultOptions;
}
//sun.nio.ch.ServerSocketChannelImpl.DefaultOptionsHolder
private static class DefaultOptionsHolder {
static final Set<SocketOption<?>> defaultOptions = defaultOptions();
private static Set<SocketOption<?>> defaultOptions() {
HashSet<SocketOption<?>> set = new HashSet<>();
set.add(StandardSocketOptions.SO_RCVBUF);
set.add(StandardSocketOptions.SO_REUSEADDR);
if (Net.isReusePortAvailable()) {
set.add(StandardSocketOptions.SO_REUSEPORT);
}
set.add(StandardSocketOptions.IP_TOS);
set.addAll(ExtendedSocketOptions.options(SOCK_STREAM));
//返回不可修改的HashSet
return Collections.unmodifiableSet(set);
}
}
对上述配置中的一些配置我们大致来瞅眼:1
2
3
4
5
6
7
8
9
10
11
12
13//java.net.StandardSocketOptions
//socket接受缓存大小
public static final SocketOption<Integer> SO_RCVBUF =
new StdSocketOption<Integer>("SO_RCVBUF", Integer.class);
//是否可重用地址
public static final SocketOption<Boolean> SO_REUSEADDR =
new StdSocketOption<Boolean>("SO_REUSEADDR", Boolean.class);
//是否可重用port
public static final SocketOption<Boolean> SO_REUSEPORT =
new StdSocketOption<Boolean>("SO_REUSEPORT", Boolean.class);
//Internet协议(IP)标头(header)中的服务类型(ToS)。
public static final SocketOption<Integer> IP_TOS =
new StdSocketOption<Integer>("IP_TOS", Integer.class);
知道了上面的支持配置,我们来看下setOption
实现细节:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//sun.nio.ch.ServerSocketChannelImpl#setOption
public <T> ServerSocketChannel setOption(SocketOption<T> name, T value)
throws IOException
{
Objects.requireNonNull(name);
if (!supportedOptions().contains(name))
throw new UnsupportedOperationException("'" + name + "' not supported");
synchronized (stateLock) {
ensureOpen();
if (name == StandardSocketOptions.IP_TOS) {
ProtocolFamily family = Net.isIPv6Available() ?
StandardProtocolFamily.INET6 : StandardProtocolFamily.INET;
Net.setSocketOption(fd, family, name, value);
return this;
}
if (name == StandardSocketOptions.SO_REUSEADDR && Net.useExclusiveBind()) {
// SO_REUSEADDR emulated when using exclusive bind
isReuseAddress = (Boolean)value;
} else {
// no options that require special handling
Net.setSocketOption(fd, Net.UNSPEC, name, value);
}
return this;
}
}
这里,大家就能看到supportedOptions().contains(name)
的作用了,首先会进行支持配置的判断,然后进行正常的设置逻辑。里面对于Socket配置设定主要执行了Net.setSocketOption
,这里,就只对其代码做中文注释就好,整个逻辑过程没有太复杂的。
1 | static void setSocketOption(FileDescriptor fd, ProtocolFamily family, |
接下来,我们来看getOption
实现,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54//sun.nio.ch.ServerSocketChannelImpl#getOption
"unchecked") (
public <T> T getOption(SocketOption<T> name)
throws IOException
{
Objects.requireNonNull(name);
//非通道支持选项,则抛出UnsupportedOperationException
if (!supportedOptions().contains(name))
throw new UnsupportedOperationException("'" + name + "' not supported");
synchronized (stateLock) {
ensureOpen();
if (name == StandardSocketOptions.SO_REUSEADDR && Net.useExclusiveBind()) {
// SO_REUSEADDR emulated when using exclusive bind
return (T)Boolean.valueOf(isReuseAddress);
}
//假如获取的不是上面的配置,则委托给Net来处理
// no options that require special handling
return (T) Net.getSocketOption(fd, Net.UNSPEC, name);
}
}
//sun.nio.ch.Net#getSocketOption
static Object getSocketOption(FileDescriptor fd, ProtocolFamily family,
SocketOption<?> name)
throws IOException
{
Class<?> type = name.type();
if (extendedOptions.isOptionSupported(name)) {
return extendedOptions.getOption(fd, name);
}
//只支持整形和布尔型,否则抛出断言错误
// only simple values supported by this method
if (type != Integer.class && type != Boolean.class)
throw new AssertionError("Should not reach here");
// map option name to platform level/name
OptionKey key = SocketOptionRegistry.findOption(name, family);
if (key == null)
throw new AssertionError("Option not found");
boolean mayNeedConversion = (family == UNSPEC);
//获取文件描述的选项配置
int value = getIntOption0(fd, mayNeedConversion, key.level(), key.name());
if (type == Integer.class) {
return Integer.valueOf(value);
} else {
//我们要看到前面支持配置处的源码其支持的类型要么是Boolean,要么是Integer
//所以,返回值为Boolean.FALSE 或 Boolean.TRUE也就不足为奇了
return (value == 0) ? Boolean.FALSE : Boolean.TRUE;
}
}
在Net.bind一节中,我们最后说了一个注意点,每个连接过来的时候都会创建一个Socket来供此连接进行操作,这个在accept方法中可以看到,其在得到连接之后,就 new SocketChannelImpl(provider(), newfd, isa)
这个对象。那这里,就引出一个话题,我们在使用bind方法的时候,是不是也应该绑定到一个Socket之上呢,那之前bio是怎么做呢,我们先来回顾一下。
我们之前在调用java.net.ServerSocket#ServerSocket(int, int, java.net.InetAddress)
方法的时候,里面有一个setImpl()
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31//java.net.ServerSocket
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
//java.net.ServerSocket#setImpl
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
// No need to do a checkOldImpl() here, we know it's an up to date
// SocketImpl!
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}
但是,我们此处的重点在bind(new InetSocketAddress(bindAddr, port), backlog);
,这里的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31//java.net.ServerSocket
public void bind(SocketAddress endpoint, int backlog) throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!oldImpl && isBound())
throw new SocketException("Already bound");
if (endpoint == null)
endpoint = new InetSocketAddress(0);
if (!(endpoint instanceof InetSocketAddress))
throw new IllegalArgumentException("Unsupported address type");
InetSocketAddress epoint = (InetSocketAddress) endpoint;
if (epoint.isUnresolved())
throw new SocketException("Unresolved address");
if (backlog < 1)
backlog = 50;
try {
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkListen(epoint.getPort());
//重点!!
getImpl().bind(epoint.getAddress(), epoint.getPort());
getImpl().listen(backlog);
bound = true;
} catch(SecurityException e) {
bound = false;
throw e;
} catch(IOException e) {
bound = false;
throw e;
}
}
我们有看到 getImpl()
我标示了重点,这里面做了什么,我们走进去:1
2
3
4
5
6//java.net.ServerSocket#getImpl
SocketImpl getImpl() throws SocketException {
if (!created)
createImpl();
return impl;
}
在整个过程中created
还是对象刚创建时的初始值,为false,那么,铁定会进入createImpl()
方法中:1
2
3
4
5
6
7
8
9
10
11//java.net.ServerSocket#createImpl
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
而此处,因为前面impl
已经赋值,所以,会走impl.create(true)
,进而将created
设定为true
。而此刻,终于到我想讲的重点了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//java.net.AbstractPlainSocketImpl#create
protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
ResourceManager.beforeUdpCreate();
// only create the fd after we know we will be able to create the socket
fd = new FileDescriptor();
try {
socketCreate(false);
SocketCleanable.register(fd);
} catch (IOException ioe) {
ResourceManager.afterUdpClose();
fd = null;
throw ioe;
}
} else {
fd = new FileDescriptor();
socketCreate(true);
SocketCleanable.register(fd);
}
if (socket != null)
socket.setCreated();
if (serverSocket != null)
serverSocket.setCreated();
}
可以看到,socketCreate(true);
,它的实现如下:1
2
3
4
5
6
7
8
9
void socketCreate(boolean stream) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");
int newfd = socket0(stream);
fdAccess.set(fd, newfd);
}
通过本地方法socket0(stream)
得到了一个文件描述符,由此,Socket创建了出来,然后进行相应的绑定。
我们再把眼光放回到sun.nio.ch.ServerSocketChannelImpl#accept()
中,这里new的SocketChannelImpl
对象是得到连接之后做的事情,那对于服务器来讲,绑定时候用的Socket呢,这里,我们在使用ServerSocketChannel
的时候,往往要使用JDK给我们提供的对我统一的方法open
,也是为了降低我们使用的复杂度,这里是java.nio.channels.ServerSocketChannel#open
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//java.nio.channels.ServerSocketChannel#open
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
//sun.nio.ch.SelectorProviderImpl#openServerSocketChannel
public ServerSocketChannel openServerSocketChannel() throws IOException {
return new ServerSocketChannelImpl(this);
}
//sun.nio.ch.ServerSocketChannelImpl#ServerSocketChannelImpl(SelectorProvider)
ServerSocketChannelImpl(SelectorProvider sp) throws IOException {
super(sp);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(fd);
}
//sun.nio.ch.Net#serverSocket
static FileDescriptor serverSocket(boolean stream) {
return IOUtil.newFD(socket0(isIPv6Available(), stream, true, fastLoopback));
}
可以看到,只要new了一个ServerSocketChannelImpl对象,就相当于拿到了一个socket
然后bind也就有着落了。但是,我们要注意下细节ServerSocketChannel#open
得到的是ServerSocketChannel
类型。我们accept到一个客户端来的连接后,应该在客户端与服务器之间创建一个Socket通道来供两者通信操作的,所以,sun.nio.ch.ServerSocketChannelImpl#accept()
中所做的是SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);
,得到的是SocketChannel
类型的对象,这样,就可以将Socket的读写数据的方法定义在这个类里面。
关于ServerSocketChannel
,我们还有方法需要接触一下,如socket():1
2
3
4
5
6
7
8
9//sun.nio.ch.ServerSocketChannelImpl#socket
public ServerSocket socket() {
synchronized (stateLock) {
if (socket == null)
socket = ServerSocketAdaptor.create(this);
return socket;
}
}
我们看到了ServerSocketAdaptor
,我们通过此类的注释可知,这是一个和ServerSocket
调用一样,但是底层是用ServerSocketChannelImpl
来实现的一个类,其适配是的目的是适配我们使用ServerSocket
的方式,所以该ServerSocketAdaptor
继承ServerSocket
并按顺序重写了它的方法,所以,我们在写这块儿代码的时候也就有了新的选择。
InterruptibleChannel 与可中断 IO这一篇文章中已经涉及过java.nio.channels.spi.AbstractInterruptibleChannel#close
的实现,这里,我们再来回顾下其中的某些细节,顺带引出我们新的话题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99//java.nio.channels.spi.AbstractInterruptibleChannel#close
public final void close() throws IOException {
synchronized (closeLock) {
if (closed)
return;
closed = true;
implCloseChannel();
}
}
//java.nio.channels.spi.AbstractSelectableChannel#implCloseChannel
protected final void implCloseChannel() throws IOException {
implCloseSelectableChannel();
// clone keys to avoid calling cancel when holding keyLock
SelectionKey[] copyOfKeys = null;
synchronized (keyLock) {
if (keys != null) {
copyOfKeys = keys.clone();
}
}
if (copyOfKeys != null) {
for (SelectionKey k : copyOfKeys) {
if (k != null) {
k.cancel(); // invalidate and adds key to cancelledKey set
}
}
}
}
//sun.nio.ch.ServerSocketChannelImpl#implCloseSelectableChannel
protected void implCloseSelectableChannel() throws IOException {
assert !isOpen();
boolean interrupted = false;
boolean blocking;
// set state to ST_CLOSING
synchronized (stateLock) {
assert state < ST_CLOSING;
state = ST_CLOSING;
blocking = isBlocking();
}
// wait for any outstanding accept to complete
if (blocking) {
synchronized (stateLock) {
assert state == ST_CLOSING;
long th = thread;
if (th != 0) {
//本地线程不为null,则本地Socket预先关闭
//并通知线程通知关闭
nd.preClose(fd);
NativeThread.signal(th);
// wait for accept operation to end
while (thread != 0) {
try {
stateLock.wait();
} catch (InterruptedException e) {
interrupted = true;
}
}
}
}
} else {
// non-blocking mode: wait for accept to complete
acceptLock.lock();
acceptLock.unlock();
}
// set state to ST_KILLPENDING
synchronized (stateLock) {
assert state == ST_CLOSING;
state = ST_KILLPENDING;
}
// close socket if not registered with Selector
//如果未在Selector上注册,直接kill掉
//即关闭文件描述
if (!isRegistered())
kill();
// restore interrupt status
//印证了我们上一篇中在异步打断中若是通过线程的中断方法中断线程的话
//最后要设定该线程状态是interrupt
if (interrupted)
Thread.currentThread().interrupt();
}
public void kill() throws IOException {
synchronized (stateLock) {
if (state == ST_KILLPENDING) {
state = ST_KILLED;
nd.close(fd);
}
}
}
也是因为close()
并没有在InterruptibleChannel 与可中断 IO这一篇文章中进行具体的讲解应用,这里其应用的更多是在SocketChannel
这里,其更多的涉及到客户端与服务端建立连接交换数据,所以断开连接后,将不用的Channel关闭是很正常的。
这里,在sun.nio.ch.ServerSocketChannelImpl#accept()
中的源码中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public SocketChannel accept() throws IOException {
...
// newly accepted socket is initially in blocking mode
IOUtil.configureBlocking(newfd, true);
InetSocketAddress isa = isaa[0];
SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);
// check permitted to accept connections from the remote address
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
try {
sm.checkAccept(isa.getAddress().getHostAddress(), isa.getPort());
} catch (SecurityException x) {
sc.close();
throw x;
}
}
return sc;
} finally {
acceptLock.unlock();
}
}
这里通过对所接收的连接的远程地址做合法性判断,假如验证出现异常,则关闭上面创建的SocketChannel
。
还有一个关于close()的实际用法,在客户端建立连接的时候,如果连接出异常,同样是要关闭所创建的Socket:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//java.nio.channels.SocketChannel#open(java.net.SocketAddress)
public static SocketChannel open(SocketAddress remote)
throws IOException
{
SocketChannel sc = open();
try {
sc.connect(remote);
} catch (Throwable x) {
try {
sc.close();
} catch (Throwable suppressed) {
x.addSuppressed(suppressed);
}
throw x;
}
assert sc.isConnected();
return sc;
}
接着,我们在implCloseSelectableChannel
中会发现nd.preClose(fd);
与nd.close(fd);
,这个在SocketChannelImpl
与ServerSocketChannelImpl
两者对于implCloseSelectableChannel
实现中都可以看到,这个nd是什么,这里,我们拿ServerSocketChannelImpl
来讲,在这个类的最后面有一段静态代码块(SocketChannelImpl
同理),也就是在这个类加载的时候就会执行:1
2
3
4
5
6
7//C:/Program Files/Java/jdk-11.0.1/lib/src.zip!/java.base/sun/nio/ch/ServerSocketChannelImpl.java:550
static {
//加载nio,net资源库
IOUtil.load();
initIDs();
nd = new SocketDispatcher();
}
也就是说,在ServerSocketChannelImpl
这个类字节码加载的时候,就会创建SocketDispatcher
对象。通过SocketDispatcher
允许在不同的平台调用不同的本地方法进行读写操作,然后基于这个类,我们就可以在sun.nio.ch.SocketChannelImpl
做Socket的I/O操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49//sun.nio.ch.SocketDispatcher
class SocketDispatcher extends NativeDispatcher
{
static {
IOUtil.load();
}
//读操作
int read(FileDescriptor fd, long address, int len) throws IOException {
return read0(fd, address, len);
}
long readv(FileDescriptor fd, long address, int len) throws IOException {
return readv0(fd, address, len);
}
//写操作
int write(FileDescriptor fd, long address, int len) throws IOException {
return write0(fd, address, len);
}
long writev(FileDescriptor fd, long address, int len) throws IOException {
return writev0(fd, address, len);
}
//预关闭文件描述符
void preClose(FileDescriptor fd) throws IOException {
preClose0(fd);
}
//关闭文件描述
void close(FileDescriptor fd) throws IOException {
close0(fd);
}
//-- Native methods
static native int read0(FileDescriptor fd, long address, int len)
throws IOException;
static native long readv0(FileDescriptor fd, long address, int len)
throws IOException;
static native int write0(FileDescriptor fd, long address, int len)
throws IOException;
static native long writev0(FileDescriptor fd, long address, int len)
throws IOException;
static native void preClose0(FileDescriptor fd) throws IOException;
static native void close0(FileDescriptor fd) throws IOException;
}
我们有看到FileDescriptor
在前面代码中有大量的出现,这里,我们对它来专门介绍。通过FileDescriptor 这个类的实例来充当底层机器特定结构的不透明处理,表示打开文件,打开socket或其他字节源或接收器。
文件描述符的主要用途是创建一个 FileInputStream或 FileOutputStream来包含它。
注意: 应用程序不应创建自己的文件描述符。
我们来看其部分源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47public final class FileDescriptor {
private int fd;
private long handle;
private Closeable parent;
private List<Closeable> otherParents;
private boolean closed;
/**
* true, if file is opened for appending.
*/
private boolean append;
static {
initIDs();
}
/**
* 在未明确关闭FileDescriptor的情况下进行清理.
*/
private PhantomCleanable<FileDescriptor> cleanup;
/**
* 构造一个无效的FileDescriptor对象,fd或handle会在之后进行设定
*/
public FileDescriptor() {
fd = -1;
handle = -1;
}
/**
* Used for standard input, output, and error only.
* For Windows the corresponding handle is initialized.
* For Unix the append mode is cached.
* 仅用于标准输入,输出和错误。
* 对于Windows,初始化相应的句柄。
* 对于Unix,缓存附加模式。
* @param fd the raw fd number (0, 1, 2)
*/
private FileDescriptor(int fd) {
this.fd = fd;
this.handle = getHandle(fd);
this.append = getAppend(fd);
}
...
}
我们平时所用的标准输入,输出,错误流的句柄可以如下,通常,我们不会直接使用它们,而是使用java.lang.System.in
,java.lang.System#out
,java.lang.System#err
:1
2
3public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);
测试该文件描述符是否有效可以使用如下方法:1
2
3
4//java.io.FileDescriptor#valid
public boolean valid() {
return (handle != -1) || (fd != -1);
}
返回值为true的话,那么这个文件描述符对象所代表的socket
文件操作
或其他活动的网络连接都是有效的,反之,false则是无效。
更多内容,读者可以自行深入源码,此处就不过多解释了。为了让大家可以更好的理解上述内容,我们会在后面的部分还要进一步涉及一下。
在前面,我们已经接触了SocketChannel
,这里,来接触下细节。
同样,我们也可以通过调用此类的open
方法来创建socket channel
。这里需要注意:
socket
创建channel
。socket channel
已打开但尚未连接。channel
上调用I/O
操作将导致抛出NotYetConnectedException
。connect
方法连接socket channel
;socket channel
会保持连接状态,直到它关闭。socket channel
可以通过确定调用其isConnected
方法。socket channel
支持 非阻塞连接:
socket channel
,然后可以通过 connect
方法建立到远程socket
的连接。finishConnect
方法来结束连接。isConnectionPending
方法来确定。socket channel
支持异步关闭,类似于Channel
类中的异步关闭操作。
socket
的输入端被一个线程关闭而另一个线程在此socket channel
上因在进行读操作而被阻塞,那么被阻塞线程中的读操作将不读取任何字节并将返回 -1
。socket
的输出端被一个线程关闭而另一个线程在socket channel
上因在进行写操作而被阻塞,则被阻塞的线程将收到AsynchronousCloseException
。接下来,我们来看其具体实现方法。
1 | //java.nio.channels.SocketChannel#open() |
关于Net.socket(true)
,我们前面已经提到过了,这里,通过其底层源码来再次调教下 (此处不想看可以跳过):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81JNIEXPORT jint JNICALL
Java_sun_nio_ch_Net_socket0(JNIEnv *env, jclass cl, jboolean preferIPv6,
jboolean stream, jboolean reuse, jboolean ignored)
{
int fd;
//字节流还是数据报,TCP对应SOCK_STREAM,UDP对应SOCK_DGRAM,此处传入的stream=true;
int type = (stream ? SOCK_STREAM : SOCK_DGRAM);
//判断是IPV6还是IPV4
int domain = (ipv6_available() && preferIPv6) ? AF_INET6 : AF_INET;
//调用Linux的socket函数,domain为代表协议;
//type为套接字类型,protocol设置为0来表示使用默认的传输协议
fd = socket(domain, type, 0);
//出错
if (fd < 0) {
return handleSocketError(env, errno);
}
/* Disable IPV6_V6ONLY to ensure dual-socket support */
if (domain == AF_INET6) {
int arg = 0;
//arg=1设置ipv6的socket只接收ipv6地址的报文,arg=0表示也可接受ipv4的请求
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&arg,
sizeof(int)) < 0) {
JNU_ThrowByNameWithLastError(env,
JNU_JAVANETPKG "SocketException",
"Unable to set IPV6_V6ONLY");
close(fd);
return -1;
}
}
//SO_REUSEADDR有四种用途:
//1.当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
//2.SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。
//3.SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。
//4.SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP;
if (reuse) {
int arg = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,
sizeof(arg)) < 0) {
JNU_ThrowByNameWithLastError(env,
JNU_JAVANETPKG "SocketException",
"Unable to set SO_REUSEADDR");
close(fd);
return -1;
}
}
#if defined(__linux__)
if (type == SOCK_DGRAM) {
int arg = 0;
int level = (domain == AF_INET6) ? IPPROTO_IPV6 : IPPROTO_IP;
if ((setsockopt(fd, level, IP_MULTICAST_ALL, (char*)&arg, sizeof(arg)) < 0) &&
(errno != ENOPROTOOPT)) {
JNU_ThrowByNameWithLastError(env,
JNU_JAVANETPKG "SocketException",
"Unable to set IP_MULTICAST_ALL");
close(fd);
return -1;
}
}
//IPV6_MULTICAST_HOPS用于控制多播的范围,
// 1表示只在本地网络转发,
//更多介绍请参考(http://www.ctt.sbras.ru/cgi-bin/www/unix_help/unix-man?ip6+4);
/* By default, Linux uses the route default */
if (domain == AF_INET6 && type == SOCK_DGRAM) {
int arg = 1;
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &arg,
sizeof(arg)) < 0) {
JNU_ThrowByNameWithLastError(env,
JNU_JAVANETPKG "SocketException",
"Unable to set IPV6_MULTICAST_HOPS");
close(fd);
return -1;
}
}
#endif
return fd;
}
Linux 3.9之后加入了SO_REUSEPORT
配置,这个配置很强大,多个socket
(不管是处于监听还是非监听,不管是TCP还是UDP)只要在绑定之前设置了SO_REUSEPORT
属性,那么就可以绑定到完全相同的地址和端口。
为了阻止”port 劫持”(Port hijacking
)有一个特别的限制:所有希望共享源地址和端口的socket都必须拥有相同的有效用户id(effective user ID
)。这样一个用户就不能从另一个用户那里”偷取”端口。另外,内核在处理SO_REUSEPORT socket
的时候使用了其它系统上没有用到的”特殊技巧”:
例如:一个简单的服务器程序的多个实例可以使用SO_REUSEPORT socket
,这样就实现一个简单的负载均衡,因为内核已经把请求的分配都做了。
在前面的代码中可以看到,在这个socket
创建成功之后,调用IOUtil.newFD
创建了文件描述符
。这里,我只是想知道这个Socket是可以输入呢,还是可以读呢,还是有错呢,参考FileDescriptor
这一节最后那几个标准状态的设定,其实这里也是一样,因为我们要往Socket中写和读,其标准状态无非就这三种:输入,输出,出错。而这个Socket是绑定在SocketChannel
上的,那就把FileDescriptor
也绑定到上面即可,这样我们就可以获取到它的状态了。由于FileDescriptor没有提供外部设置fd的方法,setfdVal是通过本地方法实现的:1
2
3
4
5JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_setfdVal(JNIEnv *env, jclass clazz, jobject fdo, jint val)
{
(*env)->SetIntField(env, fdo, fd_fdID, val);
}
假如各位有对Linux下的shell编程或者命令有了解的话,我们知道,shell对报错进行重定向要使用2>,也就是将错误信息由2号所指向的通道写出,这里0和1 同样指向一个通道。此处同样也代表了状态,这样就可以对代表Socket的状态进行操作了,也就是改变SelectionKey
的interest ops
,即首先对SelectionKey
按输入输出类型进行分类,然后我们的读写状态的操作也就有着落了。此处我们打个戳,在下一篇中会对其进行细节讲解。
我们回归到SocketChannel
的open
方法中。我们可以看到,SelectorProvider.provider().openSocketChannel()
返回的是SocketChannelImpl
对象实例。在SocketChannelImpl(SelectorProvider sp)
中我们并未看到其对this.state
进行值操作,也就是其默认为0,即ST_UNCONNECTED
(未连接状态),同时Socket默认是堵塞的。
所以,一般情况下,当采用异步方式时,使用不带参数的open方法比较常见,这样,我们会随之调用configureBlocking
来设置非堵塞。
由前面可知,我们调用connect
方法连接到远程服务器,其源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44//sun.nio.ch.SocketChannelImpl#connect
public boolean connect(SocketAddress sa) throws IOException {
InetSocketAddress isa = Net.checkAddress(sa);
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkConnect(isa.getAddress().getHostAddress(), isa.getPort());
InetAddress ia = isa.getAddress();
if (ia.isAnyLocalAddress())
ia = InetAddress.getLocalHost();
try {
readLock.lock();
try {
writeLock.lock();
try {
int n = 0;
boolean blocking = isBlocking();
try {
//支持线程中断,通过设置当前线程的Interruptible blocker属性实现
beginConnect(blocking, isa);
do {
//调用connect函数实现,如果采用堵塞模式,会一直等待,直到成功或出//现异常
n = Net.connect(fd, ia, isa.getPort());
} while (n == IOStatus.INTERRUPTED && isOpen());
} finally {
endConnect(blocking, (n > 0));
}
assert IOStatus.check(n);
//连接成功
return n > 0;
} finally {
writeLock.unlock();
}
} finally {
readLock.unlock();
}
} catch (IOException ioe) {
// connect failed, close the channel
close();
throw SocketExceptions.of(ioe, isa);
}
}
关于beginConnect
与endConnect
,是针对AbstractInterruptibleChannel
中begin()
与end
方法的一种增强。这里我们需要知道的是,假如是非阻塞Channel的话,我们无须去关心连接过程的打断。顾名思义,只有阻塞等待才需要去考虑打断这一场景的出现。剩下的细节我已经在代码中进行了完整的注释,读者可自行查看。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//sun.nio.ch.SocketChannelImpl#beginConnect
private void beginConnect(boolean blocking, InetSocketAddress isa)
throws IOException
{ //只有阻塞的时候才会进入begin
if (blocking) {
// set hook for Thread.interrupt
//支持线程中断,通过设置当前线程的Interruptible blocker属性实现
begin();
}
synchronized (stateLock) {
//默认为open, 除非调用了close方法
ensureOpen();
//检查连接状态
int state = this.state;
if (state == ST_CONNECTED)
throw new AlreadyConnectedException();
if (state == ST_CONNECTIONPENDING)
throw new ConnectionPendingException();
//断言当前的状态是否是未连接状态,如果是,赋值表示正在连接中
assert state == ST_UNCONNECTED;
//表示正在连接中
this.state = ST_CONNECTIONPENDING;
//只有未绑定本地地址也就是说未调用bind方法才执行,
//该方法在ServerSocketChannel中也见过
if (localAddress == null)
NetHooks.beforeTcpConnect(fd, isa.getAddress(), isa.getPort());
remoteAddress = isa;
if (blocking) {
// record thread so it can be signalled if needed
readerThread = NativeThread.current();
}
}
}
在连接过程中,我们需要注意的就是几个连接的状态:ST_UNCONNECTED
、ST_CONNECTED
、ST_CONNECTIONPENDING
、ST_CLOSING
、ST_KILLPENDING
、ST_KILLED
,也是因为其是一个公共状态,可能会有多个线程对其进行连接操作的。所以,state
被定义为一个volatile
变量,这个变量在改变的时候需要有stateLock
这个对象来作为synchronized
锁对象来控制同步操作的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//sun.nio.ch.SocketChannelImpl#endConnect
private void endConnect(boolean blocking, boolean completed)
throws IOException
{
endRead(blocking, completed);
//当上面代码中n>0,说明连接成功,更新状态为ST_CONNECTED
if (completed) {
synchronized (stateLock) {
if (state == ST_CONNECTIONPENDING) {
localAddress = Net.localAddress(fd);
state = ST_CONNECTED;
}
}
}
}
//sun.nio.ch.SocketChannelImpl#endRead
private void endRead(boolean blocking, boolean completed)
throws AsynchronousCloseException
{ //当阻塞状态下的话,才进入
if (blocking) {
synchronized (stateLock) {
readerThread = 0;
// notify any thread waiting in implCloseSelectableChannel
if (state == ST_CLOSING) {
stateLock.notifyAll();
}
}
//和begin成对出现,当线程中断时,抛出ClosedByInterruptException
// remove hook for Thread.interrupt
end(completed);
}
}
我们来关注connect
中的Net.connect(fd, ia, isa.getPort())
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14//sun.nio.ch.Net#connect
static int connect(FileDescriptor fd, InetAddress remote, int remotePort)
throws IOException
{
return connect(UNSPEC, fd, remote, remotePort);
}
//sun.nio.ch.Net#connect
static int connect(ProtocolFamily family, FileDescriptor fd, InetAddress remote, int remotePort)
throws IOException
{
boolean preferIPv6 = isIPv6Available() &&
(family != StandardProtocolFamily.INET);
return connect0(preferIPv6, fd, remote, remotePort);
}
该方法最终会调用native方法,具体注释如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29JNIEXPORT jint JNICALL
Java_sun_nio_ch_Net_connect0(JNIEnv *env, jclass clazz, jboolean preferIPv6,
jobject fdo, jobject iao, jint port)
{
SOCKETADDRESS sa;
int sa_len = 0;
int rv;
//地址转换为struct sockaddr格式
if (NET_InetAddressToSockaddr(env, iao, port, &sa, &sa_len, preferIPv6) != 0) {
return IOS_THROWN;
}
//传入fd和sockaddr,与远程服务器建立连接,一般就是TCP三次握手
//如果设置了configureBlocking(false),不会堵塞,否则会堵塞一直到超时或出现异常
rv = connect(fdval(env, fdo), &sa.sa, sa_len);
//0表示连接成功,失败时通过errno获取具体原因
if (rv != 0) {
//非堵塞,连接还未建立(-2)
if (errno == EINPROGRESS) {
return IOS_UNAVAILABLE;
} else if (errno == EINTR) {
//中断(-3)
return IOS_INTERRUPTED;
}
return handleSocketError(env, errno);
}
//连接建立,一般TCP连接连接都需要时间,因此除非是本地网络,
//一般情况下非堵塞模式返回IOS_UNAVAILABLE比较多;
return 1;
}
从上面可以通过注释看到,如果是非堵塞,而且连接也并未立马建立成功,其返回的是-2,也就是连接未建立成功,由之前beginConnect
部分源码可知,此时状态为ST_CONNECTIONPENDING
,那么,非阻塞条件下,什么时候会变为ST_CONNECTED
?有什么方法可以查询状态或者等待连接完成?
那就让我们来关注下sun.nio.ch.SocketChannelImpl#finishConnect
首先,我们回顾下,前面我们涉及了sun.nio.ch.ServerSocketAdaptor
的用法,方便我们只有Socket编程习惯人群使用,这里,我们也就可以看到基本的核心实现逻辑,那么有ServerSocketAdaptor
就有SocketAdaptor
,这里,在BIO的Socket编程中最后也是调用了connect(address)
操作:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//java.net.Socket#Socket
private Socket(SocketAddress address, SocketAddress localAddr,
boolean stream) throws IOException {
setImpl();
// backward compatibility
if (address == null)
throw new NullPointerException();
try {
createImpl(stream);
if (localAddr != null)
bind(localAddr);
connect(address);
} catch (IOException | IllegalArgumentException | SecurityException e) {
try {
close();
} catch (IOException ce) {
e.addSuppressed(ce);
}
throw e;
}
}
这里,我们可以调用java.nio.channels.SocketChannel#open()
,然后调用所得到的SocketChannel
对象的socket()
方法,就可以得到sun.nio.ch.SocketAdaptor
对象实例了。我们来查看SocketAdaptor
的connect实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62//sun.nio.ch.SocketAdaptor#connect
public void connect(SocketAddress remote) throws IOException {
connect(remote, 0);
}
public void connect(SocketAddress remote, int timeout) throws IOException {
if (remote == null)
throw new IllegalArgumentException("connect: The address can't be null");
if (timeout < 0)
throw new IllegalArgumentException("connect: timeout can't be negative");
synchronized (sc.blockingLock()) {
if (!sc.isBlocking())
throw new IllegalBlockingModeException();
try {
//未设定超时则会一直在此等待直到连接或者出现异常
// no timeout
if (timeout == 0) {
sc.connect(remote);
return;
}
//有超时设定,则会将Socket给设定为非阻塞
// timed connect
sc.configureBlocking(false);
try {
if (sc.connect(remote))
return;
} finally {
try {
sc.configureBlocking(true);
} catch (ClosedChannelException e) { }
}
long timeoutNanos = NANOSECONDS.convert(timeout, MILLISECONDS);
long to = timeout;
for (;;) {
//通过计算超时时间,在允许的时间范围内无限循环来进行连接,
//如果超时,则关闭这个Socket
long startTime = System.nanoTime();
if (sc.pollConnected(to)) {
boolean connected = sc.finishConnect();
//看下文解释
assert connected;
break;
}
timeoutNanos -= System.nanoTime() - startTime;
if (timeoutNanos <= 0) {
try {
sc.close();
} catch (IOException x) { }
throw new SocketTimeoutException();
}
to = MILLISECONDS.convert(timeoutNanos, NANOSECONDS);
}
} catch (Exception x) {
Net.translateException(x, true);
}
}
}
这里先解释下一个小注意点:在Java中,assert
关键字是从JAVA SE 1.4
引入的,为了避免和老版本的Java代码中使用了assert
关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都 将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。
通过上面的源码注释,相信大伙已经知道大致的流程了,那关于sun.nio.ch.SocketChannelImpl#finishConnect
到底做了什么,此处,我们来探索一番:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47//sun.nio.ch.SocketChannelImpl#finishConnect
public boolean finishConnect() throws IOException {
try {
readLock.lock();
try {
writeLock.lock();
try {
// no-op if already connected
if (isConnected())
return true;
boolean blocking = isBlocking();
boolean connected = false;
try {
beginFinishConnect(blocking);
int n = 0;
if (blocking) {
do {
//阻塞情况下,第二个参数传入true
n = checkConnect(fd, true);
} while ((n == 0 || n == IOStatus.INTERRUPTED) && isOpen());
} else {
//非阻塞情况下,第二个参数传入false
n = checkConnect(fd, false);
}
connected = (n > 0);
} finally {
endFinishConnect(blocking, connected);
}
assert (blocking && connected) ^ !blocking;
return connected;
} finally {
writeLock.unlock();
}
} finally {
readLock.unlock();
}
} catch (IOException ioe) {
// connect failed, close the channel
close();
throw SocketExceptions.of(ioe, remoteAddress);
}
}
//sun.nio.ch.SocketChannelImpl#checkConnect
private static native int checkConnect(FileDescriptor fd, boolean block)
throws IOException;
关于beginFinishConnect
与endFinishConnect
和我们之前分析的sun.nio.ch.SocketChannelImpl#beginConnect
与sun.nio.ch.SocketChannelImpl#endConnect
过程差不多,不懂读者可回看。剩下的,就是我们关注的主要核心逻辑checkConnect(fd, true)
,它也是一个本地方法,涉及到的源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50JNIEXPORT jint JNICALL
Java_sun_nio_ch_SocketChannelImpl_checkConnect(JNIEnv *env, jobject this,
jobject fdo, jboolean block)
{
int error = 0;
socklen_t n = sizeof(int);
//获取FileDescriptor中的fd
jint fd = fdval(env, fdo);
int result = 0;
struct pollfd poller;
//文件描述符
poller.fd = fd;
//请求的事件为写事件
poller.events = POLLOUT;
//返回的事件
poller.revents = 0;
//-1表示阻塞,0表示立即返回,不阻塞进程
result = poll(&poller, 1, block ? -1 : 0);
//小于0表示调用失败
if (result < 0) {
if (errno == EINTR) {
return IOS_INTERRUPTED;
} else {
JNU_ThrowIOExceptionWithLastError(env, "poll failed");
return IOS_THROWN;
}
}
//非堵塞时,0表示没有准备好的连接
if (!block && (result == 0))
return IOS_UNAVAILABLE;
//准备好写或出现错误的socket数量>0
if (result > 0) {
errno = 0;
result = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n);
//出错
if (result < 0) {
return handleSocketError(env, errno);
//发生错误,处理错误
} else if (error) {
return handleSocketError(env, error);
} else if ((poller.revents & POLLHUP) != 0) {
return handleSocketError(env, ENOTCONN);
}
//socket已经准备好,可写,即连接已经建立好
// connected
return 1;
}
return 0;
}
具体的过程如源码注释所示,其中是否阻塞我们在本地方法源码中和之前sun.nio.ch.SocketChannelImpl#finishConnect
的行为产生对应。另外,从上面的源码看到,底层是通过poll
查询socket
的状态,从而判断连接是否建立成功;由于在非堵塞模式下,finishConnect
方法会立即返回,根据此处sun.nio.ch.SocketAdaptor#connect
的处理,其使用循环的方式判断连接是否建立,在我们的nio编程中,这个是不建议的,属于半成品,而是建议注册到Selector
,通过ops=OP_CONNECT
获取连接完成的SelectionKey
,然后调用finishConnect
完成连接的建立;
那么finishConnect
是否可以不调用呢?答案是否定的,因为只有finishConnect
中才会将状态更新为ST_CONNECTED
,而在调用read
和write
时都会对状态进行判断。
这里,我们算是引出了我们即将要涉及的Selector
和SelectionKey
,我们会在下一篇中进行详细讲解。
此篇文章会详细解读NIO的功能逐步丰满的路程,为Reactor-Netty 库的讲解铺平道路。
关于Java编程方法论-Reactor与Webflux的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
接上一篇 BIO到NIO源码的一些事儿之BIO,我们来接触NIO的一些事儿。
在上一篇中,我们可以看到,我们要做到异步非阻塞,我们自己进行的是创建线程池同时对部分代码做timeout的修改来对接客户端,但是弊端也很清晰,我们转换下思维,这里举个场景例子,A班同学要和B班同学一起一对一完成任务,每对人拿到的任务是不一样的,消耗的时间有长有短,任务因为有奖励所以同学们会抢,传统模式下,A班同学和B班同学不经管理话,即便只是一个心跳检测的任务都得一起,在这种情况下,客户端根本不会有数据要发送,只是想告诉服务器自己还活着,这种情况下,假如B班再来一个同学做对接的话,就很有问题了,B班的每一个同学都可以看成服务器端的一个线程。所以,我们需要一个管理者,于是Selector
就出现了,作为管理者,这里,我们往往需要管理同学们的状态,是否在等待任务,是否在接收信息,是否在输出信息等等,Selector
更侧重于动作,针对于这些状态标签来做事情就可以了,那这些状态标签其实也是需要管理的,于是SelectionKey
也就应运而生。接着我们需要对这些同学进行包装增强,使之携带这样的标签。同样,对于同学我们应该进一步解放双手的,比如给其配台电脑,这样,同学是不是可以做更多的事情了,那这个电脑在此处就是Buffer的存在了。
于是在NIO中最主要是有三种角色的,Buffer
缓冲区,Channel
通道,Selector
选择器,我们都涉及到了,接下来,我们对其源码一步步分析解读。
有上可知,同学其实都是代表着一个个的Socket
的存在,那么这里Channel
就是对其进行的增强包装,也就是Channel
的具体实现里应该有Socket
这个字段才行,然后具体实现类里面也是紧紧围绕着Socket
具备的功能来做文章的。那么,我们首先来看java.nio.channels.Channel
接口的设定:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*
* @return {@code true} if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
*
* <p> If this channel is already closed then invoking this method has no
* effect.
*
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
此处就是很直接的设定,判断Channel是否是open状态,关闭Channel的动作,我们在接下来会讲到ClosedChannelException
是如何具体在代码中发生的。
有时候,一个Channel可能会被异步关闭和中断,这也是我们所需求的。那么要实现这个效果我们须得设定一个可以进行此操作效果的接口。达到的具体的效果应该是如果线程在实现这个接口的的Channel中进行IO操作的时候,另一个线程可以调用该Channel的close方法。导致的结果就是,进行IO操作的那个阻塞线程会收到一个AsynchronousCloseException
异常。
同样,我们应该考虑到另一种情况,如果线程在实现这个接口的的Channel中进行IO操作的时候,另一个线程可能会调用被阻塞线程的interrupt
方法(Thread#interrupt()
),从而导致Channel关闭,那么这个阻塞的线程应该要收到ClosedByInterruptException
异常,同时将中断状态设定到该阻塞线程之上。
这时候,如果中断状态已经在该线程设定完毕,此时在其之上的有Channel又调用了IO阻塞操作,那么,这个Channel会被关闭,同时,该线程会立即受到一个ClosedByInterruptException
异常,它的interrupt状态仍然保持不变。
这个接口定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface InterruptibleChannel
extends Channel
{
/**
* Closes this channel.
*
* <p> Any thread currently blocked in an I/O operation upon this channel
* will receive an {@link AsynchronousCloseException}.
*
* <p> This method otherwise behaves exactly as specified by the {@link
* Channel#close Channel} interface. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
其针对上面所提到逻辑的具体实现是在java.nio.channels.spi.AbstractInterruptibleChannel
进行的,关于这个类的解析,我们来参考这篇文章InterruptibleChannel 与可中断 IO
我们在前面有说到,Channel
可以被Selector
进行使用,而Selector
是根据Channel
的状态来分配任务的,那么Channel
应该提供一个注册到Selector
上的方法,来和Selector
进行绑定。也就是说Channel
的实例要调用register(Selector,int,Object)
。注意,因为Selector
是要根据状态值进行管理的,所以此方法会返回一个SelectionKey
对象来表示这个channel
在selector
上的状态。关于SelectionKey
,它是包含很多东西的,这里暂不提。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51//java.nio.channels.spi.AbstractSelectableChannel#register
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (!isOpen())
throw new ClosedChannelException();
synchronized (regLock) {
if (isBlocking())
throw new IllegalBlockingModeException();
synchronized (keyLock) {
// re-check if channel has been closed
if (!isOpen())
throw new ClosedChannelException();
SelectionKey k = findKey(sel);
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
return k;
}
}
}
//java.nio.channels.spi.AbstractSelectableChannel#addKey
private void addKey(SelectionKey k) {
assert Thread.holdsLock(keyLock);
int i = 0;
if ((keys != null) && (keyCount < keys.length)) {
// Find empty element of key array
for (i = 0; i < keys.length; i++)
if (keys[i] == null)
break;
} else if (keys == null) {
keys = new SelectionKey[2];
} else {
// Grow key array
int n = keys.length * 2;
SelectionKey[] ks = new SelectionKey[n];
for (i = 0; i < keys.length; i++)
ks[i] = keys[i];
keys = ks;
i = keyCount;
}
keys[i] = k;
keyCount++;
}
一旦注册到Selector
上,Channel将一直保持注册直到其被解除注册。在解除注册的时候会解除Selector分配给Channel的所有资源。
也就是Channel并没有直接提供解除注册的方法,那我们换一个思路,我们将Selector上代表其注册的Key取消不就可以了。这里可以通过调用SelectionKey#cancel()
方法来显式的取消key。然后在Selector
下一次选择操作期间进行对Channel的取消注册。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105//java.nio.channels.spi.AbstractSelectionKey#cancel
/**
* Cancels this key.
*
* <p> If this key has not yet been cancelled then it is added to its
* selector's cancelled-key set while synchronized on that set. </p>
*/
public final void cancel() {
// Synchronizing "this" to prevent this key from getting canceled
// multiple times by different threads, which might cause race
// condition between selector's select() and channel's close().
synchronized (this) {
if (valid) {
valid = false;
//还是调用Selector的cancel方法
((AbstractSelector)selector()).cancel(this);
}
}
}
//java.nio.channels.spi.AbstractSelector#cancel
void cancel(SelectionKey k) {
synchronized (cancelledKeys) {
cancelledKeys.add(k);
}
}
//在下一次select操作的时候来解除那些要求cancel的key,即解除Channel注册
//sun.nio.ch.SelectorImpl#select(long)
public final int select(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
//重点关注此方法
return lockAndDoSelect(null, (timeout == 0) ? -1 : timeout);
}
//sun.nio.ch.SelectorImpl#lockAndDoSelect
private int lockAndDoSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
synchronized (this) {
ensureOpen();
if (inSelect)
throw new IllegalStateException("select in progress");
inSelect = true;
try {
synchronized (publicSelectedKeys) {
//重点关注此方法
return doSelect(action, timeout);
}
} finally {
inSelect = false;
}
}
}
//sun.nio.ch.WindowsSelectorImpl#doSelect
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
assert Thread.holdsLock(this);
this.timeout = timeout; // set selector timeout
processUpdateQueue();
//重点关注此方法
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
...
}
/**
* sun.nio.ch.SelectorImpl#processDeregisterQueue
* Invoked by selection operations to process the cancelled-key set
*/
protected final void processDeregisterQueue() throws IOException {
assert Thread.holdsLock(this);
assert Thread.holdsLock(publicSelectedKeys);
Set<SelectionKey> cks = cancelledKeys();
synchronized (cks) {
if (!cks.isEmpty()) {
Iterator<SelectionKey> i = cks.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
i.remove();
// remove the key from the selector
implDereg(ski);
selectedKeys.remove(ski);
keys.remove(ski);
// remove from channel's key set
deregister(ski);
SelectableChannel ch = ski.channel();
if (!ch.isOpen() && !ch.isRegistered())
((SelChImpl)ch).kill();
}
}
}
}
这里,当Channel关闭时,无论是通过调用Channel#close
还是通过打断线程的方式来对Channel进行关闭,其都会隐式的取消关于这个Channel的所有的keys,其内部也是调用了k.cancel()
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40//java.nio.channels.spi.AbstractInterruptibleChannel#close
/**
* Closes this channel.
*
* <p> If the channel has already been closed then this method returns
* immediately. Otherwise it marks the channel as closed and then invokes
* the {@link #implCloseChannel implCloseChannel} method in order to
* complete the close operation. </p>
*
* @throws IOException
* If an I/O error occurs
*/
public final void close() throws IOException {
synchronized (closeLock) {
if (closed)
return;
closed = true;
implCloseChannel();
}
}
//java.nio.channels.spi.AbstractSelectableChannel#implCloseChannel
protected final void implCloseChannel() throws IOException {
implCloseSelectableChannel();
// clone keys to avoid calling cancel when holding keyLock
SelectionKey[] copyOfKeys = null;
synchronized (keyLock) {
if (keys != null) {
copyOfKeys = keys.clone();
}
}
if (copyOfKeys != null) {
for (SelectionKey k : copyOfKeys) {
if (k != null) {
k.cancel(); // invalidate and adds key to cancelledKey set
}
}
}
}
如果Selector
自身关闭掉,那么Channel也会被解除注册,同时代表Channel注册的key也将变得无效:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//java.nio.channels.spi.AbstractSelector#close
public final void close() throws IOException {
boolean open = selectorOpen.getAndSet(false);
if (!open)
return;
implCloseSelector();
}
//sun.nio.ch.SelectorImpl#implCloseSelector
public final void implCloseSelector() throws IOException {
wakeup();
synchronized (this) {
implClose();
synchronized (publicSelectedKeys) {
// Deregister channels
Iterator<SelectionKey> i = keys.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
deregister(ski);
SelectableChannel selch = ski.channel();
if (!selch.isOpen() && !selch.isRegistered())
((SelChImpl)selch).kill();
selectedKeys.remove(ski);
i.remove();
}
assert selectedKeys.isEmpty() && keys.isEmpty();
}
}
}
一个channel最多可以最多只能在特定的selector注册一次。我们可以通过调用java.nio.channels.SelectableChannel#isRegistered
的方法来确定是否向一个或多个Selector注册了channel。1
2
3
4
5
6
7
8
9//java.nio.channels.spi.AbstractSelectableChannel#isRegistered
// -- Registration --
public final boolean isRegistered() {
synchronized (keyLock) {
//我们在之前往Selector上注册的时候调用了addKey方法,即每次往//一个Selector注册一次,keyCount就要自增一次。
return keyCount != 0;
}
}
至此,继承了SelectableChannel这个类之后,这个channel就可以安全的由多个并发线程来使用。
这里,要注意的是,继承了AbstractSelectableChannel
这个类之后,新创建的channel始终处于阻塞模式。然而与Selector
的多路复用有关的操作必须基于非阻塞模式,所以在注册到Selector
之前,必须将channel
置于非阻塞模式,并且在取消注册之前,channel
可能不会返回到阻塞模式。
这里,我们涉及了Channel的阻塞模式与非阻塞模式。在阻塞模式下,在Channel
上调用的每个I/O操作都将阻塞,直到完成为止。 在非阻塞模式下,I/O操作永远不会阻塞,并且可以传输比请求的字节更少的字节,或者根本不传输任何字节。 我们可以通过调用channel的isBlocking方法来确定其是否为阻塞模式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//java.nio.channels.spi.AbstractSelectableChannel#register
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (!isOpen())
throw new ClosedChannelException();
synchronized (regLock) {
//此处会做判断,假如是阻塞模式,则会返回true,然后就会抛出异常
if (isBlocking())
throw new IllegalBlockingModeException();
synchronized (keyLock) {
// re-check if channel has been closed
if (!isOpen())
throw new ClosedChannelException();
SelectionKey k = findKey(sel);
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
return k;
}
}
}
所以,我们在使用的时候可以基于以下的例子作为参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public NIOServerSelectorThread(int port)
{
try {
//打开ServerSocketChannel,用于监听客户端的连接,他是所有客户端连接的父管道
serverSocketChannel = ServerSocketChannel.open();
//将管道设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//利用ServerSocketChannel创建一个服务端Socket对象,即ServerSocket
serverSocket = serverSocketChannel.socket();
//为服务端Socket绑定监听端口
serverSocket.bind(new InetSocketAddress(port));
//创建多路复用器
selector = Selector.open();
//将ServerSocketChannel注册到Selector多路复用器上,并且监听ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The server is start in port: "+port);
} catch (IOException e) {
e.printStackTrace();
}
}
因时间关系,本篇暂时到这里,剩下的会在下一篇中进行讲解。
]]>此篇文章会详细解读由BIO到NIO的逐步演进的心灵路程,为Reactor-Netty 库的讲解铺平道路。
关于Java编程方法论-Reactor与Webflux
的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
我们通过一个BIO的Demo来展示其用法:
1 | //服务端 |
通过上面的例子,我们可以知道,无论是服务端还是客户端,我们关注的几个操作有基于服务端的serverSocket = new ServerSocket(port)
serverSocket.accept()
,基于客户端的Socket socket = new Socket(host, port);
以及两者都有的读取与写入Socket数据的方式,即通过流来进行读写,这个读写不免通过一个中间字节数组buffer来进行。
于是,我们通过源码来看这些相应的逻辑。我们先来看ServerSocket.java
这个类的相关代码。
我们查看ServerSocket.java
的构造器可以知道,其最后依然会调用它的bind
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//java.net.ServerSocket#ServerSocket(int)
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
按照我们的Demo和上面的源码可知,这里传入的参数endpoint并不会为null,同时,属于InetSocketAddress
类型,backlog大小为50,于是,我们应该关注的主要代码逻辑也就是getImpl().bind(epoint.getAddress(), epoint.getPort());
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public void bind(SocketAddress endpoint, int backlog) throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!oldImpl && isBound())
throw new SocketException("Already bound");
if (endpoint == null)
endpoint = new InetSocketAddress(0);
if (!(endpoint instanceof InetSocketAddress))
throw new IllegalArgumentException("Unsupported address type");
InetSocketAddress epoint = (InetSocketAddress) endpoint;
if (epoint.isUnresolved())
throw new SocketException("Unresolved address");
if (backlog < 1)
backlog = 50;
try {
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkListen(epoint.getPort());
// 我们应该关注的主要逻辑
getImpl().bind(epoint.getAddress(), epoint.getPort());
getImpl().listen(backlog);
bound = true;
} catch(SecurityException e) {
bound = false;
throw e;
} catch(IOException e) {
bound = false;
throw e;
}
}
这里getImpl()
,由上面构造器的实现中,我们有看到setImpl();
,可知,其factory
默认为null,所以,这里我们关注的是SocksSocketImpl
这个类,创建其对象,并将当前ServerSocket
对象设定其中,这个设定的源码请在SocksSocketImpl
的父类java.net.SocketImpl
中查看。
那么getImpl也就明了了,其实就是我们Socket的底层实现对应的实体类了,因为不同的操作系统内核是不同的,他们对于Socket的实现当然会各有不同,我们这点要注意下,这里针对的是win下面的系统。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/**
* The factory for all server sockets.
*/
private static SocketImplFactory factory = null;
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
// No need to do a checkOldImpl() here, we know it's an up to date
// SocketImpl!
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}
/**
* Get the {@code SocketImpl} attached to this socket, creating
* it if necessary.
*
* @return the {@code SocketImpl} attached to that ServerSocket.
* @throws SocketException if creation fails.
* @since 1.4
*/
SocketImpl getImpl() throws SocketException {
if (!created)
createImpl();
return impl;
}
/**
* Creates the socket implementation.
*
* @throws IOException if creation fails
* @since 1.4
*/
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
我们再看SocksSocketImpl
的bind方法实现,然后得到其最后无非是调用本地方法bind0
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45//java.net.AbstractPlainSocketImpl#bind
/**
* Binds the socket to the specified address of the specified local port.
* @param address the address
* @param lport the port
*/
protected synchronized void bind(InetAddress address, int lport)
throws IOException
{
synchronized (fdLock) {
if (!closePending && (socket == null || !socket.isBound())) {
NetHooks.beforeTcpBind(fd, address, lport);
}
}
socketBind(address, lport);
if (socket != null)
socket.setBound();
if (serverSocket != null)
serverSocket.setBound();
}
//java.net.PlainSocketImpl#socketBind
void socketBind(InetAddress address, int port) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (address == null)
throw new NullPointerException("inet address argument is null.");
if (preferIPv4Stack && !(address instanceof Inet4Address))
throw new SocketException("Protocol family not supported");
bind0(nativefd, address, port, useExclusiveBind);
if (port == 0) {
localport = localPort0(nativefd);
} else {
localport = port;
}
this.address = address;
}
//java.net.PlainSocketImpl#bind0
static native void bind0(int fd, InetAddress localAddress, int localport,
boolean exclBind)
throws IOException;
这里,我们还要了解的是,使用了多线程只是能够实现对”业务逻辑处理”的多线程,但是对于数据报文的接收还是需要一个一个来的,也就是我们上面Demo中见到的accept以及read方法阻塞问题,多线程是根本解决不了的,那么首先我们来看看accept为什么会造成阻塞,accept方法的作用是询问操作系统是否有新的Socket套接字信息从端口XXX处发送过来,注意这里询问的是操作系统,也就是说Socket套接字IO模式的支持是基于操作系统的,如果操作系统没有发现有套接字从指定端口XXX连接进来,那么操作系统就会等待,这样accept方法就会阻塞,他的内部实现使用的是操作系统级别的同步IO。
于是,我们来分析下ServerSocket.accept
方法的源码过程:1
2
3
4
5
6
7
8
9public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}
首先进行的是一些判断,接着创建了一个Socket对象(为什么这里要创建一个Socket对象,后面会讲到),执行了implAccept方法,来看看implAccept方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/**
* Subclasses of ServerSocket use this method to override accept()
* to return their own subclass of socket. So a FooServerSocket
* will typically hand this method an <i>empty</i> FooSocket. On
* return from implAccept the FooSocket will be connected to a client.
*
* @param s the Socket
* @throws java.nio.channels.IllegalBlockingModeException
* if this socket has an associated channel,
* and the channel is in non-blocking mode
* @throws IOException if an I/O error occurs when waiting
* for a connection.
* @since 1.1
* @revised 1.4
* @spec JSR-51
*/
protected final void implAccept(Socket s) throws IOException {
SocketImpl si = null;
try {
if (s.impl == null)
s.setImpl();
else {
s.impl.reset();
}
si = s.impl;
s.impl = null;
si.address = new InetAddress();
si.fd = new FileDescriptor();
getImpl().accept(si); // <1>
SocketCleanable.register(si.fd); // raw fd has been set
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccept(si.getInetAddress().getHostAddress(),
si.getPort());
}
} catch (IOException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
} catch (SecurityException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
}
s.impl = si;
s.postAccept();
}
上面执行了<1>处getImpl的accept方法之后,我们在AbstractPlainSocketImpl找到accept方法:1
2
3
4
5
6
7
8
9
10
11
12
13//java.net.AbstractPlainSocketImpl#accept
/**
* Accepts connections.
* @param s the connection
*/
protected void accept(SocketImpl s) throws IOException {
acquireFD();
try {
socketAccept(s);
} finally {
releaseFD();
}
}
可以看到他调用了socketAccept方法,因为每个操作系统的Socket地实现都不同,所以这里Windows下就执行了我们PlainSocketImpl里面的socketAccept方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// java.net.PlainSocketImpl#socketAccept
void socketAccept(SocketImpl s) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null");
int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) { //<1>
newfd = accept0(nativefd, isaa); // <2>
} else {
configureBlocking(nativefd, false);
try {
waitForNewConnection(nativefd, timeout);
newfd = accept0(nativefd, isaa); // <3>
if (newfd != -1) {
configureBlocking(newfd, true);
}
} finally {
configureBlocking(nativefd, true);
}
} // <4>
/* Update (SocketImpl)s' fd */
fdAccess.set(s.fd, newfd);
/* Update socketImpls remote port, address and localport */
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
if (preferIPv4Stack && !(s.address instanceof Inet4Address))
throw new SocketException("Protocol family not supported");
}
//java.net.PlainSocketImpl#accept0
static native int accept0(int fd, InetSocketAddress[] isaa) throws IOException;
这里<1>到<4>之间是我们关注的代码,<2>和<3>执行了accept0方法,这个是native方法,具体来说就是与操作系统交互来实现监听指定端口上是否有客户端接入,正是因为accept0在没有客户端接入的时候会一直处于阻塞状态,所以造成了我们程序级别的accept方法阻塞,当然对于程序级别的阻塞,我们是可以避免的,也就是我们可以将accept方法修改成非阻塞式,但是对于accept0造成的阻塞我们暂时是没法改变的,操作系统级别的阻塞其实就是我们通常所说的同步异步中的同步了。
前面说到我们可以在程序级别改变accept的阻塞,具体怎么实现?其实就是通过我们上面socketAccept方法中判断timeout的值来实现,在第<1>处判断timeout的值如果小于等于0,那么直接执行accept0方法,这时候将一直处于阻塞状态,但是如果我们设置了timeout的话,即timeout值大于0的话,则程序会在等到我们设置的时间后返回,注意这里的newfd如果等于-1的话,表示这次accept没有发现有数据从底层返回;那么到底timeout的值是在哪设置?我们可以通过ServerSocket的setSoTimeout方法进行设置,来看看这个方法:1>3>2>4>1>
1 | /** |
其执行了getImpl的setOption方法,并且设置了timeout时间,这里,我们从AbstractPlainSocketImpl中查看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75//java.net.AbstractPlainSocketImpl#setOption
public void setOption(int opt, Object val) throws SocketException {
if (isClosedOrPending()) {
throw new SocketException("Socket Closed");
}
boolean on = true;
switch (opt) {
/* check type safety b4 going native. These should never
* fail, since only java.Socket* has access to
* PlainSocketImpl.setOption().
*/
case SO_LINGER:
if (val == null || (!(val instanceof Integer) && !(val instanceof Boolean)))
throw new SocketException("Bad parameter for option");
if (val instanceof Boolean) {
/* true only if disabling - enabling should be Integer */
on = false;
}
break;
case SO_TIMEOUT: //<1>
if (val == null || (!(val instanceof Integer)))
throw new SocketException("Bad parameter for SO_TIMEOUT");
int tmp = ((Integer) val).intValue();
if (tmp < 0)
throw new IllegalArgumentException("timeout < 0");
timeout = tmp;
break;
case IP_TOS:
if (val == null || !(val instanceof Integer)) {
throw new SocketException("bad argument for IP_TOS");
}
trafficClass = ((Integer)val).intValue();
break;
case SO_BINDADDR:
throw new SocketException("Cannot re-bind socket");
case TCP_NODELAY:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for TCP_NODELAY");
on = ((Boolean)val).booleanValue();
break;
case SO_SNDBUF:
case SO_RCVBUF:
if (val == null || !(val instanceof Integer) ||
!(((Integer)val).intValue() > 0)) {
throw new SocketException("bad parameter for SO_SNDBUF " +
"or SO_RCVBUF");
}
break;
case SO_KEEPALIVE:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_KEEPALIVE");
on = ((Boolean)val).booleanValue();
break;
case SO_OOBINLINE:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_OOBINLINE");
on = ((Boolean)val).booleanValue();
break;
case SO_REUSEADDR:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_REUSEADDR");
on = ((Boolean)val).booleanValue();
break;
case SO_REUSEPORT:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_REUSEPORT");
if (!supportedOptions().contains(StandardSocketOptions.SO_REUSEPORT))
throw new UnsupportedOperationException("unsupported option");
on = ((Boolean)val).booleanValue();
break;
default:
throw new SocketException("unrecognized TCP option: " + opt);
}
socketSetOption(opt, on, val);
}
这个方法比较长,我们仅看与timeout
有关的代码,即<1>处的代码。其实这里仅仅就是将我们setOption里面传入的timeout值设置到了AbstractPlainSocketImpl的全局变量timeout里而已。1>
这样,我们就可以在程序级别将accept方法设置成为非阻塞式的了,但是read方法现在还是阻塞式的,即后面我们还需要改造read方法,同样将它在程序级别上变成非阻塞式。
在正式改造前,我们有必要来解释下Socket下同步/异步和阻塞/非阻塞:
同步/异步是属于操作系统级别的,指的是操作系统在收到程序请求的IO之后,如果IO资源没有准备好的话,该如何响应程序的问题,同步的话就是不响应,直到IO资源准备好;而异步的话则会返回给程序一个标志,这个标志用于当IO资源准备好后通过事件机制发送的内容应该发到什么地方。
阻塞/非阻塞是属于程序级别的,指的是程序在请求操作系统进行IO操作时,如果IO资源没有准备好的话,程序该怎么处理的问题,阻塞的话就是程序什么都不做,一直等到IO资源准备好,非阻塞的话程序则继续运行,但是会时不时的去查看下IO到底准备好没有呢;
我们通常见到的BIO是同步阻塞式的,同步的话说明操作系统底层是一直等待IO资源准备直到ok的,阻塞的话是程序本身也在一直等待IO资源准备直到ok,具体来讲程序级别的阻塞就是accept和read造成的,我们可以通过改造将其变成非阻塞式,但是操作系统层次的阻塞我们没法改变。
我们的NIO是同步非阻塞式的,其实它的非阻塞实现原理和我们上面的讲解差不多的,就是为了改善accept和read方法带来的阻塞现象,所以引入了Channel
和Buffer
的概念。
好了,我们对我们的Demo进行改进,解决accept带来的阻塞问题(为多个客户端连接做的异步处理,这里就不多解释了,读者可自行思考,实在不行可到本人相关视频中找到对应解读):
1 | public class BIOProNotB { |
为我们的ServerSocket设置了timeout时间,这样的话调用accept方法的时候每隔1s他就会被唤醒一次,而不再是一直在那里,只有有客户端接入才会返回信息;我们运行一下看看结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
172019-01-02 17:28:43:362: serverSocket started
now time is: 2019-01-02 17:28:44:363
now time is: 2019-01-02 17:28:45:363
now time is: 2019-01-02 17:28:46:363
now time is: 2019-01-02 17:28:47:363
now time is: 2019-01-02 17:28:48:363
now time is: 2019-01-02 17:28:49:363
now time is: 2019-01-02 17:28:50:363
now time is: 2019-01-02 17:28:51:364
now time is: 2019-01-02 17:28:52:365
now time is: 2019-01-02 17:28:53:365
now time is: 2019-01-02 17:28:54:365
now time is: 2019-01-02 17:28:55:365
now time is: 2019-01-02 17:28:56:365 // <1>
2019-01-02 17:28:56:911: id为1308927845的Clientsocket connected
now time is: 2019-01-02 17:28:57:913 // <2>
now time is: 2019-01-02 17:28:58:913
可以看到,我们刚开始并没有客户端接入的时候,是会执行System.out.println("now time is: " + stringNowTime());
的输出,还有一点需要注意的就是,仔细看看上面的输出结果的标记<1>与<2>,你会发现<2>处时间值不是17:28:57:365,原因就在于如果accept正常返回值的话,是不会执行catch语句部分的。2>2>1>
这样的话,我们就把accept部分改造成了非阻塞式了,那么read部分可以改造么?当然可以,改造方法和accept很类似,我们在read的时候,会调用java.net.AbstractPlainSocketImpl#getInputStream
:1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* Gets an InputStream for this socket.
*/
protected synchronized InputStream getInputStream() throws IOException {
synchronized (fdLock) {
if (isClosedOrPending())
throw new IOException("Socket Closed");
if (shut_rd)
throw new IOException("Socket input is shutdown");
if (socketInputStream == null)
socketInputStream = new SocketInputStream(this);
}
return socketInputStream;
}
这里面创建了一个SocketInputStream
对象,会将当前AbstractPlainSocketImpl
对象传进去,于是,在读数据的时候,我们会调用如下方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58public int read(byte b[], int off, int length) throws IOException {
return read(b, off, length, impl.getTimeout());
}
int read(byte b[], int off, int length, int timeout) throws IOException {
int n;
// EOF already encountered
if (eof) {
return -1;
}
// connection reset
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
// bounds check
if (length <= 0 || off < 0 || length > b.length - off) {
if (length == 0) {
return 0;
}
throw new ArrayIndexOutOfBoundsException("length == " + length
+ " off == " + off + " buffer length == " + b.length);
}
// acquire file descriptor and do the read
FileDescriptor fd = impl.acquireFD();
try {
n = socketRead(fd, b, off, length, timeout);
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
impl.setConnectionReset();
} finally {
impl.releaseFD();
}
/*
* If we get here we are at EOF, the socket has been closed,
* or the connection has been reset.
*/
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
}
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
eof = true;
return -1;
}
private int socketRead(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
throws IOException {
return socketRead0(fd, b, off, len, timeout);
}
这里,我们看到了socketRead同样设定了timeout,而且这个timeout就是我们创建这个SocketInputStream
对象时传入的AbstractPlainSocketImpl
对象来控制的,所以,我们只需要设定serverSocket.setSoTimeout(1000)
即可。
我们再次修改服务端代码(代码总共两次设定,第一次是设定的是ServerSocket级别的,第二次设定的客户端连接返回的那个Socket,两者不一样):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89public class BIOProNotBR {
public void initBIOServer(int port) {
ServerSocket serverSocket = null;//服务端Socket
Socket socket = null;//客户端socket
ExecutorService threadPool = Executors.newCachedThreadPool();
ClientSocketThread thread = null;
try {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(1000);
System.out.println(stringNowTime() + ": serverSocket started");
while (true) {
try {
socket = serverSocket.accept();
} catch (SocketTimeoutException e) {
//运行到这里表示本次accept是没有收到任何数据的,服务端的主线程在这里可以做一些其他事情
System.out.println("now time is: " + stringNowTime());
continue;
}
System.out.println(stringNowTime() + ": id为" + socket.hashCode() + "的Clientsocket connected");
thread = new ClientSocketThread(socket);
threadPool.execute(thread);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String stringNowTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return format.format(new Date());
}
class ClientSocketThread extends Thread {
public Socket socket;
public ClientSocketThread(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader reader = null;
String inputContent;
int count = 0;
try {
socket.setSoTimeout(1000);
} catch (SocketException e1) {
e1.printStackTrace();
}
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true) {
try {
while ((inputContent = reader.readLine()) != null) {
System.out.println("收到id为" + socket.hashCode() + " " + inputContent);
count++;
}
} catch (Exception e) {
//执行到这里表示read方法没有获取到任何数据,线程可以执行一些其他的操作
System.out.println("Not read data: " + stringNowTime());
continue;
}
//执行到这里表示读取到了数据,我们可以在这里进行回复客户端的工作
System.out.println("id为" + socket.hashCode() + "的Clientsocket " + stringNowTime() + "读取结束");
sleep(1000);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BIOProNotBR server = new BIOProNotBR();
server.initBIOServer(8888);
}
}
执行如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
212019-01-02 17:59:03:713: serverSocket started
now time is: 2019-01-02 17:59:04:714
now time is: 2019-01-02 17:59:05:714
now time is: 2019-01-02 17:59:06:714
2019-01-02 17:59:06:932: id为1810132623的Clientsocket connected
now time is: 2019-01-02 17:59:07:934
Not read data: 2019-01-02 17:59:07:935
now time is: 2019-01-02 17:59:08:934
Not read data: 2019-01-02 17:59:08:935
now time is: 2019-01-02 17:59:09:935
Not read data: 2019-01-02 17:59:09:936
收到id为1810132623 2019-01-02 17:59:09: 第0条消息: ccc // <1>
now time is: 2019-01-02 17:59:10:935
Not read data: 2019-01-02 17:59:10:981 // <2>
收到id为1810132623 2019-01-02 17:59:11: 第1条消息: bbb
now time is: 2019-01-02 17:59:11:935
Not read data: 2019-01-02 17:59:12:470
now time is: 2019-01-02 17:59:12:935
id为1810132623的Clientsocket 2019-01-02 17:59:13:191读取结束
now time is: 2019-01-02 17:59:13:935
id为1810132623的Clientsocket 2019-01-02 17:59:14:192读取结束
其中,Not read data输出部分解决了我们的read阻塞问题,每隔1s会去唤醒我们的read操作,如果在1s内没有读到数据的话就会执行System.out.println("Not read data: " + stringNowTime())
,在这里我们就可以进行一些其他操作了,避免了阻塞中当前线程的现象,当我们有数据发送之后,就有了<1>处的输出了,因为read得到输出,所以不再执行catch语句部分,因此你会发现<2>处输出时间是和<1>处的时间相差1s而不是和之前的17:59:09:936相差一秒;1>2>1>
这样的话,我们就解决了accept以及read带来的阻塞问题了,同时在服务端为每一个客户端都创建了一个线程来处理各自的业务逻辑,这点其实基本上已经解决了阻塞问题了,我们可以理解成是最初版的NIO,但是,为每个客户端都创建一个线程这点确实让人头疼的,特别是客户端多了的话,很浪费服务器资源,再加上线程之间的切换开销,更是雪上加霜,即使你引入了线程池技术来控制线程的个数,但是当客户端多起来的时候会导致线程池的BlockingQueue队列越来越大,那么,这时候的NIO就可以为我们解决这个问题,它并不会为每个客户端都创建一个线程,在服务端只有一个线程,会为每个客户端创建一个通道。
accept()本地方法,我们可以来试着看一看Linux这块的相关解读:1
2
3
4
5
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。
备注:新建立的套接字准备发送send()和接收数据recv()。
参数:
sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
备注:addr是个指向局部数据结构sockaddr_in的指针,这就是要求接入的信息本地的套接字(地址和指针)。
addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;
备注:addrlen是个局部整形变量,设置为sizeof(struct sockaddr_in)。
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
备注:一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接;若没有,就阻塞等待;
为了在套接字中有到来的连接时得到通知,可以使用select()或poll()。当尝试建立新连接时,系统发送一个可读事件,然后调用accept()为该连接获取套接字。另一种方法是,当套接字中有连接到来时设定套接字发送SIGIO信号。
返回值
成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。
所以,我们在我们的Java部分的源码里(java.net.ServerSocket#accept)会new 一个Socket出来,方便连接后拿到的新Socket的文件描述符的信息给设定到我们new出来的这个Socket上来,这点在java.net.PlainSocketImpl#socketAccept
中看到的尤为明显,读者可以回顾相关源码。
也是因为之前自己的不谨慎,在写Java编程方法论-Reactor与Webflux
的时候,因觉得tomcat关于connector部分已经有不错的博文了,草草参考了下,并没有对源码进行深入分析,导致自己在录制分享视频的时候,发现自己文章内容展现的和源码并不一致,又通过搜索引擎搜索了一些中文博客的文章,并不尽如人意,索性,自己的就通过最新的源码来重新梳理一下关于tomcat connector部分内容,也是给自己一个警醒,凡事务必仔细仔细再仔细!
参考源码地址: https://github.com/apache/tomcat
关于Java编程方法论-Reactor与Webflux
的视频分享,已经完成了Rxjava 与 Reactor,b站地址如下:
Rxjava源码解读与分享:https://www.bilibili.com/video/av34537840
Reactor源码解读与分享:https://www.bilibili.com/video/av35326911
### 启动与结束Tomcat基本操作
在Linux系统下,启动和关闭Tomcat使用命令操作。
进入Tomcat下的bin目录:1
cd /java/tomcat/bin
启动Tomcat命令:
1 | ./startup.sh |
停止Tomcat服务命令:
1 | ./shutdown.sh |
执行tomcat 的./shutdown.sh
后,虽然tomcat服务不能正常访问了,但是ps -ef | grep tomcat
后,发现tomcat
对应的java
进程未随web容器关闭而销毁,进而存在僵尸java
进程。网上看了下导致僵尸进程的原因可能是有非守护线程(即User Thread)存在,jvm不会退出(当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出)。通过一下命令查看Tomcat进程是否结束:
1 | ps -ef|grep tomcat |
如果存在用户线程,给kill掉就好了即使用kill -9 pid
我们接着从startup.sh
这个shell脚本中可以发现,其最终调用了catalina.sh start
,于是,我们找到catalina.sh
里,在elif [ "$1" = "start" ] ;
处,我们往下走,可以发现,其调用了org.apache.catalina.startup.Bootstrap.java
这个类下的start()
方法:
1 | /** |
这里,在服务器第一次启动的时候,会调用其init()
,其主要用于创建org.apache.catalina.startup.Catalina.java
的类实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
* org.apache.catalina.startup.Bootstrap
* Initialize daemon.
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
接着,在Bootstrap的start()方法中会调用Catalina实例的start方法:
1 | /** |
在这里面,我们主要关心load()
,getServer().start()
,对于后者,在它的前后我们看到有启动时间的计算,这也是平时我们在启动tomcat过程中所看到的日志打印输出所在,后面的我这里就不提了。
首先我们来看load(),这里,其会通过createStartDigester()
创建并配置我们将用来启动的Digester,然后获取我们所配置的ServerXml文件,依次对里面属性进行配置,最后调用getServer().init()
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/**
* org.apache.catalina.startup.Catalina
* Start a new server instance.
*/
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));
File file = configFile();
// Create and execute our Digester
Digester digester = createStartDigester();
try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {
InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (Exception e) {
if (file == null) {
log.warn(sm.getString("catalina.configFail", getConfigFile() + "] or [server-embed.xml"), e);
} else {
log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e);
if (file.exists() && !file.canRead()) {
log.warn(sm.getString("catalina.incorrectPermissions"));
}
}
return;
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error(sm.getString("catalina.initError"), e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info(sm.getString("catalina.init", Long.valueOf((t2 - t1) / 1000000)));
}
}
这里,这个server从哪里来,我们从digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");
中可以知道,其使用了这个类的实例,我们再回到digester.push(this); digester.parse(inputSource);
这两句代码上来,可知,未开始解析时先调用Digester.push(this),此时栈顶元素是Catalina,这个用来为catalina设置server,这里,要对digester
的解析来涉及下:
如解析到<Server>
时就会创建StandardServer
类的实例并反射调用Digester
的stack
栈顶对象的setter
方法(调用的方法通过传入的name
值确定)。digester
中涉及的IntrospectionUtils.setProperty(top, name, value)
方法,即top
为栈顶对象,name
为这个栈顶对象要设置的属性名,value
为要设置的属性值。
刚开始时栈顶元素是Catalina
,即调用Catalina.setServer(Server object)
方法设置Server
为后面调用Server.start()
做准备,然后将StandardServer
对象实例放入Digester
的stack
对象栈中。
接下来,我们来看getServer().init()
,由上知,我们去找org.apache.catalina.core.StandardServer.java
这个类,其继承LifecycleMBeanBase
并实现了Server
,通过LifecycleMBeanBase
此类,说明这个StandardServer
管理的生命周期,即通过LifecycleMBeanBase
父类LifecycleBase
实现的init()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//org.apache.catalina.util.LifecycleBase.java
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}
于是,我们关注 initInternal()
在StandardServer
中的实现,代码过多,这里就把过程讲下:
1、调用父类org.apache.catalina.util.LifecycleMBeanBase#initInternal方法,注册MBean
2、注册本类的其它属性的MBean
3、NamingResources初始化 : globalNamingResources.init();
4、从common ClassLoader开始往上查看,直到SystemClassLoader,遍历各个classLoader对应的查看路径,找到jar结尾的文件,读取Manifest信息,加入到ExtensionValidator#containerManifestResources属性中。
5、初始化service,默认实现是StandardService。
i) 调用super.initInternal()方法
ii) container初始化,这里container实例是StandardEngine。
iii) Executor初始化
iv)Connector初始化:
a)org.apache.catalina.connector.Connector Connector[HTTP/1.1-8080]
b) org.apache.catalina.connector.Connector Connector[AJP/1.3-8009]
这里,我们可以看到StandardServer
的父类org.apache.catalina.util.LifecycleBase.java
的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public final synchronized void start() throws LifecycleException {
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}
return;
}
if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
startInternal();
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
对于StandardServer
,我们关注的是其对于startInternal();
的实现,源码不贴了,具体过程如下:
1、触发CONFIGURE_START_EVENT事件。
2、设置本对象状态为STARTING
3、NameingResource启动:globalNamingResources.start();
4、StandardService启动。
i) 设置状态为STARTING
ii) container启动,即StandardEngine启动
iii) Executor 启动
iv) Connector启动:
a)org.apache.catalina.connector.Connector Connector[HTTP/1.1-8080]
b) org.apache.catalina.connector.Connector Connector[AJP/1.3-8009]
终于,我们探究到了我要讲的主角Connector
。
我们由apache-tomcat-9.0.14\conf
目录(此处请自行下载相应版本的tomcat)下的server.xml中的Connector
配置可知,其默认8080端口的配置协议为HTTP/1.1
。1
2
3
4
5<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
知道了这些,我们去看它的代码中的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public Connector() {
this("org.apache.coyote.http11.Http11NioProtocol");
}
public Connector(String protocol) {
boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
AprLifecycleListener.getUseAprConnector();
if ("HTTP/1.1".equals(protocol) || protocol == null) {
if (aprConnector) {
protocolHandlerClassName = "org.apache.coyote.http11.Http11AprProtocol";
} else {
protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol";
}
} else if ("AJP/1.3".equals(protocol)) {
if (aprConnector) {
protocolHandlerClassName = "org.apache.coyote.ajp.AjpAprProtocol";
} else {
protocolHandlerClassName = "org.apache.coyote.ajp.AjpNioProtocol";
}
} else {
protocolHandlerClassName = protocol;
}
// Instantiate protocol handler
ProtocolHandler p = null;
try {
Class<?> clazz = Class.forName(protocolHandlerClassName);
p = (ProtocolHandler) clazz.getConstructor().newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
} finally {
this.protocolHandler = p;
}
// Default for Connector depends on this system property
setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
}
对于tomcat8.5以上,其默认就是Http11NioProtocol
协议,这里,我们给其设定了HTTP/1.1
,但根据上面的if语句的判断,是相等的,也就是最后还是选择的Http11NioProtocol
。
同样,由上一节可知,我们会涉及到Connector初始化,也就是其也会继承LifecycleMBeanBase
,那么,我们来看其相关initInternal()
实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
protected void initInternal() throws LifecycleException {
super.initInternal();
if (protocolHandler == null) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"));
}
// Initialize adapter
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
if (service != null) {
protocolHandler.setUtilityExecutor(service.getServer().getUtilityExecutor());
}
// Make sure parseBodyMethodsSet has a default
if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}
if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",
getProtocolHandlerClassName()));
}
if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
protocolHandler instanceof AbstractHttp11JsseProtocol) {
AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
(AbstractHttp11JsseProtocol<?>) protocolHandler;
if (jsseProtocolHandler.isSSLEnabled() &&
jsseProtocolHandler.getSslImplementationName() == null) {
// OpenSSL is compatible with the JSSE configuration, so use it if APR is available
jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
}
}
try {
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}
这里涉及的过程如下:
1、注册MBean
2、CoyoteAdapter实例化,CoyoteAdapter是请求的入口。当有请求时,CoyoteAdapter对状态进行了处理,结尾处对请求进行回收,中间过程交由pipeline来处理。
3、protocolHandler 初始化(org.apache.coyote.http11.Http11Protocol)
在这一步中,完成了endpoint的初始化
关于启动就不说了,其设定本对象状态为STARTING,同时调用protocolHandler.start();
,接下来,就要进入我们的核心节奏了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void startInternal() throws LifecycleException {
// Validate settings before starting
if (getPortWithOffset() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
}
setState(LifecycleState.STARTING);
try {
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}
这里,我们直接从其抽象实现org.apache.coyote.AbstractProtocol.java
来看,其也是遵循生命周期的,所以其也要继承LifecycleMBeanBase
并实现自己的init()
与start()
等生命周期方法,其内部都是由相应的自实现的endpoint
来执行具体逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48//org.apache.coyote.AbstractProtocol.java
public void init() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
logPortOffset();
}
if (oname == null) {
// Component not pre-registered so register it
oname = createObjectName();
if (oname != null) {
Registry.getRegistry(null, null).registerComponent(this, oname, null);
}
}
if (this.domain != null) {
rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
Registry.getRegistry(null, null).registerComponent(
getHandler().getGlobal(), rgOname, null);
}
String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length()-1));
endpoint.setDomain(domain);
endpoint.init();
}
public void start() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
logPortOffset();
}
endpoint.start();
monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(
new Runnable() {
public void run() {
if (!isPaused()) {
startAsyncTimeout();
}
}
}, 0, 60, TimeUnit.SECONDS);
}
拿org.apache.coyote.http11.Http11NioProtocol
这个类来讲,其接收的是NioEndpoint
来进行构造器的实现,其内部的方法的具体实现也经由此NioEndpoint
来实现其逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public class Http11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> {
private static final Log log = LogFactory.getLog(Http11NioProtocol.class);
public Http11NioProtocol() {
super(new NioEndpoint());
}
protected Log getLog() { return log; }
// -------------------- Pool setup --------------------
public void setPollerThreadCount(int count) {
((NioEndpoint)getEndpoint()).setPollerThreadCount(count);
}
public int getPollerThreadCount() {
return ((NioEndpoint)getEndpoint()).getPollerThreadCount();
}
public void setSelectorTimeout(long timeout) {
((NioEndpoint)getEndpoint()).setSelectorTimeout(timeout);
}
public long getSelectorTimeout() {
return ((NioEndpoint)getEndpoint()).getSelectorTimeout();
}
public void setPollerThreadPriority(int threadPriority) {
((NioEndpoint)getEndpoint()).setPollerThreadPriority(threadPriority);
}
public int getPollerThreadPriority() {
return ((NioEndpoint)getEndpoint()).getPollerThreadPriority();
}
// ----------------------------------------------------- JMX related methods
protected String getNamePrefix() {
if (isSSLEnabled()) {
return "https-" + getSslImplementationShortName()+ "-nio";
} else {
return "http-nio";
}
}
}
这里,EndPoint
用于处理具体连接和传输数据,即用来实现网络连接和控制,它是服务器对外I/O
操作的接入点。主要任务是管理对外的socket
连接,同时将建立好的socket
连接交到合适的工作线程中去。
里面两个主要的属性类是Acceptor
和Poller
、SocketProcessor
。
我们以NioEndpoint
为例,其内部请求处理具体的流程如下:
结合上一节最后,我们主要还是关注其对于Protocol
有关生命周期方法的具体实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42//org.apache.tomcat.util.net.AbstractEndpoint.java
public final void init() throws Exception {
if (bindOnInit) {
bindWithCleanup();
bindState = BindState.BOUND_ON_INIT;
}
if (this.domain != null) {
// Register endpoint (as ThreadPool - historical name)
oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
Registry.getRegistry(null, null).registerComponent(this, oname, null);
ObjectName socketPropertiesOname = new ObjectName(domain +
":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");
socketProperties.setObjectName(socketPropertiesOname);
Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);
for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
registerJmx(sslHostConfig);
}
}
}
public final void start() throws Exception {
if (bindState == BindState.UNBOUND) {
bindWithCleanup();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}
//org.apache.tomcat.util.net.AbstractEndpoint.java
private void bindWithCleanup() throws Exception {
try {
bind();
} catch (Throwable t) {
// Ensure open sockets etc. are cleaned up if something goes
// wrong during bind
ExceptionUtils.handleThrowable(t);
unbind();
throw t;
}
}
这两个方法主要调用bind
(此处可以查阅bindWithCleanup()
的具体实现) 和startlntemal
方法,它们是模板方法,可以自行根据需求实现,这里,我们参考NioEndpoint
中的实现, bind
方法代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//org.apache.tomcat.util.net.NioEndpoint.java
public void bind() throws Exception {
initServerSocket();
// Initialize thread count defaults for acceptor, poller
if (acceptorThreadCount == 0) {
// FIXME: Doesn't seem to work that well with multiple accept threads
acceptorThreadCount = 1;
}
if (pollerThreadCount <= 0) {
//minimum one poller thread
pollerThreadCount = 1;
}
setStopLatch(new CountDownLatch(pollerThreadCount));
// Initialize SSL if needed
initialiseSsl();
selectorPool.open();
}
这里的bind 方法中首先初始化了ServerSocket
(这个东西我们在jdk网络编程里都接触过,就不多说了,这里是封装了一个工具类,看下面实现),然后检查了代表Acceptor
和Poller
初始化的线程数量的acceptorThreadCount
属性和pollerThreadCount
属性,它们的值至少为1。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Separated out to make it easier for folks that extend NioEndpoint to
// implement custom [server]sockets
protected void initServerSocket() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.socket().bind(addr,getAcceptCount());
} else {
// Retrieve the channel provided by the OS
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
serverSock.configureBlocking(true); //mimic APR behavior
}
这里,Acceptor
用于接收请求,将接收到请求交给Poller
处理,它们都是启动线程来处理的。另外还进行了初始化SSL
等内容。NioEndpoint
的startInternal
方法代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* The socket pollers.
*/
private Poller[] pollers = null;
/**
* Start the NIO endpoint, creating acceptor, poller threads.
*/
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());
// Create worker collection
if ( getExecutor() == null ) {
createExecutor();
}
initializeConnectionLatch();
// Start poller threads
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}
startAcceptorThreads();
}
}
这里首先初始化了一些属性,初始化的属性中的processorCache
是SynchronizedStack<SocketProcessor>
类型, SocketProcessor
是NioEndpoint
的一个内部类, Poller
接收到请求后就会交给它处理, SocketProcessor
又会将请求传递到Handler
。
然后启动了Poller
和Acceptor
来处理请求,这里我们要注意的的是,pollers
是一个数组,其管理了一堆Runnable
,由前面可知,假如我们并没有对其进行设定,那就是1,也就是说,其默认情况下只是一个单线程。这个线程创建出来后就将其设定为守护线程,直到tomcat容器结束,其自然也会跟着结束。
这里,我们想要对其进行配置的话,可以在server.xml
中进行相应设定:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
maxHeaderCount="64"
maxParameterCount="64"
maxHttpHeaderSize="8192"
URIEncoding="UTF-8"
useBodyEncodingForURI="false"
maxThreads="128"
minSpareThreads="12"
acceptCount="1024"
connectionLinger="-1"
keepAliveTimeout="60"
maxKeepAliveRequests="32"
maxConnections="10000"
acceptorThreadCount="1"
pollerThreadCount="2"
selectorTimeout="1000"
useSendfile="true"
selectorPool.maxSelectors="128"
redirectPort="8443" />
启动Acceptor
的startAcceptorThreads
方法在 AbstractEndpoint
中,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15protected void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
Acceptor<U> acceptor = new Acceptor<>(this);
String threadName = getName() + "-Acceptor-" + i;
acceptor.setThreadName(threadName);
acceptors.add(acceptor);
Thread t = new Thread(acceptor, threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}
这里的getAcceptorThreadCount
方法就是获取的init 方法中处理过的acceptorThreadCount属性,获取到后就会启动相应数量的Acceptor 线程来接收请求。默认同样是1,其创建线程的方式和Poller一致,就不多说了。
这里,我们再来看下webapps/docs/config/http.xml的文档说明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<attribute name="acceptorThreadCount" required="false">
<p>The number of threads to be used to accept connections. Increase this
value on a multi CPU machine, although you would never really need more
than <code>2</code>. Also, with a lot of non keep alive connections, you
might want to increase this value as well. Default value is
<code>1</code>.</p>
</attribute>
<attribute name="pollerThreadCount" required="false">
<p>(int)The number of threads to be used to run for the polling events.
Default value is <code>1</code> per processor but not more than 2.<br/>
When accepting a socket, the operating system holds a global lock. So the benefit of
going above 2 threads diminishes rapidly. Having more than one thread is for
system that need to accept connections very rapidly. However usually just
increasing <code>acceptCount</code> will solve that problem.
Increasing this value may also be beneficial when a large amount of send file
operations are going on.
</p>
</attribute>
由此可知,acceptorThreadCount
用于设定接受连接的线程数。 在多CPU机器上增加这个值,虽然你可能真的不需要超过2个。哪怕有很多非keep alive连接,你也可能想要增加这个值。 其默认值为1。pollerThreadCount
用于为轮询事件运行的线程数。默认值为每个处理器1个但不要超过2个(上面的优化配置里的设定为2)。接受socket时,操作系统将保持全局锁定。 因此,超过2个线程的好处迅速减少。 当系统拥有多个该类型线程,它可以非常快速地接受连接。 尽管增加acceptCount就可以解决这个问题。但当正在进行大量发送文件操作时,增加此值也可能是有益的。
我们先来看一张NioEndpoint处理的的时序图:
我们由前面可知,Acceptor和Poller都实现了Runnable接口,所以其主要工作流程就在其实现的run方法内,这里我们先来看Acceptor对于run方法的实现:
1 | //org.apache.tomcat.util.net.NioEndpoint.java |
由上面run方法可以看到,Acceptor
使用serverSock.accept()
阻塞的监听端口,如果有连接进来,拿到了socket
,并且EndPoint
处于正常运行状态,则调用NioEndPoint
的setSocketOptions
方法,对于setSocketOptions
,概括来讲就是根据socket
构建一个NioChannel
,然后把这个的NioChannel
注册到Poller
的事件列表里面,等待poller
轮询:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59/**
* org.apache.tomcat.util.net.NioEndpoint.java
* Process the specified connection.
* 处理指定的连接
* @param socket The socket channel
* @return <code>true</code> if the socket was correctly configured
* and processing may continue, <code>false</code> if the socket needs to be
* close immediately
* 如果socket配置正确,并且可能会继续处理,返回true
* 如果socket需要立即关闭,则返回false
*/
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
//disable blocking, APR style, we are gonna be polling it
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);
//从缓存中拿一个nioChannel 若没有,则创建一个。将socket传进去
NioChannel channel = nioChannels.pop();
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) {
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset();
}
//从pollers数组中获取一个Poller对象,注册这个nioChannel
getPoller0().register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error(sm.getString("endpoint.socketOptionsError"), t);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
// Tell to close the socket
return false;
}
return true;
}
/**
* Return an available poller in true round robin fashion.
*
* @return The next poller in sequence
*/
public Poller getPoller0() {
int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
return pollers[idx];
}
关于getPoller0()
,默认情况下, 由前面可知,这个pollers数组里只有一个元素,这点要注意。我们来看NioEndPoint中的Poller实现的register方法,主要做的就是在Poller注册新创建的套接字。
1 | /** |
对以上过程进行一下总结:
从Acceptor接收到请求,它做了如下工作:
在这里,会调用NioEndPoint的setSocketOptions方法,处理指定的连接:
其中最后一步注册的过程,是调用Poller的register()方法:
由前面可知,poller也实现了Runnable接口,并在start的这部分生命周期执行的过程中创建对应工作线程并加入其中,所以,我们来通过其run方法来看下其工作机制。
其实上面已经提到了Poller将一个事件注册到事件队列的过程。接下来Poller线程要做的事情其实就是如何处理这些事件。
Poller在run方法中会轮询事件队列events,将每个PollerEvent中的SocketChannel的interestOps注册到Selector中,然后将PollerEvent从队列里移除。之后就是SocketChanel通过Selector调度来进行非阻塞的读写数据了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102/**
* Poller class.
*/
public class Poller implements Runnable {
private Selector selector;
private final SynchronizedQueue<PollerEvent> events =
new SynchronizedQueue<>();
private volatile boolean close = false;
private long nextExpiration = 0;//optimize expiration handling
private AtomicLong wakeupCounter = new AtomicLong(0);
private volatile int keyCount = 0;
public Poller() throws IOException {
this.selector = Selector.open();
}
public int getKeyCount() { return keyCount; }
public Selector getSelector() { return selector;}
/**
* The background thread that adds sockets to the Poller, checks the
* poller for triggered events and hands the associated socket off to an
* appropriate processor as events occur.
*/
public void run() {
// Loop until destroy() is called
// 循环直到 destroy() 被调用
while (true) {
boolean hasEvents = false;
try {
if (!close) {
//遍历events,将每个事件中的Channel的interestOps注册到Selector中
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
//if we are here, means we have other stuff to do
//do a non blocking select
//如果走到了这里,代表已经有就绪的IO Channel
//调用非阻塞的select方法,直接返回就绪Channel的数量
keyCount = selector.selectNow();
} else {
//阻塞等待操作系统返回 数据已经就绪的Channel,然后被唤醒
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error(sm.getString("endpoint.nio.selectorLoopError"), x);
continue;
}
//either we timed out or we woke up, process events first
//如果上面select方法超时,或者被唤醒,先将events队列中的Channel注册到Selector上。
if ( keyCount == 0 ) hasEvents = (hasEvents | events());
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// Walk through the collection of ready keys and dispatch
// any active event.
// 遍历已就绪的Channel,并调用processKey来处理该Socket的IO。
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
// 如果其它线程已调用,则Attachment可能为空
if (attachment == null) {
iterator.remove();
} else {
iterator.remove();
//创建一个SocketProcessor,放入Tomcat线程池去执行
processKey(sk, attachment);
}
}//while
//process timeouts
timeout(keyCount,hasEvents);
}//while
getStopLatch().countDown();
}
...
}
上面读取已就绪Channel的部分,是十分常见的Java NIO的用法,即 Selector调用selectedKeys(),获取IO数据已经就绪的Channel,遍历并调用processKey方法来处理每一个Channel就绪的事件。而processKey方法会创建一个SocketProcessor,然后丢到Tomcat线程池中去执行。
这里还需要注意的一个点是,events()方法,用来处理PollerEvent事件,执行PollerEvent.run(),然后将PollerEvent重置再次放入缓存中,以便对象复用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/**
* Processes events in the event queue of the Poller.
*
* @return <code>true</code> if some events were processed,
* <code>false</code> if queue was empty
*/
public boolean events() {
boolean result = false;
PollerEvent pe = null;
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
result = true;
try {
//把SocketChannel的interestOps注册到Selector中
pe.run();
pe.reset();
if (running && !paused) {
eventCache.push(pe);
}
} catch ( Throwable x ) {
log.error(sm.getString("endpoint.nio.pollerEventError"), x);
}
}
return result;
}
所以,PollerEvent.run()方法才是我们关注的重点:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69/**
* PollerEvent, cacheable object for poller events to avoid GC
*/
public static class PollerEvent implements Runnable {
private NioChannel socket;
private int interestOps;
private NioSocketWrapper socketWrapper;
public PollerEvent(NioChannel ch, NioSocketWrapper w, int intOps) {
reset(ch, w, intOps);
}
public void reset(NioChannel ch, NioSocketWrapper w, int intOps) {
socket = ch;
interestOps = intOps;
socketWrapper = w;
}
public void reset() {
reset(null, null, 0);
}
public void run() {
//Acceptor调用Poller.register()方法时,创建的PollerEvent的interestOps为OP_REGISTER,因此走这个分支
if (interestOps == OP_REGISTER) {
try {
socket.getIOChannel().register(
socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {
log.error(sm.getString("endpoint.nio.registerFail"), x);
}
} else {
final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
if (key == null) {
// The key was cancelled (e.g. due to socket closure)
// and removed from the selector while it was being
// processed. Count down the connections at this point
// since it won't have been counted down when the socket
// closed.
socket.socketWrapper.getEndpoint().countDownConnection();
((NioSocketWrapper) socket.socketWrapper).closed = true;
} else {
final NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment();
if (socketWrapper != null) {
//we are registering the key to start with, reset the fairness counter.
int ops = key.interestOps() | interestOps;
socketWrapper.interestOps(ops);
key.interestOps(ops);
} else {
socket.getPoller().cancelledKey(key);
}
}
} catch (CancelledKeyException ckx) {
try {
socket.getPoller().cancelledKey(key);
} catch (Exception ignore) {}
}
}
}
public String toString() {
return "Poller event: socket [" + socket + "], socketWrapper [" + socketWrapper +
"], interestOps [" + interestOps + "]";
}
}
至此,可以看出Poller线程的作用
剩下的事情,就是SocketProcessor怎么适配客户端发来请求的数据、然后怎样交给Servlet容器去处理了。
即Poller的run方法中最后调用的processKey(sk, attachment);
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
try {
if ( close ) {
cancelledKey(sk);
} else if ( sk.isValid() && attachment != null ) {
if (sk.isReadable() || sk.isWritable() ) {
if ( attachment.getSendfileData() != null ) {
processSendfile(sk,attachment, false);
} else {
unreg(sk, attachment, sk.readyOps());
boolean closeSocket = false;
// Read goes before write
if (sk.isReadable()) {
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
if (closeSocket) {
cancelledKey(sk);
}
}
}
} else {
//invalid key
cancelledKey(sk);
}
} catch ( CancelledKeyException ckx ) {
cancelledKey(sk);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.nio.keyProcessingError"), t);
}
}
即从processSocket
这个方法中会用到SocketProcessor
来处理请求:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* Process the given SocketWrapper with the given status. Used to trigger
* processing as if the Poller (for those endpoints that have one)
* selected the socket.
*
* @param socketWrapper The socket wrapper to process
* @param event The socket event to be processed
* @param dispatch Should the processing be performed on a new
* container thread
*
* @return if processing was triggered successfully
*/
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
return false;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This means we got an OOM or similar creating a thread, or that
// the pool and its queue are full
getLog().error(sm.getString("endpoint.process.fail"), t);
return false;
}
return true;
}
这里简单提一下SocketProcessor
的处理过程,帮助大家对接到Servlet容器处理上。通过上面可以知道,具体处理一个请求,是在SocketProcessor通过线程池去执行的,这里,我们来看其执行一次请求的时序图:
由图中可以看到,SocketProcessor
中通过Http11ConnectionHandler
,拿到Htpp11Processor
,然后Htpp11Processor
会调用prepareRequest
方法来准备好请求数据。接着调用CoyoteAdapter
的service
方法进行request
和response
的适配,之后交给Tomcat
容器进行处理。
下面通过一个系列调用来表示下过程:
connector.getService().getContainer().getPipeline().getFirst().invoke(request,response);
这里首先从Connector 中获取到Service ( Connector 在initInternal 方法中创建CoyoteAdapter的时候已经将自己设置到了CoyoteAdapter 中),然后从Service 中获取Container ,接着获取管道,再获取管道的第一个Value,最后调用invoke 方法执行请求。Service 中保存的是最顶层的容器,当调用最顶层容器管道的invoke 方法时,管道将逐层调用各层容器的管道中Value 的invoke 方法,直到最后调用Wrapper 的管道中的BaseValue ( StandardWrapperValve)来处理Filter 和Servlet。
将请求交给Tomcat容器处理后,然后将请求一层一层传递到Engine、Host、Context、Wrapper,最终经过一系列Filter,来到了Servlet,执行我们自己具体的代码逻辑。
至此关于Connector的一些东西就算涉及差不多了,剩下的假如以后有精力的话,继续探究下,接着分享Webflux的解读去。
补充:
感谢零度大佬(博客:http://www.jiangxinlingdu.com)的提问,这里我将自己的一些额外的问题理解进行内容补充:
这里对于其中NioEndpoint
中其有关生命周期部分的实现所涉及的initServerSocket()
再来关注下细节:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Separated out to make it easier for folks that extend NioEndpoint to
// implement custom [server]sockets
protected void initServerSocket() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.socket().bind(addr,getAcceptCount());
} else {
// Retrieve the channel provided by the OS
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
serverSock.configureBlocking(true); //mimic APR behavior
}
其最后一句,为什么tomcat这个不设置非阻塞?这会儿是刚初始化的时候,设定为阻塞状态,阻塞也只是阻塞在这个线程上,即Acceptor
在一条线程内执行其run方法的时候,会调用endpoint.serverSocketAccept()
来创建一个socketChannel
,接收下一个从服务器进来的连接。当成功接收到,重新对此socket
进行配置,即会调用endpoint.setSocketOptions(socket)
,在这个方法内,会调用 socket.configureBlocking(false);
,此时,会开启SocketChannel
在非阻塞模式,具体代码请回顾本文前面细节。
最近给小伙伴分享了Rxjava的源码解读,并录制成视频,也是为了配合自己的未来出版的书,也是对书的内容的补充,将未能写进去的内容通过视频来展现,也加入了一些自己的理解。希望可以对大家有用。
以下为视频分享内容:
01 响应式入门:https://www.bilibili.com/video/av34537840/?p=1
02 Java9中的响应式编程:https://www.bilibili.com/video/av34537840/?p=2
03 Rxjava开篇:https://www.bilibili.com/video/av34537840/?p=3
04 Rxjava中create方法的设计思想:https://www.bilibili.com/video/av34537840/?p=4
05 Observables和Observable.cache():https://www.bilibili.com/video/av34537840/?p=5
06 无休止数据流与定时控制:https://www.bilibili.com/video/av34537840/?p=6
07 Demo的设计初衷:https://www.bilibili.com/video/av34537840/?p=7
08 Observable.cache()源码解读:https://www.bilibili.com/video/av34537840/?p=8
09 ConnectableObservable与publish().refCount()解读:https://www.bilibili.com/video/av34537840/?p=9
10 SubmissionPublisher 中订阅者状态的管理:https://www.bilibili.com/video/av34537840/?p=10
11 RxJava中Subject解读:
https://www.bilibili.com/video/av34537840/?p=11
12 filter() map()深入解读与flatMap()初解:https://www.bilibili.com/video/av34537840/?p=12
13 flatMap()与scan()深入解读:https://www.bilibili.com/video/av34537840/?p=13
14 groupBy()进行分组:https://www.bilibili.com/video/av34537840/?p=14
15 merge()的源码解读 上:https://www.bilibili.com/video/av34537840/?p=15
16 merge()的源码解读 下:https://www.bilibili.com/video/av34537840/?p=16
17 zip()的源码解读:https://www.bilibili.com/video/av34537840/?p=17
18 combineLatest()的源码解读:https://www.bilibili.com/video/av34537840/?p=18
19 withLatestFrom() 源码解读:https://www.bilibili.com/video/av34537840/?p=19
20 amb() 操作源码解读:https://www.bilibili.com/video/av34537840/?p=20
21 scan()操作的2次深入:https://www.bilibili.com/video/av34537840/?p=21
22 reduce()源码解读:https://www.bilibili.com/video/av34537840/?p=22
23 collect() 源码解读:https://www.bilibili.com/video/av34537840/?p=23
24 distinct() distinctUntilChanged() compose() lift()及其他操作源码解读:https://www.bilibili.com/video/av34537840/?p=24
25 Observable实战之Spring MVC返回值的响应式化改造:https://www.bilibili.com/video/av34537840/?p=25
26 汇率查询的小服务及对于返回值处理抽取的前置知识讲解:https://www.bilibili.com/video/av34537840/?p=26
27 写一个SpringMVC的响应式返回值处理组件springboot-starter:https://www.bilibili.com/video/av34537840/?p=27
28 RxJava2中的多线程操作中调度器的引入:https://www.bilibili.com/video/av34537840/?p=28
29 subscribeOn() observeOn() unsubscribeOn()操作源码解读:https://www.bilibili.com/video/av34537840/?p=29
30 调度器Scheduler源码设计思路解读:https://www.bilibili.com/video/av34537840/?p=30
31 调度器Scheduler源码解读补充1:https://www.bilibili.com/video/av34537840/?p=31
32 调度器Scheduler源码解读补充2:https://www.bilibili.com/video/av34537840/?p=32
33 调度器Scheduler源码解读补充3:https://www.bilibili.com/video/av34537840/?p=33
34 背压回顾以及一些探究:https://www.bilibili.com/video/av34537840/?p=34
35 rxjava中SpscLinkedArrayQueue无界队列的实现解读:https://www.bilibili.com/video/av34537840/?p=35
36 从Observable到 Flowable 的设计思路 及Flowable.create() 中背压设计的解读:https://www.bilibili.com/video/av34537840/?p=36
37 onBackpressureXXX()操作与Flowable.generate()解读:https://www.bilibili.com/video/av34537840/?p=37
38 关于Rxjava解读简短的结束语:https://www.bilibili.com/video/av34537840/?p=38
]]>现实生活中,当我们听到有人喊我们的时候,我们会对其进行响应,也就是说,我们是基于事件驱动模式来进行的编程。
所以这个过程其实就是对于所产生事件的下发,我们的消费者对其进行的一系列的消费。
从这个角度,我们可以思考,整个代码的设计我们应该是针对于消费者来讲的,比如看电影,有些画面我们不想看,那就闭上眼睛,有些声音不想听,那就捂上耳朵,说白了,就是对于消费者的增强包装,我们将这些复杂的逻辑给其拆分,然后分割成一个个的小任务进行封装,于是就有了诸如filter、map、skip、limit等操作。而对于其中源码的设计逻辑,我们放在后面来讲。
可以这么说,并发很好的利用了CPU时间片的特性,也就是操作系统选择并运行一个任务,接着在下一个时间片会运行另一个任务,并把前一个任务设置成等待状态。
其实这里想表达的是并发并不意味着并行。
具体来举几个情况:
有时候多线程执行会提高应用程序的性能,而有时候反而会降低程序的性能。这在关于JDK中其Stream API的使用上体现的很明显,如果任务量很小,而我们又使用了并行流,反而降低了性能。
我们在多线程编程中可能会同时开启或者关闭多个线程,这会产生的很多性能开销,这也降低了程序性能。
当我们的线程同时都在等待IO过程,此时并发也就可能会阻塞CPU资源,其造成的后果不仅仅是用户在等待结果,同时会浪费CPU的计算资源。
如果几个线程共享了一个数据,情况就变得有些复杂了,我们需要考虑数据在各个线程中状态的一致性。为了达到这个目的,我们很可能会使用Synchronized或者是lock来解决。
现在,应该对并发有一定的认知了吧。并发是一个很好的东西,但并不一定会实现并行。并行是在多个CPU核心上的同一时间运行多个任务或者一个任务分为多块执行(如ForkJoin)。单核CPU的话就不要考虑了。
补充一点,实际上多线程就意味着并发,但是并行只发生在当这些线程在同一时间调度分配在不同CPU上执行。也就是说,并行是并发的一种特定的形式。往往我们一个任务里会产生很多元素,然而这些个元素在不做操作的情况下大都只能在当前线程中操作,要么我们就要对其进行ForkJoin,但这些对于我们很多程序员来讲有时候很不好操作控制,上手难度有些高,响应式的话,我们可以简单的通过其调度API就可以轻松做到事件元素的下发分配,其内部将每个元素包装成一个任务提交到线程池中,我们可以根据是否是计算型任务还是IO类型的任务来选择相应的线程池。
这里,需要强调一下:线程只是一个对象而已,不要把其想象成cpu中的某一个执行核心,这是很多人都在犯的错,cpu时间片切换执行这些个线程。
用一个不算很恰当的中国的成语来讲,就是承上启下。为了更好的解释,我们来看一个场景,大坝,在洪水时期,下游没有办法一下子消耗那么多水,大坝在此的作用就是拦截洪水,并根据下游的消耗情况酌情排放。再者,父亲的背,我们小时候,社会上很多的事情首先由父亲用自己的背来帮我们来扛起,然后根据我们自身的能力来适当的下发给我们压力,也就是说,背压应该写在连接元素生产者和消费者的一个地方,即生产者和消费者的连线者。然后,通过这里的描述,背压应该具有承载元素的能力,也就是其必须是一个容器的,而且元素的存储与下发应该具有先后的,那么使用队列则是最适合不过了。
关于响应式的Rx标准已经写入了JDK中:java.util.concurrent.Flow
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static interface Publisher<T> {
public void subscribe(Subscriber<? super T> subscriber);
}
public static interface Subscriber<T> {
public void onSubscribe(Subscription subscription);
public void onNext(T item);
public void onError(Throwable throwable);
public void onComplete();
}
public static interface Subscription {
public void request(long n);
public void cancel();
}
public static interface Processor<T,R> extends Subscriber<T>, Publisher<R> {
}
可以看到,Flow这个类中包含了这4个接口定义,Publisher
通过subscribe
方法来和Subscriber
产生订阅关系,而Subscriber
依靠onSubscribe
来首先和上游产生联系,这里就是靠Subscription
来做到的,所以说,Subscription
往往会作为生产者的内部类定义其中,其用来接收生产者所生产的元素,支持背压的话,Subscription
应该首先将其放入到一个队列中,然后根据请求数量来调用Subscriber
的onNext
等方法进行下发。这个在Rx编程中都是统一的模式,我们通过Reactor中reactor.core.publisher.Flux#fromArray
所涉及的FluxArray
的源码来对此段内容进行理解:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129final class FluxArray<T> extends Flux<T> implements Fuseable, Scannable {
final T[] array;
public FluxArray(T... array) {
this.array = Objects.requireNonNull(array, "array");
}
"unchecked") (
public static <T> void subscribe(CoreSubscriber<? super T> s, T[] array) {
if (array.length == 0) {
Operators.complete(s);
return;
}
if (s instanceof ConditionalSubscriber) {
s.onSubscribe(new ArrayConditionalSubscription<>((ConditionalSubscriber<? super T>) s, array));
}
else {
s.onSubscribe(new ArraySubscription<>(s, array));
}
}
public void subscribe(CoreSubscriber<? super T> actual) {
subscribe(actual, array);
}
public Object scanUnsafe(Attr key) {
if (key == Attr.BUFFERED) return array.length;
return null;
}
static final class ArraySubscription<T>
implements InnerProducer<T>, SynchronousSubscription<T> {
final CoreSubscriber<? super T> actual;
final T[] array;
int index;
volatile boolean cancelled;
volatile long requested;
"rawtypes") (
static final AtomicLongFieldUpdater<ArraySubscription> REQUESTED =
AtomicLongFieldUpdater.newUpdater(ArraySubscription.class, "requested");
ArraySubscription(CoreSubscriber<? super T> actual, T[] array) {
this.actual = actual;
this.array = array;
}
public void request(long n) {
if (Operators.validate(n)) {
if (Operators.addCap(REQUESTED, this, n) == 0) {
if (n == Long.MAX_VALUE) {
fastPath();
}
else {
slowPath(n);
}
}
}
}
void slowPath(long n) {
final T[] a = array;
final int len = a.length;
final Subscriber<? super T> s = actual;
int i = index;
int e = 0;
for (; ; ) {
if (cancelled) {
return;
}
while (i != len && e != n) {
T t = a[i];
if (t == null) {
s.onError(new NullPointerException("The " + i + "th array element was null"));
return;
}
s.onNext(t);
if (cancelled) {
return;
}
i++;
e++;
}
if (i == len) {
s.onComplete();
return;
}
n = requested;
if (n == e) {
index = i;
n = REQUESTED.addAndGet(this, -e);
if (n == 0) {
return;
}
e = 0;
}
}
}
void fastPath() {...}
}
static final class ArrayConditionalSubscription<T>
implements InnerProducer<T>, SynchronousSubscription<T> {
....
}
}
我们可以看到之前文字在源码内部的表达。这里就不多说了。而对于各种中间操作的包装我们该如何去做,依据之前的接口定义,我们应该更注重功能的设定,而无论是filter,flatmap,map等这些常用的操作,其实都是消费动作,理应定义在消费者层面,想到这里,我们该如何去做?
这里,我们就要结合我们的设计模式,装饰模式,对subscribe(Subscriber<? super T> subscriber)
所传入的Subscriber
进行功能增强,即从Subscriber
这个角度来讲,使用的是装饰增强模式,但从外面来看,其整体定义的依然是一个Flux
或者Mono
,这里FluxArray
的话就是例子,这样,从这个角度来讲,其属于向上适配,也就是适配模式,这里的适配玩的比较有意思,完全就是靠对内部类的包装然后通过subscribe(Subscriber<? super T> subscriber)
衔接来完成的。
所以,我们应该想到中国古代苏轼的题西林壁里有一句话:横看成岭侧成峰 远近高低各不同
讲的就是从不同的角度去看待一个事物,就会得到不同的结果。同样,一百个人心中有一百个哈姆雷特,也是对于同一个事物的看法,从这里,我们应该能学到设计模式千万不要特别刻意的去绝对化!
我们可以结合reactor.core.publisher.Flux#filter
涉及的FluxFilter
来观察理解上述涉及的内容:
1 | final class FluxFilter<T> extends FluxOperator<T, T> { |
根据这些设计,我们自己也是完全可以作为参考来通过一套api接口设计,可以衍生出很多规范逻辑的开发,比如我们看到的众多的Rx衍生操作API的设计实现,其都是按照一套模板来进行的,我们可以称之为代码层面的微服务设计。
人类最擅长描述场景,比如一套动作,假如是舞蹈的话,可以讲是什么什么编舞,但是这个编舞又要在一定的框架之下,即有一定的规范,同样,我们施展一套拳法,也需要一个规范,不能踢一脚也叫拳法。而对于这个规范的实现,那就是一套动作,对于拳法来讲,可能就是一个很简单的左勾拳或者右勾拳,也可以是比较复杂的咏春拳,太极拳等,而且一套拳法可能有很多小套路组成,这些小套路也是遵循着这个规范进行的,那么依据这个思路,我们来看下面的函数式接口定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
可以看到无论是条件判断表达式Predicate
还是无返回值动作处理函数BiConsumer
都遵循一个标准动作的设计定义思路,并通过default
方法来对同类动作进行编排,以达到更加丰富的效果。所以,函数式的应用更加倾向于干净利落,凸显自己要做的事情就好,未来,我会在自己的Java编程方法论- JDK篇
中花大量篇幅来解读函数式编程的各种奇特而实用的使用方法,来降低我们复杂接口的设计逻辑难度,做到知名见义,了然于胸的效果。这个在我的Java编程方法论- Reactor与Spring webflux篇
中也是有涉及的。
响应式编程知识一种模式,用的好与坏全看自己对于api的理解程度,不要想着会多么的降低性能,这个并没有进行什么过度包装这一说的,当讲到jdbc这里如何表现不行的时候,当前并没有一个开源的Reactor-jdbc的框架,也就造成的测试的不合理性,何况新的知识是需要大家一起共同来学习推动的,不好的地方我们推动就好,不需要上来就对其进行否定,mongodb有提供相应的响应式api,但其内部还是之前的方式,同样,关系型数据库也是一个道理,响应式编程注重的是中间过程的处理,关于生产元素的获取它没太多关系,更多的还是看元素生产者的性能,一家之言,可能有偏颇,希望理解,有问题提出就好。
]]> 首先我们先针对于上一节讲的给出一个很重要的区别:
CountDownLatch 很明显是可以不限制等待线程的数量,而会限制 countDown的操作数。
CyclicBarrier 会限制等待线程的数量。
我们来看JDK给我们带来的两种用法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class Driver { // ...
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
doSomethingElse(); // don't let run yet <1>
startSignal.countDown(); // let all threads proceed <2>
doSomethingElse(); // <3>
doneSignal.await(); // wait for all to finish <4>
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await(); // <5>
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}}
这里其实就是在传达信息,首先,这里定义了一个所传状态值为1的 startSignal和状态值为N的 doneSignal,然后通过for循环起了N个线程执行任务,但是在这些线程执行具体任务之前我主线程里有一波逻辑必须先行(因为有些变量的设定是子线程里共享的东西),那么,我就可以在其内进行 startSignal.await()的设定,可以看到,我这里N可以是很大的一个数字,这也就是我们上面讲的 CountDownLatch的一个很强的特性的应用,接着,在我主线程的一波先行逻辑执行完后(请看<1>
),我就可以放行,于是就可以调用<2>
处的 startSignal.countDown(),对各个线程进行解除挂起,这里<3>
处的代码就和各个子线程里的任务没有什么冲突,也就没什么happen-before
这种要求限定了,但我们其他线程就有担心你主线程执行完我任务没完成怎么办,使用sleep?我执行完主线程可能还在等待,这个时间真的不确定,那就在主线程里使用<4>
处的代码 doneSignal.await(),这样,当我各个子线程都结束的时候,我就可以做到主线程在第一时间也可以结束掉省的浪费资源了,这里,有童鞋可能会说主线程里也可以调用XxxThread.join()
,但要注意的是,当一个线程调用之后,主线程就休眠了,剩下的join()
操作也就无从谈起了,也就是说其他线程结束的时候会调用一下 this.notifyAll但仅针对于这个要结束的线程,所以主线程可能会经历休眠启动,再休眠,再启动,这就浪费性能了。
我们接着看JDK
给我们提供的第二个常用使用场景例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Driver2 { // ...
void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = ...
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}}
这里就实现了一个分治算法
应用,首先,我们可以将要做的工作进行策略分割,也就是 doWork()方法实现,里面可以根据所传参数进行策略执行,因为任务要放到线程中执行,而且这里还涉及到了一个策略分配,往往,我们的任务在大局上可以很快的进行策略分块操作,然后,每一个块内我们可以根据情况假如复杂再进行一个forkJoin的一个应用,这里我们无须去考虑那么多,我们通过实现一个 Runnable来适配Thread
需求,这里,为了适应子线程和主线程的等待执行关系,使用了CountDownLatch
来实现,通过上一个例子,大家应该很清楚了,主线程传入一个定义的 CountDownLatch对象,子线程调用,在其 Runnable.run方法的最后调用 doneSignal.countDown()。主线程在其最后调用 doneSignal.await(),这都是固定套路,记住就好。
最后,在 doWork()中根据策略得到的任务很复杂的话,就可以使用 forkJoin策略进行二次分治了,这样就可以做到,分模块,有计算型的模块,也有IO型的模块,而且这些模块彼此不影响,每个模块内部的话可能会有共享数据的情况,就需要根据并发的其他知识进行解决了,这里就不多讲了,具体情况具体分析。
本文配套分享视频:
http://v.youku.com/v_show/id_XMzYwMDE3ODA3Ng==.html?spm=a2h3j.8428770.3416059.1
]]>我们先来读下CountDownLatch这个类的注释:1
2
3
4/**
* A synchronization aid that allows one or more threads to wait until
* a set of operations being performed in other threads completes.
**/
此处说明了其使用场景允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。这里有两个关键点:等待,一组操作完成。这里要强调的是,等待并不意味着线程一定挂起,一组操作完成并不意味着其中一个操作所在的线程就会结束,这是两码事。
接着来看第二段注释:1
2
3
4
5
6
7
8
9/**
*<p>A {@code CountDownLatch} is initialized with a given <em>count</em>.
* The {@link #await await} methods block until the current count reaches
* zero due to invocations of the {@link #countDown} method, after which
* all waiting threads are released and any subsequent invocations of
* {@link #await await} return immediately. This is a one-shot phenomenon
* -- the count cannot be reset. If you need a version that resets the
* count, consider using a {@link CyclicBarrier}.
**/
从此处可以知道,CountDownLatch用给定的count进行初始化。 调用await方法会产生阻塞,直到当前计数count由于调用countDown方法而减至零,此后所有等待的线程被释放,并且后续无论是哪个线程再次进行await调用都会立即返回,不会产生其他动作。 也就是说,这是一次性使用的工具,其计数无法重置。 如果你需要重置计数的版本,请考虑使用CyclicBarrier。
这里,我们可以结合下源码来进一步解读,我们首先会看到,CountDownLatch只定义了一个private final Sync sync;字段,其是final类型,一旦赋值就不可变。
我们先来说CountDownLatch的初始化:1
2
3
4public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
可以看到,这里主要还是创建了一个Sync
实例,而这也是这个类的核心所在,它是一个针对于CountDownLatch而专门设计的一个实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
其主要还是利用AQS的volatile字段state来进行状态的控制,这也是我们可以进行CAS操作的核心所在。
我们在前面知道,调用await方法会产生阻塞,那么这里我们就来看下await:
1 | // java.util.concurrent.CountDownLatch#await() |
这里,我们看到了Shared
,我们仔细追寻,在AQS
的的内部类node中,有定义字段EXCLUSIVE和SHARED这俩就代表了两种情况的使用,独占和共享。其主要还是针对于资源的使用情况来讲的,前者,是对资源,这里就是这个state状态值,单个线程独占这个资源,不为0,不放弃。后者主要是将state状态值共享出来,几个线程都可以操作。而两者应用最大的区别就在于tryAcquire和tryAcquireShared的实现。这里,我并不会对ReentrantLock中的tryAcquire进行讲解。其他地方基本一致,差别点就在于addWaiter(Node.XXX)
传入的类型不同,acquireQueued
与doAcquireSharedInterruptibly
实现思路大致相仿,只是会根据自己实际实现略作调整。这里,我们就专门针对于CountDownLatch所涉及到的进行解读。
题外话:我们通过知道独占与共享的设计区别,我们就可以很轻松的设计出属于自己的一些特有逻辑的实现,主要还是在于我们首先确定api选型,然后重写相应重点方法即可。
从acquireSharedInterruptibly方法名称可以知道,其是可打断的,而且每一个调用await
正常来讲都是在一个独立的线程中的,那么这个独立的线程在整个过程中都有可能被打断掉。
我们参考上面CountDownLatch中Sync的tryAcquireShared实现,状态不为0就进入doAcquireSharedInterruptibly方法中去,这个方法就是,首先先构造个节点,这个节点有绑定当前所在线程,然后让你进个队列,接着,我们的任务就是无限循环找我们前置节点到底是不是头节点,是的话,就再试着获取下状态值,当看到大于0了,对于CountDownLatch中Sync里的实现就是1,那就进入setHeadAndPropagate(node, r);:
1 | /** |
前面的都能看懂,这里要强调的是,因为你是Shared,还有一点我们需要思考的是,什么时候才会发生tryAcquireShared(1)>0 (这里的参数1在CountDownLatch中Sync的tryAcquireShared实现里没有什么意义)?就是在状态值为0的时候,也就是产生释放的时候,即调用java.util.concurrent.CountDownLatch#countDown将状态值减为0的时候,然后激活头节点,所以我们这里首先释放的其实就是头节点,那读者可能会有疑问,那pre节点是什么,这也是我要强调的,pre节点并不一定是头节点,但是头节点的pre节点绝对就是自身,
下面我将三者的源码给出,可以很轻易的看到,假如是头节点,那么在for循环下,就再进行一次其pre节点的设定,初次设定的时候头尾都是自身。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer.Node#predecessor
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#initializeSyncQueue
/**
* Initializes head and tail fields on first contention.
*/
private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
至此,我们知道,在CountDownLatch作释放为0的时候,会率先激活头节点,然后后面的逻辑就是依次将自己设定成头节点,并将自身节点的线程状态由需要SIGNAL变为0,即属于正常运行状态,这样,我们方便在unparkSuccessor方法中激活下一个节点的所绑定的线程,而当下一个节点为空或者这个节点的线程状态标识位大于0也就是CANCELLED的时候,这里就可以根据最后一个节点的来获取线程还未激活的最靠前的那个节点,接下来就是激活这个节点的线程了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
最后,我们再次回到doAcquireSharedInterruptibly中,这里,我们来看其在最初调用await方法时候所进行的动作:1
2if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
这是for循环最后所进行的一个操作,if判断里,前者设定了该node所绑定线程需要进行singal的标志位的设定,接着对其所属线程进行线程挂起操作。代码如下,省的大家找了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
/**
* Convenience method to park and then check if interrupted.
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
对于shouldParkAfterFailedAcquire里面的代码,这里需要我给大家解惑的是,在我已确定我要挂起的情况下,因为当我是头节点的情况下,tryAcquireShared
返回的是-1,何况后面非头结点直接进入这个if语句中。但是,这个await方法的调用可能前后很快,第一次设定状态的时候依然会返回一次false,并不会进行线程挂起,所以就需要那个do while语句来判断waitStatus标志位,这样,我们就可以找到最靠近头结点的那个未将标志位设定singal的那个节点所在。
对于parkAndCheckInterrupt,我们关心的是LockSupport.park(this);1
2
3
4
5
6
7//java.util.concurrent.locks.LockSupport#park(java.lang.Object)
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}
这里首先将线程和所传对象进行setBlocker绑定,告诉我们这里是因为谁而线程挂起的,方便一但出现异常,我们好通过日志确认,然后进行挂起,在挂起结束后就解除标记对象。
至此关于CountDownLatch涉及完毕。
本文配套分享视频地址:
http://v.youku.com/v_show/id_XMzU5Nzc5NjI0NA==.html?spm=a2h3j.8428770.3416059.1
]]>在谈Spring Data之前,要先讲讲JPA,讲JPA,又不免会说到Hibernate,那就从Hibernate开始说起吧。
因为很多人都喜欢把JPA和Hibernate混为一谈,这个系列文章会把这个问题讲明白。
有两种方法来处理Hibernate中的持久性:会话(session)和实体管理器。通过这篇文章,我们将分析这两种机制的区别。
实体管理器是JPA规范的一部分,而Hibernate是基于Session对象来实现自己的解决方案,也就是处理持久性。我们已经看到他们中的一个(JPA)是一个标准。我们需要记住的是,JPA是“独立出来的”API,它是一个标准,它描述了如何以标准化的方式处理对象持久化。它可以有多个实现。因此,如果你的应用程序基于JPA的实现,则可以随时在不同的实现之间切换。但对于Hibernate来说却不是这样,它可以但不一定与其他持久性解决方案兼容。
下一个区别是用于管理持久性的类。在JPA中,我们查找EntityManagerFactory,EntityManager,可以发现它们都位于javax.persistence包中。Hibernate使用它自己的类来表示持久性上下文:SessionFactory
,Session
。由于JPA所在包(hibernate-jpa-2.1-api
中的javax.persistence包
)定义的基本都是接口,所以他们的实现可以是不同的(也就说所也可以是基于Hibernate来进行实现的)。
在使用Hibernate作为JPA实现的情况下,我们可以使用一些Hibernate所特有的功能。实际上,Hibernate的EntityManager实现调用了Session对象。我们可以从一些异常日志中观测到它,例如在关于Hibernate/JPA中的锁
(下一篇文章,到时再补链接)的这篇文章中,我们可以看到:
1 | javax.persistence.RollbackException: Error while committing the transaction |
这个异常代表一个锁超时,并使用Hibernate的会话(Session
)来管理持久性。我们可以在下面的输出片段中观察它:
1 | at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1240) |
想要从Hibernate的EntityManager实现中获取Session,通过下面简单的调用就能拿到了:
1 | Session session = entityManager.unwrap(Session.class); |
通过log日志记录这个session
对象应该返回以下输出:
1 | Hibernate's session is :SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] orphanRemovals=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] collectionQueuedOps=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) |
这两者不仅只有以上的差异。有一些方法双方都有,但是名称不同。我们首先通过标识符来获取一个实体。Hibernate的Session
使用一个称为get的方法,而JPA所使用的方法称为find。(源码 就不截取了,请自行建立环境去验证的),本文所使用Hibernate版本为:
1 | <dependency> |
为了从持久化上下文中分离出一个实体(也就是我们通常说的游离态),Hibernate使用evict方法。JPA与更通用的函数命名:detach (分离)。两个方案都有通过persist方法将对象附加到持久化上下文。两者都可以用refresh方法刷新 实体(entity)的状态。Hibernate和JPA有另一个相似之处。他们可以通过调用 clear()方法 来清除持久化上下文。调用这个clear()方法会导致其中所有的实体分离(也就是游离化)。关于Session和EntityManager方法的区别,我们应该注意到Session有更多的方法来分析它的内部状态。关于内部状态,它们有一个通用方法称为isOpen
,并允许检查Session
或EntityManager
是否处于打开状态。此外,Hibernate通过Session,我们可以检查它是否连接(isConnected),是否包含脏(损坏的)数据(isDirty),或者判断所处理对象(实体或代理)是否处于只读模式(isReadOnly)。关于对象状态的例子就不再次累述了,请参考http://blog.csdn.net/u014087286/article/details/47039349博文所述。
NOTE:Hibernate save()与persist()区别
Hibernate 之所以提供与save()功能几乎完全类似的persist()方法,一方面是为了照顾JPA的用法习惯。另一方面,save()和 persist()方法还有一个区别:使用 save() 方法保存持久化对象时,该方法返回该持久化对象的标识属性值(即对应记录的主键值);但使用 persist() 方法来保存持久化对象时,该方法没有任何返回值。因为 save() 方法需要立即返回持久化对象的标识属性,所以程序执行 save() 会立即将持久化对象对应的数据插入数据库;而 persist() 则保证当它在一个事物外部被调用时,并不立即转换成 insert 语句, 这个功能是很有用的,尤其当我们封装一个长会话流程的时候,persist() 方法就显得尤为重要了。
主要内容区别:
1,persist把一个瞬态的实例持久化,但是并”不保证”标识符(identifier主键对应的属性)被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时候。
2,save, 把一个瞬态的实例持久化标识符,及时的产生,它要返回标识符,所以它会立即执行Sql insert
同样,关于查询对象,两者也是有名字不同但作用相同的方法。JPA 通过调用getSingleResult来获取单行和通过getResultList得到一个结果列表。Hibernate分别使uniqueResult和list方法来达到相同效果。在Hibernate5.2之前还有一些额外的方法来指定查询参数。通过它提供的setters
,我们可以设置一个BigInteger,BigDecimal,二进制,布尔,字节,字符串或日期。在Hibernate5.2之后,和JPA规范进行统一化,统一调用setParameter
这个方法来达到相应目的。
这篇简短的文章描述了JPA和Hibernate的Session持久化机制之间的差异和相似之处。两者都被用来做同样的事情,将Java对象持久化到数据库中去。他们分别通过EntityManager(JPA)和Session(Hibernate)对象管理持久化上下文(persistence context)来实现它。但他们在工作过程中也有一些相似之处。两者都可以通过persist
来持久化实体和通过clear
方法从持久化上下文分离实体(使之 游离化)。一般来说,更抽象和标准化的解决方案对于应用程序的可移植性来说更好。而使用Hibernate,我们不能轻易地将其转移到另一个持久性框架中。通过在Hibernate中使用JPA的实现(不使用Hibernate特有的功能),可以更容易实现代码的可移植性。
很多时候,我们学习Java,开始的时候觉得很容易,越到后面,内容越多,反而心烦气躁起来,学了忘,忘了学,依然会忘,总是摸不到窍门,再看到社会上和身边都是搞Java的,竞争压力可想一般,看到github上人家写的牛逼哄哄的程序,自己又什么都不会,只会一些简单的Demo,更复杂的逻辑想想都头大,当学到框架之后,自己慢慢变得只会套用框架,玩玩CRUD,导致很多人认为,只要会crud,只要会写关于crud之类的业务就可以了,其他都是在 一味的调用api来完成各种组合。于是,你慢慢就害怕咯,后来者学的太快,而且还是全新的知识,自己只会所谓的业务,这些业务的生命周期,不过就是几年光景,当自己想跳槽的时候,一点底气都没。
将话题回归,面对日益更新的编程行业,每天都会有全新的概念,全新的技术诞生,如何立之于不败,其实大家都懂,以不变应万变,以静制动。就好比我们高中时做题一样,无论题型如何变,如何复杂,不都还是书中的那些基础知识,于是我们老师一直在给我们强调基础的重要性,我们也在一遍一遍的通过做题来加强我们对于基础的理解。这些基础就是不变的东西,也是静物。
同样,对于编程语言来讲,其首先是一门语言,我很纳闷国内大学为什么不把编译原理放在大二的时候就开始讲的,哪怕讲的简单一点,可以让学生知道你们是在学习和汉语英语一样的东西,都有词法分析,语法分析,语义分析。而语言是用来表达思想的,没有思想,你只能是一个行尸走肉,这就是国人学习编程最可悲的地方,无视算法的重要性,它是我们组织逻辑的基础,我们的思维需要这些逻辑和相应的语言来表达,而国内太多的培训机构两者皆可抛,大学教育同样如此,只是大家都知道重要,都不去做,因为知道,它不能给你带来短期快速收益,它在你看来不是前沿的东西,殊不知,这些才是最前沿的,经久不衰,各种技术换汤不换药,用的都是他们。
总结出来,论数据结构和算法的重要性,论基础的重要性,有时候自己迷茫的时候,回归下算法,回归下基础,Java的话,我们可以深入一些我们平时使用的API,其内部用了什么样的算法,一个小程序内部是算法,几个类组成的大点的程序,可能是按照设计模式来进行的,而设计模式,又何尝不是一种别样的算法,属于我们抽象出来的解决事情的标准。不扯更多了,沉淀自己,坚持学习,就这么简单,仅此而已。
]]>说到文件系统我们很容易就想到Linux,windows操作系统的文件系统,对应到我们的生活中,我们想去一所学校找到某个学生,假如你不了解学号所代表的意义,那就只能是一点一点的找了,不过绝对知道这个学生是几年级,然后一个班一个班的找,假如了解学号的意义的话我们就可以直接定位到哪一栋楼,哪一间教室。
说的再直白点,不就是是个找啊找啊找朋友的游戏么。这也就是我们排序查找的算法了,而面向大量有用数据最好的实践就是用树形结构来统筹,于是我们的数据库的索引,我们的zookeeper的节点管理,小到我们Java里使用的红黑树,以及对hashmap的优化等等,就是因为其复杂度可以降到最低,只需要凭借树的高度就可以快速找到我们所要找的数据了。
说了这么多,就是想要表达的是,我们的Java9中所设计的全新的JRTFS也是基于树来表达的。
我们往往会将一堆数据分析其成分,然后抽取出结构来对其组织,往往我们碰到的最多的是表结构和其数据,结构定义和数据要分开存放,这里我们首先对其进行结构的定义,接着我们要将每一份数据进行穿针引线,做成一个体系,其实就是一个索引体系,我们要做的就是对其每一个节点的管理。而最后所建立起的索引系统可以作为一个专门的文件来存放(windows系统下面的话请参照C:\Program Files\Java\jdk-9.0.1\lib\modules
这个文件),我们的结构定义作为一个专门的jar文件来存放(windows系统下面的话请参照C:\Program Files\Java\jdk-9.0.1\lib\jrt-fs.jar
)
我们可以参考Linux文件系统,其一个文件应该包含什么样的基本属性:name
,可读性,创建时间,最后修改时间,最后访问时间。
我们把我们的目光转向jdk.internal.jrtfs
这个包下。找到jdk.internal.jrtfs.JrtFileAttributes
,因为Java9要兼容Java8的东西,所以势必要做两种不一样的考虑,那么此处就应该开始做一个岔路口。里面定义了上面所说的这些基本属性。同样,我们可以看到它是基于树的节点控制来做到的。
1 | /** |
这样,我们就可以有组成一个树形文件系统的节点定义了。
接着通过jdk.internal.jrtfs.SystemImage
来作为文件系统的加载入口,在初始化这个类的时候,会首先把静态代码块给执行,接着,我们会在jdk.internal.jrtfs.JrtFileSystem
其构造函数中发现其调用了SystemImage.open()
方法,可以知道其首先会检查C:\Program Files\Java\jdk-9.0.1\lib\modules
这个文件是否存在,存在,就使用jdk.internal.jimage.ImageReader
中的静态内部类jdk.internal.jimage.ImageReader.SharedImageReader
来对此文件的进行读取然后建立相应的文件系统镜像:
1 | abstract class SystemImage { |
也就是说,上面这个类的定义,我们可以把启动封装一个open方法,最后在大一统实现文件系统的时候集中调用,每个类做好自己那份事情就好。
jdk.internal.jrtfs.JrtFileSystem
的构造器:
1 | class JrtFileSystem extends FileSystem { |
通过前面提到的索引数据和结构定义数据分开的可以知道,我们的结构定义也是需要有的,那么,走进jdk.internal.jrtfs.JrtFileSystemProvider
来看看其内在乾坤,从下面的源码中可以知道,JrtFileSystemProvider
会判断区分当前的环境状态(这里要求必须存在C:\Program Files\Java\jdk-9.0.1\lib\jrt-fs.jar
),首先拿到jrt-fs.jar
的路径,其实通过URLClassLoader.loadClass(String name, boolean resolve)
得到Classloader实例,加载完这些结构定义之后,返回一个FileSystem
实例(return new JrtFileSystem(this, env);
)
1 | public final class JrtFileSystemProvider extends FileSystemProvider { |
既然是文件系统,路径这块总要有定义的,就好像Linux使用/
作为根,对于Jrtfs来说,同样要有相应定义的。jdk.internal.jrtfs.JrtPath
就是jrt file systems
关于Path
的基本实现类。
作为一个Path
其解析的肯定是一个URI字符串路径,对于操作字符串,我们用的比较多的有切分,而且字符串内部用的比较多的同样有offset
,和判断/home/abc/ddd
一样,我们通过确认/
这个约定来对文件系统进行分层,确定父子 关系,就好像我们的/Base/A模块/B模块/C模块
,要获取某些操作,我们都需要先对这个路径以/
做偏移量操作,以方便快速获取到某模块的名字。而我们的很多方法刚开始都会调用initOffsets();
,那我们来看看这个方法的具体操作:
1 | // create offset list if not already created |
然后再加入一个JrtFileSystem
,自然很多事情就可以做到了,此处就不再多说了。
其实Jrt file systems
的文件存储实现很简单,可以说没什么内容,因为是内存里建立起来的镜像文件系统,它也只提供了一些基本的约束,如,文件系统应该以什么为开头等等。
1 | final class JrtFileStore extends FileStore { |
我们在写web项目的时候,往往会使用DTO来展示这些公开的数据,对于文件系统中的文件也是,这就出现了文件属性视图的需求,包括读取和对这些公开属性的设定,比如文件的创建修改时间。
我们找到java.nio.file.attribute.BasicFileAttributeView
这个接口,里面定义了上面所说的这些基本属性。然后我们通过jdk.internal.jrtfs.JrtFileAttributeView
来对其进行实现。
我们可以通过文件系统类的类型是否相等来判断到底是使用老版本的通过classpath来加载的方式,还是通过Jrtfs的方式来加载。请看如下代码:
1 | "unchecked") // Cast to V ( |
也可以通过一个String
关键字来判断:
1 | static JrtFileAttributeView get(JrtPath path, String type, LinkOption... options) { |
基本属性的话,首先对所操作属性进行判断了:
1 | static Object attribute(AttrID id, JrtFileAttributes jrtfas, boolean isJrtView) { |
这里的枚举类型,也是我们这个类中定义的:
1 | private static enum AttrID { |
就到此吧,关于更多对模块的解读,留在下一篇去说。
]]>如何实现IOC的效果,我们可以来想想,无非就是一个隐式实现,而想要做到,总不能什么都没有,来个巧妇难为无米之炊的境地吧,所以说,米必须要有滴,在Spring中就是一个bean,也就是说,容器里得有米,再官话点就是上下文中得存在所需要的bean。同样模块化中两个互相隔离的模块想要达到这种效果,也要先往jvm里扔个对象进去的,然后who use ,who get 就可以了。
请看例子(可以认为是我们平常写的SpringMVC项目中的service->serviceImpl->controller):
1 | package com.example.api; |
上面这个接口所在的模块定义:
1 | module migo.codec.api { |
接着,我们定义一个实现模块:
1 | module migo.codec.service { |
具体实现就省略了。
最后我们在最上层的模块内使用:
1 | module migo.codec.controller { |
具体的controller模块内使用的代码如下:
1 | ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class); |
或者:
1 | public static void main(String... args) { |
亦或者假如有很多服务实现的提供者,而某个提供服务实现的provider(也就是serviceImpl)上面有添加注解@PNG
,而我们想使用带有这个注解的实例,可以使用以下代码:
1 | ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class); |
通过在模块定义里面的provides aaa with aaaImpl 这个功能,可以很容易的想到key value
组合
当我们碰到这对关键字的时候,我们就会解析并将aaa
做为key
,aaaImpl
添加到一个list
中并将这个list
作为value
,并添加到一个Map<String,list>
中
在我们碰到uses
关键字(源码里面acc
会去确定这个权限),并通过ServiceLoader.load(key)
来找到这个key所对应的一个包含了实现类具体地址的list,可能有多个,那么,拓展功能,我们使用一个装饰模式,也就是继承了Iterable
这个接口,可以达到遍历并生成具体实例来达到要求。
那么按照这个思路,我们反着来找下,这里只列关键代码:
从上面的Demo中,我们可以看到,通过类的class字节码来加载:
之前有说,巧妇难为无米之炊,所以这个上下文很重要,我们的类加载器也是要讲究上下文的
1 | /** |
我们进去这个ServiceLoader
,其实无非就是一个构造器而已了,关键代码我截下:
1 | this.service = svc; |
有了这个加载器之后,其实我们就拿到了上下文和访问权限的一些东西,我们再来看看这个类的字段:
1 | public final class ServiceLoader<S> |
可以看到,它实现了按照我们分析的Iterable
接口,这样我们就可以多了很多操作,而且我们也看到了下面这几个东西,这样我们就可以做事情了:
1 | private Iterator<Provider<S>> lookupIterator2; |
我们走进findFirst
这个方法来看看:
1 | public Optional<S> findFirst() { |
我们看到了iterator()
这个方法:
1 | public Iterator<S> iterator() { |
现在newLookupIterator()
进入到我们的视野中,没有条件创建条件,刚开始我们可没有拿到米,现在去找米去:
1 | /** |
这里抛开其他我们来看ModuleServicesLookupIterator()
这个构造函数 :
1 | ModuleServicesLookupIterator() { |
映入眼帘的是iteratorFor(ClassLoader loader)
这个方法:
1 | /** |
这里终于找到了findServices(String service)
这个方法:
1 | /** |
结合getOrDefault
的源码可知:
1 | default V getOrDefault(Object key, V defaultValue) { |
是不是和我们的具体思路接上轨了
而我们的provider
实例从何而来,请容我娓娓道来咯:
我们从jdk.internal.module.Modules
这个模块定义类中可以找到addProvides
这个方法,也就是说在我们加载这个模块的时候,这个动作就已经要干活了:
1 | /** |
然后我们可以从sun.instrument.InstrumentationImpl
这个类来看到其工作方式(通过其注释就可以看到这个类和JVM相关):
在加载模块的时候就执行了下面的代码,看下面update provides
这个注释的代码可以知道其调用了上面的addProvides
这个方法,而最后也是调用了addProvider(m, service, impl)
1 | /** |
Instrumentation
接口有一段很重要的注释,大家自己看吧,就不多说了:
1 | /** |
那么,我们最后,走入addProvider(m, service, impl)
这个方法中:
1 | /** |
再经过了这么曲曲折折的过程,终于拿到了ServiceProvider
,里面包括了我们所要调用实现类的地址信息
于是,看下ServiceLoader这个类定义的Provider
静态内部接口:
1 |
|
然后我们回到之前追到的iteratorFor
方法,知道其返回的是 Iterator<ServiceProvider>
类型
1 | /** |
然后回到ModuleServicesLookupIterator()
这个构造函数,直接看这个内部类,也就是调用这个
1 | /** |
在newLookupIterator
这个方法中得到ModuleServicesLookupIterator
的实例first
,并调用其hasNext
方法
1 | /** |
我们来进入这个hasNext
方法,也就是在这里,调用了loadProvider
生成了一个bean
1 |
|
走进这个loadProvider
方法,抛开前面所有,我们只看最后返回为:new ProviderImpl<S>(service, type, ctor, acc)
1 | /** |
最后,我们通过查看这个ProviderImpl
类终于得到了我们想要得到的结果。
1 | /** |
IOC和模块化所提供的类似效果的最大的区别就是,前者是提供了实例化的bean(即便是通过AOP实现的,这点很重要,Java9模块化在使用Spring的时候会有特别的设置),而且是基于Spring容器的单例的存在(多例注入的问题请参考我这方面的Spring源码解析),后者是提供了class字节码所在的路径,用的时候内部会自行生成实例,所以是多例的。
其实整个过程,Java的模块化文件系统起了很大的作用(这块看情况假如篇幅比较长久不放在我的书里了),然后自己追源码的思路也在这里给大家展现了一番,希望可以对大家有所帮助,看源码不要上来就瞎找的。另外,最重要的一点就是,不要因为源码很多,很复杂就轻言放弃,看的多了,看的久了,自然就有一套属于自己的方法论了。
]]>Java提供了许多创建线程池的方式,并得到一个Future实例来作为任务结果。对于Spring同样小菜一碟,通过其scheduling
包就可以做到将任务线程中后台执行。
在本文的第一部分中,我们将讨论下Spring中执行计划任务的一些基础知识。之后,我们将解释这些类是如何一起协作来启动并执行计划任务的。下一部分将介绍计划和异步任务的配置。最后,我们来写个Demo,看看如何通过单元测试来编排计划任务。
在我们正式的进入话题之前,对于Spring,我们需要理解下它实现的两个不同的概念:异步任务和调度任务。显然,两者有一个很大的共同点:都在后台工作。但是,它们之间存在了很大差异。调度任务与异步不同,其作用与Linux中的CRON job
完全相同(windows里面也有计划任务)。举个栗子,有一个任务必须每40分钟执行一次,那么,可以通过XML文件或者注解来进行此配置。简单的异步任务在后台执行就好,无需配置执行频率。
因为它们是两种不同的任务类型,它们两个的执行者自然也就不同。第一个看起来有点像Java的并发执行器(concurrency executor
),这里会有专门去写一篇关于Java中的执行器来具体了解。根据Spring文档TaskExecutor所述,它提供了基于Spring的抽象来处理线程池,这点,也可以通过其类的注释去了解。另一个抽象接口是TaskScheduler,它用于在将来给定的时间点来安排任务,并执行一次或定期执行。
在分析源码的过程中,发现另一个比较有趣的点是触发器。它存在两种类型:CronTrigger或PeriodTrigger。第一个模拟CRON任务的行为。所以我们可以在将来确切时间点提交一个任务的执行。另一个触发器可用于定期执行任务。
让我们从org.springframework.core.task.TaskExecutor类的分析开始。你会发现,其简单的不行,它是一个扩展Java的Executor接口的接口。它的唯一方法也就是是执行,在参数中使用Runnable类型的任务。
1 | package org.springframework.core.task; |
相对来说,org.springframework.scheduling.TaskScheduler接口就有点复杂了。它定义了一组以schedule开头的名称的方法允许我们定义将来要执行的任务。所有 schedule* 方法返回java.util.concurrent.ScheduledFuture实例。Spring5中对scheduleAtFixedRate
方法做了进一步的充实,其实最终调用的还是ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period);
1 | public interface TaskScheduler { |
之前提到两个触发器组件,都实现了org.springframework.scheduling.Trigger接口。这里,我们只需关注一个的方法nextExecutionTime ,其定义下一个触发任务的执行时间。它的两个实现,CronTrigger和PeriodicTrigger,由org.springframework.scheduling.TriggerContext来实现信息的存储,由此,我们可以很轻松获得一个任务的最后一个执行时间(lastScheduledExecutionTime),给定任务的最后完成时间(lastCompletionTime)或最后一个实际执行时间(lastActualExecutionTime)。接下来,我们通过阅读源代码来简单的了解下这些东西。org.springframework.scheduling.concurrent.ConcurrentTaskScheduler包含一个私有类EnterpriseConcurrentTriggerScheduler
。在这个class
里面,我们可以找到schedule方法:
1 | public ScheduledFuture<?> schedule(Runnable task, final Trigger trigger) { |
SimpleTriggerContext
从其名字就可以看到很多东西了,因为它实现了TriggerContext
接口。
1 | /** |
也正如你看到的,在构造函数中设置的时间值来自javax.enterprise.concurrent.LastExecution的实现,其中:
Spring调度和异步执行中的另一个重要类是org.springframework.core.task.support.TaskExecutorAdapter。它是一个将java.util.concurrent.Executor作为Spring基本的执行器的适配器(描述的有点拗口,看下面代码就明了了),之前已经描述了TaskExecutor
。实际上,它引用了Java的ExecutorService,它也是继承了Executor
接口。此引用用于完成所有提交的任务。
1 | /** |
下面我们通过代码的方式来实现异步任务。首先,我们需要通过注解来启用配置。它的XML配置如下:
1 | <task:scheduler id="taskScheduler"/> |
可以通过将@EnableScheduling
和@EnableAsync
注解添加到配置类(用@Configuration注解)来激活两者。完事,我们就可以开始着手实现调度和异步任务。为了实现调度任务,我们可以使用@Scheduled
注解。我们可以从org.springframework.scheduling.annotation包中找到这个注解。它包含了以下几个属性:
cron
:使用CRON
风格(Linux配置定时任务的风格)的配置来配置需要启动的带注解的任务。
zone
:要解析CRON
表达式的时区。
fixedDelay
或fixedDelayString
:在固定延迟时间后执行任务。即任务将在最后一次调用结束和下一次调用的开始之间的这个固定时间段后执行。
fixedRate
或fixedRateString
:使用fixedRate
注解的方法的调用将以固定的时间段(例如:每10秒钟)进行,与执行生命周期(开始,结束)无关。
initialDelay
或initialDelayString
:延迟首次执行调度方法的时间。请注意,所有值(fixedDelay ,fixedRate ,initialDelay )必须以毫秒表示。 需要特别记住的是 ,用@Scheduled注解的方法不能接受任何参数,并且不返回任何内容(void),如果有返回值,返回值也会被忽略掉的,没什么卵用。定时任务方法由容器管理,而不是由调用者在运行时调用。它们由 org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor来解析,其中包含以下方法来拒绝执行所有不正确定义的函数:
1 | protected void processScheduled(Scheduled scheduled, Method method, Object bean) { |
1 | /** |
使用@Async
注解标记一个方法或一个类(通过标记一个类,我们自动将其所有方法标记为异步)。与@Scheduled
不同,异步任务可以接受参数,并可能返回某些东西。
有了上面这些知识,我们可以来编写异步和计划任务。我们将通过测试用例来展示。我们从不同的任务执行器(task executors)的测试开始:
1 | (SpringJUnit4ClassRunner.class) |
在过去,我们可以有更多的执行器可以使用(SimpleThreadPoolTaskExecutor,TimerTaskExecutor 这些都2.x 3.x的老古董了)。但都被弃用并由本地Java的执行器取代成为Spring的首选。看看输出的结果:
1 | Running task 'SimpleAsyncTask-1' in Thread thread_name_prefix_____1 |
以此我们可以推断出,第一个测试为每个任务创建新的线程。通过使用不同的线程名称,我们可以看到相应区别。第二个,同步执行器,应该执行所调用线程中的任务。这里可以看到’main’是主线程的名称,它的主线程调用执行同步所有任务。最后一种例子涉及最大可创建3个线程的线程池。从结果可以看到,他们也确实只有3个创建线程。
现在,我们将编写一些单元测试来看看@Async和@Scheduled实现。
1 | (SpringJUnit4ClassRunner.class) |
另外,我们需要创建新的配置文件和一个包含定时任务方法的类:
1 | <!-- imported configuration file first --> |
1 | // scheduled methods after, all are executed every 6 seconds (scheduledAtFixedRate and scheduledAtFixedDelay start to execute at |
该测试的输出:
1 | <R> Increment at fixed rate |
本文向我们介绍了关于Spring框架另一个大家比较感兴趣的功能–定时任务。我们可以看到,与Linux CRON风格配置类似,这些任务同样可以按照固定的频率进行定时任务的设置。我们还通过例子证明了使用@Async注解的方法会在不同线程中执行。
]]>因自己在写的关于Java9的新书因为篇幅和读者层次的原因并不能将能想到的东西都写进去,故接下来整理出一系列的博文来补充拓展。
像其他一些编程语言一样,Java通常也被称为“编译语言”。但有时你可能会感到困惑,尤其是当有人告诉你Java是JIT编译,并问你其中的一些小细节时。
本文就来说一说JIT编译的概念。在第一部分,我们将对不同类型的编译描述一番。第二部分来说说JIT编译。接下来,我们将深入一下JIT编译在Java中比较特别的地方。
在讨论编译类型之前,我们需要了解什么是编译。这是一个将编程语言翻译成机器可理解的语言(也称为机器代码)的过程。机器语言由CPU执行的指令组成。这个语言是由0-1构成的,如在wikibooks页面上的这个片段所示:
1 | 0001 00000111 |
同样,我们知道,Java的javac指令不会生成机器代码,而是一些名为字节码的东西。而这不仅仅是一种语言会这么做(而这也是很多现代语言所发展的一个方向)。比如ActionScript(由ActionScript Virtual Machine执行)或CIL(由C#使用并在Common Language Runtime上执行)。
在这里,在我们的括号中所说的“执行”,也就是即时编译完成(即字节码编译成目标机器可执行的机器码)。这种特殊类型的编译发生在解释给定字节码的机器上,如ActionScript虚拟机或Java虚拟机(JVM)。字节码由他们在运行时( on runtime)编译成机器码。
这种编译带来了一些好处。第一个显着的优点是可以做到根据所运行机器参数来优化编译的代码。静态编译器为目标机器进行优化并一次生成机器代码。另一方面,JIT编译器提供了一种中间代码,它被转换和优化为特定于执行机器的机器代码。关于这里有一篇解释的比较通俗的文章动态编译和静态编译及Java执行,有兴趣可以看看
第二个优点是便携性。转换为字节码的代码可以在安装了虚拟机的任何计算机上运行。
So,Java是即时编译为机器代码的。想要检查编译机器代码,我们可以启用多个JVM参数:
-XX:+ PrintCompilation
通过这个参数,我们可以得到方法编译结果的输出。其输出的样例:
1 | 71 1 java.lang.String :: indexOf(70 bytes) |
输出被格式化为列,第一列(例如71)是时间戳。第二列返回唯一的编译器任务ID(1,2,3 …)。之后我们可以看到编译的方法。在括号中指定了编译字节码的字节。我们可以看到indexOf方法的大小是70字节,encode 方法是361字节等等。
-XX:+ UnlockDiagnosticVMOptions
一个简单的标志,JVM诊断的补充选项。
-XX:+ PrintInlining
通过这个配置,我们可以看到编译方法的细节。内联是编译器优化编译代码重要的工作方式。请看以下方法:
1 | public void testMethod() { |
通过内联,函数callAnotherMethod()
将被callAnotherMethod
的内容替换。正因为如此,在运行时,机器不会从一个方法跳转到另一个方法,并能够以内联方式
执行代码。JIT通过此操作用来避免在堆栈上放置参数的复杂情况。当我们启用此参数(+PrintInlining)并运行代码时,我们可以看到类似下面的结果:
1 | 75 1 java.lang.String :: indexOf(70 bytes) |
让我们回到理论层面面,Java中的JIT编译(这里说是动态编译)可以是(这里可以参考一篇文章
JVM即时编译(JIT),我这里用更加暴力通俗的方式说了下,能知道是个什么作用就可以):
已经编译的字节码存储到代码缓存中。这是一个结构,所有编译的方法。当再次调用给定方法时,它不会从头开始编译,而是从代码缓存中加载。但是,当编译器认为可以更好地优化此方法时,缓存方法可以被覆盖。在优化技术中,我们可以通过以下区分:
在本文中,我们解释了即时编译,即特定用于语言的编译代码(如Java的字节码)转换为CPU可以理解的语言(机器代码)。编译器不会进行简单的编译,因为它也对编译代码进行了一些优化。由于这些优化,机器代码尽可能地适应目标机器,另外,可以根据http://blog.csdn.net/opensure/article/details/46715675这篇文章中的两张图来更好的理解下上面所说的一些细节。
]]>上一篇 Spring框架中的事件和监听器并未对Spring框架中的异步事件涉及太多,所以本篇是对其一个补充。
同步事件有一个主要缺点:它们在所调用线程的本地执行(也就是将所调用线程看成主线程的话,就是在主线程里依次执行)。如果监听器处理同步事件需要5秒钟的响应,则最终结果是用户将在至少5秒内无法看到响应(可以通过Spring框架中的事件和监听器中的例子了解具体)。所以,我们可以通过一个替代方案来解决这个问题 - 异步事件。
接下来也就是介绍Spring框架中的异步事件。老规矩,第一部分深入框架源码,将描述主要组成部分以及它们如何一起协作的。在第二部分,我们将编写一些测试用例来检查异步事件的执行情况。
在Spring中处理异步事件是基于本地的Java并发解决方案—任务执行器(可以了解下Java Executor框架的内容)。事件由multicastEvent 方法调度。它通过使用java.util.concurrent.Executor接口的实现将事件发送到专用的监听器。Multicaster会调用同步执行器,因为它是默认实现,这点在Spring框架中的事件和监听器有明确的例子,从源码的角度也就是是否设置有SyncTaskExecutor
实例。从public void setTaskExecutor(@Nullable Executor taskExecutor)
其中,@Nullable 可看出Executor参数可为null,默认不设置的话,multicastEvent也就直接 跳过异步执行了
org.springframework.context.event.SimpleApplicationEventMulticaster
1 |
|
异步执行器的实现可以参考org.springframework.core.task.SimpleAsyncTaskExecutor。这个类为每个提交的任务创建新的线程。然而,它不会重用线程,所以如果我们有很多长执行时间的异步任务需要来处理的时候,线程创建的风险就会变得太大了,会占用大量的资源,不光是cpu还包括jvm。具体源码如下:
1 | /** |
为了从线程池功能中受益,我们可以使用另一个Spring的Executor实现,org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor。类如其名,这个Executor
允许我们使用线程池。关于线程池的源码,请期待我的Java9的书籍,里面会涉及到这里面的细节分析,也可以参考其他博客的博文(哈哈,我就是打个小广告而已)。
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
1 | /** |
我们来编写一个能够同时处理同步和异步事件的multicaster。同步事件将使用本地同步调度程序进行调度(SyncTaskExecutor),异步使用Spring的ThreadPoolTaskExecutor实现。
1 | /** |
首先,我们需要为我们的测试用例添加一些bean:
1 | <bean id="syncTaskExecutor" class="org.springframework.core.task.SyncTaskExecutor" /> |
用于测试任务执行结果的两个类:
1 | // TaskStatsHolder.java |
如上代码所示,这些都是简单对象。我们会使用这些对象来检查我们的假设和执行结果是否相匹配。两个要分发的事件也很简单:
1 | // ProductChangeFailureEvent.java |
而用于处理相应调度事件的监听器也只需要将数据放入TaskStatsHolder实例类
中即可:
1 | // ProductChangeFailureListener.java |
用于测试的controller如下所示:
1 |
|
最后,测试用例:
1 | (SpringJUnit4ClassRunner.class) |
因之前整理的笔记此处SimpleEventMulticaster忘了放进去,也懒得去找了,可以通过xml定义去查看下,这个测试用例可以看出两个listener不是由同一个executor启动的,Product failure 监听器由同步执行器执行。因为他们没有做任何操作,几乎立即返回结果。关于邮件调度事件,通过休眠5秒可以得到其执行时间超过Product failure 监听器的执行时间。通过分析输出可以知道,两者在不同的线程中执行,所以由不同的执行器执行(关于这俩执行器的例子可以再搜下相关博文,其实主要还是想表达SyncTaskExecutor
是在主线程里执行,而asyncTaskExecutor
由线程池里管理的线程执行)。
1 | Product was correctly changed |
本文简单介绍了如何在Spring中处理异步事件。当监听器需要执行很长时间,而我们又不想阻塞应用程序执行,就可以使用异步执行。异步执行可以通过异步执行器(如ThreadPoolTaskExecutor或SimpleAsyncTaskExecutor)实现。
]]>