1、术语
并发 vs 并行
- 并发和并行是相关的概念,但有一些小的区别。并发意味着两个或多个任务正在取得进展,即使它们可能不会同时执行。例如,这可以通过时间切片来实现,其中部分任务按顺序执行,并与其他任务的部分混合。另一方面,当执行的任务可以真正同时进行时,就会出现并行
简单说启动一个线程在一个core上就是并行,启动两个线程在一个core上就是并发
异步 vs 同步
- 如果调用者在方法返回值或引发异常之前无法取得进展,则认为方法调用是同步的。另一方面,异步调用允许调用者在有限的步骤之后继续进行,并且可以通过一些附加机制 (它可能是已注册的回调、Future 或消息)来通知方法的完成
简单来说Java API层来说的,如下 :
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
System.out.println("执行业务逻辑");
// 根据业务逻辑判断给定返回
return true;
}
});
future.get(); // 同步API,必须等到返回
if(future.isDone()) {
future.get();// 异步API,只有执行完,再get结果
}
- 同步 API 可以使用阻塞来实现同步,但这不是必要的。CPU 密集型任务可能会产生类似 于阻塞的行为。一般来说,最好使用异步 API,因为它们保证系统能够进行
非阻塞 vs 阻塞
- 如果一个线程的延迟可以无限期地延迟其他一些线程,这就是我们讨论的阻塞。一个很好的例子是,一个线程可以使用互斥来独占使用一个资源。如果一个线程无限期地占用资源(例如意外运行无限循环),则等待该资源的其他线程将无法进行。相反,非阻塞意味着没有线程能够无限期地延迟其他线程
- 非阻塞操作优先于阻塞操作,因为当系统包含阻塞操作时,系统的总体进度并不能得到很好的保证
死锁 vs 饥饿 vs 活锁
- 当多个线程在等待对方达到某个特定的状态以便能够取得进展时,就会出现死锁。由于没有其他线程达到某种状态,所有受影响的子系统都无法继续运行。死锁与阻塞密切相关,因为线程能够无限期地延迟其他线程的进程
- 在死锁的情况下,没有线程可以取得进展,相反,当有线程可以取得进展,但可能有一个或多个线程不能取得进展时,就会发生饥饿。典型的场景是一个调度算法,它总是选择高优先级的任务而不是低优先级的任务。如果传入的高优先级任务的数量一直足够多,那么低优先级任务将永远不会完成
- 活锁类似于死锁,因为没有线程取得进展。不同之处在于,线程不会被冻结在等待他人进展的状态中,而是不断地改变自己的状态。一个示例场景是,两个线程有两个相同资源可用时。他们每一个都试图获得资源,但他们也会检查对方是否也需要资源。 如果资源是由另一个线程请求的,他们会尝试获取该资源的另一个实例。在不幸的情 况下,两个线程可能会在两种资源之间“反弹”,从不获取资源,但总是屈服于另一种资源
2、BIO vs NIO
BIO
serverSocket.accept(),这里会阻塞
socket.getInputStream.read(),也会阻塞
虽然可以使用了线程池,因为read()方法的阻塞,其实线程池也是不能复用的,说白了,就是需要一个客户端一个线程进行服务
思考:那BIO就没有使用场景了吗?
其实不是,BIO在建立长连接的流式传输场景还是很有用的,比如说HDSF,客户端向DataNode传输数据使用的就是建立一个BIO的管道,流式上传数据的。此时引入一个问题,那HDFS DataNode就不考虑到线程阻塞么?是这样的,其实要知道你不可能多个客户端上传文件都是针对某个DataNode(NameNode会进行选择DataNode),所以线程阻塞的压力是会分摊的。NIO还是擅长小数据量的RPC请求,能接受百万客户端的连接
NIO
NIO中有三个重要组件 : Buffer(ByteBuffer主要使用)、Channel(双向通道,可读可写)和Selector(多路复用选择器)
Buffer
常用的就是 ByteBuffer,缓冲池,可以作为channel写的单位,也可以接受channel读取的返回里面重要的属性 :position、capacity、flip、limit和hasRemain每个channel都需要记录可能切分的消息,因为ByteBuffer不能被多个channel使用,因此需要为每个channel维护一个独立的ByteBuffer。ByteBuffer不能太大,比如一个ByteBuffer 1M的话,需要支持百万连接要1TB内存,因此需要设计大小可变的ByteBuffer
1、首先分配一个较小的buffer,比如4k,如果发现不够的话,再分配8kb的buffer,将4kb buffer内容拷贝到8kb buffer,有点是消息连续容易处理,缺点是数据拷贝耗费性能
2、多个数组组成buffer,一个数组不够,把多出来的内容写入新的数组,缺点不连续解析复杂,有点避免了拷贝引起的性能损耗FileChannel
FileChannel在同一个JVM中是线程安全的,多个线程写也没有问题,但是在不同的JVM中同时写一个文件就会有问题需要的是 FileLock对文件进行加锁,有独占锁和共享锁
channel.lock(0, Integer.MAX_VALUE, true),可以锁一定的区间RandomAccessFile 可以支持读写文件,Channel 本身是支持读写的,只是看源头是不是支持读写,比如说FileInputStream流获取的channel只能支持读,RandomAccessFile获取的流支持读写
channel.force(true); 强制将os cache数据刷入到磁盘上
from.transferTo(position,count,dest);从from channel写到dest channel,从position 开始写,写了count长度
比如说从本地文件向网络中进行传输
to.transferFrom(src,position,count); 比如说从网络中写到本地文件,from就是从外界到,src读取,写到to中transferTo & transferFrom 底层使用的是零拷贝,零拷贝简单来说其实不走应用层数据复制,但是也是有数据复制的,是在Linux内核层
Selector & SocketChannel
服务端 ServerSocketChannel
是通过 ServerSocketChannel和Selector来获取多个连接,每个连接是一个SocketChannel
将 ServerSocketChannel 注册到 Selector上,如果有连接,selector的select阻塞方法会有事件,生成SelectionKey,每个SelectionKey其实对应一个SocketChannelSelectionkey是可以attach对象的,也可以通过 Selectionkey 通过attachment进行对象的获取,这很重要,一般会创建一个对象并和SelectioKey进行关联
照样是bind,监听OP_ACCEPT,进行isAcceptable、read和write事件(write事件是一次没有写完毕,继续要写)
客户端 SocketChannel
是进行connect,监听OP_CONNECT事件,isConnectable,read和write事件要注意,SelectionKey,每次迭代是需要删除的,否则重复请求,但是已经处理,就会有问题
写数据的时候一定要注意,最好不要 while(buffer.hasRemaining()) 一直写,这样会阻塞网络带宽的,影响读取
写一部分数据,然后关注SelectionKey.OP_WRITE事件,不断selector.select()继续写,写完毕取消写事件的关注socketChannel.write(buffer); 写一下,也不一定会把buffer中都写完毕 if(buffer.hasRemaining()) { selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE); selectionKey.attach(buffer); }
3、零拷贝
传统IO问题
比如说要将本地磁盘文件往网络中写,磁盘 -> 内核缓冲区 -> 用户缓冲区 -> socket缓冲区 -> 网卡
读磁盘数据 : 用户态 -> 内核态
内核数据写到用户缓冲区 : 内核态 -> 用户态
网卡写数据 : 用户态 -> 内核态
4次数据复制,3次内核切换
通过DirectByteBuffer,MappedByteBuffer,为什么快?
因为他使用direct buffer的方式读写文件内容,称为内存映射。这种方式直接调用系统底层的缓存,没有JVM和系统之间的复制操作,所以效率大大的提升
将堆外内存映射到JVM内存中直接访问
减少一次数据拷贝,用户态与内核态的切换次数没有减少
Linux2.4
Java调用transferTo,要从Java程序的用户态到内核态,磁盘 -> 内核缓冲区 -> 网卡,一次内核切换,两次数据复制
4、Socket参数
SocketChannel参数
SO_RCVBUF和SO_SNDBUF : Socket参数,TCP数据接收缓冲区大小,发送和接受缓冲区,128kb或者256kb
CONNECT_TIMEOUT_MILLIS : 用户在客户端建立连接时,如果在指定毫秒内无法建立连接,会抛出timeout异常
TCP_NODELAY TCP参数,立即发送数据,默认值为Ture(关闭nagle算法)
SO_KEEPALIVE Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性(2个小时)
SO_REUSEADDR : 其实就是比如说ServerSocketChannel连接关闭了,此时跟其他客户端的连接都处于一个timeout状态,重启Netty Server,如果设置了
SO_REUSEADDR 为 true,则会让ServerSocketChannel重新地址端口绑定,否则失败
ServerSocketChannel参数
SO_BACKLOG Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128
TCP三次握手是在ACCEPT之前发生的
1、第一次握手,client发送SYN到server,状态修改为SYN_SEND,server收到,状态修改为SYN_REVD,并将请求放入sync queu队列
2、第二次握手,server回复 SYN + ACK 给client,client收到,状态修改为ESTABLISHED,并发送给ACK给server
3、第三次握手,server收到ack,状态修改为 ESTABLISHED,将请求从sync queue放入accept queue
所以现在出现了半连接队列和全连接队列
在Centos Linux下对应着 /proc/sys/net/ipv4/tcp_max_syn_backlog(512),/proc/sys/net/core/somaxconn(128)
SO_BACKLOG 设置的是全连接
TCP SYNC FLOOD恶意DOS攻击方式就是建立大量的半连接状态的请求,然后丢弃
如感兴趣,点赞加关注哦!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。