<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>CodeTime</title>
  
  <subtitle>Just do IT</subtitle>
  <link href="https://www.xdull.cn/atom.xml" rel="self"/>
  
  <link href="https://www.xdull.cn/"/>
  <updated>2026-07-22T15:15:28.789Z</updated>
  <id>https://www.xdull.cn/</id>
  
  <author>
    <name>兜兜转转</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>一个模板搞定各种背包问题</title>
    <link href="https://www.xdull.cn/bag-problem.html"/>
    <id>https://www.xdull.cn/bag-problem.html</id>
    <published>2025-07-03T11:18:44.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>背包问题实际上是动态规划的一种经典应用，本文想通过介绍一种模板用于解决各种背包问题。</p><ul><li>模板统一采用二维数组来表示动态规划的状态，其中<strong>行</strong>表示物品的价值（或体积、大小、重量等），<strong>列</strong>表示背包的容量（或目标值）。</li><li>行和列都增加了一位，即从<code>1</code>开始计数，一来可以避免边界检查，二来<code>dp[...][target]</code>变得有意义，而非<code>dp[...][target-1]</code>，具体值和数组索引能够对上。根据不同题意来初始化<code>dp</code>，需要注意的是只要涉及到取物品的值，我们需要对索引减1，因为我们是从1开始计数的。</li></ul><p>接下来分三种情况考虑动态规划中的状态转移表达式：</p><ol><li><p>完全背包（每件物品可以无限使用）：<code>dp[i][j]=OPERATE(dp[i-1][j],dp[i][j-objs[i-1]])</code>。这里的<code>OPERATE</code>可以是<code>sum</code>、<code>max</code>、<code>or</code>视具体的问题而定。第二个表达式的索引是<code>i</code>。<br> <img src="/postimgs/bag-problem/1.jpg" alt="完全背包问题状态转移图示"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">bag_problem</span>(<span class="hljs-params">self, bag: <span class="hljs-built_in">int</span>, objs: List[<span class="hljs-built_in">int</span>]</span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(objs)+<span class="hljs-number">1</span>  <span class="hljs-comment">#将物品数量增加1，避免边界检查</span><br>    <span class="hljs-comment">#以下两行初始化dp状态</span><br>    dp=[[xxx]*(bag+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=xxx<br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(bag+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=objs[i-<span class="hljs-number">1</span>]:  <span class="hljs-comment">#涉及到物品的索引都需要减1</span><br>                dp[i][j]=operate(dp[i-<span class="hljs-number">1</span>][j],dp[i][j-objs[i-<span class="hljs-number">1</span>]])  <span class="hljs-comment">#注意第2项是i</span><br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure></li><li><p>01背包（每件物品只可使用一次）：<code>dp[i][j]=OPERATE(dp[i-1][j],dp[i-1][j-objs[i-1]])</code>，注意和完全背包问题的区别，第二个表达式的索引是<code>i-1</code>。<br> <img src="/postimgs/bag-problem/2.jpg" alt="01背包问题状态转移图示"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">bag_problem</span>(<span class="hljs-params">self, bag: <span class="hljs-built_in">int</span>, objs: List[<span class="hljs-built_in">int</span>]</span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(objs)+<span class="hljs-number">1</span>  <span class="hljs-comment">#将物品数量增加1，避免边界检查</span><br>    <span class="hljs-comment">#以下两行初始化dp状态</span><br>    dp=[[xxx]*(bag+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=xxx<br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(bag+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=objs[i-<span class="hljs-number">1</span>]:  <span class="hljs-comment">#涉及到物品的索引都需要减1</span><br>                dp[i][j]=operate(dp[i-<span class="hljs-number">1</span>][j],dp[i-<span class="hljs-number">1</span>][j-objs[i-<span class="hljs-number">1</span>]])  <span class="hljs-comment">#注意第2项是i-1</span><br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure></li><li><p>完全背包排列：<code>dp[i][j]=OPERATE(dp[i-1][j],dp[-1][j-objs[i-1]])</code>，第二个表达式的索引是<code>-1</code>，也即最后一行。</p><p>遍历行时的顺序也即访问每件物品的顺序，虽然物品顺序不作限制，但这里有个隐藏条件，遍历过的物品是不会再次访问的，也就是说，选择物品是组合问题，无论先选哪个后选哪个，只要都被选了，只能算作一种状态。<br>这里归到第3类的问题仍然归属于<strong>完全背包</strong>范畴，但是它是求排列而非组合，即先拿A或者先拿B，或者拿了A，B之后还可以再次拿A。<br>在前面的2类问题中，我们<strong>优先行</strong>或者<strong>优先列</strong>遍历对结果来说并没有影响，但在这里由于状态转移方程中每次都需最后一行参与计算，所以<strong>必须优先列遍历</strong>。<br><img src="/postimgs/bag-problem/3.jpg" alt="完全背包排列问题状态转移图示"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">bag_problem</span>(<span class="hljs-params">self, bag: <span class="hljs-built_in">int</span>, objs: List[<span class="hljs-built_in">int</span>]</span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(objs)+<span class="hljs-number">1</span>  <span class="hljs-comment">#将物品数量增加1，避免边界检查</span><br>    <span class="hljs-comment">#以下两行初始化dp状态</span><br>    dp=[[xxx]*(bag+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=xxx  <br>    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(bag+<span class="hljs-number">1</span>):   <span class="hljs-comment">#内外循环颠倒,按列遍历</span><br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=objs[i-<span class="hljs-number">1</span>]:  <span class="hljs-comment">#涉及到物品的索引都需要减1</span><br>                dp[i][j]=operate(dp[i-<span class="hljs-number">1</span>][j],dp[-<span class="hljs-number">1</span>][j-objs[i-<span class="hljs-number">1</span>]]  <span class="hljs-comment">#第2项是-1，表示最后一行</span><br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure></li></ol><p>下面，让我们使用该模板来解决力扣上的各种背包问题。</p><blockquote><p>现在问题的关键就在于把实际问题抽象化为背包问题中的哪一类，然后套用模板即可。<br>在以下问题中，模板中的二维数组均可优化为一维数组以降低空间复杂度。不用加重记忆负担去考虑什么时候该顺序遍历，什么时候该逆序遍历，以及行列遍历顺序能不能颠倒。当你了解了这个模板的含义，知道动态规划是如何在二维数组上实现的，这些问题都可以迎刃而解。</p></blockquote><h1 id="完全背包"><a href="#完全背包" class="headerlink" title="完全背包"></a>完全背包</h1><p>从n种物品中任选，<strong>每种物品可以无限取用</strong></p><h2 id="方案数"><a href="#方案数" class="headerlink" title="方案数"></a>方案数</h2><h3 id="518-零钱兑换-II"><a href="#518-零钱兑换-II" class="headerlink" title="518. 零钱兑换 II"></a><a href="https://leetcode-cn.com/problems/coin-change-2/">518. 零钱兑换 II</a></h3><p>给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。</p><p><strong>示例 1:</strong></p><blockquote><p>输入: amount = 5, coins = [1, 2, 5]<br>输出: 4<br>解释: 有四种方式可以凑成总金额:<br>5=5<br>5=2+2+1<br>5=2+1+1+1<br>5=1+1+1+1+1</p></blockquote><p><strong>示例 2:</strong></p><blockquote><p>输入: amount = 3, coins = [2]<br>输出: 0<br>解释: 只用面额2的硬币不能凑成总金额3。</p></blockquote><p><strong>示例 3:</strong></p><blockquote><p>输入: amount = 10, coins = [10]<br>输出: 1</p></blockquote><p>问题转换：从n种物品中任选，<strong>每种物品可以无限取用</strong>，<strong>恰好可以装满</strong>背包的<strong>组合数</strong>是多少？</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span>(<span class="hljs-params">self, amount: <span class="hljs-built_in">int</span>, coins: List[<span class="hljs-built_in">int</span>]</span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(coins)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-number">0</span>]*(amount+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-number">1</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(amount+<span class="hljs-number">1</span>):<br>            dp[i][j]+=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=coins[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment"># dp[i][j]=sum(dp[i-1][j],dp[i][j-objs[i-1]])</span><br>                dp[i][j]+=dp[i][j-coins[i-<span class="hljs-number">1</span>]]<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h2 id="特例：求排列非组合"><a href="#特例：求排列非组合" class="headerlink" title="特例：求排列非组合"></a>特例：求排列非组合</h2><h3 id="377-组合总和-Ⅳ"><a href="#377-组合总和-Ⅳ" class="headerlink" title="377. 组合总和 Ⅳ"></a><a href="https://leetcode-cn.com/problems/combination-sum-iv/">377. 组合总和 Ⅳ</a></h3><p>给你一个由<strong>不同</strong>整数组成的数组<code>nums</code>，和一个目标整数<code>target</code>。请你从<code>nums</code>中找出并返回总和为<code>target</code>的元素组合的个数。<br>题目数据保证答案符合 32 位整数范围。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：nums = [1,2,3], target = 4<br>输出：7<br>解释：<br>所有可能的组合为：<br>(1, 1, 1, 1)<br>(1, 1, 2)<br>(1, 2, 1)<br>(1, 3)<br>(2, 1, 1)<br>(2, 2)<br>(3, 1)<br>请注意，顺序不同的序列被视作不同的组合。</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入：nums = [9], target = 3<br>输出：0</p></blockquote><p>问题转换：和完全背包的恰好装满问题一样，只不过是求排列数而非组合数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">combinationSum4</span>(<span class="hljs-params">self, nums: List[<span class="hljs-built_in">int</span>], target: <span class="hljs-built_in">int</span></span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(nums)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-number">0</span>]*(target+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-number">1</span><br>    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(target+<span class="hljs-number">1</span>):   <span class="hljs-comment">#内外循环颠倒,按列遍历</span><br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>            dp[i][j]+=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=nums[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment">#这里是-1，不是i-1。dp[i][j]=sum(dp[i-1][j],dp[-1][j-objs[i-1]])</span><br>                dp[i][j]+=dp[-<span class="hljs-number">1</span>][j-nums[i-<span class="hljs-number">1</span>]]<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h2 id="最值"><a href="#最值" class="headerlink" title="最值"></a>最值</h2><h3 id="322-零钱兑换"><a href="#322-零钱兑换" class="headerlink" title="322. 零钱兑换"></a><a href="https://leetcode-cn.com/problems/coin-change/">322. 零钱兑换</a></h3><p>给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回 -1。<br>你可以认为每种硬币的数量是无限的。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：coins = [1, 2, 5], amount = 11<br>输出：3<br>解释：11 = 5 + 5 + 1</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入：coins = [2], amount = 3<br>输出：-1</p></blockquote><p><strong>示例 3：</strong></p><blockquote><p>输入：coins = [1], amount = 0<br>输出：0</p></blockquote><p><strong>示例 4：</strong></p><blockquote><p>输入：coins = [1], amount = 1<br>输出：1</p></blockquote><p><strong>示例 5：</strong></p><blockquote><p>输入：coins = [1], amount = 2<br>输出：2</p></blockquote><p>问题转换：完全背包恰好装满，所需最少物品数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">coinChange</span>(<span class="hljs-params">self, coins: List[<span class="hljs-built_in">int</span>], amount: <span class="hljs-built_in">int</span></span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(coins)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-number">0</span>]*(amount+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,amount+<span class="hljs-number">1</span>):  <span class="hljs-comment">#初始化为极值</span><br>        dp[<span class="hljs-number">0</span>][j]=<span class="hljs-built_in">float</span>(<span class="hljs-string">&#x27;inf&#x27;</span>)<br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(amount+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j] <span class="hljs-comment">#求dp[i-1][j]和dp[i][j-coins[i-1]]+1的较小值，这里是赋值而非累加</span><br>            <span class="hljs-keyword">if</span> j&gt;=coins[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment"># dp[i][j]=min(dp[i-1][j],dp[i][j-objs[i-1]])</span><br>                dp[i][j]=<span class="hljs-built_in">min</span>(dp[i][j],dp[i][j-coins[i-<span class="hljs-number">1</span>]]+<span class="hljs-number">1</span>)<br>    <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span> <span class="hljs-keyword">if</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]==<span class="hljs-built_in">float</span>(<span class="hljs-string">&#x27;inf&#x27;</span>) <span class="hljs-keyword">else</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h3 id="279-完全平方数"><a href="#279-完全平方数" class="headerlink" title="279. 完全平方数"></a><a href="https://leetcode-cn.com/problems/perfect-squares/">279. 完全平方数</a></h3><p>给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, …）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。<br>给你一个整数 n ，返回和为 n 的完全平方数的<strong>最少数量</strong>。<br><strong>完全平方数</strong>是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：n = 12<br>输出：3<br>解释：12 = 4 + 4 + 4</p></blockquote><p>示例 2：</p><blockquote><p>输入：n = 13<br>输出：2<br>解释：13 = 4 + 9</p></blockquote><p>问题转换：完全背包恰好装满，所需最少物品数。把正整数n抽象化为背包容量，从1开始的正整数抽象化为物品，其重量是值的平方。它的平方不能超过n，所以可以确定数字的上限。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">numSquares</span>(<span class="hljs-params">self, n: <span class="hljs-built_in">int</span></span>) -&gt; int:</span><br>    m=math.floor(n**<span class="hljs-number">0.5</span>)  <span class="hljs-comment">#最大物品的值</span><br>    dp=[[<span class="hljs-built_in">float</span>(<span class="hljs-string">&#x27;inf&#x27;</span>)]*(n+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m+<span class="hljs-number">1</span>)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-number">0</span>        <br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,m+<span class="hljs-number">1</span>):            <br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=i**<span class="hljs-number">2</span>:<br>                dp[i][j]=<span class="hljs-built_in">min</span>(dp[i-<span class="hljs-number">1</span>][j],dp[i][j-i**<span class="hljs-number">2</span>]+<span class="hljs-number">1</span>)<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><blockquote><p>注意：python语言采用此方法提交会超出时间限制，一个更好的办法是采用数学来解此题，会用到一个数学定理：<a href="https://baike.baidu.com/item/%E5%9B%9B%E5%B9%B3%E6%96%B9%E5%92%8C%E5%AE%9A%E7%90%86">四平方和定理</a>。</p></blockquote><h3 id="1449-数位成本和为目标值的最大数字"><a href="#1449-数位成本和为目标值的最大数字" class="headerlink" title="1449. 数位成本和为目标值的最大数字"></a><a href="https://leetcode-cn.com/problems/form-largest-integer-with-digits-that-add-up-to-target/">1449. 数位成本和为目标值的最大数字</a></h3><p>给你一个整数数组<code>cost</code>和一个整数<code>target</code>。请你返回满足如下规则可以得到的<strong>最大</strong>整数：</p><ul><li><p>给当前结果添加一个数位（<code>i + 1</code>）的成本为<code>cost[i]</code>（<code>cost</code> 数组下标从 0 开始）。</p></li><li><p>总成本必须恰好等于<code>target</code>。</p></li><li><p>添加的数位中没有数字 0 。</p></li></ul><p>由于答案可能会很大，请你以字符串形式返回。<br>如果按照上述要求无法得到任何整数，请你返回 “0” 。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：cost = [4,3,2,5,6,7,2,5,5], target = 9<br>输出：”7772”<br>解释：添加数位 ‘7’ 的成本为 2 ，添加数位 ‘2’ 的成本为 3 。所以 “7772” 的代价为 2<em>3+ 3</em>1 = 9 。 “977” 也是满足要求的数字，但 “7772” 是较大的数字。<br> 数字     成本<br>  1  -&gt;   4<br>  2  -&gt;   3<br>  3  -&gt;   2<br>  4  -&gt;   5<br>  5  -&gt;   6<br>  6  -&gt;   7<br>  7  -&gt;   2<br>  8  -&gt;   5<br>  9  -&gt;   5</p></blockquote><p>问题转换：每个物品体积不一样，完全背包恰好装满，<strong>最多</strong>可以装多少个物品。数字能选择的越多，必然能得到的数字越大，同时，我们是从<code>1-9</code>开始选择，也就是从数值的小到大，如果确定选择了这个数，也必然是目前最大的，那肯定放在最高位，得到的整数最大。</p><p>可完全套用完全背包模板，只需把<code>operate</code>抽象化为一个函数，这个函数类似于<code>max</code>函数，不过更高级，它比较2个值，在能恰好装满背包的前提下，谁组成的数字更大。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">largestNumber</span>(<span class="hljs-params">self, cost: List[<span class="hljs-built_in">int</span>], target: <span class="hljs-built_in">int</span></span>) -&gt; str:</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxValue</span>(<span class="hljs-params">a,b,v</span>):</span><br>        <span class="hljs-keyword">if</span> a <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">and</span> b <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:  <span class="hljs-comment"># 两种方案都不可行，返回None</span><br>            <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>        <span class="hljs-keyword">if</span> b <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:    <span class="hljs-comment"># 其中一种不行，就直接返回另一种</span><br>            <span class="hljs-keyword">return</span> a<br>        b=v+b<br>        <span class="hljs-keyword">if</span> a <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:<br>            <span class="hljs-keyword">return</span> b<br>        n1,n2=<span class="hljs-built_in">len</span>(a),<span class="hljs-built_in">len</span>(b)   <span class="hljs-comment"># 两种都可以，比较谁的数值更大</span><br>        <span class="hljs-keyword">if</span> n1&gt;n2:<br>            <span class="hljs-keyword">return</span> a<br>        <span class="hljs-keyword">elif</span> n1&lt;n2:<br>            <span class="hljs-keyword">return</span> b<br>        <span class="hljs-keyword">return</span> <span class="hljs-built_in">max</span>(a,b)<br>    n=<span class="hljs-built_in">len</span>(cost)<br>    dp=[[<span class="hljs-string">&#x27;&#x27;</span>]*(target+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n+<span class="hljs-number">1</span>)]  <span class="hljs-comment"># 标准完全背包模板，只是把operate换成了自定义函数maxValue</span><br>    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,target+<span class="hljs-number">1</span>):<br>        dp[<span class="hljs-number">0</span>][j]=<span class="hljs-literal">None</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n+<span class="hljs-number">1</span>):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(target+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=cost[i-<span class="hljs-number">1</span>]:<br>                dp[i][j]=maxValue(dp[i-<span class="hljs-number">1</span>][j],dp[i][j-cost[i-<span class="hljs-number">1</span>]],<span class="hljs-built_in">str</span>(i))<br>    <span class="hljs-keyword">if</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>] <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;0&quot;</span><br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h2 id="布尔值"><a href="#布尔值" class="headerlink" title="布尔值"></a>布尔值</h2><h3 id="139-单词拆分"><a href="#139-单词拆分" class="headerlink" title="139. 单词拆分"></a><a href="https://leetcode-cn.com/problems/word-break/">139. 单词拆分</a></h3><p>给定一个非空字符串 s 和一个包含非空单词的列表<code>wordDict</code>，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。</p><p><strong>说明：</strong></p><ul><li>拆分时可以重复使用字典中的单词。</li><li>你可以假设字典中没有重复的单词。</li></ul><p><strong>示例 1：</strong></p><blockquote><p>输入: s = “leetcode”, wordDict = [“leet”, “code”]<br>输出: true<br>解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入: s = “applepenapple”, wordDict = [“apple”, “pen”]<br>输出: true<br>解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。注意你可以重复使用字典中的单词。</p></blockquote><p><strong>示例 3：</strong></p><blockquote><p>输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]<br>输出: false</p></blockquote><p>问题转换：完全背包恰好装满，是否存在解决方案，单词用过之后可以再用，所以这里应该采用排列的策略。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">wordBreak</span>(<span class="hljs-params">self, s: <span class="hljs-built_in">str</span>, wordDict</span>) -&gt; bool:</span><br>    n=<span class="hljs-built_in">len</span>(wordDict)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-literal">False</span>]*(<span class="hljs-built_in">len</span>(s)+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-literal">True</span><br>    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-built_in">len</span>(s)+<span class="hljs-number">1</span>):  <span class="hljs-comment">#内外循环颠倒,按列遍历（单词可以不在乎顺序，和排列类似）</span><br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=<span class="hljs-built_in">len</span>(wordDict[i-<span class="hljs-number">1</span>]) <span class="hljs-keyword">and</span> s[j-<span class="hljs-built_in">len</span>(wordDict[i-<span class="hljs-number">1</span>]):j]==wordDict[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment">#这里是-1，不是i-1。</span><br>                <span class="hljs-comment"># dp[i][j]=or(dp[i-1][j],dp[-1][j-objs[i-1]])</span><br>                dp[i][j]|=dp[-<span class="hljs-number">1</span>][j-<span class="hljs-built_in">len</span>(wordDict[i-<span class="hljs-number">1</span>])]<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h1 id="01背包"><a href="#01背包" class="headerlink" title="01背包"></a>01背包</h1><p>从n件物品中任选，<strong>每件物品只可取用一次</strong></p><h2 id="方案数-1"><a href="#方案数-1" class="headerlink" title="方案数"></a>方案数</h2><h3 id="494-目标和"><a href="#494-目标和" class="headerlink" title="494. 目标和"></a><a href="https://leetcode-cn.com/problems/target-sum/">494. 目标和</a></h3><p>给定一个非负整数数组，a1, a2, …, an, 和一个目标数，S。现在你有两个符号 + 和 -。对于数组中的任意一个整数，你都可以从 + 或 -中选择一个符号添加在前面。<br>返回可以使最终数组和为目标数 S 的所有添加符号的方法数。</p><p><strong>示例：</strong></p><blockquote><p>输入：nums: [1, 1, 1, 1, 1], S: 3<br>输出：5<br>解释：</p><p>-1+1+1+1+1 = 3<br>+1-1+1+1+1 = 3<br>+1+1-1+1+1 = 3<br>+1+1+1-1+1 = 3<br>+1+1+1+1-1 = 3</p><p>一共有5种方法让最终目标和为3。</p></blockquote><p>问题转换：从一堆物品中选出若干个，恰好能装满背包的方案数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findTargetSumWays</span>(<span class="hljs-params">self, nums: List[<span class="hljs-built_in">int</span>], t: <span class="hljs-built_in">int</span></span>) -&gt; int:</span><br>    s=<span class="hljs-built_in">sum</span>(nums)<br>    <span class="hljs-keyword">if</span> t&gt;s:<br>        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span><br>    t+=s<br>    <span class="hljs-keyword">if</span> t%<span class="hljs-number">2</span>!=<span class="hljs-number">0</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span><br>    t=t//<span class="hljs-number">2</span>     <span class="hljs-comment">#获取目标值，转换为01背包问题</span><br>    n=<span class="hljs-built_in">len</span>(nums)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-number">0</span>]*(t+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-number">1</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(t+<span class="hljs-number">1</span>):<br>            dp[i][j]+=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=nums[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment"># 和完全背包问题不同之处：这里是i-1。</span><br>                <span class="hljs-comment"># dp[i][j]=sum(dp[i-1][j],dp[i-1][j-objs[i-1]])</span><br>                dp[i][j]+=dp[i-<span class="hljs-number">1</span>][j-nums[i-<span class="hljs-number">1</span>]]<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h2 id="最值-1"><a href="#最值-1" class="headerlink" title="最值"></a>最值</h2><h3 id="474-一和零"><a href="#474-一和零" class="headerlink" title="474. 一和零"></a><a href="https://leetcode-cn.com/problems/ones-and-zeroes/">474. 一和零</a></h3><p>给你一个二进制字符串数组 strs 和两个整数 m 和 n 。<br>请你找出并返回 strs 的最大子集的大小，该子集中 最多 有 m 个 0 和 n 个 1 。<br>如果 x 的所有元素也是 y 的元素，集合 x 是集合 y 的 子集 。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3<br>输出：4<br>解释：最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,”0001”,”1”,”0”} ，因此答案是 4 。<br>其他满足题意但较小的子集包括 {“0001”,”1”} 和 {“10”,”1”,”0”} 。{“111001”} 不满足题意，因为它含 4 个 1 ，大于 n 的值 3 。</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入：strs = [“10”, “0”, “1”], m = 1, n = 1<br>输出：2<br>解释：最大的子集是 {“0”, “1”} ，所以答案是 2 。</p></blockquote><p>问题转换：01背包恰好装满，最多可以装多少个物品。注意这里的物品体积可以看作是由<code>0</code>的个数和<code>1</code>的个数2个维度构成的，仍然是01背包问题，只不过将物品的1维变成了2维。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findMaxForm</span>(<span class="hljs-params">self, strs, m: <span class="hljs-built_in">int</span>, n: <span class="hljs-built_in">int</span></span>) -&gt; int:</span><br>    nums = []<br>    <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> strs:<br>        num = [<span class="hljs-number">0</span>,<span class="hljs-number">0</span>]<br>        <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> s:<br>            <span class="hljs-keyword">if</span> <span class="hljs-built_in">ord</span>(c) - <span class="hljs-built_in">ord</span>(<span class="hljs-string">&#x27;0&#x27;</span>) == <span class="hljs-number">0</span>:<br>                num[<span class="hljs-number">0</span>]+=<span class="hljs-number">1</span><br>            <span class="hljs-keyword">else</span>:<br>                num[<span class="hljs-number">1</span>]+=<span class="hljs-number">1</span><br>        nums.append(num)<br>    <span class="hljs-comment">#转换成01背包问题</span><br>    l = <span class="hljs-built_in">len</span>(nums) + <span class="hljs-number">1</span><br>    dp = [[[<span class="hljs-number">0</span>] * (n + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(l)]<br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,l):<br>        <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>):<br>            <span class="hljs-keyword">for</span> b <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n + <span class="hljs-number">1</span>):<br>                dp[i][a][b] = dp[i - <span class="hljs-number">1</span>][a][b]<br>                <span class="hljs-keyword">if</span> a &gt;= nums[i - <span class="hljs-number">1</span>][<span class="hljs-number">0</span>] <span class="hljs-keyword">and</span> b &gt;= nums[i - <span class="hljs-number">1</span>][<span class="hljs-number">1</span>]:<br>                    <span class="hljs-comment"># dp[i][j]=max(dp[i-1][j],dp[i-1][j-objs[i-1]])</span><br>                    dp[i][a][b] = <span class="hljs-built_in">max</span>(dp[i][a][b],dp[i - <span class="hljs-number">1</span>][a - nums[i - <span class="hljs-number">1</span>][<span class="hljs-number">0</span>]][b - nums[i - <span class="hljs-number">1</span>][<span class="hljs-number">1</span>]] + <span class="hljs-number">1</span>)<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h3 id="1049-最后一块石头的重量-II"><a href="#1049-最后一块石头的重量-II" class="headerlink" title="1049. 最后一块石头的重量 II"></a><a href="https://leetcode-cn.com/problems/last-stone-weight-ii/">1049. 最后一块石头的重量 II</a></h3><p>有一堆石头，用整数数组<code>stones</code>表示。其中<code>stones[i]</code>表示第<code>i</code>块石头的重量。<br>每一回合，从中选出任意两块石头，然后将它们一起粉碎。假设石头的重量分别为<code>x</code>和<code>y</code>，且<code>x &lt;= y</code>。那么粉碎的可能结果如下：<br>如果<code>x == y</code>，那么两块石头都会被完全粉碎；<br>如果<code>x != y</code>，那么重量为 x 的石头将会完全粉碎，而重量为<code>y</code>的石头新重量为<code>y-x</code>。<br>最后，<strong>最多只会剩下一块</strong>石头。返回此石头<strong>最小的可能重量</strong>。如果没有石头剩下，就返回 0。</p><p><strong>示例 1：</strong></p><blockquote><p>输入：stones = [2,7,4,1,8,1]<br>输出：1<br>解释：<br>组合 2 和 4，得到 2，所以数组转化为 [2,7,1,8,1]，<br>组合 7 和 8，得到 1，所以数组转化为 [2,1,1,1]，<br>组合 2 和 1，得到 1，所以数组转化为 [1,1,1]，<br>组合 1 和 1，得到 0，所以数组转化为 [1]，这就是最优值。</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入：stones = [31,26,33,21,40]<br>输出：5</p></blockquote><p>问题转换：如果把石头分成两组，使两组的重量尽可能相同，那么最终剩下的重量就越小。所以尽可能使2组的石头重量倾向于总重量的一半，问题变成01背包问题：给定一些石头和一个指定容量的背包，最多可以装多少重量的石头（不能超过背包容量）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lastStoneWeightII</span>(<span class="hljs-params">self, stones: List[<span class="hljs-built_in">int</span>]</span>) -&gt; int:</span><br>    n=<span class="hljs-built_in">len</span>(stones)+<span class="hljs-number">1</span><br>    s=<span class="hljs-built_in">sum</span>(stones)<br>    t=s//<span class="hljs-number">2</span><br>    dp=[[<span class="hljs-number">0</span>]*(t+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(t+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=stones[i-<span class="hljs-number">1</span>]:<br>                dp[i][j]=<span class="hljs-built_in">max</span>(dp[i-<span class="hljs-number">1</span>][j],dp[i-<span class="hljs-number">1</span>][j-stones[i-<span class="hljs-number">1</span>]]+stones[i-<span class="hljs-number">1</span>])<br>    <span class="hljs-keyword">return</span> s-<span class="hljs-number">2</span>*dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h2 id="布尔值-1"><a href="#布尔值-1" class="headerlink" title="布尔值"></a>布尔值</h2><h3 id="416-分割等和子集"><a href="#416-分割等和子集" class="headerlink" title="416. 分割等和子集"></a><a href="https://leetcode-cn.com/problems/partition-equal-subset-sum/">416. 分割等和子集</a></h3><p>给你一个 <strong>只包含正整数</strong> 的 <strong>非空</strong> 数组 <code>nums</code> 。请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。 </p><p><strong>示例 1：</strong></p><blockquote><p>输入：nums = [1,5,11,5]<br>输出：true<br>解释：数组可以分割成 [1, 5, 5] 和 [11] 。</p></blockquote><p><strong>示例 2：</strong></p><blockquote><p>输入：nums = [1,2,3,5]<br>输出：false<br>解释：数组不能分割成两个元素和相等的子集。</p></blockquote><p>问题转换：从<code>nums</code>选n个数字能否凑齐<code>s//2</code>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">canPartition</span>(<span class="hljs-params">self, nums: List[<span class="hljs-built_in">int</span>]</span>) -&gt; bool:</span><br>    s=<span class="hljs-built_in">sum</span>(nums)<br>    <span class="hljs-keyword">if</span> s%<span class="hljs-number">2</span>!=<span class="hljs-number">0</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span><br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">max</span>(nums)*<span class="hljs-number">2</span>&gt;s:<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span><br>    s=s//<span class="hljs-number">2</span>   <span class="hljs-comment">#转换成01背包问题</span><br>    n=<span class="hljs-built_in">len</span>(nums)+<span class="hljs-number">1</span><br>    dp=[[<span class="hljs-literal">False</span>]*(s+<span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]=<span class="hljs-literal">True</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,n):<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(s+<span class="hljs-number">1</span>):<br>            dp[i][j]=dp[i-<span class="hljs-number">1</span>][j]<br>            <span class="hljs-keyword">if</span> j&gt;=nums[i-<span class="hljs-number">1</span>]:<br>                <span class="hljs-comment"># dp[i][j]=or(dp[i-1][j],dp[i-1][j-objs[i-1]])</span><br>                dp[i][j]|=dp[i-<span class="hljs-number">1</span>][j-nums[i-<span class="hljs-number">1</span>]]<br>    <span class="hljs-keyword">return</span> dp[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]<br></code></pre></td></tr></table></figure><h1 id="多重背包"><a href="#多重背包" class="headerlink" title="多重背包"></a>多重背包</h1><p>从n种物品中任选，每种物品可选数量有范围</p><h2 id="方案数-2"><a href="#方案数-2" class="headerlink" title="方案数"></a>方案数</h2><h3 id="3333-找到初始输入字符串-II"><a href="#3333-找到初始输入字符串-II" class="headerlink" title="3333. 找到初始输入字符串 II"></a><a href="https://leetcode.cn/problems/find-the-original-typed-string-ii/">3333. 找到初始输入字符串 II</a></h3><p>有一个数组arr，比如[2,1,3,2]，其中arr[i]表示某一个物品的数量，从arr中最多选m个物品，也可以一个都不选，求总共的方案数</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_ways</span>(<span class="hljs-params">arr, m</span>):</span><br>    n = <span class="hljs-built_in">len</span>(arr)<br>    dp = [[<span class="hljs-number">0</span>] * (m + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n + <span class="hljs-number">1</span>)]<br>    dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>  <span class="hljs-comment"># 选0个的方案数为1</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>, n + <span class="hljs-number">1</span>):<br>        count = arr[i - <span class="hljs-number">1</span>]<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>): <span class="hljs-comment"># 内层j表示当前总共选了多少个</span><br>            <span class="hljs-comment"># k表示当前物品从0选到count个，上层dp范围为[j-count,j]</span><br>            <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-built_in">min</span>(count, j) + <span class="hljs-number">1</span>):<br>                dp[i][j] += dp[i - <span class="hljs-number">1</span>][j - k]<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">sum</span>(dp[-<span class="hljs-number">1</span>]) <span class="hljs-comment"># 选0~m个的方案数总和</span><br></code></pre></td></tr></table></figure><p><img src="/postimgs/bag-problem/4.jpg" alt="多重背包问题状态转移图示"></p><p>时间优化，将dp转换成前缀和</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_ways</span>(<span class="hljs-params">self, arr, m</span>):</span><br>    n = <span class="hljs-built_in">len</span>(arr)<br>    preSum = [[<span class="hljs-number">0</span>] * (m + <span class="hljs-number">2</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n + <span class="hljs-number">1</span>)]<br>    preSum[<span class="hljs-number">0</span>] = [<span class="hljs-number">0</span>] + [<span class="hljs-number">1</span>] * (m + <span class="hljs-number">1</span>)  <span class="hljs-comment"># 选0个的方案数为1</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>, n + <span class="hljs-number">1</span>):<br>        count = arr[i - <span class="hljs-number">1</span>]<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>):  <span class="hljs-comment"># 内层j表示当前总共选了多少个</span><br>            <span class="hljs-comment"># k表示当前物品从0选到count个，上层dp范围为[j-count,j]</span><br>            dp = preSum[i - <span class="hljs-number">1</span>][j + <span class="hljs-number">1</span>] - preSum[i - <span class="hljs-number">1</span>][<span class="hljs-built_in">max</span>(<span class="hljs-number">0</span>, j - count)]<br>            preSum[i][j + <span class="hljs-number">1</span>] = preSum[i][j] + dp<br>    <span class="hljs-keyword">return</span> preSum[-<span class="hljs-number">1</span>][-<span class="hljs-number">1</span>]  <span class="hljs-comment"># 选0~m个的方案数总和</span><br></code></pre></td></tr></table></figure><p>使用一维dp</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_ways</span>(<span class="hljs-params">arr, m</span>):</span><br>    n = <span class="hljs-built_in">len</span>(arr)<br>    dp = [<span class="hljs-number">0</span>] * (m + <span class="hljs-number">1</span>)<br>    dp[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>  <span class="hljs-comment"># 选0个的方案数为1</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n):<br>        count = arr[i]<br>        new_dp = dp[:]<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>): <span class="hljs-comment"># 内层j表示当前总共选了多少个</span><br>            <span class="hljs-comment"># 当前物品从1选到count个，不包含0，因为一个都不选直接从dp[j]转移过来，new_dp[j]一开始就包含了不选当前物品的情况</span><br>            <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>, <span class="hljs-built_in">min</span>(count, j) + <span class="hljs-number">1</span>):<br>                new_dp[j] += dp[j - k]<br>        dp = new_dp<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">sum</span>(dp)  <span class="hljs-comment"># 选0~m个的方案数总和</span><br></code></pre></td></tr></table></figure><p>一维dp使用前缀和优化，减少一个内层循环</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_ways</span>(<span class="hljs-params">arr, m</span>):</span><br>    dp = [<span class="hljs-number">0</span>] * (m + <span class="hljs-number">1</span>)<br>    dp[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>  <span class="hljs-comment"># 选0个的方案数为1</span><br>    <span class="hljs-keyword">for</span> count <span class="hljs-keyword">in</span> arr:<br>        preSum = [<span class="hljs-number">0</span>] * (<span class="hljs-built_in">len</span>(dp) + <span class="hljs-number">1</span>)<br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>, <span class="hljs-built_in">len</span>(preSum)):<br>            preSum[i] = preSum[i - <span class="hljs-number">1</span>] + dp[i - <span class="hljs-number">1</span>]<br>        new_dp = dp[:]<br>        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(m + <span class="hljs-number">1</span>):  <span class="hljs-comment"># 内层j表示当前总共选了多少个</span><br>            <span class="hljs-comment"># 当前物品从1选到count个，不包含0，因为一个都不选直接从dp[j]转移过来，new_dp[j]一开始就包含了不选当前物品的情况</span><br>            <span class="hljs-comment"># 当前物品的可选范围[1,count],则dp的范围是[i-count,i-1]</span><br>            <span class="hljs-comment"># 注意左端点可能小于0，当物品可选最大数量count大于总数量i，left,right=max(0,j-count),j-1</span><br>            <span class="hljs-comment"># 则dp[left,right]之和等于preSum[right+1]-preSum[left]</span><br>            new_dp[j] += preSum[j] - preSum[<span class="hljs-built_in">max</span>(<span class="hljs-number">0</span>, j - count)]<br>        dp = new_dp<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">sum</span>(dp)  <span class="hljs-comment"># 选0~m个的方案数总和</span><br></code></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;背包问题实际上是动态规划的一种经典应用，本文想通过介绍一种模板用于解决各种背包问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板统一采用二维数组来表示动</summary>
      
    
    
    
    <category term="算法" scheme="https://www.xdull.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="动态规划" scheme="https://www.xdull.cn/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"/>
    
    <category term="Leetcode" scheme="https://www.xdull.cn/tags/Leetcode/"/>
    
    <category term="算法" scheme="https://www.xdull.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="背包问题" scheme="https://www.xdull.cn/tags/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/"/>
    
  </entry>
  
  <entry>
    <title>为Spring Cloud Feign Client定制专属“装备”</title>
    <link href="https://www.xdull.cn/springcloud-feign-config.html"/>
    <id>https://www.xdull.cn/springcloud-feign-config.html</id>
    <published>2025-04-10T17:53:30.000Z</published>
    <updated>2026-07-22T15:15:28.797Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在微服务架构中，服务间的通信是核心环节。Spring Cloud Feign 作为一种声明式的 HTTP 客户端，极大地简化了服务调用的复杂性。然而，在实际开发中，我们常常会遇到需要为特定的 FeignClient 定制特殊配置的场景，例如处理特定的数据格式（如 <code>LocalDateTime</code> 的序列化）、设置特定的超时时间或添加特定的拦截器。本文将结合具体的代码示例，深入探讨如何为一个或多个特殊的 FeignClient 指定独立的配置，并解决可能遇到的配置隔离问题。</p><h2 id="问题背景：默认配置的局限性"><a href="#问题背景：默认配置的局限性" class="headerlink" title="问题背景：默认配置的局限性"></a>问题背景：默认配置的局限性</h2><p>Spring Cloud Feign 默认会使用全局配置，包括默认的 <code>Encoder</code> 和 <code>Decoder</code>（通常是基于 Jackson）。但在某些情况下，默认配置可能无法满足需求：</p><ol><li><strong>日期时间格式处理</strong>：后端服务可能要求特定的日期时间格式（如 <code>&quot;yyyy-MM-dd HH:mm:ss&quot;</code>），而默认的 Jackson 配置可能将 <code>LocalDateTime</code> 序列化为时间戳数组，导致服务调用失败或数据解析错误。</li><li><strong>特殊序列化/反序列化逻辑</strong>：某些接口可能返回非标准 JSON 结构或需要特殊的处理逻辑。</li><li><strong>不同服务的差异化需求</strong>：不同的下游服务可能对超时时间、重试策略有不同的要求。</li></ol><p>这时，我们就需要为特定的 <code>FeignClient</code> 提供定制化的配置。</p><h2 id="关键点一：使用-configuration-属性指定独立配置"><a href="#关键点一：使用-configuration-属性指定独立配置" class="headerlink" title="关键点一：使用 configuration 属性指定独立配置"></a>关键点一：使用 <code>configuration</code> 属性指定独立配置</h2><p><code>@FeignClient</code> 注解提供了一个 <code>configuration</code> 属性，允许我们为该客户端指定一个独立的配置类。这个配置类通常包含自定义的 <code>Encoder</code>, <code>Decoder</code>, <code>Logger</code>, <code>Contract</code>, <code>Retryer</code>, <code>RequestInterceptor</code> 等 Bean。</p><p>下面是一个示例，<code>ISpecialFacadeService</code> 这个 FeignClient 需要特殊的 JSON 处理（特别是针对 <code>LocalDateTime</code>），因此我们为其指定了 <code>SpecialFeignConfig</code> 作为配置类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 定义 Feign 客户端接口</span><br><span class="hljs-meta">@ResponseBody</span><br><span class="hljs-meta">@FeignClient(value = &quot;myweb&quot;, configuration = ISpecialFacadeService.SpecialFeignConfig.class)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ISpecialFacadeService</span> </span>&#123;<br><br>    <span class="hljs-comment">// 接口方法定义</span><br>    <span class="hljs-meta">@RequestMapping(&quot;/getStatus&quot;)</span><br>    <span class="hljs-function">DataResult&lt;Response&gt; <span class="hljs-title">getStatus</span><span class="hljs-params">(<span class="hljs-meta">@RequestParam(&quot;id&quot;)</span> String id)</span></span>;<br><br><br>    <span class="hljs-comment">/**</span><br><span class="hljs-comment">     * 专门为 ISpecialFacadeService 定制的 Feign 配置类</span><br><span class="hljs-comment">     * 注意：此类通常定义为内部类或单独的文件，避免被全局扫描到</span><br><span class="hljs-comment">     */</span><br>    <span class="hljs-meta">@Configuration(proxyBeanMethods = false)</span> <span class="hljs-comment">// 优化配置类，不代理Bean方法</span><br>    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpecialFeignConfig</span> </span>&#123;<br><br>        <span class="hljs-comment">/**</span><br><span class="hljs-comment">         * 自定义 JSON 解码器 (Decoder)</span><br><span class="hljs-comment">         * <span class="hljs-doctag">@return</span> Decoder Bean</span><br><span class="hljs-comment">         */</span><br>        <span class="hljs-meta">@Bean</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> Decoder <span class="hljs-title">specificFeignDecoder</span><span class="hljs-params">()</span> </span>&#123;<br>            ObjectMapper objectMapper = <span class="hljs-keyword">new</span> ObjectMapper();<br>            <span class="hljs-comment">// 注册 Java 8 时间模块，支持 LocalDateTime 等类型的序列化/反序列化</span><br>            objectMapper.registerModule(<span class="hljs-keyword">new</span> JavaTimeModule());<br>            <span class="hljs-comment">// 配置 Jackson 不将日期序列化为时间戳</span><br>            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);<br>            <span class="hljs-comment">// 可以在这里添加更多自定义配置...</span><br>            <span class="hljs-comment">// objectMapper.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS); // 根据需要启用或禁用</span><br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> JacksonDecoder(objectMapper);<br>        &#125;<br><br>        <span class="hljs-comment">/**</span><br><span class="hljs-comment">         * 自定义 JSON 编码器 (Encoder)</span><br><span class="hljs-comment">         * <span class="hljs-doctag">@return</span> Encoder Bean</span><br><span class="hljs-comment">         */</span><br>        <span class="hljs-meta">@Bean</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> Encoder <span class="hljs-title">specificFeignEncoder</span><span class="hljs-params">()</span> </span>&#123;<br>            ObjectMapper objectMapper = <span class="hljs-keyword">new</span> ObjectMapper();<br>            <span class="hljs-comment">// 同样需要注册 Java 8 时间模块并配置日期格式</span><br>            objectMapper.registerModule(<span class="hljs-keyword">new</span> JavaTimeModule());<br>            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);<br>            <span class="hljs-comment">// 可以在这里添加更多自定义配置...</span><br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> JacksonEncoder(objectMapper);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>代码解析</strong>：</p><ol><li><code>@FeignClient(value = &quot;myweb&quot;, configuration = ISpecialFacadeService.SpecialFeignConfig.class)</code>：明确告诉 Spring Cloud Feign，<code>ISpecialFacadeService</code> 这个客户端要使用 <code>SpecialFeignConfig</code> 类中定义的 Bean 来覆盖默认配置。</li><li><code>SpecialFeignConfig</code>：这是一个标准的 Spring <code>@Configuration</code>类。<ul><li><code>proxyBeanMethods = false</code>：这是一个优化选项，适用于配置类，表示不需要 CGLIB 代理。</li><li><code>specificFeignDecoder()</code> 和 <code>specificFeignEncoder()</code>：这两个 <code>@Bean</code> 方法分别创建了自定义的 <code>Decoder</code> 和 <code>Encoder</code>。</li><li><code>ObjectMapper</code> 配置：我们在 <code>ObjectMapper</code> 中注册了 <code>JavaTimeModule</code> 并禁用了 <code>WRITE_DATES_AS_TIMESTAMPS</code>。这是处理 <code>java.time.LocalDateTime</code> 等 JSR-310 日期时间类型的关键，使其能够正确地序列化为字符串（如 <code>&quot;2023-10-27 10:00:00&quot;</code>）而不是数字时间戳。</li></ul></li></ol><h2 id="关键点二：避免特殊配置污染全局环境"><a href="#关键点二：避免特殊配置污染全局环境" class="headerlink" title="关键点二：避免特殊配置污染全局环境"></a>关键点二：避免特殊配置污染全局环境</h2><p>将配置类 <code>SpecialFeignConfig</code> 定义为 <code>ISpecialFacadeService</code> 的内部类或放在单独的、不被主 <code>@ComponentScan</code> 扫描到的包下，是一种常见的避免配置污染的方式。</p><p>然而，如果配置类没有定义为内部类，并且不小心被 Spring Boot 的组件扫描（<code>@ComponentScan</code>）扫描到了，那么这个配置类中的 Bean（比如自定义的 <code>Decoder</code> 和 <code>Encoder</code>）可能会意外地成为<strong>全局默认配置</strong>，影响到所有其他的 FeignClient。</p><p>为了确保这个特殊配置<strong>只</strong>应用于指定的 FeignClient，我们需要在主应用程序或配置类上使用 <code>@ComponentScan</code> 的 <code>excludeFilters</code> 属性，将这个特殊的配置类排除在全局扫描之外。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@SpringBootApplication</span> <span class="hljs-comment">// 通常包含了 @ComponentScan</span><br><span class="hljs-meta">@ComponentScan(</span><br><span class="hljs-meta">    excludeFilters = &#123;</span><br><span class="hljs-meta">        // 通过 ASSIGNABLE_TYPE 类型精确排除指定的配置类</span><br><span class="hljs-meta">        @ComponentScan.Filter(</span><br><span class="hljs-meta">            type = FilterType.ASSIGNABLE_TYPE,</span><br><span class="hljs-meta">            classes = &#123;ISpecialFacadeService.SpecialFeignConfig.class&#125;</span><br><span class="hljs-meta">        )</span><br><span class="hljs-meta">        // 可以添加其他需要排除的类或规则</span><br><span class="hljs-meta">    &#125;</span><br><span class="hljs-meta">)</span><br><span class="hljs-comment">// 启用 Feign 客户端，并指定扫描 Feign 接口的基础包</span><br><span class="hljs-meta">@EnableFeignClients(basePackages = &#123;&quot;xxx.xxx.xx&quot;&#125;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StdSpApplication</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">SpringBootServletInitializer</span> </span>&#123;<br>    <span class="hljs-comment">// ... main method etc.</span><br>&#125;<br></code></pre></td></tr></table></figure><p><strong>代码解析</strong>：</p><ul><li><code>@ComponentScan(excludeFilters = ...)</code>：我们在这里添加了一个过滤器。</li><li><code>@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = &#123;ISpecialFacadeService.SpecialFeignConfig.class&#125;)</code>：这个过滤器指定了类型为 <code>ASSIGNABLE_TYPE</code>，这意味着任何是 <code>ISpecialFacadeService.SpecialFeignConfig</code> 类型或其子类型的类，在组件扫描时都将被忽略。这样就保证了 <code>SpecialFeignConfig</code> 不会被注册为全局 Bean，只在被 <code>ISpecialFacadeService</code> 通过 <code>configuration</code> 属性引用时才生效。</li></ul><h3 id="LocalDateTime-处理的另一种方式：-JsonFormat"><a href="#LocalDateTime-处理的另一种方式：-JsonFormat" class="headerlink" title="LocalDateTime 处理的另一种方式：@JsonFormat"></a><code>LocalDateTime</code> 处理的另一种方式：<code>@JsonFormat</code></h3><p>值得一提的是，如果仅仅是为了控制 <code>LocalDateTime</code> 字段的序列化/反序列化格式，也可以直接在 DTO 的字段上使用 <code>@JsonFormat</code> 注解：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> com.fasterxml.jackson.annotation.JsonFormat;<br><span class="hljs-keyword">import</span> java.time.LocalDateTime;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Response</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> String status;<br><br>    <span class="hljs-comment">// 直接在字段上指定格式</span><br>    <span class="hljs-meta">@JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)</span><br>    <span class="hljs-keyword">private</span> LocalDateTime handleTime;<br><br>    <span class="hljs-comment">// getters and setters</span><br>&#125;<br></code></pre></td></tr></table></figure><p>这种方式更简单直接，适用于只需要格式化特定字段的场景。但如果需要更复杂的 Jackson 定制（比如启用/禁用某些特性、注册多个模块等），或者希望对某个 FeignClient 的所有接口调用统一应用这种格式，那么通过 <code>configuration</code> 提供自定义的 <code>ObjectMapper</code> 是更优、更集中的做法。</p><blockquote><p>实际上，也可以去掉配置类<code>SpecialFeignConfig</code>的<code>@Configuration</code>注解</p><p><strong>为什么移除 <code>@Configuration</code> 可以解决问题？</strong></p><ul><li>当 <code>SpecialFeignConfig</code> 类上没有 <code>@Configuration</code> 注解时，Spring 的常规组件扫描会<strong>忽略</strong>这个类，它不会被注册为 Spring 应用上下文中的一个标准配置类。</li><li>然而，Spring Cloud OpenFeign 在处理 <code>@FeignClient</code> 的 <code>configuration</code> 属性时，<strong>仍然会读取这个指定的类</strong>（即使它没有 <code>@Configuration</code> 注解），并查找其中的 <code>@Bean</code> 方法。</li><li>Feign 会在为 <code>ISpecialFacadeService</code> 创建的<strong>专用子上下文</strong>中，实例化并注册这些 <code>@Bean</code> 方法定义的 Bean。</li><li>这样，这些 Bean 就只存在于 <code>ISpecialFacadeService</code> 的特定上下文中，不会影响到父上下文，也不会被其他 Feign 客户端共享。</li></ul></blockquote><h2 id="关键点三：contextId-彻底隔离"><a href="#关键点三：contextId-彻底隔离" class="headerlink" title="关键点三：contextId 彻底隔离"></a>关键点三：<code>contextId</code> 彻底隔离</h2><p>在实践中，我们可能会遇到一个更棘手的问题：假设你有<strong>两个或多个</strong> <code>FeignClient</code> 接口，它们都指向<strong>同一个服务名</strong>（即 <code>@FeignClient</code> 的 <code>value</code> 或 <code>name</code> 属性相同），其中一个指定了特殊的 <code>configuration</code>，而另一个没有。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 特殊配置的 Client</span><br><span class="hljs-meta">@FeignClient(name = &quot;myweb&quot;, configuration = ISpecialFacadeService.SpecialFeignConfig.class)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ISpecialFacadeService</span> </span>&#123; ... &#125;<br><br><span class="hljs-comment">// 使用默认配置（期望）的 Client，但指向同一个服务</span><br><span class="hljs-meta">@FeignClient(name = &quot;myweb&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IOtherFacadeService</span> </span>&#123; ... &#125; <span class="hljs-comment">// 调用 &quot;myweb&quot; 服务的其他接口</span><br></code></pre></td></tr></table></figure><p>在这种情况下，即使你使用了 <code>excludeFilters</code>，Spring Cloud OpenFeign 在默认情况下可能会为这两个指向相同 <code>name</code> 的客户端创建<strong>同一个 Spring ApplicationContext</strong>。这意味着，<code>IOtherFacadeService</code> <strong>仍然可能会意外地使用</strong>为 <code>ISpecialFacadeService</code> 定制的 <code>SpecialFeignConfig</code> 配置！这显然不是我们想要的。</p><p>为了实现<strong>真正的隔离</strong>，确保每个 <code>FeignClient</code>（即使它们指向同一个服务）都有自己独立的上下文和配置，我们需要使用 <code>@FeignClient</code> 的 <code>contextId</code> 属性。<code>contextId</code> 为每个 FeignClient 提供了一个唯一的标识符，强制 Spring Cloud 为其创建独立的 ApplicationContext。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// Client 1: 使用特殊配置，并指定唯一的 contextId</span><br><span class="hljs-meta">@FeignClient(value = &quot;myweb&quot;, configuration = SpecialFeignConfig.class, contextId = &quot;specialClientContext&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ISpecialFacadeService</span> </span>&#123;<br>    <span class="hljs-comment">// ... methods</span><br>&#125;<br><br><span class="hljs-comment">// Client 2: 指向同一个服务，不指定 configuration（使用默认或全局配置），</span><br><span class="hljs-comment">// 但必须指定一个不同的 contextId 以确保隔离</span><br><span class="hljs-meta">@FeignClient(value = &quot;myweb&quot;, contextId = &quot;otherClientContext&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IOtherFacadeService</span> </span>&#123;<br>    <span class="hljs-comment">// ... methods</span><br>&#125;<br></code></pre></td></tr></table></figure><p><strong>代码解析</strong>：</p><ul><li>通过为每个指向 <code>&quot;myweb&quot;</code> 服务的 <code>FeignClient</code> 设置<strong>不同的 <code>contextId</code></strong>（如 <code>&quot;specialClientContext&quot;</code>, <code>&quot;otherClientContext&quot;</code>），我们强制 Spring Cloud OpenFeign 为它们创建了隔离的上下文环境。</li><li>现在，<code>ISpecialFacadeService</code> 会使用其指定的 <code>SpecialFeignConfig</code>。</li><li><code>IOtherFacadeService</code>则会使用 Feign 的默认配置或全局自定义配置（如果存在的话），而不会受到 <code>SpecialFeignConfig</code> 的影响。</li></ul><p><strong>何时必须使用 <code>contextId</code>？</strong></p><p>当你有多个 <code>@FeignClient</code> 接口指向同一个 <code>name</code>（服务名），并且其中至少有一个需要与其他客户端不同的特定配置时，强烈建议为<strong>所有</strong>指向该服务的客户端都显式指定唯一的 <code>contextId</code>，以保证配置的隔离性。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>为 Spring Cloud Feign 中的特定客户端定制配置是微服务开发中的常见需求。本文介绍了三种关键技术：</p><ol><li><strong><code>@FeignClient(configuration = ...)</code></strong>：为特定 FeignClient 指定独立的配置类，常用于定制 <code>Encoder</code>, <code>Decoder</code> (如处理 <code>LocalDateTime</code>) 等。</li><li><strong><code>@ComponentScan(excludeFilters = ...)</code></strong>：防止特殊配置类被全局扫描，避免污染其他 FeignClient。</li><li><strong><code>@FeignClient(contextId = ...)</code></strong>：当多个 FeignClient 指向同一服务名时，使用 <code>contextId</code> 提供唯一标识，确保每个客户端拥有独立的上下文和配置，实现真正的隔离。</li></ol><p>掌握这些技巧，可以让你更灵活、更精确地控制 FeignClient 的行为，更好地应对复杂的微服务通信场景。</p>]]></content>
    
    
    <summary type="html">详细介绍如何为 Spring Cloud 中的特定 FeignClient 实现配置隔离。</summary>
    
    
    
    <category term="Spring Cloud" scheme="https://www.xdull.cn/categories/Spring-Cloud/"/>
    
    
    <category term="微服务" scheme="https://www.xdull.cn/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    <category term="Feign" scheme="https://www.xdull.cn/tags/Feign/"/>
    
    <category term="Spring Cloud" scheme="https://www.xdull.cn/tags/Spring-Cloud/"/>
    
    <category term="自定义配置" scheme="https://www.xdull.cn/tags/%E8%87%AA%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE/"/>
    
    <category term="Jackson" scheme="https://www.xdull.cn/tags/Jackson/"/>
    
    <category term="LocalDateTime" scheme="https://www.xdull.cn/tags/LocalDateTime/"/>
    
  </entry>
  
  <entry>
    <title>curl 参数用法详解</title>
    <link href="https://www.xdull.cn/curl.html"/>
    <id>https://www.xdull.cn/curl.html</id>
    <published>2025-03-13T00:08:18.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<p><code>curl</code> 是一个强大的命令行工具，用于从服务器传输数据或向服务器传输数据。它支持多种协议，包括 HTTP、HTTPS、FTP 等，并且广泛应用于调试 API、下载文件以及与 Web 服务交互等场景。本文将详细介绍 <code>curl</code> 的常用参数及其用法。</p><hr><h2 id="基础用法"><a href="#基础用法" class="headerlink" title="基础用法"></a>基础用法</h2><p>最基本的 <code>curl</code> 命令可以直接用来发送 HTTP 请求。例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl https://example.com<br></code></pre></td></tr></table></figure><p>上述命令会向 <code>https://example.com</code> 发送一个 GET 请求，并输出返回的内容。</p><h2 id="常用参数解析"><a href="#常用参数解析" class="headerlink" title="常用参数解析"></a>常用参数解析</h2><h3 id="1-X-指定请求方法"><a href="#1-X-指定请求方法" class="headerlink" title="1. -X 指定请求方法"></a>1. <code>-X</code> 指定请求方法</h3><p>默认情况下，<code>curl</code> 使用 GET 方法发送请求。如果需要指定其他方法（如 POST、PUT、DELETE 等），可以使用 <code>-X</code> 参数。例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST https://example.com/api<br></code></pre></td></tr></table></figure><h3 id="2-d-发送请求数据"><a href="#2-d-发送请求数据" class="headerlink" title="2. -d 发送请求数据"></a>2. <code>-d</code> 发送请求数据</h3><p>在发送 POST 或 PUT 请求时，通常需要附带数据。<code>-d</code> 参数可以用来指定请求体中的数据。例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST https://example.com/api \<br>     -d <span class="hljs-string">&#x27;&#123;&quot;name&quot;: &quot;John&quot;, &quot;age&quot;: 30&#125;&#x27;</span> \<br>     -H <span class="hljs-string">&quot;Content-Type: application/json&quot;</span><br></code></pre></td></tr></table></figure><h3 id="3-H-设置请求头"><a href="#3-H-设置请求头" class="headerlink" title="3. -H 设置请求头"></a>3. <code>-H</code> 设置请求头</h3><p>通过 <code>-H</code> 参数可以自定义请求头。例如，设置 <code>Authorization</code> 头以传递认证信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl https://example.com/api \<br>     -H <span class="hljs-string">&quot;Authorization: Bearer YOUR_TOKEN&quot;</span><br></code></pre></td></tr></table></figure><h3 id="4-o-保存响应内容到文件"><a href="#4-o-保存响应内容到文件" class="headerlink" title="4. -o 保存响应内容到文件"></a>4. <code>-o</code> 保存响应内容到文件</h3><p>如果希望将服务器的响应保存到本地文件中，可以使用 <code>-o</code> 参数。例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl https://example.com/file.zip -o file.zip<br></code></pre></td></tr></table></figure><h3 id="5-L-自动跳转"><a href="#5-L-自动跳转" class="headerlink" title="5. -L 自动跳转"></a>5. <code>-L</code> 自动跳转</h3><p>当服务器返回重定向响应（如 301 或 302）时，<code>curl</code> 默认不会自动跳转。使用 <code>-L</code> 参数可以让 <code>curl</code> 自动跟随重定向：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -L https://example.com<br></code></pre></td></tr></table></figure><h3 id="6-I-获取响应头"><a href="#6-I-获取响应头" class="headerlink" title="6. -I 获取响应头"></a>6. <code>-I</code> 获取响应头</h3><p>如果只需要获取响应头信息，而不需要响应体，可以使用 <code>-I</code> 参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -I https://example.com<br></code></pre></td></tr></table></figure><h3 id="7-u-基本认证"><a href="#7-u-基本认证" class="headerlink" title="7. -u 基本认证"></a>7. <code>-u</code> 基本认证</h3><p>对于需要基本认证的资源，可以使用 <code>-u</code> 参数提供用户名和密码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -u username:password https://example.com<br></code></pre></td></tr></table></figure><h3 id="8-data-urlencode-编码数据"><a href="#8-data-urlencode-编码数据" class="headerlink" title="8. --data-urlencode 编码数据"></a>8. <code>--data-urlencode</code> 编码数据</h3><p>当需要对请求数据进行 URL 编码时，可以使用 <code>--data-urlencode</code> 参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST https://example.com/api \<br>     --data-urlencode <span class="hljs-string">&quot;name=John Doe&quot;</span> \<br>     --data-urlencode <span class="hljs-string">&quot;age=30&quot;</span><br></code></pre></td></tr></table></figure><h3 id="9-v-显示详细信息"><a href="#9-v-显示详细信息" class="headerlink" title="9. -v 显示详细信息"></a>9. <code>-v</code> 显示详细信息</h3><p>使用 <code>-v</code> 参数可以查看请求和响应的详细信息，包括请求头、响应头和状态码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -v https://example.com<br></code></pre></td></tr></table></figure><h3 id="10-w-自定义输出信息"><a href="#10-w-自定义输出信息" class="headerlink" title="10. -w 自定义输出信息"></a>10. <code>-w</code> 自定义输出信息</h3><p><code>-w</code> 参数允许你指定在请求完成后输出的特定信息。你可以在输出中使用占位符（如 <code>%&#123;http_code&#125;</code>、<code>%&#123;time_total&#125;</code>）来定制输出格式。这个参数非常适用于调试和性能监控。</p><p>例如，查看 HTTP 状态码和请求总时间：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -w <span class="hljs-string">&quot;HTTP状态码: %&#123;http_code&#125;\n总耗时: %&#123;time_total&#125;秒\n&quot;</span> https://example.com<br></code></pre></td></tr></table></figure><p>该命令将输出类似以下的内容：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs plain">HTTP状态码: 200<br>总耗时: 0.123秒<br></code></pre></td></tr></table></figure><p>常用的占位符包括：</p><ul><li><code>%&#123;http_code&#125;</code>: 返回的 HTTP 状态码</li><li><code>%&#123;time_total&#125;</code>: 完成请求的总时间（秒）</li><li><code>%&#123;time_namelookup&#125;</code>: DNS 查询时间</li><li><code>%&#123;time_connect&#125;</code>: 建立连接的时间</li><li><code>%&#123;time_starttransfer&#125;</code>: 从发送请求到开始接收到第一个字节的时间</li></ul><p>更多占位符的详细列表，可以参考 <code>curl</code> 的<a href="https://curl.se/docs/manpage.html">官方文档</a>。</p><h2 id="高级用法"><a href="#高级用法" class="headerlink" title="高级用法"></a>高级用法</h2><h3 id="1-文件上传"><a href="#1-文件上传" class="headerlink" title="1. 文件上传"></a>1. 文件上传</h3><p>可以通过 <code>-F</code> 参数模拟表单上传文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -X POST https://example.com/upload \<br>     -F <span class="hljs-string">&quot;file=@/path/to/local/file.txt&quot;</span><br></code></pre></td></tr></table></figure><h3 id="2-下载多个文件"><a href="#2-下载多个文件" class="headerlink" title="2. 下载多个文件"></a>2. 下载多个文件</h3><p>结合通配符可以一次性下载多个文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -O https://example.com/files/[1-5].jpg<br></code></pre></td></tr></table></figure><h3 id="3-使用代理"><a href="#3-使用代理" class="headerlink" title="3. 使用代理"></a>3. 使用代理</h3><p>如果需要通过代理服务器访问目标地址，可以使用 <code>-x</code> 参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -x http://proxy.example.com:8080 https://example.com<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>curl</code> 是一个功能强大的工具，适用于各种网络请求场景。通过合理使用其参数，可以轻松完成复杂的任务。无论是调试 API 还是自动化脚本，<code>curl</code> 都是一个不可或缺的利器。</p><p>如果你对某些参数的具体用法还有疑问，可以通过 <code>curl --help</code> 查看更多选项说明，或者参考<a href="https://curl.se/docs">官方文档</a>进一步学习。</p>]]></content>
    
    
    <summary type="html">本文详细介绍了curl命令的常用参数和高级用法，包括发送请求、设置请求头、上传文件等功能，帮助读者掌握这一强大的命令行工具。</summary>
    
    
    
    <category term="技术交流" scheme="https://www.xdull.cn/categories/%E6%8A%80%E6%9C%AF%E4%BA%A4%E6%B5%81/"/>
    
    
    <category term="curl" scheme="https://www.xdull.cn/tags/curl/"/>
    
    <category term="命令行工具" scheme="https://www.xdull.cn/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/"/>
    
    <category term="HTTP请求" scheme="https://www.xdull.cn/tags/HTTP%E8%AF%B7%E6%B1%82/"/>
    
    <category term="API调试" scheme="https://www.xdull.cn/tags/API%E8%B0%83%E8%AF%95/"/>
    
  </entry>
  
  <entry>
    <title>五分钟带你快速了解啥是“AIGC”</title>
    <link href="https://www.xdull.cn/aigcintro.html"/>
    <id>https://www.xdull.cn/aigcintro.html</id>
    <published>2024-03-30T23:28:19.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<h1 id="AIGC"><a href="#AIGC" class="headerlink" title="AIGC"></a>AIGC</h1><p>从某一天开始，我们突然发现AI可以帮忙生成文字、图片、音频、视频等等内容了，而且让人难以分清背后的创作者到底是人类还是AI。</p><p><img src="/postimgs/aigcintro/1.png" alt=""></p><p>这些AI生成的内容被叫做<strong>AIGC</strong>，它是AI Generated Content即“AI生成内容”的简写。像ChatGPT生成的文章，Github Copilot生成的代码，Midjourney生成的图片等等都属于AIGC。</p><p><img src="/postimgs/aigcintro/2.png" alt=""></p><p>而当AIGC这个词在国内火爆的同时，海外更流行的是另外一个词 —— Generative AI，即生成式AI。从字面上来看，生成式AI和AIGC之间的关系很好理解，生成式AI所生成的内容就是AIGC。所以ChatGPT，Github Copilot，Midjourney等都属于生成式AI。由此可见，AIGC和生成式AI的概念都是很简单直白的。</p><p>但因为AIGC这个词在国内比生成式AI更加流行。很多语境下，AIGC也被用于指代生成式AI。那么生成式AI和AI、机器学习、监督学习、无监督学习、强化学习、深度学习、大语言模型等等词汇之间又是什么关系呢？有没有一种剪不断理还乱的感觉，这个很难一言以蔽之，但通过一张图就可以直观理解他们之间的关系了。</p><p><img src="/postimgs/aigcintro/3.png" alt=""></p><h1 id="AI"><a href="#AI" class="headerlink" title="AI"></a>AI</h1><p>AI也叫人工智能，是计算机科学下的一个学科，旨在让计算机系统去模拟人类的智能，从而解决问题和完成任务。</p><p>早在1956年AI就被确立为了一个学科领域。在此后数十年间，经历过多轮低谷与繁荣。</p><blockquote><p>我们认为，学习的每个方面或智能的任何其他特性原则上都可以被如此精确地描述，以至于可以制造一台机器来模拟它。我们将尝试找出如何使机器使用语言、形成抽象和概念、解决现在仅保留给人类的问题，并提高自己。我们认为，如果一个精心挑选的科学家团队在夏天一起工作，那么这一个或多个问题上都可以取得重大进展。</p><p>——《达特茅斯人工智能夏季研究项目提案》(McCarthy等，1955年)</p></blockquote><h1 id="机器学习"><a href="#机器学习" class="headerlink" title="机器学习"></a>机器学习</h1><p>机器学习是AI的一个子集。它的核心在于不需要人类做显示编程，而是让计算机通过算法，自行学习和改进去识别模式，做出预测和决策。</p><p>比如，如果我们通过代码告诉电脑，图片里有红色说明是玫瑰，图片里有橙色说明是向日葵，那程序对花种类的判断，就是通过人类直接明确编写逻辑达成的，不属于机器学习，机器什么也没学。 </p><p><img src="/postimgs/aigcintro/5.png" alt=""></p><p>但如果我们给电脑大量玫瑰和向日葵的图片，让电脑自行识别模式总结规律，从而能对没见过的图片进行预测和判断，这种就是机器学习。</p><p><img src="/postimgs/aigcintro/6.png" alt=""></p><p>机器学习领域下有多个分支，包括监督学习、无监督学习、强化学习。</p><h1 id="监督学习"><a href="#监督学习" class="headerlink" title="监督学习"></a>监督学习</h1><p>在监督学习里，机器学习算法会接受有标签的训练数据。标签就是期望的输出值，所以每个训练数据点都既包括输入特征，也包括期望的输出值。</p><p>算法的目标是学习输入和输出之间的映射关系，从而在给定新的输入特征后，能够准确预测出相应的输出值。</p><p><img src="/postimgs/aigcintro/7.png" alt=""></p><p>经典的监督学习任务，包括分类，也就是把数据划分为不同的类别；以及回归，也就是对数值进行预测。</p><p><img src="/postimgs/aigcintro/8.png" alt=""></p><p>比如拿一堆猫猫狗狗的照片和照片，对应的猫狗标签进行训练，然后让模型根据没见过的照片，预测是猫还是狗，这就属于分类。</p><p><img src="/postimgs/aigcintro/9.png" alt=""></p><p>拿一些房子特征的数据，比如面积卧室数是否带阳台等和相应的房价作为标签进行训练，然后让模型根据没见过的房子的特征预测房价，这就属于回归。</p><p><img src="/postimgs/aigcintro/10.png" alt=""></p><h1 id="无监督学习"><a href="#无监督学习" class="headerlink" title="无监督学习"></a>无监督学习</h1><p>无监督学习和监督学习不同的是，它学习的数据是没有标签的。所以算法的任务是自主发现数据里的模式或规律。</p><p><img src="/postimgs/aigcintro/11.png" alt=""></p><p>经典的无监督学习任务，包括聚类，也就是把数据进行分组。比如拿一堆新闻文章，让模型根据主题或内容的特征，自动把相似文章进行组织。</p><h1 id="强化学习"><a href="#强化学习" class="headerlink" title="强化学习"></a>强化学习</h1><p>强化学习，则是让模型在环境里采取行动，获得结果反馈，从反馈里学习，从而能在给定情况下采取最佳行动来最大化奖励或是最小化损失。</p><p><img src="/postimgs/aigcintro/12.png" alt=""></p><p>所以就跟训小狗似的。刚开始的时候，小狗会随心所欲，做出很多动作。但随着和训犬师的互动，小狗会发现，某些动作能够获得零食，某些动作没有零食，某些动作甚至会遭受惩罚。通过观察动作和奖惩之间的联系。小狗的行为会逐渐接近训犬师的期望。</p><p>强化学习可以应用在很多任务上，比如说让模型下围棋，获得不同行动导致的奖励或损失反馈，从而在一局局游戏里优化策略学习如何采取行动达到高分。</p><h1 id="深度学习"><a href="#深度学习" class="headerlink" title="深度学习"></a>深度学习</h1><p>那么问题来了，深度学习属于这三类里的哪一类呢？它不属于里面的任何一类。深度学习是机器学习的一个方法。核心在于使用人工神经网络，模仿人脑处理信息的方式，通过层次化的方法提取和表示数据的特征。</p><p>神经网络是由许多基本的计算和储存单元组成，这些单元被称为神经元。这些神经元通过层层连接来处理数据，并且深度学习模型，通常有很多层，因此称为深度。</p><p>比如要让计算机识别小猫的照片，在深度学习中，数据首先被传递到一个输入层，就像人类的眼睛看到图片一样。然后数据通过多个隐藏层，每一层都会对数据进行一些复杂的数学运算，来帮助计算机理解图片中的特征，例如小猫的耳朵眼睛等等。</p><p>最后，计算机会输出一个答案，表明这是否是一张小猫的图片。</p><p><img src="/postimgs/aigcintro/13.png" alt=""></p><p>神经网络可以用于监督学习，无监督学习，强化学习。所以，深度学习不属于他们的子集。</p><h1 id="生成式AI"><a href="#生成式AI" class="headerlink" title="生成式AI"></a>生成式AI</h1><p>生成式AI是深度学习的一种应用。它利用神经网络来识别现有内容的模式和结构，学习生成新的内容。内容形式可以是文本、图片、音频等等。</p><p><img src="/postimgs/aigcintro/a14.png" alt=""></p><h1 id="大语言模型"><a href="#大语言模型" class="headerlink" title="大语言模型"></a>大语言模型</h1><p>大语言模型也叫LLM，Large Language Model，也是深度学习的一种应用，专门用于进行自然语言处理任务。</p><p>大语言模型里面的大字说明模型的参数量非常大，可能有数十亿甚至到万亿个，而且训练过程中也需要海量文本数据集，所以能更好的理解自然语言以及生成高质量的文本。</p><p><img src="/postimgs/aigcintro/14.png" alt=""></p><p>大语言模型的例子有非常多，比如国外的GPT、LLaMA，国内的ERNIE、ChatGLM等，可以进行文本的理解和生成。以GPT-3这个模型为例子，它会根据输入提示以及前面生成过的词，通过概率计算逐步生成下一个词或Token来输出文本序列。</p><p>但不是所有的生成式AI都是大语言模型，而所有的大语言模型是否都是生成式AI这也存在些许争议。</p><p>前半句很好理解，生成图像的扩散模型就不是大语言模型，它并不输出文本。同时，有些人认为不是所有大语言模型都是生成式AI。这是因为有些大语言模型，由于其架构特点不适合进行文本生成。</p><p><img src="/postimgs/aigcintro/15.png" alt=""></p><p>谷歌的BERT模型就是一个例子，它的参数量和训练数据很大，属于大语言模型。应用方面，BERT理解上下文的能力很强。因此，被谷歌用在搜索上，用来提高搜索排名和信息摘要的准确性，它也被用于情感分析、文本分类等任务。但同时，BERT的不擅长文本生成，特别是连贯的长文本生成。所以有些人认为，此类模型不属于生成式AI的范畴。</p><p><img src="/postimgs/aigcintro/16.png" alt=""></p><p>这些概念共同构成了生成式AI的核心要素，希望能帮助你对时下热门的AIGC建立更多了解。</p>]]></content>
    
    
    <summary type="html">说起近期的热门科技词汇，AIGC当之无愧位列其中，但你真的了解AIGC吗？</summary>
    
    
    
    <category term="AI" scheme="https://www.xdull.cn/categories/AI/"/>
    
    
    <category term="AI" scheme="https://www.xdull.cn/tags/AI/"/>
    
    <category term="人工智能" scheme="https://www.xdull.cn/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    
    <category term="AIGC" scheme="https://www.xdull.cn/tags/AIGC/"/>
    
  </entry>
  
  <entry>
    <title>从程序员的角度分析2024春晚刘谦魔术</title>
    <link href="https://www.xdull.cn/magic-card.html"/>
    <id>https://www.xdull.cn/magic-card.html</id>
    <published>2024-02-15T14:00:24.000Z</published>
    <updated>2026-07-22T15:15:28.793Z</updated>
    
    <content type="html"><![CDATA[<h2 id="打乱"><a href="#打乱" class="headerlink" title="打乱"></a>打乱</h2><p>4张牌打乱从中间剪开并叠放得到8张牌，牌的顺序无关紧要，但剪开后的牌上下叠放，则2张相同的牌相互之间一定相隔4张，不妨假设这8张牌的排列顺序为<code>ABCDABCD</code>。</p><h2 id="名字"><a href="#名字" class="headerlink" title="名字"></a>名字</h2><p>无论名字有几个字，即将几张牌放到底部（最末端）得到的依然是个循环序列。比如名字2个字得到<code>CDABCDAB</code>，名字3个字得到<code>DABCDABC</code>。</p><p>可以将这8张牌排成一个圆环，无论名字是几个字，也即无论从哪张牌作为圆环起点，始终都是在圆环中循环。</p><p><img src="/postimgs/magic-card/1.png" alt=""></p><h2 id="插入三张牌"><a href="#插入三张牌" class="headerlink" title="插入三张牌"></a>插入三张牌</h2><p>观察即可发现，从这个环中任意位置取走连续的3张，则剩下的5张牌都会形成首尾相同的一个序列：</p><p><img src="/postimgs/magic-card/2.png" alt=""></p><p>将取走的3张牌插入到序列的中间位置（注意不能插到最底部），则一定能得到一个由8张牌构成的并且首尾相同的序列<code>AxxxxxxA</code>。</p><h2 id="隐藏第一张牌"><a href="#隐藏第一张牌" class="headerlink" title="隐藏第一张牌"></a>隐藏第一张牌</h2><p>根据规则，将第1张牌隐藏起来，后续经过一系列摆放和丢弃操作，最终目的都是为了将最末尾的牌留下来，而这张牌正好和隐藏的牌相同，可拼在一起，实现了魔术效果。</p><p>由于最后结果只和序列最后一张牌有关，其他牌都需要丢弃，此时序列为长度为7的<code>xxxxxxA</code>，<code>x</code>表示其他牌，<code>A</code>表示隐藏牌。</p><h2 id="南北方"><a href="#南北方" class="headerlink" title="南北方"></a>南北方</h2><p>取出序列开头任意的1张2张或3张牌并插入中间，此时并没有改变序列最末尾的牌，而序列中其他牌的位置是对结果没有任何影响的，所以该操作可有可无，刘谦可能只是为了增加神秘感或者让观众更有参与感才加入该步骤。尼格买提可能正是在该步骤将牌插入了末尾，改变了序列中最末尾的牌才导致结果错误。此时序列仍然可看作是<code>xxxxxxA</code>。</p><h2 id="性别"><a href="#性别" class="headerlink" title="性别"></a>性别</h2><p>男生丢弃最开头1张牌，形成长度为6的序列<code>xxxxxA</code>；女生丢弃最开头的2张牌，形成长度为5的序列<code>xxxxA</code>。</p><h2 id="咒语"><a href="#咒语" class="headerlink" title="咒语"></a>咒语</h2><p>“见证奇迹的时刻”共7个字，则需要将序列最开头的牌移到最末尾共执行7次。男生的牌有6张，6次为一个闭环（6次后序列相当于未发生任何改变），则相当于执行<code>7-6=1</code>次；同理，女生的牌有5张，则5次为一个闭环，相对于执行<code>7-5=2</code>次。男女生的最终序列分别为<code>xxxxAx</code>和<code>xxAxx</code>。</p><h2 id="好运留下来，烦恼丢出去"><a href="#好运留下来，烦恼丢出去" class="headerlink" title="好运留下来，烦恼丢出去"></a>好运留下来，烦恼丢出去</h2><p>将序列第1张牌插入末尾，并丢弃第2张，将第3张牌插入末尾，并丢弃第4张，以此类推，直到剩下最后一张。这很容易让人想到一个通用问题，假如有n张牌排成一个圆环，并且从1到n按顺序排列，如果一直按上述规则操作，那么最后剩下那张牌的编号是几呢？</p><p>其实这个问题早就有人研究过了，它还有一个经典名字叫做<strong>约瑟夫环</strong>。答案就是，如果这个n恰好可以表示为2的多少次方，比如16正好是2的4次方，8正好是2的3次方，那么最终留下牌的一定是编号1。</p><p><img src="/postimgs/magic-card/3.png" alt=""></p><p>观察可发现，如果n正好是2的x次幂（$ n=2^x $），那么第一轮会正好丢弃掉 $ 2^{x-1} $张牌，同时第二轮依然会从编号1开始计数，然后丢弃掉 $ 2^{x-2} $ 张牌……，每一轮都会从编号1开始，直到最后丢弃 $ 2^0=1 $ 张牌只剩下编号1。</p><p>换个角度，如果一个数n正好是2的整数次方，那么这个数用二进制表示，它的数位中只会有一个1，比如16的二进制为<code>1000</code>，8的二进制位<code>100</code>，它们都只有最高位的一个1。</p><p>对于任意数n来说，我们都可以表示为 $ n=2^x+a $ 。我们可以考虑先丢弃掉a张牌，则剩下的 $ 2^x $ 张牌中，第1张牌一定是最终留下的那张，因为如果对剩下的牌重新从1开始编号，这张牌就是编号1。对于要丢弃掉a张牌，由于是留1去1，则需要数到编号2a，才能正好丢弃a张，所以编号<code>2a+1</code>则是剩下 $ 2^x $ 张牌中的第1张。</p><p>比如对于n=7，$ 7=2^2+3 $，a=3，丢弃3张牌好正好剩下 $ 2^2=4 $ 张牌，则走到编号6时正好剩下4张牌，而对于这剩下的4张牌重新编号，则编号7成为了编号1。所以n=7时，答案为7，也就是 $ 2a+1=2*3+1=7 $。</p><p><img src="/postimgs/magic-card/4.png" alt=""></p><p>也就是说对于任意数n，$ n=2^x+a $，只要算出a，那么就能计算出答案，为<code>2a+1</code>。</p><p>站在程序员的角度，a的值等于n去除二进制表示中最高位后的值，用代码实现约瑟夫环的这个问题为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">JosephRing</span>(<span class="hljs-params">n</span>):</span><br>    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">32</span>):  <span class="hljs-comment"># 假设n最大不超过32位</span><br>        j = <span class="hljs-number">1</span> &lt;&lt; (<span class="hljs-number">31</span> - i)<br>        <span class="hljs-keyword">if</span> n &amp; j:<br>            n = n ^ j    <span class="hljs-comment"># 移除最高位的1</span><br>            <span class="hljs-keyword">break</span><br>    <span class="hljs-keyword">return</span> n * <span class="hljs-number">2</span> + <span class="hljs-number">1</span><br></code></pre></td></tr></table></figure><p>回到魔术中，男生的序列为<code>xxxxAx</code>，长度为6，相当于<code>n=6</code>，计算结果为5，而<code>A</code>对应的编号正好是5；同理女生的序列为<code>xxAxx</code>，长度为5，<code>n=5</code>计算结果为3，<code>A</code>对应的编号正好是3，所以最终一定是<code>A</code>这张牌保留到最后。</p><p>以上，就是整个魔术的秘密。</p>]]></content>
    
    
    <summary type="html">原来刘谦不是会魔法，而是会算法。</summary>
    
    
    
    <category term="数学" scheme="https://www.xdull.cn/categories/%E6%95%B0%E5%AD%A6/"/>
    
    
    <category term="数学" scheme="https://www.xdull.cn/tags/%E6%95%B0%E5%AD%A6/"/>
    
    <category term="2024春晚魔术" scheme="https://www.xdull.cn/tags/2024%E6%98%A5%E6%99%9A%E9%AD%94%E6%9C%AF/"/>
    
    <category term="约瑟夫环" scheme="https://www.xdull.cn/tags/%E7%BA%A6%E7%91%9F%E5%A4%AB%E7%8E%AF/"/>
    
  </entry>
  
  <entry>
    <title>深入理解Java并发包(JUC)中的锁与同步器</title>
    <link href="https://www.xdull.cn/juc-lock.html"/>
    <id>https://www.xdull.cn/juc-lock.html</id>
    <published>2024-01-29T17:53:30.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在现代多核CPU架构下，并发编程已成为提升应用程序性能的关键技术。Java的<code>java.util.concurrent</code>(JUC)包提供了丰富的工具类来帮助开发者更安全、更高效地处理并发问题。本文将深入探讨JUC中几个常用的锁和同步器：<code>ReentrantReadWriteLock</code>、<code>StampedLock</code>、<code>Semaphore</code>、<code>CountDownLatch</code>和<code>CyclicBarrier</code>，理解它们的设计理念、使用场景及注意事项。</p><h2 id="ReentrantReadWriteLock：读写分离锁"><a href="#ReentrantReadWriteLock：读写分离锁" class="headerlink" title="ReentrantReadWriteLock：读写分离锁"></a>ReentrantReadWriteLock：读写分离锁</h2><p><code>ReentrantReadWriteLock</code>是一种读写分离锁，它允许多个线程同时读取共享资源，但在写入时则需要独占访问。这在“读多写少”的场景下能显著提高性能，因为它避免了不必要的阻塞。</p><p><img src="" alt="读写锁原理图-Illustration showing multiple reader threads accessing a shared resource concurrently, while a single writer thread waits, conceptual lock diagram"></p><p><strong>核心特性：</strong></p><ol><li><strong>读锁与写锁互斥</strong>：当一个线程持有写锁时，其他线程（无论是读线程还是写线程）都必须等待。当一个线程持有读锁时，写线程必须等待。</li><li><strong>写锁与写锁互斥</strong>：同一时间只允许一个线程持有写锁。</li><li><strong>读锁与读锁不互斥</strong>：多个线程可以同时持有读锁，并发读取。</li></ol><p><strong>锁升级与降级：</strong></p><ul><li><strong>锁升级（不支持）</strong>：持有<strong>读锁</strong>的线程尝试获取<strong>写锁</strong>，会导致死锁或永久等待。这是因为获取写锁需要等待所有读锁释放，而当前线程持有的读锁永远无法释放（因为它在等待写锁）。</li><li><strong>锁降级（支持）</strong>：持有<strong>写锁</strong>的线程可以继续获取<strong>读锁</strong>，然后释放写锁。这样就从写锁降级为了读锁，允许其他读线程进入，但仍阻止其他写线程。</li></ul><p><strong>示例代码：</strong></p><p>下面是一个简单的示例，演示了<code>ReentrantReadWriteLock</code>的基本用法。<code>DataContainer</code>类使用读写锁来保护共享数据<code>data</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.slf4j.Logger;<br><span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;<br><span class="hljs-keyword">import</span> java.util.concurrent.TimeUnit;<br><span class="hljs-keyword">import</span> java.util.concurrent.locks.ReentrantReadWriteLock;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.TestReadWriteLock&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestReadWriteLock</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(TestReadWriteLock.class);<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> InterruptedException </span>&#123;<br>        DataContainer dataContainer = <span class="hljs-keyword">new</span> DataContainer();<br><br>        <span class="hljs-comment">// 启动两个读线程</span><br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.read();<br>        &#125;, <span class="hljs-string">&quot;t1-Reader&quot;</span>).start();<br><br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.read();<br>        &#125;, <span class="hljs-string">&quot;t2-Reader&quot;</span>).start();<br><br>        <span class="hljs-comment">// 稍等片刻，启动一个写线程</span><br>        TimeUnit.MILLISECONDS.sleep(<span class="hljs-number">100</span>);<br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.write(<span class="hljs-string">&quot;New Data&quot;</span>);<br>        &#125;, <span class="hljs-string">&quot;t3-Writer&quot;</span>).start();<br><br>        <span class="hljs-comment">// 再启动一个读线程，观察其是否被写锁阻塞</span><br>         TimeUnit.MILLISECONDS.sleep(<span class="hljs-number">100</span>);<br>         <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.read();<br>        &#125;, <span class="hljs-string">&quot;t4-Reader&quot;</span>).start();<br>    &#125;<br><br>    <span class="hljs-comment">// 模拟休眠，简化代码</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.DataContainer&quot;)</span><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DataContainer</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(DataContainer.class);<br>    <span class="hljs-keyword">private</span> Object data = <span class="hljs-string">&quot;Initial Data&quot;</span>;<br>    <span class="hljs-comment">// 1. 创建读写锁对象</span><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReentrantReadWriteLock rw = <span class="hljs-keyword">new</span> ReentrantReadWriteLock();<br>    <span class="hljs-comment">// 2. 获取读锁和写锁实例</span><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReentrantReadWriteLock.ReadLock r = rw.readLock();<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReentrantReadWriteLock.WriteLock w = rw.writeLock();<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">read</span><span class="hljs-params">()</span> </span>&#123;<br>        log.debug(<span class="hljs-string">&quot;尝试获取读锁...&quot;</span>);<br>        r.lock(); <span class="hljs-comment">// 3. 加读锁</span><br>        <span class="hljs-keyword">try</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;成功获取读锁，开始读取...&quot;</span>);<br>            sleep(<span class="hljs-number">1</span>); <span class="hljs-comment">// 模拟读取耗时</span><br>            log.debug(<span class="hljs-string">&quot;读取完成, data: &#123;&#125;&quot;</span>, data);<br>            <span class="hljs-keyword">return</span> data;<br>        &#125; <span class="hljs-keyword">finally</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;准备释放读锁...&quot;</span>);<br>            r.unlock(); <span class="hljs-comment">// 4. 释放读锁</span><br>        &#125;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">write</span><span class="hljs-params">(Object newData)</span> </span>&#123;<br>        log.debug(<span class="hljs-string">&quot;尝试获取写锁...&quot;</span>);<br>        w.lock(); <span class="hljs-comment">// 3. 加写锁</span><br>        <span class="hljs-keyword">try</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;成功获取写锁，开始写入...&quot;</span>);<br>            <span class="hljs-keyword">this</span>.data = newData;<br>            sleep(<span class="hljs-number">1</span>); <span class="hljs-comment">// 模拟写入耗时</span><br>            log.debug(<span class="hljs-string">&quot;写入完成, data: &#123;&#125;&quot;</span>, data);<br>        &#125; <span class="hljs-keyword">finally</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;准备释放写锁...&quot;</span>);<br>            w.unlock(); <span class="hljs-comment">// 4. 释放写锁</span><br>        &#125;<br>    &#125;<br><br>    <span class="hljs-comment">// 模拟休眠</span><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>运行结果分析：</strong></p><p>你会观察到 <code>t1-Reader</code> 和 <code>t2-Reader</code> 可以并发执行读取操作。当 <code>t3-Writer</code> 尝试获取写锁时，它会等待 <code>t1</code> 和 <code>t2</code> 释放读锁。一旦 <code>t3</code> 持有写锁，后续的 <code>t4-Reader</code> 会被阻塞，直到 <code>t3</code> 释放写锁。</p><h2 id="StampedLock：带版本戳的乐观读写锁"><a href="#StampedLock：带版本戳的乐观读写锁" class="headerlink" title="StampedLock：带版本戳的乐观读写锁"></a>StampedLock：带版本戳的乐观读写锁</h2><p><code>StampedLock</code> 是 JDK 8 引入的一种更高级的锁，它在<code>ReentrantReadWriteLock</code>的基础上进行了优化，特别是在读操作远多于写操作的场景下。<code>StampedLock</code> 提供了<strong>乐观读</strong>（Optimistic Reading）的模式。</p><p><strong>核心概念：</strong></p><ul><li><p><strong>戳（Stamp）</strong>：每次获取锁（读锁、写锁或乐观读）都会返回一个非零的版本戳。释放锁或转换锁状态时需要传入相应的戳。</p></li><li><p><strong>乐观读 (<code>tryOptimisticRead</code>)</strong>：尝试获取一个非独占的读锁，但实际上不加锁，只是获取当前的版本戳。之后，线程可以读取共享资源。读取完成后，需要调用<code>validate(stamp)</code>方法检查在读取期间是否有写操作介入（即版本戳是否发生变化）。</p></li><li><p>如果<code>validate</code>返回<code>true</code>，表示没有写操作干扰，读取的数据有效，性能极高（无锁操作）。</p><ul><li>如果<code>validate</code>返回<code>false</code>，表示有写操作干扰，数据可能已失效，此时需要升级为悲观读锁（调用<code>readLock()</code>）重新读取数据。</li></ul></li><li><p><strong>悲观读锁 (<code>readLock</code>)</strong>：类似<code>ReentrantReadWriteLock</code>的读锁，会阻塞写操作，但不阻塞其他读操作。</p></li><li><p><strong>写锁 (<code>writeLock</code>)</strong>：类似<code>ReentrantReadWriteLock</code>的写锁，独占访问。</p></li></ul><p><img src="https://askmanyai.cn/chat/bb268aa9-f501-49d9-bbf2-1e82d3bf34fc" alt="StampedLock乐观读流程图-Flowchart diagram illustrating the StampedLock optimistic read process: tryOptimisticRead, read data, validate stamp, if valid use data, if invalid acquire readLock and re-read"></p><p><strong>优点：</strong></p><ul><li>在读多写少且写操作时间短的场景下，乐观读能大幅减少锁竞争，提升吞吐量。</li></ul><p><strong>缺点：</strong></p><ul><li><code>StampedLock</code>是<strong>不可重入</strong>的。如果一个线程已经持有写锁，再尝试获取写锁会导致死锁；持有读锁再尝试获取读锁也可能产生问题。</li><li>不支持条件变量（Condition）。</li><li>使用相对复杂，需要正确处理乐观读失败后的锁升级逻辑。</li></ul><p><strong>示例代码：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.slf4j.Logger;<br><span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;<br><span class="hljs-keyword">import</span> java.util.concurrent.TimeUnit;<br><span class="hljs-keyword">import</span> java.util.concurrent.locks.StampedLock;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.TestStampedLock&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestStampedLock</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(TestStampedLock.class);<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>&#123;<br>        DataContainerStamped dataContainer = <span class="hljs-keyword">new</span> DataContainerStamped(<span class="hljs-number">1</span>);<br><br>        <span class="hljs-comment">// 线程t1进行一次快速读取（乐观读成功）</span><br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.read(<span class="hljs-number">1</span>); <span class="hljs-comment">// 模拟读取时间1秒</span><br>        &#125;, <span class="hljs-string">&quot;t1-optimistic-success&quot;</span>).start();<br><br>        sleep(<span class="hljs-number">0.1</span>); <span class="hljs-comment">// 确保t1开始</span><br><br>        <span class="hljs-comment">// 线程t-writer进行写操作</span><br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            log.debug(<span class="hljs-string">&quot;Writer thread starting write operation...&quot;</span>);<br>            dataContainer.write(<span class="hljs-number">99</span>);<br>        &#125;, <span class="hljs-string">&quot;t-writer&quot;</span>).start();<br><br>        sleep(<span class="hljs-number">0.5</span>); <span class="hljs-comment">// 确保写操作有机会介入</span><br><br>        <span class="hljs-comment">// 线程t2进行一次较慢的读取（乐观读失败，升级为读锁）</span><br>        <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>            dataContainer.read(<span class="hljs-number">2</span>); <span class="hljs-comment">// 模拟读取时间2秒，期间writer会修改数据</span><br>        &#125;, <span class="hljs-string">&quot;t2-optimistic-fail&quot;</span>).start();<br>    &#125;<br><br>     <span class="hljs-comment">// 模拟休眠，简化代码</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.DataContainerStamped&quot;)</span><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DataContainerStamped</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(DataContainerStamped.class);<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> data;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> StampedLock lock = <span class="hljs-keyword">new</span> StampedLock();<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DataContainerStamped</span><span class="hljs-params">(<span class="hljs-keyword">int</span> data)</span> </span>&#123;<br>        <span class="hljs-keyword">this</span>.data = data;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">(<span class="hljs-keyword">int</span> readTime)</span> </span>&#123;<br>        <span class="hljs-keyword">long</span> stamp = lock.tryOptimisticRead(); <span class="hljs-comment">// 1. 尝试乐观读，获取戳</span><br>        log.debug(<span class="hljs-string">&quot;Optimistic read attempt, stamp: &#123;&#125;&quot;</span>, stamp);<br>        sleep(readTime); <span class="hljs-comment">// 模拟读取操作耗时</span><br><br>        <span class="hljs-comment">// 2. 校验戳，检查读取期间是否有写操作介入</span><br>        <span class="hljs-keyword">if</span> (lock.validate(stamp)) &#123;<br>            log.debug(<span class="hljs-string">&quot;Optimistic read validation successful (stamp: &#123;&#125;), data: &#123;&#125;&quot;</span>, stamp, data);<br>            <span class="hljs-keyword">return</span> data; <span class="hljs-comment">// 校验成功，数据有效</span><br>        &#125;<br><br>        <span class="hljs-comment">// 3. 乐观读失败，升级为悲观读锁</span><br>        log.debug(<span class="hljs-string">&quot;Optimistic read validation failed (stamp: &#123;&#125;), upgrading to read lock...&quot;</span>, stamp);<br>        <span class="hljs-keyword">try</span> &#123;<br>            stamp = lock.readLock(); <span class="hljs-comment">// 获取悲观读锁，阻塞写操作</span><br>            log.debug(<span class="hljs-string">&quot;Acquired read lock, stamp: &#123;&#125;&quot;</span>, stamp);<br>            sleep(readTime); <span class="hljs-comment">// 模拟重新读取耗时</span><br>            log.debug(<span class="hljs-string">&quot;Read finished under read lock (stamp: &#123;&#125;), data: &#123;&#125;&quot;</span>, stamp, data);<br>            <span class="hljs-keyword">return</span> data; <span class="hljs-comment">// 返回在读锁保护下读取的数据</span><br>        &#125; <span class="hljs-keyword">finally</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;Releasing read lock, stamp: &#123;&#125;&quot;</span>, stamp);<br>            lock.unlockRead(stamp); <span class="hljs-comment">// 4. 释放悲观读锁</span><br>        &#125;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">write</span><span class="hljs-params">(<span class="hljs-keyword">int</span> newData)</span> </span>&#123;<br>        <span class="hljs-keyword">long</span> stamp = lock.writeLock(); <span class="hljs-comment">// 1. 获取写锁，阻塞所有读写</span><br>        log.debug(<span class="hljs-string">&quot;Acquired write lock, stamp: &#123;&#125;&quot;</span>, stamp);<br>        <span class="hljs-keyword">try</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;Writing data...&quot;</span>);<br>            sleep(<span class="hljs-number">2</span>); <span class="hljs-comment">// 模拟写入耗时</span><br>            <span class="hljs-keyword">this</span>.data = newData;<br>            log.debug(<span class="hljs-string">&quot;Write finished, new data: &#123;&#125;&quot;</span>, data);<br>        &#125; <span class="hljs-keyword">finally</span> &#123;<br>            log.debug(<span class="hljs-string">&quot;Releasing write lock, stamp: &#123;&#125;&quot;</span>, stamp);<br>            lock.unlockWrite(stamp); <span class="hljs-comment">// 2. 释放写锁</span><br>        &#125;<br>    &#125;<br><br>     <span class="hljs-comment">// 模拟休眠</span><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>运行结果分析：</strong></p><ul><li><code>t1-optimistic-success</code> 线程尝试乐观读，由于读取期间没有写操作，<code>validate</code>成功，快速返回。</li><li><code>t-writer</code> 获取写锁并修改数据。</li><li><code>t2-optimistic-fail</code> 线程尝试乐观读，但在其读取期间（<code>sleep(2)</code>），<code>t-writer</code> 修改了数据，导致<code>validate</code>失败。随后 <code>t2</code> 升级为悲观读锁，等待 <code>t-writer</code> 释放写锁后，再重新获取数据。</li></ul><h2 id="Semaphore：信号量"><a href="#Semaphore：信号量" class="headerlink" title="Semaphore：信号量"></a>Semaphore：信号量</h2><p><code>Semaphore</code>（信号量）用于控制<strong>同时访问特定资源</strong>的线程数量。它维护了一个许可（permit）集，线程必须先获得许可才能访问资源，使用完毕后释放许可。如果许可数量为0，则尝试获取许可的线程将被阻塞，直到有其他线程释放许可。</p><p><img src="https://askmanyai.cn/chat/bb268aa9-f501-49d9-bbf2-1e82d3bf34fc" alt="信号量示意图-Illustration of a Semaphore controlling access to limited resources, showing threads waiting and acquiring permits, like a gatekeeper"></p><p><strong>常用场景：</strong></p><ul><li><strong>限流</strong>：控制对某个服务的并发调用量。</li><li><strong>资源池</strong>：管理有限的资源，如数据库连接池。</li></ul><p><strong>核心方法：</strong></p><ul><li><code>Semaphore(int permits)</code>：构造函数，初始化许可数量。</li><li><code>acquire()</code>：获取一个许可，如果无可用许可则阻塞。</li><li><code>acquire(int permits)</code>：获取指定数量的许可。</li><li><code>release()</code>：释放一个许可。</li><li><code>release(int permits)</code>：释放指定数量的许可。</li><li><code>tryAcquire()</code>：尝试获取一个许可，立即返回成功或失败，不阻塞。</li></ul><p><strong>示例代码：</strong></p><p>下面的示例模拟了10个线程尝试访问一个只有3个许可的资源。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.slf4j.Logger;<br><span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;<br><span class="hljs-keyword">import</span> java.util.concurrent.Semaphore;<br><span class="hljs-keyword">import</span> java.util.concurrent.TimeUnit;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.TestSemaphore&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestSemaphore</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(TestSemaphore.class);<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>&#123;<br>        <span class="hljs-comment">// 1. 创建 Semaphore 对象，设置许可数量为 3</span><br>        Semaphore semaphore = <span class="hljs-keyword">new</span> Semaphore(<span class="hljs-number">3</span>);<br><br>        <span class="hljs-comment">// 2. 创建 10 个线程尝试获取许可并执行任务</span><br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) &#123;<br>            <span class="hljs-keyword">new</span> Thread(() -&gt; &#123;<br>                <span class="hljs-keyword">try</span> &#123;<br>                    <span class="hljs-comment">// 3. 尝试获取许可，获取不到则阻塞</span><br>                    semaphore.acquire();<br>                    log.debug(<span class="hljs-string">&quot; acquired permit, running...&quot;</span>);<br>                    sleep(<span class="hljs-number">1</span>); <span class="hljs-comment">// 模拟执行任务</span><br>                    log.debug(<span class="hljs-string">&quot; task finished, releasing permit...&quot;</span>);<br>                &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>                    log.error(<span class="hljs-string">&quot;Thread interrupted&quot;</span>, e);<br>                    Thread.currentThread().interrupt();<br>                &#125; <span class="hljs-keyword">finally</span> &#123;<br>                    <span class="hljs-comment">// 4. 释放许可</span><br>                    semaphore.release();<br>                &#125;<br>            &#125;, <span class="hljs-string">&quot;Thread-&quot;</span> + i).start();<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-comment">// 模拟休眠，简化代码</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>运行结果分析：</strong></p><p>同一时间内，最多只有3个线程能够获得许可并打印 “running…”。其他线程会阻塞在 <code>acquire()</code> 方法上，直到有线程调用 <code>release()</code> 释放许可。</p><h2 id="CountDownLatch：倒计时门闩"><a href="#CountDownLatch：倒计时门闩" class="headerlink" title="CountDownLatch：倒计时门闩"></a>CountDownLatch：倒计时门闩</h2><p><code>CountDownLatch</code> 是一种同步辅助类，它允许一个或多个线程等待其他一组线程完成操作。它像一个倒计时器，主线程（或任何需要等待的线程）调用 <code>await()</code> 方法阻塞，直到计数器减到零。其他线程则通过调用 <code>countDown()</code> 方法来使计数器减一。</p><p><img src="https://askmanyai.cn/chat/bb268aa9-f501-49d9-bbf2-1e82d3bf34fc" alt="CountDownLatch示意图-Illustration showing multiple worker threads decrementing a counter (CountDownLatch), while a main thread waits (await) until the counter reaches zero, like a starting gate"></p><p><strong>核心特性：</strong></p><ul><li><strong>一次性</strong>：计数器的值只能被初始化一次，并且一旦减到零，就不能再重置或增加。</li><li><strong>等待机制</strong>：<code>await()</code> 方法阻塞调用线程，直到计数器为零。</li><li><strong>计数递减</strong>：<code>countDown()</code> 方法将计数器减一。</li></ul><p><strong>常用场景：</strong></p><ul><li>等待多个初始化任务完成：主线程等待所有初始化子线程完成后再继续执行。</li><li>模拟并发：确保多个线程同时开始执行某个任务。</li></ul><p><strong>示例代码：</strong></p><p>下面的示例模拟了一个游戏加载场景，主线程需要等待10个加载任务（每个任务在一个线程中执行）全部完成后才能开始游戏。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.slf4j.Logger;<br><span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;<br><span class="hljs-keyword">import</span> java.util.Arrays;<br><span class="hljs-keyword">import</span> java.util.Random;<br><span class="hljs-keyword">import</span> java.util.concurrent.CountDownLatch;<br><span class="hljs-keyword">import</span> java.util.concurrent.ExecutorService;<br><span class="hljs-keyword">import</span> java.util.concurrent.Executors;<br><span class="hljs-keyword">import</span> java.util.concurrent.TimeUnit;<br><span class="hljs-keyword">import</span> java.util.concurrent.atomic.AtomicInteger;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestCountDownLatch</span> </span>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(TestCountDownLatch.class);<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> InterruptedException </span>&#123;<br>        testLoadingGame();<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testLoadingGame</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> InterruptedException </span>&#123;<br>        AtomicInteger threadCounter = <span class="hljs-keyword">new</span> AtomicInteger(<span class="hljs-number">0</span>);<br>        ExecutorService service = Executors.newFixedThreadPool(<span class="hljs-number">10</span>, (r) -&gt; &#123;<br>            <span class="hljs-comment">// 自定义线程名</span><br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Thread(r, <span class="hljs-string">&quot;loader-&quot;</span> + threadCounter.getAndIncrement());<br>        &#125;);<br><br>        <span class="hljs-comment">// 1. 初始化 CountDownLatch，计数为 10</span><br>        CountDownLatch latch = <span class="hljs-keyword">new</span> CountDownLatch(<span class="hljs-number">10</span>);<br>        String[] loadingProgress = <span class="hljs-keyword">new</span> String[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 用于显示加载进度</span><br>        Arrays.fill(loadingProgress, <span class="hljs-string">&quot;0%&quot;</span>); <span class="hljs-comment">// 初始化进度</span><br><br>        Random random = <span class="hljs-keyword">new</span> Random();<br>        log.info(<span class="hljs-string">&quot;Starting game loading process...&quot;</span>);<br><br>        <span class="hljs-comment">// 2. 提交 10 个加载任务到线程池</span><br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">10</span>; j++) &#123;<br>            <span class="hljs-keyword">int</span> taskIndex = j; <span class="hljs-comment">// lambda 中需要 final 或 effectively final 变量</span><br>            service.submit(() -&gt; &#123;<br>                <span class="hljs-keyword">try</span> &#123;<br>                    log.debug(<span class="hljs-string">&quot;&#123;&#125; starting loading...&quot;</span>, Thread.currentThread().getName());<br>                    <span class="hljs-comment">// 模拟加载过程</span><br>                    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt;= <span class="hljs-number">100</span>; i++) &#123;<br>                        <span class="hljs-keyword">try</span> &#123;<br>                            TimeUnit.MILLISECONDS.sleep(random.nextInt(<span class="hljs-number">50</span>)); <span class="hljs-comment">// 随机加载耗时</span><br>                        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>                             Thread.currentThread().interrupt();<br>                        &#125;<br>                        <span class="hljs-comment">// 更新进度（注意：这里的更新方式不是线程安全的，仅为演示）</span><br>                        <span class="hljs-comment">// 在实际应用中，更新共享状态需要同步或使用并发集合</span><br>                        <span class="hljs-keyword">synchronized</span> (loadingProgress) &#123;<br>                             loadingProgress[taskIndex] = Thread.currentThread().getName() + <span class="hljs-string">&quot;(&quot;</span> + i + <span class="hljs-string">&quot;%)&quot;</span>;<br>                             <span class="hljs-comment">// 打印进度条（\r 实现原地更新）</span><br>                             System.out.print(<span class="hljs-string">&quot;\rLoading progress: &quot;</span> + Arrays.toString(loadingProgress));<br>                        &#125;<br>                    &#125;<br>                    log.debug(<span class="hljs-string">&quot;&#123;&#125; finished loading.&quot;</span>, Thread.currentThread().getName());<br>                &#125; <span class="hljs-keyword">finally</span> &#123;<br>                    <span class="hljs-comment">// 3. 任务完成，计数器减一</span><br>                    latch.countDown();<br>                &#125;<br>            &#125;);<br>        &#125;<br><br>        <span class="hljs-comment">// 4. 主线程调用 await() 等待所有加载任务完成（计数器归零）</span><br>        log.info(<span class="hljs-string">&quot;Main thread waiting for all loading tasks to complete...&quot;</span>);<br>        latch.await(); <span class="hljs-comment">// 阻塞直到 latch 计数为 0</span><br><br>        System.out.println(); <span class="hljs-comment">// 换行</span><br>        log.info(<span class="hljs-string">&quot;All loading tasks completed. Game starting...&quot;</span>);<br>        service.shutdown(); <span class="hljs-comment">// 关闭线程池</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>运行结果分析：</strong></p><p>主线程会打印 “Main thread waiting…” 并阻塞在 <code>latch.await()</code>。同时，10个加载线程并发执行，每个线程完成加载后调用 <code>latch.countDown()</code>。当第10个线程完成并调用 <code>countDown()</code> 后，计数器变为0，<code>await()</code> 方法返回，主线程继续执行，打印 “Game starting…”。</p><h2 id="CyclicBarrier：循环栅栏"><a href="#CyclicBarrier：循环栅栏" class="headerlink" title="CyclicBarrier：循环栅栏"></a>CyclicBarrier：循环栅栏</h2><p><code>CyclicBarrier</code>（循环栅栏）与<code>CountDownLatch</code>类似，也是用于线程协作，但它更像一个<strong>可重用</strong>的屏障。它允许一组线程相互等待，直到所有线程都到达某个公共屏障点（barrier point），然后所有线程再同时继续执行。</p><p><img src="https://askmanyai.cn/chat/bb268aa9-f501-49d9-bbf2-1e82d3bf34fc" alt="CyclicBarrier示意图-Illustration showing multiple threads arriving at a barrier (CyclicBarrier), waiting for each other, once all arrive the barrier breaks, and they proceed together. The barrier can reset."></p><p><strong>核心特性：</strong></p><ul><li><strong>计数等待</strong>：构造时指定参与的线程数（parties）。线程调用 <code>await()</code> 方法到达屏障，并阻塞等待，直到指定数量的线程都调用了<code>await()</code>。</li><li><strong>屏障动作（可选）</strong>：可以在构造时提供一个 <code>Runnable</code> 任务，当所有线程都到达屏障后，在任何线程被释放之前，会由<strong>最后一个到达屏障的线程</strong>执行这个任务。</li><li><strong>可重用</strong>：当所有等待线程被释放后，<code>CyclicBarrier</code> 可以被重置（自动或手动），用于下一轮的同步。这就像“人满发车”，发车后可以等待下一批人。</li></ul><p><strong>常用场景：</strong></p><ul><li>多线程计算：将一个大任务分解给多个线程，等待所有线程计算完成中间结果后，再进行汇总或下一步计算。</li><li>并行测试：确保所有测试线程都准备好后再开始执行测试。</li></ul><p><strong><code>CyclicBarrier</code> vs <code>CountDownLatch</code>：</strong></p><ul><li><code>CountDownLatch</code> 是一次性的，计数减到0后不能重置；<code>CyclicBarrier</code> 是可重用的。</li><li><code>CountDownLatch</code> 主要用于一个或多个线程等待其他线程完成某事；<code>CyclicBarrier</code> 用于一组线程相互等待，直到所有成员都到达某个点。</li></ul><p><strong>示例代码：</strong></p><p>下面的示例模拟了两个任务（task1, task2）需要协同工作，每一轮都需要双方都完成后才能进入下一轮。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.slf4j.Logger;<br><span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;<br><span class="hljs-keyword">import</span> java.util.concurrent.*;<br><br><span class="hljs-comment">// 使用Slf4j或其他日志框架</span><br><span class="hljs-comment">// import lombok.extern.slf4j.Slf4j;</span><br><span class="hljs-comment">// @Slf4j(topic = &quot;c.TestCyclicBarrier&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestCyclicBarrier</span> </span>&#123;<br>     <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger log = LoggerFactory.getLogger(TestCyclicBarrier.class);<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>&#123;<br>        ExecutorService service = Executors.newFixedThreadPool(<span class="hljs-number">2</span>);<br><br>        <span class="hljs-comment">// 1. 创建 CyclicBarrier，指定参与者数量为 2，并提供一个屏障动作</span><br>        CyclicBarrier barrier = <span class="hljs-keyword">new</span> CyclicBarrier(<span class="hljs-number">2</span>, () -&gt; &#123;<br>            <span class="hljs-comment">// 这个 Runnable 会在两个线程都到达屏障后，由最后一个到达的线程执行</span><br>            log.info(<span class="hljs-string">&quot;--- Both task1 and task2 reached the barrier. Barrier action executed. Proceeding to next round... ---&quot;</span>);<br>        &#125;);<br><br>        <span class="hljs-comment">// 模拟执行 3 轮任务</span><br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++) &#123;<br>            log.info(<span class="hljs-string">&quot;Starting round &#123;&#125;&quot;</span>, i + <span class="hljs-number">1</span>);<br>            <span class="hljs-comment">// 提交 task1</span><br>            service.submit(() -&gt; &#123;<br>                <span class="hljs-keyword">try</span> &#123;<br>                    log.debug(<span class="hljs-string">&quot;task1 starting work for round...&quot;</span>);<br>                    sleep(<span class="hljs-number">1</span>); <span class="hljs-comment">// 模拟 task1 工作耗时</span><br>                    log.debug(<span class="hljs-string">&quot;task1 finished work, waiting at barrier...&quot;</span>);<br>                    <span class="hljs-comment">// 2. task1 到达屏障，等待 task2</span><br>                    barrier.await(); <span class="hljs-comment">// 如果是第一个到达，会阻塞；如果是第二个到达，会唤醒等待的线程，并触发屏障动作</span><br>                    log.debug(<span class="hljs-string">&quot;task1 passed barrier.&quot;</span>);<br>                &#125; <span class="hljs-keyword">catch</span> (InterruptedException | BrokenBarrierException e) &#123;<br>                     log.error(<span class="hljs-string">&quot;Task1 interrupted or barrier broken&quot;</span>, e);<br>                     Thread.currentThread().interrupt();<br>                &#125;<br>            &#125;);<br><br>            <span class="hljs-comment">// 提交 task2</span><br>            service.submit(() -&gt; &#123;<br>                <span class="hljs-keyword">try</span> &#123;<br>                    log.debug(<span class="hljs-string">&quot;task2 starting work for round...&quot;</span>);<br>                    sleep(<span class="hljs-number">2</span>); <span class="hljs-comment">// 模拟 task2 工作耗时 (比 task1 长)</span><br>                    log.debug(<span class="hljs-string">&quot;task2 finished work, waiting at barrier...&quot;</span>);<br>                    <span class="hljs-comment">// 2. task2 到达屏障，等待 task1</span><br>                    barrier.await();<br>                    log.debug(<span class="hljs-string">&quot;task2 passed barrier.&quot;</span>);<br>                &#125; <span class="hljs-keyword">catch</span> (InterruptedException | BrokenBarrierException e) &#123;<br>                    log.error(<span class="hljs-string">&quot;Task2 interrupted or barrier broken&quot;</span>, e);<br>                    Thread.currentThread().interrupt();<br>                &#125;<br>            &#125;);<br><br>            <span class="hljs-comment">// 等待一轮结束，为了清晰观察日志</span><br>             sleep(<span class="hljs-number">3</span>);<br>        &#125;<br><br>        service.shutdown();<br>    &#125;<br><br>    <span class="hljs-comment">// 模拟休眠，简化代码</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">(<span class="hljs-keyword">double</span> seconds)</span> </span>&#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            TimeUnit.MILLISECONDS.sleep((<span class="hljs-keyword">long</span>) (seconds * <span class="hljs-number">1000</span>));<br>        &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>            Thread.currentThread().interrupt();<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>运行结果分析：</strong></p><p>在每一轮中，<code>task1</code> 先完成工作并调用 <code>await()</code> 等待。<code>task2</code> 稍后完成工作并调用 <code>await()</code>。当 <code>task2</code> 调用 <code>await()</code> 时，满足了屏障所需的2个线程数，此时：</p><ol><li>屏障动作（打印 “— Both task1 and task2 reached…”）被执行。</li><li><code>task1</code> 和 <code>task2</code> 都从 <code>await()</code> 调用中返回，继续执行后续代码（打印 “passed barrier.”）。</li><li><code>CyclicBarrier</code> 自动重置，可以开始下一轮的等待。 这个过程会重复3次。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Java并发包（JUC）提供了强大的工具来应对并发编程的挑战。</p><ul><li><strong><code>ReentrantReadWriteLock</code></strong>：适用于读多写少的场景，通过读写分离提高性能。注意锁升级不支持，锁降级支持。</li><li><strong><code>StampedLock</code></strong>：JDK 8 引入的优化读写锁，提供乐观读模式，在特定场景下性能优于<code>ReentrantReadWriteLock</code>，但不可重入且使用更复杂。</li><li><strong><code>Semaphore</code></strong>：控制对有限资源的并发访问数量，常用于限流和资源池管理。</li><li><strong><code>CountDownLatch</code></strong>：一次性的同步工具，用于等待一组操作完成。</li><li><strong><code>CyclicBarrier</code></strong>：可重用的同步工具，用于让一组线程在某个点相互等待，达到同步执行的效果。</li></ul><p>理解这些工具的特性和适用场景，是编写高效、健壮的并发程序的关键。根据具体的业务需求选择合适的并发工具，能够有效提升系统性能和可靠性。</p>]]></content>
    
    
    <summary type="html">本文深入探讨了Java并发包（JUC）中的几种核心锁和同步器，通过原理讲解和代码示例，帮助开发者理解它们的使用场景和特性，提升并发编程能力。</summary>
    
    
    
    <category term="Java并发" scheme="https://www.xdull.cn/categories/Java%E5%B9%B6%E5%8F%91/"/>
    
    
    <category term="Java" scheme="https://www.xdull.cn/tags/Java/"/>
    
    <category term="JUC" scheme="https://www.xdull.cn/tags/JUC/"/>
    
    <category term="并发编程" scheme="https://www.xdull.cn/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/"/>
    
    <category term="ReentrantReadWriteLock" scheme="https://www.xdull.cn/tags/ReentrantReadWriteLock/"/>
    
    <category term="StampedLock" scheme="https://www.xdull.cn/tags/StampedLock/"/>
    
    <category term="Semaphore" scheme="https://www.xdull.cn/tags/Semaphore/"/>
    
    <category term="CountDownLatch" scheme="https://www.xdull.cn/tags/CountDownLatch/"/>
    
    <category term="CyclicBarrier" scheme="https://www.xdull.cn/tags/CyclicBarrier/"/>
    
  </entry>
  
  <entry>
    <title>读心术II</title>
    <link href="https://www.xdull.cn/binary-magic2.html"/>
    <id>https://www.xdull.cn/binary-magic2.html</id>
    <published>2023-10-29T00:37:07.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<style>.table-wrap{    overflow-y: hidden;}.imgTable{    width:90%;    margin:0 auto;}.imgTable td{    border:0px;    background:white;}.imgTable td img{    margin-bottom:0px;}@keyframes ringing{    /* from  {transform:rotate(-30deg);}    to  {transform:rotate(30deg);} */    from  {width:0px;height:0px;}    to  {width:210px;height:300px;}}.appearAnimation{    animation: ringing 1s linear 0s 1 normal;}</style><div id="game-begin" >这是一个古老的小游戏，请从以下牌中任意选择一张你喜欢的并把它记在心里，接下来，我会问你几个问题，你只需回答“是”或者“否”，当我问完后即可猜到你心里想的那张牌什么。<br>注意：你无需点击屏幕上的任何一张牌，页面也不会捕捉你的任何动作，你只需把所选之牌牢牢记在心里就好。<br>准备好开始了吗？请选择：<table class="imgTable"><tr><td><img id="h1-1" src="" class="nofancybox" /></td><td><img id="h1-2" src="" class="nofancybox" /></td><td><img id="h1-3" src="" class="nofancybox" /></td><td><img id="h1-4" src="" class="nofancybox" /></td></tr><tr><td><img id="h1-5" src="" class="nofancybox" /></td><td><img id="h1-6" src="" class="nofancybox" /></td><td><img id="h1-7" src="" class="nofancybox" /></td><td><img id="h1-8" src="" class="nofancybox" /></td></tr><tr><td><img id="h1-9" src="" class="nofancybox" /></td><td><img id="h1-10" src="" class="nofancybox" /></td><td><img id="h1-11" src="" class="nofancybox" /></td><td><img id="h1-12" src="" class="nofancybox" /></td></tr><tr><td><img id="h1-13" src="" class="nofancybox" /></td><td><img id="h1-14" src="" class="nofancybox" /></td><td><img id="h1-15" src="" class="nofancybox" /></td><td><img id="h1-16" src="" class="nofancybox" /></td></tr></table><div style="text-align:center;"><input type="button" style="margin-top:20px;" onclick="MyGame.start()" value="我选好了"></input></div></div><div id="game-play" style="display:none;">你选择的牌在这些牌里面吗？<table class="imgTable"><tr><td><img id="h2-1" src="" class="nofancybox" /></td><td><img id="h2-2" src="" class="nofancybox" /></td><td><img id="h2-3" src="" class="nofancybox" /></td><td><img id="h2-4" src="" class="nofancybox" /></td></tr><tr><td><img id="h2-5" src="" class="nofancybox" /></td><td><img id="h2-6" src="" class="nofancybox" /></td><td><img id="h2-7" src="" class="nofancybox" /></td><td><img id="h2-8" src="" class="nofancybox" /></td></tr></table><div><input type="button" style="margin-top:20px;margin-left:10%;" onclick="MyGame.play(true);" value="是"></input><input type="button" style="margin-top:20px;margin-right:10%;float:right;" onclick="MyGame.play(false);" value="否"></input></div></div><div id="game-end" style="display:none;">好了，现在我已经知道你心里所选的牌是什么了。接下来就是见证奇迹的时刻，<br>请在心中默念该牌3秒钟，然后点击揭晓答案。<div id="divAnswer" style="text-align:left;">你的牌就是：<input id="btnAnswer" type="button"  onclick="MyGame.result();" value="揭晓答案"></input><label id="lbAnswer" style="font-weight: bold;display:none;color:red;"></label></div><div id="imgdiv" style="position: absolute;left: calc(50% - 105px);top: 400px;display:none;width:210px;height:300px;z-index:999;"><img id="imgclose" src="/postimgs/binary-magic/close.png" style="margin: 0;float: right;border: 0;margin-bottom: -8px;margin-right: -27px;visibility: collapse;"    class="nofancybox" onclick="MyGame.close();"/><img id="imgresult"   style="width:100%;height:100%;margin:0;"  class="nofancybox" /></div><div id="ending" style="display:none;">猜对了吗？是否还想<input type="button"  onclick="window.location.reload();" value="再试一次"></input>？<br>这并不是什么魔法，只是利用了二进制的思想，如果对此感兴趣，可以参考我这篇博文<a href="/poor-pig.html">薛定谔的猪</a>。</div></div><script type="text/javascript" src="/postimgs/binary-magic2/binary-magic.js"></script>]]></content>
    
    
    <summary type="html">送给所有喜欢奥特曼的孩子们，这是为他们准备的魔术。</summary>
    
    
    
    <category term="娱乐" scheme="https://www.xdull.cn/categories/%E5%A8%B1%E4%B9%90/"/>
    
    
    <category term="读心术" scheme="https://www.xdull.cn/tags/%E8%AF%BB%E5%BF%83%E6%9C%AF/"/>
    
  </entry>
  
  <entry>
    <title>数据结构终极boss之红黑树</title>
    <link href="https://www.xdull.cn/red-black-tree.html"/>
    <id>https://www.xdull.cn/red-black-tree.html</id>
    <published>2023-07-09T23:31:07.000Z</published>
    <updated>2026-07-22T15:15:28.793Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>之前在讨论二叉搜索树的时候，已经介绍过一种可高度平衡的二叉搜索树，即<a href="/avltree.html">AVL平衡二叉树</a>。现在我们将介绍另一种鼎鼎有名的平衡二叉树-红黑树，它因节点存储红或黑两种颜色之一而命名。相较于AVL树，它是一种弱平衡二叉树，不需要像AVL树那样严格要求平衡，这也导致它的查询效率并没有AVL树那么高，但相对的，它带来的好处是对于调整失衡，红黑树的旋转次数更少，所以频繁的插入或删除操作，红黑树更有优势。</p><h1 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h1><p>引用自百度百科：</p><blockquote><p><strong>红黑树</strong>（Red Black Tree）是一种自平衡二叉查找树，是在计算机科学中用到的一种数据结构，它是在1972年由Rudolf Bayer发明的，当时被称为平衡二叉B树（symmetric binary B-trees）。后来，在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。 </p><p>红黑树是一种平衡二叉查找树的变体，它的左右子树高差有可能大于 1，所以红黑树不是严格意义上的平衡二叉树（AVL），但对之进行平衡的代价较低， 其平均统计性能要强于 AVL 。</p><p>由于每一棵红黑树都是一颗二叉排序树，因此，在对红黑树进行查找时，可以采用运用于普通二叉排序树上的查找算法，在查找过程中不需要颜色信息。</p></blockquote><h1 id="2-3搜索树"><a href="#2-3搜索树" class="headerlink" title="2-3搜索树"></a>2-3搜索树</h1><p>在介绍红黑树之前，我们需要先了解一下什么是<strong>2-3搜索树</strong>。顾名思义，2-3搜索树也是一种搜索树，不过相较于一般的二叉搜索树，它的节点不仅可以存储1个键，还可以存储2个键，也就是说，它包含2种类型的节点：</p><ul><li><code>2-</code>节点：含有一个键和两条链接，左链接指向的节点的键都小于该节点的键，右链接指向的节点的键都大于该节点的键。这也就是传统二叉搜索树的节点。</li><li><code>3-</code>节点：含有两个键和三条链接，左链接指向的节点的键都小于该节点的小键，右链接指向的节点的键都大于该节点的大键，中链接指向的节点的键都介于该节点两个键之间。</li></ul><p>简单起见，我们这里提到的2-3查找树都是指完美平衡的2-3查找树，完美平衡表示树中所有的叶子节点到根节点的距离都是相同的。我们之后将介绍如何维持这种平衡，以下是一棵2-3查找树的例子：</p><p><img src="/postimgs/redblacktree/1.jpg" alt=""></p><h2 id="查找"><a href="#查找" class="headerlink" title="查找"></a>查找</h2><p>要判断一个键是否在树中，我们先将它和根结点中的键比较。如果它和其中任意一个相等，查找命中；否则我们就根据比较的结果找到指向相应区间的链接，并在其指向的子树中递归地继续查找。如果这是个空链接，查找未命中。</p><p>以下是2个例子，对于H的命中查找：</p><p><img src="/postimgs/redblacktree/2.jpg" alt="命中查找"></p><p>对于B的未命中查找：</p><p><img src="/postimgs/redblacktree/3.jpg" alt="未命中查找"></p><h2 id="插入"><a href="#插入" class="headerlink" title="插入"></a>插入</h2><p>要在 2-3 树中插入一个新节点，我们可以和二叉查找树一样先进行一次未命中的查找，然后把新节点挂在树的底部。但这样的话就破坏了树的完美平衡性，我们需要继续维持这种平衡性。以下根据2种插入情况分别讨论。</p><h3 id="插入在2-节点"><a href="#插入在2-节点" class="headerlink" title="插入在2-节点"></a>插入在2-节点</h3><p>如果未命中的查找结束于一个<code>2-</code>节点，事情就好办了，我们只要把这个<code>2-</code>节点替换为一个<code>3-</code>结点，将要插入的键保存在其中即可，比如在树中插入K：</p><ul><li><p>对K的查找在<code>2-</code>节点结束</p><p><img src="/postimgs/redblacktree/4.jpg" alt=""></p></li><li><p>将<code>2-</code>节点替换为<code>3-</code>节点</p><p><img src="/postimgs/redblacktree/5.jpg" alt=""></p></li></ul><h3 id="插入在3-节点"><a href="#插入在3-节点" class="headerlink" title="插入在3-节点"></a>插入在3-节点</h3><p>如果未命中的查找结束于一个<code>3-</code>结点，则相对麻烦一些。我们会得到一个临时的<code>4-</code>节点，而<code>4-</code>节点在2-3树中并不被允许，我们需要将它进行分解。分解步骤为将<code>4-</code>节点分解为3个<code>2-</code>节点，然后将中键插入到它的父节点中。如果父节点是<code>2-</code>节点则转变为<code>3-</code>节点，然后停止；如果父节点是<code>3-</code>节点，则变成临时的<code>4-</code>节点，然后继续分解并向上转移，直至最终转移到根节点。根节点分解后，会使树的高度增加1，但仍然维持了树的完美平衡性。比如在树中插入Z：</p><ul><li><p>对Z的查找在<code>3-</code>节点结束</p><p><img src="/postimgs/redblacktree/6.jpg" alt=""></p></li><li><p>将<code>3-</code>节点替换为临时的<code>4-</code>节点</p><p><img src="/postimgs/redblacktree/7.jpg" alt=""></p></li><li><p><code>4-</code>节点分解为3个<code>2-</code>节点，中间节点上移至父节点</p><p><img src="/postimgs/redblacktree/8.jpg" alt=""></p></li></ul><h1 id="红黑树"><a href="#红黑树" class="headerlink" title="红黑树"></a>红黑树</h1><p>理解了2-3树的原理，接下来，我们定义一种名为红黑树的数据结构，它使用标准的二叉搜索树（完全由<code>2-</code>节点构成的树）以及一些额外的信息（颜色，用于替换<code>3-</code>节点）来模拟2-3树的实现。</p><p>我们将树中的链接分为两种类型：</p><ul><li>红链接：将两个<code>2-</code>节点连接起来构成一个<code>3-</code>节点；</li><li>黑链接：2-3树中的普通链接；</li></ul><p>准确的说，我们将<code>3-</code>节点表示为一条<strong>左斜</strong>的红色链接相连的两个<code>2-</code>节点。根据我们的定义，这棵红黑树应该是一棵满足如下条件的二叉搜索树：</p><ol><li>红链接均为左链接；</li><li>没有任何一个节点同时和两条红链接相连；</li><li>该树是<strong>完美黑色平衡</strong>的，即任意空链接到根节点的路径上的黑链接数量相同。</li></ol><p>如果将这棵红黑树的所有红链接画平，并将由红链接相连的节点看成一个整体，那么我们会发现，它和2-3树是一模一样的。</p><p><img src="/postimgs/redblacktree/9.jpg" alt=""></p><h2 id="颜色表示"><a href="#颜色表示" class="headerlink" title="颜色表示"></a>颜色表示</h2><p>除了根节点外，每个节点都只会有一条指向自己的链接（由它的父节点指向它），我们将链接的颜色保存在节点中，同时我们规定指向空节点和根节点的链接颜色为<strong>黑色</strong>。我们定义如下的数据结构：</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-keyword">const</span> <span class="hljs-built_in">bool</span> RED = <span class="hljs-literal">true</span>;<br><span class="hljs-keyword">const</span> <span class="hljs-built_in">bool</span> BLACK = <span class="hljs-literal">false</span>;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Node</span><br>&#123;<br>    <span class="hljs-keyword">public</span> TKey key;<br>    <span class="hljs-keyword">public</span> Node left, right;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> color;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Node</span>(<span class="hljs-params">TKey key</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">this</span>.key = key;<br>        color = RED;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们再封装一个私有方法，以确保可以获得任意节点（包含空节点）和它父节点的链接颜色：</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">isRed</span>(<span class="hljs-params">Node x</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (x == <span class="hljs-literal">null</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br>    <span class="hljs-keyword">return</span> x.color == RED;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="旋转"><a href="#旋转" class="headerlink" title="旋转"></a>旋转</h2><p>与AVL树类似，我们在执行完某些操作后，可能会出现<strong>红色右链接</strong>或者<strong>两条连续的红色左链接</strong>，这破坏了我们之前关于红黑树的约定，需要对其进行修复，其中最基本的操作就是旋转。</p><h3 id="左旋转"><a href="#左旋转" class="headerlink" title="左旋转"></a>左旋转</h3><p>其实现目标为将红色右链接转换为红色左链接。</p><p><img src="/postimgs/redblacktree/10.jpg" alt=""></p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">rotateLeft</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    Node x = h.right;<br>    h.right = x.left;<br>    x.left = h;<br>    x.color = h.color;<br>    h.color = RED;<br>    <span class="hljs-keyword">return</span> x;<br>&#125;<br></code></pre></td></tr></table></figure><p>旋转之后，我们返回<code>x</code>作为新的根节点，同时<code>x</code>保留了原来根节点<code>h</code>的颜色。</p><h3 id="右旋转"><a href="#右旋转" class="headerlink" title="右旋转"></a>右旋转</h3><p>其实现目标为将红色左链接转换为红色右链接，该旋转目的主要是为了消除连续的红色左链接。</p><p><img src="/postimgs/redblacktree/11.jpg" alt=""></p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">rotateRight</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    Node x = h.left;<br>    h.left = x.right;<br>    x.right = h;<br>    x.color = h.color;<br>    h.color = RED;<br>    <span class="hljs-keyword">return</span> x;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="颜色转换"><a href="#颜色转换" class="headerlink" title="颜色转换"></a>颜色转换</h2><p>我们通过旋转会消除<strong>红色右链接</strong>或者<strong>两条连续的红色左链接</strong>的情况，但最终可能会出现一种情况，亦即某个节点同时拥有左右2个红色子节点。</p><p><img src="/postimgs/redblacktree/12.jpg" alt=""></p><p>我们定义一个<code>flipColors</code>方法来转换一个节点的两个红色子节点的颜色。除了将子结点的颜色由红变黑之外，同时还将父结点的颜色由黑变红。这项操作最重要的性质在于它和旋转操作一样是局部变换，不会影响整棵树的黑色平衡性。</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">flipColors</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    h.color = RED;<br>    h.left.color = h.right.color = BLACK;<br>&#125;<br></code></pre></td></tr></table></figure><p>这个操作相当于在2-3树中，我们将临时的<code>4-</code>节点分解为3个<code>2-</code>节点，同时让中间节点和父节点相结合，并一直向上转移。</p><p>注意：根节点始终是黑色，因为红色的根节点表示根节点是一个<code>3-</code>节点的一部分，这并不符合实际情况。所以如果我们对根节点进行了<code>flipColors</code>操作，需要将根节点设置为黑色（实际操作中，每次调整失衡我们都将根节点设置为了黑色）。对根节点进行<code>flipColors</code>操作，同时也意味着树的黑链接高度增加了1。</p><h2 id="插入操作"><a href="#插入操作" class="headerlink" title="插入操作"></a>插入操作</h2><p>我们使用二叉搜索树相同的方式往红黑树中插入节点，插入会发生在树的叶子节点上，可能是<code>2-</code>节点也可能是<code>3-</code>节点。我们统一使用<strong>红链接</strong>将新节点和它的父节点相连。</p><h3 id="在2-节点中插入新键"><a href="#在2-节点中插入新键" class="headerlink" title="在2-节点中插入新键"></a>在<code>2-</code>节点中插入新键</h3><ol><li><p>新键小于<code>2-</code>节点的键</p><p><img src="/postimgs/redblacktree/13.jpg" alt=""></p><p>此情况无须做任何操作，插入完成后本身就是一个合法的<code>3-</code>节点。</p></li><li><p>新键大于<code>2-</code>节点的键</p><p><img src="/postimgs/redblacktree/14.jpg" alt=""></p><p>此情况需进行一次左旋转，将红色右链接转变成左链接，变成一个合法的<code>3-</code>节点。</p></li></ol><h3 id="在3-节点中插入新键"><a href="#在3-节点中插入新键" class="headerlink" title="在3-节点中插入新键"></a>在<code>3-</code>节点中插入新键</h3><ol><li><p>新键大于<code>3-</code>节点的最大键</p><p><img src="/postimgs/redblacktree/15.jpg" alt=""></p><p>此情况无须任何旋转操作。</p></li><li><p>新键小于<code>3-</code>节点的最小键</p><p><img src="/postimgs/redblacktree/16.jpg" alt=""></p><p>此情况出现了两条连续的红色左链接，需对节点C做右旋转。</p></li><li><p>新键介于<code>3-</code>节点两个键之间</p><p><img src="/postimgs/redblacktree/17.jpg" alt=""></p><p>此情况出现了红色左链接以及两条连续的红色左链接，需进行两次旋转。</p></li></ol><p>此三种情况最终都会产生一个同时连接到两条红链接的节点，此时我们进行一次颜色转换操作即可完成修复。</p><p><img src="/postimgs/redblacktree/177.jpg" alt=""></p><h3 id="调整失衡"><a href="#调整失衡" class="headerlink" title="调整失衡"></a>调整失衡</h3><p>针对以上情况总结，我们可以得到如下规律：</p><ul><li>如果右子节点是红色的而左子节点是黑色，进行左旋转；</li><li>如果左子节点是红色的且它的左子节点也是红色的，进行右旋转；</li><li>如果左右子节点均为红色，进行颜色转换。</li></ul><p><img src="/postimgs/redblacktree/18.jpg" alt=""></p><p>对于某个节点调整失衡的实现代码：</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">balance</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (isRed(h.right) &amp;&amp; !isRed(h.left))<br>        h = rotateLeft(h);<br>    <span class="hljs-keyword">if</span> (isRed(h.left) &amp;&amp; isRed(h.left.left))<br>        h = rotateRight(h);<br>    <span class="hljs-keyword">if</span> (isRed(h.left) &amp;&amp; isRed(h.right))<br>        flipColors(h);<br>    h.N = size(h.left) + size(h.right) + <span class="hljs-number">1</span>;<br>    <span class="hljs-keyword">return</span> h;<br>&#125;<br></code></pre></td></tr></table></figure><p>最终我们实现的插入操作和普通二叉搜索树几乎一模一样，只是插入完成后，需要由叶子节点至底向上，一步步为查找路径上的节点调整平衡。</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">put</span>(<span class="hljs-params">Node h, TKey key</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (h == <span class="hljs-literal">null</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Node(key);<br>    <span class="hljs-built_in">int</span> cmp = compare(key, h.key);<br>    <span class="hljs-keyword">if</span> (cmp &lt; <span class="hljs-number">0</span>)<br>        h.left = put(h.left, key);<br>    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (cmp &gt; <span class="hljs-number">0</span>)<br>        h.right = put(h.right, key);<br>    <span class="hljs-keyword">return</span> balance(h);<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="删除操作"><a href="#删除操作" class="headerlink" title="删除操作"></a>删除操作</h2><p>红黑树的删除操作较为复杂和难以理解，不过我们这里仍然以2-3树为例，首先明确在2-3树中应该如何操作后仍然保持平衡，再使用我们定义的红黑树来模拟这个过程。简单起见，我们先从删除最小键开始。</p><h3 id="删除最小键"><a href="#删除最小键" class="headerlink" title="删除最小键"></a>删除最小键</h3><p>在二叉搜索树中，最小键就是树最左边的节点，所以如果某个节点的左节点为空，那么这个节点就是最小节点，在一般二叉搜索树中，我们只需要将这个节点的右节点赋值给它的父节点，即完成了最小节点的删除。但是在2-3树中，我们不能将节点留空，因为这样就失去了平衡。可以考虑这样一种情况，假如我们要删除的最小键是在一个<code>3-</code>节点中，这样一来当我们删除这个键的时候，<code>3-</code>节点就变成了<code>2-</code>节点，对于2-3树来说就依然是平衡的。</p><p><img src="/postimgs/redblacktree/19.jpg" alt=""></p><p>接下来，我们定义一个共用方法<code>moveRedLeft</code>，该方法进行一系列转换，确保最小键一定位于最底端的<code>3-</code>节点（或<code>4-</code>节点）中，以下是<code>moveRedLeft</code>方法进行的2种转换：</p><ul><li><p>从兄弟节点借变成<code>3-</code>节点后删除</p><p><img src="/postimgs/redblacktree/20.jpg" alt=""></p></li><li><p>从父节点借变成<code>4-</code>节点后删除</p><p><img src="/postimgs/redblacktree/21.jpg" alt=""></p></li></ul><p>对于<code>deleteMin</code>方法，我们自顶向下递归，当我们找到最小节点（最左边的节点）时，直接返回空即可，否则继续对子节点进行变换。该变换可能导致会出现<code>4-</code>节点，不过不用担心，因为最后我们会通过之前的调整失衡<code>balance</code>方法自底向上来消除这种情况。 </p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">deleteMin</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (h.left == <span class="hljs-literal">null</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>    h = moveRedLeft(h);<br>    h.left = deleteMin(h.left);<br>    <span class="hljs-keyword">return</span> balance(h);<br>&#125;<br></code></pre></td></tr></table></figure><p>最后，我们以下面这棵红黑树为例，来理解<code>moveRedLeft</code>方法的实现</p><p><img src="/postimgs/redblacktree/22.jpg" alt=""></p><p>首先如果节点本身是<code>3-</code>节点，或者其左子节点是<code>3-</code>节点时，则无需任何处理，直接递归其左子节点即可，比如对该树根节点进行<code>moveRedLeft</code>操作会直接返回。</p><p>否则我们进行颜色变换，将父节点融合进来，变成<code>4-</code>节点：</p><p><img src="/postimgs/redblacktree/23.jpg" alt=""></p><p>如果节点的右子节点为<code>2-</code>节点，则还需要对其进行旋转操作：</p><p><img src="/postimgs/redblacktree/24.jpg" alt=""></p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">moveRedLeft</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-comment">//当节点本身是2-节点，或者其左子节点是3-节点时，无需处理</span><br>    <span class="hljs-keyword">if</span> (isRed(h.left) || isRed(h.left.left))<br>        <span class="hljs-keyword">return</span> h;<br>    <span class="hljs-comment">//颜色转换，相当于从父节点借</span><br>    h.color = BLACK;<br>    h.left.color = h.right.color = RED;<br>    <span class="hljs-comment">//旋转，相当于从兄弟节点借</span><br>    <span class="hljs-keyword">if</span> (isRed(h.right.left))<br>    &#123;<br>        h.right = rotateRight(h.right);  <span class="hljs-comment">//右子节点右旋</span><br>        h = rotateLeft(h);               <span class="hljs-comment">//h节点左旋</span><br>    &#125;<br>    <span class="hljs-keyword">return</span> h;<br>&#125;<br></code></pre></td></tr></table></figure><p>处理完毕之后，我们便可毫无顾虑的删除最小键，最后再通过平衡操作，调整成一棵合法的红黑树。</p><p><img src="/postimgs/redblacktree/25.jpg" alt=""></p><p>注意到这里的转换颜色操作和之前介绍的<code>flipColors</code>方法恰好相反，我们可以将该方法抽离出来：如果是自底向上进行调整失衡，我们定义参数<code>top</code>为<code>true</code>；如果是自顶向下，融合父节点操作，则定义参数<code>top</code>为<code>false</code>。</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">flipColors</span>(<span class="hljs-params">Node h, <span class="hljs-built_in">bool</span> top</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (top)<br>    &#123;<br>        h.color = RED;<br>        h.left.color = h.right.color = BLACK;<br>    &#125;<br>    <span class="hljs-keyword">else</span><br>    &#123;<br>        h.color = BLACK;<br>        h.left.color = h.right.color = RED;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>类似于插入叶子节点，当最终删除最小键后，也需要自底向上平衡到根节点，此时我们需要将根节点置为黑色。</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">deleteMin</span>(<span class="hljs-params"></span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (root == <span class="hljs-literal">null</span>)<br>        <span class="hljs-keyword">return</span>;<br>    root = deleteMin(root);<br>    <span class="hljs-keyword">if</span> (root != <span class="hljs-literal">null</span>)<br>        root.color = BLACK;<br>&#125;<br></code></pre></td></tr></table></figure><p>通过以上的例子，可以观察到对于红黑树最小键的删除过程，实际体现在2-3树中的结果如下图所示：</p><p><img src="/postimgs/redblacktree/26.jpg" alt=""></p><h3 id="删除任意键"><a href="#删除任意键" class="headerlink" title="删除任意键"></a>删除任意键</h3><p>现在我们考虑在红黑树中删除任意键的算法，在删除最小键的时候我们通过变换让最小键位于<code>3-</code>或<code>4-</code>节点中再进行删除，其中用到了<code>moveRedLeft</code>方法，该方法保证让右边的节点<code>3-</code>节点可以转移到左边到<code>2-</code>节点，现在当我们要删除的键可能位于右边的<code>2-</code>节点时，也需要考虑将左边的<code>3-</code>节点转移到右边的<code>2-</code>节点中，类似地，我们定义<code>moveRedRight</code>方法：</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs c#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">moveRedRight</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (isRed(h.right) || isRed(h.right.left))<br>        <span class="hljs-keyword">return</span> h;<br>    flipColors(h, <span class="hljs-literal">false</span>);<br>    <span class="hljs-keyword">return</span> h;<br>&#125;<br></code></pre></td></tr></table></figure><p>相比较<code>moveRedLeft</code>而言，这里除了颜色转换没用到任何旋转，因为我们约定红连接都是左链接，根据我们的平衡算法，即使没有从左兄弟节点转移到右<code>2-</code>节点，最后调整平衡的时候也可以恢复正常的，比如删除一个<code>2-</code>节点H：</p><p><img src="/postimgs/redblacktree/27.jpg" alt=""></p><p>而如果要删除的键在左边的<code>2-</code>节点中，则无法直接恢复平衡，删之前需先旋转：</p><p><img src="/postimgs/redblacktree/28.jpg" alt=""></p><p>值得注意的是，因为红链接都是在左侧，所以一个<code>3-</code>节点中较大的那个键为父节点，假如要删除的是这个大键，那么必须先进行右旋转，让小键提升为父节点再进行删除，而如果要删除的是小键就没有必要了，因为它本身就是一个左子节点。</p><p>比如这里如果删除的键是H则需要右旋转，而如果是F则无需旋转：</p><p><img src="/postimgs/redblacktree/29.jpg" alt=""></p><p>最后，我们来剖析一下完整的删除算法，首先从根节点开始和删除键进行比较：</p><ul><li>小于当前键，递归左子节点，类似于删除最小键的做法，递归前需先执行融合方法<code>moveRedLeft</code>：<ul><li>没有左子节点：已经不存在更小的键了，抛出异常；</li><li>有左子节点：继续递归左子节点；</li></ul></li><li>大于等于当前键，先判断是否需要进行右旋转（把大键旋转到右侧），然后分情况：<ul><li>没有右子节点：<ul><li>等于当前键：直接返回空；</li><li>大于当前键：已经不存在更大的键了，抛出异常；</li></ul></li><li>有右子节点，先通过<code>moveRedRight</code>方法融合：<ul><li>等于当前键：将当前节点的键替换成右子树的最小键，同时删除右子树的最小键；</li><li>大于当前键：继续递归右子节点</li></ul></li></ul></li></ul><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">delete</span>(<span class="hljs-params">Node h, TKey key</span>)</span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span> (h == <span class="hljs-literal">null</span>)<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>    <span class="hljs-keyword">if</span> (compare(key, h.key) &lt; <span class="hljs-number">0</span>)<br>    &#123;<br>        <span class="hljs-keyword">if</span> (h.left == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>        h = moveRedLeft(h);<br>        h.left = delete(h.left, key);<br>    &#125;<br>    <span class="hljs-keyword">else</span><br>    &#123;<br>        <span class="hljs-keyword">if</span> (isRed(h.left))<br>            h = rotateRight(h);<br>        <span class="hljs-keyword">if</span> (h.right == <span class="hljs-literal">null</span>)<br>        &#123;<br>            <span class="hljs-keyword">if</span> (compare(key, h.key) == <span class="hljs-number">0</span>)<br>                <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>        &#125;<br>        h = moveRedRight(h);<br>        <span class="hljs-keyword">if</span> (compare(key, h.key) == <span class="hljs-number">0</span>)<br>        &#123;<br>            <span class="hljs-keyword">var</span> m = min(h.right);<br>            h.key = m.key;<br>            h.right = deleteMin(h.right);<br>        &#125;<br>        <span class="hljs-keyword">else</span><br>            h.right = delete(h.right, key);<br>    &#125;<br>    <span class="hljs-keyword">return</span> balance(h);<br>&#125;<br></code></pre></td></tr></table></figure><p>下面我们以删除一棵红黑树的根节点为例，通过图片来演示算法是如何运作的：</p><p><img src="/postimgs/redblacktree/30.jpg" alt=""></p><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p>如果忽略这棵红黑树的颜色属性，那么它就是一棵普通的二叉搜索树，只不过它是高度平衡的，所以查询相关的代码和普通二叉搜索树完全一致。</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br></pre></td><td class="code"><pre><code class="hljs C#"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">RedBlackBST</span>&lt;<span class="hljs-title">TKey</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">const</span> <span class="hljs-built_in">bool</span> RED = <span class="hljs-literal">true</span>;<br>    <span class="hljs-keyword">const</span> <span class="hljs-built_in">bool</span> BLACK = <span class="hljs-literal">false</span>;<br>    <span class="hljs-keyword">class</span> <span class="hljs-title">Node</span><br>    &#123;<br>        <span class="hljs-keyword">public</span> TKey key;<br>        <span class="hljs-keyword">public</span> Node left, right;<br>        <span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> color;<br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Node</span>(<span class="hljs-params">TKey key</span>)</span><br><span class="hljs-function"></span>        &#123;<br>            <span class="hljs-keyword">this</span>.key = key;<br>            color = RED;<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">RedBlackBST</span>(<span class="hljs-params">IComparer&lt;TKey&gt; comparer</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        compare = comparer.Compare;<br>    &#125;<br><br>    <span class="hljs-keyword">private</span> Node root;<br>    <span class="hljs-keyword">private</span> Func&lt;TKey, TKey, <span class="hljs-built_in">int</span>&gt; compare;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">isRed</span>(<span class="hljs-params">Node x</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (x == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br>        <span class="hljs-keyword">return</span> x.color == RED;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">rotateLeft</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        Node x = h.right;<br>        h.right = x.left;<br>        x.left = h;<br>        x.color = h.color;<br>        h.color = RED;<br>        <span class="hljs-keyword">return</span> x;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">rotateRight</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        Node x = h.left;<br>        h.left = x.right;<br>        x.right = h;<br>        x.color = h.color;<br>        h.color = RED;<br>        <span class="hljs-keyword">return</span> x;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">flipColors</span>(<span class="hljs-params">Node h, <span class="hljs-built_in">bool</span> top</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (top)<br>        &#123;<br>            h.color = RED;<br>            h.left.color = h.right.color = BLACK;<br>        &#125;<br>        <span class="hljs-keyword">else</span><br>        &#123;<br>            h.color = BLACK;<br>            h.left.color = h.right.color = RED;<br>        &#125;<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">balance</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (isRed(h.right) &amp;&amp; !isRed(h.left))<br>            h = rotateLeft(h);<br>        <span class="hljs-keyword">if</span> (isRed(h.left) &amp;&amp; isRed(h.left.left))<br>            h = rotateRight(h);<br>        <span class="hljs-keyword">if</span> (isRed(h.left) &amp;&amp; isRed(h.right))<br>            flipColors(h, <span class="hljs-literal">true</span>);<br>        <span class="hljs-keyword">return</span> h;<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">put</span>(<span class="hljs-params">TKey key</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        root = put(root, key);<br>        root.color = BLACK;<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">put</span>(<span class="hljs-params">Node h, TKey key</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (h == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Node(key);<br>        <span class="hljs-built_in">int</span> cmp = compare(key, h.key);<br>        <span class="hljs-keyword">if</span> (cmp &lt; <span class="hljs-number">0</span>)<br>            h.left = put(h.left, key);<br>        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (cmp &gt; <span class="hljs-number">0</span>)<br>            h.right = put(h.right, key);<br>        <span class="hljs-keyword">return</span> balance(h);<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">moveRedLeft</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (isRed(h.left) || isRed(h.left.left))<br>            <span class="hljs-keyword">return</span> h;<br>        flipColors(h, <span class="hljs-literal">false</span>);<br>        <span class="hljs-keyword">if</span> (isRed(h.right.left))<br>        &#123;<br>            h.right = rotateRight(h.right);<br>            h = rotateLeft(h);<br>        &#125;<br>        <span class="hljs-keyword">return</span> h;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">moveRedRight</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (isRed(h.right) || isRed(h.right.left))<br>            <span class="hljs-keyword">return</span> h;<br>        flipColors(h, <span class="hljs-literal">false</span>);<br>        <span class="hljs-keyword">return</span> h;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">deleteMin</span>(<span class="hljs-params"></span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (root == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">return</span>;<br>        root = deleteMin(root);<br>        <span class="hljs-keyword">if</span> (root != <span class="hljs-literal">null</span>)<br>            root.color = BLACK;<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">deleteMin</span>(<span class="hljs-params">Node h</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (h.left == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>        h = moveRedLeft(h);<br>        h.left = deleteMin(h.left);<br>        <span class="hljs-keyword">return</span> balance(h);<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">delete</span>(<span class="hljs-params">TKey key</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        root = delete(root, key);<br>        <span class="hljs-keyword">if</span> (root != <span class="hljs-literal">null</span>)<br>            root.color = BLACK;<br>    &#125;<br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">delete</span>(<span class="hljs-params">Node h, TKey key</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (h == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>        <span class="hljs-keyword">if</span> (compare(key, h.key) &lt; <span class="hljs-number">0</span>)<br>        &#123;<br>            <span class="hljs-keyword">if</span> (h.left == <span class="hljs-literal">null</span>)<br>                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>            h = moveRedLeft(h);<br>            h.left = delete(h.left, key);<br>        &#125;<br>        <span class="hljs-keyword">else</span><br>        &#123;<br>            <span class="hljs-keyword">if</span> (isRed(h.left))<br>                h = rotateRight(h);<br>            <span class="hljs-keyword">if</span> (h.right == <span class="hljs-literal">null</span>)<br>            &#123;<br>                <span class="hljs-keyword">if</span> (compare(key, h.key) == <span class="hljs-number">0</span>)<br>                    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> KeyNotFoundException(<span class="hljs-string">&quot;给定关键字不在字典中。&quot;</span>);<br>            &#125;<br>            h = moveRedRight(h);<br>            <span class="hljs-keyword">if</span> (compare(key, h.key) == <span class="hljs-number">0</span>)<br>            &#123;<br>                <span class="hljs-keyword">var</span> m = min(h.right);<br>                h.key = m.key;<br>                h.right = deleteMin(h.right);<br>            &#125;<br>            <span class="hljs-keyword">else</span><br>                h.right = delete(h.right, key);<br>        &#125;<br>        <span class="hljs-keyword">return</span> balance(h);<br>    &#125;      <br>    <br>    <span class="hljs-function"><span class="hljs-keyword">private</span> Node <span class="hljs-title">min</span>(<span class="hljs-params">Node x</span>)</span><br><span class="hljs-function"></span>    &#123;<br>        <span class="hljs-keyword">if</span> (x.left == <span class="hljs-literal">null</span>)<br>            <span class="hljs-keyword">return</span> x;<br>        <span class="hljs-keyword">return</span> min(x.left);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p>参考文献：《算法》第四版</p></blockquote>]]></content>
    
    
    <summary type="html">红黑树是一种自平衡二叉查找树，是在计算机科学中用到的一种数据结构，它是在1972年由Rudolf Bayer发明的，当时被称为平衡二叉B树，后在1978年被修改为如今的“红黑树”。因为它极其抽象，在数据结构中一直都宛如游戏boss一般的存在，希望通过该文章让你彻底理解红黑树的本质原理。</summary>
    
    
    
    <category term="算法" scheme="https://www.xdull.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://www.xdull.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="二叉搜索树" scheme="https://www.xdull.cn/tags/%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/"/>
    
    <category term="平衡二叉树" scheme="https://www.xdull.cn/tags/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
    
    <category term="数据结构" scheme="https://www.xdull.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="红黑树" scheme="https://www.xdull.cn/tags/%E7%BA%A2%E9%BB%91%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>Python中的日志模块</title>
    <link href="https://www.xdull.cn/python-logging.html"/>
    <id>https://www.xdull.cn/python-logging.html</id>
    <published>2023-07-04T18:39:08.000Z</published>
    <updated>2026-07-22T15:15:28.793Z</updated>
    
    <content type="html"><![CDATA[<h1 id="logging模块"><a href="#logging模块" class="headerlink" title="logging模块"></a>logging模块</h1><p>在Python中，常用的用于写错误日志的库是<code>logging</code>。以下是一个简单的示例，演示如何在Python中使用<code>logging</code>库来记录错误日志：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> logging<br><br><span class="hljs-comment"># 创建日志记录器</span><br>logger = logging.getLogger(<span class="hljs-string">&#x27;error_logger&#x27;</span>)<br>logger.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 创建一个文件处理器，将日志写入文件</span><br>file_handler = logging.FileHandler(<span class="hljs-string">&#x27;error.log&#x27;</span>)<br>file_handler.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 创建一个控制台处理器，将日志输出到控制台</span><br>console_handler = logging.StreamHandler()<br>console_handler.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 定义日志格式</span><br>formatter = logging.Formatter(<span class="hljs-string">&#x27;%(asctime)s - %(levelname)s - %(message)s&#x27;</span>)<br>file_handler.setFormatter(formatter)<br>console_handler.setFormatter(formatter)<br><br><span class="hljs-comment"># 将处理器添加到日志记录器</span><br>logger.addHandler(file_handler)<br>logger.addHandler(console_handler)<br><br><span class="hljs-keyword">try</span>:<br>    <span class="hljs-comment"># 在这里写你的代码</span><br>    <span class="hljs-comment"># 如果发生了错误</span><br>    <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">&#x27;这是一个错误示例&#x27;</span>)<br><span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:<br>    <span class="hljs-comment"># 记录错误日志</span><br>    logger.error(<span class="hljs-built_in">str</span>(e))<br><br></code></pre></td></tr></table></figure><p>以上代码创建了一个名为<code>error_logger</code>的日志记录器，并将错误级别设置为<code>logging.ERROR</code>。然后，创建了一个文件处理器（<code>FileHandler</code>）和一个控制台处理器（<code>StreamHandler</code>），分别用于将日志写入文件和输出到控制台。最后，定义了日志的格式，并将处理器添加到日志记录器中。</p><p>在<code>try</code>块中的代码中，如果发生了错误，可以使用<code>logger.error()</code>方法将错误信息记录到日志文件和控制台中。</p><p>请注意，在使用<code>logging</code>库之前，需要先导入<code>logging</code>模块。</p><h2 id="格式"><a href="#格式" class="headerlink" title="格式"></a>格式</h2><p><code>logging.Formatter(&#39;%(asctime)s - %(levelname)s - %(message)s&#39;)</code>这句代码是用于定义日志的格式。</p><ul><li><code>%(asctime)s</code>：表示日志记录的时间，即当前时间，以可读的形式显示（例如：”2022-09-30 10:30:15”）。</li><li><code>%(levelname)s</code>：表示日志的级别，如<code>DEBUG</code>、<code>INFO</code>、<code>WARNING</code>、<code>ERROR</code>、<code>CRITICAL</code>等。</li><li><code>%(message)s</code>：表示实际的日志消息。</li></ul><p>这样的定义可以让日志以特定的格式显示，方便阅读和分析日志。在这个例子中，日志的格式为：<code>时间 - 日志级别 - 日志消息</code>。例如：<code>2022-09-30 10:30:15 - ERROR - 这是一个错误示例</code>。</p><p>还可以根据需要自定义日志的格式，使用不同的占位符来输出不同的信息，例如输出日志记录器的名称<code>%(name)s</code>、输出所在的模块<code>%(module)s</code>等等。更多的占位符和格式选项可以参考Python官方文档的<a href="https://docs.python.org/3/library/logging.html#logging.Formatter">Logging.Formatter</a>部分。</p><h1 id="分隔存储"><a href="#分隔存储" class="headerlink" title="分隔存储"></a>分隔存储</h1><p>如果所有错误日志都写入同一个文件，会导致文件越来越大。为了解决这个问题，可以使用<code>logging</code>库的<code>RotatingFileHandler</code>或<code>TimedRotatingFileHandler</code>来分隔文件存储。下面是示例代码：</p><p><strong>1.使用<code>RotatingFileHandler</code>：</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> logging<br><span class="hljs-keyword">from</span> logging.handlers <span class="hljs-keyword">import</span> RotatingFileHandler<br><br>logger = logging.getLogger(<span class="hljs-string">&#x27;error_logger&#x27;</span>)<br>logger.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 创建一个RotatingFileHandler，最多存储10个日志文件，每个文件最大为1MB</span><br>file_handler = RotatingFileHandler(<span class="hljs-string">&#x27;error.log&#x27;</span>, maxBytes=<span class="hljs-number">1048576</span>, backupCount=<span class="hljs-number">10</span>)<br>file_handler.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 设置日志格式</span><br>formatter = logging.Formatter(<span class="hljs-string">&#x27;%(asctime)s - %(levelname)s - %(message)s&#x27;</span>)<br>file_handler.setFormatter(formatter)<br><br><span class="hljs-comment"># 添加处理器到日志记录器</span><br>logger.addHandler(file_handler)<br><br><span class="hljs-keyword">try</span>:<br>    <span class="hljs-comment"># 在这里写你的代码</span><br>    <span class="hljs-comment"># 如果发生了错误</span><br>    <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">&#x27;这是一个错误示例&#x27;</span>)<br><span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:<br>    logger.error(<span class="hljs-built_in">str</span>(e))<br><br></code></pre></td></tr></table></figure><p>使用<code>RotatingFileHandler</code>时，可以通过<code>maxBytes</code>参数设置每个日志文件的最大大小，通过<code>backupCount</code>参数设置最多存储的日志文件数量。</p><p><strong>2.使用<code>TimedRotatingFileHandler</code>：</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> logging<br><span class="hljs-keyword">from</span> logging.handlers <span class="hljs-keyword">import</span> TimedRotatingFileHandler<br><br>logger = logging.getLogger(<span class="hljs-string">&#x27;error_logger&#x27;</span>)<br>logger.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 创建一个TimedRotatingFileHandler，每天生成一个日志文件</span><br>file_handler = TimedRotatingFileHandler(<span class="hljs-string">&#x27;error.log&#x27;</span>, when=<span class="hljs-string">&#x27;midnight&#x27;</span>, interval=<span class="hljs-number">1</span>)<br>file_handler.setLevel(logging.ERROR)<br><br><span class="hljs-comment"># 设置日志格式</span><br>formatter = logging.Formatter(<span class="hljs-string">&#x27;%(asctime)s - %(levelname)s - %(message)s&#x27;</span>)<br>file_handler.setFormatter(formatter)<br><br><span class="hljs-comment"># 添加处理器到日志记录器</span><br>logger.addHandler(file_handler)<br><br><span class="hljs-keyword">try</span>:<br>    <span class="hljs-comment"># 在这里写你的代码</span><br>    <span class="hljs-comment"># 如果发生了错误</span><br>    <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">&#x27;这是一个错误示例&#x27;</span>)<br><span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:<br>    logger.error(<span class="hljs-built_in">str</span>(e))<br><br></code></pre></td></tr></table></figure><p>使用<code>TimedRotatingFileHandler</code>时，可以通过<code>when</code>参数指定日志文件的轮转时间间隔（例如：<code>midnight</code>表示每天生成一个日志文件），通过<code>interval</code>参数指定轮转时间间隔的数量。</p><p>除了<code>midnight</code>，<code>TimedRotatingFileHandler</code>的<code>when</code>参数还支持以下几种有效的取值：</p><ul><li><code>S</code>: 表示每秒生成一个日志文件。</li><li><code>M</code>: 表示每分钟生成一个日志文件。</li><li><code>H</code>: 表示每小时生成一个日志文件。</li><li><code>D</code>: 表示每天生成一个日志文件。</li><li><code>W0</code> ~ <code>W6</code>: 表示每个星期的周一至周日分别生成一个日志文件。</li><li><code>midnight</code>: 表示每天的午夜（00:00）生成一个日志文件。</li></ul><p>这些取值可以根据需要灵活选择，以满足不同的日志轮转需求。例如，如果需要每小时生成一个日志文件，可以将<code>when</code>参数设置为<code>&#39;H&#39;</code>；如果需要每周生成一个日志文件，可以将<code>when</code>参数设置为<code>&#39;W0&#39;</code> ~ <code>&#39;W6&#39;</code>，其中<code>W0</code>表示周一，<code>W1</code>表示周二，依此类推。</p><p>注意，这些取值均为大小写敏感。另外，<code>TimedRotatingFileHandler</code>还支持<code>interval</code>参数，用于指定轮转时间间隔的数量。例如，<code>interval=2</code>表示每2个时间单位生成一个日志文件（如每2小时、每2天等）。</p><p>更多关于<code>TimedRotatingFileHandler</code>的详细信息，可以查看Python官方文档的<a href="https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler">Logging.handlers.TimedRotatingFileHandler</a>部分。</p>]]></content>
    
    
    <summary type="html">让AI告诉你如何在python中记录日志，该文章完全由ChatGPT创作完成。</summary>
    
    
    
    <category term="Python" scheme="https://www.xdull.cn/categories/Python/"/>
    
    
    <category term="python" scheme="https://www.xdull.cn/tags/python/"/>
    
    <category term="日志" scheme="https://www.xdull.cn/tags/%E6%97%A5%E5%BF%97/"/>
    
    <category term="logging" scheme="https://www.xdull.cn/tags/logging/"/>
    
  </entry>
  
  <entry>
    <title>基于Docker的Jenkins安装及流水线项目部署</title>
    <link href="https://www.xdull.cn/jenkins.html"/>
    <id>https://www.xdull.cn/jenkins.html</id>
    <published>2023-05-15T15:45:00.000Z</published>
    <updated>2026-07-22T15:15:28.789Z</updated>
    
    <content type="html"><![CDATA[<h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>拉取镜像：<code>docker pull jenkinsci/blueocean:latest</code></p><p>创建主目录：<code>mkdir -p /data/jenkins</code></p><p>运行容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run -d -u root \<br>-p 8080:8080 \<br>-p 50000:50000 \<br>-v /data/jenkins:/var/jenkins_home \<br>-v /etc/localtime:/etc/localtime \<br>-v /var/run/docker.sock:/var/run/docker.sock \<br>--name jenkins jenkinsci/blueocean:latest<br></code></pre></td></tr></table></figure><h2 id="访问页面"><a href="#访问页面" class="headerlink" title="访问页面"></a>访问页面</h2><p>浏览器访问<a href="http://ip:8080进入Jenkins管理界面">http://ip:8080进入Jenkins管理界面</a><br>初始密码查询：<code>cat /data/jenkins/secrets/initialAdminPassword</code><br>默认账号名：<code>admin</code></p><h2 id="插件安装"><a href="#插件安装" class="headerlink" title="插件安装"></a>插件安装</h2><p>如果在页面安装插件失败，也可进入jenkins容器内部用命令安装，比如安装<code>git</code>插件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">jenkins-plugin-cli --plugins git<br></code></pre></td></tr></table></figure><h2 id="修改插件镜像源"><a href="#修改插件镜像源" class="headerlink" title="修改插件镜像源"></a>修改插件镜像源</h2><p>Jenkins在安装插件时，默认使用官方地址下载，有时可能速度太慢导致安装失败，可换成国内镜像地址：</p><p><code>vim /data/jenkins/hudson.model.UpdateCenter.xml</code></p><p>修改改成清华大学镜像地址：</p><p><a href="https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json">https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</a></p><h1 id="部署mvn程序"><a href="#部署mvn程序" class="headerlink" title="部署mvn程序"></a>部署mvn程序</h1><p><del>安装插件Docker 和 Docker Pipeline</del></p><p>设置maven镜像仓库，创建一个文件<code>/data/jenkins/settings.xml</code>，里面指定镜像路径：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs xml"> <span class="hljs-tag">&lt;<span class="hljs-name">mirrors</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">mirror</span>&gt;</span>  <br>     <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>alimaven<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>  <br>     <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>aliyun maven<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>  <br>     <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>http://maven.aliyun.com/nexus/content/groups/public/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>  <br>     <span class="hljs-tag">&lt;<span class="hljs-name">mirrorOf</span>&gt;</span>central<span class="hljs-tag">&lt;/<span class="hljs-name">mirrorOf</span>&gt;</span>          <br><span class="hljs-tag">&lt;/<span class="hljs-name">mirror</span>&gt;</span><br> <span class="hljs-tag">&lt;/<span class="hljs-name">mirrors</span>&gt;</span><br></code></pre></td></tr></table></figure><p>新建流水线项目，由于这是使用的maven是容器，把配置文件映射进去，表示从阿里云获取maven依赖，这样拉取依赖会快很多；mvn容器默认使用的jdk11，有时候为了使用低版本的java，还需要把java环境变量配置进去：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs Jenkinsfile">pipeline &#123;<br>    agent &#123;<br>        docker &#123;<br>                    image &#39;maven:latest&#39;<br>                    args &#39;-v &#x2F;root&#x2F;.m2:&#x2F;root&#x2F;.m2 -v &#x2F;data&#x2F;jenkins&#x2F;settings.xml:&#x2F;usr&#x2F;share&#x2F;maven&#x2F;conf&#x2F;settings.xml -v &#x2F;home&#x2F;xtwh&#x2F;jdk1.8.0_271:&#x2F;usr&#x2F;local&#x2F;jdk1.8.0_271 -e JAVA_HOME&#x3D;&#x2F;usr&#x2F;local&#x2F;jdk1.8.0_271&#39; <br>              &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="配置ssh拉取git代码"><a href="#配置ssh拉取git代码" class="headerlink" title="配置ssh拉取git代码"></a>配置ssh拉取git代码</h2><ol><li><p>在Jenkins服务器上创建公私钥对<code>ssh-keygen</code>；</p></li><li><p>jenkins中添加私钥：<code>Dashboard -&gt; 凭据 -&gt; 系统 -&gt; 全局凭据</code>，添加凭据<code>SSH Username with private key</code>得到唯一凭据id；</p></li><li><p>在git服务器中添加公钥；</p></li><li><p>在流水线项目中添加唯一凭据id：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs plain">pipeline &#123;<br>    agent any<br>    stages &#123;<br>        stage(&#39;Git Clone&#39;) &#123;<br>            steps &#123;<br>                git credentialsId: &#39;xxx-xxx-xxx&#39;, url: &#39;ssh:&#x2F;&#x2F;git@xxx.xxx.xxx&#x2F;gitlab-instance-xxx&#x2F;xxx.git&#39;<br>            &#125;<br>        &#125;<br>        &#x2F;&#x2F; ...<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure></li></ol><h3 id="报错一"><a href="#报错一" class="headerlink" title="报错一"></a>报错一</h3><p><strong>You’re using ‘Known hosts file’ strategy to verify ssh host keys, but your known_hosts file does not exist, please go to ‘Manage Jenkins’ -&gt; ‘Configure Global Security’ -&gt; ‘Git Host Key Verification Configuration’ and configure host key verification.</strong></p><p>方式一：</p><p>在容器中手动创建文件<code>/root/.ssh/known_hosts</code></p><p>方式二：<br>修改配置<code>系统管理 -&gt; 全局安全配置</code></p><p><img src="/postimgs/jenkins/1.png" alt=""></p><h3 id="报错二"><a href="#报错二" class="headerlink" title="报错二"></a>报错二</h3><p><strong>Couldn’t find any revision to build. Verify the repository and branch configuration for this job.</strong></p><p>Jenkins调用git时用了默认的<code>master</code>分支，而gitlab这边使用的是<code>main</code>分支，指定分支名：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs plain">stage(&#39;Git Clone&#39;) &#123;<br>    steps &#123;<br>        git branch: &#39;main&#39;, credentialsId: &#39;xxx-xxx-xxx&#39;, url: &#39;ssh:&#x2F;&#x2F;git@xxx.xxx.xxx&#x2F;gitlab-instance-xxx&#x2F;xxx.git&#39;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="发布"><a href="#发布" class="headerlink" title="发布"></a>发布</h2><p>需要安装<code>Publish Over SSH</code>插件</p><p><code>系统管理 -&gt; 系统配置</code>中找到<code>Publish over SSH</code>选项，添加服务器，主要配置如下：</p><ul><li>SSH Server Name: ssh配置名称（自定义）记住这个名称，pipeline代码中要用到</li><li>Hostname: 主机IP</li><li>Username: ssh账户名</li><li>Password: ssh密码</li><li>Remote Directory: 远程路径，可以理解为ssh连接上后自动cd到目录</li></ul><p>完整部署脚本：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><code class="hljs plain">def remoteId &#x3D; &#39;xxx.xxx.xxx.xxx&#39;  &#x2F;&#x2F; 远程服务器配置名<br><br>pipeline &#123;<br>    agent &#123;<br>        docker &#123;<br>                    image &#39;maven:latest&#39;<br>                    args &#39;-v &#x2F;root&#x2F;.m2:&#x2F;root&#x2F;.m2 -v &#x2F;data&#x2F;jenkins&#x2F;settings.xml:&#x2F;usr&#x2F;share&#x2F;maven&#x2F;conf&#x2F;settings.xml -v &#x2F;home&#x2F;xtwh&#x2F;jdk1.8.0_271:&#x2F;usr&#x2F;local&#x2F;jdk1.8.0_271 -e JAVA_HOME&#x3D;&#x2F;usr&#x2F;local&#x2F;jdk1.8.0_271&#39; <br>              &#125;<br>    &#125;<br>    stages &#123;<br>        stage(&#39;Checkout&#39;) &#123; <br>            steps &#123;<br>                git branch: &#39;main&#39;, credentialsId: &#39;xxx-xxx-xxx&#39;, url: &#39;ssh:&#x2F;&#x2F;git@xxx.xxx.xxx&#x2F;gitlab-instance-xxx&#x2F;xxx.git&#39;<br>            &#125;<br>        &#125;   <br>        stage(&#39;Build&#39;) &#123;<br>            steps &#123;<br>                sh &#39;mvn -B -DskipTests clean package&#39;<br>            &#125;<br>        &#125; <br>        stage(&#39;Zip&#39;)&#123;<br>            steps&#123;<br>            &#x2F;&#x2F; 将生成的Dockerfile，deploy.sh 和jar包整成压缩包发往远程服务器<br>                sh &quot;&quot;&quot;<br>                cd .&#x2F;your-soa-test&#x2F;target<br>rm -rf target.tar.gz<br>tar -zcvf target.tar.gz lib&#x2F;* resources&#x2F;* your-soa-test-0.0.1-SNAPSHOT-boot.jar<br>                &quot;&quot;&quot;<br>            &#125;<br>        &#125;<br>        stage(&#39;Publish&#39;)&#123;<br>            steps &#123;<br>            &#x2F;&#x2F; 必须装了Publish over SSH插件才能写这个<br>                sshPublisher(publishers: [sshPublisherDesc(configName: &quot;&quot;&quot;$remoteId&quot;&quot;&quot;, transfers: [sshTransfer(cleanRemote: false, excludes: &#39;&#39;,<br>                        execCommand: &quot;&quot;&quot;<br>                        supervisorctl status &gt; 1.txt   &#x2F;&#x2F; 这里的1.txt在&#x2F;root目录，即默认工作目录~<br>                        &quot;&quot;&quot;,<br>                        execTimeout: 120000,<br>                        flatten: false,<br>                        makeEmptyDirs: false,<br>                        noDefaultExcludes: false,<br>                        patternSeparator: &#39;[, ]+&#39;,<br>                        remoteDirectory: &#39;&#x2F;test&#39;,         &#x2F;&#x2F; 这里是相对路径（相对于远程服务器的Remote Directory目录&#x2F;usr&#x2F;local&#x2F;jars）<br>                        remoteDirectorySDF: false,  <br>                        removePrefix: &#39;your-soa-test&#x2F;target&#39;,<br>                        sourceFiles: &#39;your-soa-test&#x2F;target&#x2F;target.tar.gz&#39;)],   &#x2F;&#x2F; 这里是相对路径(相对于Jenkins的工作目录&#x2F;var&#x2F;jenkins_home&#x2F;workspace&#x2F;project_name)<br>                        usePromotionTimestamp: false,<br>                        useWorkspaceInPromotion: false,<br>                        verbose: true)])<br>                echo &quot;发布成功&quot;<br>            &#125;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="触发（基于推送gitlab）"><a href="#触发（基于推送gitlab）" class="headerlink" title="触发（基于推送gitlab）"></a>触发（基于推送gitlab）</h2><p><strong>Jenkins操作</strong></p><p>安装插件<code>gitlab</code></p><p>项目的构建触发器，勾选gitlab选项</p><p><img src="/postimgs/jenkins/2.jpg" alt=""></p><p>设置<code>Secret token</code>，最后保存配置</p><p><img src="/postimgs/jenkins/3.jpg" alt=""></p><p><strong>Gitlab操作</strong></p><p><code>项目 -&gt; 设置 -&gt; Webhooks</code>，创建一个Webhook，主要配置如下3项</p><ul><li>Jenkins触发url地址</li><li>token值</li><li>触发条件：推送</li></ul><p><img src="/postimgs/jenkins/4.jpg" alt=""></p><h3 id="报错"><a href="#报错" class="headerlink" title="报错"></a>报错</h3><p><strong>Urlis blocked: Requests to the local network are not allowed</strong></p><p>在gitlab的<code>admin -&gt; Settings -&gt; Network</code>的<code>Outbound requests</code>选项中，勾选<code>Allow requests to the local network from webhooks and integrations</code>：</p><p><img src="/postimgs/jenkins/1.jpg" alt=""></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;安装&quot;&gt;&lt;a href=&quot;#安装&quot; class=&quot;headerlink&quot; title=&quot;安装&quot;&gt;&lt;/a&gt;安装&lt;/h1&gt;&lt;p&gt;拉取镜像：&lt;code&gt;docker pull jenkinsci/blueocean:latest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;创建主目录：</summary>
      
    
    
    
    <category term="Jenkins" scheme="https://www.xdull.cn/categories/Jenkins/"/>
    
    
    <category term="docker" scheme="https://www.xdull.cn/tags/docker/"/>
    
    <category term="jenkins" scheme="https://www.xdull.cn/tags/jenkins/"/>
    
    <category term="流水线" scheme="https://www.xdull.cn/tags/%E6%B5%81%E6%B0%B4%E7%BA%BF/"/>
    
  </entry>
  
</feed>
