<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://season8.github.io</id>
    <title>小栈</title>
    <updated>2021-10-14T13:47:52.966Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://season8.github.io"/>
    <link rel="self" href="https://season8.github.io/atom.xml"/>
    <subtitle>与君酌</subtitle>
    <logo>https://season8.github.io/images/avatar.png</logo>
    <icon>https://season8.github.io/favicon.ico</icon>
    <rights>All rights reserved 2021, 小栈</rights>
    <entry>
        <title type="html"><![CDATA[XXL-Job 调研]]></title>
        <id>https://season8.github.io/post/xxl-job-diao-yan/</id>
        <link href="https://season8.github.io/post/xxl-job-diao-yan/">
        </link>
        <updated>2021-10-08T14:12:16.000Z</updated>
        <summary type="html"><![CDATA[<p>易用的定时任务调度管理平台</p>
]]></summary>
        <content type="html"><![CDATA[<p>易用的定时任务调度管理平台</p>
<!-- more -->
<p>项目上的定时任务管理百花齐放，有用 Quartz 的，有用 Spring Schedule Job 的，还有 Crontab 的，任务实现也是跨多语言，有 Java、Groovy、Java-GroovyShell、Shell、Js.<br>
苦其久矣，正好手上事了，正好整合一波。</p>
<p>既然是奔着一劳永逸来的，这个工具就需要具备以下特点：</p>
<ol>
<li>跨语言，便于脚本移植。</li>
<li>支持动态部署任务。</li>
<li>支持集群，分摊单机压力和风险。</li>
<li>不只是关注与任务本身，需要有有任务从配置到执行整套流程的跟踪处理。</li>
<li>可视化管理后台。</li>
</ol>
<p>目前网上主流的任务调度平台有：<br>
Quartz、Elastic-Job、XXL-Job...</p>
<h3 id="quartz">quartz</h3>
<p>关注于任务任务定时触发，集合了 任务调度、任务执行的功能，支持基于数据库的集群，任务执行通过抢占DB锁的方式执行。</p>
<h3 id="elastic-job-e-job">elastic-job (E-Job)</h3>
<p>基于 Quartz 实现，由Elastic-Job-Lite和Elastic-Job-Cloud组成 。</p>
<ul>
<li>Elastic-Job-Lite：任务调度框架。通过 ZK 实现分布式集群，具有高可用的特点。</li>
<li>Elastic-Job-Cloud：供资源治理、应用分发以及进程隔离等功能，通过 ZK 实现分布式集群，使用Mesos 实现资源管理</li>
</ul>
<h3 id="xxl-job-x-job-最新版本v240">xxl-job (X-Job) (最新版本v2.4.0)</h3>
<p>v2.1.0 版本以前基于 Quartz 实现任务调度，之后使用基于 <strong>时间轮</strong> 的自研调度。<br>
由 xxl-job-admin 和 xxl-job-executor 两部分组成：</p>
<ul>
<li>xxl-job-admin：中心化的调度平台，支持基于数据库的集群方案，支持可视化管理</li>
<li>xxl-job-executor：任务执行器，支持集群，可自动注册到 调度中心。</li>
</ul>
<p><strong>对比图</strong></p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>Quartz</th>
<th>elastic-job</th>
<th>xxl-job</th>
</tr>
</thead>
<tbody>
<tr>
<td>文档、社区</td>
<td></td>
<td>文档完善，用户量较大，Apache项目</td>
<td>文档完善，开箱即用，用户量最多</td>
</tr>
<tr>
<td>依赖</td>
<td>mysql</td>
<td>jdk1.7+,zookeeper3.4.6+，maven3.0.4+，mesos</td>
<td>mysql，jdk1.7+，maven3.0+</td>
</tr>
<tr>
<td>侧重</td>
<td>定时任务触发</td>
<td>侧重数据、资源的有效利用、高可用性</td>
<td>侧重业务的实现简单化、任务流程管理方便化</td>
</tr>
<tr>
<td>动态添加任务</td>
<td>不支持</td>
<td>不支持，但可以扩展实现</td>
<td>支持</td>
</tr>
<tr>
<td>支持语言</td>
<td>可扩展定义执行器</td>
<td>Java、Shell，</td>
<td>Java、Groovy、Shell、Node、Python、Php，可扩展</td>
</tr>
<tr>
<td>集群、弹性扩容</td>
<td>多节点，部署，通过竞争数据库锁来保证只有一个节点执行任务</td>
<td>通过zookeeper的注册与发现，可以动态的添加服务器。支持水平扩容</td>
<td>集群部署唯一要求为：保证每个集群节点配置（db和登陆账号等）保持一致。调度器通过数据库锁实现集群，执行器通过总动注册到调度器，支持水平扩容</td>
</tr>
<tr>
<td>任务分片</td>
<td>不支持</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>管理界面</td>
<td>无</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>高级功能</td>
<td>无</td>
<td>弹性扩容，多种作业模式，失效转移，运行状态收集，多线程处理数据，幂等性，容错处理，spring命名空间支持</td>
<td>弹性扩容，分片广播，故障转移，Rolling实时日志，GLUE（支持在线编辑代码，免发布），任务进度监控，任务依赖，数据加密，邮件报警，运行报表，国际化</td>
</tr>
<tr>
<td>任务不能重复执行</td>
<td>数据库锁</td>
<td>任务分片执行：将任务拆分为n个任务项后，各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群，或现有服务器下线，elastic-job将在保留本次任务执行不变的情况下，下次任务开始前触发任重分片。</td>
<td>（集群）调度中心抢占db锁调度，再通过调度执行器来执行，如果任务配置的是分片广播模式，会触发所有执行器执行。</td>
</tr>
<tr>
<td>并行调度</td>
<td></td>
<td>采用任务分片方式实现。将一个任务拆分成n个独立的任务项，由分布式的服务器并行执行各自分配到的分片项。</td>
<td>调度系统多线程（默认10个线程）触发调度运行，确保调度精确执行，不被堵塞。</td>
</tr>
<tr>
<td>失败处理策略</td>
<td></td>
<td>弹性扩容缩容在下次作业运行前重分片，但本次作业执行的过程中，下线的服务器所分配的作业将不会重新被分配。失效转移功能可以在本次作业运行中用空闲服务器抓取孤儿作业分片执行。同样失效转移功能也会牺牲部分性能。</td>
<td>调度失败时的处理策略，策略包括：失败告警（默认，可扩展）、失败重试（界面可配置）</td>
</tr>
<tr>
<td>动态分片策略</td>
<td></td>
<td>支持多种分片策略，可自定义分片策略。 默认包含三种分片策略： 基于平均分配算法的分片策略、 作业名的哈希值奇偶数决定IP升降序算法的分片策略、根据作业名的哈希值对Job实例列表进行轮转的分片策略，支持自定义分片策略。elastic-job的分片是通过zookeeper来实现的。分片的分片由主节点分配，如下三种情况都会触发主节点上的分片算法执行：a、新的Job实例加入集群b、现有的Job实例下线（如果下线的是leader节点，那么先选举然后触发分片算法的执行）c、主节点选举”</td>
<td>分片广播任务以执行器为维度进行分片，支持动态扩容执行器集群从而动态增加分片数量，协同进行业务处理；在进行大数据量业务操作时可显著提升任务处理能力和速度。 执行器集群部署时，任务路由策略选择”分片广播”情况下，一次任务调度将会广播触发对应集群中所有执行器执行一次任务，同时传递分片参数；可根据分片参数开发分片任务；</td>
</tr>
<tr>
<td>缺点</td>
<td>1. 只关注定时触发，缺少任务调度、执行以及数据管理的整个流程。<br />2. 调度 以及 执行 一体，如果 任务量增大，拽行器对资源的占用可能会影响到 任务调度部分。<br />3. quartz 通过抢占DB锁 来确定执行节点，可能导致负载悬殊。<br />4. 需要持久化业务QuartzJobBean到底层数据表中，系统侵入性相当严重。<br />5. 不支持集群高可用、监控、告警 6. 无统一管理平台、统计、节点任务追踪</td>
<td>需要引入zookeeper，mesos，增加系统复杂度，学习成本较高</td>
<td>调度中心通过获取DB锁来保证集群中执行任务的唯一性，如果短任务很多，随着调度中心集群数量增加，那么数据库的锁竞争会比较厉害，性能不好。</td>
</tr>
<tr>
<td>使用场景</td>
<td></td>
<td>布式服务系统，大型任务调度需求</td>
<td>服务、任务一定量级以下的系统</td>
</tr>
</tbody>
</table>
<h3 id="评估">评估</h3>
<p>从功能和性能上来说， Quartz 无疑是过时的；<br>
从运维，E-Job 依赖与 ZooKeeper 和 Mesos，其运维管理是更加复杂的；<br>
从可用性上来说，E-Job 可用性以及资源利用率是最高的，服务节点越多、任务越多，优势越明显；<br>
从设计、使用来说，X-Job 最轻量，开发最简单；</p>
<p>EJob 和 XJob 似乎各有千秋，但基于我们的服务数量、任务数量、开发维护成本, XXL-Job 无疑是 最优解。</p>
<p>但这还不够，任务调度最关键的要素是：高效、准确地任务调度，如果是集群，还要考虑负载均衡、水平扩展等问题。</p>
<h2 id="xxl-job-设计原理">XXL-Job 设计原理</h2>
<figure data-type="image" tabindex="1"><img src="https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png" alt="架构图" loading="lazy"></figure>
<h3 id="调度中心">调度中心</h3>
<p>基于时间轮的自研调度组件（早期调度组件基于Quartz）；一方面是为了精简系统降低冗余依赖，另一方面是为了提供系统的可控度与稳定性；</p>
<p>采用线程池方式调度，避免单线程阻塞导致调度延迟。<br>
不同任务之间、单个任务在不同执行器内都是通过并行调度的。</p>
<p><strong>集群</strong>：基于数据库集群方案，随着服务节点和 短时间任务增加，数据库压力会增加。</p>
<p><strong>并发</strong>：</p>
<ul>
<li>全异步化设计：XXL-JOB系统中业务逻辑在远程执行器执行，触发流程全异步化设计。相比直接在调度中心内部执行业务逻辑，极大的降低了调度线程占用时间；
<ul>
<li>异步调度：调度中心每次任务触发时仅发送一次调度请求，该调度请求首先推送“异步调度队列”，然后异步推送给远程执行器</li>
<li>异步执行：执行器会将请求存入“异步执行队列”并且立即响应调度中心，异步运行。</li>
</ul>
</li>
<li>轻量级设计：XXL-JOB调度中心中每个JOB逻辑非常 “轻”，在全异步化的基础上，单个JOB一次运行平均耗时基本在 “10ms” 之内（基本为一次请求的网络开销）；因此，可以保证使用有限的线程支撑大量的JOB并发运行；</li>
</ul>
<p>得益于上述两点优化，理论上默认配置下的调度中心，单机能够支撑 5000 任务并发运行稳定运行；</p>
<p>实际场景中，由于调度中心与执行器网络ping延迟不同、DB读写耗时不同、任务调度密集程度不同，会导致任务量上限会上下波动。</p>
<p>如若需要支撑更多的任务量，可以通过 “调大调度线程数” 、”降低调度中心与执行器ping延迟” 和 “提升机器配置” 几种方式优化。</p>
<h3 id="执行器">执行器</h3>
<p>执行器 和 调度器之间通过 rpc 进行通讯，执行器可自动注册到调度中心，也可以通过调度中心手动录入执行器地址，执行器功能：</p>
<ul>
<li>接收调度器下发任务并执行</li>
<li>写执行日志文件</li>
<li>执行完毕调用调度器的回调方法</li>
<li>提供日志跟踪API</li>
</ul>
<p>xxl支持静态java 任务、动态 java 任务 以及其他多种脚本任务，也可以通过扩展支持更多的语言。</p>
<p>静态任务是随执行器发布的，动态任务则配置在调度中心，内容存储在数据库中。</p>
<h3 id="任务调度流程">任务调度流程</h3>
<p>1 任务执行器根据配置的调度中心的地址，自动注册到调度中心。<br>
2 达到任务触发条件，调度中心下发任务。<br>
3 执行器基于线程池执行任务，并把执行结果放入内存队列中、把执行日志写入日志文件中。<br>
4 执行器的回调线程消费内存队列中的执行结果，主动上报给调度中心。<br>
5 当用户在调度中心查看任务日志，调度中心请求任务执行器，任务执行器读取任务日志文件并返回日志详情。</p>
<h3 id="调度流程答疑">调度流程答疑</h3>
<ul>
<li>
<p>XXL-Job 会引发重复调度吗？<br>
网上很多重复调度原因是：1. 主从DB未强制走主库 2. SQL 脚本初始化不完全，导致无法生成锁记录。 但抛开这两点，XJob 还是会有重复调度的可能，这个问题 Quartz 也有。<br>
引发这个问题的原因？</p>
</li>
<li>
<p>如何避免调度器集群中的多个服务器同时调度任务？</p>
<p>节点通过抢占数据库悲观锁，任务，同一个任务同时只能被一个调度器调度。	<br>
为了避免重复调度，需要注意：DB主从部署时，务必走主库，防止主从延迟导致锁失效。<br>
另外，xxl-job优化了 Quartz中 重复触发的问题，降低了重复触发的概率，并提供了针对在度密集或者耗时任务阻塞的集群情况下预防小概率重复调度方案：通过结合 “单机路由策略（如：第一台、一致性哈希）” + “阻塞策略（如：单机串行、丢弃后续调度）” 来规避任务重复执行。</p>
</li>
<li>
<p>如何避免调度器集群中，调度器分配不均？</p>
<p>调度中心在集群部署时会自动进行任务平均分配，触发组件每次获取与线程池数量（调度中心支持自定义调度线程池大小）相关数量的任务，避免大量任务集中在单个调度中心集群节点。</p>
</li>
<li>
<p>调度器如何保证多任务调度的及时性？</p>
<p>无论是多任务还是单任务多个执行器，采用线程池 并行调度。</p>
</li>
<li>
<p>调度器线程如何避免等待慢任务造成调度线程阻塞？</p>
<p>任务调度和执行是异步的，不存在等待执行导致调度阻塞的现象，任务执行完成后会调用回调服务。</p>
</li>
<li>
<p>如何避免调度中心访问、执行器回调调度中心、API服务调用导致 调度中心节点负责失衡？</p>
<p>可以通过nginx 为 调度中心集群做负载，访问和配置调度中心时使用 nginx域名</p>
</li>
<li>
<p>如何实现任务执行器的路由？</p>
<p>xxl 提供了丰富的路由策略,包括：第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等.</p>
</li>
<li>
<p>执行节点掉线怎么办？</p>
<p>可以配置任务为 “故障转移” 策略，该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。</p>
</li>
<li>
<p>如果任务执行失败怎么处理？</p>
<p>可以配置任务失败重试次数，配置失败 告警</p>
</li>
<li>
<p>任务超时怎么处理？</p>
<p>为了防止任务超时导致执行线程阻塞，可以设置任务超时时间，超时会触发 interrupt (需要注意的是，任务中需要外抛 InterruptedException)</p>
</li>
<li>
<p>调度结果丢失处理</p>
<p>为了防止调度任务状态永远处于运行中，调度中心会检查 处于“运行中”超过10min的任务，如果对应执行器鑫涛注册失败，则将任务标记为失败。</p>
</li>
</ul>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[可编辑下拉框]]></title>
        <id>https://season8.github.io/post/ke-bian-ji-xia-la-kuang/</id>
        <link href="https://season8.github.io/post/ke-bian-ji-xia-la-kuang/">
        </link>
        <updated>2021-09-28T13:23:39.000Z</updated>
        <summary type="html"><![CDATA[<p>jquery 大法好</p>
]]></summary>
        <content type="html"><![CDATA[<p>jquery 大法好</p>
<!-- more -->
<p>应用后台开发的时候，写前端是难免的。<br>
可编辑下拉框，很多框架有实现，但总会碰到框架支持不好（比如layui）或者没有引入框架的情况。</p>
<p>网上找打原生实现的例子，鉴于前端写得少，转载记录之：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;

&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;自定义下拉选择框&lt;/title&gt;
    &lt;style type=&quot;text/css&quot;&gt;
    * {
        margin: 0;
        padding: 0;
    }

    ul,li {
        list-style: none;
    }

    :focus {
        outline: none;
    }

    input[type=text] {
        border: 1px solid #ccc;
    }

    input[type=text]:hover,
    input[type=text]:focus {
        border-color: #57bc80;
        box-shadow: none;
    }

    body {
        padding: 50px;
        font-size: 12px;
    }

    .my-select-box {
        position: relative;
    }

    .my-select-box .my-select-input {
        height: 24px;
        line-height: 24px;
        padding: 0 5px;
        padding-right: 20px !important;
        width: 100%;
        box-sizing: border-box;
    }

    .my-select-box .my-select-list {
        position: absolute;
        left: 0;
        z-index: 101;
        border: 1px solid #ccc;
        border-top: none;
        max-height: 120px;
        overflow-y: auto;
        display: none;
        background: #fff;
        width: 100%;
        box-sizing: border-box;
    }

    .my-select-box .my-select-list li {
        height: 22px;
        line-height: 22px;
        padding: 0 3px;
        cursor: pointer;
        margin-bottom: 1px
    }

    .my-select-box .my-select-list li.choosed {
        background: #ccc;
        color: #fff;
    }

    .my-select-box .my-select-list li:hover {
        background: #ccc;
        color: #fff;
    }
    &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;div class=&quot;my-select-box&quot; style=&quot;width:300px;&quot;&gt;
        &lt;input type=&quot;text&quot; class=&quot;my-select-input&quot; placeholder=&quot;可输入也可选择&quot; maxlength=&quot;20&quot; /&gt;
        &lt;ul class=&quot;my-select-list&quot;&gt;
            &lt;li data-value=&quot;1&quot;&gt;第一项&lt;/li&gt;
            &lt;li data-value=&quot;2&quot;&gt;第二项&lt;/li&gt;
            &lt;li data-value=&quot;3&quot;&gt;第三项&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div style=&quot;height: 30px&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;my-select-box&quot;&gt;
        &lt;input type=&quot;text&quot; class=&quot;my-select-input&quot; placeholder=&quot;可输入也可选择&quot; maxlength=&quot;20&quot; /&gt;
        &lt;ul class=&quot;my-select-list&quot;&gt;
            &lt;li data-value=&quot;1&quot;&gt;第一项&lt;/li&gt;
            &lt;li data-value=&quot;2&quot;&gt;第二项&lt;/li&gt;
            &lt;li data-value=&quot;3&quot;&gt;第三项&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;jquery-3.2.1.min.js&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
    ;
    (function($) {
        $.fn.MySelect = function() {
            this.each(function() {
                var $box = $(this);
                var $input = $box.find(&quot;input.my-select-input&quot;); //输入框
                var $list = $input.next(); //ul装扮成下拉框

                //计算input输入框的高度和宽度，方便定位ul和设置ul及包裹元素的宽度
                var inputHeight = $input.outerHeight();
                //var inputWidth=$input.innerWidth();
                $list.css({ &quot;top&quot;: (inputHeight) });
                //$box.width($input.outerWidth());

                $input.focus(function() {
                    //输入框获得焦点后，显示下拉选择ul
                    var $nextUl = $(this).next();
                    if ($nextUl.children().length &gt; 0) {
                        $(this).next().show();
                    }
                }).bind('input propertychange', function() {
                    //绑定监测输入框的输入值更改
                    var $this = $(this);
                    $this.attr(&quot;data-id&quot;, &quot;&quot;);
                    var curText = $this.val();
                    var $nextUl = $(this).next();
                    if ($nextUl.children().length &gt; 0) {
                        $nextUl.find(&quot;li&quot;).removeClass(&quot;choosed&quot;);
                        $nextUl.find(&quot;li&quot;).each(function(i, item) {
                            var txt = $(item).text();
                            if (txt === curText) {
                                var v = $(item).attr(&quot;data-value&quot;);
                                $this.attr(&quot;data-id&quot;, v);
                                $(item).addClass(&quot;choosed&quot;);
                            }
                        });
                    }
                });
                //修改成如下事件绑定，为了给动态添加的li也可以产生点击效果
                $list.off('click', 'li').on('click', 'li', function(e) {
                    var $this = $(this);
                    var value = $this.attr(&quot;data-value&quot;) || '';
                    $input.val($this.text()).attr(&quot;data-id&quot;, value);
                    $this.addClass(&quot;choosed&quot;).siblings().removeClass(&quot;choosed&quot;);
                    $this.parent().hide();
                });
            });

            //点击.my-select-box范围外时隐藏ul下拉框
            $(document).click(function(e) {
                var target = e.target;
                var $target = $(target);
                var $parent = $target.closest('.my-select-box');

                if ($parent.length &lt; 1) {
                    //说明不是.my-select-box范围内点击，将ul隐藏
                    $(&quot;.my-select-list&quot;).hide();

                } else if ($parent.length == 1) {
                    //如果存在多个my-select-box的情况，将其余的非这项以外的都隐藏
                    var $ul = $parent.find(&quot;.my-select-list&quot;);
                    var flag = $ul.is(&quot;:hidden&quot;);
                    $(&quot;.my-select-list&quot;).hide();
                    if (!flag) $ul.show();
                }
            });
            return this;
        }
    })(jQuery);

    $(&quot;.my-select-box&quot;).MySelect();
    &lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre>
<p>转自 <a href="https://www.jianshu.com/p/97252c501e91">原文</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[浅析 String]]></title>
        <id>https://season8.github.io/post/qian-xi-stringintern/</id>
        <link href="https://season8.github.io/post/qian-xi-stringintern/">
        </link>
        <updated>2021-09-22T14:22:11.000Z</updated>
        <summary type="html"><![CDATA[<p>妙啊😏</p>
]]></summary>
        <content type="html"><![CDATA[<p>妙啊😏</p>
<!-- more -->
<h2 id="认识-string从-0-开始">认识 String，从 0 开始</h2>
<p>无论是 初学 java 还是 java 老司机， 一定碰到过这个问题： String 是 基础类型 还是 对象类型？ 答案毫无悬念， 但问题本身是值得探究的：看似指鹿为马，实事出有因，String 作为一个对象类型，其使用方式 和 性质 还真就有点 基础类型的意思。</p>
<pre><code class="language-java">public final class String
    implements java.io.Serializable, Comparable&lt;String&gt;, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
</code></pre>
<p>从String 的源码可以看到：</p>
<ol>
<li>String 是基于 char数组实现的</li>
<li>String 以及 成员变量 value、hash 都是 final 修饰的。</li>
</ol>
<p>为什么要使用 final 去修饰呢？先看看 final 关键字的作用：</p>
<ol>
<li>修饰的类，无法被继承。</li>
<li>修饰的方法，无法被重写。</li>
<li>修饰的变量，必须初始化，且初始化之后，值不能更改。</li>
<li><strong>隐藏属性</strong>：如果 final 修饰的 是一个 普通对象类型， 那么只能限定其引用不能修改，其非 final 修饰的成员变量 的值 ，是可以修改的，就好比我改了名字，但在改名前后，我任然是代表我自己，指代没有发生变化。</li>
</ol>
<p>那么，综合来看,可以得出以下结论:</p>
<ol>
<li>Stirng 对象无法被继承。</li>
<li>成员 value 和 hash 必须被初始化。</li>
<li>成员 value 的引用不能更改、成员hash 的值不能被修改。</li>
</ol>
<p>注意第三条，value 只是引用不能修改，，因为它是 final char[] 对象，在不修改引用的前提下，修改对象内部值是不违反 final 关键字的约束的（见 final 隐藏属性）。</p>
<p>但是，在 String 中，成员 value 除了 有 final 修饰以外，还有一个 private 修饰符，它表示 value 只能被 String 对象自身 以及 <s>String 的 子类</s>（上面总结第一点：String 无法被继承）访问，而 String 类中 涉及 对 value 写入 的方法，只有 构造器了，所以 String 的成员 value 既不能被修改引用，又不能修改 value 对象内的值。</p>
<p>再次归纳一下：<br>
String 对象自创建后，它的对象引用无法被修改、内部成员 的引用 和值 也都无法被修改，它从初始化后，就没有办法被改变了，我们就称这个特性为”<strong>不可变性</strong>“  好了。</p>
<p>但是，这也太奇怪了，它明明是一个对象不是吗？为什么要给他加上这种 ”不可变性“的约束呢？<br>
要我说，这样的对象，它除了定义本身是对象、有构造器 和 诸多成员方法外，更像是一个基础数据类型呢！事实上，我们在使用 String 类型变量的 便捷、频率上，它几乎是和基础类型没什么区别，甚至我在刚刚接触编程的时候，都有一种 String 是基础数据类型的错觉！</p>
<p>要解释这个疑问，不妨逆向思维一下：假如 String 没有 ”不可变性“，现有的程序会出现哪些问题？</p>
<ol>
<li>静态 String 变量将失去意义，比如如下常量，可以通过实现 String 的子类来修改其内部属性。</li>
<li>String 对象，将变得不安全，和第一种情况相似，任何一个线程都有可能会修改 String 对象的内容。</li>
<li>String 的 hash 将可改变，这会导致 HashMap 失去作用（每次hash 改变都要去 计算hash，改变所属桶），因为 HashMap 的键值设计就是基于 String 的 hashCode。</li>
<li>我们常使用的 字符串<strong>常量</strong>池， 也将失去作用，当 String 可变时， 就已经不能满足常量这一称呼了。</li>
</ol>
<p>所以，String 类型“<strong>不可变性</strong>”的作用是如此的重要， 正是有了这一个特性，我们才能：</p>
<ul>
<li>像使用常量一样随意去使用它，不用考虑它的安全问题</li>
<li>能像常量一样把常用的 String 对象 存储到 常量池中， 节省内存</li>
<li>利用 hashCode 快速在 HashMap 中 存储、查找元素</li>
</ul>
<h2 id="string-创建机制">String 创建机制</h2>
<p>如果把 String 比作子弹，<strong>不可变性</strong>  就好比附魔，jdk 提供的 String 创建机制 就是一把能打出附魔子弹的手枪。</p>
<p>下面一段代码使用了常见的几种 方式来 创建 String 对象：</p>
<pre><code class="language-java">public class StringDemo {
	public void init() {
		String s1 = &quot;foo bar&quot;; // 1
		String s2 = new String(&quot;foo bar&quot;);// 2
		String s3 = &quot;foo &quot; + &quot;bar&quot;;// 3
		String s4 = &quot;foo &quot; + new String(&quot;bar&quot;);// 4
		String s5 = String.valueOf(&quot;foo bar&quot;);// 5
	}
}
</code></pre>
<p>直接双引号创建、支持 + 操作符，这些便捷的语法，或许和编译器有关，但，他们之间有没有区别呢？大致的原理又是什么？是时候让字节码发言了！</p>
<pre><code class="language-java">// class version 52.0 (52)
// access flags 0x21
public class StringDemo {

  // compiled from: StringDemo.java

  // access flags 0x1
  public &lt;init&gt;()V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.&lt;init&gt; ()V
    RETURN
   L1
    LOCALVARIABLE this Lcj/StringDemo; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public init()V
   L0
    LINENUMBER 14 L0
    LDC &quot;foo bar&quot; // 常量池 对象 的引用，入栈
    ASTORE 1 // 栈顶出栈，赋值给 变量表 index=1 的变量，s1
   L1
    LINENUMBER 15 L1
    NEW java/lang/String // 创建 String 对象，入栈
    DUP // 赋值栈顶对象并入栈
    LDC &quot;foo bar&quot; // 常量池 对象 的引用，入栈
    INVOKESPECIAL java/lang/String.&lt;init&gt; (Ljava/lang/String;)V // 调用 String 构造器
    ASTORE 2 // 栈顶出栈，赋值给 变量表 index=2 的变量，s2
   L2
    LINENUMBER 16 L2
    LDC &quot;foo bar&quot; // 常量池 对象 的引用，入栈
    ASTORE 3 // 栈顶出栈，赋值给 变量表 index=3 的变量，s3
   L3
    LINENUMBER 17 L3
    NEW java/lang/StringBuilder // 创建 StringBuilder 对象，入栈
    DUP
    INVOKESPECIAL java/lang/StringBuilder.&lt;init&gt; ()V // 调用 StringBuilder 构造器
    LDC &quot;foo &quot; // 常量池 对象 的引用，入栈
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; // 调用 StringBuilder#append ，返回值入栈
    NEW java/lang/String // 创建 String 对象，入栈
    DUP
    LDC &quot;bar&quot; // 常量池 对象 的引用，入栈
    INVOKESPECIAL java/lang/String.&lt;init&gt; (Ljava/lang/String;)V // 调用 String 构造器，利用 常量池中“bar”的引用创建新的对象
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; // 调用 StringBuilder#append ，返回值入栈
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 4 // 栈顶出栈，赋值给 变量表 index=4 的变量，s4
   L4
    LINENUMBER 18 L4
    LDC &quot;foo bar&quot; // 常量池 对象 的引用，入栈
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    ASTORE 5 // 栈顶出栈，赋值给 变量表 index=5 的变量，s5
   L5
    LINENUMBER 19 L5
    RETURN
   L6
    LOCALVARIABLE this Lcj/StringDemo; L0 L6 0
    LOCALVARIABLE s1 Ljava/lang/String; L1 L6 1
    LOCALVARIABLE s2 Ljava/lang/String; L2 L6 2
    LOCALVARIABLE s3 Ljava/lang/String; L3 L6 3
    LOCALVARIABLE s4 Ljava/lang/String; L4 L6 4
    LOCALVARIABLE s5 Ljava/lang/String; L5 L6 5
    MAXSTACK = 4
    MAXLOCALS = 6
}
</code></pre>
<p>先解释几个指令：</p>
<ul>
<li><strong>NEW</strong>：创建对象，参数为对象类型</li>
<li><strong>DUP</strong>: 复制栈顶的一个或两个数值并将其重新压入栈顶</li>
<li><strong>LDC</strong>: 接收一个 8 位的参数，指向常量池中的索引</li>
<li><strong>ASTORE n</strong>: 从操作数中弹出一个引用数据类型，并把它赋值给局部变量表中索引为 n 的变量</li>
<li><strong>INVOKESPECIAL</strong>: 调用一些需要特殊处理的方法，包括构造方法、私有方法和父类方法</li>
<li><strong>INVOKEVIRTUAL</strong>: 调用对象的成员方法，根据对象的实际类型进行分派，支持多态</li>
<li><strong>INVOKESTATIC</strong>: 调用静态方法</li>
</ul>
<p>逐个对象对照分析：</p>
<ul>
<li>s1 直接使用双引号创建， 其实际是常量池对象的引用，也就是说，直接双引号创建的字符串都是直接创建在了常量池中。</li>
<li>s2 通过构造器创建，它是 基于 常量池对象引用 ，创建（new）的 新的对象，即：先在常量池中创建了对象，再基于该对象引用，在堆中创建新的对象，不考虑s1的话，这里一共创建了2个对象！</li>
<li>s3 看上去和 s1 指令相同，说明编译器将其优化了</li>
<li>s4 通过 + 拼接 两段 字符串，它的本质居然是 创建了一个空的 StringBuilder，经过两次apend后，调用toString 返回的对象。</li>
</ul>
<p>至此 String 三种创建机制的原理和区别，已经清晰明了：</p>
<ol>
<li>“”创建： 创建了1个对象， 对象在常量池中</li>
<li>new String(&quot;&quot;)：创建了 2 个对象， 第一个对象在常量池中，后一个对象value 是 前一个对象value的引用</li>
<li>“+”拼接：如果拼接的是两段 双引号字符串，那么将直接被编译器优化一个双引号字符串（方式1），否则，将使用 StringBuilder 进行 拼接，其中，每一段构造器创建子串 都将执行 方式2 。</li>
</ol>
<h2 id="字符串常量池">字符串常量池</h2>
<p>上一步的字节码中，有一个指令出现得很频繁：LDC（创建常量池对象，返回其引用）<br>
这里常量池准确来说是指代 字符串常量池，那么，字符串常量池是什么？</p>
<p>探究常量池，可以从 String 的 intern 方法中找到蛛丝马迹：</p>
<pre><code class="language-java">    /**
     * Returns a canonical representation for the string object.
     * &lt;p&gt;
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * &lt;p&gt;
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * &lt;p&gt;
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * &lt;p&gt;
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * &lt;cite&gt;The Java&amp;trade; Language Specification&lt;/cite&gt;.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();
</code></pre>
<p>从注释部分，可以看出关于字符串常量池的描述：一个[String 类私有地维护的] [初始容量为空的] 字符串池。</p>
<p>intern 方法的作用就是：如果池中 已经有了一个 （与当前对象内容） 相同value 的对象，返回池中对象；否则， 将当前对象添加到池中，并返回 当前对象的引用。</p>
<p>注释中还提到：所有明面上的字符串和 字符串值常量表达式，都是已经执行了 intern 方法的。所谓 明面上的字符串 就是 上面提到的 双引号创建的字符串，如：String s1 = &quot;abc&quot;，字符串常量表达式则是形如 「String s2 =“a”+ &quot;b&quot;」  这种 表达式，这也应证了前面分析 String 创建机制 的字节码内容。</p>
<p>intern 是一个 native 方法，它的实现部分是在 jdk 的 C++ 类中，在线查看 openjdk 源码（我选择的是 <a href="http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d">open jdk 8</a>）</p>
<p><a href="http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/lang/String.c">jdk: String.c</a> ：</p>
<pre><code class="language-C">#include &quot;jvm.h&quot;
#include &quot;java_lang_String.h&quot;

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}
</code></pre>
<p>\jdk-687fd7c7986d\src\share\javavm\export\jvm.h：<br>
<a href="http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/jvm.h">hostspot: jvm.h</a>：</p>
<pre><code class="language-C">...
/*
 * java.lang.String
 */
JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);
</code></pre>
<p><a href="http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/jvm.cpp">hostspot: jvm.cpp</a>：</p>
<pre><code class="language-C">// String support ///////////////////////////////////////////////////////////////////////////

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper(&quot;JVM_InternString&quot;);
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END
</code></pre>
<p>可以看到， java.lang.String#intern 是 通过 JNI（Java Native Interface） 调用 C++ 代码中的 StringTable 的 intern 方法实现的。 StringTable 是类似与 java 中 HashMap的结构，区别是，它的长度是固定的: 1009，另外，我们可以通过设置 jvm 启动参数修改默认大小：</p>
<pre><code>  -XX:StringTableSize=9999
</code></pre>
<p>jdk1.7 以前的版本中，字符串常量池 维护在永久代中，但是在 1.7 版本开始，jdk 移除了永久代，字符串常量池移到了 元数据区，该区又是维护在了堆内存中，这样可以将 永久代 OOM 的风险转到 堆中。字符串常量池的移动对 版本的兼容是有一定影响的，典型的就是：</p>
<p>创建了几个 String 对象 的问题，我们实际应用中可能不会去触发这个问题，但是这个问题作为一个基础知识，能绕晕一大片老司机😵</p>
<p>是的，我必然是要分析这个经典八股题的，这篇八股能讲清楚了，那是真正对 『字符串常量池』、『永久代和 元数据区的关系』 了解的透彻了。</p>
<pre><code class="language-java">public static void main(String[] args) {
    String s = new String(&quot;1&quot;);
    s.intern();
    String s2 = &quot;1&quot;;
    System.out.println(s == s2); 

    String s3 = new String(&quot;1&quot;) + new String(&quot;1&quot;);
    s3.intern();
    String s4 = &quot;11&quot;;
    System.out.println(s3 == s4);
}
</code></pre>
<p>输出结果：<br>
jdk1.6: <em>false</em> <em>false</em><br>
jdk1.7:  <em>false</em> <em>true</em></p>
<p>为什么是这个结果，可以结合以下内存变量内存分布图来分析：</p>
<pre><code>     ┌────────┐      ┌───────Heap───────────┐  ┌────PermGen───────────┐
     │        │      │                      │  │                      │
     │        │      │     ┌─────────┐      │  │ ┌────String Pool───┐ │
     │   s ───┼──────┼────►│  String │      │  │ │                  │ │
     │        │      │     │   Obj   │--------------►┌──────────┐   │ │
     │        │      │     └─────────┘      │  │ │   │          │   │ │
     │        │      │                      │  │ │   │   &quot;1&quot;    │   │ │
     │  s2 ───┼──────┼──────────────────────┼──┼─┼──►│          │   │ │
     │        │      │                      │  │ │   └──────────┘   │ │
     │        │      │     ┌─────────┐      │  │ │                  │ │
     │        │      │     │ String  │      │  │ │   ┌──────────┐   │ │
     │  s3 ───┼──────┼────►│   Obj   │      │  │ │   │          │   │ │
     │        │      │     └─────────┘      │  │ │   │   &quot;11&quot;   │   │ │
     │        │      │                      │  │ │   │          │   │ │
     │  s4 ───┼──────┼──────────────────────┼──┼─┼──►└──────────┘   │ │
     │        │      │                      │  │ │                  │ │
     │        │      │                      │  │ └──────────────────┘ │    ─────►：对象引用
     │        │      │                      │  │                      │    -----►：对象值引用
     └────────┘      └──────────────────────┘  └──────────────────────┘
                             jdk 1.6
</code></pre>
<p>先来看 jdk1.6 ，结合前面的 String 实例化方式以及字节码知识逐条分析：</p>
<ol>
<li>String s = new String(&quot;1&quot;)<br>
创建 s 对象的时候， 常量池中以及有常量 &quot;1&quot; 了， 然后基于该常量创建了新的对象 s，此时， s 对象 不等于常量池中常量，但其 「char[] value」是指向 常量池中 常量 的 value （见构造器）。</li>
<li>s.intern()<br>
由于常量池中已经有了对象，s.intern() 直接返回常量池中的对象，注意该表达式并没有使用变量来接收返回值，故不产生任何影响。</li>
<li>String s2 = &quot;1&quot;<br>
使用书面表达式创建字符串，所以 s2 是直接引用的 常量池对象。</li>
</ol>
<p>因此，s 指向堆中对象，s2 指向常量池对象，二者不相同，但它们指向的对象 的 值（value）相同。</p>
<ol start="4">
<li>
<p>String s3 = new String(&quot;1&quot;) + new String(&quot;1&quot;);<br>
根据 前面字节码分析，可知 s3 创建流程为：</p>
<ol>
<li>创建 StringBuilder 对象</li>
<li>创建常量池对象 “1”</li>
<li>用常量池对象的值（value）在堆中创建一个新的对象“1”</li>
<li>StringBuilder 对象 append 新创建的对象</li>
<li>重复第 3 步，第4 步</li>
<li>s3 = StringBuilder#toString</li>
</ol>
<p>s3 创建完成后，常量池中创建了对象 “1”，但没有创建常量 “11”</p>
</li>
<li>
<p>s3.intern();<br>
由于常量池中没有 “11”，因此将 s3 放到常量池中，由于 s3 在堆中，常量池在 永久区，因此此时的 『将 s3 放入 常量池』实际是 『用 s3的值在常量池中创建一个对象，二者对象、对象值不存在任何引用关系』。这里 的 intern 放回的是 常量池中创建的新对象，其返回值没有变量接收，不产生任何影响。</p>
</li>
<li>
<p>String s4 = &quot;11&quot;;<br>
s4 使用书面表达式创建字符串，所以 s4 = 常量池中对象</p>
<p>因此，s3 指向堆中对象， s4 指向 常量池中对象， 其对象 和 对象值 没有任何关系</p>
</li>
</ol>
<pre><code>     ┌────────┐      ┌────────────────────Heap────────────────────────┐
     │        │      │                                                │
     │        │      │     ┌─────────┐           ┌─────tring Pool───┐ │
     │   s ───┼──────┼────►│  String │           │                  │ │
     │        │      │     │    Obj  │--------------►┌──────────┐   │ │
     │        │      │     └─────────┘           │   │ String   │   │ │
     │        │      │                           │   │    1     │   │ │
     │  s2 ───┼──────┼───────────────────────────┼──►│          │   │ │
     │        │      │                           │   └──────────┘   │ │
     │        │      │     ┌─────────┐           │                  │ │
     │        │      │     │ String  │           │   ┌──────────┐   │ │
     │  s3 ───┼──────┼────►│   11    │◄──────────┼───┤  String  │   │ │
     │        │      │     └─────────┘           │   │    Obj   │   │ │
     │        │      │                           │   │          │   │ │
     │  s4 ───┼──────┼───────────────────────────┼──►└──────────┘   │ │
     │        │      │                           │                  │ │
     │        │      │                           └──────────────────┘ │    ─────►：对象引用
     │        │      │                                                │    -----►：对象值引用
     └────────┘      └────────────────────────────────────────────────┘

                            jdk1.7
</code></pre>
<p>再来看 jdk1.7</p>
<ol>
<li>String s = new String(&quot;1&quot;)</li>
<li>s.intern()</li>
<li>String s2 = &quot;1&quot;</li>
</ol>
<p>s 和 s2 创建 过程和结果 与 jdk 1.6 一样，唯一区别是常量池位置。</p>
<ol start="4">
<li>String s3 = new String(&quot;1&quot;) + new String(&quot;1&quot;);<br>
s3 过程和结果 与 jdk 1.6 一样</li>
<li>s3.intern();<br>
由于常量池中没有 “11”，因此将 s3 放到常量池中，由于 s3 在堆中，常量池也在堆中，因此，intern 会直接将  s3 “放入”到常量池中，其结果就是 常量池对象 是 s3 对象（堆中对象）的引用</li>
<li>String s4 = &quot;11&quot;;<br>
s4 使用书面表达式创建字符串，所以 s4 = 常量池中对象 = s3</li>
</ol>
<h2 id="合理适用-字符串常量池intern">合理适用 字符串常量池/intern</h2>
<p>字符串常量池的作用就一个：节省空间<br>
通常来讲，空间和时间不可兼得，节省了空间，就会耗费更多时间，intern 操作 是有额外时间开销的。<br>
如果常量池使用不当， 这个时间开销可能会被放大，甚至有 OOM的风险。</p>
<p>举个栗子，假如一个应用 持续产生 不重复的字符串， 并每次获取字符串时都尝试使用 intern 将其存入常量池 或从 常量池返回已有对象，由于 字符串不重复，intern 实际 是持续往 StringTable put 数据的，前面我们提到过，StringTable 的结构和 HashMap 类似，我们可以类比 HashMap 来分析：</p>
<p>首先，HashMap 是有 若干个 桶 组成， 每个桶又由一个链表或红黑树组成，当元素特别多的时候，Hash 碰撞比较频繁， 桶内 链表长度 或 树的深度会非常大，此时若是 查询，时间开销将很大。</p>
<p>intern 在将数据 放入到 StringTable 前，是需要去查字符串值是否已经在 StringTable 中的，此时刚好就产生了大量的时间开销。</p>
<p>那么正确的姿势是？<br>
大量字符串对象且重复率高的应用，可以在字符串获取的时候使用 intern，这样虽然增加了些许时间开销，但能节省大量 新对象的空间开销。 值得注意的是，实际使用中也要根据 字符串对象的量 配置合适的 StringTableSize，太小，Hash碰撞 导致 查询耗时增加， 太大， 浪费内存。</p>
<p>参考：<br>
<a href="https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html">美团-深入解析 String#intern</a><br>
<a href="http://hg.openjdk.java.net/">openjdk 源码</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[pdf预览的几种方法]]></title>
        <id>https://season8.github.io/post/pdf-yl/</id>
        <link href="https://season8.github.io/post/pdf-yl/">
        </link>
        <updated>2021-09-06T14:08:46.000Z</updated>
        <summary type="html"><![CDATA[<p>快速实现 pdf / image 预览</p>
]]></summary>
        <content type="html"><![CDATA[<p>快速实现 pdf / image 预览</p>
<!-- more -->
<h2 id="预览-pdf-image">预览 pdf / image</h2>
<p>后台要求增加一个 预览pdf和图片文件的功能，查了一下实现方法，网上首推 pdf.js，我瞎捣鼓了一下，很是发现了几种实现方式，都挺便捷的。</p>
<ol>
<li>访问静态文件url</li>
<li>通过文件流</li>
<li>HTML标签+Base64</li>
<li>pdf.js</li>
</ol>
<p>下面详细说明一下几种方式的特点，并附上代码，由于我是使用的 layui，故例子都是layui的实现，其他框架也是差不多的，核心思路不变。</p>
<h2 id="访问静态文件url">访问静态文件url</h2>
<p>顾名思义，这种方式是针对已有的静态资源，不需要后端将其转换为流。</p>
<p>使用注意事项：</p>
<ul>
<li>依赖浏览器对图片和pdf的支持，通常浏览器会解析为预览或者下载。</li>
<li>可以被下载监听插件比如IDM监听拦截，如果有相关插件，需要禁用或者添加例外。</li>
</ul>
<p>示例：</p>
<pre><code class="language-javascript">// layui.layer
layer.open({
    title: &quot;预览&quot;,
    type: 2, // iframe层，content 为url
    area: ['1000px', '750px'],
    content: '../static/a.pdf'
});
</code></pre>
<h2 id="通过文件流">通过文件流</h2>
<p>比较适合动态的文件的访问，原理就是服务端将文件读取并写入到输出流中，前端读取流内容。<br>
这种方式比较灵活，对文件没什么限制</p>
<p>使用注意事项：</p>
<ul>
<li>依赖浏览器的支持,可能会解析成预览或下载，</li>
<li>可以被下载监听插件比如IDM监听拦截。</li>
</ul>
<p>示例：</p>
<pre><code class="language-javascript">// layui.layer
layer.open({
    title: &quot;预览&quot;,
    type: 2, // iframe层，content 为url
    area: ['1000px', '750px'],
    content: '${pageContext.request.contextPath }/preview.pdf',
});
</code></pre>
<p>Java后端返回流示例：</p>
<pre><code class="language-java">public void detail(HttpServletRequest request, HttpServletResponse response) {

        // 生成或读取文件到字节数组
        byte[] bytes = xxx;

        // 写入到输出流中
        try {
            response.getOutputStream().write(bytes);
            // 设置文件类型
            response.setContentType(&quot;application/pdf;charset=UTF-8&quot;);
        } catch (IOException e) {
            log.error(&quot;&quot;,e);
        }
    }
</code></pre>
<h2 id="html标签-base64">HTML标签 + Base64</h2>
<p>这种情况适用于使用时，已经加载到了客户端的资源。</p>
<p>特点：</p>
<ul>
<li>开发简单</li>
<li>吃客户端内存</li>
<li>只加载一次，能有效减少网络io，如果文件比较大，还能节省大量时间</li>
</ul>
<p>示例：</p>
<pre><code class="language-javascript">layer.open({
    title: &quot;预览&quot;,
    type: 1, // 页面层， content支持文本和html
    area: ['1000px', '750px'],
    // image tag
    content:  '&lt;img src=&quot;data:image/png;base64,' + data.base64 + '&quot;/&gt;',
    // pdf tag
    content: '&lt;embed src=&quot;data:application/pdf;base64,'+ data.base64 +'&quot; type=&quot;application/pdf&quot; width=&quot;100%&quot; height=&quot;100%&quot;&gt;'
});
</code></pre>
<h2 id="pdfjs">*pdf.js</h2>
<p>网上用这个的人比较多，pdf.js 支持新老版本浏览器，可以基于viewer.html进行修改定制。<br>
由于需求与效率原因，没有尝试，如果以上三板斧不能满足需求，pdf.js应该是不错的。</p>
<p>附： (pdf.js 官方连接)[https://github.com/mozilla/pdf.js]</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[分布式id生成器]]></title>
        <id>https://season8.github.io/post/id-generators/</id>
        <link href="https://season8.github.io/post/id-generators/">
        </link>
        <updated>2021-07-19T13:05:59.000Z</updated>
        <summary type="html"><![CDATA[<p>这破玩意儿能叫分布式？</p>
]]></summary>
        <content type="html"><![CDATA[<p>这破玩意儿能叫分布式？</p>
<!-- more -->
<p>最近老项目升级到cloud，用到了 snowflake，苦于手动配置workerId，撸了个没用的轮子---基于配置中心的雪花ID</p>
<h2 id="分布式-id-之-snowflake">分布式 ID 之 SNOWFLAKE</h2>
<p>说起分布式id，首推 SNOWFLAKE（雪花算法），它具有以下优势：</p>
<ul>
<li>高效：性能好</li>
<li>分布式唯一：与DB无关，适应分布式应用</li>
<li>趋势递增：与时间相关，保持一定的时序性，但又不是绝对递增，可防止id泄漏业务增长信息。</li>
</ul>
<p>缺点大概就是：</p>
<ol>
<li>与时间相关，时间错乱可能会出现重复id，即时钟回拨问题。</li>
<li>多应用服务之间，可能会产生id碰撞，这个可以通过设置workerId来解决。</li>
</ol>
<p>实际使用中，时钟回拨概率很低，比较麻烦的通常是多服务的workerId配置问题：</p>
<ol>
<li>每一个应用的workerId都要不相同从而避免ID碰撞</li>
<li>多个应用之间workerId差异化配置难以集中管理</li>
<li>对于多实例的应用，可能除了workerId，其他配置都是相同的，但不得不想办法进行差异化配置、打包、发布。</li>
</ol>
<p>何不将每一个实例使用的workerId记录到一个地方？既方便管理，又能避免重复。</p>
<p>注册中心正好可以解决这个问题。</p>
<h2 id="快速实现">快速实现</h2>
<p>这小玩意儿咱设计简陋点，就包含三个主要功能：</p>
<ul>
<li>注册id</li>
<li>注销id</li>
<li>自动加载</li>
</ul>
<h3 id="注册id">注册id</h3>
<p>这里主要考虑两点：</p>
<ol>
<li>如何保证服务之间区分</li>
<li>如何确保服务注册的id是唯一</li>
</ol>
<blockquote>
<ol>
<li>如何保证服务之间区分</li>
</ol>
</blockquote>
<p>要能区分服务，即是要有唯一的服务标识，<br>
我选取的标识是：数据中心+ip+应用标识<br>
当然我也是可以用随机生成UUID就能保证唯一的服务标识，但使用上面这种有两个好处：</p>
<ol>
<li>幂等性：每次启动生成的标识是不变的</li>
<li>可读性：能从配置中心直接看到每个服务的配置状况。</li>
</ol>
<blockquote>
<ol start="2">
<li>如何确保服务注册的id是唯一</li>
</ol>
</blockquote>
<p>要保证注册id唯一，只需要在注册前检查所有注册值，如果已注册就换一个就行了。<br>
即：<img src="https://season8.github.io/post-images/1631030861531.png" alt="" loading="lazy"></p>
<p>但这还不够，假如两个应用贼有缘，同时启动，随到同一个id，同时检查 id 是否已使用并得到否定结果，然后两人就注册了同一个id！这就是一个典型的多线程一票多卖的问题，看来还是要用锁才行啊。</p>
<p>经过一番修改，它看起来是这样：<img src="https://season8.github.io/post-images/1631030878516.png" alt="" loading="lazy"></p>
<p>用分布式锁锁住id，将检查、注册这部分功能变成串行。</p>
<h3 id="注销id">注销id</h3>
<p>利用 @PreDestroy 注释一个destroy方法（名字不限哈） ，在服务中断（非 kill -9）时，调用该方法注销配置。</p>
<p>关于 @PreDestroy 原理 和 Spring Boot 应用中断，这个有空也得自说自话一下。</p>
<h3 id="自动装载">自动装载</h3>
<p>配置越少、越简单，才能用的方便不是，不然要被同事吐槽啦（👴：虽然这功能挺鸡肋，但它配置丰富啊🐶）</p>
<p>总结了一下，得实现以下4个自动化：</p>
<ol>
<li>yaml配置得有提示。</li>
<li>能根据不同类型配置中心配置自动装载配置中心服务。</li>
<li>无需额外的@ComponentScan、@Import 或java配置，自动装载。</li>
</ol>
<blockquote>
<ol>
<li>yaml配置得有提示。</li>
</ol>
</blockquote>
<p>使用 spring-boot-configuration-processor 生成 spring-configuration-metadata.json 文件</p>
<blockquote>
<ol start="2">
<li>能根据不同类型配置中心配置自动装载配置中心服务。</li>
</ol>
</blockquote>
<p>使用@ConditionalOnProperty、@ConditionalOnClass 即可做到</p>
<blockquote>
<ol start="3">
<li>无需额外的@ComponentScan、@Import 或java配置，自动装载。</li>
</ol>
</blockquote>
<p>创建  spring.factories 并打包即可</p>
<p>关于 Spring 自动装载 和 相关注解，emmm.. 下次再说道说道好了。</p>
<h3 id="缺陷">缺陷</h3>
<ol>
<li>不能响应强制中断，从而造成id抢占，如果此时该应用永久下线或者网络配置发生变化，就造成了id泄漏，产生无效的配置数据。</li>
<li>如果注册中心不可用时，将使用兜底的随机id方式，会有id碰撞风险。</li>
<li>不能即时修改生效。</li>
</ol>
<h3 id="项目地址">项目地址</h3>
<p><a href="https://github.com/season8/id-generators">id-generators</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[花里胡哨--TranslucentTB]]></title>
        <id>https://season8.github.io/post/translucenttb/</id>
        <link href="https://season8.github.io/post/translucenttb/">
        </link>
        <updated>2021-06-23T09:08:08.000Z</updated>
        <summary type="html"><![CDATA[<p>😎炫酷好用不折腾，颜值党必备神器。</p>
]]></summary>
        <content type="html"><![CDATA[<p>😎炫酷好用不折腾，颜值党必备神器。</p>
<!-- more -->
<p>名称：TranslucentTB<br>
官网：https://github.com/TranslucentTB/TranslucentTB<br>
用途：让windows任务栏透明<br>
获取途径：官网 或 Microsoft Store</p>
<h2 id="效果">效果：</h2>
<p>此处只展示常规场景下的效果：<br>
<img src="https://season8.github.io/post-images/1624440837720.png" alt="" loading="lazy"></p>
<p>##设置：</p>
<p><img src="https://season8.github.io/post-images/1624440202821.png" alt="" loading="lazy"><br>
一级菜单主要提供（自上往下）：<br>
<strong>常规</strong>、<br>
<strong>应用全屏</strong>、<br>
<strong>打开开始菜单</strong>、<br>
<strong>打开系统搜索</strong>、<br>
<strong>打开时间轴（win+tab）</strong><br>
各场景时任务栏颜色变化<br>
对应二级菜单可设置任务栏为：常规、透明、毛玻璃效果以及自定义颜色。</p>
<p>基本上常规设置为透明就很炫酷了，其他看自己喜好即可。<br>
另外，一定要打开设置里的 Open at boot(随系统启动)</p>
<p>设置一次，基本不用管了。</p>
<h2 id="多桌面-多屏支持情况">多桌面、多屏支持情况</h2>
<p>完美支持 多桌面(ALT+WIN+⬅️/➡️)<br>
支持多显示屏，偶尔会出现扩展屏不生效的情况，解决办法是直接再打开（不需要退出）TranslucentTB，即可生效。</p>
<h1 id="资源消耗">资源消耗：</h1>
<p>对cpu和内存都很友好：<br>
<img src="https://season8.github.io/post-images/1624441031325.png" alt="" loading="lazy"></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[重新认识 ThreadPoolExecutor]]></title>
        <id>https://season8.github.io/post/threadpoolexecutor/</id>
        <link href="https://season8.github.io/post/threadpoolexecutor/">
        </link>
        <updated>2021-03-18T14:41:15.000Z</updated>
        <summary type="html"><![CDATA[<p>❓ 更深入的理解<strong>ThreadPoolExecutor</strong>原理，纠正想当然的理解误区</p>
]]></summary>
        <content type="html"><![CDATA[<p>❓ 更深入的理解<strong>ThreadPoolExecutor</strong>原理，纠正想当然的理解误区</p>
<!-- more -->
<h1 id="一个事故引发的拷问">一个事故引发的拷问</h1>
<p>或许这篇文章可以起名更惊悚一点，叫做：《不合理的线程池配置酿成的血案。。》。<br>
事情要从一个造成生产事故的案例说起：</p>
<p>同事开发了一个kafka消费者程序，并发消费消息：</p>
<ul>
<li>使用Semaphore控制每次并发处理m个消息</li>
<li>每一个消息处理后都会生成 n 个任务，为了加快单个消息的处理速率，子任务也采用了并发的方式执行（消息和子任务线程池是分开的）</li>
<li>外层消息处理等待所有子任务执行完成才算完成。</li>
</ul>
<p>整个模型设计的目标很明确，那就是每次能并发处理m个消息，m*n个子任务，每组子任务全部执行完毕后才会开始下一组。</p>
<p>但真实的情况是，发布线上后，子任务线程池大面积触发Reject，导致消息处理几乎瘫痪，下游出现数据缺失以及高延迟。 在协同检查后，我也认为这个模型设计十分精确，不会存在子任务超量的情况。</p>
<h2 id="问题复现">问题复现</h2>
<p>由于该模型在本地测试并没有触发异常，考虑到是数据量级的原因，我基于现有模型，缩放了任务量级和配置，简化出了以下模型并复现了问题：</p>
<blockquote>
<p>假设有多个消费事件组，要求同时最多三个事件组异步消费，而每个事件组由3个子事件组成，子事件同样使用异步消费。</p>
</blockquote>
<pre><code class="language-java">public static void main(String[] args) throws InterruptedException {
        // @1 创建外部消费组线程池，固定大小3，队列197，一共200个消费组
        ThreadPoolExecutor outter = new ThreadPoolExecutor(3, 3, 30, TimeUnit.SECONDS, new LinkedBlockingQueue&lt;&gt;(197));
        
        // @2 创建内部事件处理线程池，core=9，max=12 ，队列长度为1。
        ThreadPoolExecutor inner = new ThreadPoolExecutor(9, 12, 30, TimeUnit.SECONDS, new LinkedBlockingQueue&lt;&gt;(1)); //2

        for (int i = 0; i &lt; 200; i++) {
            int group = i;

            // @3 消费组异步处理，由于outter固定大小为3，首次只有三个消费组消费事件。
            outter.execute(() -&gt; {

                System.out.println(&quot;开始第&quot; + group + &quot; 组消费&quot;);
                CountDownLatch countDownLatch = new CountDownLatch(3);

                for (int j = 0; j &lt; 3; j++) {
                    int task = j;

                    // @4 3个事件异步消费，
                    inner.execute(() -&gt; {
                        System.out.println(group + &quot;组消费数据:&quot; + task);
                        countDownLatch.countDown();
                    });
                }

                // @5 outter线程等待三个inner线程结束，当前组才消费完成
                try {
                    countDownLatch.await(3, TimeUnit.SECONDS);
                    System.out.println(&quot;第&quot; + group + &quot; 消费完成&quot;);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
</code></pre>
<p>实现该模型的基本策略：</p>
<ul>
<li>利用CountDownLatch保证outter线程在对应的三个inner线程执行完之后才执行完毕，即：保证批次执行；</li>
<li>利用outter固定线程池=3，保证每次最多只有三个outter线程执行；</li>
<li>同理，inner每次只能有 3（outter执行数量）* 3（事件数量） = 9 个线程在执行。所以理论上，inner的max=12是用不上的，队列也用不上。</li>
</ul>
<p>执行结果：</p>
<pre><code class="language-java">...
Exception in thread &quot;pool-1-thread-1&quot; Exception in thread &quot;pool-1-thread-3&quot; Exception in thread &quot;pool-1-thread-2&quot; java.util.concurrent.RejectedExecutionException: Task com.review.string.Demo7$$Lambda$2/1186543288@67dad061 rejected from java.util.concurrent.ThreadPoolExecutor@67aa5a39[Running, pool size = 12, active threads = 3, queued tasks = 0, completed tasks = 14]
...

</code></pre>
<p>可发发现，子任务线程池容量竟然扩充到了12，达到了最大容量，按前面所想，子任务数量应该是控制在3*3=9个才对。</p>
<h1 id="分析">分析</h1>
<h2 id="猜测countdownlatch没有控制住">猜测：countDownLatch没有控制住</h2>
<p>假设实际执行的outter active数量 * 3 &gt; 实际inner active线程数，从而导致inner线程达到最大线程数。</p>
<p>这里countDownLatch数量配置都是对的，增加inner消费处理时间，发现在reject前，outter线程都是在组内inner全部执行完才完成，没有失控的迹象。</p>
<blockquote>
<p>如果你的猜测是这个，我觉得你可以多多使用juc中的工具，这个模型中，countDownLatch和线程池的配合使用是没有问题的。</p>
</blockquote>
<h2 id="猜测线程调度的问题">猜测：线程调度的问题</h2>
<p>假设outter线程o1执行完，o2、o3还在执行，开始执行o4，此时如果o1组内的inner线程 i1、i2、i3至少一个还没有及时归还到线程池中，这样o4组消费就会创建新的线程使得线程数&gt;core,持续几轮，线程池撑满从而触发reject。</p>
<p>这个猜测并无根据，但依然可以验证一下:</p>
<pre><code class="language-java">  countDownLatch.await(); 
  TimeUnit.SECONDS.sleep(1); // outter强制等待inner结束
</code></pre>
<p>执行，仍然reject,可以排除该猜想</p>
<p>猜想都没有实际根据且都可以被证伪，我突然有些恐慌，比起在小同事这里翻车，我发现我根本没有理解线程池这件事才是超恐怖的。</p>
<p>我此时所认知的线程池：</p>
<ol>
<li>活跃线程数 &lt; corePoolSize 时，直接创建线程来执行任务。</li>
<li>活跃线程数 = corePoolSize 时，任务添加到队列，等待空闲线程处理。</li>
<li>队列满时，直接创建线程来执行任务。</li>
<li>活跃线程数 = maxPoolSize，继续添加任务，触发reject。</li>
</ol>
<p>通过后续的研究，我才发现，这几条简短的概括，并不能说错误，但是因为简短，说明有些概念并不明确，或者很模糊，正是这种似是而非的认识，导致我对线程池的具体细节都是想当然。</p>
<h1 id="终极武器看源码实现">终极武器，看源码实现：</h1>
<p>直接从核心方法 execute 入手：</p>
<pre><code class="language-java">public void execute(Runnable command) {
    ...
    int c = ctl.get();
	if (workerCountOf(c) &lt; corePoolSize) {             // @1    
	    if (addWorker(command, true))                  // @2
	        return;
	    c = ctl.get();
	}
	if (isRunning(c) &amp;&amp; workQueue.offer(command)) {   // @3
	    int recheck = ctl.get();
	    if (! isRunning(recheck) &amp;&amp; remove(command))  // @4  
	        reject(command);
	    else if (workerCountOf(recheck) == 0)         // @5
	        addWorker(null, false);
	}
	else if (!addWorker(command, false)) // @6
            reject(command);
}
</code></pre>
<p>这里主要有6个不太明确作用的方法，我们逐个分析：</p>
<ol>
<li>workerCountOf</li>
<li>isRunning</li>
<li>addWorker</li>
<li>workQueue.offer(command)</li>
<li>remove(command)</li>
<li>reject(command)</li>
</ol>
<h3 id="workercountof-isrunning">workerCountOf  &amp; isRunning</h3>
<p>了解这两个方法是ThreadPoolExecutor类的基本方法，我们直接看ThreadPoolExecutor的源码：</p>
<pre><code class="language-java">public class ThreadPoolExecutor extends AbstractExecutorService {
    /**
     * The main pool control state, ctl, is an atomic integer packing two conceptual fields
     *   workerCount, indicating the effective number of threads
     *   runState,    indicating whether running, shutting down etc
     *
     * 译：状态ctl是一个包装了两个概念字段原子整数:
     * 	    workerCount 指示有效的线程数,
     * 	    runState 指示是否运行，关闭等
     * ...
     *
     * The workerCount is the number of workers that have been
     * permitted to start and not permitted to stop.  The value may be
     * transiently different from the actual number of live threads,
     * for example when a ThreadFactory fails to create a thread when
     * asked, and when exiting threads are still performing
     * bookkeeping before terminating. The user-visible pool size is
     * reported as the current size of the workers set.
     * 
     *  译：workerCount是【已被允许启动且未被允许停止】的worder(即线程)数量。
     *  该值可能与实际的活动线程数暂时不同：线程池创建线程时失败但又未完全注销，
     *  此时workerCount 可能会小于线程池实际大小。
     *  
     *  【已被允许启动且未被允许停止】 先简单理解为存活线程比较好理解
     * ...
     * 
     * The runState provides the main lifecycle control, taking on values:
     *
     *   RUNNING:  Accept new tasks and process queued tasks
     *   SHUTDOWN: Don't accept new tasks, but process queued tasks
     *   STOP:     Don't accept new tasks, don't process queued tasks,
     *             and interrupt in-progress tasks
     *   TIDYING:  All tasks have terminated, workerCount is zero,
     *             the thread transitioning to state TIDYING
     *             will run the terminated() hook method
     *   TERMINATED: terminated() has completed
     *   
     *   译：runState提供线程池生命周期的控制，有以下状态值：
     *   RUNNING：可接受新任务，可处理队列中的任务
     *   SHUTDOWN：不接受新任务，但可以处理队列中的任务
     *   STOP：不接受新任务，不处理队列中任务，并中断处理中的任务
     *   TIDYING：所有任务都已终止，workerCount(存活的线程数量)为0,往该状态过渡的线程将执行terminated()
     *   TERMINATED: terminated()方法结束，相当于TIDYING的终态。
     *   ...
     *   
     *   后面还有部分注释，讲的是runState 各个状态之间转换条件，不作列出，有兴趣可自研
     * /

    // 状态和线程数集合包装字段ctl。
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    
    private static int runStateOf(int c)     { return c &amp; ~CAPACITY; }  // 从 ctl 拆解出 runState
    
    private static int workerCountOf(int c)  { return c &amp; CAPACITY; } // 从 crl 拆解出 workerCount
    
    private static int ctlOf(int rs, int wc) { return rs | wc; } // 将 workerCount 和 runState 包装为 ctl
    ...
    
    private static boolean isRunning(int c) { return c &lt; SHUTDOWN;} // 当前线程池状态，是否处于Running状态
</code></pre>
<blockquote>
<p>ctl包装和解析 runState 与 workCount 的算法并未理解，但并不影响理解其概念。</p>
</blockquote>
<ul>
<li>workerCountOf方法 ：获取存活线程数量</li>
<li>isRunning方法 ：判断线程池状态是否处于Running(Running状态时线程池可接受新任务)</li>
</ul>
<h3 id="addworker">addWorker</h3>
<pre><code class="language-java">private boolean addWorker(Runnable firstTask, boolean core) {
	//...（略掉一些状态校验）
	
    int wc = workerCountOf(c); // 线程池实际容量（存活线程数量）
    if (wc &gt;= CAPACITY || wc &gt;= (core ? corePoolSize : maximumPoolSize))
    return false;  // @1 判断是否达到容量上限
    
    //...（略掉一些状态校验）
	
    w = new Worker(firstTask);  // @1 根据任务创建Worker对象
    final Thread t = w.thread; 
    if (t != null) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock(); 
        try {
            
            int rs = runStateOf(ctl.get());

            if (rs &lt; SHUTDOWN ||
                (rs == SHUTDOWN &amp;&amp; firstTask == null)) {
                if (t.isAlive()) // precheck that t is startable
                    throw new IllegalThreadStateException();

                workers.add(w); // @2 将worker对象添加到集合workers中

                int s = workers.size();
                if (s &gt; largestPoolSize)
                    largestPoolSize = s;
                workerAdded = true;
            }
        } finally {
            mainLock.unlock();
        }
        if (workerAdded) {
            t.start(); // @3 添加成功，启动worker内置线程
            workerStarted = true;
        }
    }
    //... 
}
</code></pre>
<p>addWorker在execute方法中有两处调用，区别是参数core传值不同，从源码可以看出是判断当前线程池容量上限是CorePoolSize还是MaxPoolSize</p>
<p>addWorker并不是直接创建线程，而是Worker对象，传入的Runnable任务对象作为其属性。</p>
<p>代码 @2 处 workers就是线程池的集合：</p>
<pre><code class="language-java">   /**
    * Set containing all worker threads in pool. Accessed only when holding mainLock.
    * 
    * 译：一个包含线程池内所有工作线程的集合，仅在持有mainLock时可访问
    */
   private final HashSet&lt;Worker&gt; workers = new HashSet&lt;Worker&gt;();
</code></pre>
<p>Worker就是实际线程池中的“线程”：</p>
<pre><code class="language-java">private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    
    final Thread thread; // 当前worker实际运行的线程
    
    Runnable firstTask; // 初始化的任务

    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this); // 创建线程，注意此处将worker作为Thread的构造参数
    }

    public void run() {
        runWorker(this);
    }
}

</code></pre>
<p>Worker作为Runnable实现类，并拥有一个Thread属性</p>
<p>从其构造器还可以发现，thread属性的target又是当前worker对象</p>
<p>上面addWorker方法的 @3 处显示Worker创建后会启动Worker内置的Thread对象,</p>
<p>这意味着，thread.start() 实际会调用worker.run(),而run内部又是调用runWorker方法，其源码为：</p>
<pre><code class="language-java">final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) { // @4 无限循环获取任务
            ...
            try {
                beforeExecute(wt, task); // @5 protected修饰的空方法，可用于线程池子类扩展
                Throwable thrown = null;
                try {
                    task.run(); // @6 调用实际Runnable实例的run方法
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown); // @7 protected修饰的空方法，可用于线程池子类扩展
                }
            } finally {
                task = null; // @8 每个task执行完成后，置空变量，下次再拿到一个新的task
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly); 
    }
}
</code></pre>
<p>@5 、@7处是空的protected方法，很明显是用来提供子类扩展的</p>
<p>@4 、@6、@8 处揭露了Worker在启动之后，会无限循环通过getTask获取Runnable任务，并调用任务的run()方法。</p>
<p>getTask() 方法源码：</p>
<pre><code class="language-java">private Runnable getTask() {
...(省略若干行状态校验代码)
	try {
        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>可以到，实际是调用队列的poll或take来获取任务，这两个方法都是阻塞的，没有取到任务就会阻塞住，区别只是pool有超时时间。</p>
<p>综上，可以总结出 addWorker方法作用：创建线程Worker对象，并启动线程，线程内会无限循环的从队列中带阻塞的获取执行任务。</p>
<h2 id="再回到execute方法">再回到execute方法：</h2>
<pre><code class="language-java">public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) &lt; corePoolSize) {             // @1    
        if (addWorker(command, true))                  // @2
            return;
        c = ctl.get();
    }
    if (isRunning(c) &amp;&amp; workQueue.offer(command)) {   // @3
        int recheck = ctl.get();
        if (! isRunning(recheck) &amp;&amp; remove(command))  // @4  
            reject(command);
        else if (workerCountOf(recheck) == 0)         // @5
            addWorker(null, false);
    }
    else if (!addWorker(command, false)) // @6
            reject(command);
}
</code></pre>
<ul>
<li>@1：存活Worker数量是否小于corePoolSize。</li>
<li>@2：创建并启动Worker，上限为corePoolSize，Worker执行完初始化时分配任务后，无限循环从队列有阻塞的获取任务。</li>
<li>@3：存活Worker数量 = corePoolSize，线程池状态为Running（可接受新任务），且将新任务加入队列.</li>
<li>@4：二次检查线程池状态，非Runing时，将加入的任务移除。</li>
<li>@5：二次检查线程池状态依然为Running，且存活Worker数量 = 0（因为corePoolSize可能为0），创建并启动Worker，上限为MaxPoolSize</li>
<li>@6: 线程池状态部位Running或队列添加失败（队列满），创建并启动Worker（内部有状态校验），上限为MaxPoolSize</li>
</ul>
<h1 id="结论">结论</h1>
<p>基于以上分析，发现误区有两点：</p>
<ol>
<li>线程池添加超出corePoolSize线程时，依据活跃线程数量来判断，这个活跃线程和当前处理的任务数量没有任何关系，上面说到理解成存活Worker数量是比较贴切的，而Worker启动后是无限循环读取队列的，所以 活跃的线程数 = 存活线程数 != 活跃（处理中）任务数。</li>
<li>任务提交给线程池，如果线程池有空闲线程，那么新加入的任务是能够被空闲线程（未处理任务的线程）处理的，但容易先入为主的认为，任务直接分配给空闲线程的，实际除了创建线程时给的任务，其它的任务是先放到队列中的，线程和队列是一个生产者消费者模型。</li>
</ol>
<p>案例复盘：</p>
<ol>
<li>当有3个outter线程处理时，inner线程池累计创建了9（corePoolSize）个Worker</li>
<li>后续任务到达时，任务将直接入inner队列，由于队列大小为1，线程从队列消费任务及时性无法保证 ① ，队列入队可能失败</li>
<li>队列入队失败时，inner继续创建线程，知道总数量为12（maxPoolSize）</li>
<li>后续任务到达，再次入队，同步骤 2，此时一旦入队失败，就会触发reject</li>
</ol>
<blockquote>
<p>① 事实上，线程调用 queue.take() 和 外部调用queue.offer()间没有任何关联的，线程之间是由竞争的，无法保证及时消费队列中任务是可能的。</p>
</blockquote>
<h1 id="验证">验证</h1>
<blockquote>
<p>既然已经明白可能的误区，针对误区简化案例即可</p>
</blockquote>
<pre><code>static class MyLinkedBlockingQueue&lt;E&gt; extends LinkedBlockingQueue&lt;E&gt; {
    public MyLinkedBlockingQueue(int capacity) {
      super(capacity);
    }

    @Override
    public boolean offer(E o) {
      System.out.println(&quot;任务加入，当前队列数：&quot; + this.size());
      return super.offer(o);
    }
}
  
public static void main(String[] args) throws InterruptedException {
    BlockingQueue queue = new MyLinkedBlockingQueue&lt;&gt;(1);
  
    // 3个线程的线程池
    ThreadPoolExecutor taskPoolExecutor = new ThreadPoolExecutor(3, 3, 30,       TimeUnit.SECONDS, queue);
  
    // 先将线程池拉满
    for (int i = 0; i &lt; 3; i++) {
      	final int finalI = i;
      	taskPoolExecutor.execute(() -&gt; {
        	logger.info(&quot;{}&quot;, finalI);
      	});
    }
  
    // 等待全部任务执行完
    Thread.sleep(1000);
  
    // 再次执行任务，发现每一个任务都触发加入队列操作。
    for (int i = 10; i &lt; 12; i++) { // @1
    for (int i = 10; i &lt; 15; i++) { // @2
      	final int finalI = i;
      	taskPoolExecutor.execute(() -&gt; { 
            // @3
      	    /*
        	try {
          		TimeUnit.SECONDS.sleep(1);
        	} catch (InterruptedException e) {
          		e.printStackTrace();
    		}
            */
            System.out.println(finalI);
    }
  }
</code></pre>
<p>执行结果：</p>
<pre><code>0
2
1
任务加入，当前队列数：0
任务加入，当前队列数：1
10
11
</code></pre>
<p>可以看到，线程池满了之后，哪怕线程全部空闲，新的任务也是放到队列中。</p>
<p>将上述代码中的@1 换成 @2 ，去掉 @3处的注释，使得每个任务耗时更久且要添加任务数大于corePoolSize，这样队列就会来不及出列从而触发reject。<br>
执行结果：</p>
<pre><code>2
0
1
任务加入，当前队列数：0
任务加入，当前队列数：1
任务加入，当前队列数：0
任务加入，当前队列数：1
任务加入，当前队列数：1
Exception in thread &quot;main&quot; java.util.concurrent.RejectedExecutionException: Task com.review.string.Demo8$$Lambda$2/249515771@2f7a2457 rejected from java.util.concurrent.ThreadPoolExecutor@566776ad[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 3]
</code></pre>
<p>完美验证！</p>
<h1 id="总结">总结</h1>
<p>ThreadPoolExecutor其实并不神秘，就是一个生产消费模型,特殊点：</p>
<ol>
<li>在线程池到达corePoolSize大小时，任务是直接分配给新线程的。</li>
<li>在线程池到达corePoolSize大小且添加队列满,任务可以直接分配给新线程，直到达到maxPoolSize大小，相当于生产者生产的事件满了之后，给消费者一次扩容的机会。</li>
<li>其它时候队列未满时，添加任务相当于生产事件到队列，线程从队列消费事件，当队列满时，触发reject。</li>
</ol>
<p>所以实际配置中，除了线程池大小，队列大小也要参考并发量合理设置。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[HTTP 进化简史]]></title>
        <id>https://season8.github.io/post/http/</id>
        <link href="https://season8.github.io/post/http/">
        </link>
        <updated>2021-03-17T13:52:23.000Z</updated>
        <summary type="html"><![CDATA[<p>八股八股HTTP基础</p>
]]></summary>
        <content type="html"><![CDATA[<p>八股八股HTTP基础</p>
<!-- more -->
<h2 id="本文主要概括以下内容">本文主要概括以下内容：</h2>
<ol>
<li>什么是HTTP。</li>
<li>HTTP 和 TCP 关系。</li>
<li>HTTP 的效率受哪些因素影响。</li>
<li>HTTP 主要版本极其特性。</li>
<li>什么是HTTPS，和HTTP的区别是什么。</li>
</ol>
<h2 id="什么是http协议">什么是HTTP协议</h2>
<p>超文本传输协议（HTTP：Hypertext Transport Protocol）是建立在TCP协议上的应用层协议，客户端和服务端通过建立连接，发送请求和响应报文，完成数据请求和响应。</p>
<p><strong>白话</strong>：HTTP 定义了数据的格式、数据处理方式 以及 连接的使用方式，而连接的建立和释放是由TCP协议控制的。</p>
<h2 id="http-和-tcp-关系">HTTP 和 TCP 关系</h2>
<p><strong>TCP</strong> 是传输层协议，建立TCP连接需要三次握手，释放连接需要四次挥手，因此，TCP是可靠的连接，TCP连接是有状态的，只要保持连接不释放，可以传输多组数据。<br>
<strong>HTTP</strong>是应用层协议，约定了请求行格式和响应格式，约定了关键字，在HTTP1.0中，一个HTTP响应结束后会关闭TCP连接，从而产生了无状态的特性。</p>
<h2 id="影响http请求的因素">影响HTTP请求的因素</h2>
<ol>
<li>带宽</li>
<li>浏览器阻塞造成的延迟。浏览器对同一个域名同时只能有4个连接（不同内核可能不同），超过的连接被阻塞。</li>
<li>DNS 解析造成的延迟。浏览器只有知道请求的服务器ip后才会发起请求。</li>
<li>建立连接造成的延迟。HTTP请求连接需要3次握手。</li>
<li>关闭连接造成的延迟。HTTP连接关闭需要4次挥手，当浏览器需要创建多个连接时，关闭连接耗时会造成后续请求阻塞时间的延长。</li>
</ol>
<h2 id="http主要版本极其特性">HTTP主要版本极其特性</h2>
<h3 id="http10">HTTP1.0</h3>
<ol>
<li>无连接/短连接，一个连接只处理一个请求，每一次请求都要经历创建、关闭连接。</li>
<li>无状态，对于事务处理没有记忆能力，后续请求如果要用到前序请求的数据、状态，则必须重传这部分数据和状态。</li>
<li>请求不含主机名（hostname）,HTTP1.0中认为每台服务器都绑定一个唯一的IP地址。</li>
<li>灵活的数据类型，通过请求头Content-Type字段可以支持多种数据结构。</li>
<li>简单快速：请求只需要url、method、param/post-data即可完成请求，协议简单易用，响应迅速。</li>
</ol>
<ul>
<li>问：怎么解决无状态下状态持久化问题？</li>
<li>答：客户端方案：cookie、localStorage，服务端方案：session</li>
</ul>
<h3 id="http11">HTTP1.1</h3>
<ol>
<li>新增缓存字段 和 错误状态码</li>
<li>Host头处理,增加hostname,解决一台机器多个虚拟主机的情况。</li>
<li>支持长连接，一个TCP连接可以多次发送HTTP请求，默认开启Connection： keep-alive</li>
</ol>
<h3 id="http20">HTTP2.0</h3>
<blockquote>
<p>[科普]TCP慢启动：TCP 连接会随着时间进行自我「调谐」，起初会限制连接的最大速度，如果数据成功传输，会随着时间的推移提高传输的速度</p>
</blockquote>
<ol>
<li>
<p>多路复用。利用二进制分栈技术，实现单连接多资源，减少服务端的链接压力,内存占用更少,连接吞吐量更大；减少TCP 慢启动时间，提高传输的速度。</p>
</li>
<li>
<p>首部压缩。浏览器和服务端都可以向动态字典中添加键值对，之后这个键值对就可以使用一个字符表示。</p>
<ol>
<li>维护一份相同的静态字典（Static Table），包含常见的头部名称，以及特别常见的头部名称与值的组合；</li>
<li>维护一份相同的动态字典（Dynamic Table），可以动态的添加内容；</li>
<li>支持基于静态哈夫曼码表的哈夫曼编码（Huffman Coding）；</li>
</ol>
</li>
<li>
<p>服务器推送，服务器知道浏览器需要加载附加资源时，在响应第一个请求之后，可以主动推送附加资源，充分利用网络空闲资源。</p>
</li>
</ol>
<h3 id="https和http">HTTPS和HTTP</h3>
<blockquote>
<p>超文本传输安全协议（HTTPS: Hypertext Transfer Protocol Secure：）是基于HTTP协议的安全通信协议，加密方式是 SSL/TLS。<br>
HTTPS 开发的主要目的，是提供对网站服务器的身份认证，保护交换数据的隐私与完整性。</p>
</blockquote>
<p>HTTPS 默认工作在 TCP 协议443端口，它的工作流程一般如以下方式：</p>
<ol>
<li>TCP 三次同步握手</li>
<li>客户端验证服务器数字证书</li>
<li>DH 算法协商对称加密算法的密钥、hash 算法的密钥</li>
<li>SSL 安全加密隧道协商完成</li>
<li>网页以加密的方式传输，用协商的对称加密算法和密钥加密，保证数据机密性；用协商的hash算法进行数据完整性保护，保证数据不被篡改。</li>
</ol>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[久违的开始]]></title>
        <id>https://season8.github.io/post/hello/</id>
        <link href="https://season8.github.io/post/hello/">
        </link>
        <updated>2021-03-12T02:35:39.000Z</updated>
        <summary type="html"><![CDATA[<p>👏  欢迎来到我的<strong>小栈</strong> ！<br>
✍️  <strong>小栈</strong>  并不限于技术分享。</p>
]]></summary>
        <content type="html"><![CDATA[<p>👏  欢迎来到我的<strong>小栈</strong> ！<br>
✍️  <strong>小栈</strong>  并不限于技术分享。</p>
<!-- more -->
<p><a href="https://github.com/season8">Github</a><br>
<a href="https://season8.github.io">小栈 主页</a></p>
<p>🤔总是想太多，一来没有拿得出手的吊炸天的技术，二来没有敢说精通的技能，三来写完觉得写的不好。</p>
<p>😵纠结就是拖延的开始，所以辗转一番，发现还是不能拘泥于内容、深度。明明就是一个自嗨的东西就不要总是端着一副写点什么给科学家看了学去好拯救世界的心态。</p>
<p>😁这样就豁然开朗了，没人看是常态，能写出来的东西，首先要自我满足，倘若有他人看了，能再提供一份喜闻乐见都是好的。</p>
<h2 id="写什么">写什么👇</h2>
<p>📝  技术总结，主要是Java 后端的一些东西，八股肯定是会八股的🤣</p>
<p>💻  站点、小工具等软件使用分享，没用但很舒服的瞎折腾。</p>
<p>🏡  家庭、育儿等生活方式以及一些思考。</p>
<p>📋</p>
<h2 id="怎么写">怎么写</h2>
<h3 id="金字塔法则">金字塔法则</h3>
<p>🌟  在工作中我接触了金字塔法则，这是一种用于提高书面以及口头表述能力的方法。作为技术人员，这个方法用于写作是再合适不过了。</p>
<p>1.金字塔法则的基本结构是：<br>
1）结论先行；<br>
2）以上统下；<br>
3）归类分组；<br>
4）逻辑递进。</p>
<p>2.基本规则是：<br>
1）先重要后次要；<br>
2）先总结后具体；<br>
3）先框架后细节；<br>
4）先结论后原因；<br>
5）先结果后过程；<br>
6）先结论后论据。</p>
<p>3.具体做法：<br>
1）自上而下表达、自下而上思考；<br>
2）纵向总结概括、横向归纳分组；<br>
3）序言讲故事、标题提炼精华。</p>
<h3 id="5w2h分析法">5W2H分析法</h3>
<p>🌟  这是从别的作者那里学到的技巧，这应该是更适合于技术人员的写作三板斧了。</p>
<ol>
<li>WHAT——目的、概念、原理是什么？</li>
<li>WHY——为什么要做？</li>
<li>WHO——由谁或者说哪个角色来做？</li>
<li>WHEN——什么时间做？什么时机最适宜？</li>
<li>WHERE——何处？在哪里做？</li>
<li>HOW ——怎么做？如何提高效率？如何实施？方法是什么？</li>
<li>HOW MUCH——多少？做到什么程度？数量如何？质量水平如何？费用产出如何？</li>
</ol>
<h3 id="关于辞藻">关于辞藻</h3>
<p>尝试过对刚写的文章进行辞藻加工，实在难以为继，我的幽默用不到文章上，所以，先表达清楚，写得多了，应该会有提高。</p>
<h3 id="最后">最后</h3>
<p>少BB，开始吧！</p>
<p>😘 Enjoy Myself~</p>
]]></content>
    </entry>
</feed>