<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>3247号监听站</title>
    <link>https://dyzmj.top/</link>
    <description><![CDATA[遇事不决，可问春风]]></description>
    <language>zh-CN</language>
    <managingEditor>dyzmj@qq.com (夂夂鱼)</managingEditor>
    <pubDate>Mon, 27 Apr 2026 12:14:38 +0000</pubDate>
    <lastBuildDate>Mon, 27 Apr 2026 12:14:38 +0000</lastBuildDate>
    <generator>grtblog v2.0.5</generator>
    <image>
      <url>https://img.dyzmj.top/img202509222023675.png</url>
      <title>3247号监听站</title>
      <link>https://dyzmj.top/</link>
    </image>
    <atom:link href="https://dyzmj.top/feed" rel="self" type="application/rss+xml"/><item>
      <title>关于我</title>
      <link>https://dyzmj.top/about</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/about">https://dyzmj.top/about</a></p></blockquote><p>这个人很懒，暂时还没有想好怎么描述~~</p>]]></description>
      <guid>page-8</guid>
      <pubDate>Mon, 27 Apr 2026 12:14:38 +0000</pubDate>
    </item>
    <item>
      <title>Arthas</title>
      <link>https://dyzmj.top/posts/arthas</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/arthas">https://dyzmj.top/posts/arthas</a></p></blockquote><p>[TOC]</p>
<h1>一、Arthas 能做什么</h1>
<p>引入一段官方的描述：</p>
<blockquote>
<p>当你遇到一下类似问题而束手无策是，<code>Arthas</code> 可以帮助你解决：</p>
<ol>
<li>这个类从哪个 jar 包加载的？为什么会报各种类相关的 Exception？</li>
<li>我改的代码为什么没有执行到？难道是我没 commit ？分支搞错了？</li>
<li>遇到问题无法在线上 debug，难道只能通过加日志再重新发布吗？</li>
<li>线上遇到某个用户的数据处理有问题，但线上同样无法 debug，线下无法复现！</li>
<li>是否有一个全局视角来查看系统的运行情况？</li>
<li>有什么办法可以监控到 JVM 的实时运行状态？</li>
<li>怎么快速定位应用的热点，生成火焰图？</li>
<li>怎样直接从 JVM 内查找某个类的示例？</li>
</ol>
<p><code>Arthas</code> 支持 JDK 6+，支持 Linux/Mac/Windows，采用命令行交互模式，同时提供丰富的 <code>Tab</code> 自动补全功能，进一步方便进行问题的定位和诊断。</p>
</blockquote>
<h1>二、安装与启动</h1>
<h2>2.1 推荐使用 <code>arthas-boot</code></h2>
<p>下载 <code>arthas-boot.jar</code>，然后用 <code>java -jar</code> 的方式启动：</p>
<pre><code class="language-shell">curl -O https://arthas.aliyun.com/arthas-boot.jar

java -jar arthas-boot.jar
</code></pre>
<h2>2.2 脚本一键安装</h2>
<p>使用 <code>as.sh</code>，Arthas 支持在 <code>Linux/Unix/Mac</code> 等平台上一键安装，复制以下内容，敲 <code>回车</code> 执行即可：</p>
<pre><code class="language-shell">curl -L https://arthas.aliyun.com/install.sh | sh
</code></pre>
<h2>2.3 退出</h2>
<p>不用 <code>arthas</code> 时一定要正常退出，命令如下：</p>
<ul>
<li><code>quit:</code> 只是退出当前的连接。<code>Attach</code> 到目标进程上的 <code>arthas</code> 还会继续运行，端口会保持开放，下次连接时执行 <code>java -jar arthas-boot.jar</code> 可以直接连接上。</li>
<li><code>exit:</code> 和 <code>quit</code> 命令一样的功能。</li>
<li><code>stop:</code> 完全退出 <code>arthas</code>。</li>
</ul>
<blockquote>
<p>注意：</p>
<p><code>arthas</code> 依赖 JDK 的环境变量，也依赖一些 JDK 自带的工具，比如 <code>jps</code>，如果服务器上只有 JRE 环境而没有 JDK 环境的话，是没有 <code>jps</code> 的，所以 <code>arthas</code> 也会报错。</p>
</blockquote>
<h1>三、快速入门</h1>
<h2>3.1 进入指定的 JVM 进程</h2>
<p>在命令行中执行（使用和目标进程一致的用户启动，否则可能 <code>attach</code> 失败）：</p>
<pre><code class="language-shell">curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
</code></pre>
<blockquote>
<p>注：</p>
<ul>
<li>如果下载速度比较慢，可以使用 <code>aliyun</code> 的镜像：<code> java -jar arthas-boot.jar --repo-mirror aliyun --use-http</code>。</li>
<li>可以使用 <code>java -jar arthas-boot.jar -h</code> 打印更多参数信息。</li>
</ul>
</blockquote>
<p>执行完后可以看到：</p>
<p><img src="https://img.dyzmj.top/img/202205251350269.png" alt="image-20220525135015568"></p>
<p>如上，如系统内含有多个 Java 程序，可以使用 <code>ps -ef|grep java</code>  查找要查看程序的 <code>PID</code>，输入上面 <code>PID</code> 对应的序号即可进入 <code>arthas</code> 中。</p>
<p><img src="https://img.dyzmj.top/img/202205251356271.png" alt="image-20220525135645074"></p>
<h2>3.2 基础命令</h2>
<h3>3.2.1 查看 dashboard 仪表盘</h3>
<p>输入 <code>dashboard</code>命令，会在仪表盘中展示当前进程的信息，按 <code>ctrl+c/q</code> 可以中断执行。</p>
<p><img src="https://img.dyzmj.top/img/202205251450245.png" alt="image-20220525145001099"></p>
<p><strong>数据列说明：</strong></p>
<ul>
<li>
<p><code>ID:</code> Java 级别的线程 ID，注意这个ID 不能跟 <code>jstack</code> 中的 <code>nativeID</code> 一一对应。</p>
</li>
<li>
<p><code>NAME:</code> 线程名。</p>
</li>
<li>
<p><code>GROUP:</code> 线程组名。</p>
</li>
<li>
<p><code>PRIORITY:</code> 线程优先级，1~10 之间的数字，越大表示优先级越高。</p>
</li>
<li>
<p><code>STATE:</code> 线程的状态。</p>
</li>
<li>
<p><code>%CPU:</code> 线程的 <code>cpu</code> 使用率。比如采样间隔时间 1000ms，某个线程的增量 <code>cpu</code> 时间为 100ms，则 <code>cpu</code> 使用率 = 100 / 1000 = 10%。</p>
</li>
<li>
<p><code>DELTA_TIME:</code> 上次采样之后线程运行增量 <code>cpu</code> 时间，数据格式为 <code>秒</code>。</p>
</li>
<li>
<p><code>TIME:</code> 线程运行总 <code>cpu</code> 时间，数据格式为 <code>分:秒</code>。</p>
</li>
<li>
<p><code>INTERRUPTED:</code> 线程当前的中断位状态。</p>
</li>
<li>
<p><code>DAEMON:</code> 是否是 <code>daemon</code> 线程。</p>
</li>
</ul>
<p><strong>JVM 内部线程：</strong></p>
<p>Java 8 之后支持获取 JVM 内部线程 CPU 时间， 这些线程只有名称和 CPU 时间，没有 ID 及状态等信息（显示 ID 为 -1）。通过内部线程可以观测到 JVM 活动，如 GC、JIT 编译等占用 CPU 的情况，方便了解 JVM 整体运行状况。</p>
<ul>
<li>当 JVM 堆（heap）/ 元数据（metaspace）空间不足或 OOM 时，可以看到 GC 线程 CPU 占用率明细高于其他的线程。</li>
<li>当执行 <code>trace/watch/tt/redefine</code> 等命令后，可以看到 JIT 线程活动变得更频繁。因为 JVM 热更新 class 字节码时清除了此 class 相关的 JIT 编译结果，需要重新编译。</li>
</ul>
<p>JVM内部线程包括一下几种：</p>
<ul>
<li><code>JIT 编译线程:</code> 如 <code>C1 CompilerThread0</code>、<code>C2 CompilerThread0</code>。</li>
<li><code>GC 线程:</code> 如 <code>GC Thread0</code>，<code>G1 Young RemSet Sampling</code>。</li>
<li><code>其他内部线程:</code> 如 <code>VM Periodic Task Thread</code>、<code>VM Thread</code>、<code>Service Thread</code>。</li>
</ul>
<p><strong>设置刷新间隔和次数：</strong></p>
<p>面板默认会每 5 秒刷新一次，并且会一直刷新下去，如果想要指定刷新次数和间隔时间，可以这么写：</p>
<pre><code class="language-shell">dashboard -i 2000 -n 2    // 间隔两秒刷新两次
</code></pre>
<p><code>-i</code> 表示刷新的间隔时间，单位毫秒，<code>-n</code> 表示查询的次数，到达指定次数后，自动退出仪表盘面板。</p>
<h3>3.2.2 thread 查看线程堆栈信息</h3>
<p>当没有参数，默认按照CPU增量时间降序排列，只显示第一页数据：<code>thread</code>。</p>
<p><img src="https://img.dyzmj.top/img/202205251519974.png" alt="image-20220525151932847"></p>
<p>查看某个线程的堆栈：<code>thead 1</code>，1 一般为 main 线程的线程 id。</p>
<p><img src="https://img.dyzmj.top/img/202205251523919.png" alt="image-20220525152337855"></p>
<p>除此之外，<code>thread</code> 还有其他的用法：</p>
<ul>
<li><code>thread -n 5</code> ：打印前 5 个最忙的线程并打印堆栈。</li>
<li><code>thread -all</code> ：显示所有匹配的线程。</li>
<li><code>thread -n 3 -i 1000</code>：列出 1000ms 内最忙的 3 个线程。</li>
<li><code>thread -state WAITTING</code>：查看指定状态的线程（<code>TIMED_WAIT</code>、<code>WAITING</code>、<code>RUNNABLE</code> 等）。</li>
<li><code>thread -b</code>：找出阻塞其他线程的线程，当出现死锁后，会提示出现死锁的位置。</li>
</ul>
<h3>3.2.3 通过 jad 来反编译 Class</h3>
<p>使用 <code>jad 类路径名</code> 即可反编译指定的类。</p>
<pre><code class="language-java">[arthas@16616]$ jad demo.MathGame

ClassLoader:                                                                                         
+-sun.misc.Launcher$AppClassLoader@5c647e05                                                                                                                                             
  +-sun.misc.Launcher$ExtClassLoader@77683676                                                                                                                                           
Location:                                                                                                                                                                                                         
/usr/local/soft/yudd/test/math-game.jar                                                     
       /*
        * Decompiled with CFR.
        */
       package demo;
       
       import java.util.ArrayList;
       import java.util.List;
       import java.util.Random;
       import java.util.concurrent.TimeUnit;
       
       public class MathGame {
           private static Random random = new Random();
           private int illegalArgumentCount = 0;
       
           public List&lt;Integer&gt; primeFactors(int number) {
/*44*/         if (number &lt; 2) {
/*45*/             ++this.illegalArgumentCount;
                   throw new IllegalArgumentException(&quot;number is: &quot; + number + &quot;, need &gt;= 2&quot;);
               }
               ArrayList&lt;Integer&gt; result = new ArrayList&lt;Integer&gt;();
/*50*/         int i = 2;
/*51*/         while (i &lt;= number) {
/*52*/             if (number % i == 0) {
/*53*/                 result.add(i);
/*54*/                 number /= i;
/*55*/                 i = 2;
                       continue;
                   }
/*57*/             ++i;
               }
/*61*/         return result;
           }
       
           public static void main(String[] args) throws InterruptedException {
               MathGame game = new MathGame();
               while (true) {
/*16*/             game.run();
/*17*/             TimeUnit.SECONDS.sleep(1L);
               }
           }
       
           public void run() throws InterruptedException {
               try {
/*23*/             int number = random.nextInt() / 10000;
/*24*/             List&lt;Integer&gt; primeFactors = this.primeFactors(number);
/*25*/             MathGame.print(number, primeFactors);
               }
               catch (Exception e) {
/*28*/             System.out.println(String.format(&quot;illegalArgumentCount:%3d, &quot;, this.illegalArgumentCount) + e.getMessage());
               }
           }
       
           public static void print(int number, List&lt;Integer&gt; primeFactors) {
               StringBuffer sb = new StringBuffer(number + &quot;=&quot;);
/*34*/         for (int factor : primeFactors) {
/*35*/             sb.append(factor).append('*');
               }
/*37*/         if (sb.charAt(sb.length() - 1) == '*') {
/*38*/             sb.deleteCharAt(sb.length() - 1);
               }
/*40*/         System.out.println(sb);
           }
       }

Affect(row-cnt:1) cost in 793 ms.
</code></pre>
<p>此命令反编译的内容包括类使用的类加载器、所在的 jar 包、源码信息。</p>
<p>若要将反编译的源码生成到指定的文件中可使用如下命令：</p>
<pre><code class="language-shell">jad --source-only demo.MathGame  &gt; /home/MathGame.java
</code></pre>
<h3>3.2.4 watch 查看方法的出入参</h3>
<p>通过 <code>watch</code> 命令来查看 <code>demo.MathGame#primeFactors</code>方法的返回值：</p>
<p><img src="https://img.dyzmj.top/img/202205251710484.png" alt="image-20220525171013375"></p>
<p>参数说明:</p>
<ul>
<li><code>&quot;{params, returnObj}&quot;</code> ：观察表达式，默认值：<code>{params, target, returnObj}</code>，是一个  <a href="https://commons.apache.org/proper/commons-ognl/language-guide.html"><!-- raw HTML omitted -->OGNL<!-- raw HTML omitted --></a> 表达式。</li>
<li><code>-x 3</code>：是指定输出结果的属性遍历深度，默认值为1，为1时看不到参数化的具体值，只能看到类型，最大值为4，防止展开结果占用太多内存。可以在 <code>OGNL</code> 表达式里指定更具体的 <code>field</code>。</li>
<li><code>-b</code>：在方法调用之前观察，用此命令可查看方法的入参。</li>
<li><code>-e</code>：在方法异常之后观察，用此命令可以查看方法抛出的异常。</li>
<li><code>-s</code>：在方法返回之后观察，可查看方法的返回值。</li>
<li><code>-f</code>：在方法结束之后（正常返回和异常返回）观察，可查看方法的返回值和异常信息，默认打开 <code>-f</code>。</li>
<li><code>-n 4</code>：表示监控方法只执行四次。</li>
</ul>
<h2>3.3 退出 arthas</h2>
<p>如果只是退出当前的连接，可以使用 <code>quit</code> 或 <code>exit</code>命令。Attach 到目标进程上的 <code>arthas</code> 还会继续运行，端口会保持开放，下次连接时可以直接连接上。</p>
<p>如果想要完全退出 <code>arthas</code>，可以执行 <code>stop</code> 命令。</p>
<h1>四、进阶使用</h1>
<h2>4.1 基础命令</h2>
<table>
<thead>
<tr>
<th>命令</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>help</code></td>
<td>查看命令帮助信息</td>
</tr>
<tr>
<td><code>cat</code></td>
<td>打印文件内容，和 linux 里面的 <code>cat</code> 命令类似</td>
</tr>
<tr>
<td><code>echo</code></td>
<td>打印参数，和 linux 里面的 <code>echo</code> 命令类似</td>
</tr>
<tr>
<td><code>grep</code></td>
<td>匹配查找，和 linux 里面的 <code>grep</code> 命令类似</td>
</tr>
<tr>
<td><code>base64</code></td>
<td>base64 编码转换，和 linux 里面的 <code>base64</code> 命令类似</td>
</tr>
<tr>
<td><code>tee</code></td>
<td>复制标准输入到标准输出和指定的文件，和 linux 里的 <code>tee</code> 命令类似</td>
</tr>
<tr>
<td><code>pwd</code></td>
<td>返回当前的工作目录，和 linux <code>pwd</code> 命令类似</td>
</tr>
<tr>
<td><code>cls</code></td>
<td>清空当前屏幕区域</td>
</tr>
<tr>
<td><code>session</code></td>
<td>查看当前会话的信息</td>
</tr>
<tr>
<td><code>rest</code></td>
<td>重置增强类，将被 <code>Arthas</code> 增强过的类全部还原，<code>Arthas</code> 服务端关闭时会重置所有增强过的类</td>
</tr>
<tr>
<td><code>version</code></td>
<td>输出当前目标 Java 进程所加载的 <code>Arthas</code> 版本号</td>
</tr>
<tr>
<td><code>history</code></td>
<td>打印命令历史</td>
</tr>
<tr>
<td><code>quit</code></td>
<td>退出当前 <code>Arthas</code> 客户端，其他 <code>Arthas</code> 客户端不受影响</td>
</tr>
<tr>
<td><code>stop</code></td>
<td>关闭 <code>Arthas</code> 服务端，所有 <code>Arthas</code> 客户端全部退出</td>
</tr>
<tr>
<td><code>keymap</code></td>
<td><code>Arthas</code>快捷键列表及自定义快捷键</td>
</tr>
</tbody>
</table>
<h2>4.2 JVM 相关指令</h2>
<h3>4.2.1 dashboard</h3>
<blockquote>
<p>查看当前系统的实时数据面板，按 q 或 ctrl +c 退出。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205260955293.png" alt="image-20220526095525123"></p>
<p>从上至下分别为：</p>
<ul>
<li>
<p>线程相关</p>
<ul>
<li>ID：线程ID</li>
<li>NAME：线程名字</li>
<li>GROUP：线程组</li>
<li>PRIORITY：线程优先级</li>
<li>STATE：线程状态</li>
<li>%CPU：cpu占比</li>
<li>TIME：运行时间。分钟:秒</li>
<li>INTERRUPTE：中断状态</li>
<li>DAEMON：是否是守护线程</li>
</ul>
</li>
<li>
<p>内存相关</p>
<ul>
<li>Memroy：内存区域</li>
<li>used：使用的内存</li>
<li>total：总内存</li>
<li>max：最大内存</li>
<li>usage：使用百分比</li>
<li>GC：垃圾回收机制</li>
</ul>
</li>
<li>
<p>运行环境</p>
</li>
</ul>
<h3>4.2.2 thread</h3>
<blockquote>
<p>查看当前线程信息，查看线程的堆栈。</p>
</blockquote>
<ul>
<li>数字：线程ID</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261020695.png" alt="image-20220526102016622"></p>
<ul>
<li>n：前 n 个最忙的线程堆栈信息</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261021989.png" alt="image-20220526102145907"></p>
<ul>
<li>b：找出当前线程阻塞其他线程的线程</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261022830.png" alt="image-20220526102250775"></p>
<ul>
<li>
<p>i <!-- raw HTML omitted -->：制定 CPU 占比统计采样间隔，单位为毫秒</p>
</li>
<li>
<p>查看某种状态的所有线程</p>
</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261024640.png" alt="image-20220526102452565"></p>
<h3>4.2.3 jvm</h3>
<blockquote>
<p>查看当前 JVM 信息。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261031932.png" alt="image-20220526103155732"></p>
<p><strong>THREAD相关</strong></p>
<ul>
<li>COUNT: JVM当前活跃的线程数</li>
<li>DAEMON-COUNT: JVM当前活跃的守护线程数</li>
<li>PEAK-COUNT: 从JVM启动开始曾经活着的最大线程数</li>
<li>STARTED-COUNT: 从JVM启动开始总共启动过的线程次数</li>
<li>DEADLOCK-COUNT: JVM当前死锁的线程数</li>
</ul>
<p><strong>文件描述符相关</strong></p>
<ul>
<li>MAX-FILE-DESCRIPTOR-COUNT：JVM进程最大可以打开的文件描述符数</li>
<li>OPEN-FILE-DESCRIPTOR-COUNT：JVM当前打开的文件描述符数</li>
</ul>
<h3>4.2.4 sysprop</h3>
<blockquote>
<p>查看 Java 虚拟机属性，可以修改。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261035356.png" alt="image-20220526103516258"></p>
<h3>4.2.5 sysenv</h3>
<blockquote>
<p>查看 JVM 环境属性。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261036012.png" alt="image-20220526103655940"></p>
<h3>4.2.6 vmoption</h3>
<blockquote>
<p>查看、更新 JVM 诊断相关参数。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261037865.png" alt="image-20220526103751786"></p>
<h3>4.2.7 perfcounter</h3>
<blockquote>
<p>查看当前JVM 的 <code>Perf Counter</code> 信息</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261039280.png" alt="image-20220526103935203"></p>
<h3>4.2.8 logger</h3>
<blockquote>
<p>查看 logger 信息，更新 logger level。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261041872.png" alt="image-20220526104150777"></p>
<h3>4.2.9 getstatic</h3>
<blockquote>
<p>获取静态属性，<code>getstatic 类名 属性名</code>。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261043875.png" alt="image-20220526104326828"></p>
<h3>4.2.10 ognl</h3>
<blockquote>
<p>执行 OGNL 表达式，需要学习 OGNL 语法。</p>
</blockquote>
<p>使用参考：</p>
<ol>
<li><a href="https://github.com/alibaba/arthas/issues/71"><!-- raw HTML omitted -->《OGNL 特殊用法参考》<!-- raw HTML omitted --></a></li>
<li><a href="https://commons.apache.org/proper/commons-ognl/language-guide.html"><!-- raw HTML omitted -->《OGNL 表达式官方指南》<!-- raw HTML omitted --></a></li>
</ol>
<ul>
<li>调用静态方法</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261055451.png" alt="image-20220526105522407"></p>
<ul>
<li>获取静态字段</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261057390.png" alt="image-20220526105734323"></p>
<ul>
<li>执行多行表达式，赋值给临时变量，返回一个List</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261058568.png" alt="image-20220526105840506"></p>
<h3>4.2.11 mbean</h3>
<blockquote>
<p>查看 Mbean 的信息。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261103371.png" alt="image-20220526110319266"></p>
<h3>4.2.12 heapdump</h3>
<blockquote>
<p>dump java heap，类似 <code>jmap</code> 命令的 <code>heap dump</code> 功能。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261109645.png" alt="image-20220526110909563"></p>
<h3>4.2.13 vmtool</h3>
<blockquote>
<p><code>vmtool</code> 利用 <code>JVMTI</code> 接口，实现查询内存对鞋，强制 GC 等功能。</p>
</blockquote>
<ul>
<li>获取对象</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261111870.png" alt="image-20220526111132791"></p>
<blockquote>
<p>注：通过 <code>--limit</code>参数，可以限制返回值数量，避免获取超大数据时对JVM造成压力。默认值是10。</p>
</blockquote>
<ul>
<li>强制 GC</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202205261113933.png" alt="image-20220526111307879"></p>
<h2>4.3 class/classloader 相关</h2>
<h3>4.3.1 sc</h3>
<blockquote>
<p>查看 JVM 已加载的类信息。</p>
</blockquote>
<p>“Search-Class” 的简写，这个命令能搜索出所有已经加载到 JVM 中的 Class 信息。</p>
<p><strong>参数说明</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>class-pattern</td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td>method-pattern</td>
<td>方法名表达式</td>
</tr>
<tr>
<td>[d]</td>
<td>输出当前类的详细信息，包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader 等详细信息。如果一个类被多个 ClassLoader 所加载，则会出现多次</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td>[f]</td>
<td>输出当前类的成员变量信息（需要配合参数-d一起使用）</td>
</tr>
<tr>
<td>[x:]</td>
<td>指定输出静态变量时属性的遍历深度，默认为 0，即直接使用 <code>toString</code> 输出</td>
</tr>
<tr>
<td><code>[c:]</code></td>
<td>指定class的 ClassLoader 的 hashcode</td>
</tr>
<tr>
<td><code>[classLoaderClass:]</code></td>
<td>指定执行表达式的 ClassLoader 的 class name</td>
</tr>
<tr>
<td><code>[n:]</code></td>
<td>具有详细信息的匹配类的最大数量（默认为100）</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/202205261129181.png" alt="image-20220526112951102"></p>
<h3>4.3.2 sm</h3>
<blockquote>
<p>查看已加载类的方法信息。</p>
</blockquote>
<p>“Search-Method” 的简写，这个命令能搜索出所有已经加载了 Class 信息的方法信息。</p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>class-pattern</em></td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td><em>method-pattern</em></td>
<td>方法名表达式匹配</td>
</tr>
<tr>
<td>[d]</td>
<td>展示每个方法的详细信息</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td><code>[c:]</code></td>
<td>指定class的 ClassLoader 的 hashcode</td>
</tr>
<tr>
<td><code>[classLoaderClass:]</code></td>
<td>指定执行表达式的 ClassLoader 的 class name</td>
</tr>
<tr>
<td><code>[n:]</code></td>
<td>具有详细信息的匹配类的最大数量（默认为100）</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/202205261133225.png" alt="image-20220526113356145"></p>
<h3>4.3.3 jad</h3>
<blockquote>
<p>反编译指定已加载类的源码。</p>
</blockquote>
<p><strong>反编译某个类：</strong></p>
<p><img src="https://img.dyzmj.top/img/202205261135118.png" alt="image-20220526113551002"></p>
<p><strong>反编译某个方法：</strong></p>
<p><img src="https://img.dyzmj.top/img/202205261136048.png" alt="image-20220526113623985"></p>
<h3>4.3.4 mc</h3>
<blockquote>
<p>Memory Compiler / 内存编译器，编译 <code>.java</code> 文件生成 <code>.class</code> 文件。</p>
</blockquote>
<pre><code class="language-shell">mc /tmp/Test.java
</code></pre>
<p>可以通过 <code>-c</code> 参数指定 classloader：</p>
<pre><code>mc -c 5c647e05 /home/MathGame.java
</code></pre>
<p>可以通过 <code>-d</code> 命令指定输出目录：</p>
<pre><code>mc -d /tmp/output /tmp/ClassA.java /tmp/ClassB.java
</code></pre>
<p>编译生成 <code>.class</code> 文件之后，可以结合 <code>retransform</code> 或 <code>redefine</code> 命令实现热更新代码。</p>
<blockquote>
<p>注意：<code>mc</code> 命令有可能失败。如果编译失败可以在本地编译好 <code>.class</code> 文件，再上传到服务器。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261335696.png" alt="image-20220526133547624"></p>
<h3>4.3.5 retransform</h3>
<blockquote>
<p>加载外部的 <code>.class</code> 文件，retransform jvm 已加载的类。</p>
</blockquote>
<p><strong>retransform指定的 .class 文件：</strong></p>
<p><img src="https://img.dyzmj.top/img/202205261338023.png" alt="image-20220526133801950"></p>
<p>加载指定的 <code>.class</code> 文件，然后解析出 class name，在 retransform jvm 中已加载的对应的类。每加载一个 <code>.class</code> 文件，则会记录一个 retransform entry。</p>
<blockquote>
<p>如果多次执行 <code>retransform</code> 加载同一个 class 文件，则会有多条 retransform entry。</p>
</blockquote>
<p><strong>查看 retransform entry：</strong></p>
<p><img src="https://img.dyzmj.top/img/202205261341873.png" alt="image-20220526134153815"></p>
<p><strong>删除指定 retransform entry：</strong></p>
<p>需要指定 id：</p>
<p><img src="https://img.dyzmj.top/img/202205261343280.png" alt="image-20220526134345234"></p>
<p><strong>显示触发 retransform：</strong></p>
<p><img src="https://img.dyzmj.top/img/202205261345789.png" alt="image-20220526134525734"></p>
<blockquote>
<p>注意：对于同一个类，当存在多个 retransform entry 时，如果显式触发 retransform，则最后添加的 entry 生效（id 最大的）。</p>
</blockquote>
<p><strong>消除 retransform 的影响：</strong></p>
<p>如果对某个类执行 <code>retransform</code> 之后，想消除影响，则需要：</p>
<ul>
<li>删除这个类对应的 retransform entry</li>
<li>重新触发 <code>retransform</code></li>
</ul>
<blockquote>
<p>如果不清除掉所有的 retransform entry，并重新触发 <code>retransform</code>，则 <code>arthas</code> <code>stop</code> 时，<code>retransform</code> 过时的类仍然生效。</p>
</blockquote>
<p><strong>retransform 使用限制：</strong></p>
<ul>
<li>不允许新增加 <code>filed/method</code>。</li>
<li>正在运行的方法，一直没有退出不能生效。</li>
</ul>
<h3>4.3.6 redefine</h3>
<blockquote>
<p>加载外部的 <code>.class</code> 文件，redefine jvm 已加载的类。</p>
</blockquote>
<p><strong>常见问题：</strong></p>
<blockquote>
<p>推荐使用 <code>retransform</code> 命令。</p>
</blockquote>
<ul>
<li>redefine的class不能修改、添加、删除类的field和method，包括方法参数、方法名称及返回值</li>
<li>如果mc失败，可以在本地开发环境编译好class文件，上传到目标系统，使用redefine热加载class</li>
<li>目前redefine 和watch/trace/jad/tt等命令冲突</li>
</ul>
<blockquote>
<p>注意：<code>redefine</code> 后的原来的类不能恢复，<code>redefine</code> 有可能失败（比如增加了新的 field），可参考 JDK 本身的文档。</p>
</blockquote>
<blockquote>
<p><code>reset</code> 命令对 <code>redefine</code> 的类无效。如果想重置，则需要 <code>redefine</code> 原始的字节码。</p>
</blockquote>
<blockquote>
<p><code>redefine</code> 命令 和 <code>jad</code> 、<code>watch</code>、<code>trace</code>、<code>monitor</code>、<code>tt</code> 等命令会冲突，执行完  <code>redefine</code> 之后，执行这个命令会把 <code>redefine</code> 的字节码重置。</p>
</blockquote>
<p><strong>redefine限制：</strong></p>
<ul>
<li>不允许新增加 <code>filed/method</code>。</li>
<li>正在运行的方法，一直没有退出不能生效。</li>
</ul>
<h3>4.3.7 dump</h3>
<blockquote>
<p><code>dump</code> 已加载类的 <code>bytecode</code> 到特定目录。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202205261412877.png" alt="image-20220526141258818"></p>
<h3>4.3.8 classloader</h3>
<blockquote>
<p>查看 classloader 的继承树、urls、类加载信息。</p>
</blockquote>
<p><code>classloader</code> 命令将 JVM 中所有的classloader的信息统计出来，并可以展示继承树，urls等。</p>
<p>可以让指定的 classloader 去 getResources，打印出所有查找到的resources的url。对于<code>ResourceNotFoundException</code>比较有用。</p>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>[l]</td>
<td>按类加载实例进行统计</td>
</tr>
<tr>
<td>[t]</td>
<td>打印所有 ClassLoader 的继承树</td>
</tr>
<tr>
<td>[a]</td>
<td>列出所有 ClassLoader 加载的类，请谨慎使用</td>
</tr>
<tr>
<td><code>[c:]</code></td>
<td>ClassLoader 的 hashcode</td>
</tr>
<tr>
<td><code>[classLoaderClass:]</code></td>
<td>指定执行表达式的 ClassLoader 的 class name</td>
</tr>
<tr>
<td><code>[c: r:]</code></td>
<td>用 ClassLoader 去查找 resource</td>
</tr>
<tr>
<td><code>[c: load:]</code></td>
<td>用 ClassLoader 去加载指定的类</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/202205261419112.png" alt="image-20220526141902960"></p>
<h2>4.4 monitor/watch/trace 相关</h2>
<h3>4.4.1 monitor</h3>
<blockquote>
<p>方法执行监控，非实时返回命令（输入命令后，一直等待目标 Java 进程返回信息，直到用户输入 <code>ctrl + c</code> 为止）。</p>
</blockquote>
<p><strong>监控的维度说明：</strong></p>
<table>
<thead>
<tr>
<th>监控项</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>timestamp</td>
<td>时间戳</td>
</tr>
<tr>
<td>class</td>
<td>Java类</td>
</tr>
<tr>
<td>method</td>
<td>方法（构造方法、普通方法）</td>
</tr>
<tr>
<td>total</td>
<td>调用次数</td>
</tr>
<tr>
<td>success</td>
<td>成功次数</td>
</tr>
<tr>
<td>fail</td>
<td>失败次数</td>
</tr>
<tr>
<td>rt</td>
<td>平均RT</td>
</tr>
<tr>
<td>fail-rate</td>
<td>失败率</td>
</tr>
</tbody>
</table>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>class-pattern</em></td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td><em>method-pattern</em></td>
<td>方法名表达式匹配</td>
</tr>
<tr>
<td><em>condition-express</em></td>
<td>条件表达式</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td><code>[c:]</code></td>
<td>统计周期，默认值为120秒</td>
</tr>
<tr>
<td>[b]</td>
<td>在<strong>方法调用之前</strong>计算condition-express</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/202205261432684.png" alt="image-20220526143229555"></p>
<h3>4.4.2 watch</h3>
<blockquote>
<p>方法执行数据观测。</p>
</blockquote>
<p>在诊断程序过程中，使用比较多的。检测方法的执行情况，入参、返回值、异常、出参等，并且可以使用ognl查看对应变量的值（因为支持ognl表达式，能够看到方法内部执行的情况）。</p>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>class-pattern</em></td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td><em>method-pattern</em></td>
<td>函数名表达式匹配</td>
</tr>
<tr>
<td><em>express</em></td>
<td>观察表达式，默认值：<code>{params, target, returnObj}</code></td>
</tr>
<tr>
<td><em>condition-express</em></td>
<td>条件表达式</td>
</tr>
<tr>
<td>[b]</td>
<td>在<strong>函数调用之前</strong>观察</td>
</tr>
<tr>
<td>[e]</td>
<td>在<strong>函数异常之后</strong>观察</td>
</tr>
<tr>
<td>[s]</td>
<td>在<strong>函数返回之后</strong>观察</td>
</tr>
<tr>
<td>[f]</td>
<td>在<strong>函数结束之后</strong>(正常返回和异常返回)观察</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td>[x:]</td>
<td>指定输出结果的属性遍历深度，默认为 1，最大值是4</td>
</tr>
</tbody>
</table>
<p>说明：</p>
<ul>
<li>watch 命令定义了4个观察事件点，即 <code>-b</code> 函数调用前，<code>-e</code> 函数异常后，<code>-s</code> 函数返回后，<code>-f</code> 函数结束后。</li>
<li>4个观察事件点 <code>-b</code>、<code>-e</code>、<code>-s</code> 默认关闭，<code>-f</code> 默认打开，当指定观察点被打开后，在相应事件点会对观察表达式进行求值并输出</li>
<li>这里要注意<code>函数入参</code>和<code>函数出参</code>的区别，有可能在中间被修改导致前后不一致，除了 <code>-b</code> 事件点 <code>params</code> 代表函数入参外，其余事件都代表函数出参。</li>
<li>当使用 <code>-b</code> 时，由于观察事件点是在函数调用前，此时返回值或异常均不存在</li>
<li>在watch命令的结果里，会打印出<code>location</code>信息。<code>location</code>有三种可能值：<code>AtEnter</code>，<code>AtExit</code>，<code>AtExceptionExit</code>。对应函数入口，函数正常return，函数抛出异常。</li>
</ul>
<p><strong>使用案例：</strong></p>
<p>1、监控方法出参和返回值，属性遍历深度为2</p>
<p><code>watch demo.MathGame primeFactors &quot;{params,returnObj}&quot; -x 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261457339.png" alt="image-20220526145719254"></p>
<p>2、观察方法的入参，因为观察点是执行前，所以只能看到入参，无法看到返回值</p>
<p><code>watch demo.MathGame primeFactors &quot;{params,returnObj}&quot; -x 2 -b</code></p>
<p><img src="https://img.dyzmj.top/img/202205261458697.png" alt="image-20220526145835607"></p>
<p>3、观察调用方法的对象属性，查看方法执行前后，当前对象中的属性，可以使用traget关键字，表示当前对象</p>
<p><code>watch demo.MathGame primeFactors &quot;target&quot; -x 2 -b</code></p>
<p><img src="https://img.dyzmj.top/img/202205261500179.png" alt="image-20220526150015069"></p>
<p>4、查看对象的属性</p>
<p><code>watch demo.MathGame primeFactors &quot;target.illegalArgumentCount&quot; -x 2 -b</code></p>
<p><img src="https://img.dyzmj.top/img/202205261501392.png" alt="image-20220526150129316"></p>
<p>5、方法调用前后，参数的不同，执行2次</p>
<p><code>watch demo.MathGame primeFactors &quot;{params, target, returnObj}&quot; -x 2 -b -s -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261503966.png" alt="image-20220526150304880"></p>
<p>6、查看第一个参数小于 0 的情况</p>
<p><code>watch demo.MathGame primeFactors &quot;{params[0],target}&quot; &quot;params[0]&lt;0&quot; -x 2 -b</code></p>
<p><img src="https://img.dyzmj.top/img/202205261504335.png" alt="image-20220526150440231"></p>
<h3>4.4.3 trace</h3>
<blockquote>
<p>方法内部调用路径，并输出方法路径上的每个节点耗时。</p>
</blockquote>
<p><code>trace</code> 命令能主动搜索 <code>class-pattern</code>／<code>method-pattern</code> 对应的方法调用路径，渲染和统计整个调用链路上的所有性能开销和追踪调用链路。</p>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>class-pattern</em></td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td><em>method-pattern</em></td>
<td>方法名表达式匹配</td>
</tr>
<tr>
<td><em>condition-express</em></td>
<td>条件表达式</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td><code>[n:]</code></td>
<td>命令执行次数</td>
</tr>
<tr>
<td><code>#cost</code></td>
<td>方法执行耗时</td>
</tr>
</tbody>
</table>
<p><strong>注意事项：</strong></p>
<ul>
<li><code>trace</code> 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷，但其每次只能跟踪一级方法的调用链路。</li>
<li>3.3.0 版本后，可以使用动态Trace功能，不断增加新的匹配类。</li>
<li>目前不支持 <code>trace java.lang.Thread getName</code>。</li>
</ul>
<p><strong>使用案例：</strong></p>
<p>1、查看某个方法的运行情况，只捕获2次的运行结果。</p>
<p><code>trace demo.MathGame run -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261509637.png" alt="image-20220526150920530"></p>
<p>2、跳过 JDK 执行的方法。</p>
<p><code>trace --skipJDKMethod false demo.MathGame run -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261510756.png" alt="image-20220526151042667"></p>
<p>3、对耗时进行筛选。</p>
<p><code>trace demo.MathGame run -n 2 '#cost &gt; 5'</code></p>
<p><img src="https://img.dyzmj.top/img/202205261513449.png" alt="image-20220526151351390"></p>
<h3>4.4.4 stack</h3>
<blockquote>
<p>输出当前方法被调用的调用路径。</p>
</blockquote>
<p>很多时候我们都知道一个方法被执行，但这个方法被执行的路径非常多，或者你根本就不知道这个方法是从那里被执行了，此时你需要的是 stack 命令。</p>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>class-pattern</em></td>
<td>类名表达式匹配</td>
</tr>
<tr>
<td><em>method-pattern</em></td>
<td>方法名表达式匹配</td>
</tr>
<tr>
<td><em>condition-express</em></td>
<td>条件表达式</td>
</tr>
<tr>
<td>[E]</td>
<td>开启正则表达式匹配，默认为通配符匹配</td>
</tr>
<tr>
<td><code>[n:]</code></td>
<td>执行次数限制</td>
</tr>
</tbody>
</table>
<p><strong>使用案例：</strong></p>
<p>1、查看方法的输出路径，2次。</p>
<p><code>stack demo.MathGame primeFactors -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261519739.png" alt="image-20220526151931640"></p>
<p>2、条件表达式，第 0 个参数小于 0。</p>
<p><code>stack demo.MathGame primeFactors &quot;params[0] &lt; 0&quot;  -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261520758.png" alt="image-20220526152052700"></p>
<p>3、耗时条件。</p>
<p><code>stack demo.MathGame primeFactors &quot;params[0] &lt; 0 and #cost &gt; 0.0005&quot;  -n 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261523354.png" alt="image-20220526152319281"></p>
<h3>4.4.5 tt</h3>
<blockquote>
<p>方法执行数据的时空隧道，记录下指定方法每次调用的入参和返回信息，并能对这些不同的时间下调用进行观测。</p>
</blockquote>
<p><code>watch</code> 虽然很方便和灵活，但需要提前想清楚观察表达式的拼写，这对排查问题而言要求太高，因为很多时候我们并不清楚问题出自于何方，只能靠蛛丝马迹进行猜测。</p>
<p>这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。</p>
<p>于是乎，<code>TimeTunnel</code> 命令就诞生了。</p>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>tt 的参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>-t</td>
<td>记录某个方法在一个时间段中的对调用</td>
</tr>
<tr>
<td>-l</td>
<td>显示所有已经记录的列表</td>
</tr>
<tr>
<td>-n 次数</td>
<td>只记录多少次</td>
</tr>
<tr>
<td>-s 表达式</td>
<td>搜索表达式</td>
</tr>
<tr>
<td>-i 索引号</td>
<td>查看指定索引号的详细调用信息</td>
</tr>
<tr>
<td>-p</td>
<td>重新调用指定的索引号时间碎片</td>
</tr>
</tbody>
</table>
<ul>
<li>
<p>支持条件表达式</p>
</li>
<li>
<p>解决方法重载</p>
<ul>
<li><code>tt -t *Test print params.length==1</code></li>
<li><code>tt -t *Test print 'params[1] instanceof Integer'</code></li>
</ul>
</li>
<li>
<p>​	指定参数值</p>
<ul>
<li><code>tt -t *Test print params[0].mobile==&quot;13989838402&quot;</code>、</li>
</ul>
</li>
</ul>
<p><strong>使用案例：</strong></p>
<p>1、调用某个方法的时间隧道。</p>
<p><code>tt -t demo.MathGame primeFactors</code></p>
<p><img src="https://img.dyzmj.top/img/202205261558323.png" alt="image-20220526155831225"></p>
<p>上图表格字段说明：</p>
<table>
<thead>
<tr>
<th>表格字段</th>
<th>字段解释</th>
</tr>
</thead>
<tbody>
<tr>
<td>INDEX</td>
<td>时间片段记录编号，每一个编号代表着一次调用，后续tt还有很多命令都是基于此编号指定记录操作，非常重要。</td>
</tr>
<tr>
<td>TIMESTAMP</td>
<td>方法执行的本机时间，记录了这个时间片段所发生的本机时间</td>
</tr>
<tr>
<td>COST(ms)</td>
<td>方法执行的耗时</td>
</tr>
<tr>
<td>IS-RET</td>
<td>方法是否以正常返回的形式结束</td>
</tr>
<tr>
<td>IS-EXP</td>
<td>方法是否以抛异常的形式结束</td>
</tr>
<tr>
<td>OBJECT</td>
<td>执行对象的<code>hashCode()</code>，注意，曾经有人误认为是对象在JVM中的内存地址，但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体</td>
</tr>
<tr>
<td>CLASS</td>
<td>执行的类名</td>
</tr>
<tr>
<td>METHOD</td>
<td>执行的方法名</td>
</tr>
</tbody>
</table>
<p>2、检索时间片段，显示之前记录的全部时间隧道</p>
<p><code>tt -l</code></p>
<p><img src="https://img.dyzmj.top/img/202205261600763.png" alt="image-20220526160033686"></p>
<p><code>tt -s  'method.name==&quot;primeFactors&quot;'</code></p>
<p><img src="https://img.dyzmj.top/img/202205261602092.png" alt="image-20220526160201007"></p>
<p><code>tt -i 1007</code> 查看索引为某次的调用</p>
<p><img src="https://img.dyzmj.top/img/202205261602138.png" alt="image-20220526160234054"></p>
<p><code>tt -i 1007 --replay-times 3 --replay-interval 2</code> 重新调用</p>
<p><img src="https://img.dyzmj.top/img/202205261604942.png" alt="image-20220526160419866"></p>
<p><strong>需要强调的点：</strong></p>
<ol>
<li><strong>ThreadLocal 信息丢失</strong></li>
</ol>
<p>很多框架偷偷的将一些环境变量信息塞到了发起调用线程的 ThreadLocal 中，由于调用线程发生了变化，这些 ThreadLocal 线程信息无法通过 Arthas 保存，所以这些信息将会丢失。</p>
<ol start="2">
<li>引用的对象</li>
</ol>
<p>需要强调的是，<code>tt</code> 命令是将当前环境的对象引用保存起来，但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更，或者返回的对象经过了后续的处理，那么在 <code>tt</code> 查看的时候将无法看到当时最准确的值。这也是为什么 <code>watch</code> 命令存在的意义。</p>
<h2>4.5 profiler 火焰图</h2>
<blockquote>
<p>使用 <code>async-profiler</code> 生成火焰图。</p>
</blockquote>
<p><strong>参数说明：</strong></p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>action</em></td>
<td>要执行的操作</td>
</tr>
<tr>
<td><em>actionArg</em></td>
<td>属性名模式</td>
</tr>
<tr>
<td>[i:]</td>
<td>采样间隔（单位：ns）（默认值：10'000'000，即10 ms）</td>
</tr>
<tr>
<td>[f:]</td>
<td>将输出转储到指定路径</td>
</tr>
<tr>
<td>[d:]</td>
<td>运行评测指定秒</td>
</tr>
<tr>
<td>[e:]</td>
<td>要跟踪哪个事件（cpu, alloc, lock, cache-misses等），默认是cpu</td>
</tr>
</tbody>
</table>
<p>生成火焰图的操作命令：</p>
<table>
<thead>
<tr>
<th>操作命令</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>profiler start</code></td>
<td>启动 profiler，默认生成 CPU 的火焰图</td>
</tr>
<tr>
<td><code>profiler list</code></td>
<td>显示所有支持的事件</td>
</tr>
<tr>
<td><code>profiler getSamples</code></td>
<td>获取已采集的 sample 数量</td>
</tr>
<tr>
<td><code>profiler status</code></td>
<td>查看 profiler 的状态，运行的时间</td>
</tr>
<tr>
<td><code>profiler stop</code></td>
<td>停止 profiler ，生成火焰图的结果，指定输出目录和输出格式，svg或html</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/202205261653912.png" alt="image-20220526165320776"></p>
<p>火焰图的查看方法参考：</p>
<ul>
<li>
<p><a href="https://www.jianshu.com/p/6b62f65fedde"><!-- raw HTML omitted -->【性能优化】火焰图实战<!-- raw HTML omitted --></a></p>
</li>
<li>
<p><a href="https://www.jianshu.com/p/ce292632a8ef"><!-- raw HTML omitted -->JVM那点事—火焰图（flame graph）性能分析<!-- raw HTML omitted --></a></p>
</li>
</ul>
<h1>五、使用案例</h1>
<h2>5.1 确定哪个 Controller 处理了请求</h2>
<pre><code class="language-arthas">trace org.springframework.web.servlet.DispatcherServlet *
</code></pre>
<p><img src="https://img.dyzmj.top/img/202205261705439.png" alt="image-20220526170544324"></p>
<p><code>jad org.springframework.web.servlet.DispatcherServlet doDispatch</code> 可以查看到方法 <code>getHandler</code></p>
<p><img src="https://img.dyzmj.top/img/202205261709649.png" alt="image-20220526170944455"></p>
<p><code>watch  org.springframework.web.servlet.DispatcherServlet getHandler '{params, returnObj}' -x 2</code></p>
<p><img src="https://img.dyzmj.top/img/202205261717345.png" alt="image-20220526171706171"></p>
<h2>5.2 指定方法调用的参数和返回值是多少</h2>
<p><code>watch com.goldcard.nbiot.web.home.controller.device.DeviceController replaceDevice '{params,returnObj,throwExp}'  -n 5  -x 3 </code></p>
<p><img src="https://img.dyzmj.top/img/202205261719560.png" alt="image-20220526171923434"></p>
<h2>5.3 热更新代码</h2>
<p>1、<code>jad</code> 命令，将需要更改的文件先进行翻编译，保存下来，编译器修改
<code>jad --source-only  com.example.DemoApplication  &gt; /data/DemoApplication.java</code></p>
<p>2、<code>sc</code> 命令，查找当前类是哪个 classLoader 加载的</p>
<p><code>sc -d *DempApplication | grep classLoader</code></p>
<p>3、<code>mc</code> 命令，用指定的classLoader重新将类在内存中编译，若此步错误，可以使用 IDE 编译，然后将编译好的 <code>.class</code> 文件上传至服务器。</p>
<p><code>mc -c 20ad9418  /data/DemoApplication.java  -d /data</code></p>
<p>4、<code>retransform</code> 命令，将编译后的类加载到JVM上（这里推荐使用 <code>retransform</code> 命令而不是 <code>redefine</code>命令，<code>redefine</code>命令会将原 <code>.class</code> 文件覆盖且不可还原，而且与 <code>watch</code>、<code>trace</code>命令也有冲突）。</p>
<p><code>retransform /data/com/example/DemoApplication.class</code></p>
<h2>5.4 使用 IDEA 插件</h2>
<p>推荐使用 IDEA 中的 《arthas idea》插件，选择类或方法，点击邮件即可打开：</p>
<p><img src="https://img.dyzmj.top/img/202205261734744.png" alt="image-20220526173440620"></p>
<h2>5.5 更多案例</h2>
<ul>
<li><a href="https://github.com/alibaba/arthas/issues?q=label%3Auser-case"><!-- raw HTML omitted -->更多使用案例<!-- raw HTML omitted --></a></li>
</ul>
<h1>六、附录</h1>
<h2>6.1 arthas 命令速查手册</h2>
<p><img src="https://img.dyzmj.top/img/202205270833679.png" alt="image-20220526142218804"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-13</guid>
      <pubDate>Mon, 27 Apr 2026 11:31:04 +0000</pubDate>
    </item>
    <item>
      <title>Java Debug 原理与实践</title>
      <link>https://dyzmj.top/posts/debug</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/debug">https://dyzmj.top/posts/debug</a></p></blockquote><h1>Java Debug 原理与实践</h1>
<h1>一、JPDA 体系概览</h1>
<h2>1、<strong><strong>JPDA 组成模块</strong></strong></h2>
<p>JPDA 定义了一个完整独立的体系，它由三个相对独立的层次共同组成，而且规定了它们三者之间的交互方式，或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口（JVMTI），Java 调试线协议（JDWP）以及 Java 调试接口（JDI）。这三个模块把调试过程分解成几个很自然的概念：调试者（debugger）和被调试者（debuggee），以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上，它可以通过 JVMTI 这个标准接口，监控当前虚拟机的信息；调试者定义了用户可使用的调试接口，通过这些接口，用户可以对被调试虚拟机发送调试命令，同时调试者接受并显示调试结果。在调试者和被调试着之间，调试命令和调试结果，都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包，通过传输层发送给被调试者，被调试者接收到 JDWP 命令包后，解析这个命令并转化为 JVMTI 的调用，在被调试者上运行。类似的，JVMTI 的运行结果，被格式化成 JDWP 数据包，发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据，发出指令。下图展示了这个过程：</p>
<p><img src="https://img.dyzmj.top/img/202305310856361.png" alt="Untitled"></p>
<h2>2、<strong><strong>Java 虚拟机工具接口（JVMTI）</strong></strong></h2>
<p>JVMTI（Java Virtual Machine Tool Interface）即指 Java 虚拟机工具接口，它是一套由虚拟机直接提供的 native 接口，它处于整个 JPDA 体系的最底层，所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口，开发人员不仅调试在该虚拟机上运行的 Java 程序，还能查看它们运行的状态，设置回调函数，控制某些环境变量，从而优化程序性能。我们知道，JVMTI 的前身是 JVMDI 和 JVMPI，它们原来分别被用于提供调试 Java 程序以及 Java 程序调节性能的功能。在 J2SE 5.0 之后 JDK 取代了 JVMDI 和 JVMPI 这两套接口，JVMDI 在最新的 Java SE 6 中已经不提供支持，而 JVMPI 也计划在 Java SE 7 后被彻底取代。</p>
<h2>3、<strong><strong>Java 调试线协议（JDWP）</strong></strong></h2>
<p>JDWP（Java Debug Wire Protocol）是一个为 Java 调试而设计的一个通讯交互协议，它定义了调试器和被调试程序之间传递的信息的格式。在 JPDA 体系中，作为前端（front-end）的调试者（debugger）进程和后端（back-end）的被调试程序（debuggee）进程之间的交互数据的格式就是由 JDWP 来描述的，它详细完整地定义了请求命令、回应数据和错误代码，保证了前端和后端的 JVMTI 和 JDI 的通信通畅。比如在 Sun 公司提供的实现中，它提供了一个名为 jdwp.dll（<a href="http://jdwp.so/">jdwp.so</a>）的动态链接库文件，这个动态库文件实现了一个 Agent，它会负责解析前端发出的请求或者命令，并将其转化为 JVMTI 调用，然后将 JVMTI 函数的返回值封装成 JDWP 数据发还给后端。</p>
<p>另外，这里需要注意的是 JDWP 本身并不包括传输层的实现，传输层需要独立实现，但是 JDWP 包括了和传输层交互的严格的定义，就是说，JDWP 协议虽然不规定我们是通过 EMS 还是快递运送货物的，但是它规定了我们传送的货物的摆放的方式。在 Sun 公司提供的 JDK 中，在传输层上，它提供了 socket 方式，以及在 Windows 上的 shared memory 方式。当然，传输层本身无非就是本机内进程间通信方式和远端通信方式，用户有兴趣也可以按 JDWP 的标准自己实现。</p>
<h2>4、<strong><strong>Java 调试接口（JDI）</strong></strong></h2>
<p>JDI（Java Debug Interface）是三个模块中最高层的接口，在多数的 JDK 中，它是由 Java 语言实现的。 JDI 由针对前端定义的接口组成，通过它，调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行，JDI 不仅能帮助开发人员格式化 JDWP 数据，而且还能为 JDWP 数据传输提供队列、缓存等优化服务。从理论上说，开发人员只需使用 JDWP 和 JVMTI 即可支持跨平台的远程调试，但是直接编写 JDWP 程序费时费力，而且效率不高。因此基于 Java 的 JDI 层的引入，简化了操作，提高了开发人员开发调试程序的效率。</p>
<table>
<thead>
<tr>
<th>模块</th>
<th>层次</th>
<th>编程语言</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>JVMTI</td>
<td>底层</td>
<td>C</td>
<td>获取及控制当前虚拟机状态</td>
</tr>
<tr>
<td>JDWP</td>
<td>中介层</td>
<td>C</td>
<td>定义 JVMTI 和 JDI 交互的数据格式</td>
</tr>
<tr>
<td>JDI</td>
<td>高层</td>
<td>Java</td>
<td>提供 Java API 来远程控制被调试虚拟机</td>
</tr>
</tbody>
</table>
<h2>5、<strong><strong>JPDA 实现</strong></strong></h2>
<blockquote>
<p>关于 Apache Harmony 项目</p>
<p><a href="https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fharmony.apache.org%2F">Apache Harmony</a> 旨在开发出一个独立且与现有 JDK 兼容的 Java SE 实现，它以 Apache 软件许可证 2.0 版发行。它建立了一个开放的模块化运行时架构，包括虚拟机和类库之间及其内部的模块化，通过这个平台，社区能在已有实现的基础上自由定制自己的 Java 实现，或者对某个模块单独进行创新。</p>
</blockquote>
<p>每一个虚拟机都应该实现 JVMTI 接口，但是 JDWP 和 JDI 本身与虚拟机并非是不可分的，这三个层之间是通过标准所定义的交互的接口和协议联系起来的，因此它们可以被独立替换或取代，但不会影响到整体调试工具的开发和使用。因此，开发和使用自己的 JDWP 和 JDI 接口实现是可能的。</p>
<p>Java 软件开发包（SDK）标准版里提供了 JPDA 三个层次的标准实现，事实上，调试工具开发人员还有很多其他开源实现可以选择，比如 Apache Harmony 提供了 JDWP 的实现。而 JDI，我们可以在 Eclipse 一个子项目 org.eclipse.jdt.debug 里找到其完整的实现（Harmony 也使用了这套实现，作为其 J2SE 类库的一部分）。通过标准协议，Eclipse IDE 的调试工具就可以完全在 Harmony 的环境上运行。</p>
<h2>6、<strong><strong>Java 调试接口的特点</strong></strong></h2>
<p>Java 语言是第一个使用虚拟机概念的流行的编程语言，正是因为虚拟机的存在，使很多事情变得简单而轻松，掌握了虚拟机，就掌握了内存分配、线程管理、即时优化等等运行态。同样的，Java 调试的本质，就是和虚拟机打交道，通过操作虚拟机来达到观察调试我们自己代码的目的。这个特点决定了 Java 调试接口和以前其他编程语言的巨大区别。</p>
<p>以 C/C++ 的调试为例，目前比较流行的调试工具是 GDB 和微软的 Visual Studio 自带的 debugger，在这种 debugger 中，首先，我们必须编译一个 “debug” 模式的程序，这个会比实际的 release 模式程序大很多。其次，在调试过程中，debugger 将会深层接入程序的运行，掌握和控制运行态的一些信息，并将这些信息及时返回。这种介入对运行的效率和内存占用都有一定的需求。基于这些需求，这些 Debugger 本身事实上是提供了，或者说，创建和管理了一个运行态，因此他们的程序算法比较复杂，个头都比较大。对于远端的调试，GDB 也没有很好的默认实现，当然，C/C++ 在这方面也没有特别大的需求。</p>
<p>而 Java 则不同，由于 Java 的运行态已经被虚拟机所很好地管理，因此作为 Java 的 Debugger 无需再自己创造一个可控的运行态，而仅仅需要去操作虚拟机就可以了。 Java 的 JPDA 就是一套为调试和优化服务的虚拟机的操作工具，其中，JVMTI 是整合在虚拟机中的接口，JDWP 是一个通讯层，而 JDI 是前端为开发人员准备好的工具和运行库。</p>
<p>从构架上说，我们可以把 JPDA 看作成是一个 C/S 体系结构的应用，在这个构架下，我们可以方便地通过网络，在任意的地点调试另外一个虚拟机上的程序，这个就很好地解决了部署和测试的问题，尤其满足解决了很多网络时代中的开发应用的需求。前端和后端的分离，也方便用户开发适合于自己的调试工具。</p>
<p>从效率上看，由于 Java 程序本身就是编译成字节码，运行在虚拟机上的，因此调试前后的程序、内存占用都不会有大变化（仅仅是启动一个 JDWP 所需要的内存），任意程度都可以很好地调试，非常方便。而 JPDA 构架下的几个组成部分，JDWP 和 JDI 都比较小，主要的工作可以让虚拟机自己完成。</p>
<p>从灵活性上，Java 调试工具是建立在强大的虚拟机上的，因此，很多前沿的应用，比如动态编译运行，字节码的实时替换等等，都可以通过对虚拟机的改进而得到实现。随着虚拟机技术的逐步发展和深入，各种不同种类，不同应用领域中虚拟机的出现，各种强大的功能的加入，给我们的调试工具也带来很多新的应用。</p>
<p>总而言之，一个先天的，可控的运行态给 Java 的调试工作，给 Java 调试接口带来了极大的优势和便利。通过 JPDA 这个标准，我们可以从虚拟机中得到我们所需要的信息，完成我们所希望的操作，更好地开发我们的程序。</p>
<hr>
<h1>二、JVMTI和Agent实现</h1>
<h2>1、<strong><strong>Java 程序的诊断和调试</strong></strong></h2>
<p>开发人员对 Java 程序的诊断和调试有许多不同种类、不同层次的需求，这就使得开发人员需要使用不同的工具来解决问题。比如，在 Java 程序运行的过程中，程序员希望掌握它总体的运行状况，这个时候程序员可以直接使用 JDK 提供的 jconsole 程序。如果希望提高程序的执行效率，开发人员可以使用各种 Java Profiler。这种类型的工具非常多，各有优点，能够帮助开发人员找到程序的瓶颈，从而提高程序的运行速度。开发人员还会遇到一些与内存相关的问题，比如内存占用过多，大量内存不能得到释放，甚至导致内存溢出错误（OutOfMemoryError）等等，这时可以把当前的内存输出到 Dump 文件，再使用堆分析器或者 Dump 文件分析器等工具进行研究，查看当前运行态堆（Heap）中存在的实例整体状况来诊断问题。所有这些工具都有一个共同的特点，就是最终他们都需要通过和虚拟机进行交互，来发现 Java 程序运行的问题。</p>
<p>已有的这些工具虽然强大易用，但是在一些高级的应用环境中，开发者常常会有一些特殊的需求，这个时候就需要定制工具来达成目标。 JDK 本身定义了目标明确并功能完善的 API 来与虚拟机直接交互，而且这些 API 能很方便的进行扩展，从而满足开发者各式的需求。在本文中，将比较详细地介绍 JVMTI，以及如何使用 JVMTI 编写一个定制的 Agent 。</p>
<blockquote>
<p>Agent</p>
<p>Agent 即 JVMTI 的客户端，它和执行 Java 程序的虚拟机运行在同一个进程上，因此通常他们的实现都很紧凑，他们通常由另一个独立的进程控制，充当这个独立进程和当前虚拟机之间的中介，通过调用 JVMTI 提供的接口和虚拟机交互，负责获取并返回当前虚拟机的状态或者转发控制命令。</p>
</blockquote>
<h2>2、<strong><strong>JVMTI 的简介</strong></strong></h2>
<p>JVMTI（JVM Tool Interface）是 Java 虚拟机所提供的 native 编程接口，是 JVMPI（Java Virtual Machine Profiler Interface）和 JVMDI（Java Virtual Machine Debug Interface）的更新版本。从这个 API 的发展历史轨迹中我们就可以知道，JVMTI 提供了可用于 debug 和 profiler 的接口；同时，在 Java 5/6 中，虚拟机接口也增加了监听（Monitoring），线程分析（Thread analysis）以及覆盖率分析（Coverage Analysis）等功能。正是由于 JVMTI 的强大功能，它是实现 Java 调试器，以及其它 Java 运行态测试与分析工具的基础。</p>
<p>JVMTI 并不一定在所有的 Java 虚拟机上都有实现，不同的虚拟机的实现也不尽相同。不过在一些主流的虚拟机中，比如 Sun 和 IBM，以及一些开源的如 Apache Harmony DRLVM 中，都提供了标准 JVMTI 实现。</p>
<p>JVMTI 是一套本地代码接口，因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上，开发时一般采用建立一个 Agent 的方式来使用 JVMTI，它使用 JVMTI 函数，设置一些回调函数，并从 Java 虚拟机中得到当前的运行态信息，并作出自己的判断，最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后，我们就可以在 Java 程序启动的时候来加载它（启动加载模式），也可以在 Java 5 之后使用运行时加载（活动加载模式）。</p>
<ul>
<li>agentlib:agent-lib-name=options</li>
<li>agentpath:path-to-agent=options</li>
</ul>
<h2>3、<strong><strong>Agent 的工作过程</strong></strong></h2>
<h3>3.1 启动</h3>
<p>Agent 是在 Java 虚拟机启动之时加载的，这个加载处于虚拟机初始化的早期，在这个时间点上：</p>
<ul>
<li>所有的 Java 类都未被初始化；</li>
<li>所有的对象实例都未被创建；</li>
<li>因而，没有任何 Java 代码被执行；</li>
</ul>
<p>但在这个时候，我们已经可以：</p>
<ul>
<li>操作 JVMTI 的 Capability 参数；</li>
<li>使用系统参数；</li>
</ul>
<p>动态库被加载之后，虚拟机会先寻找一个 Agent 入口函数：</p>
<pre><code class="language-c">JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
</code></pre>
<p>在这个函数中，虚拟机传入了一个 JavaVM 指针，以及命令行的参数。通过 JavaVM，我们可以获得 JVMTI 的指针，并获得 JVMTI 函数的使用能力，所有的 JVMTI 函数都通过这个 jvmtiEnv 获取，不同的虚拟机实现提供的函数细节可能不一样，但是使用的方式是统一的。</p>
<pre><code class="language-c">jvmtiEnv *jvmti; 
(*jvm)-&gt;GetEnv(jvm, &amp;jvmti, JVMTI_VERSION_1_0);
</code></pre>
<p>这里传入的版本信息参数很重要，不同的 JVMTI 环境所提供的功能以及处理方式都可能有所不同，不过它在同一个虚拟机中会保持不变（有心的读者可以去比较一下 JNI 环境）。命令行参数事实上就是上面启动命令行中的 options 部分，在 Agent 实现中需要进行解析并完成后续处理工作。参数传入的字符串仅仅在 Agent_OnLoad 函数里有效，如果需要长期使用，开发者需要做内存的复制工作，同时在最后还要释放这块存储。另外，有些 JDK 的实现会使用 JAVA_TOOL_OPTIONS 所提供的参数，这个常见于一些嵌入式的 Java 虚拟机（不使用命令行）。需要强调的是，这个时候由于虚拟机并未完成初始化工作，并不是所有的 JVMTI 函数都可以被使用。</p>
<p>Agent 还可以在运行时加载，如果您了解 Java Instrument 模块（可以参考<a href="https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-jse61%2F">这篇文章</a>
），您一定对它的运行态加载有印象，这个新功能事实上也是 Java Agent 的一个实现。具体说来，虚拟机会在运行时监听并接受 Agent 的加载，在这个时候，它会使用 Agent 的：</p>
<pre><code class="language-c">JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);
</code></pre>
<p>同样的在这个初始化阶段，不是所有的 JVMTI 的 Capability 参数都处于可操作状态，而且 options 这个 char 数组在这个函数运行之后就会被丢弃，如果需要，需要做好保留工作。</p>
<p>Agent 的主要功能是通过一系列的在虚拟机上设置的回调（callback）函数完成的，一旦某些事件发生，Agent 所设置的回调函数就会被调用，来完成特定的需求。</p>
<h3>3.2 卸载</h3>
<p>最后，Agent 完成任务，或者虚拟机关闭的时候，虚拟机都会调用一个类似于类析构函数的方法来完成最后的清理任务，注意这个函数和虚拟机自己的 VM_DEATH 事件是不同的。</p>
<pre><code class="language-c">JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
</code></pre>
<h2>4、<strong><strong>JVMTI 的环境和错误处理</strong></strong></h2>
<p>我们使用 JVMTI 的过程，主要是设置 JVMTI 环境，监听虚拟机所产生的事件，以及在某些事件上加上我们所希望的回调函数。</p>
<h3>4.1 <strong><strong>JVMTI 环境</strong></strong></h3>
<p>我们可以通过操作 jvmtiCapabilities 来查询、增加、修改 JVMTI 的环境参数。当然，对于每一个不同的虚拟机来说，基于他们的实现不尽相同，导致了 JVMTI 的环境也不一定一致。标准的 jvmtiCapabilities 定义了一系列虚拟机的功能，比如 can_redefine_any_class 定义了虚拟机是否支持重定义类，can_retransform_classes 定义了是否支持在运行的时候改变类定义等等。如果熟悉 Java Instrumentation，一定不会对此感到陌生，因为 Instrumentation 就是对这些在 Java 层上的包装。对用户来说，这块最主要的是查看当前 JVMTI 环境，了解虚拟机具有的功能。要了解这个，其实很简单，只需通过对 jvmtiCapabilities 的一系列变量的考察就可以。</p>
<pre><code class="language-c">err = (*jvmti)-&gt;GetCapabilities(jvmti, &amp;capa); // 取得 jvmtiCapabilities 指针。
if (err == JVMTI_ERROR_NONE) { 
    if (capa.can_redefine_any_class) { ... } 
} // 查看是否支持重定义类
</code></pre>
<p>另外，虚拟机有自己的一些功能，一开始并未被启动，那么增加或修改 jvmtiCapabilities 也是可能的，但不同的虚拟机对这个功能的处理也不太一样，多数的虚拟机允许增改，但是有一定的限制，比如仅支持在 Agent_OnLoad 时，即虚拟机启动时作出，它某种程度上反映了虚拟机本身的构架。开发人员无需要考虑 Agent 的性能和内存占用，就可以在 Agent 被加载的时候启用所有功能：</p>
<pre><code class="language-c">err = (*jvmti)-&gt;GetPotentialCapabilities(jvmti, &amp;capa); // 取得所有可用的功能
if (err == JVMTI_ERROR_NONE) { 
    err = (*jvmti)-&gt;AddCapabilities(jvmti, &amp;capa); 
    ... 
}
</code></pre>
<p>最后我们要注意的是，JVMTI 的函数调用都有其时间性，即特定的函数只能在特定的虚拟机状态下才能调用，比如 SuspendThread（挂起线程）这个动作，仅在 Java 虚拟机处于运行状态（live phase）才能调用，否则导致一个内部异常。</p>
<h3>4.2 <strong><strong>JVMTI 错误处理</strong></strong></h3>
<p>JVMTI 沿用了基本的错误处理方式，即使用返回的错误代码通知当前的错误，几乎所有的 JVMTI 函数调用都具有以下模式：</p>
<pre><code class="language-c">jvmtiError err = jvmti-&gt;someJVMTImethod (somePara … );
</code></pre>
<p>其中 err 就是返回的错误代码，不同函数的错误信息可以在 Java 规范里查到。</p>
<h2>5、<strong><strong>JVMTI 基本功能</strong></strong></h2>
<p>JVMTI 的功能非常丰富，包含了虚拟机中线程、内存 / 堆 / 栈，类 / 方法 / 变量，事件 / 定时器处理等等 20 多类功能，下面我们介绍一下，并举一些简单列子。</p>
<h3>5.1 <strong><strong>事件处理和回调函数</strong></strong></h3>
<p>从上文我们知道，使用 JVMTI 一个基本的方式就是设置回调函数，在某些事件发生的时候触发并作出相应的动作。因此这一部分的功能非常基本，当前版本的 JVMTI 提供了许多事件（Event）的回调，包括虚拟机初始化、开始运行、结束，类的加载，方法出入，线程始末等等。如果想对这些事件进行处理，我们需要首先为该事件写一个函数，然后在 jvmtiEventCallbacks 这个结构中指定相应的函数指针。比如，我们对线程启动感兴趣，并写了一个 HandleThreadStart 函数，那么我们需要在 Agent_OnLoad 函数里加入：</p>
<pre><code class="language-c">jvmtiEventCallbacks eventCallBacks; 
memset(&amp;ecbs, 0, sizeof(ecbs)); // 初始化
eventCallBacks.ThreadStart = &amp;HandleThreadStart; // 设置函数指针
...
</code></pre>
<p>在设置了这些回调之后，就可以调用下述方法，来最终完成设置。在接下来的虚拟机运行过程中，一旦有线程开始运行发生，虚拟机就会回调 HandleThreadStart 方法。</p>
<pre><code class="language-c">jvmti-&gt;SetEventCallbacks(eventCallBacks, sizeof(eventCallBacks));
</code></pre>
<p>设置回调函数的时候，开发者需要注意以下几点：</p>
<ul>
<li>如同 Java 异常机制一样，如果在回调函数中自己抛出一个异常（Exception），或者在调用 JNI 函数的时候制造了一些麻烦，让 JNI 丢出了一个异常，那么任何在回调之前发生的异常就会丢失，这就要求开发人员要在处理错误的时候需要当心。</li>
<li>虚拟机不保证回调函数会被同步，换句话说，程序有可能同时运行同一个回调函数（比如，好几个线程同时开始运行了，这个 HandleThreadStart 就会被同时调用几次），那么开发人员在开发回调函数时需要处理同步的问题。</li>
</ul>
<h3>5.2 <strong><strong>内存控制和对象获取</strong></strong></h3>
<p>内存控制是一切运行态的基本功能。 JVMTI 除了提供最简单的内存申请和撤销之外（这块内存不受 Java 堆管理，开发人员需要自行进行清理工作，不然会造成内存泄漏），也提供了对 Java 堆的操作。众所周知，Java 堆中存储了 Java 的类、对象和基本类型（Primitive），通过对堆的操作，开发人员可以很容易的查找任意的类、对象，甚至可以强行执行垃圾收集工作。 JVMTI 中对 Java 堆的操作与众不同，它没有提供一个直接获取的方式（由此可见，虚拟机对对象的管理并非是哈希表，而是某种树 / 图方式），而是使用一个迭代器（iterater）的方式遍历：</p>
<pre><code class="language-c">jvmtiError FollowReferences(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    jobject initial_object,// 该方式可以指定根节点
    const jvmtiHeapCallbacks* callbacks,// 设置回调函数
    const void* user_data)
</code></pre>
<p>或者</p>
<pre><code class="language-c">jvmtiError IterateThroughHeap(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    const jvmtiHeapCallbacks* callbacks, 
    const void* user_data)// 遍历整个 heap
</code></pre>
<p>在遍历的过程中，开发者可以设定一定的条件，比如，指定是某一个类的对象，并设置一个回调函数，如果条件被满足，回调函数就会被执行。开发者可以在回调函数中对当前传回的指针进行打标记（tag）操作 —— 这又是一个特殊之处，在第一遍遍历中，只能对满足条件的对象进行 tag ；然后再使用 GetObjectsWithTags 函数，获取需要的对象。</p>
<pre><code class="language-c">jvmtiError GetObjectsWithTags(jvmtiEnv* env, 
    jint tag_count, 
    const jlong* tags, // 设定特定的 tag，即我们上面所设置的
    jint* count_ptr, 
    jobject** object_result_ptr, 
    jlong** tag_result_ptr)
</code></pre>
<p>如果你仅仅想对特定 Java 对象操作，应该避免设置其他类型的回调函数，否则会影响效率，举例来说，多增加一个 primitive 的回调函数，可能会使整个操作效率下降一个数量级。</p>
<h3>5.3 <strong><strong>线程和锁</strong></strong></h3>
<p>线程是 Java 运行态中非常重要的一个部分，在 JVMTI 中也提供了很多 API 进行相应的操作，包括查询当前线程状态，暂停，恢复或者终端线程，还可以对线程锁进行操作。开发者可以获得特定线程所拥有的锁：</p>
<pre><code class="language-c">jvmtiError GetOwnedMonitorInfo(jvmtiEnv* env, 
    jthread thread, 
    jint* owned_monitor_count_ptr, 
    jobject** owned_monitors_ptr)
</code></pre>
<p>也可以获得当前线程正在等待的锁：</p>
<pre><code class="language-c">jvmtiError GetCurrentContendedMonitor(jvmtiEnv* env, 
    jthread thread, 
    jobject* monitor_ptr)
</code></pre>
<p>知道这些信息，事实上我们也可以设计自己的算法来判断是否死锁。更重要的是，JVMTI 提供了一系列的监视器（Monitor）操作，来帮助我们在 native 环境中实现同步。主要的操作是构建监视器（CreateRawMonitor），获取监视器（RawMonitorEnter），释放监视器（RawMonitorExit），等待和唤醒监视器 (RawMonitorWait,RawMonitorNotify) 等操作，通过这些简单锁，程序的同步操作可以得到保证。</p>
<h3>5.4 <strong><strong>调试功能</strong></strong></h3>
<p>调试功能是 JVMTI 的基本功能之一，这主要包括了设置断点、调试（step）等，在 JVMTI 里面，设置断点的 API 本身很简单：</p>
<pre><code class="language-c">jvmtiError SetBreakpoint(jvmtiEnv* env, 
    jmethodID method, 
    jlocation location)
</code></pre>
<p>jlocation 这个数据结构在这里代表的是对应方法方法中一个可执行代码的行数。在断点发生的时候，虚拟机会触发一个事件，开发者可以使用在上文中介绍过的方式对事件进行处理。</p>
<h3>6、<strong><strong>JVMTI 数据结构</strong></strong></h3>
<p>JVMTI 中使用的数据结构，首先也是一些标准的 JNI 数据结构，比如 jint，jlong ；其次，JVMTI 也定义了一些基本类型，比如 jthread，表示一个 thread，jvmtiEvent，表示 jvmti 所定义的事件；更复杂的有 JVMTI 的一些需要用结构体表示的数据结构，比如堆的信息（jvmtiStackInfo）。这些数据结构在文档中都有清楚的定义，本文就不再详细解释。</p>
<h2>7、<strong><strong>一个简单的 Agent 实现</strong></strong></h2>
<p>下面将通过一个具体的例子，来阐述如何开发一个简单的 Agent 。这个 Agent 是通过 C++ 编写的（读者可以在最后下载到完整的代码），他通过监听 JVMTI_EVENT_METHOD_ENTRY 事件，注册对应的回调函数来响应这个事件，来输出所有被调用函数名。有兴趣的读者还可以参照这个基本流程，通过 JVMTI 提供的丰富的函数来进行扩展和定制。</p>
<h3>7.1 <strong><strong>Agent 的设计</strong></strong></h3>
<p>具体实现都在 MethodTraceAgent 这个类里提供。按照顺序，他会处理环境初始化、参数解析、注册功能、注册事件响应，每个功能都被抽象在一个具体的函数里。</p>
<pre><code class="language-c">class MethodTraceAgent 
{ 
    public: 
        void Init(JavaVM *vm) const throw(AgentException); 
        void ParseOptions(const char* str) const throw(AgentException); 
        void AddCapability() const throw(AgentException); 
        void RegisterEvent() const throw(AgentException); 
        ... 
     
    private: 
        ... 
        static jvmtiEnv * m_jvmti; 
        static char* m_filter; 
 };
</code></pre>
<p>Agent_OnLoad 函数会在 Agent 被加载的时候创建这个类，并依次调用上述各个方法，从而实现这个 Agent 的功能。</p>
<pre><code class="language-c">JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) 
{ 
    ... 
    MethodTraceAgent* agent = new MethodTraceAgent(); 
    agent-&gt;Init(vm); 
    agent-&gt;ParseOptions(options); 
    agent-&gt;AddCapability(); 
    agent-&gt;RegisterEvent(); 
    ... 
}
</code></pre>
<p>运行过程如下图所示：</p>
<p><img src="https://img.dyzmj.top/img/202305310857411.png" alt="Untitled"></p>
<h3>7.2 <strong><strong>Agent 编译和运行</strong></strong></h3>
<p>Agent 的编译非常简单，他和编译普通的动态链接库没有本质区别，只是需要将 JDK 提供的一些头文件包含进来。</p>
<p>Windows:</p>
<pre><code class="language-bash">cl /EHsc -I${JAVA_HOME}\include\ -I${JAVA_HOME}\include\win32 
-LD MethodTraceAgent.cpp Main.cpp -FeAgent.dll
</code></pre>
<p>Linux:</p>
<pre><code class="language-bash">g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux 
MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libagent.so
</code></pre>
<p>在附带的代码文件里提供了一个可运行的 Java 类，默认情况下运行的结果如下图所示：</p>
<p><img src="https://img.dyzmj.top/img/202305310857810.png" alt="Untitled"></p>
<p>现在，我们运行程序前告诉 Java 先加载编译出来的 Agent：</p>
<pre><code class="language-bash">java -agentlib:Agent=first MethodTraceTest
</code></pre>
<p>这次的输出如图 3. 所示：</p>
<p><img src="https://img.dyzmj.top/img/202305310857484.png" alt="Untitled"></p>
<p>可以当程序运行到 MethodTraceTest 的 first 方法时，Agent 会输出这个事件。“first” 是 Agent 运行的参数，如果不指定的话，所有的进入方法的触发的事件都会被输出，如果读者把这个参数去掉再运行的话，会发现在运行 main 函数前，已经有非常多基本的类库函数被调用了。</p>
<hr>
<h1>三、JDWP 协议及实现</h1>
<p>JDWP 是 Java Debug Wire Protocol 的缩写，它定义了调试器（debugger）和被调试的 Java 虚拟机（target vm）之间的通信协议。</p>
<h2>1、<strong><strong>JDWP 协议介绍</strong></strong></h2>
<p>这里首先要说明一下 debugger 和 target vm。Target vm 中运行着我们希望要调试的程序，它与一般运行的 Java 虚拟机没有什么区别，只是在启动时加载了 Agent JDWP 从而具备了调试功能。而 debugger 就是我们熟知的调试器，它向运行中的 target vm 发送命令来获取 target vm 运行时的状态和控制 Java 程序的执行。Debugger 和 target vm 分别在各自的进程中运行，他们之间的通信协议就是 JDWP。</p>
<p>JDWP 与其他许多协议不同，它仅仅定义了数据传输的格式，但并没有指定具体的传输方式。这就意味着一个 JDWP 的实现可以不需要做任何修改就正常工作在不同的传输方式上（在 JDWP 传输接口中会做详细介绍）。</p>
<p>JDWP 是语言无关的。理论上我们可以选用任意语言实现 JDWP。然而我们注意到，在 JDWP 的两端分别是 target vm 和 debugger。Target vm 端，JDWP 模块必须以 Agent library 的形式在 Java 虚拟机启动时加载，并且它必须通过 Java 虚拟机提供的 JVMTI 接口实现各种 debug 的功能，所以必须使用 C/C++ 语言编写。而 debugger 端就没有这样的限制，可以使用任意语言编写，只要遵守 JDWP 规范即可。JDI（Java Debug Interface）就包含了一个 Java 的 JDWP debugger 端的实现（JDI 将在该系列的下一篇文章中介绍），JDK 中调试工具 jdb 也是使用 JDI 完成其调试功能的。</p>
<p><img src="https://img.dyzmj.top/img/202305310857391.png" alt="Untitled"></p>
<h2>2、<strong><strong>协议分析</strong></strong></h2>
<p>JDWP 大致分为两个阶段：握手和应答。握手是在传输层连接建立完成后，做的第一件事：</p>
<p>Debugger 发送 14 bytes 的字符串 “JDWP-Handshake” 到 target Java 虚拟机</p>
<p>Target Java 虚拟机回复 “JDWP-Handshake”</p>
<p><img src="https://img.dyzmj.top/img/202305310858487.png" alt="Untitled"></p>
<p>握手完成，debugger 就可以向 target Java 虚拟机发送命令了。JDWP 是通过命令（command）和回复（reply）进行通信的，这与 HTTP 有些相似。JDWP 本身是无状态的，因此对 command 出现的顺序并不受限制。</p>
<p>JDWP 有两种基本的包（packet）类型：</p>
<p><strong>命令包</strong>（command packet）</p>
<p><strong>回复包</strong>（reply packet）</p>
<p>Debugger 和 target Java 虚拟机都有可能发送 command packet。Debugger 通过发送 command packet 获取 target Java 虚拟机的信息以及控制程序的执行。Target Java 虚拟机通过发送 command packet 通知 debugger 某些事件的发生，如到达断点或是产生异常。</p>
<p>Reply packet 是用来回复 command packet 该命令是否执行成功，如果成功 reply packet 还有可能包含 command packet 请求的数据，比如当前的线程信息或者变量的值。从 target Java 虚拟机发送的事件消息是不需要回复的。</p>
<p>还有一点需要注意的是，JDWP 是<strong>异步</strong>
的：command packet 的发送方不需要等待接收到 reply packet 就可以继续发送下一个 command packet。</p>
<h3>3、<strong><strong>Packet 的结构</strong></strong></h3>
<p>Packet 分为<strong>包头</strong>（header）和<strong>数据</strong>（data）两部分组成。包头部分的结构和长度是固定，而数据部分的长度是可变的，具体内容视 packet 的内容而定。Command packet 和 reply packet 的包头长度相同，都是 11 个 bytes，这样更有利于传输层的抽象和实现。</p>
<p>Command packet 的 header 的结构 :</p>
<p><img src="https://img.dyzmj.top/img/202305310858476.png" alt="Untitled"></p>
<ul>
<li><strong>Length</strong> 是整个 packet 的长度，包括 length 部分。因为包头的长度是固定的 11 bytes，所以如果一个 command packet 没有数据部分，则 length 的值就是 11。</li>
<li><strong>Id</strong> 是一个唯一值，用来标记和识别 reply 所属的 command。Reply packet 与它所回复的 command packet 具有相同的 Id，异步的消息就是通过 Id 来配对识别的。</li>
<li><strong>Flags</strong> 目前对于 command packet 值始终是 0。</li>
<li><strong>Command Set</strong> 相当于一个 command 的分组，一些功能相近的 command 被分在同一个 Command Set 中。Command Set 的值被划分为 3 个部分：
<ul>
<li>0-63: 从 debugger 发往 target Java 虚拟机的命令</li>
<li>64 – 127： 从 target Java 虚拟机发往 debugger 的命令</li>
<li>128 – 256： 预留的自定义和扩展命令</li>
</ul>
</li>
</ul>
<p>Reply packet 的 header 的结构：</p>
<p><img src="https://img.dyzmj.top/img/202305310858690.png" alt="Untitled"></p>
<ul>
<li><strong>Length、Id</strong> 作用与 command packet 中的一样。</li>
<li><strong>Flags</strong> 目前对于 reply packet 值始终是 0x80。我们可以通过 Flags 的值来判断接收到的 packet 是 command 还是 reply。</li>
<li><strong>Error Code</strong> 用来表示被回复的命令是否被正确执行了。零表示正确，非零表示执行错误。</li>
</ul>
<p>Data 的内容和结构依据不同的 command 和 reply 都有所不同。比如请求一个对象成员变量值的 command，它的 data 中就包含该对象的 id 和成员变量的 id。而 reply 中则包含该成员变量的值。</p>
<p>JDWP 还定义了一些数据类型专门用来传递 Java 相关的数据信息。下面列举了一些数据类型:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>长度</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>byte</td>
<td>1 byte</td>
<td>byte 值。</td>
</tr>
<tr>
<td>boolean</td>
<td>1 byte</td>
<td>布尔值，0 表示假，非零表示真。</td>
</tr>
<tr>
<td>int</td>
<td>4 byte</td>
<td>4 字节有符号整数。</td>
</tr>
<tr>
<td>long</td>
<td>8 byte</td>
<td>8 字节有符号整数。</td>
</tr>
<tr>
<td>objectID</td>
<td>依据 target Java 虚拟机而定，最大 8 byte</td>
<td>Target Java 虚拟机中对象（object）的唯一 ID。这个值在整个 JDWP 的会话中不会被重用，始终指向同一个对象，即使该对象已经被 GC 回收（引用被回收的对象将返回 INVALID_OBJECT 错误。</td>
</tr>
<tr>
<td>Tagged-objectID</td>
<td>objectID 的长度加 1</td>
<td>第一个 byte 表示对象的类型，比如，整型，字符串，类等等。紧接着是一个 objectID。</td>
</tr>
<tr>
<td>threadID</td>
<td>同 objectID 的长度</td>
<td>表示 Target Java 虚拟机中的一个线程对象</td>
</tr>
<tr>
<td>stringID</td>
<td>同 objectID 的长度</td>
<td>表示 Target Java 虚拟机中的一字符串对象</td>
</tr>
<tr>
<td>referenceTypeID</td>
<td>同 objectID 的长度</td>
<td>表示 Target Java 虚拟机中的一个引用类型对象，即类（class）的唯一 ID。</td>
</tr>
<tr>
<td>classID</td>
<td>同 objectID 的长度</td>
<td>表示 Target Java 虚拟机中的一个类对象。</td>
</tr>
<tr>
<td>methodID</td>
<td>依据 target Java 虚拟机而定，最大 8 byte</td>
<td>Target Java 虚拟机某个类中的方法的唯一 ID。methodID 必须在他所属类和所属类的所有子类中保持唯一。从整个 Java 虚拟机来看它并不是唯一的。methodID 与它所属类的 referenceTypeID 一起在整个 Java 虚拟机中是唯一的。</td>
</tr>
<tr>
<td>fieldID</td>
<td>依据 target Java 虚拟机而定，最大 8 byte</td>
<td>与 methodID 类似，Target Java 虚拟机某个类中的成员的唯一 ID。</td>
</tr>
<tr>
<td>frameID</td>
<td>依据 target Java 虚拟机而定，最大 8 byte</td>
<td>Java 中栈中的每一层方法调用都会生成一个 frame。frameID 在整个 target Java 虚拟机中是唯一的，并且只在线程挂起（suspended）的时候有效。</td>
</tr>
<tr>
<td>location</td>
<td>依据 target Java 虚拟机而定，最大 8 byte</td>
<td>一个可执行的位置。Debugger 用它来定位 stepping 时在源代码中的位置。</td>
</tr>
</tbody>
</table>
<h2>4、<strong><strong>JDWP 传输接口（Java Debug Wire Protocol Transport Interface）</strong></strong></h2>
<p>前面提到 JDWP 的定义是与传输层独立的，但如何使 JDWP 能够无缝的使用不同的传输实现，而又无需修改 JDWP 本身的代码？ JDWP 传输接口（Java Debug Wire Protocol Transport Interface）为我们解决了这个问题。</p>
<p>JDWP 传输接口定义了一系列的方法用来定义 JDWP 与传输层实现之间的交互方式。首先传输层的必须以动态链接库的方式实现，并且暴露一系列的标准接口供 JDWP 使用。与 JNI 和 JVMTI 类似，访问传输层也需要一个环境指针（jdwpTransport），通过这个指针可以访问传输层提供的所有方法。</p>
<p>当 JDWP agent 被 Java 虚拟机加载后，JDWP 会根据参数去加载指定的传输层实现（Sun 的 JDK 在 Windows 提供 socket 和 share memory 两种传输方式，而在 Linux 上只有 socket 方式）。传输层实现的动态链接库实现必须暴露 jdwpTransport_OnLoad 接口，JDWP agent 在加载传输层动态链接库后会调用该接口进行传输层的初始化。接口定义如下：</p>
<pre><code class="language-c">JNIEXPORT jint JNICALL 
jdwpTransport_OnLoad(JavaVM *jvm, 
    jdwpTransportCallback *callback, 
    jint version, 
    jdwpTransportEnv** env);
</code></pre>
<p>callback 参数指向一个内存管理的函数表，传输层用它来进行内存的分配和释放，结构定义如下：</p>
<pre><code class="language-c">typedef struct jdwpTransportCallback { 
    void* (*alloc)(jint numBytes); 
    void (*free)(void *buffer); 
} jdwpTransportCallback;
</code></pre>
<p>env 参数是环境指针，指向的函数表由传输层初始化。</p>
<p>JDWP 传输层定义的接口主要分为两类：连接管理和 I/O 操作。</p>
<h2>5、<strong><strong>连接管理</strong></strong></h2>
<p>连接管理接口主要负责连接的建立和关闭。一个连接为 JDWP 和 debugger 提供了可靠的数据流。Packet 被接收的顺序严格的按照被写入连接的顺序。</p>
<p>连接的建立是双向的，即 JDWP 可以主动去连接 debugger 或者 JDWP 等待 debugger 的连接。对于主动去连接 debugger，需要调用方法 Attach，定义如下：</p>
<pre><code class="language-c">jdwpTransportError 
Attach(jdwpTransportEnv* env, const char* address, 
    jlong attachTimeout, jlong handshakeTimeout)
</code></pre>
<p>在连接建立后，会立即进行握手操作，确保对方也在使用 JDWP。因此方法参数中分别指定了 attch 和握手的超时时间。</p>
<p>address 参数因传输层的实现不同而有不同的格式。对于 socket，address 是主机地址；对于 share memory 则是共享内存的名称。</p>
<p>JDWP 等待 debugger 连接的方式，首先需要调用 StartListening 方法，定义如下：</p>
<pre><code class="language-c">jdwpTransportError 
StartListening(jdwpTransportEnv* env, const char* address, char** actualAddress)
</code></pre>
<p>该方法将使 JDWP 处于监听状态，随后调用 Accept 方法接收连接：</p>
<pre><code class="language-c">jdwpTransportError 
Accept(jdwpTransportEnv* env, jlong acceptTimeout, jlong handshakeTimeout)
</code></pre>
<p>与 Attach 方法类似，在连接建立后，会立即进行握手操作。</p>
<h2>6、<strong><strong>I/O 操作</strong></strong></h2>
<p>I/O 操作接口主要是负责从传输层读写 packet。有 ReadPacket 和 WritePacket 两个方法：</p>
<pre><code class="language-c">jdwpTransportError 
ReadPacket(jdwpTransportEnv* env, jdwpPacket* packet) 
 
jdwpTransportError 
WritePacket(jdwpTransportEnv* env, const jdwpPacket* packet)
</code></pre>
<p>参数 packet 是要被读写的 packet，其结构 jdwpPacket 与我们开始提到的 JDWP packet 结构一致，定义如下：</p>
<pre><code class="language-c">typedef struct { 
    jint len;        // packet length 
    jint id;         // packet id 
    jbyte flags;     // value is 0 
    jbyte cmdSet;    // command set 
    jbyte cmd;       // command in specific command set 
    jbyte *data;     // data carried by packet 
} jdwpCmdPacket; 
 
typedef struct { 
    jint len;        // packet length 
    jint id;         // packet id 
    jbyte flags;     // value 0x80 
    jshort errorCode;    // error code 
    jbyte *data;     // data carried by packet 
} jdwpReplyPacket; 
 
typedef struct jdwpPacket { 
    union { 
        jdwpCmdPacket cmd; 
        jdwpReplyPacket reply; 
    } type; 
} jdwpPacket;
</code></pre>
<h2>7、<strong><strong>JDWP 的命令实现机制</strong></strong></h2>
<p>下面将通过讲解一个 JDWP 命令的实例来介绍 JDWP 命令的实现机制。JDWP 作为一种协议，它的作用就在于充当了调试器与 Java 虚拟机的沟通桥梁。通俗点讲，调试器在调试过程中需要不断向 Java 虚拟机查询各种信息，那么 JDWP 就规定了查询的具体方式。</p>
<p>在 Java 6.0 中，JDWP 包含了 18 组命令集合，其中每个命令集合又包含了若干条命令。那么这些命令是如何实现的呢？下面我们先来看一个最简单的 VirtualMachine（命令集合 1）的 Version 命令，以此来剖析其中的实现细节。</p>
<p>因为 JDWP 在整个 JPDA 框架中处于相对底层的位置，我们无法在现实应用中来为大家演示 JDWP 的单个命令的执行过程。在这里我们通过一个针对该命令的 Java 测试用例来说明。</p>
<pre><code class="language-java">CommandPacket packet = new CommandPacket( 
    JDWPCommands.VirtualMachineCommandSet.CommandSetID, 
    JDWPCommands.VirtualMachineCommandSet.VersionCommand); 
         
ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet); 
 
String description = reply.getNextValueAsString(); 
int    jdwpMajor   = reply.getNextValueAsInt(); 
int    jdwpMinor   = reply.getNextValueAsInt(); 
String vmVersion   = reply.getNextValueAsString(); 
String vmName      = reply.getNextValueAsString(); 
 
logWriter.println(&quot;description\t= &quot; + description); 
logWriter.println(&quot;jdwpMajor\t= &quot; + jdwpMajor); 
logWriter.println(&quot;jdwpMinor\t= &quot; + jdwpMinor); 
logWriter.println(&quot;vmVersion\t= &quot; + vmVersion); 
logWriter.println(&quot;vmName\t\t= &quot; + vmName);
</code></pre>
<p>这里先简单介绍一下这段代码的作用。</p>
<p>首先，我们会创建一个 VirtualMachine 的 Version 命令的命令包实例 packet。你可能已经注意到，该命令包主要就是配置了两个参数 : CommandSetID 和 VersionComamnd，它们的值均为 1。表明我们想执行的命令是属于命令集合 1 的命令 1，即 VirtualMachine 的 Version 命令。</p>
<p>然后在 performCommand 方法中我们发送了该命令并收到了 JDWP 的回复包 reply。通过解析 reply，我们得到了该命令的回复信息。</p>
<pre><code class="language-java">description = Java 虚拟机 version 1.6.0 (IBM J9 VM, J2RE 1.6.0 IBM J9 2.4 Windows XP x86-32 
jvmwi3260sr5-20090519_35743 (JIT enabled, AOT enabled) 
J9VM - 20090519_035743_lHdSMr 
JIT  - r9_20090518_2017 
GC   - 20090417_AA, 2.4) 
jdwpMajor    = 1 
jdwpMinor    = 6 
vmVersion    = 1.6.0 
vmName       = IBM J9 VM
</code></pre>
<p>测试用例的执行结果显示，我们通过该命令获得了 Java 虚拟机的版本信息，这正是 VirtualMachine 的 Version 命令的作用。</p>
<p>前面已经提到，JDWP 接收到的是调试器发送的命令包，返回的就是反馈信息的回复包。在这个例子中，我们模拟的调试器会发送 VirtualMachine 的 Version 命令。JDWP 在执行完该命令后就向调试器返回 Java 虚拟机的版本信息。</p>
<p>返回信息的包内容同样是在 JDWP Spec 里面规定的。比如本例中的回复包，Spec 中的描述如下（测试用例中的回复包解析就是参照这个规定的 ):</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>string</td>
<td>description</td>
<td>VM version 的文字描述信息。</td>
</tr>
<tr>
<td>int</td>
<td>jdwpMajor</td>
<td>JDWP 主版本号。</td>
</tr>
<tr>
<td>int</td>
<td>jdwpMinor</td>
<td>JDWP 次版本号。</td>
</tr>
<tr>
<td>string</td>
<td>vmVersion</td>
<td>VM JRE 版本，也就是 java.version 属性值。</td>
</tr>
<tr>
<td>string</td>
<td>vmName</td>
<td>VM 的名称，也就是 java.vm.name 属性值。</td>
</tr>
</tbody>
</table>
<p>通过这个简单的例子，相信大家对 JDWP 的命令已经有了一个大体的了解。 那么在 JDWP 内部是如何处理接收到的命令并返回回复包的呢？下面以 Apache Harmony 的 JDWP 为例，为大家介绍其内部的实现架构。</p>
<p><img src="https://img.dyzmj.top/img/202305310858576.png" alt="Untitled"></p>
<p>如图所示，JDWP 接收和发送的包都会经过 TransportManager 进行处理。JDWP 的应用层与传输层是独立的，就在于 TransportManager 调用的是 <strong>JDWP 传输接口</strong>
（Java Debug Wire Protocol Transport Interface），所以无需关心底层网络的具体传输实现。TransportManager 的主要作用就是充当 JDWP 与外界通讯的数据包的中转站，负责将 JDWP 的命令包在接收后进行解析或是对回复包在发送前进行打包，从而使 JDWP 能够专注于应用层的实现。</p>
<p>对于收到的命令包，TransportManager 处理后会转给 PacketDispatcher，进一步封装后会继续转到 CommandDispatcher。然后，CommandDispatcher 会根据命令中提供的命令组号（CommandSet）和命令号（Command）创建一个具体的 CommandHandler 来处理 JDWP 命令。</p>
<p>其中，CommandHandler 才是真正执行 JDWP 命令的类。我们会为每个 JDWP 命令都定义一个相对应的 CommandHandler 的子类，当接收到某个命令时，就会创建处理该命令的 CommandHandler 的子类的实例来作具体的处理。</p>
<p><img src="https://img.dyzmj.top/img/202305310858965.png" alt="Untitled"></p>
<h2>8、<strong><strong>单线程执行的命令</strong></strong></h2>
<p>上图就是一个命令的处理流程图。可以看到，对于一个可以直接在该线程中完成的命令（我们称为单线程执行的命令），一般其内部会调用 JVMTI 方法和 JNI 方法来真正对 Java 虚拟机进行操作。</p>
<p>例如，VirtualMachine 的 Version 命令中，对于 vmVersion 和 vmName 属性，我们可以通过 JNI 来调用 Java 方法 System.getProperty 来获取。然后，JDWP 将回复包中所需要的结果封装到包中后交由 TransportManager 来进行后续操作。</p>
<h2>9、<strong><strong>多线程执行的命令</strong></strong></h2>
<p>对于一些较为复杂的命令，是无法在 CommandHandler 子类的处理线程中完成的。例如，ClassType 的 InvokeMethod 命令，它会要求在指定的某个线程中执行一个静态方法。显然，CommandHandler 子类的当前线程并不是所要求的线程。</p>
<p>这时，<strong>JDWP 线程会先把这个请求先放到一个列表中，然后等待，直到所要求的线程执行完那个静态方法后，再把结果返回给调试器</strong>。</p>
<h2>10、<strong><strong>JDWP 的事件处理机制</strong></strong></h2>
<p>前面介绍的 VirtualMachine 的 Version 命令过程非常简单，就是一个查询和信息返回的过程。在实际调试过程中，一个 JDI 的命令往往会有数条这类简单的查询命令参与，而且会涉及到很多更为复杂的命令。要了解更为复杂的 JDWP 命令实现机制，就必须介绍 JDWP 的事件处理机制。</p>
<p>在 Java 虚拟机中，我们会接触到许多事件，例如 VM 的初始化，类的装载，异常的发生，断点的触发等等。那么这些事件调试器是如何通过 JDWP 来获知的呢？下面，我们通过介绍在调试过程中断点的触发是如何实现的，来为大家揭示其中的实现机制。</p>
<p>在这里，我们任意调试一段 Java 程序，并在某一行中加入断点。然后，我们执行到该断点，此时所有 Java 线程都处于 suspend 状态。这是很常见的断点触发过程。为了记录在此过程中 JDWP 的行为，我们使用了一个开启了 trace 信息的 JDWP。虽然这并不是一个复杂的操作，但整个 trace 信息也有几千行。</p>
<p>可见，作为相对底层的 JDWP，其实际处理的命令要比想象的多许多。为了介绍 JDWP 的事件处理机制，我们挑选了其中比较重要的一些 trace 信息来说明：</p>
<pre><code class="language-bash">[RequestManager.cpp:601] AddRequest: event=BREAKPOINT[2], req=48, modCount=1, policy=1 
[RequestManager.cpp:791] GenerateEvents: event #0: kind=BREAKPOINT, req=48 
[RequestManager.cpp:1543] HandleBreakpoint: BREAKPOINT events: count=1, suspendPolicy=1, 
                          location=0 
[RequestManager.cpp:1575] HandleBreakpoint: post set of 1 
[EventDispatcher.cpp:415] PostEventSet -- wait for release on event: thread=4185A5A0, 
                          name=(null), eventKind=2 
 
[EventDispatcher.cpp:309] SuspendOnEvent -- send event set: id=3, policy=1 
[EventDispatcher.cpp:334] SuspendOnEvent -- wait for thread on event: thread=4185A5A0, 
                          name=(null) 
[EventDispatcher.cpp:349] SuspendOnEvent -- suspend thread on event: thread=4185A5A0, 
                          name=(null) 
[EventDispatcher.cpp:360] SuspendOnEvent -- release thread on event: thread=4185A5A0, 
                          name=(null)
</code></pre>
<p>首先，调试器需要发起一个断点的请求，这是通过 JDWP 的 Set 命令完成的。在 trace 中，我们看到 AddRequest 就是做了这件事。可以清楚的发现，调试器请求的是一个断点信息（event=BREAKPOINT [2]）。</p>
<p>在 JDWP 的实现中，这一过程表现为：在 Set 命令中会生成一个具体的 request, JDWP 的 RequestManager 会记录这个 request（request 中会包含一些过滤条件，当事件发生时 RequestManager 会过滤掉不符合预先设定条件的事件），并通过 JVMTI 的 SetEventNotificationMode 方法使这个事件触发生效（否则事件发生时 Java 虚拟机不会报告）。</p>
<p><img src="https://img.dyzmj.top/img/202305310858506.png" alt="Untitled"></p>
<p>当断点发生时，Java 虚拟机就会调用 JDWP 中预先定义好的处理该事件的回调函数。在 trace 中，HandleBreakpoint 就是我们在 JDWP 中定义好的处理断点信息的回调函数。它的作用就是要生成一个 JDWP 端所描述的断点事件来告知调试器（Java 虚拟机只是触发了一个 JVMTI 的消息）。</p>
<p>由于断点的事件在调试器申请时就要求所有 Java 线程在断点触发时被 suspend，那这一步由谁来完成呢？这里要谈到一个细节问题，HandleBreakpoint 作为一个回调函数，其执行线程其实就是断点触发的 Java 线程。</p>
<p>显然，我们不应该由它来负责 suspend 所有 Java 线程。</p>
<p>原因很简单，我们还有一步工作要做，就是要把该断点触发信息返回给调试器。如果我们先返回信息，然后 suspend 所有 Java 线程，这就无法保证在调试器收到信息时所有 Java 线程已经被 suspend。</p>
<p>反之，先 Suspend 了所有 Java 线程，谁来负责发送信息给调试器呢？</p>
<p>为了解决这个问题，我们通过 JDWP 的 EventDispatcher 线程来帮我们 suspend 线程和发送信息。实现的过程是，我们让触发断点的 Java 线程来 PostEventSet（trace 中可以看到），把生成的 JDWP 事件放到一个队列中，然后就开始等待。由 EventDispatcher 线程来负责从队列中取出 JDWP 事件，并根据事件中的设定，来 suspend 所要求的 Java 线程并发送出该事件。</p>
<p>在这里，我们在事件触发的 Java 线程和 EventDispatcher 线程之间添加了一个同步机制，当事件发送出去后，事件触发的 Java 线程会把 JDWP 中的该事件删除，到这里，整个 JDWP 事件处理就完成了。</p>
<h1>四、Java 调试接口（JDI）</h1>
<h2>1、<strong><strong>JDI 简介</strong></strong></h2>
<p>JDI（Java Debug Interface）是 JPDA 三层模块中最高层的接口，定义了调试器（Debugger）所需要的一些调试接口。基于这些接口，调试器可以及时地了解目标虚拟机的状态，例如查看目标虚拟机上有哪些类和实例等。另外，调试者还可以控制目标虚拟机的执行，例如挂起和恢复目标虚拟机上的线程，设置断点等。</p>
<p>目前，大多数的 JDI 实现都是通过 Java 语言编写的。比如，Java 开发者再熟悉不过的 Eclipse IDE，它的调试工具相信大家都使用过。它的两个插件 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 与其强大的调试功能密切相关，其中 org.eclipse.jdt.debug.ui 是 Eclipse 调试工具界面的实现，而 org.eclipse.jdt.debug 则是 JDI 的一个完整实现。</p>
<h2>2、<strong><strong>JDI 工作方式</strong></strong></h2>
<p>首先，调试器（Debugger）通过 Bootstrap 获取唯一的虚拟机管理器：</p>
<pre><code class="language-java">VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager();
</code></pre>
<p>虚拟机管理器将在第一次被调用时初始化可用的链接器。一般地，调试器会默认地采用启动型链接器进行链接：</p>
<pre><code class="language-java">LaunchingConnector defaultConnector = virtualMachineManager.defaultConnector();
</code></pre>
<p>然后，调试器调用链接器的 launch () 来启动目标程序，并完成调试器与目标虚拟机的链接:</p>
<pre><code class="language-java">VirtualMachine targetVM = defaultConnector.launch(arguments);
</code></pre>
<p>当链接完成后，调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令，命令通过链接被发送到前端运行目标程序的虚拟机上；然后，目标虚拟机根据接受的命令做出相应的操作，将调试的结果发回给后端的调试器；最后，调试器可视化数据信息反馈给用户。</p>
<p>从功能上，可以将 JDI 分成三个部分：</p>
<ul>
<li><strong>数据模块</strong></li>
<li><strong>链接模块</strong></li>
<li><strong>事件请求与处理模块</strong></li>
</ul>
<p>数据模块负责调试器和目标虚拟机上的数据建模，链接模块建立调试器与目标虚拟机的沟通渠道，事件请求与处理模块提供调试器与目标虚拟机交互方式，下面将逐一地介绍它们</p>
<h2>3、<strong><strong>JDI 数据模块</strong></strong></h2>
<h3>3.1 <strong><strong>Mirror</strong></strong></h3>
<p>Mirror 接口是 JDI 最底层的接口，JDI 中几乎所有其他接口都继承于它。镜像机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源，以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。例如，在目标虚拟机上，已装载的类被映射成 ReferenceType 镜像，对象实例被映射成 ObjectReference 镜像，基本类型的值（如 float 等）被映射成 PrimitiveValue（如 FloatValue 等）。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中，在调试过程中所触发的事件被映射成 Event 镜像（如 StepEvent 等），调试器发出的事件请求被映射成 EventRequest 镜像（如 StepRequest 等），被调试的目标虚拟机则被映射成 VirtualMachine 镜像。但是，JDI 并不保证目标虚拟机上的每份信息和资源都只有唯一的镜像与之对应，这是由 JDI 的具体实现所决定的。例如，目标虚拟机上的某个事件有可能存在多个 Event 镜像与之对应，例如 BreakpointEvent 等。</p>
<p>Mirror 实例或是由调试器创建，或是由目标虚拟机创建，调用 Mirror 实例 virtualMachine () 可以获取其虚拟机信息，如下所示。</p>
<pre><code class="language-java">VirtualMachine virtualMachine = mirror.virtualMachine();
</code></pre>
<p>返回的目标虚拟机对象实现了 VirtualMachine 接口，该接口提供了一套方法，可以用来直接或间接地获取目标虚拟机上所有的数据和状态信息，也可以挂起、恢复、终止目标虚拟机</p>
<p><img src="https://img.dyzmj.top/img/202305310858553.png" alt="Untitled"></p>
<p>这样，调试器便可以获取目标虚拟机上的信息，维持与目标虚拟机间的通信，并且检查，修改和控制目标虚拟机上资源等。</p>
<h3>3.2 <strong><strong>Value 和 Type</strong></strong></h3>
<p>Value 和 Type 接口分别代表着目标虚拟机中对象、实例变量和方法变量的值和类型。通过 Value 接口的 type ()，可以获取该值对应的类型。JDI 中定义了两种基本的数据类型：原始类型（PrimitiveType）和引用类型（ReferenceType）。与其对应的数值类型分别是原始值（PrimtiveValue）和对象引用（ObjectReference）。Value 和 Type 的具体对应关系，请参见下表：</p>
<table>
<thead>
<tr>
<th>(Value, Type)</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>(ByteValue, ByteType)</td>
<td>表示一个字节</td>
</tr>
<tr>
<td>(CharValue, CharType)</td>
<td>表示一个字符</td>
</tr>
<tr>
<td>(ShortValue, ShortType)</td>
<td>表示一个短整型数据</td>
</tr>
<tr>
<td>(IntegerValue, IntegerType)</td>
<td>表示一个整型数据</td>
</tr>
<tr>
<td>(LongValue, LongType)</td>
<td>表示一个长整型数据</td>
</tr>
<tr>
<td>(FloatValue, FloatType)</td>
<td>表示一个浮点型数据</td>
</tr>
<tr>
<td>(DoubleValue, DoubleType)</td>
<td>表示一个双精度浮点型数据</td>
</tr>
<tr>
<td>(BooleanValue, BooleanType)</td>
<td>表示一个布尔型数据</td>
</tr>
<tr>
<td>(ObjectReference, ReferenceType)</td>
<td>表示目标虚拟机上的一个对象</td>
</tr>
<tr>
<td>(ArrayReference, ArrayType)</td>
<td>表示目标虚拟机上的一个数组</td>
</tr>
<tr>
<td>(StringReference, ClassType)</td>
<td>表示目标虚拟机上的一个字符串对象</td>
</tr>
<tr>
<td>(ThreadReference, ClassType)</td>
<td>表示目标虚拟机上的一个线程对象，有一套方法可以获得当前设置的断点，堆栈，也能挂起和恢复该线程等</td>
</tr>
<tr>
<td>(ThreadGroupReference, ClassType)</td>
<td>表示目标虚拟机上的一个线程组对象</td>
</tr>
<tr>
<td>(ClassObjectReference, ClassType)</td>
<td>表示目标虚拟机上的一个类的 java.lang.Class 实例</td>
</tr>
<tr>
<td>(ClassLoaderReference, ClassType)</td>
<td>表示目标虚拟机上的一个 ClassLoader 对象</td>
</tr>
<tr>
<td>(VoidValue, VoidType)</td>
<td>表示 void 类型</td>
</tr>
</tbody>
</table>
<p>PrimitiveType 包括 Java 的 8 种基本类型，ReferenceType 包括目标虚拟机中装载的类，接口和数组的类型（数组也是一种对象，有自己的对象类型）。ReferenceType 有三种子接口：ClassType 对应于加载的类，InterfaceType 对应于接口，ArrayType 对应于数组。另外，ReferenceType 还提供了一组方法，可以用来获取该类型中声明的所有变量、方法、静态变量的取值、内嵌类、运行实例、行号等信息。</p>
<p>PrimtiveValue 封装了 PrimitiveType 的值，它提供一组方法可将 PrimtiveValue 转化为 Java 原始数据。例如，IntegerValue 的 value () 将返回一个 int 型数据。对应地，VirtualMachine 也提供了一组方法，用以将 Java 原始数据转化为 PrimtiveValue 型数据。例如 mirrorOf (float value) 将给定的 float 数据转化为 FloatValue 型数据。</p>
<p>ObjectReference 封装了目标虚拟机中的对象，通过 getValue () 和 setValue () 方法可以访问和修改对象中变量的值，通过 invokeMethod () 可以调用该对象中的指定方法，通过 referringObjects () 可以获得直接引用该对象的其他对象，通过 enableCollection () 和 disableCollection () 可以允许和禁止 GC 回收该对象。</p>
<h3>3.3 <strong><strong>TypeComponent</strong></strong></h3>
<p>TypeComponent 接口表示 Class 或者 Interface 所声明的实体（Entity），它是 Field 和 Method 接口的基类。Field 表示一个类或者实例的变量，调用其 type () 可返回域的类型。Method 表示一个方法。TypeComponent 通过方法 declaredType () 获得声明该变量或方法的类或接口，通过 name () 获得该变量或者方法的名字（对于 Field 返回域名，对于一般方法返回方法名，对于类构造函数返回 <!-- raw HTML omitted -->，对于静态初始化构造函数返回 <!-- raw HTML omitted -->）。</p>
<h2>4、<strong><strong>JDI 的链接模块</strong></strong></h2>
<p>链接是调试器与目标虚拟机之间交互的渠道，一次链接可以由调试器发起，也可以由被调试的目标虚拟机发起。一个调试器可以链接多个目标虚拟机，但一个目标虚拟机最多只能链接一个调试器。链接是由链接器（Connector）生成的，不同的链接器封装着不同的链接方式。JDI 中定义三种链接器接口，分别是</p>
<p><strong>依附型链接器</strong>（AttachingConnector）</p>
<p><strong>监听型链接器</strong>（ListeningConnector）</p>
<p><strong>启动型链接器</strong>（LaunchingConnector）</p>
<p>在调试过程中，实际使用的链接器必须实现其中一种接口。</p>
<p>根据调试器在链接过程中扮演的角色，可以将链接方式划分为主动链接和被动链接。主动链接是较常见一种链接方式，表示调试器主动地向目标虚拟机发起链接。下面将举两个主动链接的例子：</p>
<p>由调试器启动目标虚拟机的链接方式：这是最常见、最简单的一种链接方式。</p>
<ul>
<li>调试器调用 VirtualMachineManager 的 launchingConnectors () 方法获取所有的启动型链接器实例；</li>
<li>根据传输方式或其他特征选择一个启动型链接器，调用其 launch () 方法启动和链接目标虚拟机；</li>
<li>启动后，返回目标虚拟机的实例。</li>
</ul>
<p>更高级的，当目标虚拟机已处于运行状态时，可以采用调试器 attach 到目标虚拟机的链接方式：</p>
<ul>
<li>目标虚拟机必须以 -agentlib:jdwp=transport=xxx,server=y 参数启动，并根据传输方式生成监听地址；（其中，xxx 是传输方式，可以是 dt_socket 和 share_memory）</li>
<li>调试器启动，调用 VirtualMachineManager 的 attachingConnectors () 方法获取所有的依附型链接器实例；</li>
<li>根据目标虚拟机采用的传输方式选择一个依附型链接器，调用其 attach () 方法依附到目标虚拟机上；</li>
<li>完成链接后，返回目标虚拟机的实例。</li>
</ul>
<p>被动链接表示调试器将被动地等待或者监听由目标虚拟机发起的链接，同样也举两个被动链接的例子：</p>
<p>目标虚拟机 attach 到已运行的调试器上的链接方式：</p>
<ul>
<li>调试器通过 VirtualMachineManager 的 listeningConnectors () 方法获取所有的监听型链接器实例；</li>
<li>为每种传输类型分别选定一个链接器，然后调用链接器的 startListening () 方法让链接器进入监听状态；</li>
<li>通过 accept () 方法通知链接器开始等待正确的入站链接，该方法将返回调试器正在监听的地址描述符；</li>
<li>终端用户以 -agentlib:jdwp=transport=xxx,address=yyy 参数启动目标虚拟机（其中，yyy 是调试器的监听地址）；</li>
<li>目标虚拟机会自动地 attach 到调试器上建立链接，然后返回目标虚拟机的实例。</li>
</ul>
<p>即时（Just-In-Time）链接方式：</p>
<ul>
<li>以 -agentlib:jdwp=launch=cmdline,onuncaught=y,transport=xxx,server=y 参数启动目标虚拟机；</li>
<li>虚拟机将抛出一个未捕获的异常，同时生成特定于 xxx 传输方式的监听地址，用于确立一次链接；</li>
<li>目标虚拟机启动调试器，并告知调试器传输方式和监听地址；</li>
<li>启动后，调试器调用 VirtualMachineManager 的 attachingConnectors () 方法获取所有依附型链接器实例；</li>
<li>根据指定的 xxx 传输方式，选择一个链接器；</li>
<li>调用链接器的 attach 方法依附到对应地址的目标虚拟机上；</li>
<li>完成链接后，返回目标虚拟机的实例。</li>
</ul>
<p>Connector.Argument 是 Connector 的内嵌接口，表示链接器的一个参数，不同类型的链接器支持不同的链接器参数，LaunchingConnector 支持 home，main，suspend 等，AttachingConnector 和 ListeningConnector 支持 timeout，hostname，port 等参数。</p>
<p><img src="https://img.dyzmj.top/img/202305310859631.png" alt="Untitled"></p>
<p>下面将举一个简单例子，描述如何设置 main 链接参数，并启动目标虚拟机。首先，调用链接器的 defaultArguments () 获取该链接器所支持的一组默认参数:</p>
<pre><code class="language-java">Map&lt;String,Connector.Argument&gt; defaultArguments = connector.defaultArguments();
</code></pre>
<p>默认参数存储在一个 Key-Value 对的 Map 中，Key 是该链接器参数的唯一标识符（对终端用户不可见），Value 是对应的 Connector.Argument 实例（包括具体参数的信息和默认值）。返回的 Map 不能再新增或者删除元素，只能修改已有元素的值。</p>
<p>然后，从返回的 Map 中获取标识符为 main 的链接器参数:</p>
<pre><code class="language-java">Connector.Argument mainArgument = defaultArguments.get(“main”);
</code></pre>
<p>最后，将 main 参数值设置为 com.ibm.jdi.test.HelloWorld，以修改后的参数启动目标虚拟机</p>
<pre><code class="language-java">mainArgument.setValue(“com.ibm.jdi.test.HelloWorld”);
VirtualMachine targetVM = connector.launch(defaultArguments);
</code></pre>
<h2>5、<strong><strong>JDI 事件请求和处理模块</strong></strong></h2>
<p>JDI 的 com.sun.jdi.event 包定义了如下事件类型：</p>
<p><img src="https://img.dyzmj.top/img/202305310859732.png" alt="Untitled"></p>
<p>其中，与 Class 相关的有 ClassPrepareEvent 和 ClassUnloadEvent；与 Method 相关的有 MethodEntryEvent 和 MethodExitEvent；与 Field 相关的有 AccessWatchpointEvent 和 ModificationWatchpointEvent；与虚拟机相关的有 VMDeathEvent，VMDisconnectEvent 和 VMStartEvent 等。</p>
<table>
<thead>
<tr>
<th>事件类型</th>
<th>描述</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>ClassPrepareEvent</td>
<td>装载某个指定的类所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>ClassUnloadEvent</td>
<td>卸载某个指定的类所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>BreakingpointEvent</td>
<td>设置断点所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>ExceptionEvent</td>
<td>目标虚拟机运行中抛出指定异常所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MethodEntryEvent</td>
<td>进入某个指定方法体时引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MethodExitEvent</td>
<td>某个指定方法执行完成后引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MonitorContendedEnteredEvent</td>
<td>线程已经进入某个指定 Monitor 资源所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MonitorContendedEnterEvent</td>
<td>线程将要进入某个指定 Monitor 资源所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MonitorWaitedEvent</td>
<td>线程完成对某个指定 Monitor 资源等待所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>MonitorWaitEvent</td>
<td>线程开始等待对某个指定 Monitor 资源所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>StepEvent</td>
<td>目标应用程序执行下一条指令或者代码行所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>AccessWatchpointEvent</td>
<td>查看类的某个指定 Field 所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>ModificationWatchpointEvent</td>
<td>修改类的某个指定 Field 值所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>ThreadDeathEvent</td>
<td>某个指定线程运行完成所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>ThreadStartEvent</td>
<td>某个指定线程开始运行所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>VMDeathEvent</td>
<td>目标虚拟机停止运行所以的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>VMDisconnectEvent</td>
<td>目标虚拟机与调试器断开链接所引发的事件</td>
<td></td>
<td></td>
</tr>
<tr>
<td>VMStartEvent</td>
<td>目标虚拟机初始化时所引发的事件</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>不同的事件需要被分类地添加到不同的事件集合（EventSet）中，<strong>事件集是事件发送的最小单位</strong>
。事件集一旦创建出来，便不可再被修改。JDI 定义了一些规则，用以规定应该如何将事件分别加入到不同的事件集中：</p>
<ul>
<li>每个 VMStartEvent 事件应该分别加入到单独的一个事件集中；</li>
<li>每个 VMDisconnectEvent 事件应该分别加入到单独的一个事件集中；</li>
<li>所有的 VMDeathEvent 事件应该加入到同一个事件集中；</li>
<li>同一线程的 ThreadStartEvent 事件应该加入到同一事件集中；</li>
<li>同一线程的 ThreadDeathEvent 事件应该加入到同一事件集中；</li>
<li>同一类型的 ClassPrepareEvent 事件应该加入到同一个事件集中；</li>
<li>同一类型的 ClassUnloadEvent 事件应该加入到同一个事件集中；</li>
<li>同一 Field 的 AccessWatchpointEvent 事件应该加入到同一个事件集中；</li>
<li>同一 Field 的 ModificationWatchpointEvent 事件应该加入到同一个事件集中；</li>
<li>同一异常的 ExceptionEvent 事件应该加入到同一个事件集中；</li>
<li>同一方法的 MethodExitEvents 事件应该加入到同一个事件集中；</li>
<li>同一 Monitor 的 MonitorContendedEnterEvent 事件应该加入到同一个事件集中；</li>
<li>同一 Monitor 的 MonitorContendedEnteredEvent 事件应该加入到同一个事件集中；</li>
<li>同一 Monitor 的 MonitorWaitEvent 事件应该加入到同一个事件集中</li>
<li>同一 Monitor 上的 MonitorWaitedEvent 事件应该加入到同一个事件集中</li>
<li>在同一线程执行过程中，具有相同行号信息的 BreakpointEvent、StepEvent 和 MethodEntryEvent 事件应该加入到同一个事件集合中。</li>
</ul>
<p>生成的事件集将被依次地加入到目标虚拟机的事件队列（EventQueue）中。然后，EventQueue 将这些事件集以 “先进先出” 策略依次地发送到调试器端。<strong>EventQueue 负责管理来自目标虚拟机的事件，一个被调试的目标虚拟机上有且仅有一个 EventQueue 实例</strong>。特别地，随着一次事件集的发送，目标虚拟机上可能会有一部分的线程因此而被挂起。如果一直不恢复这些线程，有可能会导致目标虚拟机挂机。因此，在处理好一个事件集中的事件后，建议调用事件集的 resume () 方法，恢复所有可能被挂起的线程。</p>
<h2>6、<strong><strong>JDI 事件请求</strong></strong></h2>
<p>Event 是 JDI 中所有事件接口的父接口，它只定义了一个 request () 方法，用以返回由调试器发出的针对该事件的事件请求（EventRequest）。事件请求是由调试器向目标虚拟机发出的，目的是请求目标虚拟机在发生指定的事件后通知调试器。只有当调试器发出的请求与目标虚拟机上发生的事件契合时，这些事件才会被分发到各个事件集，进而等待发送至调试器端。在 JDI 中，每一种事件类型都对应着一种事件请求类型。一次事件请求可能对应有多个事件实例，但不是每个事件实例都存在与之对应的事件请求。例如，对于某些事件（如 VMDeathEvent，VMDisconnectEvent 等），即使没有对应的事件请求，这些事件也必定会被发送给调试器端。</p>
<p>另外，事件请求还支持过滤功能。通过给 EventRequest 实例添加过滤器（Filter），可以进一步筛选出调试器真正感兴趣的事件实例。事件请求支持多重过滤，通过 EventRequest 的 add*Filter () 方法可以添加多个过滤器。多个过滤器将共同作用，最终只有满足所有过滤条件的事件实例才会被发给调试器。常用的过滤器有：</p>
<ul>
<li>线程过滤器：用以过滤出指定线程中发生的事件；</li>
<li>类型过滤器：用以过滤出指定类型中发生的事件；</li>
<li>实例过滤器：用以过滤出指定实例中发生的事件；</li>
<li>计数过滤器：用以过滤出发生一定次数的事件；</li>
</ul>
<p>过滤器提供了一些附加的限制条件，减少了最终加入到事件队列的事件数量，从而提高了调试性能。除了过滤功能，还可以通过它的 setSuspendPolicy (int) 设置是否需要在事件发生后挂起目标虚拟机。</p>
<p>事件请求是由事件请求管理器（EventRequestManager）进行统一管理的，包括对请求的创建和删除。一个目标虚拟机中有且仅有一个 EventRequestManager 实例。通常，<strong>一个事件请求实例有两种状态：激活态和非激活态。</strong>
非激活态的事件请求将不起任何作用，即使目标虚拟机上有满足此请求的事件发生，目标虚拟机将不做停留，继续执行下一条指令。由 EventRequestManager 新建的事件请求都是非激活的，需要调用 setEnable (true) 方法激活该请求，而通过 setEnable (false) 则可废除该请求，使其转化为非激活态。</p>
<h2>7、<strong><strong>JDI 事件处理</strong></strong></h2>
<p>下面将介绍 JDI 中调试器与目标虚拟机事件交互的方式。首先，调试器调用目标虚拟机的 eventQueue () 和 eventRequestManager () 分别获取唯一的 EventQueue 实例和 EventRequestManager 实例。然后，通过 EventRequestManager 的 createXxxRequest () 创建需要的事件请求，并添加过滤器和设置挂起策略。接着，调试器将从 EventQueue 获取来自目标虚拟机的事件实例。</p>
<p>一个事件实例中包含着事件发生时目标虚拟机的一些状态信息。以 BreakpointEvent 为例：</p>
<p>调用 BreakpointEvent 的 thread () 可以获取产生事件的线程镜像（ThreadReference），调用 ThreadReference 的 frame (int) 可获得当前代码行所在的堆栈（StackFrame），调用 StackFrame 的 visibleVariables () 可获取当前堆栈中的所有本地变量（LocaleVariable）。通过调用 BreakpointEvent 的 location () 可获得断点所在的代码行号（Location），调用 Location 的 method () 可获得当前代码行所归属的方法。通过以上调用，调试器便可获得了目标虚拟机上线程、对象、变量等镜像信息。</p>
<p>另外，根据从事件实例中获取的以上信息，调试器还可以进一步控制目标虚拟机。例如，可以调用 ObjectReference 的 getValue () 和 setValue () 访问和修改对象中封装的 Field 或者 LocalVariable 等，进而影响虚拟机的行为。</p>
<p><img src="https://img.dyzmj.top/img/202305310859687.png" alt="Untitled"></p>
<h2>8、<strong><strong>一个 JDI 的简单实例</strong></strong></h2>
<p>下面给出一个简单例子，说明如何实现 JDI 的部分接口来提供一个简易的调试客户端。首先是被调试的 Java 类，这里给出一个简单的 Hello World 程序，main 方法第一行声明一个 “Hello World!” 的字符串变量，第二行打印出这个字符串的内容：</p>
<pre><code class="language-java">package com.ibm.jdi.test;
 
public class HelloWorld {
    public static void main(String[] args) {
        String str = &quot;Hello world!&quot;;
        System.out.println(str);
    }
}
</code></pre>
<p>接着是一个简单的调试器实现 SimpleDebugger，清单 9 列出了实现该调试器所需要导入的类库和变量。简单起见，所有的变量都声明为静态全局变量。这些变量分别代表了目标虚拟机镜像，目标虚拟机所在的进程，目标虚拟机的事件请求管理器和事件对列。变量 vmExit 标志目标虚拟机是否中止。</p>
<pre><code class="language-java">package com.ibm.jdi.test;
 
import java.util.List;
import java.util.Map;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.StringReference;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.LaunchingConnector;
import com.sun.jdi.connect.Connector.Argument;
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.ClassPrepareEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventIterator;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.VMDisconnectEvent;
import com.sun.jdi.event.VMStartEvent;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.ClassPrepareRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
 
public class SimpleDebugger {
    static VirtualMachine vm;
    static Process process;
    static EventRequestManager eventRequestManager;
    static EventQueue eventQueue;
    static EventSet eventSet;
    static boolean vmExit = false;
</code></pre>
<p>随后是 SimpleDebugger 的 main () 方法，首先从 VirtualMachineManager 获取默认的 LaunchingConnector，然后从该 Connector 取得默认的参数。接着，设置 main 和 suspend 参数，使得目标虚拟机运行 com.ibm.jdi.test.HelloWorld 类，并随后进入挂起状态。下一步，调用 LaunchingConnector.launch () 启动目标虚拟机，返回目标虚拟机的镜像实例，并且获取运行目标虚拟机的进程（ Process）。</p>
<p>然后，创建一个 ClassPrepareRequest 事件请求。当 com.ibm.jdi.test.HelloWorld 被装载时，目标虚拟机将发送对应的 ClassPrepareEvent 事件。事件处理完成后，通过 process 的 destroy () 方法销毁目标虚拟机进程，结束调试工作。</p>
<pre><code class="language-java">public static void main(String[] args) throws Exception{
    LaunchingConnector launchingConnector 
        = Bootstrap.virtualMachineManager().defaultConnector();
     
    // Get arguments of the launching connector
    Map&lt;String, Connector.Argument&gt; defaultArguments 
        = launchingConnector.defaultArguments();
    Connector.Argument mainArg = defaultArguments.get(&quot;main&quot;);
    Connector.Argument suspendArg = defaultArguments.get(&quot;suspend&quot;);
    // Set class of main method
    mainArg.setValue(&quot;com.ibm.jdi.test.HelloWorld&quot;);
    suspendArg.setValue(&quot;true&quot;);
    vm = launchingConnector.launch(defaultArguments);
 
    process = vm.process()
 
    // Register ClassPrepareRequest
    eventRequestManager = vm.eventRequestManager();
    ClassPrepareRequest classPrepareRequest 
        = eventRequestManager.createClassPrepareRequest();
    classPrepareRequest.addClassFilter(&quot;com.ibm.jdi.test.HelloWorld&quot;);
    classPrepareRequest.addCountFilter(1);
    classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
    classPrepareRequest.enable();
 
    // Enter event loop 
    eventLoop();
 
    process.destroy();
}
</code></pre>
<p>下面是 eventLoop () 函数的实现：首先获取目标虚拟机的事件队列，然后依次处理队列中的每个事件。当 vmExit（初始值为 false）标志为 true 时，结束循环。</p>
<pre><code class="language-java">private static void eventLoop() throws Exception {
    eventQueue = vm.eventQueue();
    while (true) {
        if (vmExit == true) {
            break;
        }
        eventSet = eventQueue.remove();
        EventIterator eventIterator = eventSet.eventIterator();
        while (eventIterator.hasNext()) {
            Event event = (Event) eventIterator.next();
            execute(event);
        }
    }
}
</code></pre>
<p>具体事件的处理是由 execute (Event) 实现的，这里主要列举出 ClassPreparEvent 和 BreakpointEvent 事件的处理用法。</p>
<pre><code class="language-java">private static void execute(Event event) throws Exception {
    if (event instanceof VMStartEvent) {
        System.out.println(&quot;VM started&quot;);
        eventSet.resume();
    } else if (event instanceof ClassPrepareEvent) {
        ClassPrepareEvent classPrepareEvent = (ClassPrepareEvent) event;
        String mainClassName = classPrepareEvent.referenceType().name();
        if (mainClassName.equals(&quot;com.ibm.jdi.test.HelloWorld&quot;)) {
            System.out.println(&quot;Class &quot; + mainClassName
                    + &quot; is already prepared&quot;);
        }
        if (true) {
            // Get location
            ReferenceType referenceType = prepareEvent.referenceType();
            List locations = referenceType.locationsOfLine(10);
            Location location = (Location) locations.get(0);
 
            // Create BreakpointEvent
            BreakpointRequest breakpointRequest = eventRequestManager
                    .createBreakpointRequest(location);
            breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
            breakpointRequest.enable();
        }
        eventSet.resume();
    } else if (event instanceof BreakpointEvent) {
        System.out.println(&quot;Reach line 10 of com.ibm.jdi.test.HelloWorld&quot;);
        BreakpointEvent breakpointEvent = (BreakpointEvent) event;
        ThreadReference threadReference = breakpointEvent.thread();
        StackFrame stackFrame = threadReference.frame(0);
        LocalVariable localVariable = stackFrame
                .visibleVariableByName(&quot;str&quot;);
        Value value = stackFrame.getValue(localVariable);
        String str = ((StringReference) value).value();
        System.out.println(&quot;The local variable str at line 10 is &quot; + str
                + &quot; of &quot; + value.type().name());
        eventSet.resume();
    } else if (event instanceof VMDisconnectEvent) {
        vmExit = true;
    } else {
        eventSet.resume();
    }
}
</code></pre>
<p>最后列出了以上程序的运行结果:</p>
<pre><code class="language-java">VM started
Class com.ibm.jdi.test.HelloWorld is already prepared
Reach line 10 of com.ibm.jdi.test.HelloWorld
The local variable str at line 10 is Hello world! of java.lang.String
</code></pre>
<hr>]]></description>
      <author>夂夂鱼</author>
      <guid>article-12</guid>
      <pubDate>Mon, 27 Apr 2026 11:30:18 +0000</pubDate>
    </item>
    <item>
      <title>Java NIO</title>
      <link>https://dyzmj.top/posts/nio</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/nio">https://dyzmj.top/posts/nio</a></p></blockquote><h2>Java NIO概述</h2>
<p>原英文地址：<a href="http://tutorials.jenkov.com/java-nio/index.html">Java NIO</a></p>
<p>JavaNIO 由以下几个部分组成：</p>
<ul>
<li>Channel</li>
<li>Buffer</li>
<li>Selector</li>
</ul>
<p>虽然 Java NIO 中除此之外还有很多的类和组件，但在我看来，<code>Channel</code>、<code>Buffer</code> 和 <code>Selector</code> 构成了核心的API。其他组件，如 <code>Pipe</code> 和 <code>FileLock</code>，只不过是与三个核心组价共同使用的工具类。因此，在概述中我将集中在这三个组件上，其他组件会在单独的章节中讲到。</p>
<h3>Channel 和 Buffer</h3>
<p>基本上，所有的 IO 在 NIO 中都从一个 <code>Channel</code> 开始。<code>Channel</code> 有点像流，数据可以从 <code>Channel</code> 读到 <code>Buffer</code> 中，也可以从 <code>Buffer</code> 写到 <code>Channel</code> 中。这里有个图示：</p>
<p><img src="https://img.dyzmj.top/img/image-20211222203115225.png" alt="image-20211222203115225"></p>
<p><code>Channel</code> 和 <code>Buffer</code> 有好几种类型，下面是 Java NIO 中的一些主要 <code>Channel</code> 实现：</p>
<ul>
<li>FileChannel</li>
<li>DatagramChannel</li>
<li>SocketChannel</li>
<li>ServerSocketChannel</li>
</ul>
<p>如上，这些通道覆盖了 <code>UDP</code> 和 <code>TCP</code> 网络 IO，以及文件 IO。</p>
<p>以下是 Java NIO 里关键的 Buffer 实现：</p>
<ul>
<li>ByteBuffer</li>
<li>CharBuffer</li>
<li>DoubleBuffer</li>
<li>FloatBuffer</li>
<li>IntBuffer</li>
<li>LongBuffer</li>
<li>ShortBuffer</li>
</ul>
<p>这些 Buffer 覆盖了能通过 IO 发送的基本数据类型：<code>byte</code>，<code>short</code>，<code>int</code>，<code>long</code>，<code>float</code>，<code>double</code> 和 <code>char</code></p>
<p>Java NIO 还有个 <code>MappedByteBuffer</code> ，用于表示内存映射文件，这里不再详述。</p>
<h3>Selector</h3>
<p><code>Selector</code> 允许单线程处理多个 <code>Channel</code>。如果你的应用打开了多个连接（通道），但每个连接的流量都很低，使用 <code>Selector</code> 就会很方便，例如在一个聊天服务器中。</p>
<p>这是一个单线程中使用一个 <code>Selector</code> 处理3个 <code>channel</code> 的图示：</p>
<p><img src="https://img.dyzmj.top/img/image-20211222204549019.png" alt="image-20211222204549019"></p>
<p>要使用 <code>Selector</code>，得向 <code>Selector</code> 注册 <code>Channel</code>，然后调用它的 <code>select()</code> 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回，线程就可以处理这些事件，事件的例子如有新连接进来、数据接收等。</p>
<h2>Channel 通道</h2>
<p>Java NIO 的通道类似于流，但有有些不同：</p>
<ul>
<li>既可以从通道中读取数据，又可以写数据到通道，但流的读写通常是单向的。</li>
<li>通道可以异步的读写。</li>
<li>通道中的数据总是要先读到一个 <code>Buffer</code>，或者总要从一个 <code>Buffer</code> 中写入。</li>
</ul>
<p>正如上面所说，从通道读取数据到缓冲区，从缓冲区写入数据到通道。如下图所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20211222203115225.png" alt="image-20211222203115225"></p>
<h3>Channel 的实现</h3>
<p>这些是 Java NIO 中最重要的通道的实现：</p>
<ul>
<li>FileChannel	-- 从文件中读写流</li>
<li>DatagramChannel  -- 能通过 UDP 读写网络中的数据</li>
<li>SocketChannel  -- 能通过 TCP 读写网络中的数据</li>
<li>ServerSocketChannel  -- 可以监听新进来的 TCP 连接，想WEB服务器那样。对每一个新进来的连接都会创建一个 SocketChannel</li>
</ul>
<h3>基本的 Channel 示例：</h3>
<p>下面是一个使用 <code>FileChannel</code> 读取数据到 <code>Buffer</code> 中的示例：</p>
<pre><code class="language-java">@Slf4j
public class FileChannelDemo {
    public static void main(String[] args) {
        try (RandomAccessFile accessFile = new RandomAccessFile(&quot;file.data&quot;, &quot;rw&quot;)) {
            FileChannel inChannel = accessFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(48);
            int bytesRead = inChannel.read(buf);
            while (bytesRead != -1) {
                log.info(&quot;Read &quot; + bytesRead);
                buf.flip();
                while (buf.hasRemaining()) {
                    log.info((char) buf.get() + &quot;&quot;);
                }
                buf.clear();
                bytesRead = inChannel.read(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<!-- raw HTML omitted -->
<p>注意 <code>buf.filp()</code> 的调用，首先读取数据到 <code>Buffer</code>，然后反转 <code>Buffer</code>，接着再从 <code>Buffer</code> 中去读数据。</p>
<h2>Buffer</h2>
<p>Java NIO 中的 <code>Buffer</code> 用于和 NIO 通道进行交互，数据是从通道读入缓冲区，从缓冲区写入到通道中。</p>
<p>缓冲区本质上是一块可以写入数据，然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象，并提供一组方法，用来方便的访问该块内存。</p>
<h3>Buffer 的基本用法</h3>
<p>使用 Buffer 读写数据一般遵循一下四个步骤：</p>
<ol>
<li>写入数据到 <code>Buffer</code></li>
<li>调用 <code>flip()</code> 方法</li>
<li>从 <code>Buffer</code> 中读取数据</li>
<li>调用 <code>clear()</code> 方法 或者 <code>compact()</code> 方法</li>
</ol>
<p>当向 <code>Buffer</code> 写入数据时，<code>Buffer</code> 会记录写下了多少数据。一旦要读取数据，需要通过 <code>flip()</code> 方法将 <code>Buffer</code> 从写模式切换到读模式。在读模式下，可以读取之前写入到 <code>Buffer</code> 的所有数据。</p>
<p>一旦读取完了所有的数据，就需要清空缓冲区，让它可以再次被写入。有两个方式能清空缓冲区：调用 <code>clear()</code> 或 <code>compact()</code> 方法。<code>clear()</code> 方法会清空整个缓冲区。<code>compact()</code> 方法只会清除已经读过的数据。任何未读的数据都被移动到缓冲区，新写入的数据将被放到缓冲区未读数据的后面。</p>
<h3>Buffer 的 capacity，position 和 limit</h3>
<p>为了理解 Buffer 的工作原理，需要熟悉它的三个属性：</p>
<ul>
<li>capacity</li>
<li>position</li>
<li>limit</li>
</ul>
<p><code>position</code> 和 <code>limit</code> 的含义取决于 <code>Buffer</code> 处在读模式还是写模式。不管 <code>Buffer</code> 处在什么模式，<code>capacity</code> 的含义总是一样的。</p>
<p>这里有一个关于 <code>capacity</code>，<code>position</code> 和 <code>limit</code> 在读写模式中的说明，详细的解释在插图后面：</p>
<p><img src="https://img.dyzmj.top/img/image-20211227152110589.png" alt="image-20211227152110589"></p>
<h3>capacity</h3>
<p>作为一个内存块，<code>Buffer</code> 有一个固定的大小值，只能往里面写 <code>capacity</code> 个 <code>byte</code>、<code>long</code>、<code>char</code> 等类型，需要将其清空（通过读数据或者清除数据）才能继续写数据。</p>
<h3>position</h3>
<p>当写数据到 <code>Buffer</code> 中时，<code>position</code> 表示当前的位置。初始的 <code>position</code> 值为 0。当一个<code>byte</code>、<code>long</code> 等数据写到 <code>Buffer</code> 后，<code>position</code> 会向前移动到下一个可插入数据的 <code>Buffer</code> 单元。<code>position</code> 最大可为 <code>capacity - 1</code>。</p>
<p>当读取数据时，也是从某个特定位置读。当将 <code>Buffer</code> 从写模式切换到读模式，<code>position</code> 会被重置为0，当从 <code>Buffer</code> 的 <code>position</code> 除读取数据时，<code>position</code> 向前移动到下一个可读的位置。</p>
<h3>limit</h3>
<p>在写模式下，<code>Buffer</code> 的 <code>limit</code> 表示最多能往 <code>Buffer</code> 里写多少数据。写模式下，<code>limit</code> 等于 <code>Buffer</code> 的 <code>capacity</code>。</p>
<p>当切换 <code>Buffer</code> 到读模式时，<code>limit</code> 表示最多能读到多少数据。因此，当切换 ·到读模式时，<code>limit</code> 会被设置成写模式下的 <code>position</code> 值。换句话说，就是能读取到之前写入的所有数据（limit 被设置成已写数据的数量，这个值在写模式下就是 position）。</p>
<h3>Buffer 的类型</h3>
<p>Java NIO 有以下 Buffer 类型：</p>
<ul>
<li>ByteBuffer</li>
<li>MappedByteBuffer</li>
<li>CharBuffer</li>
<li>DoubleBuffer</li>
<li>FloatBuffer</li>
<li>IntBuffer</li>
<li>LongBuffer</li>
<li>ShortBuffer</li>
</ul>
<h3>Buffer 的分配</h3>
<p>要想获得一个 <code>Buffer</code> 对象首先要进行分配。每一个 <code>Buffer</code> 类都有一个 <code>allocate</code> 方法。下面是分配一个48字节 <code>capacity</code> 的 <code>ByteBuffer</code>的例子。</p>
<pre><code class="language-java">ByteBuffer buf = ByteBuffer.allocate(48);
</code></pre>
<p>这是分配一个可存储1024个字符的 <code>CharBuffer</code>：</p>
<pre><code class="language-java">CharBuffer buf = CharBuffer.alloacte(1024);
</code></pre>
<h3>向 Buffer 中写数据</h3>
<p>写数据到 <code>Buffer</code> 有两种方式：</p>
<ul>
<li>从 <code>Channel</code> 写到 <code>Buffer</code></li>
<li>通过 <code>Buffer</code> 的 <code>put()</code> 方法写到 <code>Buffer</code>里。</li>
</ul>
<p>从 <code>Channel</code> 写到 <code>Buffer</code> 的例子：</p>
<pre><code class="language-java">int bytesRead = inChannel.read(buf);
</code></pre>
<p>通过 <code>put()</code> 方法写 <code>Buffer</code> 的例子：</p>
<pre><code class="language-java">buf.put(127);
</code></pre>
<p>put 方法有很多版本，允许以不同的方式把数据写入到 <code>Buffer</code> 中。例如，写到一个指定的位置，或者把一个字节数组写入到 <code>Buffer</code>。</p>
<h3>flip() 方法</h3>
<p><code>flip()</code> 方法将 <code>Buffer</code> 从写模式切换到读模式。调用 <code>flip()</code> 方法会将 <code>position</code> 设置为0，并将 <code>limit</code> 设置成之前 <code>position</code> 的值。</p>
<p>换句话说，<code>position</code> 现在用于标记读的位置，<code>limit</code> 表示之前写进去了多少个 <code>byte</code>、<code>char</code> 等，也就是现在能读取多少个<code>byte</code>、<code>char</code>等。</p>
<h3>从 Buffer 中读取数据</h3>
<p>从 <code>Buffer</code> 中读取数据有两种方式：</p>
<ol>
<li>从 <code>Buffer</code> 读取数据到 <code>Channel</code>。</li>
<li>使用 <code>get()</code> 方法从 <code>Buffer</code> 中读取数据。</li>
</ol>
<p>从 <code>Buffer</code> 读取数据到 <code>Channel</code> 的例子：</p>
<pre><code class="language-java">int	byteWritten = inChannel.write(buf);
</code></pre>
<p>使用 <code>get()</code> 方法从 <code>Buffer</code> 中读取数据的例子：</p>
<pre><code class="language-java">byte aByte = buf.get();
</code></pre>
<p><code>get()</code> 方法有很多个版本，允许你以不同的方式从 <code>Buffer</code> 中读取数据。例如，从指定的 <code>position</code> 读取，或者从 <code>Buffer</code> 中读取数据到字节组。</p>
<h3>rewind() 方法</h3>
<p><code>Buffer.rewind()</code> 方法将 <code>position</code> 设回 0，所以你可以重读 <code>Buffer</code> 中的所有数据。<code>limit</code> 保持不变，仍然表示能从 <code>Buffer</code> 中读取多少个元素（<code>byte</code>、<code>char</code>等）。</p>
<h3>clear() 与 compact() 方法</h3>
<p>一旦读完 <code>Buffer</code> 中的数据，需要让 <code>Buffer</code> 准备好再次被写入。可以通过 <code>clear()</code> 或 <code>compact()</code> 方法来完成。</p>
<p>如果调用的 <code>clear()</code> 方法，<code>position</code> 将被设回 0，limit 被设置成 <code>capacity</code> 的值。换句话说，<code>Buffer</code> 被清空了，<code>Buffer</code> 中的数据并未被清除，只是这些标记告诉我们可以从哪里开始往 <code>Buffer</code> 里写数据。</p>
<p>如果 <code>Buffer</code> 中有一些未读的数据，调用 <code>clear()</code> 方法，数据将 “被遗忘”，意味着不再有任何标记会告诉你哪些数据被读过，哪些还没有。</p>
<p>如果 <code>Buffer</code> 中仍有未读的数据，且后续还需要这些数据，但此时想要先写些数据，那么使用 <code>compact()</code> 方法。</p>
<p><code>compact()</code> 方法将所有未读的数据拷贝到 <code>Buffer</code> 起始处，然后将 <code>position</code> 设到最后一个未读元素的后面。<code>limit</code> 属性依然像 <code>clear()</code> 方法一样，设置成 <code>capacity</code>。现在 <code>Buffer</code> 准备好写数据了，但是不会覆盖之前未读的数据。</p>
<h3>mark() 与 reset() 方法</h3>
<p>通过调用 <code>Buffer.mark()</code> 方法，可以标记 <code>Buffer</code> 中的一个特定 <code>position</code>，之后可以通过调用 <code>Buffer.reset()</code> 方法恢复到这个 <code>position</code>。例如：</p>
<pre><code class="language-java">buffer.mark();
// call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();	// set position back to mark.
</code></pre>
<h3>equals() 与 compareTo() 方法</h3>
<p>可以使用 <code>equals()</code> 和 <code>compareTo</code> 方法比较两个 <code>Buffer</code>。</p>
<p><strong>equals()</strong></p>
<p>当满足下列条件时，表示两个 <code>Buffer</code> 相等：</p>
<ol>
<li>有相同的类型（<code>byte</code>、<code>char</code>、<code>int</code> 等）。</li>
<li><code>Buffer</code> 中剩余的 <code>byte</code>、<code>char</code> 等元素的个数相等。</li>
<li><code>Buffer</code> 中所有剩余的 <code>byte</code>、<code>char</code> 等元素都相同。</li>
</ol>
<p>如上，<code>equals</code> 只是比较 <code>Buffer</code> 的一部分，不是每一个在它里面的元素都比较。实际上，它只比较 <code>Buffer</code> 中的剩余元素。</p>
<p><strong>compareTo() 方法</strong></p>
<p><code>compareTo()</code> 方法比较两个 <code>Buffer</code> 元素的剩余元素（byte、char 等），如果满足下列条件，则认为一个 <code>Buffer</code> “小于” 另一个 <code>Buffer</code>：</p>
<ol>
<li>第一个不相等的元素小于另一个 <code>Buffer</code> 中对应的元素。</li>
<li>所有元素都相等，但第一个 <code>Buffer</code> 比另一个先耗尽（第一个 <code>Buffer</code> 的元素个数比另一个少）。</li>
</ol>
<p>注：剩余元素是从 <code>position</code> 到 <code>limit</code> 之间的元素。</p>
<h2>Scatter / Gather</h2>
<p>Java NIO 开始支持 <code>scatter/gather</code>，~ 用于描述从 <code>Channel</code> 中读取或者写入到 <code>Channel</code> 的操作。</p>
<p>分散 (scatter) 从 <code>Channel</code> 中读取是指在读操作时将读取的数据写入多个 <code>buffer</code> 中。因此，<code>Channel</code> 将从 <code>Channel</code> 中读取的数据 “分散（scatter）”到多个 <code>Buffer</code> 中。</p>
<p>聚集（gather）写入 <code>Channel</code> 是指在写操作时将多个 <code>buffer</code> 的数据写入同一个 <code>Channel</code>，因此，<code>Channel</code> 将多个 <code>Buffer</code> 中的数据 “聚集（gather）”后发送到 <code>Channel</code>。</p>
<p><code>scatter/gather</code> 经常用于需要将传输的数据分开处理的场合，例如传输一个由消息头和消息体组成的消息，你可能会将消息头和消息体分散到不同的 <code>Buffer</code> 中，这样可以方便的处理消息头和消息体。</p>
<h3>Scattering Reads</h3>
<p>Scattering Reads 是指数据从一个 <code>Channel</code> 读取到多个 <code>Buffer</code> 中。如下图描述：</p>
<p><img src="https://img.dyzmj.top/img/image-20220105134950005.png" alt="image-20220105134950005"></p>
<p>代码示例如下：</p>
<pre><code class="language-java">ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header, body};
// read data from channel
channel.read(bufferArray);
</code></pre>
<p>注意 <code>Buffer</code> 首先被插入到数组，然后再将数组作为 <code>channel.read()</code> 的输入参数。<code>read()</code> 方法按照 <code>Buffer</code> 在数组中的顺序将从 <code>Channel</code> 中读取的数据写入到 <code>Buffer</code>，当一个 <code>Buffer</code> 被写满后，<code>Channel</code> 紧接着向另一个 <code>Buffer</code> 中写。</p>
<p>Scattering Reads 在移动下一个 Buffer 前，必须先填满当前的 Buffer，这也意味着它不适用与动态消息（消息大小不固定）。换句话说，如果存在消息头和消息体，消息头必须完成填充（例如128byte），Scattering Reads 才能正常工作。</p>
<h3>Gathering Writes</h3>
<p>Gathering Writes 是指数据从多个 Buffer 写入到同一个 Channel，如下图描述：</p>
<p><img src="https://img.dyzmj.top/img/image-20220105135757578.png" alt="image-20220105135757578"></p>
<p>代码示例如下：</p>
<pre><code class="language-java">ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header, body};
// write data into buffers
channel.write(bufferArray);
</code></pre>
<p>Buffers 数组是 write() 方法的入参，write() 方法会按照 Buffer 在数组中的顺序，将数据写入到 Channel，注意只有 position 和 limit 之前的数据才会被写入。因此，如果一个 Buffer 的容量为 128byte，但是仅仅包含 58byte 的数据，那么这 58byte 的数据将被写入到 Channel 中。因此与 Scattering Reads 相反，Gathering Writes 能较好的处理动态消息。</p>
<h2>通道之前的数据传输</h2>
<p>在 Java NIO 中，如果两个通道中有一个是 FileChannel，那么可以直接将数据从一个 Channel 传输到另外一个 Channel。</p>
<h3>transferFrom()</h3>
<p>FileChannel 的 transferFrom() 方法可以将数据从源通道传输到 FileChannel中（注：这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中）。下面是一个简单的例子：</p>
<pre><code class="language-java">RandomAccessFile fromFile = new RandomAccessFile(&quot;fromFile.txt&quot;, &quot;rw&quot;);
FileChannel fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile(&quot;toFile.txt&quot;, &quot;rw&quot;);
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count );
</code></pre>
<p>方法的输入参数 position 表示从 position 处开始向目标文件写入数据，count 表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节，则所传输的字节数要小于请求的字节数。</p>
<p>此外要注意，在 SocketChannel 的实现中，SocketChannel 只会传输此刻准备好的数据（可能不足 count 字节）。因此，SocketChannel 可能不会将请求的所有数据（count 个字节）全部传输到 FileChannel 中。</p>
<h3>transferTo()</h3>
<p>transferTo() 方法将数据从 FileChannel 传输到其他的 Channel 中，下面是一个简单的例子：</p>
<pre><code class="language-java">RandomAccessFile fromFile = new RandomAccessFile(&quot;fromFile.txt&quot;, &quot;rw&quot;);
FileChannel fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile(&quot;toFile.txt&quot;, &quot;rw&quot;);
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);
</code></pre>
<p>是不是发现这个例子和前面的那个例子特别相似？除了调用方法的 FileChannel 对象不一样外，其他的都一样。</p>
<p>上面所说的关于 SocketChannel 的问题在 transferTo() 方法中同样存在，SocketChannel 会一直传输数据直到目标 Buffer 被填满。</p>
<h2>Selector</h2>
<p>Selector（选择器）是 Java NIO 中能够检测一到多个 NIO 通道，并能够知晓通道是否为诸如读写事件做好准备的组件。这样，一个单独的线程可以管理多个 Channel，从而管理多个网络连接。</p>
<h3>为什么使用 Selector</h3>
<p>仅用单个线程来处理多个 Channel 的好处是，只需要更少的线程来处理通道。事实上，可以只用一个线程处理所有的通道。对于操作系统来说，线程之间上下文切换的开销很大，而且每个线程都要占用系统的一些资源（如内存）。因此使用的线程越少越好。</p>
<p>但是，现代的操作系统和 CPU 在多任务方面表现的越来越好，所以多线程的开销随着时间的推移，变得越来越小。实际上，如果一个 CPU 有多个内核，不使用多任务可能是在浪费 CPU 的能力。在这里，只要知道使用 Selector 能够处理多个通道就足够了。</p>
<p>下面是单线程使用一个 Selector 处理3个 Channel 的示例图：</p>
<p><img src="https://img.dyzmj.top/img/image-20220105151110846.png" alt="image-20220105151110846"></p>
<h3>Selector 的创建</h3>
<p>通过调用 Selector.open() 方法创建一个 Selector，如下：</p>
<pre><code class="language-java">Selector selector = Selector.open();	
</code></pre>
<h3>向 Selector 注册通道</h3>
<p>为了将 Channel 和 Selector 配合使用，必须将 Channel 注册到 Selector 上。通过 SelectableChannel.register() 方法来实现，如下：</p>
<pre><code class="language-java">channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
</code></pre>
<p>与 Selector 一起使用时，Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用，因为 FileChannel 不能切换到非阻塞模式。而Socket Channel 可以这样使用。</p>
<p>注意 register() 方法的第二个参数，这是一个 “interest 集合”，意思是在通过 Selector 监听 Channel 时对什么事件感兴趣。可以监听一下四种不同类型的事件：</p>
<ol>
<li>Connect</li>
<li>Accept</li>
<li>Read</li>
<li>Write</li>
</ol>
<p>通道触发了一个时间意思是该事件已经就绪。所以，某个 Channel 成功连接到另一个服务器称为 “连接就绪”；一个 Server Socket Channel 准备好接收新进入的连接称为 “连接就绪”；一个有数据可读的通道可以说是 “读就绪”；等待写数据的通道可以说是 “写就绪”。</p>
<p>这四种事件用 SelectionKey 的四个常量来表示：</p>
<ol>
<li>SelectionKey.OP_CONNECT</li>
<li>SelectionKey.OP_ACCEPT</li>
<li>SelectionKey.OP_READ</li>
<li>SelectionKey.OP_WRITE</li>
</ol>
<p>如果对不止一种事件感兴趣，那么可以用 “ | (位或) ” 操作符将常量连接起来，如下：</p>
<pre><code class="language-java">int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
</code></pre>
<h3>SelectionKey</h3>
<p>在上一小节中，当向 Selector 注册 Channel 时，register() 方法会返回一个 SelectionKey 对象。这个对象包含了一些你感兴趣的属性：</p>
<ul>
<li>interest 集合</li>
<li>ready 集合</li>
<li>Channel</li>
<li>Selector</li>
<li>附加的对象（可选）</li>
</ul>
<h4>interest 集合</h4>
<p>就像 [向 Selector 注册通道](### 向 Selector 注册通道)  一节中所描述的，interest 集合是你所选择的感兴趣的事件集合。可以通过 SelectionKey 读写 interest 集合，像这样：</p>
<pre><code class="language-java">int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = SelectionKey.OP_ACCEPT  == (interests &amp; SelectionKey.OP_ACCEPT);
boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests &amp; SelectionKey.OP_CONNECT);
boolean isInterestedInRead    = SelectionKey.OP_READ    == (interests &amp; SelectionKey.OP_READ);
boolean isInterestedInWrite   = SelectionKey.OP_WRITE   == (interests &amp; SelectionKey.OP_WRITE);
</code></pre>
<p>可以看到，用 “&amp;（位与）”操作 interest 集合和给定的 SelectionKey 常量，可以确定某个确定的事件是否在 interest 集合中。</p>
<h4>Ready 集合</h4>
<p>ready 集合是通道已经准备就绪的操作的集合。在一次选择（Selection）之后，你会首先访问这个 ready set。Selection 将在下一小节进行解释。可以这样访问 Ready 集合：</p>
<pre><code class="language-java">int readySet = selectionKey.readyOps();
</code></pre>
<p>可以用像检测 interest 集合那样的方法，来检测 Channel 中什么事件或者操作已经就绪。但是，也可以使用一下四个方法，它们都会返回一个布尔类型：</p>
<pre><code class="language-java">selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
</code></pre>
<h4>Channel + Selector</h4>
<p>从 SelectionKey 访问 Channel 和 Selector 很简单，如下：</p>
<pre><code class="language-java">Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
</code></pre>
<h4>附加的对象</h4>
<p>可以将一个对象或者更多的信息附着到 SelectionKey 上，这样就能很方便的识别某个给定的通道。例如，可以附加与通道一起使用的 Buffer，或者是包含聚集数据的某个对象。使用方法如下：</p>
<pre><code class="language-java">selectionKey.attach(theObject);
Object attachObj = selectionKey.attachment();
</code></pre>
<p>还可以在用 register() 方法想 Selector 注册 Channel 的时候附加对象，如：</p>
<pre><code class="language-java">SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
</code></pre>
<h3>通过 Selector 选择通道</h3>
<p>一旦向 Selector 注册了一个或多个通道，就可以调用几个重载的 select() 方法。这些方法返回你所感兴趣的事件（如<code>连接</code>、<code>接收</code>、<code>读或写</code>）已经准备就绪的那些通道。换句话说，如果你对 <code>读就绪</code> 的通道感兴趣，select() 方法会返回读事件已经就绪的那些通道。</p>
<p>下面是 select() 方法：</p>
<ul>
<li><code>int select()</code></li>
<li><code>int select(long timeout)</code></li>
<li><code>int selectNow()</code></li>
</ul>
<p><code>select()</code> 阻塞到至少有一个通道在你注册的事件上就绪了。</p>
<p><code>selct(long timeout)</code> 和 <code>select()</code> 一样，除了最长会阻塞 <code>timeout</code> 毫秒（参数）。</p>
<p><code>selectNow()</code> 不会阻塞，不管什么通道就绪都会立刻返回（注：此方法执行非阻塞的选择操作。如果自从前一次选择操作后，没有通道变成可选择的，则此方法直接返回零）。</p>
<p><code>select()</code> 方法返回的 int 值表示有多少通道已经就绪。即自上次调用 <code>select()</code> 方法后有多少通道变成就绪状态。如果调用了 <code>select()</code> 方法，因为有一个通道变成就绪状态，返回了1，若再次调用 <code>select()</code> 方法，如果另一个通道就绪了，它会再次返回1。如果对第一个就绪的 Channel 没有做任何操作，现在就有两个就绪的通道，但在每次 <code>select()</code> 方法调用之间，只有一个通道就绪了。</p>
<h4>selectedKeys()</h4>
<p>一旦调用了 <code>select()</code> 方法，并且返回值表明有一个或更多个通道就绪了，然后可以通过调用 selector 的 <code>selectedKeys()</code> 方法，访问 <code>已选择键集（selected key set）</code> 中的就行通道，如下所示：</p>
<pre><code class="language-java">Set selectedKeys = selector.selectedKeys();
</code></pre>
<p>当向 Selector 注册 Channel 时，Channel.register() 方法会返回一个 SelectionKey 对象。这个对象代表了注册到该 Selector 的通道。可以通过 SelectionKey 的 <code>selectedKeySet()</code> 方法访问这些对象。</p>
<p>可以遍历这个已选择的键集合来访问就绪的通道，如下：</p>
<pre><code class="language-java">Set&lt;SelectionKey&gt; selectedKeys = selector.selectedKeys();
Iterator&lt;SelectionKey&gt; keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()){
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()){
    // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()){
    // a connection was established with a remote server.
    } else if (key.isReadable()){
    // a channel is ready for reading.
    } else if (key.isWritable()){
    // a channel is ready for writing.
    }
    keyIterator.remove();
}
</code></pre>
<p>这个循环遍历已选择键集中的每个键，并检测各个键所对应的通道的就绪事件。</p>
<p>注意每次迭代末尾的 <code>keyIterator.remove()</code> 调用。Selector 不会自己从已选择键集中移除 SelectionKey 实例，必须在处理完通道时自己移除。下次该通道变成就绪时，Selector 会再次将其放入已选择键集中。</p>
<p><code>SelectionKey.channel()</code> 方法返回的通道需要转换成你要处理的类型，如 ServerSocketChannel 或 SocketChannel 等。</p>
<h4>wakeup()</h4>
<p>某个线程调用 <code>select()</code> 方法后阻塞了，即使没有通道已经就绪，也有办法让其从 <code>select()</code> 方法返回。只要让其他线程在第一个线程调用 <code>select()</code> 方法的那个对象上调用 <code>Selector.wakeup()</code> 方法即可。阻塞在 <code>select()</code> 方法上的线程会立马返回。</p>
<p>如果有其他线程调用了 <code>wakeup()</code> 方法，当前没有线程阻塞在 <code>select()</code> 方法上，那么下个调用 <code>select()</code> 方法的线程会立即 “醒来（wake up）”。</p>
<h4>close()</h4>
<p>用完 Selector 后调用其 <code>close()</code> 方法会关闭该 Selector ，且使注册到该 Selector 上的所有 SelectionKey 实例无效。通道本身并不会关闭。</p>
<h3>完整的示例</h3>
<p>这里有一个完整的示例，打开一个 Selector，把一个通道注册到这个 Selector 上，然后监控这个 Selector 上的事件（连接、接受、读、写）是否就绪。</p>
<pre><code class="language-java">import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;

@Slf4j
public class SocketChannelDemo {
    public static void main(String[] args) {
        try (ServerSocketChannel serverSocket = ServerSocketChannel.open()) {
            // 创建一个 ServerSocketChannel
            // 设置为非阻塞
            serverSocket.configureBlocking(false);
            // 绑定IP和端口
            int port = 32470;
            serverSocket.socket().bind(new InetSocketAddress(port));
            // 创建一个选择器
            Selector selector = Selector.open();
            // 将通道注册在选择器中并监听 OP_ACCEPT 事件（如果有客户端发来连接请求，则该键在 select() 后被选中）
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);

            log.info(&quot;服务端已启动！端口：{}&quot;, port);
            log.info(&quot;===========================================&quot;);
            // 轮询服务
            while (true) {
                // 选择准备好的事件
                selector.select();
                // 已选择的键集
                Iterator&lt;SelectionKey&gt; iterator = selector.selectedKeys().iterator();
                // 处理已选择键集事件
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 处理掉后将键移除，避免重复消费（因为下次选择后，还在已选择键集中）
                    iterator.remove();

                    // 处理连接请求
                    if (key.isAcceptable()) {
                        // 处理请求
                        SocketChannel socket = serverSocket.accept();
                        socket.configureBlocking(false);
                        // 注册 READ事件，监听客户端发送的消息
                        socket.register(selector, SelectionKey.OP_READ);
                        // keys 为所有键，除掉serverSocket注册的键就是已连接 socketChannel的数量
                        String message = &quot;连接成功，你是第 &quot; + (selector.keys().size() - 1) + &quot; 个用户&quot;;
                        // 向客户端发送消息
                        socket.write(ByteBuffer.wrap(message.getBytes()));
                        InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteAddress();
                        // 输入客户端地址
                        log.info(&quot;当前时间: &quot; + LocalDateTime.now() + &quot;\t&quot; + remoteAddress.getHostString() + &quot;:&quot; + remoteAddress.getPort());
                        log.info(&quot;客户端已连接&quot;);
                        log.info(&quot;===========================================&quot;);
                    }
                    if (key.isReadable()) {
                        SocketChannel socket = (SocketChannel) key.channel();
                        InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteAddress();
                        // 输入客户端地址
                        log.info(&quot;当前时间: &quot; + LocalDateTime.now() + &quot;\t&quot; + remoteAddress.getHostString() + &quot;:&quot; + remoteAddress.getPort());
                        ByteBuffer bf = ByteBuffer.allocate(1024 &lt;&lt; 2);
                        int len;
                        byte[] res = new byte[1024 &lt;&lt; 2];
                        // 捕获异常，因为客户端关闭后会发送FIN报文，会触发read事件，但连接已关闭，此时read() 会产生异常
                        try {
                            while ((len = socket.read(bf)) != 0) {
                                bf.flip();
                                bf.get(res, 0, len);
                                System.out.println(new String(res, 0, len));
                                bf.clear();
                            }
                            log.info(&quot;===========================================&quot;);
                        } catch (IOException e) {
                            // 客户端关闭了
                            key.cancel();
                            socket.close();
                            log.warn(&quot;客户端已断开&quot;);
                            log.warn(&quot;===========================================&quot;);
                        }
                    }
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
            log.warn(&quot;服务端异常，即将关闭.......&quot;);
            log.warn(&quot;===========================================&quot;);
        }

    }
}

</code></pre>
<h2>FileChannel</h2>
<p>Java NIO 中的 <code>FileChannel</code> 是一个连接到文件的通道。可以通过文件通道读写文件。<code>FileChannel</code> 无法设置为非阻塞模式，它总是运行在阻塞模式下。</p>
<h3>打开 FileChannel</h3>
<p>在使用 <code>FileChannel</code> 之前，必须先打开它，但是，我们无法直接打开一个 <code>FileChannel</code> ，需要通过使用 <code>InputStream</code>、<code>OutputStream</code> 或 <code>RandomAccessFile</code> 来获取一个 <code>FileChannel</code> 实例。下面是通过 <code>RandomAccessFile</code> 打开 <code>FileChannel</code> 的示例：</p>
<pre><code class="language-java">RandomAccessFile accessFile = new RandomAccessFile(&quot;file.txt&quot;, &quot;rw&quot;))；
FileChannel fileChannel = accessFile.getChannel();
</code></pre>
<h3>从 FileChannel 读取数据</h3>
<p>要从 <code>FileChannel</code> 中读取数据可以调用 <code>read()</code> 方法其中的一种，下面是一个例子：</p>
<pre><code class="language-java">ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = fileChannel.read(buf);
</code></pre>
<p>首先、分配一个 <code>Buffer</code> ，从 <code>FileChannel</code> 中读取的数据将被读到 <code>Buffer</code>中。</p>
<p>然后调用 <code>FileChannel.read()</code> 方法，该方法将数据从 <code>FileChannel</code> 读取到 <code>Buffer</code> 中。<code>read()</code> 方法返回的 <code>int</code> 值表示了有多少字节被读取到了 <code>Buffer</code> 中。如果返回 -1，则表示到了文件末尾。</p>
<h3>向 FileChannel 写入数据</h3>
<p>使用 <code>FileChannel.write()</code> 方法向 <code>FileChannel</code> 写数据，该方法的参数是一个 <code>Buffer</code>，如下：</p>
<pre><code class="language-java">String newData = &quot;New String will be write to file... &quot; + System.currentTimeMillis();
        
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();

while (buffer.hasRemaining()){
    channel.write(buffer);
}
</code></pre>
<p>注意 <code>FileChannel.write()</code> 是在 <code>while</code> 循环中调用的。因为无法保证 <code>write()</code> 方法一次想 <code>FileChannel</code> 写入多少个字节，因此需要重复调用 <code>write()</code> 方法，直到 <code>Buffer</code> 中已经没有尚未写入通道的字节。</p>
<h3>关闭 FileChannel</h3>
<p>用完 <code>FileChannel</code> 后必须将其关闭，如下：</p>
<pre><code class="language-java">fileChannel.close();
</code></pre>
<h3>FileChannel 的 size() 方法</h3>
<p><code>FileChannel</code> 实例的 <code>size()</code> 方法将返回该实例所关联文件的大小。如：</p>
<pre><code class="language-java">long fileSize = fileChannel.size();
</code></pre>
<h3>FileChannel 的 truncate() 方法</h3>
<p>可以使用 <code>FileChannel.truncate()</code> 方法截取一个文件，截取文件时，将在给定的长度将它截断，超出长度后面的部分将被删除。如：</p>
<pre><code class="language-java">fileChannel.truncate(1024);
</code></pre>
<p>这个例子截取文件的前1024个字节。</p>
<h3>FileChannel 的 force() 方法</h3>
<p><code>FileChannel.force()</code> 方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑，操作系统会将数据缓存在内存中，所以无法保证写入到 <code>FileChannel</code> 里的数据一定会及时写到磁盘上。要保证这一点，需要调用 <code>force</code> 方法。</p>
<p><code>force()</code> 方法有一个布尔类型的参数，指明是否同时将文件元数据（权限信息等）写到磁盘上。</p>
<p>下面的例子同时将文件数据和元数据强制写到磁盘上：</p>
<pre><code class="language-java">fileChannel.force(true);
</code></pre>
<h2>SocketChannel</h2>
<p>Java NIO 中的 <code>SocketChannel</code> 是一个连接到 <code>TCP</code> 网络套接字的通道。可以通过一下2种方式创建：</p>
<ol>
<li>打开一个 <code>SocketChannel</code>并连接到互联网上的某台服务器。</li>
<li>一个新的连接到达 <code>ServerSocketChannel</code> 时，会创建一个 <code>SocketChannel</code>。</li>
</ol>
<h3>打开 SocketChannel</h3>
<p>下面是 <code>SocketChannel</code>的打开方式：</p>
<pre><code class="language-java">SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(&quot;127.0.0.1&quot;, 3247));
</code></pre>
<h3>关闭 SocketChannel</h3>
<p>当用完 <code>SocketChannel</code> 之后要调用 <code>SocketChannel.close()</code> 方法将其关闭。如：</p>
<pre><code class="language-java">socketChannel.close();
</code></pre>
<h3>从 SocketChannel 读取数据</h3>
<p>要从 <code>SocketChannel</code> 中读取数据可以调用 <code>read()</code> 方法其中的一种，下面是一个例子：</p>
<pre><code class="language-java">ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
</code></pre>
<p>首先、分配一个 <code>Buffer</code> ，从 <code>SocketChannel</code> 中读取的数据将被读到 <code>Buffer</code>中。</p>
<p>然后调用 <code>SocketChannel.read()</code> 方法，该方法将数据从 <code>SocketChannel</code> 读取到 <code>Buffer</code> 中。<code>read()</code> 方法返回的 <code>int</code> 值表示了有多少字节被读取到了 <code>Buffer</code> 中。如果返回 -1，则表示到了流的末尾（连接关闭了）。</p>
<h3>向 SocketChannel 写入数据</h3>
<p>使用 <code>SocketChannel.write()</code> 方法向 <code>SocketChannel</code> 写数据，该方法的参数是一个 <code>Buffer</code>，如下：</p>
<pre><code class="language-java">String newData = &quot;New String will be write to file... &quot; + System.currentTimeMillis();
        
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();

while (buffer.hasRemaining()){
    socketChannel.write(buffer);
}
</code></pre>
<p>注意 <code>SocketChannel.write()</code> 是在 <code>while</code> 循环中调用的。<code>write()</code> 方法无法保证能写多少个字节到 <code>SocketChannel</code> ，因此需要重复调用 <code>write()</code> 方法，直到 <code>Buffer</code> 中已经没有尚未写入通道的字节。</p>
<h3>非阻塞模式</h3>
<p>可以设置 <code>SocketChannel</code> 为非阻塞模式（non-blocking mode）。设置之后，就可以在异步模式下调用 <code>connect()</code>、<code>read()</code>、<code>write()</code> 方法了。</p>
<h4>connnet()</h4>
<p>如果 <code>SocketChannel</code> 在非阻塞模式下，此时调用 <code>connet()</code> 方法可能在连接之前就返回了。为了确定连接是否建立，可以调用 <code>finishConnect()</code> 方法，如下：</p>
<pre><code class="language-java">SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(&quot;127.0.0.1&quot;, 3247));

while(!socketChannel.finishConnect()){
    // wait, or do something else...
}
</code></pre>
<h4>write()</h4>
<p>非阻塞模式下，<code>write()</code> 方法在尚未写出任何内容时可能就返回了，所以需要在循环中调用。前面已经有例子，这里就不再赘述了。</p>
<h4>read()</h4>
<p>非阻塞模式下，<code>read()</code> 方法在在尚未读取到任何数据的时候就可能返回了，所以需要关注它的 <code>int</code> 返回值，它会告诉你读取了多少字节。</p>
<h3>非阻塞模式与选择器</h3>
<p>非阻塞模式与选择器搭配使用会工作的更好，通过将一个或多个 <code>SocketChanel</code> 注册到 <code>Selector</code> 上，可以询问选择器哪个通道已经准备好了读取、写入等。</p>
<h2>ServerSocketChannel</h2>
<p>Java NIO 中的 <code>ServerSocketChannel</code> 是一个可以监听新进来的 <code>TCP</code> 连接的通道，就像标准 IO 中的 <code>ServerSocket</code> 一样。</p>
<p>这里有一个例子：</p>
<pre><code class="language-java">ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(3247));
while(true){
    SocketChannel socket = serverSocket.accept();
    // do something with socketchannel
}
</code></pre>
<h3>打开 ServerSocketChannel</h3>
<p>通过调用 <code>ServerSocketChannel.open()</code> 方法来打开 <code>ServerSocketChannel</code>，如：</p>
<pre><code>ServerSocketChannel serverSocket = ServerSocketChannel.open();
</code></pre>
<h3>关闭 ServerSocketChannel</h3>
<p>通过调用 <code>ServerSocketChannel.close()</code> 方法来关闭 <code>ServerSocketChannel</code>，如：</p>
<pre><code class="language-java">serverSocketChannel.close()
</code></pre>
<h3>监听新进来的连接</h3>
<p>通过 <code>ServerSocketChannel.accept()</code> 方法监听新进来的连接。 当 <code>accept()</code> 方法返回的时候，它返回一个包含新进来的连接的 <code>SocketChannel</code>。因此，<code>accept()</code> 方法会一直阻塞到有新连接到达。</p>
<p>通常不会仅仅监听一个连接，在 <code>while</code> 循环中调用 <code>accept()</code> 方法，如下面的例子：</p>
<pre><code class="language-java">while(true){
    SocketChannel socket = serverSocket.accept();
    // do something with socketchannel
}
</code></pre>
<p>当然，也可以在 <code>while</code> 循环中使用除了 <code>true</code> 以外的其他的退出准则。</p>
<h3>非阻塞模式</h3>
<p><code>ServerSocketChannel</code> 可以设置成非阻塞模式。在非阻塞模式下，<code>accept()</code> 方法会立即返回，如果还没有新进来的连接，返回的将是 <code>null</code>。因此需要检查返回的 <code>SocketChannel</code> 是否为空。如下：</p>
<pre><code class="language-java">ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.configureBlocking(false);
serverSocket.socket().bind(new InetSocketAddress(3247));
while(true){
    SocketChannel socket = serverSocket.accept();
    if(socket != null) {
        // do something with socketChannel
    }
}
</code></pre>
<h2>DatagramChannel</h2>
<p>Java NIO 中的 <code>DatagramChannel</code> 是一个能收发 <code>UDP</code> 包的通道。因为 <code>UDP</code> 是无连接的网络协议，所以不能像其他通道那样读取和写入。它发送和接收的是数据包。</p>
<h3>打开 DatagramChannel</h3>
<p>下面是 <code>DatagramChannel</code> 的打开方式：</p>
<pre><code class="language-java">DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(3247));
</code></pre>
<h3>接收数据</h3>
<p>通过 <code>receive()</code> 方法从 <code>DatagramChannel</code> 接收数据，如：</p>
<pre><code class="language-java">ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
datagramChannel.receive(buf);
</code></pre>
<p><code>receive()</code> 方法会将接收到的数据包内容复制到指定的 <code>Buffer</code>，如果 <code>Buffer</code> 容不下收到的数据，多出的数据将被丢弃。</p>
<h3>发送数据</h3>
<p>通过 <code>send()</code> 方法从 <code>DatagramChannel</code> 发送数据，如：</p>
<pre><code class="language-java">String newData = &quot;New String will be write to file... &quot; + System.currentTimeMillis();
        
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();

int bytesSent = datagramChannel.send(buffer, new InetSocketAddress(&quot;127.0.0.1&quot;, 3247));
</code></pre>
<p>这个例子发送一串字符到本地服务器的 <code>UDP</code>端口 <code>3247</code> 上。因为服务端并没有监听这个端口，所以什么也不会发生，也不会通知你发出的数据包是否已收到，因为 <code>UDP</code> 在数据传输方面没有任何保证。</p>
<h3>连接到特定的地址</h3>
<p>可以将 <code>DatagramChannel</code> “连接” 到网络中的特定地址的。由于 <code>UDP</code> 是无连接的，连接到特定的地址并不会像 <code>TCP</code> 通道那样创建一个真正的连接，而是锁住 <code>DatagramChannel</code>，让其只能从特定地址收发数据。</p>
<p>这里有个例子：</p>
<pre><code class="language-java">datagramChannel.connect(new InetSocketAddress(&quot;127.0.0.1&quot;, 3247));
</code></pre>
<p>当连接后，也可以使用 <code>read()</code> 和 <code>write()</code> 方法，就像用传统的通道那样。只是在数据传送方面没有任何保证。如下：</p>
<pre><code class="language-java">int bytesRead = datagramChannel.read(buffer);
int bytesWrite = datagramChannel.write(buffer);
</code></pre>
<h2>Pipe</h2>
<p>Java NIO 管道是2个线程之前的单向数据连接。<code>Pipe</code> 有一个 <code>source</code> 通道和一个 <code>sink</code> 通道。数据会被写到 <code>sink</code> 通道，从 <code>source</code> 通道读取。</p>
<p>这里是 <code>Pipe</code> 原理的图示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220107134004502.png" alt="image-20220107134004502"></p>
<h3>创建管道</h3>
<p>通过 <code>Pipe.open()</code> 方法打开管道，如下：</p>
<pre><code class="language-java">Pipe pipe = Pipe.open();
</code></pre>
<h3>向管道写入数据</h3>
<p>要向管道写数据，需要访问 <code>sink</code> 通道，如下：</p>
<pre><code class="language-java">Pipe.SinkChannel sinkChannel = pipe.sink();
</code></pre>
<p>通过调用 <code>SinkChannel</code> 的 <code>write()</code> 方法，将数据写入 <code>SinkChannel</code>，如下：</p>
<pre><code class="language-java">String newData = &quot;New String will be write to file... &quot; + System.currentTimeMillis();
        
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();

while (buffer.hasRemaining()){
    sinkChannel.write(buffer);
}
</code></pre>
<h3>从管道读取数据</h3>
<p>要读取管道的数据，需要访问 <code>source</code> 通道，如下：</p>
<pre><code class="language-java">Pipe.SourceChannel sourceChannel = pipe.source();
</code></pre>
<p>调用 <code>source</code> 通道的 <code>read()</code> 方法来读取数据，如下：</p>
<pre><code class="language-java">ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
</code></pre>
<p><code>read()</code> 方法返回的 <code>int</code> 值会告诉我们多少字节被读进了缓冲区。</p>
<h2>Java NIO 和 IO</h2>
<p>当研究了 Java NIO 和 IO 的API后，很快就会想到一个问题：</p>
<p>什么时候用 IO，什么时候用 NIO 呢？</p>
<p>在下文中，将尽量阐明 Java NIO 和 IO 的差异，它们的使用场景，已经它们是如何影响代码设计的。</p>
<h3>Java NIO 和 IO 的主要区别</h3>
<p>下表总结了 Java NIO 和 IO 之间的主要差别，后面会更详细的阐述表中每部分的差异：</p>
<table>
<thead>
<tr>
<th style="text-align:center">IO</th>
<th style="text-align:center">NIO</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">面向流</td>
<td style="text-align:center">面向缓冲区</td>
</tr>
<tr>
<td style="text-align:center">阻塞 IO</td>
<td style="text-align:center">非阻塞 IO</td>
</tr>
<tr>
<td style="text-align:center">无</td>
<td style="text-align:center">选择器</td>
</tr>
</tbody>
</table>
<h4>面向流与面向缓冲区</h4>
<p>Java NIO 和 IO 之间第一个最大的区别就是，IO 是面向流的，NIO 是面向缓冲区的。</p>
<p>Java IO 面向流意味着每次从流中读一个或多个字节，直至读取所有字节，它们没有被缓存在任何地方，此外，它不能前后移动流中的数据。如果需要前后移动流中读取的数据，需要先将它缓存到一个缓冲区。</p>
<p>Java NIO 的缓冲导向方法略有不同，数据读取到一个它稍后处理的缓冲区，需要时可在缓冲区中前后移动，这就增加了处理过程中的灵活性。但是，还需要检查是否该缓冲区中包含所有你需要处理的数据，而且，需确保当更多的数据读入缓冲区时，不要覆盖缓冲区里尚未处理的数据。</p>
<h4>阻塞与非阻塞 IO</h4>
<p>Java IO 的各种流是阻塞的，这意味着，当一个线程调用 <code>read()</code> 或 <code>write()</code> 时，该线程被阻塞，直到有一些数据被读取，或数据完全写入，该线程在此期间不能再干任何事情了。</p>
<p>Java NIO 的非阻塞模式，使一个线程从某通道发送请求读取数据，但是它仅能得到目前可用的数据，如果目前没有数据可读时，就什么都不会获取，而不是保持线程阻塞，所以直至数据变得可读之前，该线程可以继续做其他的事情。非阻塞也就是如此，一个线程请求吸入一些数据到某通道，但不需要等待它完全写入，这个线程同时可以去做别的事情。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作，所以一个单独的线程现在可以管理多个输入输出通道。</p>
<h4>选择器</h4>
<p>Java NIO 的选择器（Selector）允许一个单独的线程来监视多个输入通道，你可以注册多个通道使用一个选择器，然后使用一个单独的线程来 “选择” 通道：这些通道里已经有可以处理的输入、或者选择已准备写入的通道。这种选择机制，使得一个单独的线程很容易来管理多个通道。</p>
<h3>NIO 和 IO 如何影响应用程序的设计</h3>
<p>无论是选择 IO 还是 NIO，可能会影响应用程序设计的有以下几个方面：</p>
<ol>
<li>对 NIO 或 IO 类的 API 调用。</li>
<li>数据处理。</li>
<li>用来处理数据的线程数。</li>
</ol>
<h4>API 调用</h4>
<p>当然，使用 NIO 的 API 调用时看起来与使用 IO 时有所不同，但这并不意外，因为并不是仅从一个 <code>InputStream</code> 逐字节读取，而是数据必须先读入缓冲区再处理。</p>
<h4>数据处理</h4>
<p>使用 NIO 设计与 IO 设计相比，数据处理也是不一样的。</p>
<p>在 IO 设计中，我们从 <code>InputStream</code> 或 <code>Reader</code> 逐字节读取数据。假设你正在处理一个基于行的文本数据流，例如：</p>
<pre><code class="language-sh">Name: Tome
Age: 25
Email: tom@mail.com
Phone: 12345678900
</code></pre>
<p>该文本行的流可以这样处理：</p>
<pre><code class="language-java">InputStream input = ... ; // get the InputStream from the client socket

BufferedReader reader = new BufferedReader(new InputStreamReader(input));

String nameLine   = reader.readLine();
String ageLine    = reader.readLine();
String emailLine  = reader.readLine();
String phoneLine  = reader.readLine();
</code></pre>
<p>注意处理状态由程序执行多久决定。换句话说，一旦 <code>reader.readline()</code> 方法返回，你就知道文本行已经读完了。这就是 <code>readinle()</code> 会阻塞直到整行读完的原因。此时你也就知道了 <code>nameLine</code> 对应的内容。同样，第二个 <code>readline()</code> 调用返回时就知道了 <code>ageLine</code> 对应的内容。正如你看到的，该处理程序仅在有新数据读入时运行，并知道每一步的数据是什么。一旦正在运行的线程已经处理过读入的某些数据，该线程不会再回退数据（大多如此）。下图也说明了这条原则：</p>
<p><img src="https://img.dyzmj.top/img/image-20220112202358804.png" alt="image-20220112202358804"></p>
<p>而一个 NIO 的实现会有所不同，下面是一个简单的例子：</p>
<pre><code class="language-java">ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
</code></pre>
<p>注意第二行，从通道读取字节到 <code>ByteBuffer</code>。当这个方法调用返回时，你不知道你所需的所有数据是否在缓冲区内。你只知道缓冲区包含一些字节，这使得处理有些困难。</p>
<p>假设第一次 <code>read(buffer)</code> 调用后，读入缓冲区的数据只有半行，例如，<code>Name:Bo</code>，你能处理数据吗？显然不能，需要等待，知道整行数据读入缓冲区，在此之前，对数据的任何处理都毫无意义。</p>
<p>所以，你怎么知道该缓冲区是否包含足够的数据可以处理了呢？好了，你并不知道。发现的方法只能查看缓冲区中的数据。其结果是，在你知道所有数据都在缓冲区之前，你必须检查几次缓冲区的数据。这不仅效率低下，而且让程序设计方案杂乱不堪，例如：</p>
<pre><code class="language-java">ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(!bufferFull(bytesRead)) {
	bytesRead = inChannel.read(buffer);
}
</code></pre>
<p><code>bufferFull()</code> 方法必须跟踪有多少数据读入缓冲区，并返回真或假，这取决于缓冲区是否已满。换句话说，如果缓冲区准备好被处理，那么表示缓冲区满了。</p>
<p><code>bufferFull()</code> 方法扫描缓冲区，但必须保持在 <code>bufferFull()</code> 方法被调用之前状态相同。如果没有，下一个读入缓冲区的数据可能无法读到正确的位置，这并非不可能，但这是另一个需要注意的问题。</p>
<p>如果缓冲区已满，则可以对其进行处理。如果它不完整，并且在你的实际案例中有意义，你或许能处理其中的部分数据。但是多数情况下并非如此。</p>
<p><code>is-data-in-buffer-ready</code> 循环如下如所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220112205441994.png" alt="image-20220112205441994"></p>
<h4>用来处理数据的线程数</h4>
<p>NIO 可以让你只使用一个（或几个）单线程管理多个通道（网络连接或文件），但代价是解析数据可能会比从一个阻塞流中读取数据要复杂一些。</p>
<p>如果需要管理同时打开的成千上万个连接，这些连接每次只是发送少量的数据，例如聊天服务器，实现 NIO 的服务器可能是一个优势。同样，如果你需要与其他计算机保持大量开放连接，例如在 P2P网络中，使用单个线程来管理所有出站连接可能是一个优势。一个线程多个连接的设计方案如下：</p>
<p><img src="https://img.dyzmj.top/img/image-20220113090134822.png" alt="image-20220113090134822"></p>
<p>如果你有少量的连接且使用非常高的宽带，一次发送大量数据，也许典型的 IO 服务器实现可能非常契合。下图说明了一个典型的 IO 服务器设计：</p>
<p><img src="https://img.dyzmj.top/img/image-20220113091428664.png" alt="image-20220113091428664"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-11</guid>
      <pubDate>Mon, 27 Apr 2026 11:29:18 +0000</pubDate>
    </item>
    <item>
      <title>Java 多线程</title>
      <link>https://dyzmj.top/posts/mult_thread</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/mult_thread">https://dyzmj.top/posts/mult_thread</a></p></blockquote><h1>第一篇：基础篇</h1>
<h2>一、进程与线程的基本概念</h2>
<h3>1.1  进程产生的背景</h3>
<p>最初的计算机只能接受一些特定的指令，用户每输入一个指令，计算机就做出一个操作。当用户在思考或者输入时，计算机就在等待。这样效率非常低下，在很多时候，计算机都处在等待状态。</p>
<p><strong>批处理操作系统</strong></p>
<p>后来有了批处理操作系统，把一系列需要操作的指令写下来，形成一个清单，一次性交给计算机。用户需要将多个需要执行的程序写在磁带上，然后交由计算机去读取并逐个执行这些程序，并将输出结果写在另一个磁带上。</p>
<p>批处理操作系统在一定程度上提高了计算机的效率，但是由于<strong>批处理操作系统的指令运行方式仍然是串行的，内存中始终只有一个程序在运行</strong>，后面的程序需要等待前面的程序执行完后才能开始执行，而前面的程序有时会由于 I/O 操作、网络等原因阻塞，所以批处理操作效率也不高。</p>
<p><strong>进程的提出</strong></p>
<p>人们对于计算机的性能要求越来越高，现有的批处理操作系统并不能满足人们的需求，而批处理操作系统的瓶颈在于内存中只存在一个程序，那么内存中能不能存在多个程序呢？这是人们亟待解决的问题。</p>
<p>于是，科学家们提出来进程的概念。</p>
<p>进程就是<strong>应用程序在内存中分配的空间，也就是正在运行的程序</strong>，各个进程之间互不干扰。同时进程保存着程序每个时刻运行的状态。</p>
<blockquote>
<p>程序：有某种变成语言（Java、Python 等）编写，能够完成一定任务或者功能的代码集合，是指令和数据的有序集合，是一段静态代码。</p>
</blockquote>
<p>此时，CPU 采用时间片轮转的方式运行这进程：CPU 为每个进程分配一个时间段，称作它的时间片。如果在时间片结束时进程还在运行，这暂停这个进程的运行，并将 CPU 分配给另一个进程（这个过程叫做上下文切换）。如果进程在时间片结束前阻塞或结束，则 CPU 立即进行切换，不用等待时间片用完。</p>
<blockquote>
<p>当进程暂停时，它会保存当前进程的状态（进程标识、进程使用的资源等），在下一次切换回来时根据之前保存的状态进行恢复，接着执行下去。</p>
</blockquote>
<p>使用进程 + 时间片轮转方式的操作系统，在宏观上看起来同一时间执行多个任务，换句话说，<strong>进程让操作系统的并发成为了可能</strong>。虽然从宏观上看有多个任务在并发执行，但实际上，对于<strong>单核 CPU</strong> 来说，任意具体时刻都只有一个任务在占用 CPU 资源。</p>
<p><strong>对操作系统的要求进一步提高</strong></p>
<p>虽然进程的出现，使得操作系统的性能大大提升，但是随着时间的推移，人们并不满足一个进程在一段时间只能做一件事情，如果一个进程有多个子任务时，只能逐个的执行这些子任务，很影响效率。</p>
<blockquote>
<p>比如杀毒软件在检测用户电脑时，如果在某一项检测中卡住了，那么后面的检测项也会受到影响。或者说当你使用杀毒软件中的扫描病毒功能时，在检测病毒结束前，无法使用杀毒软件中清理垃圾的功能，这显然是无法满足人们的要求。</p>
</blockquote>
<p><strong>线程的提出</strong></p>
<p>那么能不能让这些子任务同时执行呢？于是人们又提出了线程的概念，<strong>让一个线程执行一个子任务，这样一个进程就包含了多个线程，每个线程负责一个单独的子任务</strong>。</p>
<blockquote>
<p>使用线程之后，事情就变得简单多了。当用户使用扫描病毒的功能时，就让扫描病毒这个线程去执行。同时如果用户又使用清理垃圾的功能，那么可以先暂停扫描病毒线程，先响应用户清理垃圾的操作，让清理垃圾这个线程去执行。响应完后再切换回来，接着执行扫描病毒线程。</p>
<p>注意：操作系统如何分配时间片给每一个线程，涉及到线程的调度策略，有兴趣的同学可以看下《操作系统》这本书，本文不做深入详解。</p>
</blockquote>
<p>总之，进程和线程的提出极大的提高了操作系统的性能。<strong>进程让操作系统的并发性成为了可能，而线程让进程内部并发成为了可能。</strong></p>
<p><strong>多进程的方式也可以试下并发，为什么我们要使用多线程？</strong></p>
<p>多进程方式可以实现并发，但使用多线程，有以下几个好处：</p>
<ul>
<li>进程间的通信比较复杂，而线程间的通信比较简单，通常情况下我们需要使用共享资源，这些资源在线程间的通信比较容易获取。</li>
<li>进程是重量级的，而线程是轻量级的，故多线程方式的系统开销更小。</li>
</ul>
<p><strong>进程和线程的区别</strong></p>
<p>进程是一个独立的运行环境，而线程是在进程中执行的一个任务。他们两个的本质区别是<strong>是否单独占用内存地址空间及其他系统资源（比如 I/O）</strong>：</p>
<ul>
<li>进程单独占用一定的内存地址空间，所以进程间存在内存隔离，数据是分开的，数据共享复杂，但是同步简单，各个进程之间互不干扰；而线程共享所属进程占有的内存地址空间和资源，数据共享简单，但是同步复杂。</li>
<li>进程单独占有一定的内存地址空间，一个进程出现问题不会影响到其他进程，不影响主程序的稳定，可靠性高；一个线程的崩溃可能影响整个程序的稳定，可靠性较低。</li>
<li>进程单独占有一定的内存地址空间，进程的创建和销毁不仅需要保存寄存器和栈信息，还需要资源的分配回收及页调度，开销较大；线程只需要保存寄存器和栈信息，开销较小。</li>
</ul>
<p>另外一个重要区别就是，<strong>进程是操作系统进行资源分配的基本单位，而线程是操作系统进行调度的基本单位</strong>，即 CPU 分配时间的单位。</p>
<h3>1.2 上下文切换</h3>
<p>上下文切换（有时也称作进程切换或任务切换）是指 CPU 从一个进程（或线程）切换到另一个进程（或线程）。上下文是指<strong>某一时间点 CPU 寄存器和程序计数器的内容</strong>。</p>
<blockquote>
<p>寄存器是 CPU 内部的少量的速度很快的闪存，通常存储和访问计算过程中的中间值，提高计算机程序的运行速度。</p>
<p>程序计数器是一个专用的寄存器，用于表明指令序列中 CPU 正在执行的位置，存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置，具体实现依赖于特定的系统。</p>
<p>举例说明 线程 A -B</p>
<p>1、先挂起线程 A，将其在 CPU 中的状态保存在内存中。</p>
<p>2、在内存中检索下一个线程 B 的上下文并将其在 CPU 的寄存器中恢复，执行 B 线程。</p>
<p>3、当 B 执行完，根据程序计数器中指向的位置恢复线程 A。</p>
</blockquote>
<p>CPU 通过为每个线程分配 CPU 时间片来实现多线程机制。CPU 通过时间片分配算法来循环执行任务，当前任务执行一个时间片后会切换到下一个任务。</p>
<p>但是，在切换前会保存上一个任务的状态，以便下次切换回这个任务时，可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。</p>
<p>上下文切换通常是计算密集型的，意味着此操作会<strong>消耗大量的 CPU 时间，故线程也不是越多越好</strong>。如何减少系统中上下文切换次数，是提升多线程性能的一个重点课题。</p>
<h2>二、Java 多线程入门类和接口</h2>
<h3>2.1 Thread 类和 Runnable 接口</h3>
<p>上一章我们了解了操作系统中多线程的基本概念，那么在 Java 中，我们是如何使用多线程的呢?</p>
<p>首先，我们需要一个 “线程” 类。JDK 提供了 <code>Thread</code> 和 <code>Runnable</code> 接口来让我们实现自己的 “线程” 类。</p>
<ul>
<li>继承 <code>Thread</code> 类，并重写 <code>run()</code> 方法。</li>
<li>实现 <code>Runnable</code> 接口的 <code>run()</code> 方法。</li>
</ul>
<h4>2.1.1 继承 Thread 类</h4>
<p>先学会怎么用，再学原理。首先我们来看怎么用 <code>Thread</code> 和 <code>Runnable</code> 来写一个 Java 多线程程序。</p>
<p>首先是继承 <code>Thread</code> 类：</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }

    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(&quot;MyThread&quot;);
        }
    }
}
</code></pre>
<p>注意调用 <code>start()</code> 方法后，该线程才算启动！</p>
<blockquote>
<p>我们在程序里面调用了 <code>start()</code> 方法后，虚拟机会先为我们创建一个线程，然后等到这个线程第一次得到时间片是再调用 <code>run()</code> 方法。</p>
<p>注意不可多次调用 <code>start()</code> 方法。在第一次调用 <code>start()</code> 方法后，再次调用 <code>start()</code> 方法会抛出 <code>IllegalThreadStateException</code> 异常。</p>
</blockquote>
<h4>2.1.2 实现 Runnable 接口</h4>
<p>接着我们来看一下 <code>Runnable</code> 接口（JDK 1.8+）：</p>
<pre><code class="language-java">@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
</code></pre>
<p>可以看到 <code>Runnable</code> 是一个函数式的接口，这意味着我们可以使用 Java 8 的函数式编程来简化代码。</p>
<p>示例代码：</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        new Thread(new MyRunnable()).start();

        // Java 8 函数式编程
        new Thread(() -&gt; {
            System.out.println(&quot;Java 8 匿名内部类&quot;);
        }).start();
    }

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(&quot;MyRunnable&quot;);
        }
    }
}
</code></pre>
<h4>2.1.3 Thread 类构造方法</h4>
<p><code>Thread</code> 类是一个 <code>Runnable</code> 接口的实现类，我们来看看 <code>Thread</code> 类的源码。</p>
<p>查看 <code>Thread</code> 类的构造方法，发现其实是简单调用一个私有的 <code>init()</code> 方法来实现初始化。<code>init()</code> 方法的签名：</p>
<pre><code class="language-java">// Thread 类源码

// 片段1 - init 方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals)

// 片段2 - 构造方法调用 init 方法
public Thread(Runnable target) {
    init(null, target, &quot;Thread-&quot; + nextThreadNum(), 0);
}

// 片段3 - 使用在 init 方法里初始化 AccessControlContext 类型的私有属性
this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();

// 片段4 - 两个用于支持 ThreadLocal 的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
</code></pre>
<p>我们挨个来解释下 <code>init</code> 方法的这些参数：</p>
<ul>
<li>
<p>g：线程组，指定这个线程是在哪个线程组下；</p>
</li>
<li>
<p>target：指定要执行的任务；</p>
</li>
<li>
<p>name：线程的名字，多个线程的名字是可以重复的。如果不知道名称，见片段2；</p>
</li>
<li>
<p>acc：见片段3，用于初始化私有变量 <code>inheritedAccessControlContext</code>。</p>
<blockquote>
<p>这个变量有点神奇。它是一个私有变量，但是在 <code>Thread</code> 类里只有 <code>init</code> 方法对它进行初始化，在 <code>exit</code> 方法把它设为 <code>null</code>。其他没有任何地方使用它。一般我们是不会使用它的，那么什么使用会使用到这个变量呢？可以参考这个 Stack Overflow 的问题：<a href="https://stackoverflow.com/questions/13516766/restrict-permissions-to-threads-which-execute-third-party-software">Restrict permissions to threads which execute third party software</a> 。</p>
</blockquote>
</li>
<li>
<p>inheritThreadLocals：可继承的 <code>ThreadLocal</code>，见片段4，<code>Thread</code> 类里有两个私有属性来支持 <code>ThreadLocal</code>，我们会在后面的章节介绍 <code>ThreadLocal</code> 的概念。</p>
</li>
</ul>
<p>实际情况下，我们大多是直接调用下面这两个构造方法：</p>
<pre><code class="language-java">Thread(Runnable target);
Thread(Runnable, String name);
</code></pre>
<h4>2.1.4 Thread 类的几个常用方法</h4>
<p>这里介绍一下 <code>Thread</code> 类的几个常用的方法：</p>
<ul>
<li>currentThread()：静态方法，返回对当前正在执行的线程对象的引用。</li>
<li>start()：开始执行线程的方法，Java 虚拟机会调用线程内的 <code>run()</code> 方法。</li>
<li>yield()：yield 在英语里有放弃的意思，同样，这里的 <code>yield()</code> 指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是，就算当前线程调用了 <code>yield()</code> 方法，程序在调度的时候，也还有可能继续云溪这个线程的。</li>
<li>sleep()：静态方法，使当前线程睡眠了一段时间。</li>
<li>join()：是当前线程等待另一个线程执行完毕之后再继续执行，内部调用的是 <code>Object</code> 类的 <code>wait()</code> 方法实现的。</li>
</ul>
<h4>2.1.5 Thread 类与 Runnable 接口的比较</h4>
<p>实现一个自定义的线程类，可以由继承 <code>Thread</code> 类或实现 <code>Runnable</code> 接口这两种方式，它们之间有什么优劣呢？</p>
<ul>
<li>由于 Java “单继承，多实现” 的特性，<code>Runnable</code> 接口使用起来比 <code>Thread</code> 更灵活。</li>
<li><code>Runnable</code> 接口出现更符合面向对象，将线程单独进行对象的封装。</li>
<li><code>Runnable</code> 接口的实现 ，降低了线程对象和线程任务的耦合性。</li>
<li>如果使用线程时不需要使用 <code>Thread</code> 类的诸多方法，显然使用 <code>Runnable</code> 接口更为轻量。</li>
</ul>
<p>所以，我们通常优先使用实现 <code>Runnable</code> 接口的方式开自定义线程类。</p>
<h3>2.2 Callable、Future 与 FutureTask</h3>
<p>通常来说，我们使用 <code>Thread</code> 和 <code>Runnable</code> 来创建一个新的线程，但是它们有一个弊端，就是 <code>run()</code> 方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务，并且这个任务执行完成后有一个返回值。</p>
<p>JDK 提供了 <code>Callable</code> 接口与 <code>Future</code> 接口为我们解决这个问题，这也就是所谓的 “异步” 模型。</p>
<h4>2.2.1 Callable 接口</h4>
<p><code>Callable</code> 与 <code>Runnable</code> 类似，同样是只有一个抽象方法的函数式接口。不同的是，<code>Callable</code> 提供的方法是有返回值的，而且支持泛型。</p>
<pre><code class="language-java">@FunctionalInterface
public interface Callable&lt;V&gt; {
    V call() throws Exception;
}
</code></pre>
<p>那一般是怎么使用 <code>Callable</code> 的呢？ <code>Callable</code> 一般是配合线程池工具 <code>ExecutorService</code> 来使用的。我会在后续的章节解释线程池的使用。这里只介绍 <code>ExecutorService</code> 可以使用 <code>submit()</code> 方法来让一个 <code>Callable</code> 接口执行。它会返回一个 <code>Future</code>，我们后续的程序可以通过这个 <code>Future</code> 的 <code>get()</code> 方法得到结果。</p>
<p>这里可以看一个简单使用的 demo：</p>
<pre><code class="language-java">public class DemoTask implements Callable&lt;Integer&gt; {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        DemoTask demo = new DemoTask();
        Future&lt;Integer&gt; result = executor.submit(demo);
        // 注意：调用 get 方法会阻塞当前线程，直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载 get 方法。
        System.out.println(result.get());
    }

    @Override
    public Integer call() throws Exception {
        // 模拟 计算需要耗时1秒
        Thread.sleep(1000);
        return 2;
    }
}
</code></pre>
<p>输出结果： <code>2</code></p>
<h4>2.2.2 Future 接口</h4>
<p><code>Future</code> 接口只有几个比较简单的方法：</p>
<pre><code class="language-java">public abstract interface Future&lt;V&gt; {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}
</code></pre>
<p><code>cancel()</code> 方法是试图取消一个线程的执行。</p>
<p>注意是<strong>试图</strong>取消，并不一定能取消成功。因为任务可能已完成、已取消或者一些其他因素不能取消，存在取消失败的可能。<code>boolean</code> 类型的返回值是 “是否取消成功” 的意思。参数 <code>paramBoolean</code> 表示是否采用中断的方式取消线程执行。</p>
<p>所以，有的时候为了让任务有能够取消的功能，就使用 <code>Callable</code> 来代替 <code>Runnable</code>。如果为了可取消性而使用 <code>Future</code> 但又不提供可用的返回结果，则可以声明 <code>Future&lt;?&gt;</code> 形式类型，并返回 <code>null</code> 作为底层任务的结果。</p>
<h4>2.2.3 FutureTask 类</h4>
<p>上面介绍了 <code>Future</code> 接口，而这个接口有一个实现类叫 <code>FutureTask</code>。<code>FutureTask</code> 是实现 <code>RunnableFuture</code> 接口的，而 <code>RunnableFuture</code> 接口同时继承了 <code>Runnable</code> 接口和 <code>Future</code> 接口：</p>
<pre><code class="language-java">public interface RunnableFuture&lt;V&gt; extends Runnable, Future&lt;V&gt; {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}
</code></pre>
<p>那 <code>FutureTask</code> 类有什么用？为什么要有一个 <code>FutureTask</code> 类？前面说到了 <code>Future</code> 只是一个接口，而它里面的 <code>cancel</code>、<code>get</code>、<code>isDone</code> 等方法要自己实现起来都是非常复杂的。所以 JDK 提供了一个 <code>FutureTask</code> 类来供我们使用。</p>
<p>示例代码：</p>
<pre><code class="language-java">public class DemoTask implements Callable&lt;Integer&gt; {

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask&lt;Integer&gt; futureTask = new FutureTask&lt;&gt;(new DemoTask());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }

    @Override
    public Integer call() throws Exception {
        // 模拟 计算需要耗时1秒
        Thread.sleep(1000);
        return 2;
    }
}
</code></pre>
<p>上面的例子与第一个 demo 有一点小的区别，首先 <code>submit</code> 方法是没有返回值的，这里实际上调用的是 <code>submit(Runnable task)</code> 方法，而第一个 demo调用的是 <code>submit(Callable&lt;T&gt; task)</code> 方法。</p>
<p>然后，这里是使用 <code>FutureTask</code> 直接用 <code>get()</code> 方法取值，而第一个 demo 是通过 <code>submit</code> 方法返回的 <code>Future</code> 去取值。</p>
<p>在高并发的环境下，有可能 <code>Callable</code> 和 <code>FutureTask</code> 会创建多次，<code>FutureTask</code> 能够在高并发环境下确保任务只执行一次，对这块有兴趣的同学可以去看看 <code>FutureTask</code> 的源码。</p>
<h4>2.2.4 FutureTask 的几个状态</h4>
<pre><code class="language-java">/**
  *
  * state可能的状态转变路径如下：
  * NEW -&gt; COMPLETING -&gt; NORMAL
  * NEW -&gt; COMPLETING -&gt; EXCEPTIONAL
  * NEW -&gt; CANCELLED
  * NEW -&gt; INTERRUPTING -&gt; INTERRUPTED
  */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;
</code></pre>
<blockquote>
<p>state 表示任务的运行状态，初始状态为 NEW。运行状态只会在 set、setException、cancel 方法中终止。COMPLETING、INTERRUPTING 是任务完成后的瞬时状态。</p>
</blockquote>
<p>以上就是 Java 多线程几个基本的类和接口的介绍。可以打开 JDK 源码看看，体会这几个类的设计思路和用途。</p>
<h2>三、线程组和线程优先级</h2>
<h3>3.1 线程组（ThreadGroup）</h3>
<p>Java 中用 <code>ThreadGroup</code> 来表示线程组，我们可以使用线程组对线程进行批量控制。</p>
<p><code>ThreadGroup</code> 和 <code>Thread</code> 的关系就如同它们的字面意思一样简单粗暴，每个 <code>Thread</code> 必然存在于一个 <code>ThreadGroup</code> 中，<code>Thread</code> 不能独立于 <code>ThreadGroup</code> 存在。执行 <code>main()</code> 方法线程的名字是 main，如果在 <code>New Thread()</code> 时没有显式指定，那么默认将父线程（当前执行 <code>New Thread()</code> 的线程）的线程组设置为自己的线程组。</p>
<p>示例代码：</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        Thread testThread = new Thread(() -&gt; {
            System.out.println(&quot;-- testThread 当前线程组的名字&quot; + Thread.currentThread().getThreadGroup().getName());
            System.out.println(&quot;-- testThread 当前线程的名字&quot; + Thread.currentThread().getName());

        });

        testThread.start();

        System.out.println(&quot;&gt;&gt; 执行main方法所在线程组的名字&quot; + Thread.currentThread().getThreadGroup().getName());
        System.out.println(&quot;&gt;&gt; 执行main方法线程的名字&quot; + Thread.currentThread().getName());
    }
}
</code></pre>
<p>输出结果：</p>
<p><img src="https://img.dyzmj.top/img/202201211111528.png" alt="image-20220121111136342"></p>
<p><code>ThreadGroup</code> 管理着它下面的 <code>Thread</code>，<code>ThreadGroup</code> 是一个标准的<strong>向下引用</strong>的树状结构，这样设计的原因是<strong>防止 “上级” 线程被 “下级” 线程引用而无法有效的被 <code>GC</code> 回收</strong>。</p>
<h3>3.2 线程的优先级</h3>
<p>Java 中的线程优先级是可以指定的，返回是1~10。但是并不是所有的操作系统都支持10级优先级的划分（比如有的操作系统只支持3级划分：低、中、高），Java 只是给操作系统一个优先级的<strong>参考值</strong>，线程<strong>最终在操作系统的优先级</strong>是多少还是由操作系统决定。</p>
<p>Java 默认的线程优先级为5，线程的执行顺序由调度程序来决定，线程的优先级会在线程被调用之前设定。</p>
<p>通常情况下，高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用 <code>Thread</code> 类的 <code>setPriority()</code> 实例方法来设定线程的优先级。</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        Thread a = new Thread();
        System.out.println(&quot;我是默认线程优先级：&quot; + a.getPriority());
        Thread b = new Thread();
        b.setPriority(10);
        System.out.println(&quot;我是设置过的线程优先级：&quot; + b.getPriority());
    }
}
</code></pre>
<p>输出结果：</p>
<p><img src="https://img.dyzmj.top/img/202201211118303.png" alt="image-20220121111859167"></p>
<p>既然有1-10的级别来设定线程的优先级，这个时候可能会问了，是不是可以在业务实现的时候，采用这种方法来指定一线线程执行的先后顺序呢？</p>
<p>对于这个问题，我们的答案是：NO！</p>
<p>Java 中的优先级不是特别的可靠，<strong>Java 程序中对线程所设置的优先级只是给操作系统一个建议，操作系统不一定会采纳，而真正的调用顺序，是由操作系统的线程调度算法决定的。</strong></p>
<p>我们来通过代码来验证一下：</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        IntStream.range(1, 10).forEach(i -&gt; {
            Thread thread = new Thread(new T1());
            thread.setPriority(i);
            thread.start();
        });
    }

    public static class T1 extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(String.format(&quot;当前执行的线程是：%s，优先级：%d&quot;,
                    Thread.currentThread().getName(),
                    Thread.currentThread().getPriority()));
        }
    }
}
</code></pre>
<p>输出结果：</p>
<p><img src="https://img.dyzmj.top/img/202201211131446.png" alt="image-20220121113140294"></p>
<p>Java 提供一个<strong>线程调度器</strong>来监视和控制处于<strong>RUNNABLE状态</strong>的线程。线程的调度策略采用<strong>抢占式</strong>，优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下，按照 “先到先得” 的原则。每个 Java 程序都有个默认的主线程，就是通过 JVM 启动的第一个线程 main 线程。</p>
<p>还有一种线程称为 <strong>守护线程（Daemon）</strong>，守护线程的默认优先级比较低。</p>
<blockquote>
<p>如果某线程是守护线程，那如果所有的非守护线程都结束了，这个守护线程也自动结束。</p>
<p>应用场景是：当所有非守护线程结束时，结束其余的子线程（守护线程）自动关闭，就免去了还要继续关闭子线程的麻烦。</p>
<p>一个线程默认是非守护线程，可以通过 <code>Thread</code> 类的 <code>setDaemon(boolean on)</code> 方法来设置。</p>
</blockquote>
<p>在之前，我们有谈到一个线程必然存在于一个线程组中，那么当线程和线程组的优先级不一致的时候将会怎样呢？我们用下面的案例来验证一下：</p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup(&quot;t1&quot;);
        threadGroup.setMaxPriority(6);
        Thread thread = new Thread(threadGroup, &quot;thread&quot;);
        thread.setPriority(9);
        System.out.println(&quot;我是线程组的优先级&quot; + threadGroup.getMaxPriority());
        System.out.println(&quot;我是线程的优先级&quot; + thread.getPriority());
    }
}
</code></pre>
<p>输出结果：</p>
<p><img src="https://img.dyzmj.top/img/202201211145702.png" alt="image-20220121114531598"></p>
<p>所以，如果某个线程优先级大于线程所在线程组的优先级，那么该线程的优先级将会消失，取而代之的是线程组的最大优先级。</p>
<h3>3.3 线程组的常用方法及数据结构</h3>
<h4>3.3.1 线程组的常用方法</h4>
<p><strong>获取当前线程组的名字</strong></p>
<pre><code class="language-java">Thread.currentThread().getThreadGroup().getName();
</code></pre>
<p><strong>复制线程组</strong></p>
<pre><code class="language-java">// 获取当前的线程组
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
// 复制一个线程组到一个线程数组（获取Thread信息）
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
</code></pre>
<p><strong>线程组统一异常处理</strong></p>
<pre><code class="language-java">public class Demo {
    public static void main(String[] args) {

        ThreadGroup threadGroup = new ThreadGroup(&quot;group-1&quot;) {
            // 继承 ThreadGroup并重写方法
            // 在线程成员抛出 unchecked exception 时会执行此方法
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println(t.getName() + &quot;:&quot; + e.getMessage());
            }
        };

        // 这个线程是 threadGroup 的一员
        Thread thread1 = new Thread(threadGroup, new Runnable() {
            @Override
            public void run() {
                // 抛出 unchecked 异常
                throw new RuntimeException(&quot;测试异常&quot;);
            }
        });

        thread1.start();

    }
}
</code></pre>
<h4>3.3.2 线程组的数据结构</h4>
<p>线程组还可以包含其他的线程组，不仅仅是线程。</p>
<p>首先看看 <code>ThreadGroup</code> 源码中的成员变量：</p>
<pre><code class="language-java">public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent; // 父ThreadGroup
    String name; // ThreadGroupr 的名称
    int maxPriority; // 线程最大优先级
    boolean destroyed; // 是否被销毁
    boolean daemon; // 是否守护线程
    boolean vmAllowSuspension; // 是否可以中断

    int nUnstartedThreads = 0; // 还未启动的线程
    int nthreads; // ThreadGroup中线程数目
    Thread threads[]; // ThreadGroup中的线程

    int ngroups; // 线程组数目
    ThreadGroup groups[]; // 线程组数组
}
</code></pre>
<p>然后再看看构造函数：</p>
<pre><code class="language-java">// 私有构造函数
private ThreadGroup() { 
    this.name = &quot;system&quot;;
    this.maxPriority = Thread.MAX_PRIORITY;
    this.parent = null;
}

// 默认是以当前ThreadGroup传入作为parent  ThreadGroup，新线程组的父线程组是目前正在运行线程的线程组。
public ThreadGroup(String name) {
    this(Thread.currentThread().getThreadGroup(), name);
}

// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
    this(checkParentAccess(parent), parent, name);
}

// 私有构造函数，主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
    this.name = name;
    this.maxPriority = parent.maxPriority;
    this.daemon = parent.daemon;
    this.vmAllowSuspension = parent.vmAllowSuspension;
    this.parent = parent;
    parent.add(this);
}
</code></pre>
<p>第三个构造函数里调用了 <code>checkParentAccess</code> 方法，这里看看这个方法的源码：</p>
<pre><code class="language-java">// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
    parent.checkAccess();
    return null;
}

// 判断当前运行的线程是否具有修改线程组的权限
public final void checkAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccess(this);
    }
}
</code></pre>
<blockquote>
<p>这里涉及到 <code>SecurityManager</code> 这个类，它是 Java 的安全管理器，它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作的是什么，以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。</p>
<p>比如引入了第三方类库，但是不能保证它的安全性。</p>
<p>其实 <code>Thread</code> 类也有一个 <code>checkAccess()</code> 方法，不过是用来确认当前运行的线程是否有权限修改被调用的这个线程实例（Determines if the currently running thread has permission to modify this thread ）。</p>
</blockquote>
<p>总结来说，线程组是一个树状的结构，每个线程组下面可以由多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程权限的作用。</p>
<h2>四、Java线程的状态及主要转化方法</h2>
<h3>4.1 操作系统中的线程状态转换</h3>
<p>首先我们来看看操作系统中的线程状态转换。</p>
<blockquote>
<p>在现在的操作系统中，线程是被视为轻量级进程的，所以操作系统线程的状态其实和操作系统进程的状态是一致的。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202201211518409.png" alt="image-20220121151853280"></p>
<p>操作系统线程主要有以下三个状态：</p>
<ul>
<li>就绪状态（ready）：线程在等待使用 CPU，经调度程序调用之后可进入 <code>running</code> 状态。</li>
<li>执行状态（running）：线程正在使用 CPU。</li>
<li>等待状态（waiting）：线程经过等待事件的调用或者正在等待其他资源（如 I/O）。</li>
</ul>
<h3>4.2 Java 线程的6个状态</h3>
<pre><code class="language-java">// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
</code></pre>
<h4>4.2.1 NEW</h4>
<p>处于 <code>NEW</code> 状态的线程此时尚未启动。这里的尚未启动指的是还没有调用 <code>Thread</code> 实例的 <code>start()</code> 方法。</p>
<pre><code class="language-java">public void testStateNew() {
    Thread thread = new Thread(() -&gt; {});
    System.out.println(thread.getState()); // 输出 NEW 
}
</code></pre>
<p>从上面可以看出，只是创建了线程而没有调用 <code>start()</code> 方法，此时线程处于 <code>NEW</code> 状态。</p>
<p><strong>关于 <code>start()</code> 方法的两个引申问题</strong></p>
<p>1、反复调用同一个线程的 <code>start()</code> 方法是否可行？</p>
<p>2、加入一个线程执行完毕（此时处于 <code>TERMINATED</code> 状态），再次调用这个线程的 <code>start()</code> 方法是否可行？</p>
<p>要分析这两个问题，我们先来看看 <code>start()</code> 方法的源码：</p>
<pre><code class="language-java">public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. 
     */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}
</code></pre>
<p>我们可以看到，在 <code>start()</code> 内部有一个 <code>threadStatus</code> 的变量。如果它不等于0，调用 <code>start()</code> 是会直接抛出异常的。</p>
<p>接着往下看，有一个 <code>native</code> 修饰的 <code>start0()</code> 方法，这个方法里并没有对 <code>threadStatus</code> 的处理。到了这里我们仿佛就拿这个 <code>threadStatus</code> 没辙了，我们通过 <code>debug</code> 的方式再看一下：</p>
<pre><code class="language-java">@Test
public void testStatusMethod(){
    Thread thread = new Thread(() -&gt;{});
    // 第一次调用
    thread.start();
    // 第二次调用
    thread.start();
}
</code></pre>
<p>我在 <code>start()</code> 方法内部的最开始打的断点，下面是运行时的结果：</p>
<ul>
<li>第一次调用时 <code>threadStatus</code> 的值为 0。</li>
<li>第二次调用时 <code>threadStatus</code> 的值不为 0。</li>
</ul>
<p>查看当前线程状态的源码：</p>
<pre><code class="language-java">// Thread.getState 方法源码
public State getState() {
    // get current thread state
    return jdk.internal.misc.VM.toThreadState(threadStatus);
}

// jdk.internal.misc.VM 源码
public static Thread.State toThreadState(int threadStatus) {
    if ((threadStatus &amp; JVMTI_THREAD_STATE_RUNNABLE) != 0) {
        return RUNNABLE;
    } else if ((threadStatus &amp; JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
        return BLOCKED;
    } else if ((threadStatus &amp; JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
        return WAITING;
    } else if ((threadStatus &amp; JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
        return TIMED_WAITING;
    } else if ((threadStatus &amp; JVMTI_THREAD_STATE_TERMINATED) != 0) {
        return TERMINATED;
    } else if ((threadStatus &amp; JVMTI_THREAD_STATE_ALIVE) == 0) {
        return NEW;
    } else {
        return RUNNABLE;
    }
}
</code></pre>
<p>所以，我们结合上面源码可以得到引申的两个问题的结果：</p>
<blockquote>
<p>两个问题的答案都是不可行，在调用一次 <code>start()</code> 之后，<code>threadStatus</code> 的值会改变<code>（threadStatus != 0）</code> ，此时再次调用 <code>start()</code> 方法会抛出 <code>IllegalThreadStateException</code> 异常。</p>
<p>比如，<code>threadStatus</code> 为 2 代表当前线程状态为 <code>TERMINATED</code> 。</p>
</blockquote>
<h4>4.2.1 RUNNABLE</h4>
<p>表示当前线程正在运行中。处于 <code>RUNNABLE</code> 状态的线程在 Java 虚拟机中运行，也有可能在等待 CPU 分配资源。</p>
<p><strong>Java 中线程的 RUNNABLE 状态</strong></p>
<p>看了操作系统线程的几个状态之后我们来看看 <code>Thread</code> 源码里对 <code>RUNNABLE</code> 状态的定义：</p>
<pre><code class="language-java">/**
* Thread state for a runnable thread.  A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
</code></pre>
<blockquote>
<p>Java 线程的 <code>RUNNABLE</code> 状态其实是包括了传统操作系统线程的 <code>ready</code> 和 <code>running</code> 两个状态的。</p>
</blockquote>
<h4>4.2.3 BLOCKED</h4>
<p>阻塞状态。处于 <code>BLOCKED</code> 状态的线程正等待锁的释放以进入同步区。</p>
<p>我们用 <code>BLOCKED</code> 状态举个生活中的例子：</p>
<blockquote>
<p>假如今天你下班后准备去食堂吃饭，你来到食堂仅有的一个窗口，发现前面已经有个人在窗口前了，此时你必须得等前面的人从窗口离开才行。</p>
<p>假设你是线程 t2，你前面的那个人是线程 t1，此时 t1 占有了锁（食堂唯一的窗口），t2 正在等待锁的释放，所以此时 t2 就处于 <code>BLOCKED</code> 状态。</p>
</blockquote>
<h4>4.2.4 WAITING</h4>
<p>等待状态。处于等待状态的线程变成 <code>RUNNABLE</code> 状态需要其他线程唤醒。</p>
<p>调用如下3个方法会使线程进入等待状态：</p>
<ul>
<li><code>Object.wait():</code> 使当前线程处于等待状态直到另一个线程唤醒它；</li>
<li><code>Thread.join():</code> 等待线程执行完毕，底层调用的是 <code>Object</code> 实例的 <code>wait</code> 方法；</li>
<li><code>LockSupport.part():</code>  除非获得调用许可，否则禁用当前线程进行线程调度。</li>
</ul>
<p>我们延续上面的例子继续解释一下 <code>WAITING</code> 状态：</p>
<blockquote>
<p>你等待了好几分钟现在终于轮到你了，突然你们有一个 “不懂事” 的经理突然来了。你看到他你就有一种不祥的预感，果然，他是来找你的。</p>
<p>他把你拉到一旁叫你待会儿再吃饭，说他下午要去作报告，赶紧来找你了解一下项目的情况。你心里虽然有一万个不愿意但是你还是从食堂窗口走开了。</p>
<p>此时，假设你还是线程 t2，你的经理是线程 t1。虽然你此时都占有锁（窗口）了，“不速之客” 来了你还是得释放掉锁。此时你 t2 的状态就是 <code>WAITING</code>。然后经理 t1 获得锁，进入 <code>RUNNABLE</code> 状态。</p>
<p>要是经理 t1 不主动唤醒你 t2（notify、notifyAll..），可以说你 t2 只能一直等待了。</p>
</blockquote>
<h4>4.2.5 TIMED_WAITING</h4>
<p>超时等待状态。线程等待一个具体的时间，时间到后会被自动唤醒。</p>
<p>调用如下方法会使线程进入超时等待状态：</p>
<ul>
<li><code>Thread.sleep(long millis):</code> 为当前线程睡眠指定时间；</li>
<li><code>Object.wait(long timeout):</code> 线程休眠指定时间，等待期间可以通过 <code>notify()/notifyAll()</code> 唤醒；</li>
<li><code>Thread.join(long millis):</code> 等待当前线程最多执行 milis 毫秒，如果 millis 为0，则会一直执行；</li>
<li><code>LockSupport.parkNanos(long nanos):</code> 除非获得调用许可，否则禁用当前线程进行线程调度指定时间；</li>
<li><code>LockSupport.partUntil(long deadline):</code> 同上，也是禁用线程进行调度指定时间；</li>
</ul>
<p>我们继续延续上面的例子来解释一下 <code>TIME_WAITING</code> 状态：</p>
<blockquote>
<p>到了第二天中午，又到了饭点，你还是到了窗口前。</p>
<p>突然间想起你的同事叫你等他一起，他说让你等他十分钟他改个bug。</p>
<p>好吧，你说那你就等等吧，你就离开了窗口。很快十分钟过去了，你见他还没来，你想都等了这么久了还不来，那你还是先去吃饭好了。</p>
<p>这时你还是线程t1，你改bug的同事是线程t2。t2让t1等待了指定时间，此时t1等待期间就属于TIMED_WATING状态。</p>
<p>t1等待10分钟后，就自动唤醒，拥有了去争夺锁的资格。</p>
</blockquote>
<h4>4.2.6 TERMINATED</h4>
<p>终止状态。此时线程已执行完毕。</p>
<h3>4.3 线程状态转换</h3>
<p>根据上面关于线程状态的介绍我们可以得到下面的线程状态转换图：</p>
<p><img src="https://img.dyzmj.top/img/202202092012582.png" alt="image-20220209201239371"></p>
<h4>4.3.1 BLOCKED 与 RUNNABLE 状态的转换</h4>
<p>我们在上面说到：处于 <code>BLOCKED</code> 状态的线程因为是在等待锁的释放，假如这里有两个线程 a 和线程 b，a 线程提前获得了锁并且暂未释放锁，此时 b 就处于 <code>BLOCKED</code> 状态。先看一个例子：</p>
<pre><code class="language-java">@Test
public void blockedTest() {
    Thread a = new Thread(() -&gt; testMethod(), &quot;a线程&quot;);
    Thread b = new Thread(() -&gt; testMethod(), &quot;b线程&quot;);
    a.start();
    b.start();
    System.out.println(a.getName() + &quot;: &quot; + a.getState());
    System.out.println(b.getName() + &quot;: &quot; + b.getState());
}

/**
* 同步方法争夺锁
*/
private synchronized void testMethod() {
    try {
    	Thread.sleep(2000L);
    } catch (InterruptedException e) {
    	e.printStackTrace();
    }
}
</code></pre>
<p>初看之下，大家可能会觉得线程 a 会先调用同步方法，同步方法内又调用了 <code>Thread.sleep()</code> 方法，必然会输出 <code>TIMED_WAITING</code>，而线程 b 因为等待线程 a 释放锁所以必然会输出 <code>BLOCKED</code>。</p>
<p>其实不然，有两点需要值得大家注意，一是在测试方法 <code>blockedTest()</code> 内还有一个 <code>main</code> 线程，二是启动线程后执行 <code>run()</code> 方法还是需要消耗一定时间的。</p>
<blockquote>
<p>测试方法的 <code>main</code> 线程只是保证了 a，b 两个线程调用 <code>start()</code> 方法（转化为 <code>RUNNABLE</code> 状态），如果 CPU 执行效率高一点，还没等两个线程真正开始争夺锁，就已经打印测试两个线程的状态（<code>RUNNABLE</code>）了。</p>
<p>当然，如果 CPU 执行效率低一点，其中某个线程也是可能打印出 <code>BLOCKED</code> 状态的（此时两个线程已经开始争夺锁了）。</p>
</blockquote>
<p><img src="https://img.dyzmj.top/img/202202092034923.png" alt="image-20220209203455829"></p>
<p>这时你可能又会问了，要是我想要打印出 <code>BLOCKED</code> 状态我该怎么处理么？<code>BLOCKED</code> 状态的产生需要两个线程争夺锁才行。那么我们处理下测试方法里的 <code>main</code> 线程就可以了，让它 “休息一会儿”，调用一下 <code>Thread.sleep()</code> 方法。</p>
<p>这里需要注意的是 <code>main</code> 线程休息的时间，要保证在线程争夺锁的时间内，不要等到前一个线程都释放了你再去争夺锁，此时还是得不到 <code>BLOCKED</code> 状态。</p>
<p>我们把上面的测试方法 <code>blockedTest()</code> 改动一下：</p>
<pre><code class="language-java">@Test
public void blockedTest() throws InterruptedException {
    Thread a = new Thread(() -&gt; testMethod(), &quot;a线程&quot;);
    Thread b = new Thread(() -&gt; testMethod(), &quot;b线程&quot;);
    a.start();
    // 需要注意这里main线程休眠了1k毫秒，而testMethod()里休眠了2k毫秒
    Thread.sleep(1000L);
    b.start();
    System.out.println(a.getName() + &quot;: &quot; + a.getState());
    System.out.println(b.getName() + &quot;: &quot; + b.getState());
}
</code></pre>
<p>在这个例子中两个线程的状态转换如下：</p>
<ul>
<li>a 的状态转换过程：<code>RUNNABLE</code>（ a.start() ）-&gt;  <code>TIMED_WATING</code>（ Thread.sleep()） -&gt;  <code>RUNNABLE</code> ( sleep() 时间到) -&gt; <em><code>BLOCKED</code>（未抢到锁）</em> -&gt; <code>TERMINATED</code></li>
<li>b 的状态转换过程：<code>RUNNABLE</code>（ b.start() ） -&gt; <em><code>BLOCKED</code>（未抢到锁）</em>-&gt; <code>TERMINATED</code></li>
</ul>
<blockquote>
<p>斜体表示可能出现的状态，可以在自己的电脑多试几次看看输出。同样，这里的输出也可能有多重情况。</p>
</blockquote>
<h4>4.3.2 WAITING 状态与 RUNNABLE 状态的转换</h4>
<p>根据转换图我们知道有3个方法可以使线程从 <code>RUNNABLE</code> 状态转换为 <code>WAITING</code> 状态。这里主要介绍下 <code>Object.wait() </code> 和 <code>Thread.join()</code>。</p>
<p><strong><code>Object.wait()</code></strong></p>
<blockquote>
<p>调用 <code>wait()</code> 方法前线程必须持有对象的锁。</p>
<p>线程调用 <code>wait()</code> 方法时，会释放当前的锁，直到有其他线程调用 <code>notify()/notifyAll()</code> 方法唤醒等待锁的线程。</p>
<p>需要注意的是，其他线程调用 <code>notify()</code> 方法只会唤醒单个等待锁的线程，如有多个线程都在等待这个锁的话，不一定会唤醒到之前调用 <code>wait()</code> 方法的线程。</p>
<p>同样，调用 <code>notifyAll()</code> 方法唤醒所有等待锁的线程之后，也不一定马上把时间片分配给刚才放弃锁的那个线程，具体要看系统的调度。</p>
</blockquote>
<p><strong><code>Thread.join()</code></strong></p>
<blockquote>
<p>调用 <code>join()</code> 方法，会一直等待这个线程执行完毕（转换为 <code>TERMINATED</code> 状态）。</p>
</blockquote>
<p>我们再把上面的例子线程启动那里改变一下：</p>
<pre><code class="language-java">@Test
public void blockedTest() throws InterruptedException {
    Thread a = new Thread(() -&gt; testMethod(), &quot;a线程&quot;);
    Thread b = new Thread(() -&gt; testMethod(), &quot;b线程&quot;);
    a.start();
    a.join();
    b.start();
    System.out.println(a.getName() + &quot;: &quot; + a.getState());
    System.out.println(b.getName() + &quot;: &quot; + b.getState());
}
</code></pre>
<p>要是没有调用 <code>join()</code> 方法，<code>main</code> 线程不管 a 线程是否执行完毕都会继续往下走。</p>
<p>a 线程启动之后马上调用了 <code>join()</code> 方法，这里 <code>main</code> 线程就会等到 a 线程执行完毕，所以这里 a 线程打印的状态固定是 <code>TERMINATED</code>。</p>
<p>至于 b 线程的状态，有可能打印 <code>RUNNABLE(尚未进入同步方法)</code>，也有可能打印 <code>TIMED_WAITING(进入了同步方法)</code>。</p>
<h4>4.3.3 TIMED_WAITING 与 RUNNABLE 状态转换</h4>
<p><code>TIMED_WAITING</code> 与 <code>WAITING</code> 状态类似，只是 <code>TIMED_WAITING</code> 状态等待的时间是指定的。</p>
<p><strong><code>Thread.sleep(long)</code></strong></p>
<blockquote>
<p>使当前线程睡眠指定时间。需要注意这里的 “睡眠” 只是暂时使线程停止执行，并不会释放锁。时间到后，线程会重新进入 <code>RUNNABLE</code> 状态。</p>
</blockquote>
<p><strong><code>Object.wait(long)</code></strong></p>
<blockquote>
<p><code>wait(long)</code> 方法使线程进入 <code>TIMED_WAITING</code> 状态。这里的 <code>wait(long)</code> 与无参方法 <code>wait()</code> 相同的地方是，都可以通过其他线程调用 <code>notify()</code> 或 <code>notifyAll()</code> 方法来唤醒。</p>
<p>不同的地方是，有参方法 <code>wait(long)</code> 就算其他线程不来唤醒它，经过指定时间 <code>long</code> 之后它会自动唤醒，拥有去争夺锁的资格。</p>
</blockquote>
<p><strong><code>Thread.join(long)</code></strong></p>
<blockquote>
<p><code>join(long)</code> 使当前线程执行指定时间，并且使线程进入 <code>TIMED_WAITING</code> 状态。</p>
</blockquote>
<p>我们再来改一下刚才的示例：</p>
<pre><code class="language-java">@Test
public void blockedTest() throws InterruptedException {
    Thread a = new Thread(() -&gt; testMethod(), &quot;a线程&quot;);
    Thread b = new Thread(() -&gt; testMethod(), &quot;b线程&quot;);
    a.start();
    a.join(1000L);
    b.start();
    System.out.println(a.getName() + &quot;: &quot; + a.getState());
    System.out.println(b.getName() + &quot;: &quot; + b.getState());
}
</code></pre>
<p>这里调用 <code>a.join(1000L)</code>，因为是指定了具体 a 线程执行的时间，并且执行时间是小于 a 线程 <code>sleep</code> 的时间，所以 a 线程状态输出 <code>TIMED_WAITING</code>。</p>
<p>b 线程状态仍然不固定（<code>RUNNABLE</code> 或 <code>BLOCKED</code>）。</p>
<h4>4.3.4 线程中断</h4>
<blockquote>
<p>在某些情况下，我们在线程启动后发现并不需要它继续执行下去时，需要中断线程。目前在 Java 里还没有安全直接的方法来停止线程，但是 Java 提供了线程中断机制来处理中断线程的情况。</p>
<p>线程中断机制是一种协作机制。需要注意，通过中断操作并不能直接终止一个线程，而是通知需要被中断的线程自行处理。</p>
</blockquote>
<p>简单介绍下 <code>Thread</code> 类里提供的关于线程中断的几个方法：</p>
<ul>
<li><code>Thread.interript():</code> 中断线程。这里的中断线程并不会立即停止线程，而是设置线程的中断状态为 <code>true</code> （默认是 <code>false</code>）。</li>
<li><code>Thread.currentThread().isInterrupted():</code> 测试当前线程是否被中断。线程的中断状态收这个方法的影响，意思是调用一次使线程中断状态设置为 <code>true</code>，连续调用两次会使得这个线程的中断状态重新转为 <code>false</code>。</li>
<li><code>Thread.isInterrupted():</code> 测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。</li>
</ul>
<blockquote>
<p>在线程中断机制里，当其他线程通知需要被中断的线程后，线程中断的状态被设置为 <code>true</code>，但是具体被要求中断的线程要怎么处理，完全由被中断线程自己而定，可以在合适的实际处理中断请求，也可以完全不处理继续执行下去。</p>
</blockquote>
<h2>五、Java 线程间的通信</h2>
<p>合理的使用 Java 多线程可以更好的利用服务器资源。一般来讲，线程内部有自己私有的线程上下文，互不干扰。但是当我们需要多个线程之间相互协作的时候，就需要我们掌握 Java 线程的通信方式。本文将介绍 Java 线程之间的几种通信原理。</p>
<h3>5.1 锁与同步</h3>
<p>在 Java 中，锁的概念都是基于对象，所以我们又经常称它为对象锁。线程和锁的关系，我们可以用婚姻关系来理解，一个锁同一时间只能被一个线程持有，也就是说，一个锁如果和一个线程“结婚”（持有），那其他线程如果需要得到这个锁，就得等到这个线程和这个锁“离婚”（释放）。</p>
<p>在我们的线程之间，有一个同步的概念。什么是同步呢，假如我们现在有2位正在抄暑假作业答案的同学：线程 A 和线程 B。当他们正在抄的时候，老师突然来修改了一些答案，可能 A 和 B 最后写出的暑假作业就不一样。我们为了 A，B 能写出2本相同的暑假作业，我们就需要让老师先修改答案，然后 A，B 同学再抄。或者 A，B 同学先抄完，老师再修改答案。这就是线程 A，线程 B 的线程同步。</p>
<p>可以解释为：线程同步是线程之间按照<strong>一定的顺序</strong>执行。</p>
<p>为了达到线程同步，我们可以使用锁来实现它。</p>
<p>我们先来看看一个无锁的程序：</p>
<pre><code class="language-java">public class NoneLock {
    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i &lt; 1000; i++) {
                System.out.println(&quot;Thread A: &quot; + i);
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i &lt; 1000; i++) {
                System.out.println(&quot;Thread B: &quot; + i);
            }
        }
    }

}

</code></pre>
<p>执行这个程序，你会在控制台看到，线程 A 和线程 B 各自独立工作，输出自己的打印值。如下是我电脑上某一次的运行结果。每一次运行结果都会不一样：</p>
<p><img src="https://img.dyzmj.top/img/202202101127911.png" alt="image-20220210112701667"></p>
<p>那我现在有一个需求，我想等 线程 A 执行完之后，再由线程 B 去执行，怎么办呢？最简单的方式就是使用一个 “对象锁”：</p>
<pre><code class="language-java">public class ObjectLock {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(10L);
        new Thread(new ThreadB()).start();
    }

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i &lt; 1000; i++) {
                    System.out.println(&quot;Thread A: &quot; + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i &lt; 1000; i++) {
                    System.out.println(&quot;Thread B: &quot; + i);
                }
            }
        }
    }
}
</code></pre>
<p>这里声明一个名字为 <code>lock</code> 的对象锁。我们在 <code>ThreadA</code> 和 <code>ThreadB</code> 内需要同步的代码块里，都是用 <code>synchronized</code> 关键字加上了同一个对象锁 <code>lock</code>。</p>
<p>上文我们说到了，根据线程和锁的关系，同一时间只有一个线程持有一个锁，那么线程 B 就会等线程 A 执行完成后释放 <code>lock</code>，线程 B 才能获得锁 <code>lock</code>。</p>
<blockquote>
<p>这里主线程里使用 <code>sleep()</code> 方法休眠了 10毫秒，是为了防止线程 B 先获得锁。因为如果同时 <code>start</code>，线程 A 和线程 B 都是处于就绪状态，操作系统可能会先让 B 运行。这样就会先输出 B 的内容，然后 B 执行完成之后自动释放锁，线程 A 再执行。</p>
</blockquote>
<h3>5.2 等待 / 通知机制</h3>
<p>上面一种基于 “锁” 的方式，线程需要不断地尝试获得锁，如果失败了，再继续尝试。这可能会耗费服务器资源。</p>
<p>而等待/通知机制是另一种方式。</p>
<p>Java 多线程的等待/通知机制是基于 <code>Object</code> 类的 <code>wait()</code> 方法和 <code>notify()、notifyAll()</code> 方法来实现的。</p>
<blockquote>
<p><code>notify()</code> 方法会随机叫醒一个正在等待的线程，而 <code>notifyAll()</code> 会叫醒所有正在等待的线程。</p>
</blockquote>
<p>前面我们讲到，一个锁同一时刻只能被一个线程持有。而假如线程 A 现在持有了一个锁 <code>lock</code> 并开始执行，它可以使用 <code>lock.wait()</code> 让自己进入等待状态。这个时候，<code>lock</code> 这个锁是被释放了的。</p>
<p>这时，线程 B 获得了 <code>lock</code> 这个锁并开始执行，它可以在某一时刻，使用 <code>lock.notify()</code>，通知之前持有 <code>lock</code> 锁并进入等待状态的线程 A，说 “线程 A 你不用等了，可以往下执行了”。</p>
<blockquote>
<p>需要注意的是，这个时候线程 B 并没有释放锁 <code>lock</code>，除非线程 B 这个时候使用 <code>lock.wait()</code> 释放锁，或者线程 B 执行结束自行释放锁，线程 A 才能等到 <code>lock</code> 锁。</p>
</blockquote>
<p>我们用代码来实现一下：</p>
<pre><code class="language-java">public class WaitAndNotify {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000L);
        new Thread(new ThreadB()).start();
    }

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i &lt; 5; i++) {
                    try {
                        System.out.println(&quot;Thread A: &quot; + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i &lt; 5; i++) {
                    try {
                        System.out.println(&quot;Thread B: &quot; + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

}
</code></pre>
<p>输出结果：</p>
<p><img src="https://img.dyzmj.top/img/202202101550066.png" alt="image-20220210155005519"></p>
<p>在这个 Demo 里，线程 A 和线程 B 首先打印自己需要的东西，然后使用 <code>notify()</code> 方法唤醒另一个正在等待的线程，然后自己使用 <code>wait()</code> 方法陷入等待并释放 <code>lock</code> 锁。</p>
<blockquote>
<p>需要注意的是，等待/通知机制使用的是同一个对象锁，如果两个线程使用的是不同的对象锁，那么它们之间是不可能用等待/通知机制通讯的。</p>
</blockquote>
<h3>5.3 信号量</h3>
<p>JDK 提供了一个类似于 “信号量” 功能的类 <code>Semaphore</code>。但是本文不是要介绍这个类，而是介绍一种基于 <code>volatile</code> 关键字自己实现的信号量通讯。</p>
<p>后面会有专门的章节介绍 <code>volatile</code> 关键字，这里只是做一个简单的介绍：</p>
<blockquote>
<p><code>volatile</code> 关键字能够保证内存的可见性，如果使用 <code>volatile</code> 关键字声明了一个变量，在一个线程里面改变了这个变量的值，那其他线程是立马可见更改后的值的。</p>
</blockquote>
<p>比如我现在有一个需求，我想让线程 A 输出0，然后线程 B 输出1，再然后线程 A 输出2 ... 以此类推。我应该怎么实现呢？</p>
<p>代码：</p>
<pre><code class="language-java">public class SignalDemo {

    private static volatile int signal = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000L);
        new Thread(new ThreadB()).start();
    }

    static class ThreadA implements Runnable {

        @Override
        public void run() {
            while (signal &lt; 5) {
                if (signal % 2 == 0) {
                    System.out.println(&quot;Thread A: &quot; + signal);
                    signal++;
                }
            }
        }
    }

    static class ThreadB implements Runnable {

        @Override
        public void run() {
            while (signal &lt; 5) {
                if (signal % 2 == 1) {
                    System.out.println(&quot;Thread B: &quot; + signal);
                    signal++;
                }
            }
        }
    }
}
</code></pre>
<p>运行结果：</p>
<p><img src="https://img.dyzmj.top/img/202202101605112.png" alt="image-20220210160508937"></p>
<p>我们可以看到，使用一个 <code>volatile</code> 变量 <code>signal</code> 来实现了 “信号量” 的模型。这里需要注意的是，<code>volatile</code> 变量需要进行原子操作。</p>
<p>需要注意的是，<code>signal++</code> 并不是一个原子操作，所以我们在实际开发中，会根据需要使用 <code>synchronized</code>给它 “上锁”，或者是使用 <code>AtomicInteger</code> 等原子类。并且上面的程序也并不是线程安全的，因为执行 <code>while</code> 语句后，可能当前线程就暂停等待时间片了，等线程醒来，可能 <code>signal</code> 已经大于等于 5 了。</p>
<blockquote>
<p>这种实现方式并不一定高效，本例只是演示信号量。</p>
</blockquote>
<p><strong>信号量的应用场景：</strong></p>
<p>假如在一个停车场中，车位是我们的公共资源，线程就如同车辆，而看门的管理员就是起的 “信号量” 的作用。</p>
<p>因为在这种场景下，多个线程需要相互合作，我们用简单的 “锁” 和 “等待/通知机制” 就不那么方便了，这个时候就可以用到信号量。</p>
<p>其实 JDK 中提供的很多多线程通信工具类都是基于信号量模型的，后面会在第三篇的文章中介绍一些常用的通信工具类。</p>
<h3>5.4 管道</h3>
<p>管道是基于 “管道流” 的通信方式。JDK 提供了 <code>PipeWrite</code>、<code>PipeReader</code>、<code>PipeOutputStream</code>、<code>PipeInputStream</code>。其中，前面两个是基于字符的，后面两个是基于字节流的。</p>
<p>这里的示例代码使用的是基于字符的：</p>
<pre><code class="language-java">public class PipeDemo {

    public static void main(String[] args) throws InterruptedException, IOException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader);

        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000L);
        new Thread(new WriterThread(writer)).start();
    }

    static class ReaderThread implements Runnable {
        private final PipedReader reader;

        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println(&quot;This is reader&quot;);
            int receive = 0;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char) receive + &quot; &quot;);
                }
                System.out.println();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {
        private final PipedWriter writer;

        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println(&quot;This is writer&quot;);
            int receive = 0;
            try {
                writer.write(&quot;test&quot;);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
</code></pre>
<p>运行结果：</p>
<p><img src="C:%5CUsers%5CG003247%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220210163708424.png" alt="image-20220210163708424"></p>
<p>我们通过线程的构造函数，传入了 <code>PipedWrite</code> 和 <code>PipedReader</code> 对象。可以简单分析一下这个示例代码的执行流程：</p>
<ol>
<li>线程 <code>ReaderThread</code> 开始执行；</li>
<li>线程 <code>ReaderThread</code> 使用管道 <code>reader.read()</code> 进入“阻塞”；</li>
<li>线程 <code>WriterThread</code> 开始执行；</li>
<li>线程 <code>WritedThread</code> 用 <code>writer.write(&quot;test&quot;)</code> 往管道写入字符串；</li>
<li>线程 <code>WritedThread</code> 使用 <code>writer.close()</code> 结束管道写入，并执行完毕；</li>
<li>线程 <code>ReaderThread</code> 接受到管道输出的字符串并打印；</li>
<li>线程 <code>ReaderThread</code> 执行完毕。</li>
</ol>
<p><strong>管道通信的应用场景：</strong></p>
<p>这个很好理解，使用管道多半与 I/O 流相关。当我们一个线程需要先另一个线程发送一个信息（比如字符串）或者文件等等时，就需要使用管道通信了。</p>
<h3>5.5 其他通信相关</h3>
<p>以上介绍了一些线程间通信的基本原理和方法。除此以外，还有一些与线程通信相关的知识点，这里一并介绍。</p>
<h4>5.5.1 join 方法</h4>
<p><code>join()</code> 方法是 <code>Thread</code> 类的一个实例方法。它的作用是当线程陷入 “等待” 状态，等 <code>join</code> 的这个线程执行完成后，再继续执行当前线程。</p>
<p>有时候，主线程创建并启动了子线程，如果子线程中需要进行大量的耗时运算，主线程往往将早于子线程结束之前结束。</p>
<p>如果主线程想等待子线程执行完毕后，获得子线程中处理完的某个数据，就要用到 <code>join()</code> 方法了。</p>
<p>示例代码：</p>
<pre><code class="language-java">public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println(&quot;如果不加join方法，我会被先打印出来，加了就不一样了&quot;);
    }

    static class ThreadA implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(&quot;我是子线程，我先休眠一秒&quot;);
                Thread.sleep(1000L);
                System.out.println(&quot;我是子线程，我睡完一秒了&quot;);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
</code></pre>
<blockquote>
<p>注意 <code>join()</code> 方法有两个重载方法，一个是 <code>join(long)</code>，一个是 <code>join(long,int)</code>。</p>
<p>实际上，通过源码会发现，<code>join()</code> 方法及其重载方法底层都是利用了 <code>wait(long)</code> 这个方法。</p>
<p>对于 <code>join(long,int)</code>，通过查看源码发现，底层并没有精确到纳秒，而是对第二个参数做了简单的判断和处理。</p>
</blockquote>
<h4>5.5.2 sleep 方法</h4>
<p><code>sleep()</code> 方法是 <code>Thread</code> 类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法：</p>
<ul>
<li><code>Thread.sleep(long)</code></li>
<li><code>Thread.sleep(long, int)</code></li>
</ul>
<blockquote>
<p>同样，查看源码发现，第二个方法只是对第二个参数进行简单的处理，没有精确到纳秒。实际上还是调用的第一个方法。</p>
</blockquote>
<p>这里需要强调一下：<code>sleep()</code> 方法是不会释放当前的锁的，而 <code>wait()</code> 方法会。</p>
<p>它们还有这些区别：</p>
<ul>
<li><code>wait()</code> 可以指定时间，也可以不指定；而 <code>sleep()</code> 必须指定时间。</li>
<li><code>wait()</code> 释放 CPU 资源，同时释放锁；<code>sleep</code> 释放 CPU 资源，但是不释放锁，所以易死锁。</li>
<li><code>wait()</code> 必须放在同步代码块或同步方法中，而 <code>sleep()</code> 可以放在任意位置。</li>
</ul>
<h4>5.5.3 ThreadLocal 类</h4>
<p><code>ThreadLocal</code> 是一个本地线程副本变量工具类。内部是一个 <strong>弱引用</strong> 的 <code>Map</code> 来维护。这里不详细介绍它的原理，而是仅介绍它的使用，后面有独立章节来介绍 <code>ThreadLocal</code> 类的原理。</p>
<p>有些朋友称 <code>ThreadLocal</code> 为线程本地变量或线程本地存储。严格来说，<code>ThreadLocal</code> 类并不属于多线程间的通信，而是让每个线程有自己 “独立” 的变量，线程之间互不影响。它为每个线程都创建了一个副本，每个线程可以访问自己内部的副本变量。</p>
<p><code>ThreadLocal</code> 类最常用的就是 <code>set()</code> 和 <code>get()</code> 方法。实例代码：</p>
<pre><code class="language-java">public class ThreadLocalDemo {

    public static void main(String[] args) {
        ThreadLocal&lt;String&gt; threadLocal = new ThreadLocal&lt;&gt;();
        new Thread(new ThreadA(threadLocal)).start();
        new Thread(new ThreadB(threadLocal)).start();
    }

    static class ThreadA implements Runnable {

        private final ThreadLocal&lt;String&gt; threadLocal;

        public ThreadA(ThreadLocal&lt;String&gt; threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set(&quot;A&quot;);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(&quot;ThreadA 输出：&quot; + threadLocal.get());
        }
    }

    static class ThreadB implements Runnable {

        private final ThreadLocal&lt;String&gt; threadLocal;

        public ThreadB(ThreadLocal&lt;String&gt; threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set(&quot;B&quot;);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(&quot;ThreadB 输出：&quot; + threadLocal.get());
        }
    }

}
</code></pre>
<p>可以看到，虽然两个线程使用的同一个 <code>ThreadLocal</code> 实例（通过构造方法传入），但是它们各自可以存取自己当前线程的一个值。</p>
<p>那么 <code>ThreadLocal</code> 有什么用呢？如果只是单纯的想要线程隔离，在每个线程中声明一个私有变量就好了呀，为什么要使用 <code>ThreadLocal</code>？</p>
<p>如果开发者希望将类的某个静态变量（user ID 或者 transcation ID）与线程状态关联，则可以考虑使用 <code>ThreadLocal</code>。</p>
<p>最常见的 <code>ThreadLocal</code> 使用场景是用来解决数据库连接、Session 管理等。数据库连接和 Session 管理设计多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作，那这个线程就变得不那么 “轻量” 了，需要频繁的创建和关闭连接。</p>
<h4>5.5.4 InheritableThreadLocal</h4>
<p><code>InheritableThreadLocal</code> 类与 <code>ThreadLocal</code> 类稍有不同，<code>Inheritable</code> 是继承的意思。它不仅仅是当前线程可以存取副本值，而且它的子类也可以存取这个副本值。</p>
<h1>第二篇：原理篇</h1>
<h2>六、Java 内存模型基础知识</h2>
<h3>6.1 并发编程模型的两个关键问题</h3>
<ul>
<li>线程如何通信？即：线程之间以何种机制来交换信息</li>
<li>线程如何同步？即：线程以何种机制来控制不同线程间操作发生的相对顺序</li>
</ul>
<p>有两种并发模型可以解决这两个问题：</p>
<ul>
<li>消息传递并发模型</li>
<li>共享内存并发模型</li>
</ul>
<p>这两种模型之间的区别如下表所示：</p>
<table>
<thead>
<tr>
<th></th>
<th>如何通信</th>
<th>如何同步</th>
</tr>
</thead>
<tbody>
<tr>
<td>消息传递并发模型</td>
<td>线程之间没有公共状态，线程间的通信必须通过发送消息来显式进行通信</td>
<td>发送消息天然同步，因为发送消息总是在接受消息之前，因此同步是隐式的</td>
</tr>
<tr>
<td>共享内存并发模型</td>
<td>线程之间共享程序的公共状态，通过读-写内存中的公共状态进行隐式通信</td>
<td>必须显式指定某段代码需要在线程之间互斥执行，同步是显式的</td>
</tr>
</tbody>
</table>
<p><strong>在 Java 中，使用的是共享内存并发模型。</strong></p>
<h3>6.2 Java 内存模型的抽象结构</h3>
<h4>6.2.1 运行时内存的划分</h4>
<p>先谈一下运行时数据区，下面这张图相信大家一点都不陌生：</p>
<p><img src="https://img.dyzmj.top/img/image-20220211142955195.png" alt="image-20220211142955195"></p>
<p>对于每一个线程来说，栈都是私有的，而堆是共有的。</p>
<p>也就是说在栈中的变量（局部变量、方法定义参数、异常处理器参数）不会在线程之间共享，也就不会有内存可见性的问题，也不受内存模型的影响。而在堆中的变量是共享的，本文称为共享变量。</p>
<p>所以，内存可见性是针对的共享变量。</p>
<h4>6.2.2 堆中内存不可见问题</h4>
<p>既然堆是共享的，为什么在堆中会有内存不可见问题？</p>
<p>这是因为现代计算机为了高效，往往会在高速缓存区中缓存共享变量，因为 CPU 访问缓存区比访问内存要快得多。</p>
<blockquote>
<p>线程之间的共享变量存在主内存中，每个线程都有一个私有的本地内存，存储了该线程以读、写共享变量的副本。本地内存是 Java 内存模型的一个抽象概念，并不真实存在，它涵盖了缓存、写缓冲区、寄存器等。</p>
</blockquote>
<p>Java 线程之间的通信由 Java 内存模型（简称 JMM）控制，从抽象的角度来说，JMM 定义了线程和主内存之间的抽象关系。JMM 的抽象示意图如图所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220211164000416.png" alt="image-20220211164000416"></p>
<p>从图中可以看出：</p>
<ol>
<li>所有的共享变量都存在主内存中。</li>
<li>每个线程都保存了一份该线程使用到的共享变量的副本。</li>
<li>如果线程 A 与线程 B 之间要通信的话，必须经历下面2个步骤：
<ul>
<li>线程 A 将本地内存 A 中更新过的共享变量刷新到主内存中去；</li>
<li>线程 B 到主内存中去读取线程 A 之前已经更新过的共享变量。</li>
</ul>
</li>
</ol>
<p><strong>所以，线程 A 无法直接访问线程 B 的工作内存，线程间通信必须经过主内存。</strong></p>
<p>注意，根据 JMM 的规定，<strong>线程对共享变量的所有操作都必须在自己的本地内存中进行，不能直接从主内存中读取。</strong></p>
<p>所以线程 B 并不是直接去主内存中读取共享变量的值，而是现在本地内存 B 中找到这个共享变量，发现这个共享变量已经被更新了，然后本地内存 B 去从主内存中读取这个共享变量的新值，并拷贝到本地内存 B 中，最后线程 B 再读取本地内存 B 中的新值。</p>
<p>那么怎么知道这个共享变量被其他线程更新了呢？这就是 JMM 的功劳了，也是 JMM 存在的必要性之一。</p>
<p><strong>JMM 通过控制主内存与每个线程的本地内存之间的交互，来提供内存可见性保证</strong></p>
<blockquote>
<p>Java 中的 <code>volatile</code> 关键字可以保证多线程操作共享变量的可见性已经禁止指令重排序，<code>synchronized</code> 关键字不仅保证可见性，同时也保证了原子性（互斥性）。在更底层，JMM 通过内存屏障来实现内存的可见性以及禁止重排序。</p>
<p>为了程序员的方便理解，提出了 <code>happens-before</code>，它更加的简单易懂，从而避免了程序员为了理解内存可见性而且协议复杂的重排序规则以及这些规则的实现方法。这里涉及到的所有内容后面都会有专门的章节去介绍。</p>
</blockquote>
<h4>6.2.3 JMM 与 Java 内存区域划分的区别和联系</h4>
<p>上面两小节分别提到了 JMM 和 Java 运行时内存区域的划分，这两者既有差别又有联系：</p>
<ul>
<li>
<p>区别</p>
<p>两者是不同的概念层次。JMM 是抽象的，它是用来描述一组规则，通过这个规则来控制各个变量的访问方式，围绕原子性、有序性、可见性等展开的。而 Java 运行时内存的划分是具体的，是 JVM 运行 Java 程序时，必要的内存划分。</p>
</li>
<li>
<p>联系</p>
<p>都存在私有数据区域和共享数据区域。一般来说，JMM 中的主内存属于共享数据区域，它是包含了堆和方法区；同样，JMM 中的本地内存属于私有数据区域，包含了程序计数器、本地方法栈、虚拟机栈。</p>
</li>
</ul>
<p><strong>实际上，他们表达的是同一种含义，这里不做区分。</strong></p>
<h2>七、重排序与 happens-before</h2>
<h3>7.1 什么是重排序</h3>
<p>计算机在执行程序时，为了提高性能，编译器和处理器常常会对指令做重排。</p>
<p><strong>为什么指令重排序可以提高性能？</strong></p>
<p>简单地说，每一个指令都会包含多个步骤，每个步骤可能使用不同的硬件。因此，<strong>流水线技术</strong> 产生了，它的原理是指令 1 还没有执行完，就可以开始执行指令 2，而不用等到指令 1 执行结束之后再执行指令 2，这样就大大提高了效率。</p>
<p>但是，流水线技术最害怕 <strong>中断</strong>，恢复中断的代价是比较大的，所以我们要想尽办法不让流水线中断。指令重排就是减少中断的一种技术。</p>
<p>我们分析一下下面这个代码的执行情况：</p>
<pre><code class="language-java">a = b + c;
d = e + f;
</code></pre>
<p>先加载 b、c（<strong>注意，即有可能先加载 b，也有可能先加载 c</strong>），但是在执行 <code>add(b,c)</code> 的时候，需要等待 b、c 装载结束才能继续执行，也就是增加了停顿，那么后面的指令也会一次有停顿，这降低了计算机的执行效率。</p>
<p>为了减少这个停顿，我们可以先加载 e 和 f，然后哦再去加载 <code>add(b,c)</code>，这样做对程序（串行）是没有影响的，但却减少了停顿。既然 <code>add(b,c)</code> 需要停顿，那还不如去做一些有意义的事情。</p>
<p>综上所述，<strong>指令重排对于提高 CPU 处理性能十分必要。虽然由此带来了乱序问题，但是这点牺牲是值得的。</strong></p>
<p>指令重排一般分为一下三种：</p>
<ul>
<li>
<p><strong>编译器优化重排</strong></p>
<p>编译器在 <strong>不改变单线程程序语义</strong> 的前提下，可以重新安排语句的执行顺序。</p>
</li>
<li>
<p><strong>指令并行重排</strong></p>
<p>现代处理器采用了指令级并行技术来将多条指令重叠执行。如果 <strong>不存在数据依赖性</strong> （即后一个执行的语句无需依赖前面执行的语句的结果），处理器可以改变语句对应的机器指令的执行顺序。</p>
</li>
<li>
<p>内存系统重排</p>
<p>由于处理器使用缓存和读写缓冲区，这使得加载（load）和存储（store）操作看上去可能是在乱序执行，因为三级缓存的存在，导致内存与缓存的数据同步存在时间差。</p>
</li>
</ul>
<p><strong>指令重排可以保证串行语义一致，但是没有义务保证多线程间的语义也一致。</strong> 所以在多线程下，指令重排序可能会导致一些问题。</p>
<h3>7.2 顺序一致性模型与 JMM 的保证</h3>
<p>顺序一致性模型是一个<strong>理论参考模型</strong>，内存模型在设计的时候都会以顺序一致性内存模型作为参考。</p>
<h4>7.2.1 数据竞争与顺序一致性</h4>
<p>当程序未正确同步的时候，就可能存在数据竞争。</p>
<blockquote>
<p>数据竞争：在一个线程中写一个变量，在另一个线程读同一个变量，并且写和读没有通过同步来排序。</p>
</blockquote>
<p>如果程序中包含了数据竞争，那么运行的结果往往充满了 <strong>不确定性</strong>，比如读发生在写之前，可能会就会读到错误的值；如果一个线程程序能够正确同步，那么就不存在数据竞争。</p>
<p>Java 内存模型（JMM）对于正确同步多线程程序的内存一致性做了一下保证：</p>
<blockquote>
<p><strong>如果程序是正确同步的，程序的执行将具有顺序一致性。</strong> 即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。</p>
</blockquote>
<p>这里的同步包括了使用 <code>volatile</code>、<code>final</code>、<code>synchronized</code> 等关键字来实现多线程下的同步。</p>
<p>如果程序员没有正确使用  <code>volatile</code>、<code>final</code>、<code>synchronized</code>，那么即便是使用了同步（单线程下的同步），JMM 也不会有内存可见性的保证，可能会导致你的程序出错，并且具有不可重现性，很难排查。</p>
<p>所以如何正确使用  <code>volatile</code>、<code>final</code>、<code>synchronized</code> 是程序员应该去了解的，后面会有专门的章节介绍这几个关键字的内存语义及使用。</p>
<h4>7.2.2 顺序一致性模型</h4>
<p>顺序一致性内存模型是一个 <strong>理想化的理论参考模型</strong>，它为程序员提供了极强的内存可见性保证。</p>
<p>顺序一致性模型有两大特性：</p>
<ul>
<li>一个线程中的所有操作必须按照程序的顺序（即 Java 代码的顺序）来执行。</li>
<li>不管程序是否同步，所有线程都只能看到一个单一的操作执行顺序。即在顺序一致性模型中，每个操作必须是<strong>原子性的，且立刻对所有线程可见</strong>。</li>
</ul>
<p>为了立即这两个特性，我们举个例子，假设有两个线程 A 和线程 B 并发执行，线程 A 有3个操作，他们在程序中的顺序是 A1 -&gt; A2 -&gt; A3，线程 B 也有3个操作，B1 -&gt; B2 -&gt; B3。</p>
<p>假设 <strong>正确使用了同步</strong>，A 线程的3个操作执行后释放锁，B 线程获取同一个锁。那么在 <strong>顺序一致性模型</strong>中的执行效果如下所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220214113555157.png" alt="image-20220214113555157"></p>
<p>操作的执行整体上有序，并且两个线程都只能看到这个执行顺序。</p>
<p>假设 <strong>没有使用同步</strong>，那么在<strong>顺序一致性模型</strong>中农的执行效果如下所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220214113625114.png" alt="image-20220214113625114"></p>
<p>操作的执行整体上无序，但是两个线程都只能看到这个执行顺序，之所以可以得到这个保证，是因为顺序一致性模型中<strong>每个操作必须立即对任意线程可见。</strong></p>
<p><strong>但是 JMM 没有这样的保证。</strong></p>
<p>比如，在当前线程把写过的数据缓存在本地内存中，在没有刷新到主内存之前，这个写操作仅对当前线程可见；从其他线程的角度来观察，这个写操作根本没有被当前线程所执行。只有当前线程把本地内存中写过的数据刷新到主内存之后，这个写操作才会对其他线程可见。在这种情况下，当前线程和其他线程看到的执行顺序是不一样的。</p>
<h4>7.2.3 JMM 中同步程序的顺序一致性结果</h4>
<p>在顺序一致性模型中，所有操作完全按照程序的顺序串行执行。但是 JMM 中，临界区（同步块或同步方法中）的代码可以发生重排序（但不允许临界区内的代码 “逃逸” 到临界区之外，因为会破坏锁的内存语义）。</p>
<p>虽然线程 A 在临界区做了重排序，但是锁的特性，线程 B 无法观察到线程 A 在临界区的重排序。这种重排序既提高了执行效率，又没有改变程序的执行结果。</p>
<p>同时，JMM 会在退出临界区和进入临界区做特殊的处理，使得在临界区内程序获得与顺序一致性模型相同的内存视图。</p>
<p><strong>由此可见，JMM 的具体实现方针是：在不改变（正确同步的）程序执行结果的前提下，尽量为编译器和处理器的优化打开方便之门。</strong></p>
<h4>7.2.4 JMM 中未同步程序的顺序一致性结果</h4>
<p>对于未同步的多线程程序，JMM 只提供 <strong>最小安全性</strong>：线程读取到的值，要么是之前某个线程写入的值，要么是默认值，不会无中生有。</p>
<p>为了实现这个安全性，JVM 在堆上分配对象时，首先会对内存空间清零，然后才会在上面分配对象（这两个操作是同步的）。</p>
<p><strong>JMM 没有保证未同步程序的执行结果与该程序在顺序一致性中执行结果一致。因为如果要保证执行结果一致，那么 JMM 需要禁止大量的优化，对程序的执行性能会产生很大的影响。</strong></p>
<p>为同步程序在 JMM 和顺序一致性内存模型中的执行特性有如下差异：</p>
<ol>
<li>顺序一致性保证单线程内的操作会按程序的顺序执行；JMM 不保证单线程内的操作会按照程序的顺序执行（因为重排序，但是 JMM 保证单线程下的重排序不影响执行结果）。</li>
<li>顺序一致性模型保证所有线程只能看到一致的操作执行顺序，而 JMM 不保证所有线程都看到一致的操作执行顺序（因为 JMM 不保证所有的操作立即可见）。</li>
<li>顺序一致性模型保证对所有的内存读写操作都具有原子性，而 JMM 不保证大对 64位的 <code>long</code> 型和 <code>double</code> 型变量的写操作具有原子性。</li>
</ol>
<h3>7.3 happens-before</h3>
<h4>7.3.1 什么是 happens-before</h4>
<p>一方面，程序员需要 JMM 提供一个强得多内存模型来编写代码；另一方面，编译器和处理器希望 JMM 对它们的束缚越少越好，这样它们就可以最可能多的做优化来提高性能，希望的是一个弱的内存模型。</p>
<p>JMM 考虑了这两种需求，并且找到了平衡点，对编译器和处理器来说，<strong>只要不改变程序执行结果（单线程程序和正确同步了的多线程程序），编译器和处理器怎么优化都行。</strong></p>
<p>而对于程序员，JMM 提供了 **happens-before规则 **（JSR-133规范），满足了程序员的需求--<strong>简单易懂，并且提供了足够强的内存可见性。</strong> 换言之，程序员只要遵循 happens-before 规则，那他写的程序就能保证在 JMM 中具有强的内存可见性。</p>
<p>JMM 使用 happens-before 的概念来制定两个操作之间的执行顺序。这两个操作可以在一个线程内，也可以是不同的线程之内。因此，JMM 可以通过 happens-before 关系向程序员提供跨线程的内存可见性保证。</p>
<p>happens-before 关系的定义如下：</p>
<ol>
<li>如果一个操作 happens-before 另一个操作，那么第一个操作的执行结果将对第二个操作可见，而且第一个操作的执行顺序排在第二个操作之前。</li>
<li><strong>两个操作之间存在 happens-before 关系，并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果，与按 happens-before 关系来执行的结果一致，那么 JMM 也允许这样的重排序。</strong></li>
</ol>
<p>happens-before 关系本质上和 as-if-serial 语义是一回事。</p>
<p>as-if-serial 语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的，happens-before 关系保证正确同步的多线程程序的执行结果不被重排序改变。</p>
<p>总之，<strong>如果操作 A happens-before 操作 B，那么操作 A 在内存上所做的操作对操作 B 都是可见的，不管他们在不在一个线程。</strong></p>
<h4>7.3.2 天然的 happens-before 关系</h4>
<p>在 Java 中，有以下天然的 happens-before 关系：</p>
<ul>
<li>程序顺序规则：一个线程中的每个操作，happens-before 于该线程中的任意后续操作。</li>
<li>监视器锁规则：对一个锁的解锁，happens-before 于随后对这个锁的加锁。</li>
<li>volatile 变量规则：对一个 <code>volatile</code>域的写，happens-before 于任意后续对这个 <code>volatile</code>域的读。</li>
<li>start 规则：如果线程 A 执行操作 <code>ThreadB.start()</code> 启动线程 B，那么 A 线程的 <code>ThreadB.start()</code> 操作 happens-before 于线程 B 中的任意操作。</li>
<li>join 规则：如果线程 A 执行操作 <code>ThreadB.join()</code> 并成功返回，那么线程 B 中任意操作 happens-before 于线程 A 从 <code>ThreadB,join</code> 操作成功返回。</li>
</ul>
<p>举例：</p>
<pre><code class="language-java">int a = 1; // A 操作
int b = 2; // B 操作
int sum = a + b; // C 操作
System.out.println(sum);
</code></pre>
<p>根据以上介绍的 happens-before 规则，假如只有一个线程，那么不难得出：</p>
<pre><code>A happens-before B
B happens-before C
A happens-before C
</code></pre>
<p>注意，真正在执行指令的时候，其实 JVM 有可能对 操作 A 和 操作 B 进行重排序，因为无论先执行 A 还是 B，它们都对对方是可见的，并且不影响执行结果。</p>
<p>如果这里发生了重排序，这在视觉上违背了 happens-before 原则，但是 JMM 是允许这样的重排序的。</p>
<p>所以，我们只关心 happens-before 规则，不用关心 JVM 到底是怎样执行的。只要确定操作 A happens-before 操作 B 就行了。</p>
<p>重排序有两类，JMM 对这两类重排序有不同的策略：</p>
<ul>
<li>会改变程序执行结果的重排序，比如 A -&gt; C，JMM 要求编译器和处理器都禁止这种重排序。</li>
<li>不会改变程序执行结果的重排序，比如 A -&gt; B，JMM 对编译器和处理器不做要求，允许这种重排序。</li>
</ul>
<h2>八、volatile</h2>
<h3>8.1 几个基本概念</h3>
<p>在介绍 <code>volatile</code> 之前，我们先回顾及介绍几个基本的概念。</p>
<h4>8.1.1 内存可见性</h4>
<p>在 Java 内存模型那一章我们介绍了 JMM 有一个主内存，每个线程都有自己私有的工作内存，工作内存中保存了一些变量在主内存的拷贝。</p>
<p><strong>内存可见性，指的是线程之间的可见性，当一个线程修改了共享变量时，另一个线程可以读取到这个修改后的值。</strong></p>
<h4>8.1.2 重排序</h4>
<p>为优化程序性能，对原有的指令执行顺序进行优化重新排序。重排序可能发生在多个阶段，比如编译重排序、CPU 重排序等。</p>
<h4>8.1.3 happens-before 规则</h4>
<p>是一个给程序员使用的规则，只要程序员在写代码的时候遵循 happens-before 规则，JVM 就能保证指令在多线程之间的顺序性符合程序员的预期。</p>
<h3>8.2 volatile 的内存语义</h3>
<p>在 Java 中，<code>volatile</code> 关键字有特殊的内存语义。<code>volatile</code> 主要有一下两个功能：</p>
<ul>
<li>保证变量的 **内存可见性 **</li>
<li>禁止 <code>volatile</code> 变量与普通变量 <strong>重排序</strong> （ JSR-133 提出，Java 5 开始才有了这个 “增强的 volatile 内存语义”）</li>
</ul>
<h4>8.2.1 内存可见性</h4>
<pre><code class="language-java">public class VolatileDemo {

    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        // step 1
        a = 1;
        // step 2
        flag = true;
    }

    public void reader() {
        // step 3
        if (flag) {
            // step 4
            System.out.println(a);
        }
    }
}
</code></pre>
<p>在这段代码里，我们使用 <code>volatile</code> 关键字修饰了一个 <code>boolean</code> 类型的变量 <code>flag</code>。</p>
<p>所谓内存可见性，指的是当一个线程对 <code>volatile</code> 修饰的变量进行 <strong>写操作</strong>（比如 step 2）时，JMM 会立即把该线程对应的本地内存中的共享变量的值刷新到主内存；当一个线程对 <code>volatile</code> 修饰的变量进行 <strong>读操作</strong>（比如 step 3）时，JMM 会立即把该线程对应的本地内存置为无效，从主内存中读取共享变量的值。</p>
<blockquote>
<p>在这一点上，<code>volatile</code> 与锁具有相同的内存效果，<code>volatile</code> 变量的写和锁的释放具有相同的内存语义，<code>volatile</code> 变量的读和锁的获取具有相同的内存语义。</p>
</blockquote>
<p>假设在时间线上，线程 A 先执行方法 <code>writer()</code> 方法，线程 B 后执行 <code>reader()</code> 方法。那必然会有下图：</p>
<p><img src="https://img.dyzmj.top/img/image-20220214154730016.png" alt="image-20220214154730016"></p>
<p>而如果 <code>flag</code> 变量没有用 <code>volatile</code> 修饰，在 step 2，线程 A 的本地内存里面的变量就不会立即更新到主内存，那随后线程 B 也同样不会去主内存拿最新的值，仍然使用线程 B 本地内存缓存的变量的值 <code>a=0,flag=false</code>.</p>
<h4>8.2.1 禁止重排序</h4>
<p>在 JSR-133 之前的旧的 Java 内存模型中，是允许 <code>volatile</code> 变量与普通变量重排序的。那上面的案例中，可能就会被重排序成下列顺序来执行：</p>
<ol>
<li>线程 A 写 <code>volatile</code> 变量，step 2，设置 <code>flag</code> 为 <code>true</code>；</li>
<li>线程 B 读同一个 <code>volatile</code>，step 3，读取到 <code>flag</code> 为 <code>true</code>；</li>
<li>线程 B 读普通变量，step 4，读取到 <code>a = 0</code>；</li>
<li>线程 A 修改普通变量，step 1，设置 <code>a = 1</code>。</li>
</ol>
<p>可见，如果 <code>volatile</code> 变量与普通变量发生了重排序，虽然 <code>volatile</code> 变量能保证内存可见性，也可能导致普通变量读取错误。</p>
<p>所以在旧的内存模型中，<code>volatile</code> 的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的 <strong>线程间的通信机制</strong>，<strong>JSR-133</strong> 专家组决定增强 <code>volatile</code> 的内存语义：严格限制编译器和处理器对 <code>volatile</code> 变量与普通变量的重排序。</p>
<p>编译器还好说，JVM 是怎么还能限制处理器的重排序呢？它是通过 <strong>内存屏障</strong> 来实现的。</p>
<p><strong>什么是内存屏障？</strong></p>
<p>硬件层面上，内存屏障分为两种：读屏障（Load Barrier）和写屏障（Store Barrier）。内存屏障有两个作用：</p>
<ol>
<li>阻止屏障两侧的指令重排序；</li>
<li>强制把写缓冲区/高速缓存中的脏数据等写回主内存，或者让缓存中相应的数据失效。</li>
</ol>
<blockquote>
<p>注意这里的缓存只要指的是 CPU 缓存，如 L1，L2 等。</p>
</blockquote>
<p>编译器在 <strong>生成字节码时</strong>，会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个 <strong>比较保守的 JMM 内存屏障插入策略，</strong> 这样可以保证在任何处理器平台， 任何程序中都能得到正确的 <code>volatile</code> 内存语义。这个策略是：</p>
<ul>
<li>在每个 <code>volatile</code> 写操作前插入一个 StoreStore 屏障；</li>
<li>在每个 <code>volatile</code> 写操作后插入一个 StoreLoad 屏障；</li>
<li>在每个 <code>volatile</code> 读操作前插入一个 LoadLoad 屏障；</li>
<li>在每个 <code>volatile</code> 读操作后插入一个 LoadStore 屏障；</li>
</ul>
<p>大概示意图如下：</p>
<p><img src="https://img.dyzmj.top/img/image-20220214165126663.png" alt="image-20220214165126663"></p>
<p>在逐个解释一下这几个屏障。注：下述 Load 代表读操作，Store 代表写操作。</p>
<blockquote>
<p><strong>LoadLoad 屏障：</strong> 对于这样的语句 Load1；LoadLoad；Load2，在 Load2 及后续读取操作要读取的数据被访问前，保证 Load1 要读取的数据被读取完毕。</p>
<p><strong>StoreStore 屏障：</strong> 对于这样的语句 Store1；LoadStore；Store2，在 Store2 及后续写入操作执行前，这个屏障会把 Store1 强制刷新到内存，保证 Store1 的写入操作对其他处理器可见。</p>
<p><strong>LoadStore 屏障：</strong> 对于这样的语句 Load1；LoadStore；Store2，在 Store2 及后续写入操作被刷出前，保证 Load1 要读取的数据被读取完毕。</p>
<p><strong>StoreLoad 屏障：</strong> 对于这样的语句 Store1；StoreLoad；Load2，在 Load2 及后续所有读取操作执行前，保证 Store1 的写入对所有的处理器可见。它的开销是四种屏障中最大的（冲刷写缓冲器，清空无效化队列）。在大多数处理器的实现中，这个屏障是个万能屏障，兼具其他三种内存屏障的功能。</p>
</blockquote>
<p>对于连续多个 <code>volatile</code> 变量读或者连续多个 <code>volatile</code>变量写，编译器做了一定的优化来提高性能，比如：</p>
<blockquote>
<p>第一个 <code>volatile</code> 读；</p>
<p>LoadLoad 屏障；</p>
<p>第二个 <code>volatile</code> 读；</p>
<p>LoadStore 屏障。</p>
</blockquote>
<p>再介绍一下 <code>volatile</code> 与普通变量的重排序规则：</p>
<ol>
<li>如果第一个操作是 <code>volatile</code> 读，那无论第二个操作是什么，都不能重排序；</li>
<li>如果第二个操作是 <code>volatile</code> 写，那无论第一个操作是什么，都不能重排序；</li>
<li>如果第一个操作是 <code>volatile</code> 写，第二个操作是 <code>volatile</code>读，那不能重排序。</li>
</ol>
<p>举一个例子，我们在案例中 step 1，是普通变量的写，step 2 是 <code>volatile</code> 变量的写，那符合第2个规则，这两个 steps 不能重排序。而 step 3 是 <code>volatile</code> 变量读。step 4 是普通变量读，符合第1个规则，同样不能重排序。</p>
<p>但如果是下列情况：第一个操作是普通变量的读，第二个操作是 <code>volatile</code> 变量读，那是可以重排序的：</p>
<pre><code class="language-java">// 声明普通变量
int a = 0;
// 声明 volatile 变量
volatile boolean flag = false;

// 以下两个变量的读操作是可以重排序的
// 普通变量读
int i = a;
// volatile 变量读
boolean j = flag;
</code></pre>
<h3>8.3 volatile 的用途</h3>
<p>从 <code>volatile</code> 的内存语义上来看，<code>volatile</code> 可以保证内存可见性且禁止重排序。</p>
<p>在保证内存可见性这一点上，<code>volatile</code> 有着与锁相同的内存语义，所以可以作为一个 “轻量级” 的锁来使用。但由于 <code>volatile</code> 仅仅保证对单个 <code>volatile</code> 变量的读/写具有原子性，而锁可以保证整个 <strong>临界区代码</strong> 的执行具有原子性。所以<strong>在功能上，锁比 <code>volatile</code> 更强大；在性能上，<code>volatile</code> 更有优势。</strong></p>
<p>在禁止重排序这一点上，<code>volatile</code>也是非常有用的。比如我们熟悉的单例模式，其中有一种实现方式是 “双重锁检查”，比如这样的代码：</p>
<pre><code class="language-java">public class SingletonDemo {
    // 不使用 volatile 关键字
    private static SingletonDemo instance;

    // 双重锁检验
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SignalDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

}
</code></pre>
<p>如果这里的变量声明不使用 <code>volatile</code> 关键字，是可能会发生错误的。它可能会被重排序：</p>
<pre><code class="language-java"> instance = new SingletonDemo();
 
 // 可以分解为以下三个步骤：
 1. memory = allocate();  // 分配内存，相当于c 的 malloc
 2. ctorInstance(memory); // 初始化对象
 3. s = memory;		 	  // 设置s指向刚分配的地址
 
 // 上述3个步骤可能会别重排序为 1-3-2，也就是
 1. memory = allocate();  // 分配内存，相当于c 的 malloc
 3. s = memory;		 	  // 设置s指向刚分配的地址
 2. ctorInstance(memory); // 初始化对象
</code></pre>
<p>而一旦假设发生了这样的重排序，比如线程 A 在第10行指向了步骤 1  和步骤 3，但是步骤 2 还没有执行完。这个时候另一个线程 B 执行到第 7 行，它会判定 <code>instance</code>不为空，然后直接返回一个未初始化完成的 <code>instance</code>!</p>
<p>所以 JSR-133 对 <code>volatile</code> 做了增强后，<code>volatile</code> 的禁止重排序功能还是非常有用的。</p>
<h2>九、synchronized 与锁</h2>
<p>这篇文章我们来聊一聊 Java 多线程里面的 “锁”。</p>
<p>首先需要明确一点的是：<strong>Java 多线程的锁都是基于对象的，</strong> Java 中的每一个对象都可以作为一个锁。</p>
<p>还有一点需要注意的是，我们常听到的 <strong>类锁</strong> 其实也是对象锁。</p>
<p>Java 类只有一个 Class 对象（可以由多个实例对象，对个实例对象共享这个 Class 对象），而 Class 对象也是特殊的 Java 对象。所以我们常说的类锁，其实就是 Class 对象的锁。</p>
<h3>9.1 synchronized 关键字</h3>
<p>说到锁，我们通常会谈到 <code>synchronized</code> 这个关键字，它翻译成中文就是 “同步” 的意思。</p>
<p>我们通常使用 <code>synchronized</code> 关键字来给一段代码或一个方法上锁，它通常有以下三种形式：</p>
<pre><code class="language-java">// 关键字在静态方法上，锁为当前 Class 对象
public static synchronized void classLock() {
	// code
}

// 关键字在实例方法上，锁为当前实例
public synchronized void instanceLock() {
	// code
}

// 关键字在代码块上，锁为括号里面的对象
public void blockLock() {
	Object o = new Object();
	synchronized (o) {
		// code
	}
}
</code></pre>
<p>我们这里介绍一下 “临界区” 的概念。所谓 “临界区”，指的是某一块代码区域，它同一时刻只能由一个线程执行。在上面的例子中，如果 <code>synchronized</code> 关键字在方法上，那临界区就是就是整个方法内部。而如果是使用 <code>synchronized</code> 代码块，那临界区就指的是代码块内部的区域。</p>
<p>通过上面的例子我们可以看到，下面这两个写法其实是等价的作用：</p>
<pre><code class="language-java">// 关键字在实例方法上，锁为当前实例
public synchronized void instanceLock() {
	// code
}

// 关键字在代码块上，锁为括号里面的对象
public void blockLock() {
	Object o = new Object();
	synchronized (o) {
		// code
	}
}
</code></pre>
<p>同理，下面这两个方法也应该是等价的：</p>
<pre><code class="language-java">// 关键字在静态方法上，锁为当前 Class 对象
public static synchronized void classLock() {
	// code
}

// 关键字在代码块上，锁为括号里面的对象
public void blockLock(){
	synchronized (this.getClass()){
		// code
	}
}
</code></pre>
<h3>9.2 几种锁</h3>
<p>Java 6 为了减少获得锁和释放锁带来的性能消耗，引入了 “偏向锁” 和 “轻量级锁”。在 Java 6 以前，所有的锁都是 “重量级” 锁。所以在 Java 6 及其以后，一个对象其实有四种锁状态，它们级别由低到高依次是：</p>
<ol>
<li>无锁状态</li>
<li>偏向锁状态</li>
<li>轻量级锁状态</li>
<li>重量级锁状态</li>
</ol>
<p>无锁就是没有对资源进行锁定，任何线程都可以尝试去修改它，无锁在这里不再细讲。</p>
<p>几种锁会随着竞争情况逐渐升级，锁的升级很容易发生，但是锁降级发生的条件会比较苛刻，锁降级发生在 <code>Stop The World</code> 期间，当 JVM 进入安全点的时候，会检查是否有闲置的锁，然后进行降级。</p>
<blockquote>
<p>关于锁降级有两点说明：</p>
<p>1、不同于大部分文章说的锁不能降级，实际上 HotSpot JVM 是支持锁降级的（<a href="https://www.jianshu.com/p/9932047a89be">JVM 锁降级</a>）。</p>
<p>2、上面提到的 <code>Stop The World</code> 期间，以及安全点，这些知识属于 JVM 的知识范畴，本文不做细讲。</p>
</blockquote>
<p>下面分辨介绍这几种锁以及他们之间的升级。</p>
<h4>9.2.1 Java 对象头</h4>
<p>前面我们提到，Java 的锁都是基于对象的。首先我们来看看一个对象的 “锁” 的信息是存在什么地方的。</p>
<p>每个 Java 对象都有对象头。如果是非数据类型，则用 2 个字宽来存储对象头，如果是数组，则会用 3 个字宽来存储对象头。在 32 位处理器中，一个字宽是32位；在62位处理器中，一个字宽是64位。对象头的内容如下表：</p>
<table>
<thead>
<tr>
<th>长度</th>
<th>内容</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>32/64 bit</td>
<td>Mark Word</td>
<td>存储对象的 hashCode 或锁信息等</td>
</tr>
<tr>
<td>32/64 bit</td>
<td>Class Metadata Address</td>
<td>存储到对象类型数据的指针</td>
</tr>
<tr>
<td>32/64 bit</td>
<td>Array length</td>
<td>数组的长度（如果是数组）</td>
</tr>
</tbody>
</table>
<p>我们主要来看看 <code>Mark World</code> 的格式：</p>
<table>
<thead>
<tr>
<th>锁状态</th>
<th>29 bit 或 61 bit</th>
<th>1 bit 是否是偏向锁</th>
<th>2 bit 锁标志位</th>
</tr>
</thead>
<tbody>
<tr>
<td>无锁</td>
<td></td>
<td>0</td>
<td>01</td>
</tr>
<tr>
<td>偏向锁</td>
<td>线程 ID</td>
<td>1</td>
<td>01</td>
</tr>
<tr>
<td>轻量级锁</td>
<td>指向栈中锁记录的指针</td>
<td>此时这一位不用于标识偏向锁</td>
<td>00</td>
</tr>
<tr>
<td>重量级锁</td>
<td>指向互斥量(重量级锁)的指针</td>
<td>此时这一位不用于标识偏向锁</td>
<td>10</td>
</tr>
<tr>
<td>GC 标记</td>
<td></td>
<td>此时这一位不用于标识偏向锁</td>
<td>11</td>
</tr>
</tbody>
</table>
<p>可以看到，当对象状态为偏向锁是，<code>Mark Word</code> 存储的是偏向的线程 ID；当状态为轻量级锁时，<code>Mark Word</code> 存储的是指向线程栈中 <code>Lock Record</code> 的指针；当状态为重量级锁时，<code>Mark Word</code> 为指向堆中的 <code>monitor</code> 对象的指针。</p>
<h4>9.2.2 偏向锁</h4>
<p>Hotspot 的作者经过以往的研究发现大多数情况下 <strong>锁不仅不存在多线程竞争，而且总是由同一线程多次获得</strong>，于是引入来了偏向锁。</p>
<p>偏向锁会偏向于第一个访问锁的线程，如果在接下来的运行过程中，该锁没有被其他的线程访问，则持有偏向锁的线程将永远不需要触发同步。也就是说，<code>偏向锁在资源无竞争情况下消除了同步语句，连 CAS 操作都不做了，提高了程序的运行性能。</code></p>
<blockquote>
<p>大白话就是对锁设置个变量，如果发现为 <code>true</code>，代表资源无竞争，则无需再走各种加锁/解锁流程。如果为 <code>false</code>，代表存在其他线程竞争资源，那么就会走后面的流程。</p>
</blockquote>
<p><strong>实现原理</strong></p>
<p>一个线程在第一次进入同步块时，会在对象头和栈帧中的锁记录里存储锁的偏向的线程 ID。当下次该线程进入这个同步块时，会去检查锁的 <code>Mark Word</code> 里面是不是放的自己的线程 ID。</p>
<p>如果是，表明该线程已经获得了锁，以后该线程在进入和退出同步块时不需要花费 <code>CAS</code> 操作来加锁和解锁。</p>
<p>如果不是，就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 <code>CAS</code> 来替代 <code>Mark Word</code> 里面的线程 ID 为新线程的 ID，这个时候要分两种情况：</p>
<ul>
<li>成功：表示之前的线程不存在了，<code>Mark Word</code> 里面的线程 ID 为新线程的 ID，锁不会升级，仍然为偏向锁；</li>
<li>失败：表示之前的线程仍然存在，那么暂停之前的线程，设置偏向锁标识为0，并设置锁标志位为 00，升级为轻量级锁，会按照轻量级锁的方式进行竞争锁。</li>
</ul>
<blockquote>
<p>CAS: Compare And Swap</p>
<p>比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中，比较并交换通过指令 cmpxchg 实现。</p>
<p>比较是否和给定的数值一致，如果一致则修改，不一致则不修改。</p>
</blockquote>
<p>线程竞争偏向锁的过程如下：</p>
<p><img src="https://img.dyzmj.top/img/image-20220215105329822.png" alt="image-20220215105329822"></p>
<p>图中涉及到了 <code>lock record</code> 指针指向当前堆栈中的最近一个 <code>lock record</code>，是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。</p>
<p><strong>撤销偏向锁</strong></p>
<p>偏向锁使用了一种 <strong>等到竞争出现才释放锁的机制</strong>，所以当其他线程尝试竞争偏向锁时，持有偏向锁的线程才会释放锁。</p>
<p>偏向锁升级成轻量级锁时，会暂停用于偏向锁的线程，重置偏向锁标识，这个过程看起来容易，实则开销还是很大的，大概的过程如下：</p>
<ol>
<li>在一个安全点（在这个时间点上没有字节码正在执行）停止拥有锁的线程。</li>
<li>遍历线程栈，如果存在锁记录的话，需要修复锁记录和 <code>Mark Word</code>，使其变成无锁状态。</li>
<li>唤醒被停止的线程，将当前锁升级成轻量级锁。</li>
</ol>
<p>所以，如果应用程序里所有的锁通常处于竞争状态，那么偏向锁就会是一种累赘，对于这种情况，我们一开始就把偏向锁这个默认功能给关闭：</p>
<pre><code class="language-java">-XX:UseBiasedLocking=false
</code></pre>
<p>下面这个经典的图总结了偏向锁的获得和撤销：</p>
<p><img src="https://img.dyzmj.top/img/image-20220215114249975.png" alt="image-20220215114249975"></p>
<h4>9.2.3 轻量级锁</h4>
<p>多个线程在不同时段获取同一把锁，即不存在锁竞争的情况，也就是没有线程阻塞。针对这种情况，JVM 采用轻量级锁来避免线程的阻塞与唤醒。</p>
<p><strong>轻量级锁的加锁</strong></p>
<p>JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间，我们称为 <code>Displaced Mark Word</code>。如果一个线程获得锁的时候发现是轻量级锁，会把锁的 <code>Mark Word</code> 复制到自己的 <code>Displaced Mark Word</code> 里面。</p>
<p>然后线程尝试用 <code>CAS</code> 将锁的 <code>Mark Word</code> 替换为指向锁记录的指针。如果成功，当前线程获得锁，如果失败，表示 <code>Mark Word</code> 已经被替换成了其他线程的锁记录，说明在于其他线程竞争锁，当前线程就尝试使用自旋来获取锁。</p>
<blockquote>
<p>自旋：不断尝试去获取锁，一般用循环来实现。</p>
</blockquote>
<p>自旋是需要消耗 CPU 的，如果一直获取不到锁的话，那该线程就一直处在自旋状态，白白浪费 CPU 资源。解决这个问题最简单的办法就是指定自旋的次数，例如让其循环10次，如果还没获取到锁就进入阻塞状态。</p>
<p>但是 JDK 采用了更聪明的方式 ——适应性自旋，简单来说就是线程如果自旋成功了，则下次自旋的次数会更多，如果自旋失败了，则自旋的次数就会减少。</p>
<p>自旋也不是一直进行下去的，如果自旋到一定程度（和 JVM、操作系统相关），依然没有获取到锁，称为自旋失败，那么这个线程会阻塞。同时这个锁就会 <strong>升级成重量级锁</strong>。</p>
<p><strong>轻量级锁的释放：</strong></p>
<p>在释放锁时，当前线程会使用 <code>CAS</code> 操作将 <code>Displaced Mark Word</code> 的内容复制回锁的 <code>Mark Word</code> 里面。如果没有发生竞争，那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁，那么 <code>CAS</code> 操作会失败，此时会释放锁并唤醒被阻塞的线程。</p>
<p>一张图说明加锁和释放锁的过程：</p>
<p><img src="https://img.dyzmj.top/img/image-20220215114215718.png" alt="image-20220215114215718"></p>
<h4>9.2.4 重量级锁</h4>
<p>重量级锁依赖于操作系统的互斥量（mutex）实现的，而操作系统中线程间状态的转换需要相对比较长的时间，所以重量级锁效率很低，但被阻塞的线程不会消耗 CPU。</p>
<p>前面说到，每一个对象都可以当做一个锁，当多个线程同时请求某个对象锁时，对象锁会设置几种状态用来区分请求的线程：</p>
<pre><code class="language-java">Contention List：所有请求锁的线程将被首先放置到该竞争队列
Entry List：Contention List 中那些有资格成为候选人的线程被移到 Entry List
Wait Set：那些调用 wait 方法被阻塞的线程被放置到 Wait List
OnDeck：任何时刻最多只有一个线程正在竞争锁，该线程称为 OnDeck
Owner：获得锁的线程称为 Owner
!Owner：释放锁的线程
</code></pre>
<p>当一个线程尝试获得锁时，如果该锁已经被占用，则会将该线程封装成一个 <code>ObjectWaiter</code> 对象插入到 <code>Contention List</code> 的队列的队首，然后调用 <code>park</code> 函数挂起当前线程。</p>
<p>当现场释放锁时，会从 <code>Contention List</code> 或 <code>Entry List</code> 中挑选一个线程唤醒，被选中的线程叫做 <code>Heirpresumptive</code> 即假定继承人，假定继承人被唤醒后会尝试获得锁，但 <code>synchronized</code> 是非公平的，所以假定继承人不一定能获得锁。这是因为对于重量级锁，线程先自旋尝试获得锁，这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说，稍微显得不公平，还有一个不公平的地方是自旋线程可能会抢占 Ready 线程的锁。</p>
<p>如果线程获得锁后调用 <code>Object.wait()</code> 方法，则会将线程假如到 <code>Wait Set</code> 中，当被 <code>Object.notify()</code> 唤醒后，会将线程从 <code>Wait Set</code> 移动到 <code>Contention List</code> 或 <code>Entry List</code> 中去。需要注意的是，当调用一个锁对象的 <code>wait</code> 或 <code>notify</code> 方法时，<strong>如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。</strong></p>
<h4>9.2.5 总结锁的升级流程</h4>
<p>每一个线程在准备获取共享资源时：</p>
<p>第一步，检查 <code>Mark Word</code>里面是不是放的自己的 <code>Thread ID</code>，如果是，表示当前线程是处于 “偏向锁”。</p>
<p>第二步，如果 <code>Mark Word</code> 不是自己的 <code>Thread ID</code>，锁升级，这时候用 <code>CAS</code> 来执行切换，新的线程根据 <code>Mark Word</code> 里面现有的 <code>Thread ID</code>，通知之前线程暂停，之前线程将 <code>Mark Word</code> 的内容置为空。</p>
<p>第三步，两个线程都把锁对象的 <code>HashCode</code> 复制到自己新建的用于存储锁的记录空间，接着开始通过 <code>CAS</code> 操作，把锁对象的 <code>Mark Word</code> 的内容修改为自己新建的记录空间的地址的方式竞争 <code>Mark Word</code>。</p>
<p>第四步，第三步中成功执行 <code>CAS</code> 的获得资源，失败的则进入自旋。</p>
<p>第五步，自旋的线程在自旋过程中，成功获得资源（即之前获得资源的线程执行完成并释放了共享资源），则整个状态依然处于轻量级锁的状态。如果自旋失败，则锁继续升级。</p>
<p>第六步，进入重量级锁的状态，这个时候，自旋的线程进行阻塞，等待之前线程执行完成并唤醒自己。</p>
<h4>9.2.6 各种锁的优缺点比较</h4>
<p>下表来自《Java 并发编程的艺术》：</p>
<table>
<thead>
<tr>
<th>锁</th>
<th>优点</th>
<th>缺点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>偏向锁</td>
<td>加锁和解锁不需要额外的消耗，和执行非同步方法相比仅存在纳秒级的差距</td>
<td>如果线程间存在锁竞争，会带来额外的锁撤销的消耗</td>
<td>适用于只有一个线程访问同步块场景</td>
</tr>
<tr>
<td>轻量级锁</td>
<td>竞争的线程不会阻塞，提高了程序的响应速度</td>
<td>如果始终得不到锁竞争的线程使用自旋会消耗CPU</td>
<td>追求响应时间，同步块执行速度非常快</td>
</tr>
<tr>
<td>重量级锁</td>
<td>线程进程不使用自旋，不会消耗 CPU</td>
<td>线程阻塞，响应时间缓慢</td>
<td>追求吞吐量，同步块执行时间较长</td>
</tr>
</tbody>
</table>
<h2>十、CAS 与原子操作</h2>
<h3>10.1 乐观锁与悲观锁的概念</h3>
<p>锁可以从不同的角度分类，其中，乐观锁和悲观锁是一种分类方式。</p>
<p><strong>悲观锁：</strong></p>
<p>悲观锁就是我们常说的锁。对于悲观锁来说，它总是认为每次访问共享资源时会发生冲突，所以必须对每次数据操作加上锁，已保证临界区的程序同一时间只能有一个线程在执行。</p>
<p><strong>乐观锁：</strong></p>
<p>乐观锁又称为 “无锁”，顾名思义，它是乐观派。乐观派总是假设对共享资源的访问没有冲突，线程可以不停地执行，无需加锁也无需等待。而一旦多个线程发生冲突，乐观锁通常是使用一种称为 <code>CAS</code> 的技术来保证线程执行的安全性。</p>
<p>由于无锁操作中没有锁的存在，因此不可能出现死锁的情况，也就是说 <strong>乐观锁天生免疫死锁。</strong></p>
<p>乐观锁多用于 “读多写少” 的环境，避免频繁加锁影响性能；而悲观锁多用于 “写多读少” 的环境，避免频繁失败和重试影响性能。</p>
<h3>10.2 CAS 的概念</h3>
<p><code>CAS</code> 的全称是：比较并交换（Compare And Swap）。在 <code>CAS</code> 中，有这样三个值：</p>
<ul>
<li>V：要更新的变量（var）</li>
<li>E：预期值（expected）</li>
<li>N：新值（new）</li>
</ul>
<p>比较并交换的过程如下：</p>
<p>判断 <code>V</code> 是否等于 <code>E</code>，如果等于，将 <code>V</code> 的值设置为 <code>N</code>；如果不等，说明已经有其他线程更新了 <code>V</code>，则当前线程放弃更新，什么都不做。</p>
<p>所以这里的 <strong>预期值 <code>E</code> 本质上指的是 “旧值”</strong>。</p>
<p>我们以一个简单的例子来解释这个过程：</p>
<ol>
<li>如果有一个多线程共享的变量 <code>i</code> 原本等于5，我现在在线程 A 中，想把它设置为新的值6；</li>
<li>我们使用 <code>CAS</code> 来做这个事情；</li>
<li>首先我们用 <code>i</code> 去与5对比，发现它等于5，说明没有被其他线程改过，那我就把它设置为新的值6，此次 <code>CAS</code> 成功，<code>i</code> 的值被设置成了6；</li>
<li>如果不等于5，说明 <code>i</code> 被其他线程改过了（比如现在  <code>i</code> 的值为2），那么我就什么也不做，此次 <code>CAS</code> 失败，<code>i</code> 的值仍然为2。</li>
</ol>
<p>在这个例子中，<code>i</code> 就是 <code>V</code>，5就是 <code>E</code>，6就是 <code>N</code>。</p>
<p>那有没有可能我在判断了 <code>i</code> 为5之后，正准备更新它的新值的时候，被其他线程更改了 <code>i</code> 的值呢？</p>
<p>不会的。因为 <code>CAS</code> 是一种原子操作，它是一直系统原语，是一条 CPU 的原子指令，从 CPU 层面保证它的原子性。</p>
<p><strong>当多个线程同时使用 <code>CAS</code> 操作一个变量时，只有一个会胜出，并更新成功，其余均会失败，但失败的线程并不会被挂起，仅是被告知失败，并且运行再次尝试，当然也允许失败的线程放弃操作。</strong></p>
<h3>10.3 Java 实现 CAS 的原理 —— Unsafe 类</h3>
<p>前面提到，<code>CAS</code> 是一种原子操作。那么 Java 是怎样来使用 <code>CAS</code> 的呢？我们知道，在 Java 中如果一个方法是被 <code>native</code> 修饰的，那么 Java 就不负责具体实现它，而是交给底层的 JVM 使用 <code>c 或者 c++</code> 去实现。</p>
<p>在 Java 中，有一个 <code>Unsafe</code> 类，它里面是一些 <code>native</code> 方法，其中就有今个关于 <code>CAS</code> 的：</p>
<pre><code class="language-java">boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
</code></pre>
<p>当然，它们都是 <code>public native</code> 的。</p>
<p><code>Unsafe</code> 中对 <code>CAS</code> 的实现是 <code>c++</code> 写的，它的具体实现和操作系统、CPU 都有关系。</p>
<p>Linux x86 下只要是通过 <code>cmpxchgl</code> 这个指令在 CPU 级完成 <code>CAS</code> 操作的，但在多事处理器下必须使用 <code>lock</code> 指令加锁来完成。当然不同的操作系统和处理器的实现会有所不同，大家可以自行了解。</p>
<p>当然，<code>Unsafe</code> 类里面还有其他方法用于不同的用途。比如支持线程挂起和恢复的 <code>park</code> 和 <code>unpark</code>，<code>LockSupport</code> 类底层就是调用了这两个方法，还有支持反射操作的 <code>allocateInstance()</code> 方法。</p>
<h3>10.4 原子操作—— AtomicInteger 类源码解析</h3>
<p>上面介绍了 <code>Unsafe</code> 类的几个支持 <code>CAS</code> 的方法，那 Java 具体是如何使用这几个方法来实现原子操作的呢？</p>
<p>JDK 提供了一些用于原子操作的类，在 <code>java.util.concurrent.atomic</code> 包下面，JDK 11 提供了如下17个类：</p>
<p><img src="https://img.dyzmj.top/img/image-20220215142112804.png" alt="image-20220215142112804"></p>
<p>从名字就可以看得出来这些类大概的用途：</p>
<ul>
<li>原子更新基本类型</li>
<li>原子更新数组</li>
<li>原子更新引用</li>
<li>原子更新字段（属性）</li>
</ul>
<p>这里我们以 <code>AtomicInteger</code> 类的 <code>getAndAdd(int delta)</code> 方法为例，来看看 Java 是如何实现原子操作的。</p>
<p>先看看这个方法的源码：</p>
<pre><code class="language-java">public final int getAndAdd(int delta) {
	return U.getAndAddInt(this, VALUE, delta);
}
</code></pre>
<p>这里的 <code>U</code> 其实就是一个 <code>Unsafe</code> 对象：</p>
<pre><code class="language-java">private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
</code></pre>
<p>所以其实 <code>AtomicInteger</code> 类的 <code>getAndAdd(int delta)</code> 方法是调用 <code>Unsafe</code> 类的方法来实现的：</p>
<pre><code class="language-java">@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
</code></pre>
<blockquote>
<p>注：这个方法是在 JDK 1.8 才新增的，在 JDK 1.8 之前，<code>AtomicInteger</code> 源码实现有所不同，是基于 <code>for</code> 死循环的。</p>
</blockquote>
<p>我们来一步步解析这段代码。首先，对象 <code>o</code> 是 <code>this</code>，也就是一个 <code>AtomicInteger</code> 对象。然后 <code>offset</code> 是一个常量 <code>VALUE</code>，这个常量是在 <code>AtomicInteger</code> 类中声明的：</p>
<pre><code class="language-java">private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, &quot;value&quot;);
</code></pre>
<p>同样是调用 <code>Unsafe</code> 类的方法。从方法名字上来看，是得到了一个对象字段偏移量。</p>
<blockquote>
<p>用于获取某个字段相对于 Java 对象的 “起始地址” 的偏移量。</p>
<p>一个 Java 对象可以看成是一段内存，各个字段都得安装一定的顺序放在这段内存里，同时考虑到对齐要求，可能这些字段不是连续放置的。</p>
<p>用这个方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量，因为是相对偏移量，所以它其实跟某个具体对象又没有太大关系，跟 class 的定义和虚拟机的内存模型的实现细节更相关。</p>
</blockquote>
<p>继续看源码，前面我们讲到，<code>CAS</code> 是 “无锁” 的基础，它允许更新失败，所以经常会与 <code>while</code> 循环搭配使用，在失败后不断去重试。</p>
<p>这里声明了一个 <code>v</code>，也就是要返回的值，从 <code>getAndAddInt</code> 来看，它返回的应该是原来的值，而新的值的 <code>V + delta</code>。</p>
<p>这里使用的是 <strong>do-while 循环</strong>，它的目的是 <strong>保证循环体内的语句至少会被执行一遍</strong>。这样才能保证 return 的值 <code>v</code> 是我们期望的值。</p>
<p>循环体的条件是一个 <code>CAS</code> 方法：</p>
<pre><code class="language-java">@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x);

}
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);
</code></pre>
<p>可以看到，最终其实是调用了我们之前说到的 <code>CAS native</code> 方法。那为什么要经过一层 <code>weakCompareAndSetInt</code> 呢？从 JDK 源码上看不出来什么，在 JDK 1.8 及之前的版本，这两个方法是一样的。</p>
<blockquote>
<p>而在 JDK 9 开始，这两个方法上面增加了 <code>@HotSpotIntrinsicCandidate</code> 注解，这个注解允许 HotSpot VM 自己来写汇编或 IR 编译器来实现该方法以提供性能。也就是说虽然外面看到的在 JDK 9 中 <code>weakCompareAndSetObject</code> 和 <code>compareAndSetObject</code> 底层依旧是调用了一样的代码，但是不排除 HotSpot VM 会手动实现 <code>weakCompareAndSetObject</code> 真正含义的功能的可能性。</p>
</blockquote>
<p><code>weakCompareAndSetObject</code> 操作仅保留了 <code>volatile</code> 自身变量的特性，而除去了 <code>happens-before</code> 规则带来的内存语义。也就是说，<code>weakCompareAndSetObject</code> <strong>无法保证处理操作目标的 <code>volatile</code> 变量外的其他变量的执行顺序（编译器和处理器为了优化程序性能而对指令序列进行重排序），同时也无法保证这些变量的可见性。</strong> 这在一定程度上可以提高性能。</p>
<p>再回到循环条件上来，可以看到它是在不断尝试去用 <code>CAS</code> 更新。如果更新失败，就继续重试。那为什么要把获取 “旧值” <code>v</code> 的操作放到循环体内呢？其实这也很好理解，前面我们说了，<code>CAS</code> 如果旧值 <code>V</code> 不等于预期值 <code>E</code>，它就会更新失败。说明旧的值发生了变化，那么我们当然需要返回的是被其他线程改变之后的旧值了，因此放在了 do 循环体内。</p>
<h3>10.5 CAS 实现原子操作的三大问题</h3>
<p>这里介绍一下 <code>CAS</code> 实现原子操作的三大问题及其解决方案。</p>
<h4>10.5.1 ABA 问题</h4>
<p>所谓 ABA 问题，就是一个值原来是 A，变成了 B，然后又变回了 A，这个时候使用 <code>CAS</code> 是检查不出变化的，但实际上却被更新了两次。</p>
<p>ABA 问题的解决思路是在变量前面追加上 <strong>版本号或者时间戳</strong>。从 JDK 1.5 开始，JDK 的 <code>atomic</code> 包里提供了一个 <code>AtomicStampedReference</code> 类来解决 ABA 问题。</p>
<p>这个类的 <code>compareAndSet</code> 方法的作用是首先检查当前引用是否等于预期引用，并且检查当前标志是否等于预期标志，如果二者都相等，才使用 <code>CAS</code> 设置为新的值和标志。</p>
<pre><code class="language-java">public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair&lt;V&gt; current = pair;
    return
        expectedReference == current.reference &amp;&amp;
        expectedStamp == current.stamp &amp;&amp;
        ((newReference == current.reference &amp;&amp;
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
</code></pre>
<h4>10.5.2 循环时间长开销大</h4>
<p><code>CAS</code> 多与自旋结合。如果自旋 <code>CAS</code> 长时间不成功，会占用大量的 CPU 资源。</p>
<p>解决思路是让 JVM 支持处理器提供的 <strong>pause 指令</strong>。</p>
<p><strong>pause 指令</strong>能让自旋失败时 CPU 睡眠一小段时间再继续自旋，从而使得读操作得到频率低很多，为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。</p>
<h4>10.5.3 只能保证一个共享变量的原子操作</h4>
<p>这个问题你可能已经知道怎么解决了，有两种解决方案：</p>
<ol>
<li>使用 JDK 1.5 开始就提供的 <code>AtomicReference</code> 类保证对象之间的原子性，把多个变量放到一个对象里面进行 CAS 操作。</li>
<li>使用锁。锁内的临界区代码可以保证只有当前线程能操作。</li>
</ol>
<h2>十一、 AQS</h2>
<h3>11.1 AQS 简介</h3>
<p><strong>AQS</strong> 是 <code>AbstractQueueSynchronizer</code> 的简称，即 <code>抽象队列同步器</code>，从字面意思理解：</p>
<ul>
<li>抽象：抽象类，只实现一些主要逻辑，有些方法由子类实现；</li>
<li>队列：使用先进先出（FIFO）队列存储数据；</li>
<li>同步：实现了同步功能。</li>
</ul>
<p>那 AQS 有什么用呢？AQS 是一个用来构建锁和同步器的框架，使用 AQS 能简单且高效地构造出应用广泛的同步器，比如我们提到的 <code>ReentrantLock</code>、<code>Semaphore</code>、<code>ReentrantReadWriteLock</code>、<code>SynchronousQueue</code>、<code>FutureTask</code> 等等皆是基于 AQS 的。</p>
<p>当然，我们自己也能利用 AQS 非常轻松容易的构造出符合我们自己需求的同步器，只要子类实现了它的几个 <code>protected</code> 方法就可以了，下文会有详细的介绍。</p>
<h3>11.2 AQS 的数据结构</h3>
<p>AQS 内部使用了一个 <code>volatile</code> 的变量 <code>state</code>来作为资源的标识。同时定义了几个获取和改变 <code>state</code> 的 <code>protected</code> 方法，子类可以覆盖这些方法来实现自己的逻辑：</p>
<pre><code class="language-java">getState();
setStete();
compareAndSetState();
</code></pre>
<p>这三种操作均是原子操作，其中 <code>compareAndSetState</code> 的实现依赖于 <code>Unsafe</code> 的 <code>compareAndSwapInt()</code> 方法。</p>
<p>而 AQS 类本身实现的是一些排队和阻塞的机制，比如具体线程等待队列的维护（如获取资源失败，入队/唤醒出队等）。它内部使用了一个先进先出（FIFO）的双端队列，并使用了两个指针 <code>head</code> 和 <code>tail</code> 用于标识队列的头部和尾部。其数据结构如图：</p>
<p><img src="C:%5CUsers%5CG003247%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220215155511612.png" alt="image-20220215155511612"></p>
<p>但是它并不是直接存储线程，而是存储拥有线程的 Node 节点。</p>
<h3>11.3 资源共享模式</h3>
<p>资源有两种共享模式，或者说两种同步方式：</p>
<ul>
<li>独占模式（Exclusive）：资源是独占的，一次只能一个线程获取。如 <code>ReentrantLock</code>。</li>
<li>共享模式（Share）：同时可以被多个线程获取，具体的资源个数可以通过参数指定。如 <code>Semaphore/CountDownLatch</code>。</li>
</ul>
<p>一般情况下，子类只需要根据需求实现其中一种模式，当然也有同时实现两种模式的同步类，如 <code>ReadWriteLock</code>。</p>
<p>AQS 中关于这两种资源共享模式的定义源码（均在内部类 Node 中）。我们来看看 Node 的结构：</p>
<pre><code class="language-java">static final class Node {
    // 标记一个结点（对应的线程）在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点（对应的线程）在独占模式下等待
    static final Node EXCLUSIVE = null; 

    // waitStatus的值，表示该结点（对应的线程）已被取消
    static final int CANCELLED = 1; 
    // waitStatus的值，表示后继结点（对应的线程）需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的值，表示该结点（对应的线程）在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的值，表示有资源可用，新head结点需要继续唤醒后继结点（共享模式下，多线程并发释放资源，而head唤醒其后继结点后，需要把多出来的资源留给后面的结点；设置新的head结点时，会继续唤醒其后继结点）*/
    static final int PROPAGATE = -3;

    // 等待状态，取值范围，-3，-2，-1，0，1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点


    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 其它方法忽略，可以参考具体的源码
}

// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}
</code></pre>
<blockquote>
<p>注意：通过 Node 我们可以实现两个队列，一是通过 <code>prev</code> 和 <code>next</code> 实现 CLH 队列（线程同步队列，双向队列），二是 <code>nextWaiter</code> 实现 Condition 条件上的等待线程队列（单向队列），这个 Condition 主要用在 <code>ReentrantLock</code> 类。</p>
</blockquote>
<h3>11.4 AQS 的主要方法源码解析</h3>
<p>AQS 的设计是基于 <strong>模板方法模式</strong> 的，它有一些方法必须要子类去实现的，它们主要有：</p>
<ul>
<li><code>isHeldExclusively():</code> 该线程是否正在独占资源。只有用到 condition 才需要去实现它。</li>
<li><code>tryAcquire(int):</code> 独占方式。尝试获取资源，成功则返回 true，失败则返回 false。</li>
<li><code>tryRelease(int):</code> 独占方式。尝试释放资源，成功则返回 true，失败则返回 false。</li>
<li><code>tryAcquireShared(int):</code> 共享方式。尝试获取资源。负数表示失败；0表示成功，但没有剩余可用资源；正数表示成功，且有剩余资源。</li>
<li><code>tryReleaseShared(int):</code> 共享方式。尝试释放资源，如果释放后允许唤醒后续等待节点返回 true，否则返回 false。</li>
</ul>
<p>这些方法虽然都是 <code>protected</code> 方法，但是它们并没有在 AQS 具体实现，而是直接抛出异常（这里不使用抽象方法的目的是：避免强迫子类中把所有的抽象方法都实现一遍，减少无用功，这样子类只需要实现自己关心的抽象方法即可，比如 <code>Semaphore</code> 只需要实现 <code>tryAcquire</code> 方法而不用实现其余不需要用到的模板方法）：</p>
<pre><code class="language-java">protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
</code></pre>
<p>而 AQS 实现了一系列主要的逻辑，下面我们从源码来分析一下获取和释放资源的主要逻辑：</p>
<h4>11.4.1 获取资源</h4>
<p>获取资源的入口是 <code>acquire(int arg)</code> 方法。<code>arg</code> 是要获取的资源的个数，在独占模式下始终为1，我们先来看看这个方法的逻辑：</p>
<pre><code class="language-java">public final void acquire(int arg) {
    if (!tryAcquire(arg) &amp;&amp;
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
</code></pre>
<p>首先调用 <code>tryAcquire(arg)</code> 尝试去获取资源。前面提到了这个方法是在子类具体实现的。</p>
<p>如果获取资源失败，就通过 <code>addWaiter(Node.EXCLUSIVE)</code> 方法把这个线程插入到等待队列中。其中传入的参数代表要插入的 Node 时候独占式的。这个方法的具体实现：</p>
<pre><code class="language-java">// 为当前线程和给定模式创建和排队节点。
private Node addWaiter(Node mode) {
    // 生成当前线程对应的 Node 节点
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            // 使用 CAS 尝试，如果成功就返回
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}
// 在第一次竞争时初始化头部和尾部字段。
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}
</code></pre>
<blockquote>
<p>上面的两个函数比较好理解，就是在队列的尾部插入新的 Node 节点，但是需要注意的是由于 AQS 中会存在多个线程同时争夺资源的情况，因此肯定会出现多个线程同时插入节点的操作，在这里是通过 CAS 自旋的方式保证了操作的线程安全性。</p>
</blockquote>
<p>现在回到最开始的 <code>acquire(int arg)</code> 方法，现在通过 <code>addWaiter</code> 方法，已经把一个 Node 放到等待队列尾部了。而处于等待队列的节点时从头节点一个一个去获取资源的。具体实现我们来看看 <code>acquireQueued</code> 方法：</p>
<pre><code class="language-java">// 以独占不间断模式获取已在队列中的线程。由条件等待方法和获取使用。
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 如果node的前驱节点p是head，表示node是第2个节点，接可以尝试获取资源了
            if (p == head &amp;&amp; tryAcquire(arg)) {
                // 拿到资源后，将head指向该节点
                // 所以head所指的节点，就是当前获取到资源的那个节点或null
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 如果自己可以休息了，就进入waiting状态，知道被unpark()
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}
</code></pre>
<blockquote>
<p>这里 <code>parkAndCheckInterrupt</code> 方法内部使用到了 <code>LockSupport.park(this)</code>，顺便介绍一下 <code>park()</code>。</p>
<p><code>LockSupport</code> 类是 Java 6 引入的一个类，提供了基本的线程同步原语。<code>LockSupport</code> 实际上是调用了 <code>Unsafe</code> 类里的函数，归结到 <code>Unsafe</code> 里，只有两个函数：</p>
<ul>
<li><code>park(boolean isAbsolute, long time):</code> 阻塞当前线程</li>
<li><code>unpark(Object thread):</code> 使给定的线程停止阻塞</li>
</ul>
</blockquote>
<p>所以 <strong>节点进入等待队里后，是调用 <code>park</code> 是它进入阻塞状态的。只有头节点的线程是处于活跃状态的。</strong></p>
<p>当然，获取资源的方法除了 <code>acquire</code> 外，还有以下三个：</p>
<ul>
<li><code>acquireInterruptibly:</code> 申请可中断的资源（独占模式）</li>
<li><code>acquireShared:</code> 申请共享模式的资源</li>
<li><code>acquireSharedInterruptibly:</code> 申请可中断的资源（共享模式）</li>
</ul>
<blockquote>
<p>可中断的意思是，在线程中断时可能抛出 <code>InterruptedException</code></p>
</blockquote>
<p>总结起来的一个流程图：</p>
<p><img src="https://img.dyzmj.top/img/image-20220216101040252.png" alt="image-20220216101040252"></p>
<h4>11.4.2 释放资源</h4>
<p>释放资源相比于获取资源来说，会简单许多。在 AQS 中只有一小段实现。源码如下：</p>
<pre><code class="language-java">// 以独占模式发布。如果tryRelease返回 true，则通过解锁一个或多个线程来实现
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null &amp;&amp; h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    // 如果状态是负数，尝试把它设置为0
    int ws = node.waitStatus;
    if (ws &lt; 0)
        node.compareAndSetWaitStatus(ws, 0);

   // 得到头节点的后继节点 head.next
    Node s = node.next;
    // 如果这个后继节点为空或者状态大于0
    // 通过前面的定义可以知道，大于0只有一种可能，就是这个节点已被取消
    if (s == null || s.waitStatus &gt; 0) {
        s = null;
        // 等待队列中所有可用的几点，都向前移动
        for (Node p = tail; p != node &amp;&amp; p != null; p = p.prev)
            if (p.waitStatus &lt;= 0)
                s = p;
    }
    // 如果后继节点不为空，则此后继节点所在线程停止阻塞
    if (s != null)
        LockSupport.unpark(s.thread);
}
</code></pre>
<h1>第三篇：工具篇</h1>
<h2>十二、线程池原理</h2>
<h3>12.1 为什么要使用线程池</h3>
<p>使用线程池主要有以下三个原因：</p>
<ol>
<li>创建/销毁线程需要消耗系统资源，线程池可以 <strong>复用已创建的线程</strong>。</li>
<li><strong>控制并发数量</strong>。并发数量过多，可能会导致资源消耗过多，从而造成服务器崩溃（主要原因）。</li>
<li><strong>可以对线程做统一管理</strong>。</li>
</ol>
<h3>12.2 线程池的原理</h3>
<p>Java 中的线程池顶层接口是 <code>Executor</code> 接口，<code>ThreadPoolExecutor</code> 是这个接口的实现类。</p>
<p>我们先来看看 <code>ThreadPoolExecutor</code> 类。</p>
<h4>12.2.1 ThreadPoolExecutor 提供的构造方法</h4>
<p>一共四个构造方法：</p>
<pre><code class="language-java">// 5个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&lt;Runnable&gt; workQueue);

// 6个参数的构造方法-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&lt;Runnable&gt; workQueue,
                          ThreadFactory threadFactory);
// 6个参数的构造方法-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&lt;Runnable&gt; workQueue,
                          RejectedExecutionHandler handler);

// 7个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&lt;Runnable&gt; workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);
</code></pre>
<p>涉及到5~7个参数，我们先看看必须的5个参数是什么意思：</p>
<ul>
<li><code>int corePoolSize:</code> 该线程池中<strong>核心线程数最大值</strong></li>
</ul>
<blockquote>
<p>核心线程：线程池中有两类线程，核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中，即使这个核心线程什么都不干（铁饭碗），而非核心线程如果长时间的闲置，就会别销毁（临时工）。</p>
</blockquote>
<ul>
<li><code>int maximumPoolSize:</code> 该线程池中<strong>线程总数最大值</strong></li>
</ul>
<blockquote>
<p>该值等于核心线程数量 + 非核心线程数量</p>
</blockquote>
<ul>
<li><code>long keepAliveTime:</code> <strong>非核心线程闲置超时时长</strong></li>
</ul>
<blockquote>
<p>非核心线程如果处于闲置状态超过该值，就会被销毁。如果设置 <code>allowCoreThreadTimeOut(true)</code>，则也会作用于核心线程。</p>
</blockquote>
<ul>
<li><code>TimeUnit unit:</code> <strong><code>keepAliveTime</code> 的单位</strong></li>
</ul>
<blockquote>
<p><code>TimeUnit</code> 是一个枚举类型，包括一下属性：</p>
<p><code>NANOSECONDS</code>、<code>MICROSECONDS</code>、<code>MILLISECONDS</code>、<code>SECONDS</code>、<code>MINUTES</code>、<code>HOURS</code>、<code>DAYS</code></p>
</blockquote>
<ul>
<li><code>BlockingQueue workQueue:</code> 阻塞队列，维护着<strong>等待执行的 Runnable 任务对象</strong>。</li>
</ul>
<p>常用的几个阻塞队列：</p>
<ol>
<li><code>LinkedBlockingQueue:</code> 链式阻塞队列，底层时间结构是链表，默认大小是 <code>Integer.MAX_VALUE</code>，也可以指定大小。</li>
<li><code>ArrayBlockingQueue:</code> 数组阻塞队列，底层数据结构是数组，需要指定队列的大小。</li>
<li><code>SynchronousQueue:</code> 同步队列，内部容量为0，每个 <code>put</code> 操作必须等待一个 <code>take</code> 操作，反之亦然。</li>
<li><code>DelayQueue:</code> 延迟队列，该队列中的元素只有当其指定的延迟时间到了，才能够从队列中获取到该元素。</li>
</ol>
<blockquote>
<p>我们将在下一章重点介绍各种阻塞队列。</p>
</blockquote>
<p>好了，介绍完5个必须的参数之后，还有两个非必须的参数。</p>
<ul>
<li><code>ThreadFactory threadFactory</code></li>
</ul>
<p>创建线程的工厂，用于批量创建线程，统一在创建线程时设置一些参数，如是否守护线程、线程的优先级等。如果不指定，会新建一个默认的线程工厂。</p>
<pre><code class="language-java">DefaultThreadFactory() {
    SecurityManager s = System.getSecurityManager();
    group = (s != null) ? s.getThreadGroup() :
    						Thread.currentThread().getThreadGroup();
    namePrefix = &quot;pool-&quot; +
        		  poolNumber.getAndIncrement() +
       			  &quot;-thread-&quot;;
}
</code></pre>
<ul>
<li><code>RejectedExecutionHandler handler</code></li>
</ul>
<p><strong>拒绝处理策略</strong>，线程数量大于最大线程数就会采用拒绝处理策略，四种拒绝处理的策略为：</p>
<ol>
<li><code>ThreadPoolExecutor.AbortPolicy:</code> <strong>默认拒绝处理策略</strong>，丢弃任务并抛出 <code>RejectedExecutionException</code> 异常。</li>
<li><code>ThreadPoolExecutor.DiscardPolicy:</code> 丢弃新来的任务，但是不抛出异常。</li>
<li><code>ThreadPoolExecutor.DiscardOldestPolicy:</code> 丢弃队列头部（最旧的）任务，然后重新尝试执行程序（如果再次失败，重复此过程）。</li>
<li><code>ThreadPoolExecutor.CallerRunsPolicy:</code> 由调用线程处理该任务。</li>
</ol>
<h4>12.2.2 ThreadPoolExecutor 的策略</h4>
<p>线程池本身有一个调度线程，这个线程就是用于管理布控整个线程池里的各种任务和事务，例如创建线程、销毁线程、任务队列管理、线程队列管理等等。</p>
<p>故线程池也有自己的状态。<code>ThreadPoolExecutor</code> 类中使用了一些 <code>final int</code> 常量变量来表示线程池的状态，分别为 <code>RUNNING</code>、<code>SHUTDOWN</code>、<code>STOP</code>、<code>TIDYING</code>、<code>TERMINATED</code>。</p>
<pre><code class="language-java">// runState is stored in the high-order bits
private static final int RUNNING    = -1 &lt;&lt; COUNT_BITS;
private static final int SHUTDOWN   =  0 &lt;&lt; COUNT_BITS;
private static final int STOP       =  1 &lt;&lt; COUNT_BITS;
private static final int TIDYING    =  2 &lt;&lt; COUNT_BITS;
private static final int TERMINATED =  3 &lt;&lt; COUNT_BITS;
</code></pre>
<ul>
<li>
<p>线程池创建后处于 <code>RUNNING</code> 状态。</p>
</li>
<li>
<p>调用 <code>shutdown()</code> 方法后处于 <code>SHUTDOWN</code> 状态，线程池不能接受新的任务，清除一些空闲 <code>worker</code>，不会等待阻塞队列的任务完成。</p>
</li>
<li>
<p>调用 <code>shutdownNow()</code> 方法后处于 <code>STOP</code> 状态，线程池不能接受新的任务，中断所有线程，阻塞队列中没有被执行的任务全部丢弃。此时 <code>poolSize=0</code>，阻塞队列的 <code>size</code> 也为0。</p>
</li>
<li>
<p>当所有的任务已终止， ctl 记录的 “任务数量” 为0，线程池会变为 <code>TIDYING</code> 状态。接着会执行 <code>terminated()</code> 函数。</p>
<blockquote>
<p><code>ThreadPoolExecutor</code> 中有一个控制状态的属性叫 <code>ctl</code>，它是一个 <code>AtomicInteger</code> 类型的变量。线程池状态就是通过 <code>AtomicInteger</code> 类型的成员变量 <code>ctl</code> 来获取的。</p>
<p>获取的 <code>ctl</code> 值传入 <code>runStateOf</code> 方法，与 <code>~CAPACITY</code> 位于运算（<code>CAPACITY</code> 是低 29 位全 1 的 int 变量）。</p>
<p><code>~CAPACITY</code> 在这里相当于掩码，用来获取 <code>ctl</code> 的高3位，表示线程状态；而另外的低29位用于表示工作线程数</p>
</blockquote>
</li>
<li>
<p>线程池处在 <code>TIDYING</code> 状态时，<strong>执行完 <code>terminated()</code> 方法之后</strong>，就会由 <code>TIDYING -&gt; TERMINATED</code>，线程池被设置为 <code>TERMINATED</code> 状态。</p>
</li>
</ul>
<h4>12.2.3 线程池主要的任务处理流程</h4>
<p>处理任务的核心方法是 <code>execute</code>，我们来看看 <code>ThreadPoolExecutor</code> 中是如何处理线程任务的：</p>
<pre><code class="language-java">// 在未来的某个时间执行给定的任务。该任务可以在新线程或现有池线程中执行。如果任务无法提交执行，要么是因为这个执行器已经关闭，要么是因为它的容量已经达到，任务由当前的RejectedExecutionHandler处理。
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 1、当前线程数小于corePoolSize，则调用addWorker创建核心线程执行任务
    if (workerCountOf(c) &lt; corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2、如果不小于corePoolSize，则将任务添加到workQueue中
    if (isRunning(c) &amp;&amp; workQueue.offer(command)) {
        int recheck = ctl.get();
        // 2.1 如果isRunning 返回false(状态检查)，则remove这个任务，然后执行拒绝策略
        if (! isRunning(recheck) &amp;&amp; remove(command))
            reject(command);
        // 2.2 线程池处于running状态，但是没有线程，则创建线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3、如果放入workQueue失败，则创建非核心线程执行任务
    // 如果这是创建非核心线程失败（当前线程总数不小于 maximumPoolSize时），就会执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
</code></pre>
<p><code>ctl.get()</code> 是获取线程池状态，用 <code>int</code> 类型表示。第二步中，入队前进行一次 <code>isRunning</code> 判断，入队之后，又进行一次 <code>isRunning</code> 判断。</p>
<p><strong>为什么要二次检查线程池的状态？</strong></p>
<p>在多线程的环境下，线程池的状态是时刻发生变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将 <code>command</code> 加入 <code>workQueue</code> 是线程池之前的状态。倘若没有二次检查，万一线程池处于非 <code>RUNNING</code> 状态（在多线程环境下很有可能发生），那么 <code>command</code> 永远不会执行。</p>
<p><strong>总结一下处理流程</strong></p>
<ol>
<li>线程总数量 &lt; <code>corePoolSize</code>，无论线程是否空闲，都会新建一个核心线程执行任务（让核心线程数量快速达到 <code>corePoolSize</code>，在核心线程数据 &lt; <code>corePoolSize</code> 时）。<strong>注意，这一步需要获得全局锁</strong>。</li>
<li>线程总数量 &gt;= <code>corePoolSize</code> 时，新来的线程任务会进入任务队列中等待，然后空闲的核心线程会依次去缓存队列中取任务来执行（体现了<strong>线程复用</strong>）。</li>
<li>当缓存队列满了，说明这个时候任务已经多到爆棚，需要一些 “临时工” 来执行这些任务了，于是会创建非核心线程去执行这个任务。<strong>注意，这一步需要获得全局锁</strong>。</li>
<li>缓存队列满了，且总线程数达到了 <code>maximumPoolSize</code>，则会采取上面提到拒绝策略进行处理。</li>
</ol>
<p>整个过程如图所示：</p>
<p><img src="https://img.dyzmj.top/img/image-20220216132523165.png" alt="image-20220216132523165"></p>
<h4>12.2.4 ThreadPoolExecutor 如何做到线程复用的？</h4>
<p>我们知道，一个线程在创建的时候会指定一个线程任务，当执行完这个线程任务之后，线程自动销毁。但是线程池却可以复用线程，即一个线程执行完线程任务后不销毁，继续执行另外的线程任务。<strong>那么，线程池如何做到线程复用呢？</strong></p>
<p>原来，<code>ThreadPoolExecutor</code> 创建线程时，会将线程封装成 <code>工作线程 worker</code>，并放入 <strong>工作线程组</strong> 中，然后这个 <code>worker</code> 反复从阻塞队列中拿任务去执行。话不多说，我们继续看看源码。</p>
<p>这里的 <code>addWorker</code> 方法是在上面提到的 <code>execute</code> 方法里面调用的，先看看上半部分：</p>
<pre><code class="language-java">private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                &amp;&amp; (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                if (workerCountOf(c)
                    &gt;= ((core ? corePoolSize : maximumPoolSize) &amp; COUNT_MASK))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
</code></pre>
<p>上半部分主要是判断线程数量是否超出阈值，超过了就返回 <code>false</code>。我们继续看下半部分：</p>
<pre><code class="language-java">boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
    // 1.创建一个worker对象
    w = new Worker(firstTask);
    // 2.实例化一个Thread对象
    final Thread t = w.thread;
    if (t != null) {
        // 3.线程池全局锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // Recheck while holding lock.
            // Back out on ThreadFactory failure or if
            // shut down before lock acquired.
            int c = ctl.get();

            if (isRunning(c) ||
                (runStateLessThan(c, STOP) &amp;&amp; firstTask == null)) {
                if (t.getState() != Thread.State.NEW)
                    throw new IllegalThreadStateException();
                workers.add(w);
                workerAdded = true;
                int s = workers.size();
                if (s &gt; largestPoolSize)
                    largestPoolSize = s;
            }
        } finally {
            mainLock.unlock();
        }
        if (workerAdded) {
            // 4.启动这个线程
            t.start();
            workerStarted = true;
        }
    }
} finally {
    if (! workerStarted)
        addWorkerFailed(w);
}
return workerStarted;
}
</code></pre>
<p>创建 <code>worker</code> 对象，并初始化一个 <code>Thread</code> 对象，然后启动这个线程对象。</p>
<p>我们接着看看 <code>Worker</code> 类，仅展示部分源码：</p>
<pre><code class="language-java">// Worker类部分源码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;
    Runnable firstTask;

    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
            runWorker(this);
    }
    //其余代码略...
}
</code></pre>
<p><code>Worker</code> 类实现了 <code>Runnable</code> 接口，所以 <code>Worker</code> 也是一个线程任务。在构造方法中，创建了一个线程，线程的任务就是自己。故 <code>addWorker</code> 方法源码下半部分中的第4步 <code>t.start()</code>，会触发 <code>Worker</code> 类的 <code>run</code> 方法被 JVM 调用。</p>
<p>我们再看看 <code>runWorker</code> 的逻辑：</p>
<pre><code class="language-java">final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 1.线程启动之后，通过unlock方法释放锁
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
      // 2.Worker执行firstTask或从workQueue中获取任务，如果getTask()方法不返回null，循环不退出
        while (task != null || (task = getTask()) != null) {
            // 2.1进行加锁操作，保证thread不被其他线程中断（除非线程池被中断）
            w.lock();
            // 2.2检查线程池状态，倘若线程池处于中断状态，当前线程将中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &amp;&amp;
                  runStateAtLeast(ctl.get(), STOP))) &amp;&amp;
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 2.3执行beforeExecute
                beforeExecute(wt, task);
                try {
                    // 2.4执行任务
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    // 2.5执行afterExecute
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                // 2.6解锁操作
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
</code></pre>
<p>首先去执行创建这个 <code>worker</code> 时就有的任务，当执行完这个任务后，<code>worker</code> 的声明周期并没有结束，在 <code>while</code> 循环中，<code>worker</code> 会不断地调用 <code>getTask()</code> 方法从 <strong>阻塞队列</strong> 中获取任务然后调用 <code>task.run()</code> 执行任务，从而达到 <strong>复用线程</strong> 的目的。只要 <code>getTask()</code> 方法不返回 <code>null</code>，此线程就不会退出。</p>
<p>当然，核心线程池中创建的线程想要拿到阻塞队列中的任务，先要判断线程池的状态，如果 <strong>STOP</strong> 或者 <strong>TERMINATED</strong>，返回 <code>null</code>。</p>
<p>最后看看 <code>getTask()</code> 方法的实现：</p>
<pre><code class="language-java">private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();

        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            &amp;&amp; (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 1.allowCoreThreadTimeOut变量默认是false，核心线程即使空闲也不会被销毁
        // 如果为true，核心线程在keepAliveTime内仍空闲则不会被销毁
        boolean timed = allowCoreThreadTimeOut || wc &gt; corePoolSize;
		// 2.如果运行线程数超过了最大线程数，但是缓存队列已经空了，这时递减worker数量
        // 如果有设置允许线程超时或者线程数量超过了核心线程数量，
        // 并且线程在规定时间内均未poll到任务切队列为空则递减worker数量
        if ((wc &gt; maximumPoolSize || (timed &amp;&amp; timedOut))
            &amp;&amp; (wc &gt; 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 3.如果timed为true，则会调用workQueue的poll方法获取任务.
            // 超时时间是keepAliveTime,如果超过keepAliveTIme时长
            // poll返回了null，上边提到的while循环就会退出，线程也就执行完了
            // 如果timed为false（allowCoreThreadTimeOut为false，
            // 且wc&gt; corePoolSize为false），则会调用workQueue的take方法阻塞在当前。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
</code></pre>
<p>核心线程会一直卡在 <code>workQueue.take</code> 方法，被阻塞并挂起，不会占用 CPU 资源，直到拿到 <code>Runnable</code> 然后返回（当然如果 <code>allowCoreThreadTimeOut</code> 设置为 <code>true</code>，那么核心线程就会去调用 <code>poll</code> 方法，因为 <code>poll</code> 可能会返回 <code>null</code>，所以这个时候核心线程满足超时条件也会被销毁）。</p>
<p>非核心线程会 <code>workQueue.poll(keepAliveTime, TimeUnit,NANOSECONDS)</code>，如果超时还没有拿到，下一次循环判断 <code>compareAndDecrementWorkerCount</code> 就会返回 <code>null</code>，<code>Worker</code> 对象的 <code>run()</code> 方法循环体的判断为 <code>null</code>，任务结束，然后线程被系统回收。</p>
<h3>12.3 四种常见的线程池</h3>
<p><code>Executors</code> 类中提供的几个静态方法来创建线程池。到了这一步，如果看懂了前面讲的 <code>ThreadPoolExecutor</code> 构造方法中各种参数的意义，那么一看到 <code>Executors</code> 类中提供的线程池的源码就应该知道这个线程池是干嘛的了。</p>
<h4>12.3.1 newCachedThreadPool</h4>
<pre><code class="language-java">public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue&lt;Runnable&gt;());
}
</code></pre>
<p><code>CacheThreadPool</code> 的运行流程如下：</p>
<ol>
<li>提交任务进线程池；</li>
<li>因为 <code>corePoolSize</code> 为0的关系，不创建核心线程，线程池最大为 <code>Integer.MAX_VALUE</code>。</li>
<li>尝试将任务添加到 <code>SynchornousQueue</code> 队列。</li>
<li>如果 <code>SynchornousQueue</code> 入列成功，等待被当前运行的线程空闲后拉去执行。如果当前没有空闲线程，那么就创建一个非核心线程，然后从 <code>SynchornousQueue</code> 拉去任务并在当前线程执行。</li>
<li>如果 <code>SynchornousQueue</code> 已有任务在等待，入列操作将会阻塞。</li>
</ol>
<p>当需要执行很多短时间的任务时，<code>CacheThreadPool</code> 的线程复用率比较高，会显著的提高性能，而且线程60s 后会回收，意味着即使没有任务进来，<code>CacheThreadPool</code> 并不会占用很多资源。</p>
<h4>12.3.2 newFixedThreadPool</h4>
<pre><code class="language-java">public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue&lt;Runnable&gt;());
}
</code></pre>
<p>核心线程数量和总线程数量相等，都是传入的参数 <code>nThreads</code>，所以只能创建核心线程，不能创建非核心线程。因为 <code>LinkedBlockingQueue</code> 的默认大小是 <code>Integer.MAX_VALUE</code>，故如果核心线程空闲，则交给核心线程处理；如果核心线程不空闲，则入列等待，直到核心线程空闲。</p>
<p><strong>与 <code>CachedThreadPool</code> 的区别：</strong></p>
<ul>
<li>因为 <code>corePoolSize == maximumPoolSize</code>，所以 <code>FixedThreadPool</code> 只会创建核心线程。而 <code>CachedThreadPool</code> 因为 <code>corePoolSize=0</code>，所以只会创建非核心线程。</li>
<li>在 <code>getTask()</code> 方法，如果队列里没有任务可取，线程会一直阻塞在 <code>LinkedBlockingQueue.take()</code>，线程不会被回收，<code>CachedThreadPool</code> 会在 60s 后收回。</li>
<li>由于线程不会被回收，会一直卡在阻塞，所以 <strong>没有任务的情况下，<code>FixedThreadPool</code> 占用资源更多</strong>。</li>
<li>都几乎不会触发拒绝策略，但是原理不同。<code>FixedThreadPool</code> 是因为阻塞队列可以很大（最大为 <code>Integer</code> 最大值），故几乎不会触发拒绝策略；<code>CachedThreadPool</code> 是因为线程池很大（最大为 <code>Integer</code> 最大值），几乎不会导致线程数量大于最大线程数，故几乎不会触发拒绝策略。</li>
</ul>
<h4>12.3.3 newSingleThreadExecutor</h4>
<pre><code class="language-java">public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue&lt;Runnable&gt;()));
}
</code></pre>
<p>有且仅有一个核心线程（<code>corePoolSize == maximumPoolSize = 1</code>），使用了 <code>LinkedBlockingQueue</code> （容量很大），所以，<strong>不会创建非核心线程</strong>。所有任务按照 <strong>先来先执行</strong> 的顺序执行。如果这个唯一的线程不空闲，那么新来的任务会存储在任务队列里等待执行。</p>
<h4>12.3.4 newScheduledThreadPool</h4>
<p>创建一个定长线程池，支持定时及周期性任务执行。</p>
<pre><code class="language-java">public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);

}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
</code></pre>
<p>这四种常见的线程池基本够我们使用了，但是《阿里巴巴开发手册》不建议我们直接使用 <code>Executors</code> 类中的线程池，而是通过 <code>ThreadPoolExecutor</code> 的方式，这样的处理方式让写的同学需要更加明确线程池的运行规则，避免资源耗尽的风险。</p>
<p>但如果你及团队本身对线程池非常熟悉，又确定业务规模不会达到资源耗尽的程度（比如线程数量或任务队列长度可能达到 <code>Integer.MAX_VALUE</code>）时，其实是可以使用 JDK 提供的这几个接口的，它能让我们的代码具有更强的可读性。</p>
<h2>十三、阻塞队列</h2>
<h3>13.1 阻塞队列的由来</h3>
<p>我们假设一种场景，生产者一直生成资源，消费者一直消费资源，资源存储在一个缓冲池中，生产者将生产的资源存进缓存池中，消费者从缓冲池中拿到资源进行消费，这就是大名鼎鼎的 <strong>生产者——消费者模式</strong>。</p>
<p>该模式能够简化开发过程，一方面消除了生产者类与消费者类之间的代码依赖性，另一方面将生产数据的过程与使用数据的过程解耦简化负载。</p>
<p>我们自己 <code>coding</code> 实现这个模式的时候，因为需要让 <strong>多个线程操作共享变量（即资源）</strong>，所以很容易引发 <strong>线程安全问题</strong>，造成 <strong>重复消费和死锁</strong>，尤其是生产者和消费者存在多个的情况。另外，当缓冲池空了，我们需要阻塞消费者，唤醒生产者；当缓冲池满了，我们需要阻塞生产者，唤醒消费者。这些个 <strong>等待—唤醒</strong> 逻辑都需要自己实现。</p>
<p>这么容易出错的事情，JDK 当然帮我们做啦，这就是阻塞队列（<code>BlockingQueue</code>），<strong>你只管往里面存、取就行，而不用担心多线程环境下存、取共享变量的线程安全问题。</strong></p>
<blockquote>
<p><code>BlockingQueue</code> 是 <code>Java.util.concurrent</code> 包下重要的数据结构，区别与普通的队列，<code>BlockingQueue</code> 提供了 <strong>线程安全得到队列访问方式</strong>，并发包下很多高级同步类的实现都是基于 <code>BlockingQueue</code> 的。</p>
</blockquote>
<p><code>BlockingQueue</code> 一般用于生产者——消费者模式，生产者是往队列里添加元素的线程，消费者是从队列里拿元素的线程。<strong><code>BlockingQueue</code> 就是存放元素的容器</strong>。</p>
<h3>13.2 BlockingQueue 的操作方法</h3>
<p>阻塞队列提供了四组不同的方法用于插入、移除、检查操作：</p>
<table>
<thead>
<tr>
<th>方法\处理方式</th>
<th>抛出异常</th>
<th>返回特殊值</th>
<th>一直阻塞</th>
<th>超时退出</th>
</tr>
</thead>
<tbody>
<tr>
<td>插入方法</td>
<td>add(e)</td>
<td>offer(e)</td>
<td>put(e)</td>
<td>offer(e,time,unit)</td>
</tr>
<tr>
<td>移除方法</td>
<td>remove()</td>
<td>poll()</td>
<td>take()</td>
<td>poll(time,unit)</td>
</tr>
<tr>
<td>检查方法</td>
<td>element()</td>
<td>peek()</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
<ul>
<li>抛出异常：如果试图的操作我发立即执行，抛出异常。当阻塞队列满的时候，再往队列里插入元素，会抛出 <code>IllegalStateException(&quot;Queue full&quot;)</code> 异常。当队列为空时，从队列里获取元素时会抛出 <code>NoSuchElementException</code> 异常。</li>
<li>返回特殊值：如果试图的操作无法立即执行，返回一个特殊值，通常是 <code>true/false</code>。</li>
<li>一直阻塞：如果试图的操作无法立即执行，则一直阻塞或者响应中断。</li>
<li>超时退出：如果试图的操作无法立即执行，该方法调用会发生阻塞，直到能够执行，但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功，通常是 <code>true/false</code>。</li>
</ul>
<p><strong>注意之处：</strong></p>
<ul>
<li>不能往阻塞队列中插入 <code>null</code>，会抛出空指针异常。</li>
<li>可以访问阻塞队列中的任意元素，调用 <code>remove(o)</code> 可以将队列之中的特定对象移除，但并不高效，尽量避免使用。</li>
</ul>
<h3>13.3 BlockingQueue 的实现类</h3>
<h4>13.3.1 ArrayBlockingQueue</h4>
<p>由<strong>数组</strong>结构组成的有界阻塞队列。内部结构是数组，故具有数组的特性。</p>
<pre><code class="language-java">public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity &lt;= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
</code></pre>
<p>可以初始化队列大小，且一旦初始化不能改变。构造方法中的 <code>fair</code> 表示控制对象的内部锁是否采用公平锁，默认是 <strong>非公平锁</strong>。</p>
<h4>13.3.2 LinkedBlockingQueue</h4>
<p>由<strong>链表</strong>结构组成的<strong>有界</strong>阻塞队列。内部结构是链表，具有链表的特性。默认队列的大小是 <code>Integer.MAX_VALUE</code>，也可以是指定大小。此队列按照<strong>先进先出</strong>的原则对元素进行排序。</p>
<h4>13.3.3 DelayQueue</h4>
<p>该队列中的元素只有当其指定的延迟时间到了，才能从队列中获取该元素。注入其中的元素必须实现 <code>java.util.concurrent.Delayed</code> 接口。</p>
<p><code>DelayQueue</code> 是一个没有大小限制的队列，因此往队列中插入数据的操作（生产者）永远不会被阻塞，而只有获取数据的操作（消费者）才会被阻塞。</p>
<h4>13.3.4 PriorityBlockingQueue</h4>
<p>基于优先级的无界阻塞队列（优先级的判断通过构造函数传入的 <code>compator</code> 对象来决定），内部控制线程同步的锁采用的是非公平锁。</p>
<pre><code class="language-java">public PriorityBlockingQueue(int initialCapacity,
                             Comparator&lt;? super E&gt; comparator) {
    if (initialCapacity &lt; 1)
        throw new IllegalArgumentException();
    this.comparator = comparator;
    this.queue = new Object[Math.max(1, initialCapacity)];
}
</code></pre>
<h4>13.3.5 SynchronousQueue</h4>
<p>这个队列比较特殊，没有任何内部容量，甚至连一个队列的容量都没有。并且每个 put 必须等待一个 take，反之亦然。</p>
<p>需要区别容量为1的 <code>ArrayBlockingQueue</code>、<code>LinkedBlockingQueue</code>。</p>
<p>以下方法的返回值，可以帮助理解这个队列：</p>
<ul>
<li><code>iterator()</code> 永远返回空，因为里面没有东西。</li>
<li><code>peek()</code> 永远返回 null</li>
<li><code>put()</code> 往 queue 放进去一个 element 以后就一直 wait，直到有其他线程进来把这个 element 取走</li>
<li><code>offer()</code> 往 queue 里放一个 element 后立即返回，如果碰巧这个 element 被另一个线程取走了，offer 方法返回 true，认为 offer 成功，否则返回 false</li>
<li><code>take()</code> 取出并且 remove 掉 queue 里的 element，取不到东西它会一直等</li>
<li><code>poll()</code> 取出并且 remove 掉queue 里的 element，只有碰巧另外一个线程正在往 queue 里 offer 数据或者 put 数据的时候，该方法才会取到东西，否则立即返回 null</li>
<li><code>isEmpty()</code> 永远返回 true</li>
<li><code>remove() &amp; removeAll()</code> 永远返回false</li>
</ul>
<p><strong>注意</strong></p>
<p><code>PriorityBlockingQueue</code> 不会阻塞数据生产者（因为队列是无界的），而只会在没有可消费的数据时，阻塞塑胶的消费者。因此使用的时候要特别注意，<strong>生产者生产数据的速度绝对不能快于消费者消费数据的速度，否则时间一长，会最终耗尽所有的可用堆内存空间。</strong> 对于使用默认大小的 <code>LinkedBlockingQueue</code> 也是一样的。</p>
<h3>13.5 阻塞队列的原理</h3>
<p>阻塞队列的原理很简单，利用了 <code>Lock</code> 锁的多条件（Condition）阻塞控制。接下来我们分析下 <code>ArrayBlockingQueue</code> 的源码：</p>
<p>首先是构造器，处理初始化队列大小和是否公平锁之外，还对同一个锁（lock）初始化了两个监视器，分别是 <code>notEmpty</code> 和 <code>notFull</code>。这两个监视器的所用目前可以简单理解为标记分组，当该线程是 <code>pull</code> 操作时，给它减伤监视器 <code>notFull</code>，标记这个线程是一个生产者；当线程操作时 <code>take</code> 时，给它加上监视器 <code>notEmpty</code>，标记这个线程是消费者。</p>
<pre><code class="language-java">//数据元素数组
final Object[] items;
//下一个待取出元素索引
int takeIndex;
//下一个待添加元素索引
int putIndex;
//元素个数
int count;
//内部锁
final ReentrantLock lock;
//消费者监视器
private final Condition notEmpty;
//生产者监视器
private final Condition notFull;  

public ArrayBlockingQueue(int capacity, boolean fair) {
    //..省略其他代码
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
</code></pre>
<p><strong>put 操作的源码</strong></p>
<pre><code class="language-java">public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    // 1.自旋拿锁
    lock.lockInterruptibly();
    try {
        // 2.判断队列是否满了
        while (count == items.length)
            // 2.1如果满了，阻塞该线程，并标记为notFull线程，
            // 等待notFull的唤醒，唤醒之后继续执行while循环。
            notFull.await();
        // 3.如果没有满，则进入队列
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    // 4 唤醒一个等待的线程
    notEmpty.signal();
}
</code></pre>
<p>总结 <code>put</code> 的流程：</p>
<ol>
<li>所有执行 <code>put</code> 操作的线程竞争 lock 锁，拿到 lock 锁的线程进入下一步，没有拿到锁的线程自旋竞争锁。</li>
<li>判断阻塞队列是否满了，如果满了，则调用 <code>await</code> 方法阻塞这个线程，并标记为 <code>notFull</code> （生产者）线程，同时释放 lock 锁，等待被消费者线程唤醒。</li>
<li>如果没有满，则调用 <code>enqueue</code> 方法将元素 <code>put</code> 进阻塞队列。注意这一步的线程还有一种情况是第二步中阻塞的线程被唤醒且又拿到了 lock 锁的线程。</li>
<li>唤醒一个标记为 <code>notEmpty</code>（消费者）的线程。</li>
</ol>
<p><strong>take 操作的源码：</strong></p>
<pre><code class="language-java">public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings(&quot;unchecked&quot;)
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}
</code></pre>
<p><code>take</code> 操作和 <code>put</code> 操作的流程是类似的，总结下 <code>take</code> 操作的流程：</p>
<ol>
<li>所有执行 <code>take</code> 操作的线程竞争 lock 锁，拿到了 lock 锁的线程进入下一步，没有拿到 lock 锁的线程自旋竞争锁。</li>
<li>判断阻塞队列是否为空，如果是空，则调用 <code>await</code> 方法阻塞这个线程，并标记为 <code>notEmpty</code>（消费者）线程，同时释放 lock 锁，等待被生产者线程唤醒。</li>
<li>如果没有空，则调用 <code>dequeue</code> 方法，注意这一步的线程还有一种情况是第二步中阻塞的线程被唤醒且又拿到了 lock 锁的线程。</li>
<li>唤醒一个标记为 <code>notFull</code>（生产者）的线程。</li>
</ol>
<p><strong>注意</strong></p>
<ol>
<li><code>put</code> 和 <code>take</code> 操作都需要 <strong>先获取锁</strong>，没有获取到锁的线程会被挡在第一道大门之外自旋拿锁，直到获取到锁。</li>
<li>就算拿到锁了之后，也不一定会顺利进行 <code>put/get</code> 操作，需要判断 <strong>队列是否可用</strong>(是否满/空)，如果不可用，则会被阻塞，并释放锁。</li>
<li>在第2点被阻塞的线程会被唤醒，但是在唤醒之后，<strong>依然需要拿到锁</strong> 才能继续往下执行，否则，自旋拿到锁，拿到锁了再 while 判断队列是否可用（这也就是为什么不用 if 判断，而使用 while 判断的原因）。</li>
</ol>
<h3>13.6 示例和使用场景</h3>
<h4>13.6.1 生产者-消费者模型</h4>
<pre><code class="language-java">public class ProductDemo {

    private final int queueSize = 10;

    private final ArrayBlockingQueue&lt;Integer&gt; queue = new ArrayBlockingQueue&lt;&gt;(queueSize);

    public static void main(String[] args) {
        ProductDemo test = new ProductDemo();

        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();

        producer.start();
        consumer.start();
    }

    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                try {
                    queue.take();
                    System.out.println(&quot;从队列中取走一个元素，队列剩余 &quot; + queue.size() + &quot; 个元素&quot;);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                try {
                    queue.put(1);
                    System.out.println(&quot;向队列中插入一个元素，队列剩余空间： &quot; + queue.size());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
</code></pre>
<p>下面是这个例子的输出片段：</p>
<p><img src="https://img.dyzmj.top/img/image-20220308101807185.png" alt="image-20220308101807185"></p>
<p>注意，这个例子中的输出结果看起来可能有问题，比如有几行在插入一个元素之后，队列的剩余空间不变。这是由于 <code>System.out.println语句没有锁</code>。考虑到这样的情况：线程 1 在执行完 <code>put/take</code> 操作后立即失去 CPU 时间片，然后切换到线程 2 执行 <code>put/take</code> 操作，执行完毕后回到线程 1 的 <code>System.out.println</code> 语句输出，发现这个时候阻塞队列的 <code>size</code> 已经被线程 2 改变了，所以这个时候输出的 <code>size</code> 并不是当时线程 1 执行完 <code>put/take</code> 操作之后阻塞队列的 <code>size</code>，但可以确保的是 <code>size</code> 不会超过 10 个。实际上使用阻塞队列是没有问题的。</p>
<h4>13.6.2 线程池中使用阻塞队列</h4>
<pre><code class="language-java"> public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue&lt;Runnable&gt; workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}
</code></pre>
<p>Java 中的线程池就是使用阻塞队列实现的，我们在了解了阻塞队列之后，无论是使用 <code>Executors</code> 类中已经提供的线程池，还是自己通过 <code>ThreadPoolExecutor</code> 实现线程池，都会更加得心应手，想要了解线程池的同学，可以看第十二章：线程池原理。</p>
<blockquote>
<p>注：上面提到的生产者-消费者模式，大家可以参考生产者-消费者模型，可以更好的理解阻塞队列。</p>
</blockquote>
<h2>十四、锁接口和类</h2>
<p>前面我们介绍了 Java 原生的锁——基于对象的锁，它一般是配合 <code>synchronized</code> 关键字来使用的。实际上，Java 在 <code>java.util.concurrent.locks</code> 包下，还为我们提供了几个关于锁的类和接口，它们有更强大的功能或更高的性能。</p>
<h3>14.1 synchronized 的不足之处</h3>
<p>我们先来看看 <code>synchronized</code> 有什么不足之处：</p>
<ul>
<li>如果临界区是只读操作，其实可以多线程一起执行，但使用 <code>synchronized</code> 的话，<strong>同一时间只能有一个线程执行</strong>。</li>
<li><code>synchronized</code> 无法知道线程有没有成功获取到锁。</li>
<li>使用 <code>synchronized</code> 如果临界区因为 IO 或者 sleep 方法等原因阻塞了，而当前线程又没有释放锁，就会导致 <strong>所有线程等待</strong>。</li>
</ul>
<p>而这些都是 locks 包下的锁可以解决的。</p>
<h3>14.2 锁的几种分类</h3>
<p>锁可以根据以下几种方式来进行分类，下面我们逐一介绍。</p>
<h4>14.2.1 可重入锁和非可重入锁</h4>
<p>所谓重入锁，顾名思义就是支持重新进入的锁，也就是说这个锁支持一个 <strong>线程对资源重复加锁</strong>。</p>
<p><code>synchronized</code> 关键字就是使用的重入锁。比如说，你在一个 <code>synchronized</code> 实例方法里面调用另一个本实例的 <code>synchronized</code> 实例方法，它可以重新进入这个锁，不会出现任何异常。</p>
<p>如果我们自己在继承 AQS 实现同步器的时候，没有考虑到占有锁的线程再次获取锁的场景，可能就会导致线程阻塞，那这个就是一个 “非可重入锁”。</p>
<p><code>ReentrantLock</code> 的中文意思就是可重入锁，也是本文后续要介绍的重点类。</p>
<h4>14.2.2 公平锁与非公平锁</h4>
<p>这里的 “公平”，其实通俗意义来说就是 “先来后到”，也就是 FIFO。如果对一个锁来说，先对锁获取获取请求的线程一定先会被满足，后对锁获取请求的线程后被满足，那这个锁就是公平的。反之，那就是不公平的。</p>
<p>一般情况下，<strong>非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿（有一些线程长时间得不到锁）的情况。</strong> 所以要根据实际的需求来选择非公平锁和公平锁。</p>
<p><code>ReentrantLock</code> 支持非公平锁和公平锁。</p>
<h4>14.2.3 读写锁和排它锁</h4>
<p>我们前面讲到的 <code>synchronized</code> 用的锁和 <code>ReentrantLook</code>，其实都是排它锁，也就是说，这些锁在同一时刻只允许一个线程进行访问。</p>
<p>而读写锁可以在同一时刻允许多个读线程访问。Java 提供了 <code>ReentrantReadWriteLock</code> 类作为读写锁的默认实现，内部维护了两个锁：一个读锁，一个写锁。通过分离读锁和写锁，使得在 “读多写少” 的环境下，大大地提高了性能。</p>
<blockquote>
<p>注意：即使用读写锁，在写线程访问时，所有的读线程和其它写写线程均被阻塞。</p>
</blockquote>
<p><strong>可见，只是 <code>synchronized</code> 是远远不能满足多样化的业务对锁的要求的。</strong> 接下来我们介绍一下 JDK 中有关锁的一些接口和类。</p>
<h3>14.3 JDK 中有关锁的一些接口和类</h3>
<p>众所周知，JDK 中关于并发的类大多都在 <code>java.util.concurrent</code>（以下简称 <strong>juc</strong>）包下，而 <code>juc.locks</code> 包看名字就知道，是提供了一些并发锁的工具类。前面我们介绍的 AQS（<code>AbstractQueuedSynchronizer</code>）就是在这个包下。下面分别介绍一下这个包下的类和接口以及它们之间的关系。</p>
<h4>14.3.1 抽象类 AQS/AQLS/AOS</h4>
<p>这三个抽象类有一定的关系，所以这里放到一起讲。</p>
<p>首先我们看 <strong>AQS</strong> （<code>AbstractQueuedSynchronizer</code>），之前专门有章节介绍这个类，它是在 JDK 1.5 发布的，提供了一个 “队列同步器” 的基本功能实现。而 AQS 里面的 “资源” 是用一个 <code>int</code> 类型的数据来表示的，有时候我们的业务需求的数量超出了 <code>int</code> 的范围，所以在 JDK 1.6 中，多了一个 <strong>AQLS</strong>（<code>AbstractQueuedLongSynchronizer</code>），它的代码跟 AQS 几乎一样，只是把资源的类型变成了 <code>long</code> 类型。</p>
<p>AQS 和 AQLS 都继承了一个类叫 <strong>AOS</strong>（<code>AbstractOwnableSynchronizer</code>），这个类也是在 JDK 1.6 中出现的。这个类只有几行简答的代码。从源码类上的注释可以知道，它是用于表示锁与持有者之间的关系（独占模式）。可以看下它的主要方法：</p>
<pre><code class="language-java">// 独占模式，锁的持有者  
private transient Thread exclusiveOwnerThread;  

// 设置锁持有者  
protected final void setExclusiveOwnerThread(Thread t) {  
    exclusiveOwnerThread = t;  
}  

// 获取锁的持有线程  
protected final Thread getExclusiveOwnerThread() {  
    return exclusiveOwnerThread;  
}
</code></pre>]]></description>
      <author>夂夂鱼</author>
      <guid>article-10</guid>
      <pubDate>Mon, 27 Apr 2026 11:28:37 +0000</pubDate>
    </item>
    <item>
      <title>Java 中的 native 方法</title>
      <link>https://dyzmj.top/posts/native</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/native">https://dyzmj.top/posts/native</a></p></blockquote><h1>Java 中的 native 方法</h1>
<p>最近在学习 <code>Thread</code> 类源码时，发现类中第一行的 <strong>registerNatives()</strong> 方法在很多 <code>Java</code> 类中都出现过，比如 <code>Object</code> 类、<code>System</code> 类、<code>Class</code> 类等中都有，而这个方法又是使用 <strong>native</strong> 关键字修饰，代码中没有具体的实现，故对此比较感兴趣，想知道其究竟有什么作用，为何这么多类中都有这个方法，本文则对学习过程做下记录。 下面是类中具体的方法：</p>
<pre><code class="language-java">/* Make sure registerNatives is the first thing &lt;clinit&gt; does. */
private static native void registerNatives();
    static {
        registerNatives();
    }
</code></pre>
<p>既然 <strong>registerNatives()</strong> 方法是 <strong>native</strong> 修饰的本地方法，那我们就从本地方法开始，探究内部的实现。在***《深入Java虚拟机》***一书中，作者<code>Bill Venners</code> 在1.3.1节对本地方法有这样一段描述：</p>
<blockquote>
<p>Java 中有两种方法：Java 方法和本地方法。Java 方法是由 Java 语言编写，编译成字节码，存储在 class 文件中的。本地方法是由其他语言（比如 C，C++，或者汇编语言）编写的，编译成和处理器相关的机器代码。本地方法保存在动态链接库中，格式是各个平台专用的。Java 方法是与平台无关的，但是本地方法却不是。运行中的 Java 程序调用本地方法时，虚拟机装载包含这个本地方法的动态库，并调用这个方法。如下图中，<strong>本地方法是联系 Java 程序和底层操作系统的连接方法。通过本地方法，Java 程序可以直接访问底层操作系统的资源。</strong></p>
</blockquote>
<!-- raw HTML omitted -->
<p>由上述可知，本地方法的实现是由其他语言编写并保存在动态库中，因而在 Java 类中不需要方法的实现。<strong>registerNative()</strong> 方法就是一个本地方法，但其又有别于一般的本地方法，根据方法名称我们可以猜到这是用来注册本地方法的，当 Thread 类被加载时，通过 <strong>static{}</strong> 静态代码块调用本地方法进行注册。这里可能会产生一些疑问：registerNative() 究竟注册了哪些方法？为什么要注册？具体又是如何实现注册的？</p>
<h3>1. registerNative() 究竟注册了哪些方法？</h3>
<p>带着这个问题，我们打开 <strong>Thread</strong> 类的源码，可以看到除了 <strong>registerNative()</strong> 方法外，<strong>Thread</strong> 类中还存在着 <em><strong>currentThread()</strong></em>、<em><strong>yield()</strong></em>、<em><strong>sleep(long millis)</strong></em>、<em><strong>start0()</strong></em>、<em><strong>interrupted()</strong></em> 等本地方法，我们再打开其他类也可以看到，<strong>Object</strong> 类中含有 <em><strong>getClass()</strong></em>、<em><strong>hashCode()</strong></em>、<em><strong>clone()</strong></em> 等本地方法，<strong>System</strong> 类中含有 <em><strong>currentTimeMillis()</strong></em>、<em><strong>nanoTime()</strong></em> 等本地方法。这里可以先猜测一下，<strong>registerNative()</strong> 方法注册的就是除了其本身外的这些本地方法。</p>
<p><img src="https://img.dyzmj.top/img/2021-08-19_163006.jpg" alt="2021-08-19_163006"></p>
<p>现在打开 <strong>OpenJDK11</strong> 的源码包来验证一下：</p>
<p>打开 <em><strong>src</strong></em> 目录，进入到 <em><strong>java.base.share</strong></em> 下可以看到有一个 <em><strong>native</strong></em> 的包，这里应该就是 Java 本地方法的实现。</p>
<!-- raw HTML omitted -->
<p>打开其中的 <em><strong>libjava</strong></em> 包，可以看到很多熟悉的名称，诸如上面提到的 <strong>Object.c</strong> 、<strong>Thread.c</strong> 、<strong>System.c</strong> 等，这些都是使用 C 语言实现的。</p>
<!-- raw HTML omitted -->
<p>下面是 <strong>Thread.c</strong> 的代码：</p>
<pre><code class="language-c">#include &quot;jni.h&quot;
#include &quot;jvm.h&quot;

#include &quot;java_lang_Thread.h&quot;

#define THD &quot;Ljava/lang/Thread;&quot;
#define OBJ &quot;Ljava/lang/Object;&quot;
#define STE &quot;Ljava/lang/StackTraceElement;&quot;
#define STR &quot;Ljava/lang/String;&quot;

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {&quot;start0&quot;,           &quot;()V&quot;,        (void *)&amp;JVM_StartThread},
    {&quot;stop0&quot;,            &quot;(&quot; OBJ &quot;)V&quot;, (void *)&amp;JVM_StopThread},
    {&quot;isAlive&quot;,          &quot;()Z&quot;,        (void *)&amp;JVM_IsThreadAlive},
    {&quot;suspend0&quot;,         &quot;()V&quot;,        (void *)&amp;JVM_SuspendThread},
    {&quot;resume0&quot;,          &quot;()V&quot;,        (void *)&amp;JVM_ResumeThread},
    {&quot;setPriority0&quot;,     &quot;(I)V&quot;,       (void *)&amp;JVM_SetThreadPriority},
    {&quot;yield&quot;,            &quot;()V&quot;,        (void *)&amp;JVM_Yield},
    {&quot;sleep&quot;,            &quot;(J)V&quot;,       (void *)&amp;JVM_Sleep},
    {&quot;currentThread&quot;,    &quot;()&quot; THD,     (void *)&amp;JVM_CurrentThread},
    {&quot;countStackFrames&quot;, &quot;()I&quot;,        (void *)&amp;JVM_CountStackFrames},
    {&quot;interrupt0&quot;,       &quot;()V&quot;,        (void *)&amp;JVM_Interrupt},
    {&quot;isInterrupted&quot;,    &quot;(Z)Z&quot;,       (void *)&amp;JVM_IsInterrupted},
    {&quot;holdsLock&quot;,        &quot;(&quot; OBJ &quot;)Z&quot;, (void *)&amp;JVM_HoldsLock},
    {&quot;getThreads&quot;,        &quot;()[&quot; THD,   (void *)&amp;JVM_GetAllThreads},
    {&quot;dumpThreads&quot;,      &quot;([&quot; THD &quot;)[[&quot; STE, (void *)&amp;JVM_DumpThreads},
    {&quot;setNativeName&quot;,    &quot;(&quot; STR &quot;)V&quot;, (void *)&amp;JVM_SetNativeThreadName},
};

#undef THD
#undef OBJ
#undef STE
#undef STR

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)-&gt;RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
</code></pre>
<p>可以看到 <strong>Thread</strong> 类中的本地方法都在这里面， <strong>registerNative()</strong> 方法注册的正是这些本地方法，我们上面的猜测得到了验证，<strong>registerNative()</strong> 注册的方法就是该类所包含的除了 <strong>registerNative()</strong> 方法外的所有本地方法。</p>
<h3>2. 为什么要使用registerNative() 进行注册？</h3>
<p>针对这个问题，我们在**《 The Java Native Interface -- Programmer’s Guide and Specification 》**一书的8.3节 <code>Registering Native Methods</code> 中找到如下描述：</p>
<blockquote>
<p>Before an application executes a native method it goes through a two-step process to load the native library containing the native method implementation and then link to the native method implementation:</p>
<ol>
<li>System.loadLibrary locates and loads the named native library. For example,  System.loadLibrary(&quot;foo&quot;) may cause foo.dll to be loaded on Win32.</li>
<li>The virtual machine locates the native method implementation in one of the loaded native libraries. For example, a Foo.g native method call requires locat-ing and linking the native function Java_Foo_g, which may reside in foo.dll .</li>
</ol>
<p>This section will introduce another way to accomplish the second step. Instead of relying on the virtual machine to search for the native method in the already loaded native libraries, the JNI programmer can manually link native methods by registering a function pointer with a class reference, method name, and method descriptor:</p>
<pre><code class="language-c">JNINativeMethod nm;
nm.name = &quot;g&quot;;
/* method descriptor assigned to signature field */
nm.signature = &quot;()V&quot;;
nm.fnPtr = g_impl;
(*env)-&gt;RegisterNatives(env, cls, &amp;nm, 1);
</code></pre>
<p>The above code registers the native function g_impl as the implementation of the Foo.g native method:</p>
<pre><code class="language-c">void JNICALL g_impl(JNIEnv *env, jobject self);
</code></pre>
<p>The native function g_impl does not need to follow the JNI naming convention because only function pointers are involved, nor does it need to be exported from the library (thus there is no need to declare the function using JNIEXPORT ).
The native function g_impl must still, however, follow the JNICALL calling convention.</p>
<p>The RegisterNatives function is useful for a number of purposes:</p>
<p>• It is sometimes more convenient and more efficient to register a large number of native method implementations eagerly, as opposed to letting the virtual machine link these entries lazily.</p>
<p>• You may call RegisterNatives multiple times on a method, allowing the native method implementation to be updated at runtime.</p>
<p>• RegisterNatives is particularly useful when a native application embeds a virtual machine implementation and needs to link with a native method implementation defined in the native application. The virtual machine would not be able to find this native method implementation automatically because it only searches in native libraries, not the application itself.</p>
</blockquote>
<p>根据上述内容可以得知，一个 Java 应用程序想要调用一个本地方法，需要执行两个步骤：</p>
<p>1、首先使用 System.loadLibrary() 将包含本地方法实现的动态库文件加载到内存中。</p>
<p>2、当 Java 程序需要调用到本地方法时，虚拟机在加载的动态库文件中定位并链接这个本地方法，从而得以执行该本地方法。</p>
<p><strong>registerNative()</strong> 方法的作用就是取代第二步，在程序加载启动时就将本地方法主动链接到调用方，当 Java 程序需要调用本地方法时就可以直接调用，而不需要虚拟机再去定位并链接本地方法。</p>
<p>同时上述文中还总结了一些使用 registerNatives() 方法的优点：</p>
<ul>
<li>
<p><input checked="" disabled="" type="checkbox"> 通过 registerNatives() 方法在类被加载的时候就主动将本地方法链接到调用方，这比当本地方法被调用时再由虚拟机去定位和链接更方便有效。</p>
</li>
<li>
<p><input checked="" disabled="" type="checkbox"> 如果本地方法在程序运行时被更新了，可以通过多次调用 registerNative() 方法来加载更新。</p>
</li>
<li>
<p><input checked="" disabled="" type="checkbox"> 函数命名自由，使用 registerNative() 方法，在定义本地方法实现的时候，不用去遵循 JNI 命名规范。</p>
</li>
</ul>
<h3>3. registerNative()是如何实现注册的？</h3>
<p>对于这个问题，我们重新回到 <strong>Thread.c</strong> 及 <strong>Object.c</strong> 文件中看下 registerNative() 方法：</p>
<pre><code class="language-c">/*
 *      Stuff for dealing with threads.
 *      originally in threadruntime.c, Sun Sep 22 12:09:39 1991
 */
#include &quot;jni.h&quot;
#include &quot;jvm.h&quot;

#include &quot;java_lang_Thread.h&quot;

#define THD &quot;Ljava/lang/Thread;&quot;
#define OBJ &quot;Ljava/lang/Object;&quot;
#define STE &quot;Ljava/lang/StackTraceElement;&quot;
#define STR &quot;Ljava/lang/String;&quot;

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {&quot;start0&quot;,           &quot;()V&quot;,        (void *)&amp;JVM_StartThread},
    {&quot;stop0&quot;,            &quot;(&quot; OBJ &quot;)V&quot;, (void *)&amp;JVM_StopThread},
    {&quot;isAlive&quot;,          &quot;()Z&quot;,        (void *)&amp;JVM_IsThreadAlive},
    {&quot;sleep&quot;,            &quot;(J)V&quot;,       (void *)&amp;JVM_Sleep},
    {&quot;currentThread&quot;,    &quot;()&quot; THD,     (void *)&amp;JVM_CurrentThread},
	/**  此处省略部分方法... */
};

#undef THD
#undef OBJ
#undef STE
#undef STR

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)-&gt;RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
</code></pre>
<pre><code class="language-c++">/*-
 *      Implementation of class Object
 *
 *      former threadruntime.c, Sun Sep 22 12:09:39 1991
 */
#include &lt;stdio.h&gt;
#include &lt;signal.h&gt;
#include &lt;limits.h&gt;

#include &quot;jni.h&quot;
#include &quot;jni_util.h&quot;
#include &quot;jvm.h&quot;

#include &quot;java_lang_Object.h&quot;

static JNINativeMethod methods[] = {
    {&quot;hashCode&quot;,    &quot;()I&quot;,                    (void *)&amp;JVM_IHashCode},
    {&quot;wait&quot;,        &quot;(J)V&quot;,                   (void *)&amp;JVM_MonitorWait},
    {&quot;notify&quot;,      &quot;()V&quot;,                    (void *)&amp;JVM_MonitorNotify},
    {&quot;notifyAll&quot;,   &quot;()V&quot;,                    (void *)&amp;JVM_MonitorNotifyAll},
    {&quot;clone&quot;,       &quot;()Ljava/lang/Object;&quot;,   (void *)&amp;JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)-&gt;RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}

</code></pre>
<p>由上面的源码可以看到 <strong>registerNatives()</strong> 调用的都是  <code>(*env)-&gt;RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0]));</code> ，上述代码开头 <code>#include &quot;jni.h&quot;</code> 引入 <code>jni.h</code> 头文件，我们进入该头文件下可以看到 <code>JNINativeMethod</code> 、<code>JNIEnv</code> <code>JNIInvokeInterface_</code>  等结构体的定义。</p>
<pre><code class="language-c">/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */
typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;

/*
 * JNI Native Method Interface.
 */
struct JNINativeInterface_;

struct JNIEnv_;

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
</code></pre>
<p>我们抽出其中 <code>JNINativeInterface_</code> 和 <code>JNIEnv_</code> 结构体中的关键方法，可以看到 <code>RegisterNative()</code> 调用的实际是由 <code>JNINativeInterface_</code> 实现的函数。</p>
<pre><code class="language-c++">struct JNINativeInterface_ {
    jint (JNICALL *RegisterNatives)
      (JNIEnv *env, jclass clazz, const JNINativeMethod *methods,
       jint nMethods);
};

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
    jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
                         jint nMethods) {
        return functions-&gt;RegisterNatives(this,clazz,methods,nMethods);
    }
#endif /* __cplusplus */
};
</code></pre>
<p>打开 <code>jni.h</code> 头文件对应的 <code>jni.cpp</code> 文件中的实现函数代码：</p>
<pre><code class="language-c++">struct JNINativeInterface_ jni_NativeInterface = {
    jni_RegisterNatives,
};
</code></pre>
<pre><code class="language-c++">// 注册Java系统类中的本地方法
JNI_ENTRY(jint, jni_RegisterNatives(JNIEnv *env, jclass clazz,
                                    const JNINativeMethod *methods,
                                    jint nMethods))
  JNIWrapper(&quot;RegisterNatives&quot;);
  HOTSPOT_JNI_REGISTERNATIVES_ENTRY(env, clazz, (void *) methods, nMethods);
  jint ret = 0;
  DT_RETURN_MARK(RegisterNatives, jint, (const jint&amp;)ret);

  // 加载对应的类并转换成Klass对象
  Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz));
  // 循环对数组中的本地方法进行处理
  for (int index = 0; index &lt; nMethods; index++) {
    const char* meth_name = methods[index].name;
    const char* meth_sig = methods[index].signature;
    int meth_name_len = (int)strlen(meth_name);

    // The class should have been loaded (we have an instance of the class
    // passed in) so the method and signature should already be in the symbol
    // table.  If they're not there, the method doesn't exist.
    // 方法名
    TempNewSymbol  name = SymbolTable::probe(meth_name, meth_name_len);
    // 方法签名
    TempNewSymbol  signature = SymbolTable::probe(meth_sig, (int)strlen(meth_sig));

    // 如果没找到该方法则抛出java.lang.NoSuchMethodError()
    if (name == NULL || signature == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print(&quot;Method %s.%s%s not found&quot;, k-&gt;external_name(), meth_name, meth_sig);
      // Must return negative value on failure
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), -1);
    }
	// 执行注册本地方法
    bool res = register_native(k, name, signature,
                               (address) methods[index].fnPtr, THREAD);
    if (!res) {
      ret = -1;
      break;
    }
  }
  return ret;
JNI_END
</code></pre>
<pre><code class="language-c++">static bool register_native(Klass* k, Symbol* name, Symbol* signature, address entry, TRAPS) {
  // 找到对应的方法
  Method* method = k-&gt;lookup_method(name, signature);
  if (method == NULL) {
    ResourceMark rm;
    stringStream st;
    st.print(&quot;Method '&quot;);
    Method::print_external_name(&amp;st, k, name, signature);
    st.print(&quot;' name or signature does not match&quot;);
    THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
  }
  // 判断输入的方法是否是一个本地方法
  if (!method-&gt;is_native()) {
    // trying to register to a non-native method, see if a JVM TI agent has added prefix(es)
    // 检查JVMTI是否指定native方法前缀，如果不存在则抛出 NoSuchMethodError 异常
    method = find_prefixed_native(k, name, signature, THREAD);
    if (method == NULL) {
      ResourceMark rm;
      stringStream st;
      st.print(&quot;Method '&quot;);
      Method::print_external_name(&amp;st, k, name, signature);
      st.print(&quot;' is not declared as native&quot;);
      THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
    }
  }

  if (entry != NULL) {
    // 设置本地方法  
    method-&gt;set_native_function(entry,
      Method::native_bind_event_is_interesting);
  } else {
    method-&gt;clear_native_function();
  }
  if (PrintJNIResolving) {
    ResourceMark rm(THREAD);
    tty-&gt;print_cr(&quot;[Registering JNI native method %s.%s]&quot;,
      method-&gt;method_holder()-&gt;external_name(),
      method-&gt;name()-&gt;as_C_string());
  }
  return true;
}
</code></pre>
<h3>4. 如何实现自定义的本地方法？</h3>
<p>① 首先在 Java 类中声明一个被 <code>native</code> 关键字修饰的方法，然后在 <code>static</code> 静态代码块中使用 <code>System.loadLibrary()</code> 方法导入一个外部动态链接库。</p>
<pre><code class="language-java">package com.goldcard.custom;

/**
 * desc: for custom goldcard native method
 *
 * @author dongYu
 * @date 2021/08/20
 */
public class Goldcard {

  // 使用静态代码块导入动态链接库
  static {
    // 注意：loadLibrary的参数必须和动态链接库名一致（不包括后缀名）
    System.loadLibrary(&quot;GoldcardLibrary&quot;);
  }

  // 使用 native 声明一个本地方法
  public native void sayHello();

  // 此处为了方便后续编译执行代码
  public static void main(String[] args) {
    new Goldcard().sayHello();
  }
}

</code></pre>
<p>② 在命令行中使用 <code>javac -h . Goldcard.java</code> 命令编译 Java 源文件，注意中间的 <code>.</code> 不能省略，它代表了在当前路径下生成编译产生的头文件，当然其实你可以把它换成你想要的路径，但为了方便，这里推荐直接使用点，生成字节码文件和头文件，编译完成后的目录结构如下图：</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_095832.jpg" alt="2021-08-23_095832"></p>
<p>说明：如果使用的 jdk 版本过低，可能没有 <code>javac -h</code> 命令，可以使用 <code>javac className.java</code> 先编译 Java 代码，然后使用 <code>javah -jni className</code> 命令生成头文件。</p>
<p>③ 打开刚才编译生成的 <code>com_goldcard_custom_Goldcard.h</code> 头文件，注意，将 <code>#include &lt;jni.h&gt;</code> 修改为 <code>#include &quot;jni.h&quot;</code>，方便后续后续导入 <code>jni.h</code>文件。</p>
<pre><code class="language-c++">/* DO NOT EDIT THIS FILE - it is machine generated */
#include &quot;jni.h&quot;
/* Header for class com_goldcard_custom_Goldcard */

#ifndef _Included_com_goldcard_custom_Goldcard
#define _Included_com_goldcard_custom_Goldcard
#ifdef __cplusplus
extern &quot;C&quot; {
#endif
/*
 * Class:     com_goldcard_custom_Goldcard
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
</code></pre>
<p>其中上面代码中的 <code>JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello(JNIEnv *, jobject)</code> 函数就是我们要实现的 <code>sayHello</code> 方法。</p>
<p>因为我们生成的头文件中包含了一个<code>jni.h</code> 的头文件，后续编译的话需要导入这个文件，所以我们从 jdk 的安装包中找到这个头文件，并将其复制到我们代码所在目录下（注：如果不想复制的话可使用 gcc -l 命令链接到指定的库文件，具体方法可自行百度）：</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_101332.jpg" alt="2021-08-23_101332"></p>
<p>打开 <code>jni.h</code>文件可以看到，其中又包含了一个 <code>#include &quot;jni_md.h&quot;</code> 的文件，这个文件这可以在上面截图的 <code>win32</code> 的目录下找到：</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_101744.jpg" alt="2021-08-23_101744"></p>
<p>完成后我们使用 <code>c++</code> 语言去实现上面提到的 <code>JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello(JNIEnv *, jobject)</code> 的这个函数：</p>
<pre><code class="language-c++">#include &quot;com_goldcard_custom_Goldcard.h&quot;
#include &lt;stdio.h&gt;

JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello
  (JNIEnv *, jobject)
{
	printf(&quot;&gt;&gt;&gt; Hello Goldcard &lt;&lt;&lt;&quot;);
	return;
}
</code></pre>
<p>因为这个函数是声明在 <code>com_goldcard_custom_Goldcard.h</code> 这个头文件中，而且打印输入用到了 <code>printf()</code> 函数，所以在文件头部我们导入这两个文件。</p>
<p>最后在命令行中使用 <code>g++ --share goldcardLibrary.cpp -o goldcardLibrary.dll</code> 命令生成动态链接库文件，注意：生成的动态库名称需要与 Java 代码里的 <code>System.loadLibrary(&quot;文件名&quot;)</code> 中的文件名保持一致。</p>
<p>生成的文件目录结构如下图所示：</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_104203.jpg" alt="2021-08-23_104203"></p>
<p>④ 测试验证</p>
<p>在 <code>IDEA</code> 中新建一个测试类 <code>DemoTest.java</code> 文件，调用 <code>Goldcard.java</code> 中的本地方法</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_110757.jpg" alt="2021-08-23_110757"></p>
<p>直接运行后，系统报 如下异常：</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_112933.jpg" alt="2021-08-23_112933"></p>
<p>此异常有两种解决方式：</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 1、将生成的动态库文件放入 <code>java.library.path</code> 的目录中。</li>
<li><input checked="" disabled="" type="checkbox"> 2、在 IDEA 的运行启动参数的 VM 参数配置 <code>-Djava.library.path=动态库所在目录</code> 即可。</li>
</ul>
<p>这里采用第二种方式执行:</p>
<p><img src="https://img.dyzmj.top/img/2021-08-23_113515.jpg" alt="2021-08-23_113515"></p>
<p>运行程序，最终输出 <code>&gt;&gt;&gt; Hello Goldcard &lt;&lt;&lt;</code> 的执行结果：</p>
<!-- raw HTML omitted -->
<h3>5. 附录</h3>
<p>1、<a href="https://zhuanlan.zhihu.com/p/76613134">C++环境配置</a></p>
<p>2、<a href="https://blog.csdn.net/dz_hexiang/article/details/79257739?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control">调用jni的两种方法javah和RegisterNatives</a></p>
<p>3、<a href="https://blog.csdn.net/zhao007z5/article/details/80062897">JNI原生方法命名规则</a></p>
<p>4、<a href="https://www.aliyundrive.com/s/tRhDdmXtban">深入Java虚拟机PDF</a></p>
<p>5、<a href="https://www.aliyundrive.com/s/FZ7af1m6ioz">The Java Native Interface — Programmer’s Guide and Specification PDF</a></p>
<p>6、<a href="https://www.aliyundrive.com/s/xkXzVdN9w4E">MinGw安装包下载</a></p>
<p>7、<a href="https://hg.openjdk.java.net/jdk-updates/jdk11u/archive/tip.zip">OpenJDK11源码包下载</a></p>
<p>8、<a href="https://www.jianshu.com/p/713a79293bf1">JNI 基础 - JNIEnv 的实现原理</a></p>
<p>9、<a href="https://blog.csdn.net/Soinice/article/details/98674266?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&amp;depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control">Java 关键字之native关键字的作用</a></p>
<p>10、<a href="https://www.dazhuanlan.com/frank0/topics/1311570">后端【JVM源码探索】深入registerNative()底层实现</a></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-9</guid>
      <pubDate>Mon, 27 Apr 2026 11:27:36 +0000</pubDate>
    </item>
    <item>
      <title>NB-IoT 通讯 CoAP 协议解析</title>
      <link>https://dyzmj.top/posts/coap</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/coap">https://dyzmj.top/posts/coap</a></p></blockquote><h2>通讯协议</h2>
<p><strong>通讯协议又称通讯规程，是通信双方对数据传送控制的一种约定，双方必须共同准守</strong>。约定的内容包括：数据格式、同步方式、传送速度、传送速度、传送步骤、检纠错方式、控制字符定义等。双方实体需要准守通信协议中既定的规则，才能将有意义的信息传递给对方。</p>
<p>市面上有很多物联网应用层协议，COAP、HTTP、MQTT 是最常见的三种。HTTP 和 MQTT 均使用 TCP 作为传输层协议，而 COAP 则是基于 UDP 传输协议。同时，传输层的 UDP 和 TCP 协议又都是依赖网络层的 IP 技术。</p>
<p><img src="https://img.dyzmj.top/img/202201171926888.png" alt="image-20220117192652265"></p>
<h3>TCP/IP 网络模型</h3>
<p>TCP/IP 是互联网相关的各类协议族的总称，比如 TCP、UDP、IP、FTP、HTTP、SMTP 等都属于 TCP/IP 族内的协议。</p>
<p>TCP/IP 模型是互联网的基础，它是一系列网络协议的总称。这些协议可以划分为四层，分别为链路层、网络层、传输层和应用层。</p>
<ul>
<li>链路层：负责封装和解封装 IP 报文，发送和接收 ARP 和 RARP 报文等。</li>
<li>网络层：负责路由以及把分组报文发送给目标网络或主机。</li>
<li>传输层：负责对报文进行分组和重组，并以 TCP 或 UDP 协议格式封装报文。</li>
<li>应用层：负责向用户提供应用程序，比如 HTTP、COAP、Telnet、MQTT、DNS 等。</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202201181035019.png" alt="image-20220118103526918"></p>
<p>在网络体系结构中，网络通讯的建立必须是在通讯双方在对等层进行，不能交错。在整个数据传输过程中，数据在发送端时经过各层都要附加上相应层的协议头和协议尾（仅数据链路层需要封装协议尾）部分，也就是要对数据进行协议封装，以标识对应层所用的通讯协议。</p>
<h3>TCP  协议</h3>
<p>当一台计算机想要与另一台计算机通讯时，两台计算机之间的通行需要畅通且可靠，这样才能保证正确收发数据。例如，当你想查看网页或查看电子邮件时，希望完整且按顺序查看网页，而不丢失任何内容。当你下载文件时，希望获得的是完整的文件，而不仅仅是文件的一部分，于是就用到了 TCP 。</p>
<p>TCP（Transmission Control Protocol，传输控制协议），是是一种面向连接的、可靠的、基于字节流得到传输层通信协议，由 IETF 的 RFC 793 定义。TCP 是面向连接的、可靠的流协议，流是指不间断的数据结构，你可以把它想象成排水管中的水流。</p>
<h4>TCP 连接过程</h4>
<p>如下图所示，可以看到建立一个 TCP 连接的过程（三次握手的过程）：</p>
<p><img src="https://img.dyzmj.top/img/202201172040886.png" alt="image-20220117204026784"></p>
<ul>
<li>第一次握手：客户端发送 SYN（SEQ=x）报文给服务器，进入 SYN_SEND 状态</li>
<li>第二次握手：服务端接收到 SYN 报文，回应一个 SYN（SEQ=y）+ ACK（ACK=x+1）报文，并进入 SYN_RECV 状态。</li>
<li>第三次握手：客户端收到服务端的 SYN 报文，回应一个 ACK（ACK=y+1）报文，进入连接（Established）状态。</li>
</ul>
<p>为了防止出现失效的连接请求报文被服务端接收的情况，从而产生错误，这个就是为什么 TCP 建立连接需要三次握手，而不是两次的原因。</p>
<p>通过 <code>wireshark</code> 抓包工具我们也能复现这个过程：</p>
<p><img src="https://img.dyzmj.top/img/202201172055057.png" alt="image-20220117205517952"></p>
<h4>TCP 断开连接</h4>
<p>建立一个连接需要三次握手，而终止一个连接需要进经过四次握手，这是由于 TCP 的半关闭造成的，具体过程如下图所示：</p>
<p><img src="https://img.dyzmj.top/img/202201172100167.png" alt="image-20220117210019092"></p>
<ul>
<li>第一次握手：某个应用进程首先调用 <code>close</code>，称该端执行 “主动关闭（active close）”，该端的 TCP 于是发送一个 FIN 字节，表示数据发送完毕。</li>
<li>第二次握手：接收到这个 FIN 的队端执行 “被动关闭（passive close）”，这个 FIN 由 TCP 确认。</li>
</ul>
<blockquote>
<p>注意：FIN 的接收也作为一个文件结束符（end-of-file）传递给接收端应用进程，放在已排队等候该应用进程接收的任何其他数据之后，因为，FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。</p>
</blockquote>
<ul>
<li>第三次握手：一段时间后，接收到这个文件结束符的应用进程将调用 <code>close</code> 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。</li>
<li>第四次握手：接收这个最终 FIN 的原发送端 TCP（即执行主动关闭的那一端）确认这个 FIN。</li>
</ul>
<p>既然每个方向都需要一个 FIN 和一个 ACK，因此通常需要4个分节。</p>
<p>如下，通过 <code>wireshark</code> 抓包工具我们也能复现断开连接的过程：</p>
<p><img src="https://img.dyzmj.top/img/202201172102633.png" alt="image-20220117210219537"></p>
<h4>TCP 协议特点</h4>
<ul>
<li>面向连接</li>
</ul>
<p>面向连接，是指发送数据之前必须在两端建立连接。建立连接的方法是 ”三次握手“，这样能建立可靠的连接。</p>
<ul>
<li>仅支持单传播</li>
</ul>
<p>每条 TCP 传输连接只能有两个端点，只能进行点对点的数据传输，不支持多端和广播传输方式。</p>
<ul>
<li>面向字节流</li>
</ul>
<p>TCP 不像 UDP 那样一个个报文独立的传输，而是在不保留报文边界的情况下已字节流方式进行传输。</p>
<ul>
<li>可靠传输</li>
</ul>
<p>对于可靠传输，判断丢包、误码靠的是 TCP 的段编号以及确认号。</p>
<ul>
<li>提供拥塞控制</li>
</ul>
<p>当网络出现拥塞的时候，TCP 能够减小向网络注入数据的速率和数量，缓解拥塞。</p>
<ul>
<li>TCP 提供全双工通信</li>
</ul>
<p>TCP 允许通信双方的应用程序在任何时候都能发送数据，因为 TCP 连接的两端都没有缓存，用来临时存放存放双向通信的数据。当然，TCP可以立即发送一个数据段，也可以缓存一段时间以便一次发送更多的数据段。</p>
<p>TCP 协议报文格式：</p>
<p><img src="https://img.dyzmj.top/img/202201181124230.png" alt="image-20220118112408115"></p>
<ul>
<li>源端口号（2字节）：源设备上发送数据包进程的16位端口号。</li>
<li>目标端口号（2字节）：目标设备上接收进程的16位端口号。</li>
<li>序号（4字节）：由滑动窗口确认系统使用，作为数据分包的第一个字节的序号。在 SYN 位置为1的情况下，它表示初始序号。</li>
<li>确认号（4字节）：如果 ACK 位置为1，该字段有效并包含设备用于对接收到的数据包进行确认的序号。</li>
<li>数据偏移（4位）：表示数据包的开始位置与 TCP 首部的开始处偏移多少个 32位（4字节）的偏移量。该字段的值乘以4才得到字节形式表示的偏移量、</li>
<li>控制位（6位）：用于相关控制信息。这些控制信息包括 URG 紧急位、ACK 确认位、PUSH 推送位、RST 复位位、SYN 同步位和 FIN 结束位。</li>
<li>窗口（2字节）：用于流控制，表示该数据包的发送者在一定时间内能从其他设备接收的字节数。</li>
<li>校验和（2字节）：用于检测错误的 16 位校验和。</li>
<li>紧急指针（2字节）：如果 URG 位置为1，该字段包含紧急数据后面的 “正常” 数据的第一字节的序列号。</li>
</ul>
<h3>UDP  协议</h3>
<p>UDP（User Datagram Protocol，用户数据报协议），在网络中它与 TCP 协议一样用于处理数据包，是一种无连接的协议。在 OSI 模型中，在第四层传输层，处于 IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的特点，也就是说，当报文发送之后，是无法得知其是否安全完整到达的。</p>
<h4>UDP 协议特点</h4>
<ol>
<li>面向无连接</li>
</ol>
<p>首先 UDP 是不需要和 TCP 一样在发送数据前进行三次握手建立连接的，想发数据就可以开始发送了，并且也只是数据报文的搬运工，不会对数据报文进行任何拆分和拼接操作。</p>
<p>具体来说就是：</p>
<ul>
<li>在发送端，应用层将数据传递给传输层的 UDP 协议，UDP 只会给数据增加一个 UDP 头，标识下是 UDP 协议，然后就传递给网络层了。</li>
<li>在接收端，网络层将数据传递给传输层，UDP 只去除 IP 报文头后就传递给应用层，不会进行任何拼接操作。</li>
</ul>
<ol start="2">
<li>有单播，多播和广播的功能</li>
</ol>
<p>UDP 不只支持一对一的传输方式，同样支持一对多，多对多，多对一的方式，也就是说 UDP 提供了单播、多播、广播的功能。</p>
<ol start="3">
<li>UDP 是面向报文的</li>
</ol>
<p>发送方的 UDP 对应用程序交下来的报文，在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文，既不合并，也不拆分，而是保留这些报文的边界。因此应用程序必须选择合适大小的报文。</p>
<ol start="4">
<li>不可靠性</li>
</ol>
<p>首先不可靠性体现在无连接上，通信都不需要建立连接，想发送就发送，这样的情况肯定不可靠。并且收到什么数据就传递什么数据，并且也不会备份数据，发送数据也不会关心对方是否已经正确接收到数据了。</p>
<p>再者网络环境时好时坏，但是 UDP 因为没有拥塞控制，一直会以恒定的速度发送数据。即使网络条件不好，也不会对发送速率进行调整，这样实现的弊端就是在网络条件不好的情况下可能会导致丢包，但是优点也很明显，在某些实时性要求高的场景（比如电话会议、直播等）就需要使用 UDP 而不是 TCP。</p>
<ol start="5">
<li>头部开销小，传输数据报文时是很高效的</li>
</ol>
<p><img src="https://img.dyzmj.top/img/202201181012944.png" alt="image-20220118101202861"></p>
<p>UDP 头部包含了以下几个数据：</p>
<ul>
<li>两个十六位的端口号，分别为源端口（可选字段）和目标端口。</li>
<li>整个数据报文的长度。</li>
<li>整个数据报文的校验和（IPv4 可选字段），该字段用于发现头部信息和数据中的错误。</li>
</ul>
<p>因此 UDP 的头部开销小，只有八字节，相比 TCP 的至少二十字节要少的多，在传输数据报文时是很高效的。</p>
<h2>应用层协议简介</h2>
<h3>HTTP 协议</h3>
<p>HTTP 协议，即超文本传输协议（Hypertext Transfer Protocol），是一种详细规定了浏览器和万维网服务器之间互相通信的规则，通过因特网传送万维网文档得到数据传送协议。</p>
<p>目前 HTTP 协议作为 WEB 的标准协议已被广泛使用，它在一些物联网场景中同样可以使用，例如手机、PC等终端设备。但是作为适应浏览器场景的 HTTP 协议，并不适用于物联网的其他设备。</p>
<p>适用范围：开放物联网中的资源，实现服务被其他应用所调用。</p>
<p><strong>优势：</strong></p>
<ol>
<li>简单的工作模式，请求 / 响应。</li>
<li>完整的方法定义。</li>
<li>合理的状态码设计。</li>
<li>友好的媒体类型支持，如文本、图片、视频等。</li>
</ol>
<p><strong>缺点：</strong></p>
<ol>
<li>单向传输，可以通过客户端轮询实现类似推送效果或者HTTP2.0</li>
<li>安全性不高，HTTP是明文协议，可以使用 HTTPS 传输。</li>
<li>HTTP 是文本协议，冗长的协议头部，对于运算、存储、带宽资源受限的设备来说开销大。</li>
</ol>
<h3>CoAP 协议</h3>
<p>CoAP（Constrained Application Protocol）即受限的应用协议。CoAP 是为了让低功耗受限设备可以接入互联网，由 IETF 组织制定的，它借鉴了 HTTP 大量成功经验，同样使用请求 / 响应工作模式。</p>
<p>使用范围：适用于互联网环境下一对一 M2M 通讯。</p>
<p><strong>优势：</strong></p>
<ol>
<li>采用和 HTTP 相似语义的请求和响应码，使用二进制报文，报文大小较小。</li>
<li>传输层基于 UDP 协议，比 TCP 数据包小，并不需要建立连接带来握手的开销。</li>
<li>资源发送支持，通过观察者模式实现类似发布 / 订阅效果。</li>
</ol>
<p><strong>缺点：</strong></p>
<ol>
<li>基于 UDP 的不可靠传输，但通过四种报文类型的组合及重传机制提高了传输的可靠性。</li>
<li>基于 UDP 的无连接传输，不利于不同网络间消息的回传。</li>
</ol>
<h3>MQTT 协议</h3>
<p>MQTT（Message Queuing Telemetry Transport）即消息队列遥测传输。</p>
<p>MQTT 协议最初是在 1999 年由 IBM 公司开发的，用于将石油管道上的传感器与卫星相连接。2014 年正式成为 OASIS 开放标准。</p>
<p>MQTT 使用类似 MQ 常用的发布 / 订阅模式，起到应用程序解耦、异步消息、削峰填谷的作用。很多 MQ 中间件也支持 MQTT 协议，比如 ActiveMQ、RabbitMQ、Kafka 等。</p>
<p>适用范围：在低带宽、不可靠的网络下提供基于云平台的远程设备的数据传输和监控。</p>
<p><strong>优势：</strong></p>
<ol>
<li>使用发布 / 订阅模式，提供一对多的消息发布，使消息发送者和接受者在时间和时空上解耦。</li>
<li>二进制协议，网络传输开销非常小（固定头部是2字节）。</li>
<li>灵活的 Topic 订阅、QoS 等特性。</li>
</ol>
<p><strong>缺点：</strong></p>
<ol>
<li>集中化部署，服务端压力大，需要考虑流程控制及高可用。</li>
<li>对于请求 / 响应模式的支持需要在应用层根据消息 ID 做发布主题和订阅主题之间的关联。</li>
</ol>
<h2>CoAP 协议详解</h2>
<h3>REST 风格</h3>
<blockquote>
<p>REST（具象状态传输）是 Roy Thomas Fielding 博士于2000年在他的博士论文 “Architectural Styles and the Design of Network-based SoftwareArchitectures” 中提出来的一种 Web 软件架构风格。目前在三种主流的 Web 服务实现方案中，REST 模式与复杂的 SOAP 和 XML-RPC 相比更加简洁，越来越多的 Web服务 开始采用 REST 风格设计和实现。REST 是设计风格而不是标准，REST 通常基于使用 HTTP、URI、XML、JSON 和HTML 这些现有的协议和标准来实现。</p>
</blockquote>
<p>REST 风格具有以下特点：</p>
<ol>
<li>资源一般由 URI 来指定。</li>
<li>无状态通信。</li>
<li>对资源的操作包括获取、创建、修改和删除等，这些操作对应 HTTP 的 GET、POST、PUT 和 DELETE 方法。</li>
<li>资源的表现形式可以是 XML、JSON 或 HTML 格式文件。</li>
</ol>
<p><img src="https://img.dyzmj.top/img/202201181431241.png" alt="image-20220118143109121"></p>
<p>CoAP 基于 REST 架构风格，基于简洁、清晰，且易于实现的特点。CoAP 和 HTTP 一样，使用请求 / 响应工作模式：客户端发送 CoAP请求，服务器一旦侦听到请求便会根据其包含的 URI 对资源进行定位，并按请求方法决定如何操作该资源：读取（GET）、创建（POST）、修改（PUT）或者删除（DELETE）。服务器处理完请求之后，将返回客户端一个 CoAP响应，其中包含响应码，也有可能有响应内容。</p>
<p><img src="https://img.dyzmj.top/img/202201181445029.png" alt="image-20220118144546954"></p>
<p>虽然 CoAP 和 HTTP 多有相似，但其较 HTTP 简单的多。CoAP 建立在 UDP 栈上，这是与 HTTP 相比最主要的区别，它可以更快更好的进行资源优化。CoAP 可以使用与 10kb RAM 系统上，占用内存较少，同时，CoAP 能够发现网络中的节点，这对于低功耗无线传感器网络的自治金额自我修复设计非常有用。</p>
<h3>CoAP 首部分析</h3>
<p>CoAP 是一个完整的二进制应用层协议，和 TCP/IP 协议族中的其他协议一样，CoAP协议带有报文头，负载（Payload）和报文头之间使用单字节分隔符 <code>0xFF</code> 隔离。报文头里的版本编号 Ver、报文类型 T、标签长度指示 TKL、准则 Code、报文序号 MessageID 为必填内容。</p>
<p>CoAP 协议报文结构如下：</p>
<p><img src="https://img.dyzmj.top/img/202201181516432.png" alt="image-20220118151654325"></p>
<ul>
<li><strong>【 Ver 】版本编号</strong>，占2位，取固定值 0b01（0b开头表示后面的数据为二进制数）；</li>
<li><strong>【 T 】报文类型</strong>，占2位，CoAP 协议定义了4种不同形式的报文：<code>CON</code>、<code>NON</code>、<code>ACK</code>、<code>RST</code> :
<ul>
<li><em>Confirmable Message（CON）</em>：CON 报文需要被接受者确认，即每一个 CON 报文都需要对应一个 ACK 报文或 RST 报文，此时 T = 0b00。</li>
<li><em>Non-Confirmable Message（NON）</em>：不需要被确认的报文，通常用于传感器一类只需要单向传送数据的应用场景，此时 T = 0b01。</li>
<li><em>Acknowledgement Message（ACK）</em>：应答报文，用于确认 CON 报文，此时 T = 0b10。</li>
<li><em>Rest Message（RST）</em>：复位报文，当服务器收到一个 CON 报文，如果报文中出现上下文缺失，导致无法处理时，服务器将返回一个 RST 报文，此时 T = 0b11。</li>
</ul>
</li>
<li><strong>【 TKL 】标签长度指示</strong>，占4位，用于指示 Token 区域的具体长度。当 CoAP 报文包含 Token 时，TKL 取值 0b0001（1个字节长度）、0b0010（2个字节长度）、0b0100（4个字节长度），当 CoAP 报文省略 Token 时，TKL 取值 0b0000。</li>
<li><strong>【 Code 】准则</strong>，占8位（一个字节）。Code 的值分为高三位 Class 部分和低五位 Detail 部分，采用 <code>c.dd</code> 的形式描述：<code>c</code> 和 <code>dd</code> 均采用十进制，<code>c</code> 的取值范围是 0<del>7，<code>dd</code> 的取值范围是 0</del>31。Code 在请求和响应中的具体表现显示有所不同：当 <code>c=0</code> 时，表示 CoAP 请求，否则表示 CoAP 响应。<code>Code=0.00</code> 表示空报文，是一种特殊形式的 CoAP 响应，Code 值与表达含义如下：</li>
</ul>
<p><img src="https://img.dyzmj.top/img/202201181545214.png" alt="image-20220118154544117"></p>
<p><img src="https://img.dyzmj.top/img/202201181607278.png" alt="image-20220118160756207"></p>
<p><strong>【 Message ID 】报文序号</strong>，占2个字节，并采用大端格式描述，用于客户端和服务器建立请求和响应报文之间的一一对应关系，即起报文确认作用。一组对应的 CoAP 请求和 CoAP 响应使用相同的 <code>Message ID</code>，在同一次会话中 ID 保持不变，此次会话结束后该 ID 将会被回收利用。<code>Message ID</code> 可以弥补 UDP 传输方式带来的不可靠性。</p>
<p><strong>【 Token 】标签</strong>，长度由 TKL 定义，可以是1字节、2字节或4字节。通常用于应用确认。</p>
<p><strong>【 Options 】选项</strong>，CoAP 请求或响应中可携带一组或多组 Options，功能类似于 HTTP 中的通信首部字段、请求首部字段、响应首部字段和实体首部字段。Options 是 CoAP 核心协议中较为复杂的部分，但 Option 也给 CoAP 的应用带来了诸多灵活性。CoAP 选项包括 Uri-Host、Uri-Port、Uri-Path、Uri-Query、Content-Format、Accept、Etag、If-Match和If-None-Match 等部分。</p>
<p><img src="https://img.dyzmj.top/img/202201181642118.png" alt="image-20220118164258042"></p>
<p><strong>Option Delta：</strong> 占4位，转换为10进制数值范围在0~12之间 。</p>
<p>如果 Option Delta == 13，那么 Option Delta (extended) 部分为 1 字节，Option Delta = 13 + Option Delta(extended)</p>
<p>如果 Option Delta == 14，那么 Option Delta (extended) 部分为 2 字节，Option Delta = 14 + 255 + Option Delta(extended)</p>
<p>Option Delta == 15 无效</p>
<p><strong>Option Length：</strong> 占4位， 转换为10进制数值范围在0~12之间 。</p>
<p>如果 Option Length == 13，那么 Option Length (extended) 部分为 1 字节，Option Length = 13 + Option Length(extended)</p>
<p>如果 Option Length == 14，那么 Option Length (extended) 部分为 2 字节，Option Length = 14 + 255 + Option Length(extended)</p>
<p>Option Delta == 15 无效</p>
<p><strong>Option Value：</strong> 由 Option Length 的值决定 Option Value 的长度。</p>
<p>Option Delta 对应值的含义如下：</p>
<p><img src="https://img.dyzmj.top/img/202201191602766.png" alt="img"></p>
<p><strong><code>3</code></strong> Uri-Host：CoAP 主机名称，例如 iot.eclipse.org</p>
<p><strong><code>7</code></strong>  Uri-Port：CoAP 端口号，默认为5683</p>
<p><strong><code>11</code></strong> Uri-Path：资源路由或路径，例如 \temperature。资源路径采用 UTF8 字符串形式，不计 <code>\</code> 的长度。</p>
<p><strong><code>15</code></strong> Uri-Query：访问资源参数，例如 ?value1=1&amp;value2=2，参数与参数之间使用 “&amp;” 分隔，Uri-Query 和 Uri-Path之间采用“?”分隔。</p>
<p>在这些option中，Content-Format 和 Accept 用于表示CoAP负载的媒体格式。</p>
<p><strong><code>12</code></strong> Content-Format：指定 CoAP 复杂媒体类型，媒体类型采用整数描述，例如 application/json 对应整数50，application/octet-stream 对应整数40。</p>
<p><strong><code>17</code></strong> Accept：指定 CoAP 响应复杂中的媒体类型，媒体类型的定义和 Content-Format 相同。</p>
<p><strong>【 0xFF 】分隔符</strong>，占1个字节，用于区分 CoAP 首部和具体负载。</p>
<p><strong>【 Payload 】负载</strong>，正在有用的被交互的数据。CoAP 负载可能包含不同的媒体类型：二进制、文本、XML、JSON、CBOR 等，但并不支持 HTML 类型的负载。</p>
<h3>CoAP 媒体类型</h3>
<p>CoAP 负载支持多种媒体类型，并采用编号的方式对其进行定义。编号一般为2字节的无符号整数。</p>
<p><img src="https://img.dyzmj.top/img/202201191132809.png" alt="image-20220119113221718"></p>
<p>文本类型（text/plain）、二进制类型（application/octet-stream）、JSON类型（application/json）是在物联网中应用最广泛的三种媒体类型，application/link-format 则是专属于 CoAP 的媒体类型，一般在 CoAP 资源发现中使用。</p>
<p><strong>（一）文本类型</strong></p>
<p>CoAP 中默认的媒体类型为文本类型，若负载为文本类型，那么 CoAP 请求和响应都不需要指定 Content-Format。虽然文本类型使用方便，但需要提前对负载结果的每个部分按顺序进行定义。</p>
<p><img src="https://img.dyzmj.top/img/202201191144817.png" alt="image-20220119114410763"></p>
<p>如传感器的数据：21.3,62,2022-01-01 17:31:10，传输双方需要事先约定第一个值表示 ”温度“，第二个值表示 ”湿度“，第三个值表示 ”时间“，数据才能被正确解析。</p>
<p><strong>（二）二进制类型</strong></p>
<p>TLV 形式的二进制负载也经常出现在某些物联网应用中出现。TLV 是 Tag，Length 和 Value 的缩写，一个基本的数据元就包含上面三个域。Tag 是该数据元的唯一标识，Length 是 Value 域的长度，Value 就是数据本身。Type 和 Length 的长度固定，一般为 2 或 4 个字节，Value 的长度由 Length 指定，如字符串 <code>9F0607A0000000031010</code>，其中 <code>9F06</code> 是 Tag，<code>07</code> 是长度，<code>A0000000031010</code> 就是数据本身。</p>
<p><strong>（三）JSON类型</strong></p>
<p>JSON（JavaScript Object Notation，JS对象表示法）是一种轻量级的数据交换格式，易于使用者阅读和编写，也易于机器解析和生成。JSON 分为 JSON对象和 JSON 数组两种构造结构：</p>
<ul>
<li>JSON 对象</li>
</ul>
<p>JSON 对象是一个无序的键值对集合，以 <code>{</code> 开始并以 <code>}</code> 结束，每个键后面跟一个分号 <code>:</code>，每组键值对之间使用逗号 <code>，</code> 分隔。</p>
<ul>
<li>JSON 数组</li>
</ul>
<p>JSON 数组是值有有序集合，以 <code>[</code> 开始并以 <code>]</code> 结束，值之间使用逗号 <code>,</code> 分隔。</p>
<h3>CoAP 重传机制</h3>
<p>由于 CoAP 采用 UDP 作为其传输层协议，故在网络数据传输过程中，无论是请求还是响应，均存在丢包的风险。为此，CoAP 设计了双层结构 -- 消息层和请求/响应层，消息层处理端点之间的数据交换，并为各报文类型提供重传机制，来弥补传输过程中的不可靠性。</p>
<p><img src="https://img.dyzmj.top/img/202201191319455.png" alt="image-20220119131910374"></p>
<p>前面提到过，CoAP 定义了四种不同类型的报文：<code>CON</code>、<code>NON</code>、<code>ACK</code>、<code>RST</code>。<code>CON</code> 报文需要被接受者确认，即每一个 <code>CON</code> 报文都对应一个准确热 <code>ACK</code> 报文或 <code>RST</code> 报文，如果在规定的时间内客户端未接受到 <code>ACK</code> 报文或 <code>RST</code> 报文，那么客户端将启动 “重传机制”，如：</p>
<p><img src="https://img.dyzmj.top/img/202201191334972.png" alt="image-20220119133416901"></p>
<p><img src="https://img.dyzmj.top/img/202201191337165.png" alt="image-20220119133726110"></p>
<p>在某个时刻，CoAP 客户端发起 <code>CON</code> 类型的 GET 请求，试图从服务端获得温度传感器数据 <code>temperature</code>。如请求没有准确到达 CoAP 服务器（称作 “CoAP 请求丢失”），或服务器虽收到请求并返回响应，但响应未正确到达客户端（称作 “CoAP 响应丢失”），超过一定时间后，客户端就会将前一次 CoAP 请求判定为失败，并再次发送同样的 GET 请求，两次请求的首部和负载完全相同。</p>
<p>CoAP 报文重传机制受制于 <code>ACK_TIMEOUT</code>、<code>ACK_RAMDOM_FACTOR</code>、<code>MAX_RETRANSMIT</code> 三个参数。<code>ACK_TIMEOUT</code> 是响应等待超时时间，典型值为 2s，<code>ACK_RAMDOM_FACTOR</code> 是一个不小于1的随机系数，典型值取1.5，<code>MAX_RETRANSMIT</code> 表示最大重传次数，典型值为4：</p>
<p><img src="https://img.dyzmj.top/img/202201191348111.png" alt="image-20220119134816066"></p>
<p>对于一个 <code>CON</code> 报文而言，初始超时时间是 <code>ACK_TIMEOUT</code> 到 <code>ACK_RAMDOM_FACTOR * MAX_RETRANSMIT</code> 之间的随机数。重传计数器从0开始计数，一旦在初始超时时间内客户端未收到服务器回传的 <code>ACK</code> 报文或 <code>RST</code> 报文，那么 <code>CON</code> 报文将会被重新发送，重传计数器自动增加至1，并且下一次重传超时间自动递增为上一次超时时间的两倍。</p>
<p>假设响应等待超时时间 <code>ACK_TIMEOUT</code> 取2s，随机系数 <code>ACK_RAMDOM_FACTOR</code> 取1.5，最大重传次数 <code>MAX_RETRANSMIT</code> 为4，则初始超时时间是2~3s之间的随机数（本例中我们取2.5s）。最极端的情况下，CoAP 客户端会发送一次正常的 <code>CON</code> 报文，并在重传4次后停止对该报文的传输。</p>
<p><img src="https://img.dyzmj.top/img/202201191416545.png" alt="image-20220119141604497"></p>
<p>在该过程中，相邻 <code>CON</code> 报文的时间间隔分别为 2.5s、2 * 2.5s、4 * 2.5s、8 * 2.5s，总计时间即<strong>最大传输时间</strong> <strong>MAX_TRANSMIT_SPAN</strong> = 2.5 * (1 + 2 + 8) = 37.5s。</p>
<p>最后一次 <code>CON</code> 报文的重传并不意味整个传输过程已经结束，CoAP 客户端还要等待最后一次超时时间：16 * 2.5s，然后将停止本条报文的传输。从 CoAP 客户端发送第一次 <code>CON</code> 报文到最后停止传输，总共消耗的时间称为<strong>最大等待时间</strong> <strong>MAX_TRANSMIT_WAIT</strong> = 2.5 *（1+2+4+8+16）= 77.5s。</p>
<p><img src="https://img.dyzmj.top/img/202201191453402.png" alt="image-20220119145354315"></p>
<h3>CoAP 报文解析</h3>
<p>以 电信 OC 平台上设备登录报文为例：</p>
<pre><code class="language-bash">4402AD3D3DAD0700B272641128396C776D326D3D312E300D0565703D38363131303730353937333934353903623D55066C743D333030FF3C2F3E3B72743D226F6D612E6C776D326D222C3C2F312F303E2C3C2F322F303E2C3C2F332F303E2C3C2F342F303E2C3C2F352F303E2C3C2F362F303E2C3C2F372F303E2C3C2F31392F303E2C3C2F31392F313E
</code></pre>
<p>上述报文为十六进制数据，根据 [CoAP 首部分析](### CoAP 首部分析) 可以将报文分成7个部分：</p>
<p><strong>①  44：</strong> 1字节，转换为二进制可得 0b01000100，对应Ver、T、TKL 的值为：</p>
<p><img src="https://img.dyzmj.top/img/202201191519482.png" alt="image-20220119151946419"></p>
<p><strong>②  02：</strong> 1字节，Code 准则，转换为二进制可得 0b00000010，对应 Code 值为：</p>
<p><img src="https://img.dyzmj.top/img/202201191528791.png" alt="image-20220119152805743"></p>
<p><strong>③  AD3D：</strong> 2字节，Message ID 报文序号，对应的值为：</p>
<p><img src="https://img.dyzmj.top/img/202201191550077.png" alt="image-20220119155023018"></p>
<p><strong>④  3DAD0700：</strong> 4字节，表示Token值， TLK 可知此报文 Token 长度为4字节，故此处截取4字节长度 Token 值。</p>
<p><strong>⑤  B272641128396C776D326D3D312E300D0565703D38363131303730353937333934353903623D55066C743D333030：</strong> Options选项，</p>
<p>由 Option Delta 和 Option Length 占一个字节，故取 <code>B2</code> 先进行解析，转换为二进制数为 <strong>0b10110010</strong>，Option Delta 和 Option Length 解析为：</p>
<p><img src="https://img.dyzmj.top/img/202201191620019.png" alt="image-20220119162056952"></p>
<p>Option Value 取2个字节长度为：7264，转ASSIC为 rd，此 Option 表示为 uri-path=/rd。</p>
<p>继续往下，截取 <strong>11</strong>，转换为二进制数为 <strong>0b00010001</strong>，Option Delta 和 Option Length 解析为 1、1，累加上第一个偏移量为 11 + 1 = 12，对应的 Option 选项名称为 <strong><code>Content-Format</code></strong> ，Option Value 长度为1，故值截取为 28，转换为十进制数为 40 ，对应 CoAP 媒体类型为 <code>application/link-format</code>。</p>
<p><img src="https://img.dyzmj.top/img/202201191657563.png" alt="image-20220119165756484"></p>
<p>截取 <strong>39</strong>，转为为二进制数为 <strong>0b00111001</strong>，Option Delta 和 Option Length 解析为 3、9，累加前面的偏移量为 12 + 3 = 15，对应的 Option 选项名称为 <code>Uri-Query</code>，Option Value 长度为9，故值截取为 <strong>6C776D326D3D312E30</strong>，进行 ASCII 转换可以得到值为 <strong>lwm2m=1.0</strong>。</p>
<p><img src="https://img.dyzmj.top/img/202201191710618.png" alt="image-20220119171037536"></p>
<p>截取 <strong>0D</strong>，转为二进制数为 0b00001101，Option Delta 和 Option Length 解析为 0、13，由于 Option Length 为 13，故下面还含有 Option Delta(extended) 和 Option Length(extended)，截取 <strong>05</strong> 可得二进制数 0b00000101，Option Delta(extended) 和 Option Length(extended) 解析为 0、5，故 Option Length 值为 13 + 5 = 18，即 Option Value 的长度为18，截取 <strong>65703D383631313037303539373339343539</strong>，可得值为 <strong>ep=861107059739459</strong>。</p>
<p><img src="https://img.dyzmj.top/img/202201191725214.png" alt="image-20220119172527077"></p>
<p>截取 <strong>03</strong>，转为二进制数得 0b00000011，Option Delta 和 Option Length 解析为 0、3，累加前面的偏移量为 15 + 0 = 15，对应的 Option 选项名称为 <code>Uri-Query</code>，Option Value 长度为3，故值截取为 <strong>623D55</strong>，进行 ASCII 转换可以得到值为 <strong>b=U</strong>。</p>
<p><img src="https://img.dyzmj.top/img/202201191731310.png" alt="image-20220119173113241"></p>
<p>截取 <strong>06</strong>，转为二进制数得 0b00000110，Option Delta 和 Option Length 解析为 0、6，累加前面的偏移量为 15 + 0 = 15，对应的 Option 选项名称为 <code>Uri-Query</code>，Option Value 长度为6，故值截取为 <strong>6C743D333030</strong>，进行 ASCII 转换可以得到值为 <strong>lt=300</strong>。</p>
<p><img src="https://img.dyzmj.top/img/202201191733881.png" alt="image-20220119173314812"></p>
<p><strong>Uri-Query</strong> 参数与参数之间使用 <code>&amp;</code> 符号隔开，故此参数完整形式为 <code>lwm2m=1.0&amp;ep=861107059739459&amp;b=U&amp;lt=300</code>。</p>
<p><strong>⑥  FF：</strong> 1个字节，分隔符，用于区分 CoAP 首部 与 负载数据。</p>
<p><strong>⑦  3C2F3E3B72743D226F6D612E6C776D326D222C3C2F312F303E2C3C2F322F303E2C3C2F332F303E2C3C2F342F303E2C3C2F352F303E2C3C2F362F303E2C3C2F372F303E2C3C2F31392F303E2C3C2F31392F313E：</strong> Payload 负载数据，ASSIC 转换后得 <strong><code>&lt;/&gt;;rt=&quot;oma.lwm2m&quot;,&lt;/1/0&gt;,&lt;/2/0&gt;,&lt;/3/0&gt;,&lt;/4/0&gt;,&lt;/5/0&gt;,&lt;/6/0&gt;,&lt;/7/0&gt;,&lt;/19/0&gt;,&lt;/19/1&gt;</code></strong> ，与 <a href="https://www.ctwing.cn/sbjr/31#see">OC 平台文档</a> 要求一致。</p>
<p><img src="https://img.dyzmj.top/img/202201191911868.png" alt="image-20220119191159791"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-8</guid>
      <pubDate>Mon, 27 Apr 2026 11:26:45 +0000</pubDate>
    </item>
    <item>
      <title>Netty 源码分析之二 ServerBootstrap 服务引导器</title>
      <link>https://dyzmj.top/posts/netty_02</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/netty_02">https://dyzmj.top/posts/netty_02</a></p></blockquote><h2><strong>ServerBootstrap类结构</strong></h2>
<p>我们继续上一章的例子，上一章已经创建了<strong>bossGroup</strong>和<strong>workerGroup</strong>，再往下就是创建ServerBootstrap服务启动器了，先看下这个类的继承结构图：</p>
<p><img src="https://img.dyzmj.top/img/202305311100081.png" alt="image-20230531110042897"></p>
<p>可以看到服务启动器和其父类中都是一些基本的属性的设置，构造方法中并没有做太多事情，只初始化了一些属性选项及配置：</p>
<pre><code class="language-java">// EventLoopGroup对象  -- bossGroup
volatile EventLoopGroup group;
// channel工厂用于创建channel
@SuppressWarnings(&quot;deprecation&quot;)
private volatile ChannelFactory&lt;? extends C&gt; channelFactory;
// 本地地址
private volatile SocketAddress localAddress;

// The order in which ChannelOptions are applied is important they may depend on each other for validation
// purposes.
// 可选项集合
private final Map&lt;ChannelOption&lt;?&gt;, Object&gt; options = new LinkedHashMap&lt;ChannelOption&lt;?&gt;, Object&gt;();
// 集合属性
private final Map&lt;AttributeKey&lt;?&gt;, Object&gt; attrs = new ConcurrentHashMap&lt;AttributeKey&lt;?&gt;, Object&gt;();
// 启动器启动的时候设置的处理器， 如 .handler(new LoggingHandler(LogLevel.INFO))设置进来的
private volatile ChannelHandler handler;

AbstractBootstrap(AbstractBootstrap&lt;B, C&gt; bootstrap) {
    group = bootstrap.group;
    channelFactory = bootstrap.channelFactory;
    handler = bootstrap.handler;
    localAddress = bootstrap.localAddress;
    synchronized (bootstrap.options) {
        options.putAll(bootstrap.options);
    }
    attrs.putAll(bootstrap.attrs);
}
</code></pre>
<pre><code class="language-java">private final Map&lt;ChannelOption&lt;?&gt;, Object&gt; childOptions = new LinkedHashMap&lt;ChannelOption&lt;?&gt;, Object&gt;();
private final Map&lt;AttributeKey&lt;?&gt;, Object&gt; childAttrs = new ConcurrentHashMap&lt;AttributeKey&lt;?&gt;, Object&gt;();
private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;
// 构造方法
public ServerBootstrap() { }
</code></pre>
<h2><strong>bossGroup 和 workerGroup</strong></h2>
<p>然后就是将之前创建的两个Group设置一下：</p>
<pre><code class="language-java">bootstrap.group(bossGroup, workerGroup)
</code></pre>
<p>其中bossGroup也就是parentGroup，主要是负责处理TCP/IP连接的，而workerGroup也就是childGroup，主要是负责Channel(通道)的I/O事件处理。</p>
<pre><code class="language-java">public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);
    if (this.childGroup != null) {
        throw new IllegalStateException(&quot;childGroup set already&quot;);
    }
    this.childGroup = ObjectUtil.checkNotNull(childGroup, &quot;childGroup&quot;);
    return this;
}
</code></pre>
<h2><strong>ServerBootstrap 参数设置</strong></h2>
<p>再往下看：</p>
<pre><code class="language-java">.channel(NioServerSocketChannel.class)
</code></pre>
<p>这里就比较讲究了，传入的对象是一个Class对象，应该可以想到里面使用的可能是反射，进入到源码里面可以看到：</p>
<pre><code class="language-java">public B channel(Class&lt;? extends C&gt; channelClass) {
    return channelFactory(new ReflectiveChannelFactory&lt;C&gt;(
            ObjectUtil.checkNotNull(channelClass, &quot;channelClass&quot;)
    ));
}
</code></pre>
<p>上面代码中首先通过<strong>new ReflectiveChannelFactory</strong> 创建了一个反射通道工厂，在内部经过反射获取无参的构造方法，当调用工厂中的<strong>newChannel()</strong> 方法时就可以创建通道实例对象了：</p>
<pre><code class="language-java">public class ReflectiveChannelFactory&lt;T extends Channel&gt; implements ChannelFactory&lt;T&gt; {
    // 在反射通道工厂中取得构造方法，在后面可以直接创建实例对象
    private final Constructor&lt;? extends T&gt; constructor;

    public ReflectiveChannelFactory(Class&lt;? extends T&gt; clazz) {
        ObjectUtil.checkNotNull(clazz, &quot;clazz&quot;);
        try {
            // 无参构造方法
            this.constructor = clazz.getConstructor();
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(&quot;Class &quot; + StringUtil.simpleClassName(clazz) +
                    &quot; does not have a public non-arg constructor&quot;, e);
        }
    }

    @Override
    public T newChannel() {
        try {
            // 创建实例对象
            return constructor.newInstance();
        } catch (Throwable t) {
            throw new ChannelException(&quot;Unable to create Channel from class &quot; + constructor.getDeclaringClass(), t);
        }
    }
}
</code></pre>
<p>继续进入channelFactory方法中，主要就是设置下通道工厂，返回自身对象，使其能够继续进行链式调用。</p>
<pre><code class="language-java">public B channelFactory(io.netty.channel.ChannelFactory&lt;? extends C&gt; channelFactory) {
    return channelFactory((ChannelFactory&lt;C&gt;) channelFactory);
}
public B channelFactory(ChannelFactory&lt;? extends C&gt; channelFactory) {
    ObjectUtil.checkNotNull(channelFactory, &quot;channelFactory&quot;);
    if (this.channelFactory != null) {
        throw new IllegalStateException(&quot;channelFactory set already&quot;);
    }
    this.channelFactory = channelFactory;
    return self();
}
</code></pre>
<p>继续往下，设置通道的可选项参数，其中option是针对parentGroup的，childOption是针对childGroup的</p>
<pre><code class="language-java">.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE,true)
</code></pre>
<h2><strong>ChannelOption配置</strong></h2>
<p><code>ChannelOption.SO_BACKLOG</code></p>
<blockquote>
<p>BACKLOG用于构造服务端套接字<strong>ServerSocket</strong>对象，标识当服务器请求处理线程全满时，用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1，Java将使用默认值50。ChannelOption.SO_BACKLOG对应的是TCP/IP协议listen函数中的<strong>backlog</strong>参数（在linux服务器中输入 <strong>man</strong> <strong>listen</strong>可查看详细），函数<strong>listen</strong>(int socketfd, int backlog)用来初始化服务端可连接队列，服务端处理客户端连接请求是顺序处理的，所以同一时间只能处理一个客户端连接，多个客户端连接的时候，服务端将不能处理的客户端连接请求放在队列中等待处理，backlog参数指定了队列的大小。</p>
</blockquote>
<p><code>               ChannelOption.SO_REUSEADDR             </code></p>
<blockquote>
<p>ChannelOption.SO_REUSEADDR对应于套接字选项中的<strong>SO_REUSEADDR</strong>，这个参数表示允许重复使用本地地址和端口。比如，某个服务器进程占用了TCP的3247端口进行监听，此时再次监听该端口就会返回错误，使用该参数就可以解决问题，该参数允许共用该端口，这个在服务器程序中会经常使用，比如某个进程非正常退出，该程序占用的端口可能要被占用一段时间才能允许其他进程中使用，而且程序死掉以后，内核也需要一定的时间才能够是否此端口，不设置SO_REUSEADDR就无法正常使用该端口。(<a href="https://www.cnblogs.com/heroinss/p/9910977.html">参考：SO_REUSEPORT</a>)</p>
</blockquote>
<p><code>               ChannelOption.SO_KEEPALIVE             </code></p>
<blockquote>
<p>ChannelOption.SO_KEEPALIVE参数对应于套接字中的SO_KEEPALIVE，该参数用于设置TCP连接，当设置该选项以后，系统会测试连接的状态，这个选项用于可能长时间没有数据交流的连接。如果在两个小时内没有数据的通信时，服务端会自动发送一个活动探测数据报文。如果客户端因为断电、网路异常或客户端异常时，那么服务端的连接可以关闭，释放资源。</p>
</blockquote>
<p><code>               ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF             </code></p>
<blockquote>
<p>ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF分别对应套接字选项中SO_SNDBUF和SO_RCVBUF参数，这两个参数用于操作发送缓冲区和接收缓冲区的大小。发送缓冲区用于保存发送数据，直到发送成功；接收缓冲区用于保存网络协议站内接收到的数据，直到应用程序读取成功。</p>
</blockquote>
<p><code>               ChannelOption.SO_LINGER             </code></p>
<blockquote>
<p>ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER，Linux内核默认的处理方式是当用户调用close()方法的时候，函数返回，在可能的情况下，尽量发送数据，不一定保证会发送完剩余的数据，造成的数据的不确定性，使用SO_LINGER可以阻塞close()的调用时间，直到数据完全发送。</p>
</blockquote>
<p><code>               ChannelOption.TCP_NODELAY             </code></p>
<blockquote>
<p>ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY，该参数的使用与Nagle算法有关。Nagle算法是将小的数据包组装为更大的帧然后进行发送，而不是输入一次发送一次，因此在数据包不足的时候会等待其他数据到了，组装成大的数据包进行发送，虽然该方式有效提高网络的有效负载，但是却造成了延时，而该参数是的作用就是禁止使用Nagle算法，适用于小数据即时传输。与TCP_NODELAY相对应的是TCP_CORK，该选项是需要等到发送的数据量最大的时候，一次性发送数据，适用于文件传输。在实际使用中，通常希望服务是低延时的，因此建议将TCP_NODELAY设置为true。</p>
</blockquote>
<p><code>               ChannelOption.ALLOCATOR             </code></p>
<blockquote>
<p>Netty4.x版本中使用对象池，重用缓冲区</p>
<p>bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);</p>
</blockquote>
<p><strong>Java支持的套接字选项</strong></p>
<p><code>               TCP_NODELAY             </code></p>
<blockquote>
<p>用于设置是否禁用Nagle算法.Nagle算法用于自动合并大量小的缓冲区消息;这个过程(称为nagling)通过减少必须发送的包的数量来提高网络应用程序系统的性能.</p>
<p>仅用于TCP: SocketImpl</p>
</blockquote>
<p><code>               SO_BINDADDR             </code></p>
<blockquote>
<p>套接字绑定的本地地址,类型为 INADDR_ANY.套接字创建时绑定的地址无法在之后修改.</p>
<p>可用于: SocketImpl, DatagramSocketImpl</p>
</blockquote>
<p><code>               SO_REUSEADDR             </code></p>
<blockquote>
<p>此选项在Java中仅用于标识多播套接字.多播套接字默认配置该选项.</p>
<p>可用于: DatagramSocketImpl</p>
</blockquote>
<p><code>               SO_BROADCAST             </code></p>
<blockquote>
<p>用于关闭或开启数据报套接字的广播功能</p>
</blockquote>
<p><code>               IP_MULTICAST_IF             </code></p>
<blockquote>
<p>设置发送多播数据包的传出接口.用于多个网络接口的主机希望使用系统默认值以外的其他值.可用于广播: DatagramSocketImpl</p>
</blockquote>
<p><code>               IP_MULTICAST_IF2             </code></p>
<blockquote>
<p>和<strong>IP_MULTICAST_IF</strong>一样,但是可以支持IPV4和IPV6.</p>
</blockquote>
<p><code>               IP_MULTICAST_LOOP             </code></p>
<blockquote>
<p>此选项启用或禁用多播数据报的loopback. 多播套接字默认启用此选项.</p>
</blockquote>
<p><code>               IP_TOS             </code></p>
<blockquote>
<p>此选项在IP头为TCP或UDP套接字设置服务类型(type-of-service)或流量类(traffic class)字段</p>
</blockquote>
<p><code>               SO_LINGER             </code></p>
<blockquote>
<p>指定延迟关闭超时. 此选项禁用/启用从TCP套接字的close()立即返回. 使用非零整数超时启用此选项,意味着close()将阻塞,等待向对等方发送和确认所有写入的数据, 之后套接字将被优雅地关闭. 在到达延迟超时时, 使用TCP RST强制关闭套接字. 启用超时为0的选项会立即强制关闭. 如果指定的超时值超过65,535, 则会减少到65,535.</p>
<p>仅对TCP: SocketImpl有效</p>
</blockquote>
<p><code>               SO_TIMEOUT             </code></p>
<blockquote>
<p>设置阻塞套接字操作的超时:ServerSocket.accept();SocketInputStream.read();DatagramSocket.receive();必须在进入阻塞操作之前设置, 该选项才能生效. 如果超时过期, 操作将继续阻塞,</p>
<p>java.io.InterruptedIOException. 在这种情况下, 套接字不会关闭.</p>
<p>适用于所有的套接字实现:SocketImpl, DatagramSocketImpl</p>
</blockquote>
<p><code>               SO_SNDBUF             </code></p>
<blockquote>
<p>设置平台底层I/O发送缓冲区大小.当setSendBufferSize时, 这是应用程序向内核提供的关于通过套接字发送数据时使用的缓冲区大小的建议.当getSendBufferSize时, 这必须返回平台在此套接字上发送数据时实际使用的缓冲区的大小.</p>
<p>适用于所有的套接字实现:SocketImpl, DatagramSocketImpl.</p>
</blockquote>
<p><code>               SO_RCVBUF             </code></p>
<blockquote>
<p>设置平台底层I/O接收缓冲区大小.当setReceiveBufferSize时, 这是应用程序向内核提供的关于通过套接字接收数据时使用的缓冲区大小的建议.当getReceiveBufferSize时, 这必须返回平台在此套接字上接收数据时实际使用的缓冲区的大小.</p>
<p>适用于所有的套接字实现:SocketImpl, DatagramSocketImpl.</p>
</blockquote>
<p><code>               SO_KEEPALIVE             </code></p>
<blockquote>
<p>当为一个TCP套接字设置了keepalive选项，并且在两个小时内套接字之间没有任何数据交换(注意:实际的值取决于实现)，TCP会自动向对等端发送一个keepalive探测。这个探测是一个TCP段，对等端必须响应它。预期会有三种响应:</p>
<ol>
<li>对等端响应期望的ACK. 应用程序不会被通知(因为一切正常). TCP将在下一个2小时无活动后再次发送一个探测.</li>
<li>对等端响应RST, 这表示对端主机已崩溃并重启. 套接字将被关闭.</li>
<li>对等端无响应. 套接字将被关闭.</li>
<li>此选项的目的时检测对端主机是否已崩溃.</li>
<li>仅适用于 TCP socket: SocketImpl</li>
</ol>
</blockquote>
<p><code>               SO_OOBINLINE             </code></p>
<blockquote>
<p>当设置OOBINLINE选项时,在套接字上接收到的任何TCP紧急数据(TCP URG)都将通过套接字输入流接收. 当该选项被禁用时(这是默认值), 紧急数据将被悄悄地丢弃(不通知应用程序).</p>
</blockquote>
<p><code>               SO_BACKLOG             </code></p>
<blockquote>
<p>backlog参数是套接字上挂起连接的最大数量. 它的确切语义是特定于实现的. 具体地说, 实现可以设置最大长度, 也可以选择忽略参数. 如果backlog参数的值为0或负值, 则使用特定于实现的缺省值</p>
</blockquote>
<h2><strong>ChannelHandler配置</strong></h2>
<p>现在回到代码中继续往下看，设置通道处理器，其中handler是针对parentGroup的，childHandler是针对childGroup的。</p>
<pre><code class="language-java">.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer&lt;SocketChannel&gt;() {
    @Override
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(new MyNettyServerHandler());
    }
});
</code></pre>
<pre><code class="language-java">public B handler(ChannelHandler handler) {
    this.handler = ObjectUtil.checkNotNull(handler, &quot;handler&quot;);
    return self();
}

public ServerBootstrap childHandler(ChannelHandler childHandler) {
    this.childHandler = ObjectUtil.checkNotNull(childHandler, &quot;childHandler&quot;);
    return this;
}
</code></pre>
<h3><strong>Netty的日志处理</strong></h3>
<p>在hander方法内我们首先定义了一个日志处理器，用于设置netty在通讯过程中记录日志的等级。至于ChannelHandler通道处理器后面会单独详谈，此处我们先来研究一下netty是如何处理日志的：</p>
<p>首先我们先看下LoggingHandler类的信息：</p>
<pre><code class="language-java">@Sharable
@SuppressWarnings({ &quot;StringConcatenationInsideStringBufferAppend&quot;, &quot;StringBufferReplaceableByString&quot; })
public class LoggingHandler extends ChannelDuplexHandler {
    // 默认日志级别为 Debug
    private static final LogLevel DEFAULT_LEVEL = LogLevel.DEBUG;

    // 实际使用的日志框架，如 slf4j、log4j等
    protected final InternalLogger logger;
    // 日志框架使用的日志级别
    protected final InternalLogLevel internalLevel;
	
    // Netty 使用的日志级别
    private final LogLevel level;
   
    // 用于控制ByteBuf和ByteBufHolder的日志记录格式和详细程度
    private final ByteBufFormat byteBufFormat;
}
</code></pre>
<p><code>@Sharable</code> ：表示同一实例的ChannelHandler实例可以多次添加到一个或多个ChannelPipeline中，没有竞争条件。如果未指定此注释，则每次将其添加到管道时都必须创建一个新的处理程序实例，因为它具有非共享状态 (例如成员变量)。</p>
<p><a href="https://blog.csdn.net/wb_snail/article/details/106263470?spm=1001.2101.3001.6650.5&amp;utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-5-106263470-blog-78431948.235%5Ev36%5Epc_relevant_default_base3&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-5-106263470-blog-78431948.235%5Ev36%5Epc_relevant_default_base3&amp;utm_relevant_index=6"> @Sharable注解详解可参考此链接 </a></p>
<p>此处使用 <code>@Sharable</code> 说明 LoggingHandler没有状态相关变量，所有 Channel 都可以使用一个实例。继承自 ChannelDuplexHandler 表示对出入站事件都进行日志记录。</p>
<p>日志等级：</p>
<pre><code class="language-java">public enum LogLevel {
    TRACE(InternalLogLevel.TRACE),
    DEBUG(InternalLogLevel.DEBUG),
    INFO(InternalLogLevel.INFO),
    WARN(InternalLogLevel.WARN),
    ERROR(InternalLogLevel.ERROR);
}
</code></pre>
<p>看下 LoggingHandler 的构造方法：</p>
<pre><code class="language-java">public LoggingHandler(LogLevel level) {
    // ByteBufFormat.HEX_DUMP: 表示将指定的ByteBuf格式化打印，便于人工阅读
    this(level, ByteBufFormat.HEX_DUMP);
}

public LoggingHandler(LogLevel level, ByteBufFormat byteBufFormat) {
    this.level = ObjectUtil.checkNotNull(level, &quot;level&quot;);
    this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, &quot;byteBufFormat&quot;);
    // 设置实际的日志框架
    logger = InternalLoggerFactory.getInstance(getClass());
    // 设置框架日志级别
    internalLevel = level.toInternalLevel();
}
</code></pre>
<p>既然 LoggingHandler 继承了 ChannelDuplexHandler 类，那它一定实现了很多其中的方法，如 channelActive、channelRead、close等方法，看下这些方法中的日志是如何打印的，以 channelRead为例</p>
<pre><code class="language-java">@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 根据日志等级打印输入日志内容，并格式化显示 msg
    if (logger.isEnabled(internalLevel)) {
        logger.log(internalLevel, format(ctx, &quot;READ&quot;, msg));
    }
    ctx.fireChannelRead(msg);
}

// 格式话输入的 msg
protected String format(ChannelHandlerContext ctx, String eventName, Object arg) {
    if (arg instanceof ByteBuf) {
        return formatByteBuf(ctx, eventName, (ByteBuf) arg);
    } else if (arg instanceof ByteBufHolder) {
        return formatByteBufHolder(ctx, eventName, (ByteBufHolder) arg);
    } else {
        return formatSimple(ctx, eventName, arg);
    }
}
// 根据 byteBufFormat 判断使用何种类型格式化日志内容
private String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) {
    String chStr = ctx.channel().toString();
    int length = msg.readableBytes();
    if (length == 0) {
        StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 4);
        buf.append(chStr).append(' ').append(eventName).append(&quot;: 0B&quot;);
        return buf.toString();
    } else {
        int outputLength = chStr.length() + 1 + eventName.length() + 2 + 10 + 1;
        if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
            int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
            int hexDumpLength = 2 + rows * 80;
            outputLength += hexDumpLength;
        }
        StringBuilder buf = new StringBuilder(outputLength);
        buf.append(chStr).append(' ').append(eventName).append(&quot;: &quot;).append(length).append('B');
        if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
            buf.append(NEWLINE);
            appendPrettyHexDump(buf, msg);
        }
        return buf.toString();
    }
}
</code></pre>
<h3><strong>childHandler处理</strong></h3>
<p>我们继续回到 <code>.childHandler(new ChannelInitializer&lt;SocketChannel&gt;() {})</code> 方法中，其实也就是设置了 <code>childHandler</code>，此时设置的就是通道初始化的 <code>ChannelInitializer</code>:</p>
<pre><code class="language-java">public ServerBootstrap childHandler(ChannelHandler childHandler) {
    this.childHandler = ObjectUtil.checkNotNull(childHandler, &quot;childHandler&quot;);
    return this;
}
</code></pre>
<h3><strong>ChannelInitializer 通道初始化器</strong></h3>
<p>ChannelInitializer 是一种特殊的 ChannelInboundHandler，它提供了在通道注册到 eventLoop 后初始化通道的简单方法，用于在某个 Channel 注册到 EventLoop 后，对这个 Channel 执行一些初始化操作。ChannelInitializer 虽然会在一开始被注册到 Channel 相关的 pipeline 里，但是在初始化完成之后，ChannelInitializer 会将自己从 pipeline 中移除，不会影响后续的操作。</p>
<h4><strong>ChannelInitializer类图</strong></h4>
<p><img src="https://img.dyzmj.top/img/202306021535748.png" alt="image-20230602153515280"></p>
<p>可以看到 <strong>ChannelInitializer</strong> 继承自ChannelInboundHandler 接口，且为抽象类，不能直接使用。</p>
<pre><code class="language-java">public abstract class ChannelInitializer&lt;C extends Channel&gt; extends ChannelInboundHandlerAdapter
</code></pre>
<p><img src="https://img.dyzmj.top/img/202306021626184.png" alt="image-20230602162620244"></p>
<p><strong>ChannelInitializer</strong> 声明了一个抽象方法 <code>initChannel(C ch)</code>，要想实例化 <strong>ChannelInitializer</strong>，就需要实现这个方法。</p>
<p>由于示例代码中启动的是 TCP 的服务，所以在初始化的 ChannelInitializer 时，我们使用 <code>SocketChannel</code>，即TCP 通道。</p>
<p><img src="https://img.dyzmj.top/img/202306021646100.png" alt="image-20230602164647687"></p>
<p>继续看 initChannel 抽象方法的实现：</p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-7</guid>
      <pubDate>Mon, 27 Apr 2026 11:25:37 +0000</pubDate>
    </item>
    <item>
      <title>Netty 源码分析之一 NioEventLoopGroup 初始化</title>
      <link>https://dyzmj.top/posts/netty_01</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/netty_01">https://dyzmj.top/posts/netty_01</a></p></blockquote><h2><strong>一、Netty如何运行？</strong></h2>
<p>运行环境：</p>
<p><code>Windows10  12核  16G  Dell台式机</code></p>
<p>netty版本：</p>
<p><code>4.1.65.Final</code></p>
<p>​</p>
<pre><code class="language-java">public class MyNettyServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
    				 .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer&lt;SocketChannel&gt;() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new MyNettyServerHandler());
                        }
                    });
            ChannelFuture cf = bootstrap.bind(3247).sync();
            cf.addListener(future -&gt; {
                if (future.isSuccess()) {
                    System.out.println(&quot;listen port 3247 success&quot;);
                } else {
                    System.out.println(&quot;listen port 3247 failure&quot;);
                }
            });
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
</code></pre>
<p>以上是一个常用的Netty启动模板，代码虽然不多，但已经搭起了一个强大健壮的服务器，下面我将按照上述代码梳理下Netty的启动流程，方便进一步理解Netty的运行机制和原理。</p>
<h2><strong>二、NioEventLoopGroup</strong></h2>
<p>先看下main方法中NioEventLoopGroup类的继承结构图：</p>
<p><img src="https://img.dyzmj.top/img/202305311033461.png" alt="image-20230531103317996"></p>
<p>上图中上面部分是由Java提供的并发接口和迭代器，主要用于提交任务、调度任务这些，继承迭代器主要是为了统一外界遍历接口。下面部分就是<strong>Netty</strong>提供的，比如<strong>EventExecutorGroup</strong>事件执行器组，根据名称可以猜到里面应该定义了一些任务执行相关的方法，打开类结构可以看到里面的方法很多跟Java并发线程部分很多一样。</p>
<p><img src="https://img.dyzmj.top/img/202305311033348.png" alt="image-20230531103335984"></p>
<p>下面<strong>EventLoopGroup</strong>继承了<strong>EventExecutorGroup</strong>接口，并且增加了一些方法，比如next()用来获取下一个<strong>EventLoop</strong>事件循环器。</p>
<p><img src="https://img.dyzmj.top/img/202305311034303.png" alt="image-20230531103421025"></p>
<p>再向下由<strong>MultithreadEventLoopGroup</strong>扩展到了多线程事件循环组，继承了<strong>MultithreadEventExecutorGroup</strong>，实现<strong>EventLoopGroup</strong>，将两者结合，一方面是需要事件执行器，一方面又需要定义事件循环接口，也就是说我应该告诉执行器们它们应该做什么。最后<strong>NioEventLoopGroup</strong>只实现了很关键的<strong>newChild</strong>()方法：</p>
<p><img src="https://img.dyzmj.top/img/202305311034896.png" alt="image-20230531103435591"></p>
<h2><strong>三、NioEventLoop</strong></h2>
<p>进入newChild()方法可以看到，最后返回的是一个<strong>NioEventLoop</strong>，我们先看下这个类的继承结构图：</p>
<p><img src="https://img.dyzmj.top/img/202305311034448.png" alt="image-20230531103455404"></p>
<p>上图也是将继承结构归纳为JDK提供的和Netty提供的两类，不过先不用深究，只用看继承了哪些类的名称，最终继承了一个SinleThreadEventLoop单线程事件循环类，这时就可以知道NioEventLoop内有一个单线程循环，这里就不用再考虑线程安全问题了。</p>
<p>回过头来，我们已经可以看到NioEventLoopGroup和NioEventLoop的关系了：</p>
<p><img src="https://img.dyzmj.top/img/202305311035001.png" alt="image-20230531103513140"></p>
<h2><strong>四、初始化事件循环组</strong></h2>
<h3><strong>1、创建事件循环组</strong></h3>
<p>现在，我们从<strong>main</strong>方法中的第一行来看看**new NioEventLoopGrop(1)**内究竟做了些什么。</p>
<pre><code class="language-java">public NioEventLoopGroup() {
    this(0);
}

public NioEventLoopGroup(int nThreads) {
    this(nThreads, (Executor) null);
}

public NioEventLoopGroup(int nThreads, Executor executor) {
    this(nThreads, executor, SelectorProvider.provider());
}

public NioEventLoopGroup(
        int nThreads, Executor executor, final SelectorProvider selectorProvider) {
    this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
}

public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
                         final SelectStrategyFactory selectStrategyFactory) {
    super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    // 若未指定线程数量的话，默认输入为0，此处会使用CPU核数*2作为默认线程数量
}

static {
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
            &quot;io.netty.eventLoopThreads&quot;, NettyRuntime.availableProcessors() * 2));
}
</code></pre>
<p>继续往里执行：</p>
<pre><code class="language-java">protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
    // 这里创建了一个执行器选择工厂，就是如何选择执行器来做事，默认是可以从头到尾轮着选择，即取模的方式
    this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
</code></pre>
<p>最后到达终点：</p>
<pre><code class="language-java">protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                        EventExecutorChooserFactory chooserFactory, Object... args) {
    checkPositive(nThreads, &quot;nThreads&quot;);
    // 线程池
    if (executor == null) {
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }
    // 事件执行器
    children = new EventExecutor[nThreads];

    for (int i = 0; i &lt; nThreads; i ++) {
        boolean success = false;
        try {
            children[i] = newChild(executor, args);
            success = true;
        } catch (Exception e) {
            // TODO: Think about if this is a good exception type
            throw new IllegalStateException(&quot;failed to create a child event loop&quot;, e);
        } finally {
            if (!success) {
                for (int j = 0; j &lt; i; j ++) {
                    children[j].shutdownGracefully();
                }
                for (int j = 0; j &lt; i; j ++) {
                    EventExecutor e = children[j];
                    try {
                        while (!e.isTerminated()) {
                            e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                        }
                    } catch (InterruptedException interrupted) {
                        // Let the caller handle the interruption.
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
    }
    // 事件执行选择器
    chooser = chooserFactory.newChooser(children);
    // 终止事件
    final FutureListener&lt;Object&gt; terminationListener = new FutureListener&lt;Object&gt;() {
        @Override
        public void operationComplete(Future&lt;Object&gt; future) throws Exception {
            if (terminatedChildren.incrementAndGet() == children.length) {
                terminationFuture.setSuccess(null);
            }
        }
    };
    for (EventExecutor e: children) {
        e.terminationFuture().addListener(terminationListener);
    }
    Set&lt;EventExecutor&gt; childrenSet = new LinkedHashSet&lt;EventExecutor&gt;(children.length);
    Collections.addAll(childrenSet, children);
    readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
</code></pre>
<h3><strong>2、创建任务执行器new ThreadPerTaskExecutor(newDefaultThreadFactory())</strong></h3>
<p>newDefaultThreadFactory</p>
<p>首先创建线程工厂，类似于创建线程池的线程工厂，代码跟进去以后可以看到：</p>
<p><img src="https://img.dyzmj.top/img/202305311037095.png" alt="image-20230531103710919"></p>
<p>此处可以看到前缀<strong>prefix</strong>变成了<strong>nioEventLoopGroup-2-</strong>，因为在前面初始化<strong>MultithreadEventExecutorGroup</strong>时，在<strong>MultithreadEventExecutorGroup</strong>中有个全局事件执行器<strong>GlobalEventExecutor</strong>的变量要初始化，根据类的加载流程，其会在构造方法执行前初始化。</p>
<p><img src="https://img.dyzmj.top/img/202305311037520.png" alt="image-20230531103725368"></p>
<p>进入<strong>GlobalEventExecutor</strong>中可以看到构造方法已经创建了一个默认的线程工厂：</p>
<p><img src="https://img.dyzmj.top/img/202305311037161.png" alt="image-20230531103744973"></p>
<p>进入默认线程工厂中可以看到，此时<strong>prefix</strong>前缀为<strong>globalEventExecutor-1-</strong></p>
<p><img src="https://img.dyzmj.top/img/202305311037295.png" alt="image-20230531103757227"></p>
<h3><strong>3、new ThreadPerTaskExecutor</strong></h3>
<p>我们再进入ThreadPerTaskExecutor的构造方法中可以看到，这个类就是设置了一个线程工厂，当有任务时就创建一个线程执行：</p>
<p><img src="https://img.dyzmj.top/img/202305311038763.png" alt="image-20230531103817705"></p>
<h3><strong>4、初始化事件执行器</strong></h3>
<p>我们前面提到的<strong>nThreads</strong>线程数量就是用在此处，用来创建对应个数的<strong>EventExecutor</strong>事件执行器：</p>
<pre><code class="language-java">children = new EventExecutor[nThreads];

for (int i = 0; i &lt; nThreads; i ++) {
    boolean success = false;
    try {
        children[i] = newChild(executor, args);
        success = true;
    } catch (Exception e) {
        // TODO: Think about if this is a good exception type
        throw new IllegalStateException(&quot;failed to create a child event loop&quot;, e);
    } finally {
        ...
    }
}
</code></pre>
<h3><strong>5、进入newChild创建NioEventLoop</strong></h3>
<pre><code class="language-java">@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
    EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
    return new NioEventLoop(this, executor, (SelectorProvider) args[0],
        ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
}
</code></pre>
<p>进入newChild方法中，首先判断<strong>args</strong>可变参数是否存在4个，若存在则直接取出<strong>EventLoopTaskQueueFactory</strong>类型的参数，不存在直接为空。调试环境下可以看到我们实际输入的参数只有3个：</p>
<p><img src="https://img.dyzmj.top/img/202305311041308.png" alt="image-20230531104112991"></p>
<p>打开当前类<strong>NioEventLoopGroup</strong>的构造方法可以看到第四个参数，NioEventLoopGroup支持使用指定的任务队列工厂来替换默认的（<a href="https://github.com/netty/netty/pull/9247">参考:#9247</a>）</p>
<p><img src="https://img.dyzmj.top/img/202305311041547.png" alt="image-20230531104148836"></p>
<h3><strong>6、NioEventLoop构造方法</strong></h3>
<p>接下来进入<strong>NioEventLoop</strong>的构造方法中：</p>
<pre><code class="language-java">NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
             EventLoopTaskQueueFactory queueFactory) {
    super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
            rejectedExecutionHandler);
    // 选择器提供器
    this.provider = ObjectUtil.checkNotNull(selectorProvider, &quot;selectorProvider&quot;);
    // 选择器策略
    this.selectStrategy = ObjectUtil.checkNotNull(strategy, &quot;selectStrategy&quot;);
    // 获取选择器元组
    final SelectorTuple selectorTuple = openSelector();
    // 包装后的选择器
    this.selector = selectorTuple.selector;
    // 未包装的选择器(JDK提供的原始的NIO选择器)
    this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
</code></pre>
<p>首先调用了父类的构造方法，可以看到内部调用了两次<strong>newTaskQueue</strong>，创建了两个任务队列：</p>
<pre><code class="language-java">private static Queue&lt;Runnable&gt; newTaskQueue(
        EventLoopTaskQueueFactory queueFactory) {
    // 由上面的分析可知queueFactory队列工厂默认输入为空
    if (queueFactory == null) {
        return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
    }
    return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
}
</code></pre>
<p>然后进入<strong>newTaskQueue0</strong>方法中：</p>
<pre><code class="language-java">private static Queue&lt;Runnable&gt; newTaskQueue0(int maxPendingTasks) {
    // This event loop never calls takeTask()
    return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.&lt;Runnable&gt;newMpscQueue()
            : PlatformDependent.&lt;Runnable&gt;newMpscQueue(maxPendingTasks);
}
</code></pre>
<p>PlatformDependent类就是根据不同的操作系统创建不同的数据，此处创建的是<strong>MpscChunkedArrayQueue</strong>队列，它是JCTools下的一个高性能队列，这里只要知道是个队列就行，后面会单独列一章说明。</p>
<p>然后进入<strong>NioEventLoop</strong>的父类<strong>SingleThreadEventLoop</strong>构造方法中：</p>
<pre><code class="language-java">protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
                                boolean addTaskWakesUp, Queue&lt;Runnable&gt; taskQueue, Queue&lt;Runnable&gt; tailTaskQueue,
                                RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);
    // 尾任务队列，此处暂不太了解有什么用处
    tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, &quot;tailTaskQueue&quot;);
}
</code></pre>
<p>再进入<strong>SingleThreadEventLoop</strong>的父类<strong>SingleThreadEventExecutor</strong>的构造方法中：</p>
<pre><code class="language-java">protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                    boolean addTaskWakesUp, Queue&lt;Runnable&gt; taskQueue,
                                    RejectedExecutionHandler rejectedHandler) {
    super(parent);
    // 添加任务唤醒，默认false
    this.addTaskWakesUp = addTaskWakesUp;
    // 最大等待任务数量
    this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
    // 封装过的执行器
    this.executor = ThreadExecutorMap.apply(executor, this);
    // 任务队列
    this.taskQueue = ObjectUtil.checkNotNull(taskQueue, &quot;taskQueue&quot;);
    // 拒绝策略
    this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, &quot;rejectedHandler&quot;);
}
</code></pre>
<p>最后是把NIO事件循环组放入<strong>AbstractEventExecutor</strong>抽象事件执行器中：</p>
<p><img src="https://img.dyzmj.top/img/202305311043592.png" alt="image-20230531104333539"></p>
<p>最后再看下类的层次结构(见下图)，<strong>NioEventLoop</strong>的继承关系就很清晰了：</p>
<p><img src="https://img.dyzmj.top/img/202305311043044.png" alt="image-20230531104350050"></p>
<p>让我们回到<strong>SingleThreadEventExecutor</strong>的构造方法中继续往下执行，可以看到这一行：</p>
<pre><code class="language-java">this.executor = ThreadExecutorMap.apply(executor, this);
</code></pre>
<p>这里面是将上面创建好的<strong>ThreadPerTaskExecutor</strong>和<strong>NioEventLoop</strong>再包装一下，返回的是<strong>ThreadExecutorMap</strong>中的匿名内部对象<strong>executor</strong>，只是这里面使用<strong>ThreadPerTaskExecutor</strong>执行匿名内部类中的任务：</p>
<p><img src="https://img.dyzmj.top/img/202305311044556.png" alt="image-20230531104420323"></p>
<p>在匿名内部类中还有一个<strong>apply()<strong>方法，任务真正运行之前需要设置</strong>setCurrentEventExecutor</strong>中当前的事件执行器，也就是<strong>NioEventLoop</strong>，这里面使用的是Netty提供的<strong>FastThreadLocal</strong>，当前线程独占，任务执行完后就会置空，具体里面也比较复杂，暂不深入，后续会单独列一章进行说明：</p>
<pre><code class="language-java">public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) {
    ObjectUtil.checkNotNull(command, &quot;command&quot;);
    ObjectUtil.checkNotNull(eventExecutor, &quot;eventExecutor&quot;);
    return new Runnable() {
        @Override
        public void run() {
            setCurrentEventExecutor(eventExecutor);
            try {
                command.run();
            } finally {
                setCurrentEventExecutor(null);
            }
        }
    };
}
</code></pre>
<p>到现在我们知道了，</p>
<p>1、<strong>SingleThreadEventExecutor</strong>的构造函数设置了执行器 <strong>ThreadExecutorMap</strong>中内部<strong>executor</strong>，这里面是封装了<strong>ThreadPerTaskExecutor</strong>和<strong>NioEventLoop</strong>，设置了<strong>taskQueue</strong>任务队列和<strong>rejectedExecutionHandler</strong>拒绝处理器。</p>
<p>2、在<strong>SingleThreadEventLoop</strong>中设置了尾任务<strong>tailTasks****。</strong></p>
<p>3、最后回到<strong>NioEventLoop</strong>的构造函数中，设置<strong>provider</strong>选择器提供器，<strong>selectStrategy</strong>选择器策略，然后通过<strong>openSelector()<strong>获取选择器元组，这里面包含了未封装的</strong>unwrappedSelector</strong>原始的JDK的选择器和封装的<strong>SelectedSelectionKeySetSelector</strong>选择器。如果创建失败，则调用**shutdownGracefully()**进行优雅关闭。</p>
<p>完成NioEventLoop实例化后，我们重新回到MultithreadEventExecutorGroup的构造方法中继续往下走。</p>
<h3><strong>7、chooserFactory.newChooser(children)创建选择器</strong></h3>
<pre><code class="language-java">chooser = chooserFactory.newChooser(children);
</code></pre>
<p>这里其实就是怎么选择执行器，默认采用从头到尾轮询的方式：</p>
<pre><code class="language-java">@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
    // 判断执行器的长度是否为2的次幂
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTwoEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}
</code></pre>
<p>根据执行器的长度是否为2的次幂选择不同的计算方式。首先判断一个数是否是2的整数次幂：</p>
<p><strong>思路一</strong>：</p>
<p>1、首先把2的整数次幂换成二进制数，十进制数2转换为二进制数是10B，4转换为二进制数是100B...</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>10B</td>
<td>是</td>
</tr>
<tr>
<td>4</td>
<td>100B</td>
<td>是</td>
</tr>
<tr>
<td>8</td>
<td>1000B</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>10000B</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>100000B</td>
<td>是</td>
</tr>
<tr>
<td>100</td>
<td>1100100B</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>2、如果一个数是2的整数次幂，那么当其转换为二进制时，最高位为1，其余位都是0，如果把这些2的整数次幂各自减一，再转为二进制数</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>原值-1</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>10B</td>
<td>1B</td>
<td>是</td>
</tr>
<tr>
<td>4</td>
<td>100B</td>
<td>11B</td>
<td>是</td>
</tr>
<tr>
<td>8</td>
<td>1000B</td>
<td>111B</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>10000B</td>
<td>1111B</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>100000B</td>
<td>11111B</td>
<td>是</td>
</tr>
<tr>
<td>100</td>
<td>1100100B</td>
<td>1100011B</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>3、此时将2的整数次幂原值和其减一的结果进行按位与运算，即计算 <strong>n &amp; (n-1)</strong>  ，得到的结果为 <strong>0</strong>。</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>原值减一</th>
<th>n &amp; (n-1)</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>10B</td>
<td>1B</td>
<td>0</td>
<td>是</td>
</tr>
<tr>
<td>4</td>
<td>100B</td>
<td>11B</td>
<td>0</td>
<td>是</td>
</tr>
<tr>
<td>8</td>
<td>1000B</td>
<td>111B</td>
<td>0</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>10000B</td>
<td>1111B</td>
<td>0</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>100000B</td>
<td>11111B</td>
<td>0</td>
<td>是</td>
</tr>
<tr>
<td>100</td>
<td>1100100B</td>
<td>1100011B</td>
<td>1100000</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>**结论：**0和1按位运算的结果为0，因此所有的2的整数次幂和它自身减一的值进行按位与运算后，得到的结果都是0，反之，如果一个整数不是2的整数次幂时，得到的结果就不会是0。</p>
<pre><code class="language-java">public static boolean isPowerOfTwo(int val) {
    return (val &amp; val - 1) == 0;
}
</code></pre>
<p><strong>思路二</strong>：</p>
<p>在开始前，我们先熟悉下负数转二进制的一些操作，首先定义一个Integer类型的数字比如32768，由于Integer类型占用了32个bit位，因此转为二进制的完整形式为：</p>
<table>
<thead>
<tr>
<th>0000</th>
<th>0000</th>
<th>0000</th>
<th>0000</th>
<th>1000</th>
<th>0000</th>
<th>0000</th>
<th>0000</th>
</tr>
</thead>
</table>
<p>计算机中对有符号数字的表示有三种方法：原码、反码和补码，补码=反码+1，二进制数据中最高位表示符号位，最高位为0表示整数，最高位为1表示负数。</p>
<p>将-32768转化为二进制数据:</p>
<p>①首先将-32768的绝对值转换为二进制可得：0000 0000 0000 0000 1000 0000 0000 0000</p>
<table>
<thead>
<tr>
<th>0000</th>
<th>0000</th>
<th>0000</th>
<th>0000</th>
<th>1000</th>
<th>0000</th>
<th>0000</th>
<th>0000</th>
</tr>
</thead>
</table>
<p>②求该二进制的反码：</p>
<table>
<thead>
<tr>
<th>1111</th>
<th>1111</th>
<th>1111</th>
<th>1111</th>
<th>0111</th>
<th>1111</th>
<th>1111</th>
<th>1111</th>
</tr>
</thead>
</table>
<p>③最后将反码加1可得补码即为原值的二进制数据：</p>
<table>
<thead>
<tr>
<th>1111</th>
<th>1111</th>
<th>1111</th>
<th>1111</th>
<th>1000</th>
<th>0000</th>
<th>0000</th>
<th>0000</th>
</tr>
</thead>
</table>
<p>对应算法：</p>
<p>1、将2的整数次幂转换为二进制数据</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>8</td>
<td>00000000000000000000000000001000</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>00000000000000000000000000010000</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>00000000000000000000000000100000</td>
<td>是</td>
</tr>
<tr>
<td>64</td>
<td>00000000000000000000000001000000</td>
<td>是</td>
</tr>
<tr>
<td>128</td>
<td>00000000000000000000000010000000</td>
<td>是</td>
</tr>
<tr>
<td>257</td>
<td>00000000000000000000000100000001</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>2、取2的整数次幂的负数，并通过取反加一操作获取对应的二进制数</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>原值的负数</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>8</td>
<td>00000000000000000000000000001000</td>
<td>11111111111111111111111111111000</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>00000000000000000000000000010000</td>
<td>11111111111111111111111111110000</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>00000000000000000000000000100000</td>
<td>11111111111111111111111111100000</td>
<td>是</td>
</tr>
<tr>
<td>64</td>
<td>00000000000000000000000001000000</td>
<td>11111111111111111111111111000000</td>
<td>是</td>
</tr>
<tr>
<td>128</td>
<td>00000000000000000000000010000000</td>
<td>11111111111111111111111110000000</td>
<td>是</td>
</tr>
<tr>
<td>257</td>
<td>00000000000000000000000100000001</td>
<td>11111111111111111111111011111111</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>3、此时将2的整数次幂原值与2的整数次幂负值进行按位与运算，即计算 <strong>n &amp; (-n)</strong> ，得到的结果为 <strong>n</strong></p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
<th>原值的负数</th>
<th>n &amp; (-n)</th>
<th>是否为2的整数次幂</th>
</tr>
</thead>
<tbody>
<tr>
<td>8</td>
<td>00000000000000000000000000001000</td>
<td>11111111111111111111111111111000</td>
<td>00000000000000000000000000001000</td>
<td>是</td>
</tr>
<tr>
<td>16</td>
<td>00000000000000000000000000010000</td>
<td>11111111111111111111111111110000</td>
<td>00000000000000000000000000010000</td>
<td>是</td>
</tr>
<tr>
<td>32</td>
<td>00000000000000000000000000100000</td>
<td>11111111111111111111111111100000</td>
<td>00000000000000000000000000100000</td>
<td>是</td>
</tr>
<tr>
<td>64</td>
<td>00000000000000000000000001000000</td>
<td>11111111111111111111111111000000</td>
<td>00000000000000000000000001000000</td>
<td>是</td>
</tr>
<tr>
<td>128</td>
<td>00000000000000000000000010000000</td>
<td>11111111111111111111111110000000</td>
<td>00000000000000000000000010000000</td>
<td>是</td>
</tr>
<tr>
<td>257</td>
<td>00000000000000000000000100000001</td>
<td>11111111111111111111111011111111</td>
<td>00000000000000000000000000000001</td>
<td>否</td>
</tr>
</tbody>
</table>
<p><strong>结论：</strong></p>
<p>2的整数次幂和其对应的负数进行按位与运算后，得到的结果都是其本身。</p>
<pre><code class="language-java">private static boolean isPowerOfTwo(int val) {
    return (val &amp; -val) == val;
}
</code></pre>
<p>现在我们重新回到源码中可以看到，根据执行器的长度是否为2的整数次幂后选择不同的计算方式，一种是位运算，另一种就是常规取模的方式：</p>
<p><img src="https://img.dyzmj.top/img/202305311049127.png" alt="image-20230531104951021"></p>
<h3><strong>8、terminationListener设置终止监听器</strong></h3>
<p>创建完选择器后，在MultithreadEventExecutorGroup中设置终止监听器，就是如果要终止的时候，此处会有回调：</p>
<pre><code class="language-java">final FutureListener&lt;Object&gt; terminationListener = new FutureListener&lt;Object&gt;() {
    @Override
    public void operationComplete(Future&lt;Object&gt; future) throws Exception {
        if (terminatedChildren.incrementAndGet() == children.length) {
            terminationFuture.setSuccess(null);
        }
    }
};
for (EventExecutor e: children) {
    // 添加终止事件监听器
    e.terminationFuture().addListener(terminationListener);
}
</code></pre>
<h3><strong>9、Collections.unmodifiableSet(childrenSet)将执行器存入一个不可变集合</strong></h3>
<p>将执行器添加到LinkedHashSet中，然后再将存入一个不可变集合，此处我们暂时不知道为什么要这样存放，暂不深究继续往下执行。</p>
<pre><code class="language-java">Set&lt;EventExecutor&gt; childrenSet = new LinkedHashSet&lt;EventExecutor&gt;(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
</code></pre>
<p>至此<strong>NioEventLoopGroup</strong>基本上完成初始化，当然还很多细节，我们继续往下学习。</p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-6</guid>
      <pubDate>Mon, 27 Apr 2026 11:24:52 +0000</pubDate>
    </item>
    <item>
      <title>OpenResty(Nginx + Lua)实现流量定向分发</title>
      <link>https://dyzmj.top/posts/openresty</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/openresty">https://dyzmj.top/posts/openresty</a></p></blockquote><h1>OpenResty(Nginx + Lua)实现流量定向分发</h1>
<h2>一、概要</h2>
<blockquote>
<p>OpenResty® 是一个基于 <a href="http://openresty.org/cn/nginx.html">Nginx</a> 与 Lua 的高性能 Web 平台，其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。</p>
</blockquote>
<p>由Ngnix实现负载均衡的优势此处不再赘述，本篇主要结合实际的业务场景谈下流量的定向分发负载。基于NB-IoT技术的物联网燃气表设备通讯到基站及运营商IoT平台时，多采用COAP或UDP协议，通讯参数到达运营商IoT平台后由平台自动转为HTTP协议回调发出，由于后台的采集抄表系统使用的是TCP服务，因此在运营商IoT平台和采集抄表系统中间增加了一层HTTP转TCP的前置服务，如下图：</p>
<p><img src="https://img.dyzmj.top/img/202305310905361.png" alt="image-20230531090517623"></p>
<p>不过当设备数量不断增加时，HTTP转TCP前置服务就需要增加负载，而前置服务在与采集抄表系统进行通讯交互时，要建立一条Socket连接通道，一台设备在通讯周期内仅有一个连接，这就为负载问题增加了复杂度，多机负载时，设备进行指令交互就需要保证运营商IoT平台发出的HTTP请求每次都是发到同一台前置服务上。</p>
<p>运营商平台回调通讯时，请求参数中会附带NB设备的device_id，根据设备请求参数的不同，在Nginx中实现特定的负载均衡算法，从而达到定向的流量分发。</p>
<p><img src="https://img.dyzmj.top/img/202305310905799.png" alt="image-20230531090542541"></p>
<h2>二、环境搭建</h2>
<p><strong>OpenResty环境搭建</strong></p>
<p>服务器环境：CentOS 7.5   IP:10.200.6.171</p>
<p>1、安装基础依赖组件</p>
<pre><code class="language-bash">yum install -y libreadline-dev libpcre3-dev pcre-devel openssl-devel libssl-dev perl gcc
</code></pre>
<p>2、创建目录并下载安装包，完成后解压即可</p>
<pre><code class="language-bash">mkdir -p /usr/local/software
cd /usr/local/software
wget https://openresty.org/download/ngx_openresty-1.9.7.1.tar.gz
tar -zxvf ngx_openresty-1.9.7.1.tar.gz
</code></pre>
<p>3、进入解压目录后编译安装</p>
<pre><code class="language-bash">cd ngx_openresty-1.9.7.1
./configure
</code></pre>
<p><img src="https://img.dyzmj.top/img/202305310907753.png" alt="image-20230531090703689"></p>
<p>配置完成后执行完成安装</p>
<pre><code class="language-bash">gmake
gmake install
</code></pre>
<p>默认情况下程序会被安装到 /usr/local/openresty 目录，可以使用</p>
<pre><code class="language-bash">./configure --help
</code></pre>
<p>查看更多配置选项。</p>
<p>4、运行实例</p>
<p>安装成功后，进入OpenResty目录中可以看到有很多模块，Nginx和Lua都包含其中，进入nginx目录，启动nginx服务试试。</p>
<p><img src="https://img.dyzmj.top/img/202305310908832.png" alt="image-20230531090808521"></p>
<p><img src="https://img.dyzmj.top/img/202305310908466.png" alt="image-20230531090817698"></p>
<p><img src="https://img.dyzmj.top/img/202305310908440.png" alt="image-20230531090827661"></p>
<p>可以看到nginx服务正常运行，但这并不是我们想要达到的效果，nginx可以作为反向代理服务器拦截特定的请求，做负载均衡转发等，但对于特定接口的类似于http之类的接口的精细化配置，直接使用nginx的话，实际是有点麻烦的，也不推荐这种做法。而OpenResty正是基于这些需求集成了像Redis、MySQL、Cache等大量组件，同时无缝整合了nginx和lua开发环境，使得上述的需求场景实现起来更方便更简单。</p>
<h2>三、引入 Lua 脚本</h2>
<p>下面通过配置OpenResty来实现访问特定的路径，也就是基于nginx做进一步的拦截，实现nginx和lua的开发。</p>
<p>创建一个工作目录：</p>
<pre><code class="language-bash">mkdir -p /home/www
cd /home/www/
mkdir logs/ conf/
</code></pre>
<p>logs目录用于存放日志，conf用于存放配置文件。</p>
<p>然后在conf目录下创建 nginx.conf配置文件，配置内容如下(此处我们将html代码直接写入文件中)：</p>
<pre><code class="language-nginx">worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 9000;
        location /lua {
            default_type text/html;
            content_by_lua '
                ngx.say(&quot;&lt;p&gt;Hello, OpenResty!&lt;/p&gt;&quot;)
            ';
        }
    }
}
</code></pre>
<p>启动openresty</p>
<pre><code class="language-bash">cd /home/www
/usr/local/openresty/nginx/sbin/nginx -p `pwd`/ -c conf/nginx.conf
</code></pre>
<p>浏览器访问：http://10.200.6.171:3247/lua 页面展示正常</p>
<p><img src="https://img.dyzmj.top/img/202305310910174.png" alt="image-20230531091000876"></p>
<p>上述我们实现了一个简单的页面打印&quot;hello，OpenResty&quot;功能，下面我们使用OpenResty来实现nginx集群的负载均衡。</p>
<p>准备三台服务器，</p>
<p>10.200.6.171		cache1 - 部署OpenResty</p>
<p>10.200.6.170		cache2 - 部署nginx</p>
<p>10.20.11.72		cache3 - 部署nginx</p>
<p>将cache1作为流量或接口转发的节点，所有需要通过nginx作为代理的请求节点首先经过OpenResty进行分发，cache1统一将流量按照特定的负载均衡算法转到cache2和cache3服务器。</p>
<p><img src="https://img.dyzmj.top/img/202305310910656.png" alt="image-20230531091038382"></p>
<p>进入conf目录添加配置文件</p>
<pre><code class="language-bash">cd /home/www/conf
vim hello.conf
</code></pre>
<pre><code class="language-nginx">server {
    listen  8181;
    server_name _;

    location /lua {
        default_type 'text/html';
        content_by_lua_file /home/www/conf/hello.lua;
    }
}
</code></pre>
<p>修改nginx.conf文件，添加lua配置环境</p>
<pre><code class="language-bash">vim nginx.conf
</code></pre>
<pre><code class="language-nginx">worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    lua_package_path &quot;/usr/local/openresty/lualib/?.lua;;&quot;;
    lua_package_cpath &quot;/usr/local/openresty/lualib/?.so;;&quot;;
    include       hello.conf;
}
</code></pre>
<p>添加hello.lua脚本，实现负载均衡算法（此处演示简单的GET请求的负载方法）</p>
<pre><code class="language-bash">vim hello.lua
</code></pre>
<pre><code class="language-lua">-- 获取url地址，并取出GET请求的参数
local uri_args = ngx.req.get_uri_args()
local deviceId = uri_args[&quot;deviceId&quot;]
-- 配置负载的服务器地址
local host = {&quot;10.20.11.72:12580&quot;, &quot;10.200.6.170:12580&quot;}
-- 对参数进行取模计算
local hash = ngx.crc32_long(deviceId)
hash = (hash % 2) + 1
backend = &quot;http://&quot;..host[hash]

local requestBody = &quot;?deviceId=&quot;..deviceId
-- 将请求转发到指定的负载地址中
local http = require(&quot;resty.http&quot;)
local httpc = http.new()
local urlpath = backend..requestBody
local resp, err = httpc:request_uri(urlpath, {
    method = &quot;GET&quot;,
    keepalive = false
})

if not resp then
    ngx.say(&quot;request error :&quot;, err)
    return
end

ngx.say(resp.body)
  
httpc:close()
</code></pre>
<p>由于OpenResty默认包中没有&quot;resty.http&quot;依赖，我们需要手动加入</p>
<p>下载<a href="https://github.com/ledgetech/lua-resty-http">lua-resty-http</a> 依赖包，解压后将http.lua和http_header.lua复制到</p>
<pre><code class="language-bash">/usr/local/openresty/lualib/resty
</code></pre>
<p><img src="https://img.dyzmj.top/img/202305310912098.png" alt="image-20230531091237140"></p>
<p>运行 <code>nging -t</code> 检查下配置文件是否正确</p>
<pre><code class="language-bash">/usr/local/openresty/nginx/sbin/nginx -p `pwd`/ -t
</code></pre>
<p><img src="https://img.dyzmj.top/img/202305310913732.png" alt="image-20230531091320379"></p>
<p>运行nginx</p>
<pre><code class="language-bash">/usr/local/openresty/nginx/sbin/nginx -p `pwd`/ -c conf/nginx.conf
</code></pre>
<p>打开浏览器输入地址：</p>
<p><code>               http://10.200.6.171:8181/lua?deviceId=2233             </code></p>
<p><img src="https://img.dyzmj.top/img/202305310914631.png" alt="image-20230531091411899"></p>
<p>修改地址后的参数:</p>
<p><code>               http://10.200.6.171:8181/lua?deviceId=2234             </code></p>
<p><img src="https://img.dyzmj.top/img/202305310914531.png" alt="image-20230531091439693"></p>
<p>可以看到根据deviceId参数的不同，经负载均衡算法后请求被转发到指定的节点。</p>
<h2>四、一致性哈希算法</h2>
<p>当然上述的负载算法仅仅只是计算RCR校验和并进行取模，直接使用取模计算效率较低，实际使用不推荐这种方式，这里建议可以使用nginx实现的一致性哈希算法，原理可参考<a href="https://www.zsythink.net/archives/1182">白话解析:一致性哈希算法</a></p>
<p>下载依赖包 <a href="https://github.com/replay/ngx_http_consistent_hash">ngx_http_consistent_hash-master.zip</a></p>
<p>将压缩包上传至 /usr/local/software 目录后解压</p>
<pre><code class="language-bash">unzip ngx_http_consistent_hash-master.zip
</code></pre>
<p>完成后进入之前的openresty的解压目录后，重新配置新模块</p>
<pre><code class="language-bash">cd ngx_openresty-1.9.7.1
./configure --add-module=../ngx_http_consistent_hash-master
</code></pre>
<p><img src="https://img.dyzmj.top/img/202305310915648.png" alt="image-20230531091533714"></p>
<p>完成后使用 <code>gmake</code> 进行编译</p>
<p><img src="https://img.dyzmj.top/img/202305310916304.png" alt="image-20230531091604576"></p>
<p>复制编译好的nginx包到openresty下的nginx目录中，先将原来的可执行文件备份</p>
<pre><code class="language-bash">cd /usr/local/openresty/nginx/sbin
cp nginx nginx.old
</code></pre>
<p>复制编译好的nginx可执行文件</p>
<pre><code class="language-bash">cd /usr/local/software/ngx_openresty-1.9.7.1/build/nginx-1.9.7/objs
cp -r nginx /usr/local/openresty/nginx/sbin/
</code></pre>
<p>完成后我们开始修改配置文件(此处演示POST请求的负载方案，负载机部署的为实际使用程序)</p>
<pre><code class="language-bash">cd /home/www/conf
vim hello.conf
</code></pre>
<pre><code class="language-nginx">server {
    listen  8181;
    server_name _;

    location /HWIoT {
        set $hashkey &quot;&quot;;
        set $backendupstream &quot;hashbackend&quot;;
        rewrite_by_lua_file '/home/www/conf/hello.lua';
        proxy_pass http://$backendupstream;
    }

}

upstream hashbackend {
    consistent_hash $hashkey;
    server 10.200.6.170:30235;
    server 10.20.11.72:30238;
}

</code></pre>
<p>修改hello.lua脚本</p>
<pre><code class="language-bash">vim hello.lua
</code></pre>
<pre><code class="language-lua">-- 入参：$hashkey, $backendupstream
-- 如果是一致性hash，会set $hashkey
-- $backendupstream 表示将会采用的upstream
--
-- 获取POST请求的参数
ngx.req.read_body()
local param = ngx.req.get_body_data()

-- 调用cjson解析库取出参数对象中的deviceId参数
local cjson = require(&quot;cjson&quot;)
local json = cjson.decode(param)
local deviceId = json[&quot;deviceId&quot;]

-- 对nginx中的计算HASH值的参数进行赋值
ngx.var.hashkey = deviceId
ngx.var.backendupstream = &quot;hashbackend&quot;
ngx.log(ngx.INFO, &quot;backendupstream=&quot;, ngx.var.backendupstream, &quot;, key=&quot;, deviceId)
</code></pre>
<p>修改后检测一下</p>
<pre><code class="language-bash">cd /home/www
/usr/local/openresty/nginx/sbin/nginx -p `pwd`/ -t
</code></pre>
<p>完成后启动，在postman中使用不同的deviceId参数，查看后台日志也被分发到不同的服务器端中。</p>
<pre><code class="language-bash">cd /home/www
/usr/local/openresty/nginx/sbin/nginx -p `pwd`/ -c conf/nginx.conf
</code></pre>
<p><img src="https://img.dyzmj.top/img/202305310918464.png" alt="image-20230531091812857"></p>
<p>本篇OpenResty的定向负载方案就介绍到这儿，OpenResty的强大之处远不至此，需要大家自己去探索了，附录为OpenResty的一些使用案例。</p>
<h2>五、附录</h2>
<p><a href="https://zhuanlan.zhihu.com/p/83209234">OpenResty实战应用</a></p>
<p><a href="https://my.oschina.net/u/4084220/blog/3147997?_from=gitee_rec">OpenResty在马蜂窝广告监测中的应用</a></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-5</guid>
      <pubDate>Mon, 27 Apr 2026 11:23:53 +0000</pubDate>
    </item>
    <item>
      <title>Thread源码分析</title>
      <link>https://dyzmj.top/posts/thread</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/thread">https://dyzmj.top/posts/thread</a></p></blockquote><h1>Thread</h1>
<h2>Thread 类</h2>
<h4>Thread 类的定义</h4>
<pre><code class="language-java">public class Thread implements Runnable {}
</code></pre>
<h4>加载本地资源</h4>
<pre><code class="language-java">private static native void registerNatives();
    static {
        registerNatives();
    }
</code></pre>
<h4>Thread 类中的成员变量</h4>
<pre><code class="language-java">// 当前线程的名称
private volatile String name;
// 线程的优先级
private int priority;

// 当前线程是否是守护线程
private boolean daemon = false;

// JVM独占使用的字段
private boolean stillborn = false;
private long eetop;

// 实际执行的任务
private Runnable target;

// 当前线程所在的线程组
private ThreadGroup group;

// 当前线程的类加载器
private ClassLoader contextClassLoader;

// 当前线程继承的访问控制上下文
private AccessControlContext inheritedAccessControlContext;

// 为匿名线程生成的自动编号
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

// 与当前线程相关的ThreadLocal，这个Map维护的是ThreadLocal类
ThreadLocal.ThreadLocalMap threadLocals = null;

// 与当前线程相关的ThreadLocal，
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

// 当前线程请求的堆栈大小，若未指定，则默认为0，并由JVM来控制
private final long stackSize;

// 当前线程终止后存在的JVM私有状态
private long nativeParkEventPointer;

// 线程ID
private final long tid;

// 用于生成线程ID的参数
private static long threadSeqNumber;

// 线程状态，初始为未启动状态
private volatile int threadStatus;

// 提供给当前调用java.util.concurrent.locks.LockSupport.park的参数
volatile Object parkBlocker;

// Interruptible接口中定义了interrupt方法，用来中断指定的线程
private volatile Interruptible blocker;
// 当前线程的内部锁
private final Object blockerLock = new Object();

// 线程的最小优先级
public static final int MIN_PRIORITY = 1;
// 线程默认优先级
public static final int NORM_PRIORITY = 5;
// 线程最大优先级
public static final int MAX_PRIORITY = 10;
</code></pre>
<h4>线程的状态定义</h4>
<pre><code class="language-java">public enum State {
        // 未启动状态
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * &lt;ul&gt;
         *   &lt;li&gt;{@link Object#wait() Object.wait} with no timeout&lt;/li&gt;
         *   &lt;li&gt;{@link #join() Thread.join} with no timeout&lt;/li&gt;
         *   &lt;li&gt;{@link LockSupport#park() LockSupport.park}&lt;/li&gt;
         * &lt;/ul&gt;
         *
         * &lt;p&gt;A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called {@code Object.wait()}
         * on an object is waiting for another thread to call
         * {@code Object.notify()} or {@code Object.notifyAll()} on
         * that object. A thread that has called {@code Thread.join()}
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * &lt;ul&gt;
         *   &lt;li&gt;{@link #sleep Thread.sleep}&lt;/li&gt;
         *   &lt;li&gt;{@link Object#wait(long) Object.wait} with timeout&lt;/li&gt;
         *   &lt;li&gt;{@link #join(long) Thread.join} with timeout&lt;/li&gt;
         *   &lt;li&gt;{@link LockSupport#parkNanos LockSupport.parkNanos}&lt;/li&gt;
         *   &lt;li&gt;{@link LockSupport#parkUntil LockSupport.parkUntil}&lt;/li&gt;
         * &lt;/ul&gt;
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
</code></pre>]]></description>
      <author>夂夂鱼</author>
      <guid>article-4</guid>
      <pubDate>Mon, 27 Apr 2026 11:22:51 +0000</pubDate>
    </item>
    <item>
      <title>ThreadLocal源码分析</title>
      <link>https://dyzmj.top/posts/thread_local</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/thread_local">https://dyzmj.top/posts/thread_local</a></p></blockquote><h2><strong>1、ThreadLocal用在什么地方？</strong></h2>
<p>讨论ThreadLocal用在什么地方前，我们先明确下，如果仅仅就一个线程，那么都不用谈ThreadLocal的，ThreadLocal是用在多线程的场景中的！</p>
<p>ThreadLocal归纳下来就2类用途：</p>
<ul>
<li><strong>保存线程上下文信息，在任意需要的地方可以获取</strong></li>
<li><strong>线程安全的，避免某些情况需要考虑线程安全必须同步带来的性能损失</strong></li>
</ul>
<p>①</p>
<p>由于ThreadLocal的特性，同一线程在某些地方进行设置，在随后的任意地方都可以获取到，从而可以用来保存线程上下文信息。</p>
<p>常用的比如每个请求怎么把一串后续关联起来，就可以用ThreadLocal进行set，在后续的任意需要记录日志的方法里面进行get获取到请求ID，从而把整个请求串起来。</p>
<p>还有比如Spring的事务管理，用ThreadLocal存储Connection，从而各个DAO可以获取同一个Connection，可以进行事务回滚、提交等操作。</p>
<p>②</p>
<p>ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但ThreadLocal也有局限性，来看看阿里规范：</p>
<p><img src="https://img.dyzmj.top/img/202305311017175.png" alt="image-20230531101659871"></p>
<p>每个线程往ThreadLocal中读写数据是线程隔离的，互相之间不会影响的，所以ThreadLocal无法解决共享对象的更新问题。</p>
<blockquote>
<p>由于不需要共享信息，自然不存在竞争问题了，从而保证的某些情况下的线程安全，以及避免了某些情况下需要考虑线程安全必须同步带来的性能损失！</p>
</blockquote>
<p>这类场景阿里规范里面也提到过了：</p>
<p><img src="https://img.dyzmj.top/img/202305311017628.png" alt="image-20230531101725037"></p>
<h2><strong>2、ThreadLocal一些细节！</strong></h2>
<p>ThreadLocal使用示例代码：</p>
<pre><code class="language-java">package com.goldcard;

/**
 * @author 3247
 * @date 2023/4/2
 */
public class ThreadLocalTest {

    private static ThreadLocal&lt;Integer&gt; threadLocal = new ThreadLocal&lt;&gt;();

  public static void main(String[] args) {

      new Thread(()-&gt;{
          try{
              for(int i = 0; i &lt; 100; i++) {
                  threadLocal.set(i);
                  System.out.println(Thread.currentThread().getName()+&quot;====&quot;+threadLocal.get());
                  try{
                      Thread.sleep(200);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
              }
          }finally{
              threadLocal.remove();
          }
      },&quot;ThreadLocal-1&quot;).start();

      new Thread(()-&gt;{
          try{
              for(int i = 0; i &lt; 100; i++) {
                  System.out.println(Thread.currentThread().getName()+&quot;====&quot;+threadLocal.get());
                  try{
                      Thread.sleep(200);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
              }
          }finally{
              threadLocal.remove();
          }
      },&quot;ThreadLocal-2&quot;).start();

  }
}
</code></pre>
<p>代码运行结果：</p>
<p><img src="https://img.dyzmj.top/img/202305311018619.png" alt="image-20230531101820386"></p>
<p>从运行结果可以看到ThreadLocal-1进行set值对ThreadLocal-2并没有任何影响！</p>
<p>Thread、ThreadLocalMap、ThreadLocal总览图：</p>
<p><img src="https://img.dyzmj.top/img/202305311018851.png" alt="image-20230531101841198"></p>
<p><img src="https://img.dyzmj.top/img/202305311018310.png" alt="image-20230531101852283"></p>
<p>Thread类中有属性变量threadLocals（类型是ThreadL.ThreadLocalMap），也就是说每个线程有一个自己的ThreadLocalMap，所以每个线程往这个ThreadLocal中读写是隔离的，并且是相互不会影响的。</p>
<p><strong>一个ThreadLocal只能存储一个Object对象，如果需要存储多个Object对象那么就需要多个ThreadLocal！</strong></p>
<p>如图：</p>
<p><img src="https://img.dyzmj.top/img/202305311019167.png" alt="image-20230531101914093"></p>
<p>看到上面几个图，大概思路应该都清晰了，我们Entry的key指向ThreadLocal用虚线表示弱引用，下面来看看ThreadLocalMap：</p>
<p><img src="https://img.dyzmj.top/img/202305311019475.png" alt="image-20230531101929085"></p>
<p>Java对象的引用包括：强引用、软引用、弱引用、虚引用。</p>
<p>因为这里涉及到弱引用，简单说明下：</p>
<p>弱引用也是用来描述非必须对象的，当JVM进行垃圾回收时，无论内存是否充足，<strong>该对象仅仅被弱引用关联，那么就会被回收。</strong></p>
<p><strong>当仅仅只有ThreadLocalMap中的Entry的key指向ThreadLocal的时候，ThreadLocal会进行回收的！</strong></p>
<p>ThreadLocal被垃圾回收后，在ThreadLocalMap里对应的Entry的键值会变成null，但是Entry是强引用，那么Entry里面存储的Object就没有办法进行回收，所以ThreadLocalMap做了一些额外的回收工作。</p>
<p><img src="https://img.dyzmj.top/img/202305311019115.png" alt="image-20230531101946298"></p>
<p>虽然做了一些回收工作，但仍会存在内存泄漏的风险。</p>
<h2><strong>3、ThreadLocal最佳实践！</strong></h2>
<blockquote>
<p>很多时候，我们都是用在线程池的场景，程序不停止，线程基本不会销毁！</p>
</blockquote>
<p>由于线程的生命周期长，如果我们往ThreadLocal里面set里很大很大的Object对象，虽然set、get等等方法在特定的条件会调用进行额外的清理，但是<strong>ThreadLocal被垃圾回收后，在ThreadLocalMap里对应的Entry的键值会变成null，但是后续再也没有操作set、get等方法了。</strong></p>
<p><strong>所以最佳实践，应该在我们不使用的时候，主动调用remove方法进行清理。</strong></p>
<p><img src="https://img.dyzmj.top/img/202305311020585.png" alt="image-20230531102025893"></p>
<p>这里把ThreadLocal定义为static还有一个好处就是，用于ThreadLocal有强引用在，那么在ThreadLocalMap里对应的Entry的键会永远存在，那么执行remove的时候就可以正确进行定位并且删除。</p>
<p>阿里规约中也给出了具体的做法：</p>
<p><img src="https://img.dyzmj.top/img/202305311020920.png" alt="image-20230531102051277"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-3</guid>
      <pubDate>Mon, 27 Apr 2026 11:22:11 +0000</pubDate>
    </item>
    <item>
      <title>冬季天干物燥，如何实时采集办公室温湿度数据？</title>
      <link>https://dyzmj.top/posts/iot_01</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/iot_01">https://dyzmj.top/posts/iot_01</a></p></blockquote><h1>冬季天干物燥，如何实时采集办公室温湿度数据？</h1>
<h1>--树莓派+阿里云IoT平台实现</h1>
<h2>一、树莓派简介</h2>
<h3>树莓派是什么？</h3>
<p><code>树莓派</code>不是一款餐后甜点，而是一个只有信用卡大小的计算机，更准确的说它是一款单板计算机。树莓派由注册于英国的慈善组织 “Raspberry Pi 基金会” 开发和维护，其设计初衷是用来教孩子们学习程序设计的低成本计算机，而现在它已经可以用来做很多有趣的事情。</p>
<!-- raw HTML omitted -->
<p>上图中列出了树莓派提供的一些接口，比如支持 USB 供电接口、HDMI 连接显示器的接口、摄像头接口、音频输出接口、以太网接口、USB2.0 接口等常用接口，通过这些接口使得树莓派可以无限扩展，甚至可以作为一个完整的 PC 机使用。不过这里我们要说一下上图中树莓派自带的40个<code>GPIO</code>引脚接口。</p>
<h3>GPIO针脚</h3>
<p>所谓<code>GPIO</code> (General Purpose I/O Ports ，意思为通用输入/输出端口），通俗地说就是一些引脚，可以通过它们输入高（低）电平或者通过它们读取引脚的状态（高电平或低电平）。用户可以通过<code>GPIO</code>接口和硬件进行数据交互（如UART），控制硬件工作（如控制LED灯、蜂鸣器等），读取硬件的工作状态信号（如中断信号）等，GPIO的使用非常广泛，掌握了GPIO，差不多相当掌握了操作硬件的能力。(引脚与实际树莓派对照方式：将4个USB接口正对使用者，40个引脚即为下图所示)</p>
<!-- raw HTML omitted -->
<p><strong>针脚编码方式：</strong></p>
<p>目前有三种方式对树莓派的引脚进行编码：</p>
<p>① 使用 <code>BOARD编号系统</code>，这种方式参考树莓派主板上接线柱的针脚编号。使用这种方式的优点是无需考虑主板的修订版本，引脚数量增减不影响同编号引脚的功能，编号相同的引脚功能相同。</p>
<p>② 使用 <code>BCM编码</code>，该方式参考Broadcom SOC 的通道编号，编号中也就省去了不能用程序控制的 VCC 和 GND 引脚。使用过程中要保持主板上的针脚与图表上标注的通道编号相对应。</p>
<p>③ 使用 <code>WiringPi编码</code>，它是一个用 C 语言编写的树莓派软件包，功能强大，可用于树莓派GPIO引脚控制、串口通信、SPI 通信及 I2C 通信等功能（不过作者大佬由于其开源版权无法得到尊重以及使用者无数次无效的提问邮件烦扰，其已经宣布软件包不再更新：<a href="http://wiringpi.com/wiringpi-deprecated/">wiringPi – deprecated…</a>）</p>
<h2>二、温湿度传感器</h2>
<h3>温湿度传感器DHT11简介</h3>
<p><code>数字温湿度传感器 DHT11</code> 是一种复合传感器，包含温度和湿度的标准数字信号输出，采用专用的数字模块采集技术和温湿度传感技术，确保产品具有高度可靠性和优异的长期稳定性。</p>
<!-- raw HTML omitted -->
<p>该传感器包括一个电阻式感湿元件和一个NTC测温元件，并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高(价格便宜，某宝几块钱一个)等优点。其精度湿度±5%RH， 温度±2℃，量程湿度5<del>95%RH， 温度-20</del>+60℃。</p>
<h3>DHT11的数据格式</h3>
<!-- raw HTML omitted -->
<p><code>DATA</code> 用于树莓派与 <code>DHT11</code> 之间的通讯和同步，采用单总线数据格式，一次通讯时间 4ms 左右，数据分为小数部分、整数部分及校验位部分。一次传输5个字节共40位数据，高位先出。</p>
<blockquote>
<p>数据格式：</p>
<p>8bit 湿度整数数据 + 8bit 湿度小时数据 + 8bit 温度整数数据 + 8bit 温度小数数据 + 8bit 校验位</p>
<p>校验位数据定义：</p>
<p>8bit 校验位 等于 8bit 湿度整数数据 + 8bit 湿度小时数据 + 8bit 温度整数数据 + 8bit 温度小数数据 所得结果的末8位</p>
</blockquote>
<!-- raw HTML omitted -->
<p>详细工作原理可参考附录1。</p>
<h2>三、获取温湿度数据</h2>
<h3>线路连接</h3>
<table>
<thead>
<tr>
<th>树莓派</th>
<th>温湿度传感器</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPIO.7</td>
<td>DATA</td>
</tr>
<tr>
<td>5V</td>
<td>VCC</td>
</tr>
<tr>
<td>GND</td>
<td>GND</td>
</tr>
</tbody>
</table>
<p>温湿度传感器最左边为DATA接口、中间为VCC接口，最右边为GND接口。</p>
<p>树莓派GPIO接口对应DATA我们取GPIO.7，也就是BOARD编码7号针脚；电源取4号针脚5V接口；GND取6号针脚。</p>
<p><img src="https://img.dyzmj.top/img/%E6%95%B0%E6%8D%AE%E7%BA%BF%E8%BF%9E%E6%8E%A5.jpg" alt="数据线连接"></p>
<h3>控制程序</h3>
<p>编写控制程序，将温湿度传感器中的二进制数据转化为十进制数据，校验后将温湿度数据打印出来。</p>
<pre><code class="language-python">#!/usr/bin/env python
import RPi.GPIO as GPIO
import numpy as np
import time
 
DHTPIN = 4         #引脚号4
GPIO.setmode(GPIO.BCM)      #以BCM编码格式

def read_dht11_dat():
    GPIO.setup(DHTPIN, GPIO.OUT)
    GPIO.output(DHTPIN, GPIO.LOW)
    #给信号提示传感器开始工作,并保持低电平18ms以上
    time.sleep(0.02)                #这里保持20ms   
    GPIO.output(DHTPIN, GPIO.HIGH)  #然后输出高电平
    
    GPIO.setup(DHTPIN, GPIO.IN)    
    # 发送完开始信号后得把输出模式换成输入模式，不然信号线上电平始终被拉高
 
    while GPIO.input(DHTPIN) == GPIO.LOW:
        continue
    #DHT11发出应答信号，输出 80 微秒的低电平
    
    while GPIO.input(DHTPIN) == GPIO.HIGH:
        continue
    #紧接着输出 80 微秒的高电平通知外设准备接收数据
    
    #开始接收数据
    j = 0               #计数器
    data = []           #收到的二进制数据
    kk=[]               #存放每次高电平结束后的k值的列表
    while j &lt; 40:
        k = 0
        while GPIO.input(DHTPIN) == GPIO.LOW:  # 先是 50 微秒的低电平
            continue
        
        while GPIO.input(DHTPIN) == GPIO.HIGH: # 接着是26-28微秒的高电平，或者 70 微秒的高电平
            k += 1
            if k &gt; 100:
                break
        kk.append(k)
        if k &lt; 8:       #26-28 微秒时高电平时通常k等于5或6
            data.append(0)      #在数据列表后面添加一位新的二进制数据“0”
        else:           #70 微秒时高电平时通常k等于17或18
            data.append(1)      #在数据列表后面添加一位新的二进制数据“1”
 
        j += 1
 
    print(&quot;sensor is working.&quot;)
    print ('初始数据高低电平:\n',data)    #输出初始数据高低电平
    print ('参数k的列表内容：\n',kk)      #输出高电平结束后的k值
    
    m = np.logspace(7,0,8,base=2,dtype=int) #logspace()函数用于创建一个于等比数列的数组
    #即[128 64 32 16 8 4 2 1]，8位二进制数各位的权值
    data_array = np.array(data) #将data列表转换为数组

    #dot()函数对于两个一维的数组，计算的是这两个数组对应下标元素的乘积和(数学上称之为内积)
    humidity = m.dot(data_array[0:8])           #用前8位二进制数据计算湿度的十进制值
    humidity_point = m.dot(data_array[8:16])
    temperature = m.dot(data_array[16:24])
    temperature_point = m.dot(data_array[24:32])
    check = m.dot(data_array[32:40])
    
    print (humidity,humidity_point,temperature,temperature_point,check)
    
    tmp = humidity + humidity_point + temperature + temperature_point
    #十进制的数据相加
 
    if check == tmp:    #数据校验，相等则输出
        return humidity, temperature
    else:               #错误输出错误信息
        return False
 
def main():
    print (&quot;Raspberry Pi DHT11 Temperature test program\n&quot;)
    time.sleep(1)           #通电后前一秒状态不稳定，时延一秒
    while True:
        result = read_dht11_dat()
        if result:
            humidity, temperature = result
            print (&quot;humidity: %s %%,  Temperature: %s  ℃&quot; % \
                  (humidity, temperature))
            print ('\n') 
            time.sleep(1)

        if result == False:
            print (&quot;Data are wrong,skip\n&quot;)
            time.sleep(1)
            
def destroy():
    GPIO.cleanup()

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        destroy() 
        
     
</code></pre>
<p><code>vsCode</code> 中安装 <code>Remote-SSH</code> 插件，可以直接在 vsCode 中远程连接树莓派，方便进行代码编写和运行。</p>
<!-- raw HTML omitted -->
<p>可以看到，已经可以正常读取温湿度数据。</p>
<h2>四、阿里云IoT平台</h2>
<h3>设备接入</h3>
<h4>1、准备工作</h4>
<h5>1.1 注册阿里云账号</h5>
<p>使用支付宝或手机号开通阿里云账号，并完成实名认证。</p>
<h5>1.2 开通物联网平台服务</h5>
<p>打开产品与服务，找到物联网平台并进入，然后点击【立即开通】即可。</p>
<!-- raw HTML omitted -->
<p>开通物联网平台，进入管理控制台可以看到系统提供了一个免费的公共实例，等待公共实例完成开通（大概需要1~2分钟）</p>
<p><img src="https://img.dyzmj.top/img/image-20211109103028373.png" alt="image-20211109103028373"></p>
<h4>2、产品配置</h4>
<h5>2.1 创建产品</h5>
<!-- raw HTML omitted -->
<h5>2.2 功能定义，为产品物模型添加自定义功能属性</h5>
<table>
<thead>
<tr>
<th>属性名</th>
<th>标识符</th>
<th>数据类型</th>
<th>取值范围</th>
</tr>
</thead>
<tbody>
<tr>
<td>温度</td>
<td>temperature</td>
<td>float (单精度浮点型)</td>
<td>-50 ~ 100</td>
</tr>
<tr>
<td>湿度</td>
<td>humidity</td>
<td>float (单精度浮点型)</td>
<td>0 ~ 100</td>
</tr>
<tr>
<td>当前时间</td>
<td>currentTime</td>
<td>text (字符串)</td>
<td>数据长度：40</td>
</tr>
</tbody>
</table>
<p><img src="https://img.dyzmj.top/img/image-20211109104327962.png" alt="image-20211109104327962"></p>
<p>注意：物模型需要点击发布上线后才能生效使用。</p>
<p>打开物<code>模型通讯Topic</code>，可以看到<code>属性上报</code> 功能，后续传感器上报数据即是通过此 topic 上传。</p>
<p><img src="https://img.dyzmj.top/img/image-20211109105115442.png" alt="image-20211109105115442"></p>
<h5>2.3 设备注册</h5>
<p>新建一个设备，打开设备信息获取身份三元组数据，用于后续传感器上报数据的验证。</p>
<p><img src="https://img.dyzmj.top/img/image-20211109104944893.png" alt="image-20211109104944893"></p>
<h4>3、程序开发</h4>
<h5>3.1 创建文件夹并安装依赖包</h5>
<pre><code class="language-bash">mkdir sensor
cd sendor
pip install paho-mqtt
</code></pre>
<h5>3.2 创建程序文件，添加内容</h5>
<pre><code class="language-bash">touch thermometer.py
</code></pre>
<p>程序代码（<strong>注意修改里面的参数</strong>）：</p>
<pre><code class="language-python"># -*- coding: utf-8 -*-
import paho.mqtt.client as mqtt
import time
import hashlib
import hmac
import random
import json

options = {
    'productKey':'你的productKey',
    'deviceName':'你的deviceName',
    'deviceSecret':'你的deviceSecret',
    'regionId':'cn-shanghai'
}

HOST = options['productKey'] + '.iot-as-mqtt.'+options['regionId']+'.aliyuncs.com'
PORT = 1883 
PUB_TOPIC = &quot;/sys/&quot; + options['productKey'] + &quot;/&quot; + options['deviceName'] + &quot;/thing/event/property/post&quot;;


# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    print(&quot;Connected with result code &quot;+str(rc))
    # client.subscribe(&quot;the/topic&quot;)

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    print(msg.topic+&quot; &quot;+str(msg.payload))

def hmacsha1(key, msg):
    return hmac.new(key.encode(), msg.encode(), hashlib.sha1).hexdigest()

def getAliyunIoTClient():
	timestamp = str(int(time.time()))
	CLIENT_ID = &quot;paho.py|securemode=3,signmethod=hmacsha1,timestamp=&quot;+timestamp+&quot;|&quot;
	CONTENT_STR_FORMAT = &quot;clientIdpaho.pydeviceName&quot;+options['deviceName']+&quot;productKey&quot;+options['productKey']+&quot;timestamp&quot;+timestamp
	# set username/password.
	USER_NAME = options['deviceName']+&quot;&amp;&quot;+options['productKey']
	PWD = hmacsha1(options['deviceSecret'],CONTENT_STR_FORMAT)
	client = mqtt.Client(client_id=CLIENT_ID, clean_session=False)
	client.username_pw_set(USER_NAME, PWD)
	return client

if __name__ == '__main__':

	client = getAliyunIoTClient()
	client.on_connect = on_connect
	client.on_message = on_message
	
	client.connect(HOST, 1883, 300)
    
	payload_json = {
		'id': int(time.time()),
		'params': {
			'temperature': random.randint(20, 30),
            'humidity': random.randint(40, 50)
		},
	    'method': &quot;thing.event.property.post&quot;
	}
	print('send data to iot server: ' + str(payload_json))

	client.publish(PUB_TOPIC,payload=str(payload_json),qos=1)
	client.loop_forever()
</code></pre>
<p>在控制台中执行下程序，可以看到</p>
<p><img src="https://img.dyzmj.top/img/image-20211109113203560.png" alt="image-20211109113203560"></p>
<p>此时打开阿里云物联网平台，选择设备可以看到设备已经被激活，物模型上已经有刚才上传的数据记录。</p>
<p><img src="https://img.dyzmj.top/img/image-20211109154101842.png" alt="image-20211109154101842"></p>
<h3>获取传感器数据</h3>
<p>当然上述例子中生成的只是一些随机数且只能运行一次，下面需要对程序改动一下使其能够获取树莓派温湿度传感器数据。</p>
<p>创建读取传感器数据的文件：</p>
<pre><code class="language-bash">touch  dht_sensor.py	
</code></pre>
<p>在文件中填充以下内容：</p>
<pre><code class="language-python">#!/usr/bin/python3
#
import RPi.GPIO as GPIO
import time

tmp=[]      # 用来存放读取到的数据

data = 4   # DHT11的data引脚连接到的树莓派的GPIO引脚，使用BCM编号
a,b=0,0

def __delayMicrosecond(t):    # 微秒级延时函数
    start,end=0,0           # 声明变量
    start=time.time()       # 记录开始时间
    t=(t-3)/1000000     # 将输入t的单位转换为秒，-3是时间补偿
    while end-start&lt;t:  # 循环至时间差值大于或等于设定值时
        end=time.time()     # 记录结束时间

def __set_gpio_and_read():
    GPIO.setup(data, GPIO.OUT)  # 设置GPIO口为输出模式
    GPIO.output(data,GPIO.HIGH) # 设置GPIO输出高电平
    __delayMicrosecond(10*1000)   # 延时10毫秒
    GPIO.output(data,GPIO.LOW)  # 设置GPIO输出低电平
    __delayMicrosecond(25*1000)   # 延时25毫秒      
    GPIO.output(data,GPIO.HIGH) # 设置GPIO输出高电平
    GPIO.setup(data, GPIO.IN)   # 设置GPIO口为输入模式

    a=time.time()           # 记录循环开始时间
    while GPIO.input(data): # 一直循环至输入为低电平
        b=time.time()       # 记录结束时间
        if (b-a)&gt;0.1:       # 判断循环时间是否超过0.1秒，避免程序进入死循环卡死
            break           # 跳出循环
        
    a=time.time()
    while GPIO.input(data)==0:  # 一直循环至输入为高电平
        b=time.time()
        if (b-a)&gt;0.1:
            break
                
    a=time.time()
    while GPIO.input(data): # 一直循环至输入为低电平
        b=time.time()
        if (b-a)&gt;=0.1:
            break   
            
    for i in range(40):         # 循环40次，接收温湿度数据
        a=time.time()
        while GPIO.input(data)==0:  #一直循环至输入为高电平
            b=time.time()
            if (b-a)&gt;0.1:
                break

        __delayMicrosecond(28)    # 延时28微秒
            
        if GPIO.input(data):    # 超过28微秒后判断是否还处于高电平
            tmp.append(1)       # 记录接收到的bit为1
                
            a=time.time()
            while GPIO.input(data): # 一直循环至输入为低电平
                b=time.time()
                if (b-a)&gt;0.1:
                    break
        else:
            tmp.append(0)       # 记录接收到的bit为0
            
def get_sensor():
    GPIO.setmode(GPIO.BCM)      # 设置为BCM编号模式
    GPIO.setwarnings(False)
    del tmp[0:]                 # 删除列表
    time.sleep(1)               # 延时1秒
    
    # 配置GPIO信息及读取数据
    __set_gpio_and_read()
  
    humidity_bit=tmp[0:8]       # 分隔列表，第0到7位是湿度整数数据
    humidity_point_bit=tmp[8:16]# 湿度小数
    temperature_bit=tmp[16:24]  # 温度整数
    temperature_point_bit=tmp[24:32]    # 温度小数
    check_bit=tmp[32:40]        # 校验数据
 
    humidity_int=0
    humidity_point=0
    temperature_int=0
    temperature_point=0
    check=0
#  
    for i in range(8):          # 二进制转换为十进制( 2 ** n  表示2的n次方)
        humidity_int+=humidity_bit[i] * 2 ** (7-i)
        humidity_point+=humidity_point_bit[i] * 2 ** (7-i)
        temperature_int+=temperature_bit[i] * 2 ** (7-i)
        temperature_point+=temperature_point_bit[i] * 2 ** (7-i)
        check+=check_bit[i] * 2 ** (7-i)
  
    humidity=humidity_int+humidity_point/10
    temperature=temperature_int+temperature_point/10 - 7
    currentTime = time.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;, time.localtime())
  
    check_tmp=humidity_int+humidity_point+temperature_int+temperature_point
  
    if check==check_tmp and temperature!=0 and temperature!=0:  # 判断数据是否正常
        print(&quot;温度: &quot;, temperature, &quot;℃\n湿度: &quot;, humidity, &quot;%\n当前时间: &quot;, currentTime)# 打印温湿度数据
        print(&quot;--------------&quot;)
        GPIO.cleanup()
        return temperature,humidity,currentTime
    else:
        GPIO.cleanup()
        print(&quot;error&quot;)
        return 0,0,currentTime

</code></pre>
<p>修改 thermometer.py 文件</p>
<pre><code class="language-python"># -*- coding: utf-8 -*-
import paho.mqtt.client as mqtt
import time
import hashlib
import hmac
import random
import json
import dht_sensor as dht

options = {
    'productKey':'你的productKey',
    'deviceName':'你的deviceName',
    'deviceSecret':'你的deviceSecret',
    'regionId':'cn-shanghai'
}

HOST = options['productKey'] + '.iot-as-mqtt.'+options['regionId']+'.aliyuncs.com'
PORT = 1883 
PUB_TOPIC = &quot;/sys/&quot; + options['productKey'] + &quot;/&quot; + options['deviceName'] + &quot;/thing/event/property/post&quot;;


# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    print(&quot;Connected with result code &quot;+str(rc))
    # client.subscribe(&quot;the/topic&quot;)

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    print(msg.topic+&quot; &quot;+str(msg.payload))

def hmacsha1(key, msg):
    return hmac.new(key.encode(), msg.encode(), hashlib.sha1).hexdigest()

def getAliyunIoTClient():
	timestamp = str(int(time.time()))
	CLIENT_ID = &quot;paho.py|securemode=3,signmethod=hmacsha1,timestamp=&quot;+timestamp+&quot;|&quot;
	CONTENT_STR_FORMAT = &quot;clientIdpaho.pydeviceName&quot;+options['deviceName']+&quot;productKey&quot;+options['productKey']+&quot;timestamp&quot;+timestamp
	# set username/password.
	USER_NAME = options['deviceName']+&quot;&amp;&quot;+options['productKey']
	PWD = hmacsha1(options['deviceSecret'],CONTENT_STR_FORMAT)
	client = mqtt.Client(client_id=CLIENT_ID, clean_session=False)
	client.username_pw_set(USER_NAME, PWD)
	return client


if __name__ == '__main__':
        while True:
            client = getAliyunIoTClient()
            client.on_connect = on_connect
            client.on_message = on_message
            client.connect(HOST, 1883, 300)
            temp,hum,currentTime = dht.get_sensor()
            
            payload_json = {
                'id': int(time.time()),
                    'params': {
                        'temperature': temp,
                        'humidity': hum,
                        'currentTime': currentTime
                    },
                'method': &quot;thing.event.property.post&quot;
            }
            client.publish(PUB_TOPIC,payload=str(payload_json),qos=1)
            time.sleep(3)
        client.loop_forever()

</code></pre>
<p>在控制台中运行一下程序，可以看到每隔4秒数据就会上报一次。</p>
<p><img src="https://img.dyzmj.top/img/image-20211109155935054.png" alt="image-20211109155935054"></p>
<p>打开阿里云物联网平台可以看到数据上报也是正常的</p>
<p><img src="https://img.dyzmj.top/img/image-20211109160136568.png" alt="image-20211109160136568"></p>
<h2>五、更好玩的方式</h2>
<p>上述例子中我们描述的是一个简单的 <code>IoT</code> 设备数据到物联网平台的数据采集流程，要想实时的获取办公室的温湿度数据，还得去网页上查看，或者得写一个专门从物联网平台获取转发数据的程序，这种就显得比较麻烦了，有没有什么办法把这些数据直接推送给我们呢，比如企业微信、钉钉等，这里我们以企业微信为例，实现这种好玩的方式。</p>
<p>打开企业微信，选择群聊新建一个群机器人。</p>
<!-- raw HTML omitted -->
<p>可以看到群机器人中提供了一个<code>Webhook</code>的功能（钉钉上同样也提供 webhook 功能），点击 <code>Webhook</code> 地址可以进入到机器人配置页面。</p>
<!-- raw HTML omitted -->
<p>我们复制上图中的示例脚本在服务器中执行下：</p>
<pre><code class="language-bash">curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693axxx6-7aoc-4bc4-97a0-0ec2sifa5aaa' \
   -H 'Content-Type: application/json' \
   -d '
   {
        &quot;msgtype&quot;: &quot;text&quot;,
        &quot;text&quot;: {
            &quot;content&quot;: &quot;hello world&quot;
        }
   }'

</code></pre>
<p>执行结果：</p>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p>可以看到企业微信中也正常推送 <code>hello world</code> 消息，由此可知，我们可以通过调用企业微信中群机器人的 <code>Webhook</code> 地址，并传入期望的参数，就可以在企业微信中获取我们想要的数据。</p>
<p>下面我们仍然使用 <code>python</code>  编码实现（注意修改 webhook 地址）：</p>
<pre><code class="language-bash">touch weixin.py
</code></pre>
<pre><code class="language-python">#-*- coding: utf-8 -*-

import os
import logging
import requests
import dht_sensor as dht
import time

#Webhook地址
headers = {&quot;Content-Type&quot;: &quot;application/json&quot;}
curl='填写群机器人的 Webhook地址'


while True:
        # 获取温湿度数据
        temperature,humidity,currentTime = dht.get_sensor()
        if humidity == 0:
            	# 异常数据，等待下一次循环
                continue
        # 上传至企业微信的数据
        data = {
                &quot;msgtype&quot;: &quot;markdown&quot;,
                &quot;markdown&quot;: {
                        &quot;content&quot;: &quot;### -- 办公室当前温湿度数据 --\n\n  &gt;温度:  &lt;font color=\&quot;info\&quot;&gt;&quot;+str(temperature)
                        +&quot; ℃&lt;/font&gt;\n\n  &gt;湿度:  &lt;font color=\&quot;info\&quot;&gt;&quot;+str(humidity)
                        +&quot; %&lt;/font&gt;\n\n  &gt;时间:  &lt;font color=\&quot;info\&quot;&gt;&quot;+str(currentTime)+&quot;&lt;/font&gt;&quot;
                }
        }
        # 调用 webhook 地址
        r = requests.post(url = curl,headers = headers,json = data)
        logging.basicConfig(level = logging.DEBUG,format = '%(asctime)s, %(filename)s, %(levelname)s, %(message)s',
                datefmt = '%a, %d %b %Y %H:%M:%S',filename = os.path.join('/home/pi/Desktop/sensor','weixin.log'),filemode = 'a')
        subject=&quot;办公室当前温湿度数据&quot;
        logging.info('subject:' + subject + ';message: 温度：' + str(temperature)+'℃，湿度：'+str(humidity) + &quot;%&quot;)
        time.sleep(20)

</code></pre>
<p>这里设置的是间隔20秒左右上报一次，运行程序：</p>
<!-- raw HTML omitted -->
<p>打开企业微信即可看到实时采集上来的办公室温湿度数据：</p>
<!-- raw HTML omitted -->
<h2>附录</h2>
<h3>1、<a href="https://www.aliyundrive.com/s/m4nRtr6Sob7">DHT11 温湿度传感器原理.pdf</a></h3>
<h3>2、<a href="https://iot.console.aliyun.com/lk/document">阿里云物联网平台文档与工具</a></h3>
<h3>3、<a href="https://shumeipai.nxez.com/2014/10/10/raspberry-dht11-get-temperature-data.html">树莓派从DHT11温湿度传感器读取数据</a></h3>
<h3>4、<a href="https://www.jianshu.com/p/9319c911825d">树莓派电脑控制温湿度传感器</a></h3>]]></description>
      <author>夂夂鱼</author>
      <enclosure url="https://img.dyzmj.top/img202410211536730.jpg" length="0" type="image/jpeg"></enclosure>
      <guid>article-2</guid>
      <pubDate>Mon, 27 Apr 2026 11:21:00 +0000</pubDate>
    </item>
    <item>
      <title>人在夏天的主要任务就是过夏天，工作象征性做一做，恋爱象征性谈一谈，饭差不多吃吃行...</title>
      <link>https://dyzmj.top/thinkings#thinking-1</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/thinkings#thinking-1">https://dyzmj.top/thinkings#thinking-1</a></p></blockquote><p>人在夏天的主要任务就是过夏天，工作象征性做一做，恋爱象征性谈一谈，饭差不多吃吃行了，都是陪衬。过夏天，长长地散步，虚度时间，听摇滚乐，让风吹过衣襟和头发，是第一重要的事情。</p>]]></description>
      <author>夂夂鱼</author>
      <guid>thinking-1</guid>
      <pubDate>Mon, 27 Apr 2026 11:16:21 +0000</pubDate>
    </item>
    <item>
      <title>星露谷写实图</title>
      <link>https://dyzmj.top/moments/2026/04/27/stardew_valley</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/moments/2026/04/27/stardew_valley">https://dyzmj.top/moments/2026/04/27/stardew_valley</a></p></blockquote><p><img src="https://img.dyzmj.top/img202604211348578.jpg" alt="原图"></p>
<p><img src="https://img.dyzmj.top/img202604211348627.jpg" alt="实图"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>moment-1</guid>
      <pubDate>Mon, 27 Apr 2026 11:15:55 +0000</pubDate>
    </item>
    <item>
      <title>大模型全栈技术图谱：从底层引擎到超级智能体</title>
      <link>https://dyzmj.top/posts/llm-full-stack-tech-map</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://dyzmj.top/posts/llm-full-stack-tech-map">https://dyzmj.top/posts/llm-full-stack-tech-map</a></p></blockquote><h1>大模型全栈技术图谱</h1>
<p><img src="https://img.dyzmj.top/img202604201648396.png" alt="image-20260420164812774"></p>
<h2>构建AI大厦的底层逻辑</h2>
<p><img src="https://img.dyzmj.top/img202604201648304.png" alt="image-20260420164827790"></p>
<h3>第一关：底层引擎 -- 大预言模型</h3>
<p><img src="https://img.dyzmj.top/img202604201648862.png" alt="image-20260420164841445"></p>
<p><img src="https://img.dyzmj.top/img202604201648593.png" alt="image-20260420164851568"></p>
<h3>第二关：数据原子 -- AI眼里的数字乐高</h3>
<p><img src="https://img.dyzmj.top/img202604201649593.png" alt="image-20260420164908818"></p>
<p><img src="https://img.dyzmj.top/img202604201649474.png" alt="image-20260420164922812"></p>
<p><img src="https://img.dyzmj.top/img202604201649832.png" alt="image-20260420164935406"></p>
<h3>第三关：临时记忆体 -- 大模型的即时工作台</h3>
<p><img src="https://img.dyzmj.top/img202604201649580.png" alt="image-20260420164946076"></p>
<p><img src="https://img.dyzmj.top/img202604201650505.png" alt="image-20260420164959932"></p>
<p><img src="https://img.dyzmj.top/img202604201650520.png" alt="image-20260420165015948"></p>
<h3>第四关：沟通桥梁 -- 与 AI 对话的灵魂画手<img src="https://img.dyzmj.top/img202604201650666.png" alt="image-20260420165029028"></h3>
<h3>第五关：能力外设 -- 给 “书呆子” 装上手脚</h3>
<p><img src="https://img.dyzmj.top/img202604201650661.png" alt="image-20260420165043192"></p>
<p><img src="https://img.dyzmj.top/img202604201650175.png" alt="image-20260420165054634"></p>
<h3>第六关：协作标准 -- AI世界的 “USB-C” 接口</h3>
<p><img src="https://img.dyzmj.top/img202604201651420.png" alt="image-20260420165110114"></p>
<p><img src="https://img.dyzmj.top/img202604201651246.png" alt="image-20260420165121830"></p>
<h3>第七/八关：超级智能体 -- 具备感知与执行的 “超级管家”</h3>
<p><img src="https://img.dyzmj.top/img202604201651744.png" alt="image-20260420165137396"></p>
<p><img src="https://img.dyzmj.top/img202604201651813.png" alt="image-20260420165152614"></p>
<p><img src="https://img.dyzmj.top/img202604201652360.png" alt="image-20260420165203823"></p>
<h2>现代大模型全栈架构</h2>
<p><img src="https://img.dyzmj.top/img202604201652334.png" alt="image-20260420165217725"></p>
<h2>生态网络</h2>
<p><img src="https://img.dyzmj.top/img202604201652303.png" alt="image-20260420165236108"></p>
<p><img src="https://img.dyzmj.top/img202604201652149.png" alt="image-20260420165247806"></p>
<h2>实战标杆：OpenClaw 个人智能体网关</h2>
<p><img src="https://img.dyzmj.top/img202604201653908.png" alt="image-20260420165301592"></p>
<p><img src="https://img.dyzmj.top/img202604201653812.png" alt="image-20260420165313442"></p>]]></description>
      <author>夂夂鱼</author>
      <guid>article-1</guid>
      <pubDate>Mon, 27 Apr 2026 03:01:09 +0000</pubDate>
    </item>
  </channel>
</rss>