Java高并发核心编程卷1读书笔记

Netty

基本组件

  • Channel
  • Reactor
  • Handler
  • Pipeline(双向链表)
  • EventLoopGroup
  • parentGroup: 负责新连接的监听和接收
  • childGroup:负责 IO 事件的轮询和分发
    如果不分开 parent 和 child,则会带来一个风险:新连接的接收被更加耗时的数据传输或业务处理所阻塞
  • ChannelOption

优点

  • API 使用起来相对简单,降低了开发门槛
  • 对多种主流协议都有很好的支持(编解码功能)
  • 支持自定义配置,通过 ChannelHandler 可以灵活地扩展通信框架
  • 其他业界主流的 NIO 框架相比,Netty 的综合性能最优(谁出书都会说自己的好)
  • 成熟稳定,Netty 修复了在 JDK NIO 中所有已发现的 bug(我也不知道有什么 bug)
  • 有活跃的社区,版本迭代周期短,bug 修复及时

ZooKeeper

核心优势就是解决了分布式环境的数据一致性问题,每时每刻访问 ZooKeeper 的树结构时,返回的数据都是一致的,不会引起脏读、幻读、不可重复读

脏读、幻读、不可重复读

  • 脏读:一个事务中访问到了另一个事务未提交的数据
  • 幻读:当两个完全相同的查询执行时,两次返回的结果集不一样,原因另一个事务新增或删除了第一个事务的结果集中的数据
  • 不可重复读:在一个事务内根据同一个条件对数据进行多次查询,返回的结果不一致,原因是其他事务对数据进行了修改

不可重复读和幻读的区别:

不可重复读关心的是记录的更新操作,对同样的记录,再次读取数据发生变化

幻读关注的是记录的增删操作,条数发生了变化

高并发环境下的接入层

网关不外乎完成以下工作:

  • 鉴权
  • 限流
  • 反向代理
  • 负载均衡

操作系统

将内存划分为两部分,内核模块运行在内核空间,对应的进程处于内核态;用户程序运行在用户空间,对应的进程处于用户态

IO 读写:

上层应用通过操作系统的 read 系统调用,把数据从内核缓冲区复制到应用程序的进程缓冲区,通过操作系统的 write 系统调用,把数据从应用程序的进程缓冲区复制到操作系统的内核缓冲区

那么为什么要设置缓冲区呢?

是为了减少设备之间的频繁物理交换,因为外部物理设备与内存和 CPU 相比,有着非常大的差距

四种主要的 IO 模型

阻塞 IO 指的是,需要内核 IO 操作彻底完成后,才返回用户空间执行用户程序的操作指令,非阻塞则无需等待

同步指的是用户空间(进程或线程)是主动发起 IO 请求的一方,系统内核是被动接收方;异步 IO 则反过来

  • 同步阻塞 IO:用户空间主动发起,需要等待内核 IO 操作彻底完成后,才返回到用户空间的 IO 操作
    • 优点:应用程序开发非常简单,在阻塞等待数据的过程中,用户线程挂起,基本不会占用 CPU 资源
    • 缺点:一般情况下会为每个连接配备一个独立的线程,高并发场景下,需要大量线程,内存和线程切换开销会非常大
  • 同步非阻塞 IO:用户空间发起,不需要等待内核 IO 操作彻底完成,就可以立即返回用户空间去执行后续的指令(这个也叫 NIO,但是和 Java 里面的 NIO 有区别,Java 里的 NIO 指的是 NewIO,用的是 IO 多路复用模型),特点是需要不断进行轮询
    • 优点:线程不会阻塞,实时性较好
    • 缺点:需要不断轮询,占用 CPU 资源,效率低下
  • IO 多路复用:在 Linux 系统中,IO 多路复用的系统调用为 select/epoll,通过该系统调用,一个用户进程(或线程)可以监视多个文件描述符,一旦某个文件描述符就绪(一般是指内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或线程),用户空间就可以根据文件描述符的就绪状态进行对应的 IO 系统调用(Reactor)
    • 优点:一个选择器查询线程可以同时处理成千上万的网络连接,不必创建大量的线程
    • 缺点:select/epoll 系统调用是阻塞的,属于同步 IO
  • 异步 IO:指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者;用户线程通过系统调用向内核注册某个 IO 操作,内核在这个 IO 操作完成后通知用户程序,用户程序执行后续的业务操作(回调)
    • 优点:非阻塞
    • 缺点:应用程序仅需要进行事件的注册和接收,其余的工作都留给了操作系统,也就是需要底层内核提供支持

连接极限值

1
ulimit -n 1000000(默认 1024)
1
ulimit -SHn(S 软性极限值,超过警告;H 硬性极限值,超过报错)

如何彻底解除限制?

编辑文件 /etc/security/limits.conf

1
2
soft nofile 1000000
hard nofile 1000000

Java NIO

基本组件

  • Channel
  • Buffer
  • Selector

OIO 和 NIO 的区别

  • OIO(Old IO) 面向流,NIO(New IO)面向缓存区
  • OIO 是阻塞的,NIO 非阻塞
  • OIO 没有 Selector 概念,NIO 有

NIO Buffer

常用方法

  • allocate(int capacity): 默认是写模式,从数组下标 0 开始,长度限制是 capacity

  • put(int i):写 i 进数组里,如果超过长度,则抛出 BufferOverflowException(可使用 remaining() 来判断剩余可写长度;如果是读模式,则抛出 ReadOnlyBufferException(可使用 isReadOnly() 来判断是否可写)

  • filp():读写模式转换

    1
    2
    3
    4
    5
    6
    public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
    }
  • get()

  • rewind():倒带

    1
    2
    3
    4
    5
    public Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
    }

    flip() 和 rewind() 的区别就是 limit 参数

  • mark() 和 reset(): mark() 标记当前位置,当调用 reset() 的时候,回到标记的位置

  • clear()

    1
    2
    3
    4
    5
    6
    public Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
    }

使用 clear() 或者 compact() 可以将读模式改为写模式

生命周期

allocate() -> put() -> flip() -> get() -> clear() / compoact() -> put()

申请内存的两种方式

申请堆内存,读写效率较低,受到 GC 影响

1
ByteBuffer.allocate() -> java.nio.HeapByteBuffer

申请直接内存,读写效率高(少一次拷贝),不会受到 GC 影响,分配内存的时候效率低,使用不当可能造成内存泄漏

1
ByteBuffer.allocateDirect() -> java.nio.DirectByteBuffer

NIO Channels

常用 Channel

  • FileChannel
  • (FileInputStream() / FileOutputStream()).geChannel(), ByteBuffer.flip(), channel.force(true), channel.close()
  • transferTo(position, length, target):零拷贝, 每次 2G 上限
  • ServerSocketChannel
  • SocketChannel
  • socketChannel.configureBlocking(true / false),默认是 blocking, 因为有个属性设置 nonBlocking 为 false
  • DatagramChannael

NIO Selector

选择器的使命是处理 IO 的多路复用,完成通道的注册,监听,事件查询

选择器和通道的关系是监控和被监控的关系

IO 事件类型

  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT

SelectionKey

如何知道一个 Channel 是否能被监测?

看这个 Channel 是否直接或间接继承 SelectableChannel,比如 FileChannel 就不是,而 SocketChannel 就是

注册到 Selector 的 Channel 必须是处于非阻塞模式下,因为 FileChannel 只有阻塞模式,不能切换到非阻塞模式,所以 FileChannel 不能和选择器一起使用

Selector.select() 本身是阻塞的,所以放在 while true 里不用担心 CPU 空转

Reactor 模式

Reactor 模式由 Reactor 线程,Handlers 处理器两大角色组成

  • Reactor 线程:负责响应 IO 事件,并且分发到 Handlers 处理器
  • Handlers 处理器:非阻塞地执行业务逻辑处理

执行流程

channel -> selector -> reactor -> handler

优点

  • 响应速度快,虽然同一 Reactor 线程本身是同步的,但是不会被单个连接的 IO 操作所阻塞
  • 避免了多线程同步和线程之前频繁切换的开销
  • 通过增加 Reactor 线程的个数,可以进行灵活扩展,充分利用 CPU 资源

缺点

  • 每次引入新东西肯定是增加了原系统的复杂性
  • 需要操作系统底层支持 IO 多路复用系统调用才可以

为什么在海量连接的情况下,线程池的方式不好使

  • 线程的创建和销毁成本很高
  • 线程本身占用较大内存,Java 线程栈内存分配 512K - 1M
  • 线程切换的成本很高,因为需要保存上下文,如果线程切换的时间比线程本身执行所需要的时间还多,就是负优化

序列化与反序列化

  • 数据结构大小:原则上,越小,效率越高
  • 结构复杂度:越复杂,效率越低