coderZoe的博客
type: channel
link: https://coderzoe.com/
items: Gitlab CICD
items: 操作系统导论笔记——虚拟化
items: Netconf和Cmd近期问题定位整理
items: Apache mina-sshd 使用中的一些坑
items: Netty的异步——future-promise
items: 聊一聊这个十一
items: 简单聊聊DNS
items: JUC源码详细分析之Part4——《线程池》
items: JUC源码详细分析之Part3——《常见的线程安全集合》
items: JUC源码详细分析之Part2——《锁》

Gitlab CICD
type: item
date: 2025-08-01T08:16:00+00:00
description: Gitlab CICD本文讲一下如何使用gitlab来做CICD：使用gitlab做代码仓库，同时用gitlab的CI功能完成代码扫描、编译打包、镜像构建和镜像中心，并使用k8s来做部署。我们的流程是先使用gitlab来做完整的流程，一步步的由简到繁，一点点增加功能和优化过程，致力于达到真实gitlab企业的CICD效果。优化内容包括但不限于使用nexus、harbor等搭建私库优化CI的构建效率，使用k8snamespace做多环境隔离，并通过ConfigMap和Secret存储环境变量等。整个CICD均采用GitOps的一切皆代码 (Everything as Code)思想。Git作为一切的来源，这代表除了代码本身，CI流水线要代码化、k8s的部署要代码化、k8s环境变量配置也要代码化。整个测试将使用四台服务器，服务器操作系统均为Debian12192.168.31.193 作为gitlab、gitlab-runner以及后续的jenkins、habor等部署192.168.31.240、192.168.31.66、192.168.31.97 三个k8s节点，为服务做到多节点部署项目所用gitlab版本为最新的18.1.1-ce.0项目完整代码和配置可见：coderZoe/cicd-test-project1. Gitlab CI1.1 Gitlab部署services: gitlab: image: gitlab/gitlab-ce:18.1.1-ce.0 container_name: gitlab restart: always hostname: 'gitlab.example.com' environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://192.168.31.193' registry_external_url 'http://192.168.31.193:5050' ports: - '80:80' - '443:443' - '22:22' # 镜像中心的端口 - '5050:5050' volumes: - '/home/gitlab/config:/etc/gitlab' - '/home/gitlab/logs:/var/log/gitlab' - '/home/gitlab/data:/var/opt/gitlab' shm_size: '1024m'注意，这里gitlab使用了ssh的22端口和http与https的80与443端口，且我直接映射出了，因此宿主机不能占用这些端口，如果宿主机占用了，可以修改gitlab所用端口，见Install GitLab in a Docker container | GitLab Docs1.2 创建项目登录gitlab后，创建一个group并在该group下创建一个项目：1.3 创建runnergitlab的runner负责执行具体的CICD任务，我们创建一个runner并将其分配到上面的test-group下。首先修改docker-compose.yml，创建一个runner：services: gitlab: image: gitlab/gitlab-ce:18.1.1-ce.0 container_name: gitlab restart: always hostname: 'gitlab.example.com' environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://192.168.31.193' registry_external_url 'http://192.168.31.193:5050' ports: - '80:80' - '443:443' - '22:22' - '5050:5050' volumes: - '/home/gitlab/config:/etc/gitlab' - '/home/gitlab/logs:/var/log/gitlab' - '/home/gitlab/data:/var/opt/gitlab' shm_size: '1024m' # gitlab runner gitlab-runner: image: 'gitlab/gitlab-runner:v18.1.1' container_name: gitlab-runner restart: always volumes: # runner的配置 - '/home/gitlab/runner:/etc/gitlab-runner' # 将宿主机的docker server映射出来，方便runner可以操作宿主机docker - …

操作系统导论笔记——虚拟化
type: item
date: 2025-06-03T14:24:00+00:00
description: 操作系统导论笔记——虚拟化本文是笔者在阅读操作系统导论 时做的笔记。虽然这本书笔者曾反复阅读过多次，但每次阅读都有不少收获（主要是读后就忘）。笔者最近决定重读本书，并做一份笔记。本文的笔记不是对原书的摘抄或总结，而是笔者自身的理解和感悟，本文中部分疑惑都是从AI那里解答的，但整篇笔记依然是笔者用自己的写的，杜绝使用任何AI直接生成的东西。操作系统导论 共分三个部分：虚拟化、并发和持久性，本篇笔记是第一部分：虚拟化1. CPU虚拟化操作系统实现cpu虚拟化主要依赖两个机制：对需要访问硬件的操作做限制，要求只能在内核态执行对硬件的访问，如读写文件，读写网卡等。这种受限访问就是我们说的系统调用，一般通过软中断实现。时分共享。每个进程仅能持续占用cpu一小段时间，不同的进程交替获得cpu执行，就好像每个进程都拥有cpu一样。时分共享实现的核心是时钟中断，硬件设备时钟会每隔一定时间给cpu发送硬中断，检测到硬中断的cpu会跳转到操作系统预置的中断处理程序中，此时操作系统重新获得cpu控制权，然后决定要继续运行当前进程，还是切换一个新的进程来运行。系统调用保证了进程对硬件访问的保护以及提供了统一的抽象，而时分共享则保证了操作系统可以频繁的拿到cpu的控制权，并决定进程的调度，避免一些用户进程长时间占用cpu问题。1.1 上下文切换当前CPU运行进程A，此时发生硬中断，cpu会自动将进程A的关键寄存器，如PC、SP、Flag等信息压入进程A的内核栈上，cpu根据中断向量号找到中断向量表中对应的ISR程序入口，同时切换到内核模式。内核会将当前进程A的所有通用寄存器的信息（如EAX，EBX等）继续保存到内核栈，这样就相当于内核栈当前栈顶保存了进程A用户态下的所有寄存器信息，记录了进程A在用户态执行的完整快照，这段将用户态进程A的上下文保存到内核栈栈顶的结构叫trapframe。然后内核开始执行ISR代码，如果ISR代码判断无需进行上下文切换，则将之前保存在trapframe中的通用寄存器(EAX, EBX 等)从A的内核栈中弹出并恢复到CPU寄存器。然后执行iret，cpu识别到iret返回后，自动从当前sp指向的栈（此时是A的内核栈）的栈顶pop出User PC, User SP, Flags等（也即进程A在用户态的关键上下文）并加载到自己的寄存器中，此时切换到用户态，cpu开始继续执行进程A的代码。如果判断需要进行上下文切换则执行swicth()程序，switch程序做两件事：当前正处在进程A的内核态下，cpu寄存器信息是进程A内核态执行的上下文，因此我们需要先将这些关键信息，如PC、SP等保存起来，使用context保存，context位于PCB或TCB等内存中。将要执行的进程B的context加载进CPU的寄存器，可以很显然的知道，进程B的context其实就是进程B在内核态执行的上下文，此时CPU的SP寄存器就是进程B的内核栈，而PC指向的也是上一次进程B在内核态执行中止的下一条指令地址。此时CPU再取指执行，其实就是恢复之前进程B的内核态执行代码。因此switch函数返回后，其实整个执行已经被换为了进程B的内核态，进程B可能会在内核态再做一些收尾工作，将自己内核栈的trapframe中的通用寄存器(EAX, EBX 等)恢复到cpu，然后执行iret准备将控制权转移到用户态，也即进程B的用户态，CPU判断到iret指令后，就从SP指向的内核栈pop出栈顶的User PC, User SP, Flags等，因为当前sp指向的是进程B的内核栈，因此此时的trapFrame自然是用户态进程B的完整上下文。上述过程便完成了一次完整的进程上下文切换。我们可以将整个流程梳理为：用户态进程A => 内核态进程A => 内核态进程B => 用户态进程B。其中用户态进程A的完整上下文保存在进程A的内核栈叫trapFrame，而内核态进程A的上下文保存在TCB或PCB中叫context。如果以后某个时刻进程A被恢复运行，那恢复的点其实就是内核态进程A switch()函数切换完成的点，此时内核态进程A恢复运行，cpu的寄存器已经换为了内核态进程A的上下文（也即A的context），然后内核态进程A执行iret，cpu捕捉到从A的内核栈pop出trapFrame，正是我们之前保存的用户态进程A的上下文，此时用户态进程A就得以恢复运行。上述流程也可以理解为：一旦进程由用户态切为内核态，操作系统都需要完整备份整个用户态的上下文信息，只不过这个上下文信息是通过两阶段实现的，首先是cpu先将用户态的关键寄存器如PC、SP、Flag等保存进内核栈，然后切换到内核态，内核程序将再用户态的剩余完整寄存器保存到内核态。同样的道理，上下文的恢复也是两阶段恢复，内核态先将其…

Netconf和Cmd近期问题定位整理
type: item
date: 2025-01-15T09:25:00+00:00
description: Netconf和Cmd近期问题定位整理笔者目前工作在一家云计算公司，主要业务是云网络。年初的时候，公司有一些功能的重构，需要一个用Java写一个与设备进行通信的RPC框架，笔者独自负责这个框架的技术选型与开发。所谓的设备其实就是一些交换机或路由器。通信的方式是通过netconf和cmd(ssh)协议。0. 前言2024-12-30 有这样一个报错：org.apache.sshd.common.io.WritePendingException: A write operation is already pending; cannot write 18 bytes。 服务在使用CMD命令向设备10.230.10.94发送命令的时候，出现了如下错误：[ERROR] 15:29:16.894 [XNIO-1 task-23] [Ny6AcwwR7TiTUQCJVbcBMQgAvOsFqr] c.u.n.v.c.c.s.s.BaseSshSessionSender - 发送给设备10.230.10.94,10.230.10.94:22 发送失败,请求详情:[请求配置={senderType=CMD, timeout=PT10M, responseClass=class java.lang.String, expectedEnd=[]], expectedEndRegex='null', queueName='UCA_NETCONF_CMD_DEFAULT_QUEUE_INDEX', messageIdPrefix='null'}, 请求报文=irf topo-domain 0 , 响应结果=null],失败原因: org.apache.sshd.common.io.WritePendingException: A write operation is already pending; cannot write 18 bytes错误出现的地方在core-drvier服务的BaseSshSessionSender#send第238行，其代码如下：IoWriteFuture writeFuture = this.channel.getAsyncIn().writeBuffer(new ByteArrayBuffer(bytes));这行代码是发送操作，其中参数bytes是要发送的内容，已经做了序列化。上述内容出错的原因是：出现了同时调用当前channel的writeBuffer的操作（或者可以解释为：上一条指令的writeBuffer还没操作完，这时候又调了writeBuffer）。1. 问题定位理论上不应该出现这个问题才对，因为本框架是严格按照指令串行设计的。要想定位这个问题，不妨再细追下日志：我把上述日志分为了T1 ~T5 五个时间点1.1 相关代码要想深入分析这些问题，需要对发送流程有一些了解，我们先来看正常的代码流程：业务操作：deviceTemplate.cmd("10.230.10.94","irf priority 1","]").get(); deviceTemplate.cmd("10.230.10.94","irf topo-domain 0","]").get();其中cmd指令会返回一个future，而.get()会同步阻塞等待指令的返回。业务层调了cmd来发送指令后，会先进入：AbstractDevice#sendpublic DeviceFuture send(Object request, RequestConfig requestConfig) { //构造请求上下文 RequestContext requestContext = getRequestContext(request, requestConfig); log.info("收到发送请求，要发送的设备:{},请求详情:{}",this.metaData.getDeviceId(),requestContext); //判断是否能发送（一般只能队列里没有其他指令当前指令才能发送，因此是在这里保证了串行发送） if (checkCanSend(requestContext)) { //如果能发送就直接发送 doSend(requestContext); } //... return requestContext.getDevicePromise(); }如果可以发送，最终会进入：BaseSshSessionSender#send的如下方法：public Boolean send(RequestContext context) { //编码 序列化 byte[] bytes = encode(context); //发送 IoWriteFuture wri…

Apache mina-sshd 使用中的一些坑
type: item
date: 2024-06-18T03:04:00+00:00
description: Apache mina-sshd 使用中的一些坑笔者目前工作在一家云计算公司，主要业务是云网络。年初的时候，公司有一些功能的重构，需要一个用Java写一个与设备进行通信的RPC框架，笔者独自负责这个框架的技术选型与开发。所谓的设备其实就是一些交换机或路由器。通信的方式是通过netconf协议，而netconf基于ssh，因此需要一款合适的ssh客户端，笔者最终选择的是Apache mina-sshd，但使用过程中遇到了不少坑，mina的文档又非常的少，所以在这里记录下踩过的一些坑。首先在技术选型的时候，笔者一直想的是需要一个NIO的客户端，这很重要。在Java下NIO首先想到的是Netty，但netty没有实现默认的ssh协议，自己从零写一版是完全不现实的。除了netty，还剩mina，这俩框架的其实都出自Trustin Lee之手，但这两个框架的区别还是非常大的。首先最大的区别就是生态，netty的资料很多很多，最经典的是其核心贡献者Norman Maurer写的Netty in Action。但mina就不同了，你找遍全网，也很难找到靠谱的mina文档，唯一的文档可能就是官网下的几个md，可以说是写的非常非常不靠谱了，因为作为一个基本的NIO框架，竟然完全没写怎么如何响应式（异步）地读取和写入数据，官方demo居然都是阻塞式等待的写法。其次的区别就是API，netty的API设计非常非常的优雅，阅读netty源码的时候，你随时随地都能感受到netty中的asynchronous 和event-driven思想。反观mina却设计的很乱，它似乎也想有异步和事件驱动，但总是这一个入口，那一个入口，又没有详细的文档，导致使用起来十分的困难。笔者淌了很多地坑，所以想在此记录一下这些内容。1. 响应式地读取和写出数据读取public ClientChannel createChannel() throws Exception { //创建netconf的subsystem 详情请见：https://www.rfc-editor.org/rfc/pdfrfc/rfc6242.txt.pdf ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_SUBSYSTEM, NETCONF_SUBSYSTEM_NAME); //设为异步channel channel.setStreaming(ClientChannel.Streaming.Async); channel.open().verify(); //并同时为channel注册一个监听器，用于监听读取事件 channel.getAsyncOut().read(new ByteArrayBuffer(BUFFER_PIPE_SIZE)).addListener(new NetconfChannelResponseListener(this)); return channel; }上面通过sesion创建出channel后，需要将channel的streaming设置为Streaming.Async，并同时为channel注册一个监听器，用于监听读取事件。监听器的主要内容如下：public static class ChannelResponseListener implements SshFutureListener<IoReadFuture> { protected final BaseSshSessionSender sender; /** * 当前可以理解为是一个字节缓冲池，用于承装设备返回的字节，但由于netconf和ssh都是字符型协议，所以这里直接用一个StringBuilder来作为缓冲池 */ protected StringBuilder responseBuilder; public ChannelResponseListener(BaseSshSessionSender sender) { this.sender = sender; this.responseBuilder = new StringBuilder(); } /** * 一旦设备响应，触发当前方法 * * @param readFuture 读future */ @Override public void operationComplete(IoReadFuture readFuture) { Buffer buffer = null; try { readFuture.verify(READ_TIMEOUT); buffer = readFuture.getBuffer(); byte[] bytes = new byt…

Netty的异步——future-promise
type: item
date: 2023-12-12T14:15:00+00:00
description: 1. Java FutureFutureTask在说FutureTask之前先说下Task，对于Java语言，Task代表一个可执行任务，而一个可执行任务往往是继承自Runnable接口或Future接口假设如果我们直接交由一个线程执行Runnable，那么我们既不能停止这个任务也不能看到当前任务得到当前任务是否完成，如果完成我们也得不到执行结果，因此Java就抽象出一个接口，这个接口叫FutureFuture表示一个异步执行的情况的结果，我们可以对这个cancel还可以get结果还可以判断它是否执行完成或是否取消等FutureTask就是结合了Runnable和Future，即对一个可运行的任务，可以执行上述说的，查询结果或查看状态。2. Netty Future基本的Java Future前面已经说了，但基本的Java Future接口存在问题，表面上是异步，但实际想在执行完成后做些什么还得是同步的，即需要阻塞等待执行完成。也即传统的Java Future其问题是虽然可以得到当前执行的情况和获得执行的结果，但都不是真正上的异步，因此Netty扩充了Java Future的Futute功能，其补充功能如下：//判断当前执行是否成功 boolean isSuccess(); //判断当前执行是否取消Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener); boolean isCancellable(); //判断当前任务是否出错，如果出错返回出错信息 Throwable cause(); //添加监听器，在任务执行完成时回调 Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener); Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners); //移除监听器 Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener); Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners); //同步操作，会一直等到任务执行完成，如果任务执行失败则抛出失败原因 Future<V> sync() throws InterruptedException; //同步操作，等待任务完成，如果任务中断会抛出异常 Future<V> await() throws InterruptedException; //直接返回结果，不阻塞，如果任务未执行完会返回null V getNow();与Java Future最大的不同是Netty的Future多了监听器这一事件，监听器本质是一种回调，用于在任务完成时进行回调。通常而言，异步和回调是成对出现的，也即我们不仅要异步的执行任务，还要通过回调函数在任务执行完后处理结果ChannelFuture在Futute的基础上提供了channel()的操作，也即ChannelFuture认为，其事件必然会绑定一个Channel，通过channel()可以获得当前ChannelFuture绑定的Channel不同的是，Netty不仅引入了Future，还引入了异步思想下的Promise我们之前知道，Netty的future与java的不同有一点是包含isSuccess方法，也即Java的future是只关心干没干完的，到底成没成功我不知道，你别问我，但netty的future是可以返回这个任务是否成功的，因此我们就需要一个对当前任务是否成功的状态设置的功能，这就是Promise接口首先Promise继承自netty的future，也即promise也具有future的这些功能，比如获得结果，注册监听器等其次，promise还具有对结果的设置能力//设置结果为成功 Promise<V> setSuccess(V result); //设置结果为失败 Promise<V> setFailure(Throwable cause);换言之，promise比futute强的地方是可以设置结果的成功或失败，这样在获取结果的时候就可以通过成功或失败来判断而不是是否完成来判断了Netty下的ChannelPromise继承自ChannelFuture和PromiseChanne…

聊一聊这个十一
type: item
date: 2023-10-07T01:44:00+00:00
description: 聊一聊这个十一十一的时候，大学舍友小宝结婚，我回了趟西安。之所以说回，是因为我大学是在西安读的。宿舍的其他人也基本都到了，上次大家聚齐还是两年前我结婚的时候，只能感慨时光荏苒。再次相聚，熟悉的感觉一下子拉回到大学，当时我们是三室一厅，一个小室4个人，12个人的关系很好，很怀念那段无忧无虑又无所事事的时光。小宝的婚礼办的很好，新娘是我们学校同专业的学姐，当时在团委认识的。后来他们又都保研到了本校研究生，算下来俩人都谈了六七年了，今天也算修成正果。我本来打算是回趟母校的，毕竟毕业四年多也没回母校看看。但也许是近乡情怯，也许是不敢面对，总之我没有去。我参加完小宝的婚礼后，就定了转天早上的机票，匆匆回了天津。在外两三天，看着小宝的婚礼，我只是觉得十分想家，想立刻回去。我那时有种说不出的悲伤和急切。我偶然间在某论坛看到一个同行聊起他的职业生涯，过的很是坎坷，一路爬摸滚打。时间总是过得很快，我都毕业四年多了，我不知道这个行业我未来的出路是什么，感觉到的只有无尽的迷茫和失望。这次出行，我也意识到了一个问题，自己虽然已经毕业四年多，结婚两年多，但一直在忽视身边的人，对待家人一直做的不够，还像个孩子一样自私。自己的身体也发福越来越严重，也开始出现一些健康问题，尤其是今年。忽然有种人到中年的感觉。今年四月份换了份工作，新的工作还算比较满意，但公司的财务不是很好，不知道公司能活多久，自己什么时候会被裁。今年我想，将自己的更多重心放在家庭和自己的身体上，珍惜时间，珍惜眼前人。

简单聊聊DNS
type: item
date: 2023-08-26T14:27:00+00:00
description: 简单聊聊DNS本文主要内容源自 计算机网络（原书第7版）本文是笔者在学习的过程中整理的内容1. DNS的功能DNS的主要作用我们都知道是将域名解析为IP地址，这是因为我们人类往往擅长记忆一些名字和字符，而不擅长冗长的数字，但实际的网路连接和路由中又需要具体的IP地址而非无规律的字符。但除此以外，DNS还具备如下功能：主机别名。这主要靠DNS的CNAME记录实现（关于CNAME下面我们会说），比如你现在的服务器域名是c6282k4786.qficp.xyz，这很长很难记住，这时你想换个更简单的域名如coderzoe.com，因为简单的域名更利于你网站的推广，但你又不想老的域名失效（比如有些老用户是使用老域名访问的），此时就可以做到将coderzoe.com域名指向c6282k4786.qficp.xyz。解析邮箱服务。我们常使用的如xxx@qq.com或者xxx@163.com其中qq.com和163.com都是邮箱服务器的域名，这些域名也需要解析为IP地址，因此DNS除了解析Web端的域名还会解析邮箱的域名。这主要靠DNS的MX记录实现（关于MX我们下面也会说）。事实上MX记录允许一个公司的邮件服务器和Web服务器使用相同的域名（如网站https://qq.com和邮箱xxx@qq.com）。负载均衡。一个域名可以对应多个IP地址，当客户端向DNS服务器发起某一域名的查询请求时，如果这个域名对应一个IP集合，那么DNS服务器往往会将这整个集合响应给客户端，但在每个响应中循环这些地址次序。因为客户通常总是向IP地址排在最前面的服务器发送HTTP请求报文，所以 DNS就在这些IP地址集对应的服务器集之间循环实现了负载均衡。2. DNS记录本文不详细展开DNS的报文，只简单讲下DNS记录。DNS记录大致可以概括为一个四元组：（Name，Value，Type，TTL）其中TTL是当前记录的有效时间，我们暂时忽略掉。Type是DNS的类型，Name和Value的取值取决于Type，目前DNS主要有4种Type：Type = A，往往叫A记录。它的Name是域名而Value是该域名对应的IP地址。比如(baidu.com, 172.83.159.98, A)就是一条A记录，它最根本的作用就是提供了域名和IP地址的映射关系，当我们向DNS请求baidu.com的IP地址时，DNS服务器会响应这样一条A记录给我们。Type = CNAME，往往叫CNAME记录。它的Name是域名，而Value也是个域名，比如(coderzoe.com, baidu.com, CNAME)，我将coderzoe.com域名指向baidu.com，这样大家输入https://coderzoe.com响应的就是百度的网站（如果百度服务器未做拦截）。CNAME之所以生效的原因是当客户端向DNS请求coderzoe.com的IP时，DNS会先发现一个CNAME记录(coderzoe.com, baidu.com, CNAME)，然后DNS服务器再查询baidu.com，这时会发现一个(baidu.com, 172.83.159.98, A)的A记录，最后将这个A记录响应给客户端。Type = MX，往往叫MX记录。MX记录主要用于邮箱服务器的解析，它的Name是邮箱的域名，而Value也是个域名。比如(qq.com, mx1.qq.com, MX)。假设我们想通过QQ邮箱xxx@qq.com发送邮件，邮箱客户端会向DNS服务器请求qq.comMX记录，DNS服务器收到请求后，会将(qq.com, mx1.qq.com, MX)记录返回给邮箱客户端，邮箱客户端再向DNS服务器发起mx1.qq.com的A记录请求，DNS服务器会将(mx1.qq.com, 113.96.210.17, A)记录返回给客户端，这样客户端就知道了QQ邮箱服务器的IP地址。我们知道qq.com既是TX公司的邮箱域名也是它的Web域名，那当我们向DNS服务器发起qq.com的请求时，DNS服务器要按邮箱解析还是按Web解析呢？这两种解析情况往往还会对应不同的服务器。为了区别这两种情况，请求客户端会带上自己想要的DNS类型，当进行Web访问的时候，请求就是qq.com+A记录；而进行邮件发送时，请求客户端会发送qq.com+MX记录，得到MX记录后再向DNS发送一次A记录请求，最终得到邮箱服务器IP。Type = NS，往往叫NS记录。NS记录的主要作用是指引DNS的查询路径，告诉客户端去哪里进一步获取查询域的信息。这主要与DNS的工作原理有关，我们来详细的讲一下。3. DNS工作原理我们在上文讲到DNS服务器的主要功能是将域名解析为IP地址。为阐述DNS的工作原理，我们假设DNS采用极其简单的一种…

JUC源码详细分析之Part4——《线程池》
type: item
date: 2023-07-23T02:18:00+00:00
description: JUC源码详细分析之Part4——《线程池》本系列笔记笔者将会详细的讲解JUC中核心类的源码，其中基于的JDK版本为1.8.0_291。由于JUC内容很多，因此笔者将笔记拆为四部分：JUC源码详细分析之Part1——《CAS和原子类型》JUC源码详细分析之Part2——《锁》JUC源码详细分析之Part3——《常见的线程安全集合》JUC源码详细分析之Part4——《线程池》另外，本系列笔记部分内容参考自《深入浅出 Java Concurrency》5. 线程池在讲解线程池前先简单说下线程，我们都知道线程是由操作系统创建的，代表了任务调度的最小颗粒度。Java下使用Thread类代表线程，一个Thread对象就代表一个操作系统所创建线程的引用。Java下创建一个Thread对象与操作系统创建一个线程并不相等，当我们执行new Tnread()时只是创建了一个Thread对象，此时OS并未创建真实线程，但是当我们执行thread.start()时，操作系统会创建并启动一个线程。但是操作系统创建和启动线程属于系统调用，往往系统调用的开销比较大，如果我们频繁的创建线程和销毁线程肯定会降低系统的吞吐量，此时就需要将线程的生命周期与任务的执行解耦，引入线程池来管理线程，避免重复创建/销毁线程时的系统调用开销。5.1 线程池体系简介JUC下线程池体系如下：Executor接口，最顶层接口，只有一个方法void execute(Runable command)，提供了任务提交的基本方法ExecutorService接口，继承了 Executor 接口，最主要的就是获取异步任务执行结果和线程池销毁等方法ScheduledExecutorService 增加了定时任务，周期执行任务的功能AbstractExecutorService 默认实现了ExecutorService的submit()方法并且提供 newTaskFor() 方法将Runnable或Callable转换为RunnableFuture，以便提交给 Executor 执行ThreadPoolExecutor 默认的线程池实现类ScheduledThreadPoolExecutor 默认的带有定时/周期执行任务的线程池实现类线程池的创建是比较复杂的，为解决这一情况，JDK提供了Executors类，用于快速的创建线程池。Executors类下常用API如下：Executors.newCachedThreadPool() 无界线程池，可以进行自动线程回收Executors.newFixedThreadPool(int) 固定大小的线程池Executors.newSingleThreadExecutor() 单个线程的线程池Executors.newScheduledThreadPool(int) 执行定时任务的线程池Executors.newWorkStealingPool(int) 支持并行执行的线程池但是阿里巴巴Java开发手册禁止使用使用Executors创建线程池，其描述如下：【强制】线程资源必须通过线程池提供，不允许在应用中自行显式创建线程。说明：使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销，解决资源不足的问题。如果不使用线程池，有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。【强制】线程池不允许使用Executors去创建，而是通过ThreadPoolExecutor的方式，这样的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险。说明：Executors返回的线程池对象的弊端如下：1）FixedThreadPool和SingleThreadPool：允许的请求队列长度为Integer.MAX_VALUE，可能会堆积大量的请求，从而导致OOM。2）CachedThreadPool和ScheduledThreadPool：允许的创建线程数量为Integer.MAX_VALUE，可能会创建大量的线程，从而导致OOM。第一个不显示创建线程的问题我们已经说过了；第二个不能用Executors去创建线程池，其与线程池创建时的参数有关，我们下面看一下ThreadPoolExecutor 创建的参数。5.2 ThreadPoolExecutor参数和工作原理public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler …

JUC源码详细分析之Part3——《常见的线程安全集合》
type: item
date: 2023-07-20T12:48:00+00:00
description: JUC源码详细分析之Part3——《常见的线程安全集合》本系列笔记笔者将会详细的讲解JUC中核心类的源码，其中基于的JDK版本为1.8.0_291。由于JUC内容很多，因此笔者将笔记拆为四部分：JUC源码详细分析之Part1——《CAS和原子类型》JUC源码详细分析之Part2——《锁》JUC源码详细分析之Part3——《常见的线程安全集合》JUC源码详细分析之Part4——《线程池》另外，本系列笔记部分内容参考自《深入浅出 Java Concurrency》4. 常见的线程安全集合本章主要讲JUC下部分线程安全的数据结构，我们在日常开发中会经常使用他们，但是关于他们的实现原理与源码很多同学可能没了解过，现在就带大家了解JUC下常用数据结构的源码。4.1 ConcurrentHashMap我们之前利用读写锁实现了一个线程安全的HashMap，但是这个Map的性能并不高，因为首先他只支持同一时间一个线程的写入，另外读取的时候也需要加锁读取，在写的时候不允许读取，我们希望一个效率更高的map来替换之前读写锁版本的map，至少他需要改进两个功能：可以支持一定量的并发修改读取的时候不需要加锁在理解ConcurrentHashMap前，我们默认读者对HashMap的实现是有一定了解的，有HashMap源码的经验，包括JDK1.7和JDK1.8版本不同的实现方式。同样，ConcurrentHashMap的实现在JDK1.7和JDK1.8版本中也有所不同，我们先来说JDK1.7版本中ConcurrentHashMap的实现。4.1.1 JDK1.7 ConcurrentHashMap在JDK1.7中，ConcurrentHashMap的实现采用了锁分段的思想（JDK1.8也是，不过锁的颗粒度不同）。首先我们知道JDK1.7中HashMap是一个数组加链表的结构，假设我们将数组进行拆分，拆成n份，那么其实每一份是一个更小的数组，或者说每一份是一个更小的HashMap。我们将每一份小的HashMap称为段(Segment)。那么如果我们在操作的时候，先定位到使用哪个段，然后再在段内进行线程安全的增删改查。也即，每个段都是彼此并行的，段与段没有交集，这样最多可支持并发为n的写入/删除操作（n为段的数量）。如：当前Map数组长度2n，将Map分为n个段，那么每个段的数组长度就是2，每个段都可以认为是个小的HashMap，段与段之间并行，段内的元素操作都是线程安全的。JDK1.7中ConcurrentHashMap的实现，其结构如下：首先ConcurrentHashMap内包含一个segments，即段数组，它包含16个段，每个段都是一个类HashMap结构，这个结构（Segment）持有一个独占锁，保证在对段进行修改操作时是线程安全的。另外哈希桶的实现（HashEntry）的实现是链表的方式，即相同hash值的节点通过链表串起来（与JDK1.7相同）。在这里我们引入了段，为了提高map修改时的并发能力，但同样也带来了一个问题：当我们要操作一个元素时，如何定位这个元素在哪个段上，即原来我们只需要定位哈希桶，然后遍历链表节点，现在需要变为段->哈希桶->节点。JDK1.7中的实现依然是通过hash，即先通过hash定位到段，再通过二次hash定位到哈希桶(hashEntry)，然后再通过遍历链表得到具体的节点。了解了上面这些实现，我们可以从另一个角度看一下ConcurrentHashMap。首先ConcurrentHashMap内部有一个HashMap，这个HashMap的key是段号，value值是Segment。同时Segment也是一个HashMap，因此我们可以认为ConcurrentHashMap是一个value为HashMap的HashMap，这样也就不难理解进行元素操作时需要二次hash了。我们先来看下JDK1.7中ConcurrentHashMap是如何通过key获得段的：首先会将key值的hashCode(每一个Java Object都有hashCode)通过hash()进行二次编码（哈希算法打散均匀），然后将得到打散后的hash值通过segmentFor()函数，就可以定位到key对应的segments数组对应的数组下角标，返回数组下角标出处的Segment即可。熟悉HashMap的同学不难发现这与HashMap获得哈希桶的方式一模一样。//hash算法，打散key的hashcode private static int hash(int h) { // Spread bits to regularize both segment and index locations, // using variant of single-wor…

JUC源码详细分析之Part2——《锁》
type: item
date: 2023-07-20T12:45:00+00:00
description: JUC源码详细分析之Part2——《锁》本系列笔记笔者将会详细的讲解JUC中核心类的源码，其中基于的JDK版本为1.8.0_291。由于JUC内容很多，因此笔者将笔记拆为四部分：JUC源码详细分析之Part1——《CAS和原子类型》JUC源码详细分析之Part2——《锁》JUC源码详细分析之Part3——《常见的线程安全集合》JUC源码详细分析之Part4——《线程池》另外，本系列笔记部分内容参考自《深入浅出 Java Concurrency》3. 锁锁是JUC中最核心的内容，在本章我们将讲解JUC下常见的锁，以及介绍下著名的AQS框架。在讲解源码前，我们先来介绍一些基本的但常见的概念。锁是什么？解决了什么问题？在多线程下，我们希望有些代码是线程安全的执行的，我们将这样的代码行叫临界区。锁保证了临界区的代码可以像单条指令一样原子的执行，当某个线程持有锁的时候，就可以执行临界区的代码，其他线程此时是不可以的，只有这个持有锁的线程释放锁才能继续由下一个线程执行。悲观锁，乐观锁悲观锁：每次都以悲观的态度假设最坏的情况，每次拿数据的时候都认为别人会修改，所以每次在操作前会先上锁，操作后再将锁释放，Java中的synchronized和ReentrantLock都是悲观锁。乐观锁：每次都是乐观的态度假设当前是最好的情况，每次拿数据的时候都认为别人不会修改，所以不会上锁，但是在修改的时候会判断别人有没有修改这个数据（通过CAS和版本号实现），Java中的AtomicInteger就是乐观锁的一种实现，依赖了CAS。我们一般也认为悲观锁是阻塞锁，而乐观锁是非阻塞锁。非阻塞指的是不阻塞其他线程，即自己可以失败可以阻塞挂起，但不能因为自己的失败和挂起影响其他线程导致其他线程失败挂起。很多乐观锁的实现都依赖于CAS，但CAS本质是没有锁的，虽然叫乐观锁但它并没有锁。CAS只是通过比较和替换的方式，只有比较之后符合要求才进行替换，其中比较和替换是个原子操作，这其中并没有上锁以及并不会干扰任何其他线程，通常CAS都是自旋的，不断的尝试比较和替换，所以这里可以看到CAS是非阻塞锁，他的执行并不会干扰其他线程。由于CAS没有加锁，所以需要通过CAS同步修改的变量都应该使用volatile修饰符，这样保证变量的修改可以被其他线程看到。通常而言乐观锁应用于写少读多的情况，悲观锁应用于写多读少的情况。可重入锁，不可重入锁所谓可重入锁就是如果一个线程持有了锁，那么它可以重复执行需要这种锁的临界区代码，可以进行重复加锁。反之为不可重入锁。如：public void test(){ lock.lock(); try { while (x > 10){ x--; test(); } }finally { lock.unlock(); } }独占锁，共享锁所谓独占锁和共享锁是对资源而言的，如果某个资源只能同时被一个线程访问，那么就需要使用独占锁。而如果可以被所有线程一起访问，那么就可以使用共享锁。其中独占锁又叫互斥锁。大多数情况下我们在说锁的时候都是默认独占锁。公平锁，非公平锁如果获取一个锁是按照请求的顺序得到的，那么就是公平锁，否则就是非公平锁。这里有一篇很棒的文章，可以方便大家对Java中的锁有个大体了解：不可不说的Java“锁”事 - 美团技术团队 (meituan.com)3.1 Lock与AQSJava下的Lock接口定义如下：public interface Lock { //获取锁，如果锁不可用，出于线程调度目的，将禁用当前线程，并且在获得锁之前，该线程将一直处于休眠状态。 void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁 void unlock(); Condition newCondition(); }JUC下大部分锁的实现均是继承自该接口，其中Lock比较重要的两个函数是lock和unlock，我们在讲具体实现类的时候也主要讲这两个接口的实现。在讲JUC下锁的实现类前，我们先想下如果让你设计一个锁，实现lock()方法，你需要做什么？首先要判断是否允许当前线程获取锁，对于是否允许获取锁，得有一个状态位，这个状态位的修改必须是线程安全的。其次如果允许获取锁，就获取，否则就阻塞等待或者直接获取失败。若线程需要阻塞等待，那么就需要一个阻塞等待的队列。最后阻塞的时候，线程往往需要挂起不消耗资源。当状态位由不允许获取变更到允许获取时，得唤醒队列里的线程，并且线程得从阻塞队列中移除出去。综上…