【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

Netty 学习和进阶策略

  • 2019-10-14
  • 本文字数:7936 字

    阅读完需:约 26 分钟

Netty学习和进阶策略

背景

Netty 框架的特点

Netty 的一个特点就是入门相对比较容易,但是真正掌握并精通是非常困难的,原因有如下几个:


  1. 涉及的知识面比较广:Netty 作为一个高性能的 NIO 通信框架,涉及到的知识点包括网络通信、多线程编程、序列化和反序列化、异步和同步编程模型、SSL/TLS 安全、内存池、HTTP、MQTT 等各种协议栈,这些知识点在 Java 语言中本身就是难点和重点,如果对这些基础知识掌握不扎实,是很难真正掌握好 Netty 的。

  2. 调试比较困难:因为大量使用异步编程接口,以及消息处理过程中的各种线程切换,相比于传统同步代码,调试难度比较大。

  3. 类继承层次比较深,有些代码很晦涩(例如内存池、Reactor 线程模型等),对于初学者而言,通过阅读代码来掌握 Netty 难度还是比较大的。

  4. 代码规模庞大:目前,Netty 的代码规模已经非常庞大,特别是协议栈部分,提供了对 HTTP/2MQTTWebSocketSMTP 等多种协议的支持,相关代码非常多。如果学习方式不当,抓不住重点,全量阅读 Netty 源码,既耗时又很难吃透,很容易半途而废。

  5. 资料比较零散,缺乏实践相关的案例:网上各种 Netty 的资料非常多,但是以理论讲解为主,Netty 在各行业中的应用、问题定位技巧以及案例实践方面的资料很少,缺乏系统性的实践总结,也是 Netty 学习的一大痛点。

初学者常见问题

对于很多初学者,在学习过程中经常会遇到如下几个问题:


  1. 相关领域知识的储备不足:想了解学习 Netty 需要储备哪些技能,掌握哪些知识点,有什么学习技巧可以更快的掌握 Netty。由于对 Java 多线程编程、Socket 通信、TCP/IP 协议栈等知识掌握不扎实,后续在学习 Netty 的过程中会遇到很多困难。

  2. 理论学习完,实践遇到难题:学习完理论知识之后,想在实际项目中使用,但是真正跟具体项目结合在一起解决实际问题时,又感觉比较棘手,不知道自己使用的方式是否最优,希望能够多学一些案例实践方面的知识,以便更好的在业务中使用 Netty。

  3. 出了问题不会定位:在项目中遇到了问题,但是由于对 Netty 底层细节掌握不扎实,无法有效的定位并解决问题,只能靠网上搜索相关案例来参考,问题解决效率比较低,甚至束手无策。

Netty 学习策略

Netty 入门相对简单,但是要在实际项目中用好它,出了问题能够快速定位和解决,却并非易事。只有在入门阶段扎实的学好 Netty,后面使用才能够得心应手。

入门知识准备

Java NIO 类库

需要熟悉和掌握的类库主要包括:


  1. 缓冲区 Buffer。

  2. 通道 Channel。

  3. 多路复用器 Selector。


首先介绍缓冲区(Buffer)的概念,Buffer 是一个对象,它包含一些要写入或者要读出的数据。在 NIO 类库中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。


缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。


最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。比较常用的就是 get 和 put 系列方法,如下所示:



图 1 ByteBuffer 常用接口定义


Channel 是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。


比较常用的 Channel 是 SocketChannel 和 ServerSocketChannel,其中 SocketChannel 的继承关系如下图所示:



图 2 SocketChannel 继承关系


Selector 是 Java NIO 编程的基础,熟练地掌握 Selector 对于掌握 NIO 编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector 会不断地轮询注册在其上的 Channel,如果某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。

Java 多线程编程

作为异步事件驱动、高性能的 NIO 框架,Netty 代码中大量运用了 Java 多线程编程技巧,熟练掌握多线程编程是掌握 Netty 的必备条件。


需要掌握的多线程编程相关知识包括:


  1. Java 内存模型。

  2. 关键字 synchronized。

  3. 读写锁。

  4. volatile 的正确使用。

  5. CAS 指令和原子类。

  6. JDK 线程池以及各种默认实现。


以关键字 synchronized 为例,它可以保证在同一时刻,只有一个线程可以执行某一个方法或者代码块。同步的作用不仅仅是互斥,它的另一个作用就是共享可变性,当某个线程修改了可变数据并释放锁后,其它的线程可以获取被修改变量的最新值。如果没有正确的同步,这种修改对其它线程是不可见的。


下面我们就通过对 Netty 的源码进行分析,看看 Netty 是如何对并发可变数据进行正确同步的。以 AbstractBootstrap 为例进行分析,首先看它的 option 方法:



这个方法的作用是设置 ServerBootstrap 或 Bootstrap 的 Socket 属性,它的属性集定义如下:


private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
复制代码


由于是非线程安全的 LinkedHashMap, 所以如果多线程创建、访问和修改 LinkedHashMap 时,必须在外部进行必要的同步。由于 ServerBootstrap 和 Bootstrap 被调用方线程创建和使用,无法保证它的方法和成员变量不被并发访问。因此,作为成员变量的 options 必须进行正确的同步。由于考虑到锁的范围需要尽可能的小,所以对传参的 option 和 value 的合法性判断不需要加锁,保证锁的范围尽可能的细粒度。


Netty 加锁的地方非常多,大家在阅读代码的时候可以仔细体会下,为什么有的地方要加锁,有的地方有不需要?如果不需要,为什么?当你对锁的原理理解以后,对于这些锁的使用时机和技巧理解起来就相对容易了。

Netty 源码学习

关键类库学习

Netty 的核心类库可以分为 5 大类,需要熟练掌握:


1、ByteBuf 和相关辅助类:ByteBuf 是个 Byte 数组的缓冲区,它的基本功能应该与 JDK 的 ByteBuffer 一致,提供以下几类基本功能:


  1. 7 种 Java 基础类型、byte 数组、ByteBuffer(ByteBuf)等的读写。

  2. 缓冲区自身的 copy 和 slice 等。

  3. 设置网络字节序。

  4. 构造缓冲区实例。

  5. 操作位置指针等方法。

  6. 动态的扩展和收缩。


从内存分配的角度看,ByteBuf 可以分为两类:堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被 JVM 自动回收;缺点就是如果进行 Socket 的 I/O 读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核 Channel 中,性能会有一定程度的下降。直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从 Socket Channel 中读取时,由于少了一次内存复制,速度比堆内存快。


2、Channel 和 Unsafe:io.netty.channel.Channel 是 Netty 网络操作抽象类,它聚合了一组功能,包括但不限于网路的读、写,客户端发起连接、主动关闭连接,链路关闭,获取通信双方的网络地址等。它也包含了 Netty 框架相关的一些功能,包括获取该 Chanel 的 EventLoop,获取缓冲分配器 ByteBufAllocator 和 pipeline 等。Unsafe 是个内部接口,聚合在 Channel 中协助进行网络读写相关的操作,它提供的主要功能如下表所示:



Unsafe API 功能列表


3、ChannelPipeline 和 ChannelHandler: Netty 的 ChannelPipeline 和 ChannelHandler 机制类似于 Servlet 和 Filter 过滤器,这类拦截器实际上是职责链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。Servlet Filter 是 JEE Web 应用程序级的 Java 代码组件,它能够以声明的方式插入到 HTTP 请求响应的处理过程中,用于拦截请求和响应,以便能够查看、提取或以某种方式操作正在客户端和服务器之间交换的数据。


拦截器封装了业务定制逻辑,能够实现对 Web 应用程序的预处理和事后处理。过滤器提供了一种面向对象的模块化机制,用来将公共任务封装到可插入的组件中。


这些组件通过 Web 部署配置文件(web.xml)进行声明,可以方便地添加和删除过滤器,无须改动任何应用程序代码或 JSP 页面,由 Servlet 进行动态调用。通过在请求 / 响应链中使用过滤器,可以对应用程序(而不是以任何方式替代)的 Servlet 或 JSP 页面提供的核心处理进行补充,而不破坏 Servlet 或 JSP 页面的功能。由于是纯 Java 实现,所以 Servlet 过滤器具有跨平台的可重用性,使得它们很容易地被部署到任何符合 Servlet 规范的 JEE 环境中。


Netty 的 Channel 过滤器实现原理与 Servlet Filter 机制一致,它将 Channel 的数据管道抽象为 ChannelPipeline,消息在 ChannelPipeline 中流动和传递。ChannelPipeline 持有 I/O 事件拦截器 ChannelHandler 的链表,由 ChannelHandler 对 I/O 事件进行拦截和处理,可以方便地通过新增和删除 ChannelHandler 来实现不同的业务逻辑定制,不需要对已有的 ChannelHandler 进行修改,能够实现对修改封闭和对扩展的支持。ChannelPipeline 是 ChannelHandler 的容器,它负责 ChannelHandler 的管理和事件拦截与调度:



图 3 ChannelPipeline 对事件流的拦截和处理流


Netty 中的事件分为 inbound 事件和 outbound 事件。inbound 事件通常由 I/O 线程触发,例如 TCP 链路建立事件、链路关闭事件、读事件、异常通知事件等。


Outbound 事件通常是由用户主动发起的网络 I/O 操作,例如用户发起的连接操作、绑定操作、消息发送等操作。ChannelHandler 类似于 Servlet 的 Filter 过滤器,负责对 I/O 事件或者 I/O 操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。基于 ChannelHandler 接口,用户可以方便地进行业务逻辑定制,例如打印日志、统一封装异常信息、性能统计和消息编解码等。


4、EventLoop:Netty 的 NioEventLoop 并不是一个纯粹的 I/O 线程,它除了负责 I/O 的读写之外,还兼顾处理以下两类任务:


  1. 普通 Task:通过调用 NioEventLoop 的 execute(Runnable task) 方法实现,Netty 有很多系统 Task,创建它们的主要原因是:当 I/O 线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成 Task 放入消息队列中,由 I/O 线程负责执行,这样就实现了局部无锁化。

  2. 定时任务:通过调用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 方法实现。


Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty 可以同时支持 Reactor 单线程模型、多线程模型和主从 Reactor 多线层模型。它的工作原理如下所示:



图 4 Netty 的线程模型


通过调整线程池的线程个数、是否共享线程池等方式,Netty 的 Reactor 线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最大程度地满足不同用户的个性化定制。


为了尽可能地提升性能,Netty 在很多地方进行了无锁化的设计,例如在 I/O 线程内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列—多个工作线程的模型性能更优。它的设计原理如下图所示:



图 5 NioEventLoop 串行执行 ChannelHandler


5、Future 和 Promise:在 Netty 中,所有的 I/O 操作都是异步的,这意味着任何 I/O 调用都会立即返回,而不是像传统 BIO 那样同步等待操作完成。异步操作会带来一个问题:调用者如何获取异步操作的结果?ChannelFuture 就是为了解决这个问题而专门设计的。下面我们一起看它的原理。ChannelFuture 有两种状态:uncompleted 和 completed。当开始一个 I/O 操作时,一个新的 ChannelFuture 被创建,此时它处于 uncompleted 状态——非失败、非成功、非取消,因为 I/O 操作此时还没有完成。一旦 I/O 操作完成,ChannelFuture 将会被设置成 completed,它的结果有如下三种可能:


  1. 操作成功。

  2. 操作失败。

  3. 操作被取消。


ChannelFuture 的状态迁移图如下所示:



图 6 ChannelFuture 状态迁移图


Promise 是可写的 Future,Future 自身并没有写操作相关的接口,Netty 通过 Promise 对 Future 进行扩展,用于设置 I/O 操作的结果,它的接口定义如下:



图 7 Netty 的 Promise 接口定义

关键流程学习

需要重点掌握 Netty 服务端和客户端的创建,以及创建过程中使用到的核心类库和 API、以及消息的发送和接收、消息的编解码。


Netty 服务端创建流程如下:



图 8 Netty 服务端创建流程


Netty 客户端创建流程如下:



图 9 Netty 客户端创建流程

Netty 项目实践

实践主要分为两类,如果项目中需要用到 Netty,则直接在项目中应用,通过实践来不断提升对 Netty 的理解和掌握。如果暂时使用不到,则可以通过学习一些开源的 RPC 或者服务框架,看这些框架是怎么集成并使用 Netty 的。以 gRPC Java 版为例,我们一起看下 gRPC 是如何使用 Netty 的。

gRPC 服务端

gRPC 通过对 Netty HTTP/2 的封装,向用户屏蔽底层 RPC 通信的协议细节,Netty HTTP/2 服务端的创建流程如下:



图 10 Netty HTTP/2 服务端创建流程


服务端 HTTP/2 消息的读写主要通过 gRPC 的 NettyServerHandler 实现,它的类继承关系如下所示:



图 11 gRPC NettyServerHandler 类继承关系


从类继承关系可以看出,NettyServerHandler 主要负责 HTTP/2 协议消息相关的处理,例如 HTTP/2 请求消息体和消息头的读取、Frame 消息的发送、Stream 状态消息的处理等,相关接口定义如下:



图 12 NettyServerHandler 处理 HTTP/2 协议消息相关接口

gRPC 客户端

gRPC 的客户端调用主要包括基于 Netty 的 HTTP/2 客户端创建、客户端负载均衡、请求消息的发送和响应接收处理四个流程,gRPC 的客户端调用总体流程如下图所示:


gRPC 的客户端调用总体流程如下图所示:



图 13 gRPC 客户端总体调用流程


gRPC 的客户端调用流程如下:


  1. 客户端 Stub(GreeterBlockingStub) 调用 sayHello(request),发起 RPC 调用。

  2. 通过 DnsNameResolver 进行域名解析,获取服务端的地址信息(列表),随后使用默认的 LoadBalancer 策略,选择一个具体的 gRPC 服务端实例。

  3. 如果与路由选中的服务端之间没有可用的连接,则创建 NettyClientTransport 和 NettyClientHandler,发起 HTTP/2 连接。

  4. 对请求消息使用 PB(Protobuf)做序列化,通过 HTTP/2 Stream 发送给 gRPC 服务端。

  5. 接收到服务端响应之后,使用 PB(Protobuf)做反序列化。

  6. 回调 GrpcFuture 的 set(Response) 方法,唤醒阻塞的客户端调用线程,获取 RPC 响应。


需要指出的是,客户端同步阻塞 RPC 调用阻塞的是调用方线程(通常是业务线程),底层 Transport 的 I/O 线程(Netty 的 NioEventLoop)仍然是非阻塞的。

线程模型

gRPC 服务端线程模型整体上可以分为两大类:


  1. 网络通信相关的线程模型,基于 Netty4.1 的线程模型实现。

  2. 服务接口调用线程模型,基于 JDK 线程池实现。


gRPC 服务端线程模型和交互图如下所示:



图 14 gRPC 服务端线程模型


其中,HTTP/2 服务端创建、HTTP/2 请求消息的接入和响应发送都由 Netty 负责,gRPC 消息的序列化和反序列化、以及应用服务接口的调用由 gRPC 的 SerializingExecutor 线程池负责。


gRPC 客户端的线程主要分为三类:


  1. 业务调用线程

  2. 客户端连接和 I/O 读写线程

  3. 请求消息业务处理和响应回调线程


gRPC 客户端线程模型工作原理如下图所示(同步阻塞调用为例):



图 15 客户端调用线程模型


客户端调用主要涉及的线程包括:


  • 应用线程,负责调用 gRPC 服务端并获取响应,其中请求消息的序列化由该线程负责。

  • 客户端负载均衡以及 Netty Client 创建,由 grpc-default-executor 线程池负责。

  • HTTP/2 客户端链路创建、网络 I/O 数据的读写,由 Netty NioEventLoop 线程负责。

  • 响应消息的反序列化由 SerializingExecutor 负责,与服务端不同的是,客户端使用的是 ThreadlessExecutor,并非 JDK 线程池。

  • SerializingExecutor 通过调用 responseFuture 的 set(value),唤醒阻塞的应用线程,完成一次 RPC 调用。


gRPC 采用的是网络 I/O 线程和业务调用线程分离的策略,大部分场景下该策略是最优的。但是,对于那些接口逻辑非常简单,执行时间很短,不需要与外部网元交互、访问数据库和磁盘,也不需要等待其它资源的,则建议接口调用直接在 Netty /O 线程中执行,不需要再投递到后端的服务线程池。避免线程上下文切换,同时也消除了线程并发问题。


例如提供配置项或者接口,系统默认将消息投递到后端服务调度线程,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中执行消息的序列化和反序列化、以及服务接口调用。


减少锁竞争优化:当前 gRPC 的线程切换策略如下:



图 16 gRPC 线程锁竞争


优化之后的 gRPC 线程切换策略:



图 17 gRPC 线程锁竞争优化


通过线程绑定技术(例如采用一致性 hash 做映射), 将 Netty 的 I/O 线程与后端的服务调度线程做绑定,1 个 I/O 线程绑定一个或者多个服务调用线程,降低锁竞争,提升性能。

Netty 故障定位技巧

尽管 Netty 应用广泛,非常成熟,但是由于对 Netty 底层机制不太了解,用户在实际使用中还是会经常遇到各种问题,大部分问题都是业务使用不当导致的。Netty 使用者需要学习 Netty 的故障定位技巧,以便出了问题能够独立、快速的解决。‘’

接收不到消息

如果业务的 ChannelHandler 接收不到消息,可能的原因如下:


  1. 业务的解码 ChannelHandler 存在 BUG,导致消息解码失败,没有投递到后端。

  2. 业务发送的是畸形或者错误码流(例如长度错误),导致业务解码 ChannelHandler 无法正确解码出业务消息。

  3. 业务 ChannelHandler 执行了一些耗时或者阻塞操作,导致 Netty 的 NioEventLoop 被挂住,无法读取消息。

  4. 执行业务 ChannelHandler 的线程池队列积压,导致新接收的消息在排队,没有得到及时处理。

  5. 对方确实没有发送消息。


定位策略如下:


  1. 在业务的首个 ChannelHandler 的 channelRead 方法中打断点调试,看是否读取到消息。

  2. 在 ChannelHandler 中添加 LoggingHandler,打印接口日志。

  3. 查看 NioEventLoop 线程状态,看是否发生了阻塞。

  4. 通过 tcpdump 抓包看消息是否发送成功。

内存泄漏

通过 jmap -dump:format=b,file=xx pid 命令 Dump 内存堆栈,然后使用 MemoryAnalyzer 工具对内存占用进行分析,查找内存泄漏点,然后结合代码进行分析,定位内存泄漏的具体原因,示例如下所示:



图 18 通过 MemoryAnalyzer 工具分析内存堆栈


性能问题如果出现性能问题,首先需要确认是 Netty 问题还是业务问题,通过 jstack 命令或者 jvisualvm 工具打印线程堆栈,按照线程 CPU 使用率进行排序(top -Hp 命令采集),看线程在忙什么。通常如果采集几次都发现 Netty 的 NIO 线程堆栈停留在 select 操作上,说明 I/O 比较空闲,性能瓶颈不在 Netty,需要继续分析看是否是后端的业务处理线程存在性能瓶颈:



图 19 Netty NIO 线程运行堆栈


如果发现性能瓶颈在网络 I/O 读写上,可以适当调大 NioEventLoopGroup 中的 work I/O 线程数,直到 I/O 处理性能能够满足业务需求。

作者介绍

李林锋,10 年 Java NIO、平台中间件设计和开发经验,精通 Netty、Mina、分布式服务框架、API Gateway、PaaS 等,《Netty 进阶之路》、《分布式服务框架原理与实践》作者。目前在华为终端应用市场负责业务微服务化、云化、全球化等相关设计和开发工作。


2019-10-14 17:117611
用户头像

发布了 25 篇内容, 共 68.7 次阅读, 收获喜欢 227 次。

关注

评论 3 条评论

发布
用户头像
我说咋写的这么系统,专业,最后看到作者,原来是netty进阶那本书的作者。
2020-12-11 16:25
回复
用户头像
图2贴错了
2020-08-22 16:39
回复
用户头像
好文
2020-01-20 16:20
回复
没有更多了
发现更多内容

经常觉得每天时间不够用,试试合批吧!

Justin

心理学 时间管理 28天写作

心仪的岗位

正午看星星

作业

Soul 网关实践 04|dubbo 服务接入网关

哼干嘛

期货合约系统软件开发|期货合约APP开发

系统开发

汽车行业产品经理备忘录(作业三)

Rui

如何恶意使用微信视频号 | 视频号 28 天 (12)

赵新龙

28天写作

智能手表

张老蔫

28天写作

云原生动态周报 | KubeEdge被评为2020十大边缘计算开源项目

华为云原生团队

云计算 云原生 边缘计算 华为云 边缘技术

产品经理训练营第一章作业

铭白

产品经理

一张自查清单

熊斌

学习 个人总结 成长笔记 28天写作

Java学习(一)

灵霄

java基础

如何处理消息队列中的重复消息

废材姑娘

RocketMQ

产品经理训练营作业00

KingSwim

第 1 周作业

老元宵

Soul 学习笔记---搭建编译项目(一)

fightingting

Soul网关

一个button的成长过程

德育处主任

CSS html css3 html/css 28天写作

《原神》运维自动化的探索与实践

OpsMind

运维 运维自动化

Mybatis【12】-- Mybatis多条件怎么查询?

秦怀杂货店

mybatis

产品经理00期训练营第一周作业 4班

mas

产品经理训练营

绩效管理,上下同心者胜(三)

一笑

管理 绩效 28天写作

深入剖析RSA密钥原理及实践

vivo互联网技术

算法 https 公钥加密 rsa

轻松几步教你将React Native 项目运行在Web端

华为云开发者联盟

html 大前端 Web API React Native

经验说丨华为云视频Cloud Native架构下实践

华为云开发者联盟

架构 微服务 华为云 CloudNative

这样提问,大牛才会为你解答(提问的智慧)

yes

Java 程序经验小结:消除GC触及不到的过期对象引用

后台技术汇

28天写作

ReentrantLock原理分析

旺旺

Java ReentrantLock JUC lock

芯片领域产品经理的发展

Lujohn

产品经理 芯片 产品经理训练营 极客大学产品经理训练营 PM

甲方日常 86

句子

工作 随笔杂谈 日常

第1周总结

老元宵

Soul学习笔记---运行 soul-examples-dubbo(三)

fightingting

Soul网关

项目管理系列(6)-能力成熟度

Ian哥

28天写作

Netty学习和进阶策略_文化 & 方法_李林锋_InfoQ精选文章