<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>MeowRain的技术博客</title><description>技术分享与实践</description><link>https://blog.meowrain.cn/</link><language>zh_CN</language><item><title>JUC-join方法</title><link>https://blog.meowrain.cn/posts/java/juc/juc-join%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-join%E6%96%B9%E6%B3%95/</guid><pubDate>Wed, 18 Feb 2026 18:44:10 GMT</pubDate><content:encoded>&lt;h1&gt;JUC-join方法&lt;/h1&gt;
&lt;p&gt;在多线程编程中，我们经常遇到一种场景：线程 A 的执行必须依赖于线程 B 的执行结果。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须先“下载图片”（线程 B），才能“显示图片”（线程 A）。&lt;/li&gt;
&lt;li&gt;必须先“加载数据库数据”（线程 B），才能“计算报表”（线程 A）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果不加控制，线程 A 可能会在 B 还没结束时就跑完了，导致数据错误。这时候，就是 Thread.join() 登场的时候了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/ukdni5-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package demo;

import java.util.concurrent.TimeUnit;

public class Demo1 {

    public static void main(String[] args) {

        new Thread(()-&amp;gt;{
            System.out.println(&quot;妈妈开始做饭&quot;);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(&quot;妈妈做好饭了&quot;);

        }).start();

        System.out.println(&quot;家人们可以吃饭了&quot;);
    }


}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;join 到底做了什么？&lt;/h1&gt;
&lt;p&gt;当在 main 方法中执行 mom.join() 时，发生的事情如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;谁在等待？ 是 Main 线程（调用者）在等待。

等谁？ 等 mom 线程（被调用者）。

状态变化：

    Main 线程：从 RUNNABLE 变为 WAITING（无限期等待）。

    mom 线程：继续欢快地运行。

何时唤醒？ 当 mom 线程运行结束（Terminated），JVM 会自动唤醒所有在 mom 对象上等待的线程（这里就是 Main 线程）。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/um5z1o-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;t.join() 插队 调用方 等待 t 执行完 Yes (释放 t 的锁) RUNNABLE -&amp;gt; WAITING&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/umxyzl-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;进阶用法：join(long millis)&lt;/h1&gt;
&lt;p&gt;如果你不想死等，可以使用 join(long millis)。&lt;/p&gt;
</content:encoded></item><item><title>JUC-yield方法</title><link>https://blog.meowrain.cn/posts/java/juc/juc-yield%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-yield%E6%96%B9%E6%B3%95/</guid><pubDate>Wed, 18 Feb 2026 18:37:38 GMT</pubDate><content:encoded>&lt;h1&gt;JUC-yield方法&lt;/h1&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;定义：yield 是 Thread 类的静态方法。它告诉当前正在执行的线程：“如果你现在不急的话，可以把 CPU 让给其他线程去跑一跑。”

作用：它会提示线程调度器（Scheduler），当前线程愿意放弃当前的 CPU 使用权。

结果：

    线程从 Running（运行中） 状态转变为 Ready（就绪） 状态。

    注意：在 Java 的线程状态定义中，这两个都属于 RUNNABLE。所以调用 yield() 后，线程状态依然是 RUNNABLE，不会变成 WAITING 或 BLOCKED。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;yield() 只是给操作系统的线程调度器发送一个建议。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;调度器完全可以忽略这个建议。

如果 CPU 资源很空闲，或者没有其他同优先级的线程在等待，调度器可能让当前线程继续执行，yield 就跟没调一样。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意： yield也不会让出锁，和sleep是一样的。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>JUC-线程各状态触发表</title><link>https://blog.meowrain.cn/posts/java/juc/juc-%E7%BA%BF%E7%A8%8B%E5%90%84%E7%8A%B6%E6%80%81%E8%A7%A6%E5%8F%91%E8%A1%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-%E7%BA%BF%E7%A8%8B%E5%90%84%E7%8A%B6%E6%80%81%E8%A7%A6%E5%8F%91%E8%A1%A8/</guid><pubDate>Wed, 18 Feb 2026 18:33:21 GMT</pubDate><content:encoded>&lt;h1&gt;JUC-线程各状态触发表&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/ubkq6z-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;操作系统线程状态和java线程状态的区别&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/ucj8lx-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/ucq4sl-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/uda2da-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Java 的 RUNNABLE 状态 涵盖了 操作系统层面的 Ready、Running 以及部分 Blocked（主要是 I/O 阻塞）状态。&lt;/p&gt;
</content:encoded></item><item><title>JUC-Thread.sleep</title><link>https://blog.meowrain.cn/posts/java/juc/juc-sleep%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-sleep%E6%96%B9%E6%B3%95/</guid><pubDate>Wed, 18 Feb 2026 18:28:23 GMT</pubDate><content:encoded>&lt;h1&gt;深入理解 Java 并发：Thread.sleep() 与线程状态流转&lt;/h1&gt;
&lt;p&gt;在 Java 并发编程中，控制线程的执行节奏是基础且重要的技能。&lt;/p&gt;
&lt;h2&gt;1. 什么是 Thread.sleep()？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Thread.sleep(long millis)&lt;/code&gt; 是 &lt;code&gt;Thread&lt;/code&gt; 类的静态方法。它的主要作用是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;暂停执行&lt;/strong&gt;：让&lt;strong&gt;当前正在执行的线程&lt;/strong&gt;暂停一段指定的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不释放锁&lt;/strong&gt;：这是它最关键的特性。如果当前线程持有了某个对象的锁，在睡觉（sleep）的过程中，它&lt;strong&gt;不会&lt;/strong&gt;释放这个锁，其他线程依然无法访问该锁保护的资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态变化&lt;/strong&gt;：它会让线程从 &lt;code&gt;RUNNABLE&lt;/code&gt; 状态变为 &lt;code&gt;TIMED_WAITING&lt;/code&gt;（计时等待）状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 实战演示：代码重构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package demo;

import java.util.concurrent.TimeUnit;

public class SleepStateDemo {

    public static void main(String[] args) {
        // 1. 创建线程
        Thread thread = new Thread(() -&amp;gt; {
            try {
                // 模拟耗时操作或等待，睡眠 3 秒
                TimeUnit.SECONDS.sleep(3); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, &quot;customThread&quot;);

        // 此时线程对象已创建，但尚未启动
        System.out.println(thread.getName() + &quot; state: &quot; + thread.getState()); // 预期：NEW

        // 2. 启动线程
        thread.start();
        // 启动后，线程通常处于 RUNNABLE 状态（取决于操作系统调度）
        System.out.println(thread.getName() + &quot; state: &quot; + thread.getState()); // 预期：RUNNABLE

        // 3. 主线程暂停，观察子线程状态
        try {
            // 主线程睡眠 100 毫秒，确保子线程有足够的时间开始执行并进入 sleep 状态
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 子线程正在执行 sleep(3)，此时应处于计时等待状态
        System.out.println(thread.getName() + &quot; state: &quot; + thread.getState()); // 预期：TIMED_WAITING
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行结果解析&lt;/h3&gt;
&lt;p&gt;执行上述代码，控制台将输出如下内容（这解释了你截图中看到的现象）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;customThread state: NEW
customThread state: RUNNABLE
customThread state: TIMED_WAITING

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;状态流转图解：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NEW&lt;/strong&gt;: &lt;code&gt;new Thread(...)&lt;/code&gt; 被调用后，线程对象被创建，但还没调用 &lt;code&gt;start()&lt;/code&gt;，此时就像一个还没通电的灯泡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RUNNABLE&lt;/strong&gt;: 调用 &lt;code&gt;start()&lt;/code&gt; 后，线程进入可运行池。注意，Java 中的 &lt;code&gt;RUNNABLE&lt;/code&gt; 涵盖了操作系统层面的 &quot;Running&quot;（正在跑）和 &quot;Ready&quot;（等待CPU调度）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIMED_WAITING&lt;/strong&gt;: 当子线程执行到 &lt;code&gt;TimeUnit.SECONDS.sleep(3)&lt;/code&gt; 时，它会主动交出 CPU 使用权，进入计时等待状态。主线程在等待了 100ms 后去查看它，正好抓到了它在 &quot;睡觉&quot; 的瞬间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 关键知识点总结&lt;/h2&gt;
&lt;p&gt;在面试或实际开发中，关于 &lt;code&gt;sleep()&lt;/code&gt; 有几个核心细节需要注意：&lt;/p&gt;
&lt;h3&gt;3.1 sleep() vs wait() (高频面试题)&lt;/h3&gt;
&lt;p&gt;这是最容易混淆的一点。虽然它们都能让线程暂停，但本质完全不同：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Thread.sleep()&lt;/th&gt;
&lt;th&gt;Object.wait()&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;所属类&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Thread&lt;/strong&gt; 类 (静态方法)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Object&lt;/strong&gt; 类 (实例方法)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;锁的释放&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不释放锁&lt;/strong&gt; (抱着锁睡觉)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;释放锁&lt;/strong&gt; (让出资源给别人)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;使用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仅仅是让线程暂停执行&lt;/td&gt;
&lt;td&gt;用于线程间的通信 (配合 notify)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;唤醒方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;时间到了自动醒，或被 interrupt&lt;/td&gt;
&lt;td&gt;需要 notify/notifyAll 或时间到&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.2 InterruptedException&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sleep&lt;/code&gt; 方法强制要求捕获 &lt;code&gt;InterruptedException&lt;/code&gt;。这意味着当一个线程正在睡眠时，其他线程可以使用 &lt;code&gt;thread.interrupt()&lt;/code&gt; 方法来 &quot;叫醒&quot; 它。被中断时，sleep 会抛出异常并清除中断标志位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package demo;

import java.util.concurrent.TimeUnit;

public class Demo1 {

    public static void main(String[] args) {
        Thread thread = new Thread(() -&amp;gt; {
            try {

                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }


        },&quot;customeThread&quot;);
        System.out.println(thread.getName()+ &quot; state: &quot; + thread.getState());
        thread.start();
        System.out.println(thread.getName()+ &quot; state: &quot; + thread.getState());


        try {
            Thread.sleep(100);
            // 打断会抛出异常
            thread.interrupt();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(thread.getName() + &quot; state: &quot; + thread.getState());



    }



}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/u9sm5j-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.3 推荐使用 TimeUnit&lt;/h3&gt;
&lt;p&gt;在 JDK 1.5 之后，强烈建议使用 &lt;code&gt;TimeUnit&lt;/code&gt; 来替代直接调用 &lt;code&gt;Thread.sleep&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bad:&lt;/strong&gt; &lt;code&gt;Thread.sleep(180000)&lt;/code&gt; (这是多久？3分钟？还是30秒？)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good:&lt;/strong&gt; &lt;code&gt;TimeUnit.MINUTES.sleep(3)&lt;/code&gt; (清晰明了)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JUC-start()和run()的区别</title><link>https://blog.meowrain.cn/posts/java/juc/juc-start%E5%92%8Crun%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-start%E5%92%8Crun%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Wed, 18 Feb 2026 17:44:02 GMT</pubDate><content:encoded>&lt;h1&gt;Thread.start()和Thread.run()的区别&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;start()&lt;/code&gt; 和 &lt;code&gt;run()&lt;/code&gt; 最大的区别就一句话：&lt;strong&gt;&lt;code&gt;start()&lt;/code&gt; 会真的开新线程；&lt;code&gt;run()&lt;/code&gt; 只是普通方法调用，不会并发。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1）&lt;code&gt;run()&lt;/code&gt;：普通方法调用（不新建线程）&lt;/h2&gt;
&lt;p&gt;你直接调用 &lt;code&gt;run()&lt;/code&gt;，代码就在&lt;strong&gt;当前线程&lt;/strong&gt;里顺序执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t = new Thread(() -&amp;gt; System.out.println(Thread.currentThread().getName()));
t.run(); // 还是 main 线程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出通常是：&lt;code&gt;main&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2）&lt;code&gt;start()&lt;/code&gt;：启动新线程（并发执行）&lt;/h2&gt;
&lt;p&gt;调用 &lt;code&gt;start()&lt;/code&gt; 后，JVM 会创建一个新的操作系统线程，然后在&lt;strong&gt;新线程&lt;/strong&gt;里回调你的 &lt;code&gt;run()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t = new Thread(() -&amp;gt; System.out.println(Thread.currentThread().getName()));
t.start(); // 新线程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出通常是：&lt;code&gt;Thread-0&lt;/code&gt;（或类似名字）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;start()&lt;/code&gt; 只是“让线程进入可运行状态”，什么时候真正执行由调度器决定，所以输出时序不固定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/txrk2e-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>JUC-创建线程</title><link>https://blog.meowrain.cn/posts/java/juc/juc-%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc-%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B/</guid><pubDate>Wed, 18 Feb 2026 17:44:02 GMT</pubDate><content:encoded>&lt;h1&gt;JUC 笔记-创建线程&lt;/h1&gt;
&lt;h2&gt;继承Thread类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package demo;

public class Demo1 {

    public static void main(String[] args) {
        Demo1Thread demo1Thread = new Demo1Thread();
        demo1Thread.start();

    }
}
class Demo1Thread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/sms8im-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过这样还是有缺陷的，因为java是单继承，所以一个类一旦继承了Thread类，就没办法继承其它类了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;实现Runnable接口&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package demo;

public class Demo1 {

    public static void main(String[] args) {
        new Thread(new Demo1Thread()).start();

    }
}
class Demo1Thread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/somi9e-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/spob7o-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/spua8q-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Callable接口&lt;/h2&gt;
&lt;p&gt;前面的线程运行不会返回数据，Callable接口就是为了能接收会返回数据的线程的结果的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo1 {

    public static void main(String[] args) {
        Callable callable = new Demo1Thread(3,10);
        FutureTask&amp;lt;Integer&amp;gt; futureTask = new FutureTask(callable);
        Thread t = new Thread(futureTask);
        t.start();
        try {
            Integer i = futureTask.get();

            System.out.println(&quot;result: &quot; + i);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }


    }
}
class Demo1Thread implements Callable&amp;lt;Integer&amp;gt; {
    private int a;
    private int b;
    public Demo1Thread(int a,int b) {
        this.a = a;
        this.b = b;
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }

    public void setA(int a) {
        this.a = a;
    }

    public void setB(int b) {
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return a + b;
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/02/18/stf4hy-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Python面向对象快速入门</title><link>https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/</guid><description>一篇关于 Python 面向对象编程（OOP）的快速入门指南，涵盖类、对象、封装、继承和多态等核心概念。</description><pubDate>Mon, 19 Jan 2026 22:53:40 GMT</pubDate><content:encoded>&lt;p&gt;面向对象编程（Object-Oriented Programming，简称 OOP）是一种程序设计思想。在 Python 中，一切皆对象。掌握 OOP 是进阶 Python 编程的关键一步。&lt;/p&gt;
&lt;p&gt;本文将带你快速理解 Python 面向对象的核心概念。&lt;/p&gt;
&lt;h2&gt;1. 类 (Class) 与 对象 (Object)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;类&lt;/strong&gt;是创建对象的蓝图（模板），&lt;strong&gt;对象&lt;/strong&gt;是类的具体实例。&lt;/p&gt;
&lt;p&gt;比喻：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类&lt;/strong&gt;：汽车的设计图纸。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象&lt;/strong&gt;：根据图纸制造出来的具体的一辆辆汽车（如你的宝马、他的奔驰）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;定义类与创建对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 定义一个类
class Dog:
    pass

# 创建对象（实例化）
dog1 = Dog()
dog2 = Dog()

print(dog1)  # &amp;lt;__main__.Dog object at ...&amp;gt;
print(dog1 == dog2) # False，它们是两个不同的对象
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 构造方法与属性&lt;/h2&gt;
&lt;p&gt;在类中，我们可以定义属性（变量）来描述对象的特征。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;__init__&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;__init__&lt;/code&gt; 是一个特殊方法（构造方法），在创建对象时自动调用，用于初始化对象的属性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;self&lt;/code&gt;：代表类的实例（对象）本身。在定义类的方法时，第一个参数通常是 &lt;code&gt;self&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Cat:
    def __init__(self, name, age):
        self.name = name  # 实例属性
        self.age = age    # 实例属性

# 创建对象时传入参数
tom = Cat(&quot;Tom&quot;, 3)
jerry = Cat(&quot;Jerry&quot;, 2)

print(f&quot;{tom.name} is {tom.age} years old.&quot;)
# 输出: Tom is 3 years old.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 方法 (Methods)&lt;/h2&gt;
&lt;p&gt;方法就是定义在类内部的函数，用来描述对象的行为。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name):
        self.name = name

    def say_hello(self):
        print(f&quot;Hello, my name is {self.name}.&quot;)

p = Person(&quot;Alice&quot;)
p.say_hello()
# 输出: Hello, my name is Alice.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 封装 (Encapsulation)&lt;/h2&gt;
&lt;p&gt;封装是指将数据（属性）和操作数据的方法绑定在一起，并隐藏对象的内部实现细节。&lt;/p&gt;
&lt;p&gt;在 Python 中，通过在属性名前加双下划线 &lt;code&gt;__&lt;/code&gt; 将其变为私有属性（Private），外部无法直接访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # 私有属性

    def deposit(self, amount):
        if amount &amp;gt; 0:
            self.__balance += amount
            print(f&quot;Deposited {amount}&quot;)

    def get_balance(self):
        return self.__balance

account = BankAccount(100)
account.deposit(50)
print(account.get_balance()) # 输出: 150

# print(account.__balance) # 报错！无法直接访问私有属性
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 继承 (Inheritance)&lt;/h2&gt;
&lt;p&gt;继承允许我们创建一个新类（子类），从现有的类（父类）继承属性和方法。这提高了代码的复用性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 父类
class Animal:
    def speak(self):
        print(&quot;Animal speaks&quot;)

# 子类继承父类
class Dog(Animal):
    def speak(self):
        print(&quot;Woof!&quot;)  # 重写父类方法

class Cat(Animal):
    pass 

dog = Dog()
dog.speak() # 输出: Woof!

cat = Cat()
cat.speak() # 输出: Animal speaks (直接继承父类方法)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;super()&lt;/code&gt; 函数&lt;/h3&gt;
&lt;p&gt;子类可以使用 &lt;code&gt;super()&lt;/code&gt; 调用父类的方法，常用于扩展父类的 &lt;code&gt;__init__&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Bird(Animal):
    def __init__(self, name, can_fly):
        super().__init__() # 调用父类构造方法（如果有的话）
        self.name = name
        self.can_fly = can_fly
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 多态 (Polymorphism)&lt;/h2&gt;
&lt;p&gt;多态指“多种形态”。不同的子类对象调用相同的方法，产生不同的行为。&lt;/p&gt;
&lt;p&gt;上面的 &lt;code&gt;Dog&lt;/code&gt; 和 &lt;code&gt;Cat&lt;/code&gt; 都继承自 &lt;code&gt;Animal&lt;/code&gt; 并调用 &lt;code&gt;speak()&lt;/code&gt; 方法，但表现不同，这就是多态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def animal_sound(animal):
    animal.speak()

dog = Dog()
cat = Cat()

animal_sound(dog) # 输出: Woof!
animal_sound(cat) # 输出: Animal speaks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类&lt;/strong&gt;是模板，&lt;strong&gt;对象&lt;/strong&gt;是实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;属性&lt;/strong&gt;描述特征，&lt;strong&gt;方法&lt;/strong&gt;描述行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装&lt;/strong&gt;保护数据安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承&lt;/strong&gt;实现代码复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多态&lt;/strong&gt;提供灵活的接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;掌握这些概念，你就迈入了 Python 面向对象编程的大门！&lt;/p&gt;
</content:encoded></item><item><title>Python面向对象编程终极指南：原理、进阶与元编程</title><link>https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90/</guid><description>一篇涵盖 Python 面向对象编程（OOP）所有核心细节的终极指南。从底层的对象模型、内存管理，到进阶的描述符、MRO 算法、元类编程及设计模式。</description><pubDate>Mon, 19 Jan 2026 22:53:40 GMT</pubDate><content:encoded>&lt;p&gt;这是一篇旨在彻底讲透 Python 面向对象编程（OOP）的终极指南。我们将不再局限于基础语法，而是深入到 Python 的对象模型底层，探讨&lt;strong&gt;元类&lt;/strong&gt;、&lt;strong&gt;描述符&lt;/strong&gt;、&lt;strong&gt;方法解析顺序 (MRO)&lt;/strong&gt; 以及&lt;strong&gt;内存管理&lt;/strong&gt;等高级话题。&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对象模型底层&lt;/strong&gt;：&lt;code&gt;__new__&lt;/code&gt; vs &lt;code&gt;__init__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深入属性系统&lt;/strong&gt;：&lt;code&gt;__slots__&lt;/code&gt; 与 描述符协议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承的奥秘&lt;/strong&gt;：多重继承、Mixin 与 C3 算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口与约束&lt;/strong&gt;：抽象基类 (ABC) 与 协议 (Protocol)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元编程 (Metaprogramming)&lt;/strong&gt;：动态创建类与元类&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;魔术方法大全&lt;/strong&gt;：模拟 Python 内置行为&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理与垃圾回收&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 对象模型底层：&lt;code&gt;__new__&lt;/code&gt; vs &lt;code&gt;__init__&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;很多人认为 &lt;code&gt;__init__&lt;/code&gt; 是构造函数，其实不然。对象的创建过程分为两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构造 (Construction)&lt;/strong&gt;：&lt;code&gt;__new__&lt;/code&gt; 分配内存，创建对象实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化 (Initialization)&lt;/strong&gt;：&lt;code&gt;__init__&lt;/code&gt; 给这个已经创建好的实例设置初始值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.1 &lt;code&gt;__new__&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;__new__&lt;/code&gt; 是一个静态方法（虽然不需要写 &lt;code&gt;@staticmethod&lt;/code&gt;），它的第一个参数是 &lt;code&gt;cls&lt;/code&gt;。它&lt;strong&gt;必须&lt;/strong&gt;返回一个实例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不可变对象 (Immutable Objects)&lt;/strong&gt;：继承自 &lt;code&gt;str&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;tuple&lt;/code&gt; 的子类，因为它们一旦创建就无法修改，所以必须在 &lt;code&gt;__new__&lt;/code&gt; 中定制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单例模式 (Singleton)&lt;/strong&gt;：控制只创建一个实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元类编程&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class UpperStr(str):
    def __new__(cls, value):
        # 在对象创建前拦截，强制转换为大写
        return super().__new__(cls, value.upper())

s = UpperStr(&quot;hello&quot;)
print(s) # HELLO (str 是不可变的，必须在 __new__ 处理)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 单例模式实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            # 只有第一次调用时才真正创建对象
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 深入属性系统：&lt;code&gt;__slots__&lt;/code&gt; 与 描述符&lt;/h2&gt;
&lt;h3&gt;2.1 &lt;code&gt;__slots__&lt;/code&gt;：内存优化&lt;/h3&gt;
&lt;p&gt;默认情况下，Python 对象使用字典 (&lt;code&gt;__dict__&lt;/code&gt;) 存储属性。这提供了极大的灵活性，但对于创建数百万个小对象的场景，内存消耗巨大。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;__slots__&lt;/code&gt; 告诉 Python：“这个类只有这些属性，不要创建 &lt;code&gt;__dict__&lt;/code&gt;”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Pixel:
    __slots__ = (&apos;x&apos;, &apos;y&apos;)  # 锁定属性，禁止动态添加其他属性

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Pixel(10, 20)
# p.z = 30  # AttributeError: &apos;Pixel&apos; object has no attribute &apos;z&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;副作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象不再有 &lt;code&gt;__dict__&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;无法动态添加新属性。&lt;/li&gt;
&lt;li&gt;继承时如果不重复定义 &lt;code&gt;__slots__&lt;/code&gt;，子类依然会有 &lt;code&gt;__dict__&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 描述符 (Descriptors)&lt;/h3&gt;
&lt;p&gt;这是 Python 属性魔法的&lt;strong&gt;核心&lt;/strong&gt;。&lt;code&gt;@property&lt;/code&gt;、类方法、静态方法，底层全都是描述符。&lt;/p&gt;
&lt;p&gt;一个实现了 &lt;code&gt;__get__&lt;/code&gt;, &lt;code&gt;__set__&lt;/code&gt;, 或 &lt;code&gt;__delete__&lt;/code&gt; 方法的&lt;strong&gt;类&lt;/strong&gt;，就是一个描述符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Integer:
    &quot;&quot;&quot;数据描述符：强制属性必须是整数&quot;&quot;&quot;
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError(f&quot;{self.name} must be an integer&quot;)
        instance.__dict__[self.name] = value

class Point:
    x = Integer(&quot;x&quot;)  # 描述符实例作为类属性
    y = Integer(&quot;y&quot;)

    def __init__(self, x, y):
        self.x = x  # 触发 Integer.__set__
        self.y = y

p = Point(1, 2)
# p.x = &quot;hello&quot;  # ValueError: x must be an integer
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 继承的奥秘：多重继承、Mixin 与 MRO&lt;/h2&gt;
&lt;h3&gt;3.1 多重继承与菱形问题&lt;/h3&gt;
&lt;p&gt;Python 支持多重继承。当一个类继承多个父类时，如果父类中有同名方法，Python 如何决定调用哪一个？&lt;/p&gt;
&lt;p&gt;Python 2.3 之后引入了 &lt;strong&gt;C3 线性化算法&lt;/strong&gt; 来计算 &lt;strong&gt;MRO (Method Resolution Order)&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A:
    def say(self): print(&quot;A&quot;)

class B(A):
    def say(self): print(&quot;B&quot;)

class C(A):
    def say(self): print(&quot;C&quot;)

class D(B, C):
    pass

d = D()
d.say() # 输出 B
print(D.mro()) 
# [&amp;lt;class &apos;D&apos;&amp;gt;, &amp;lt;class &apos;B&apos;&amp;gt;, &amp;lt;class &apos;C&apos;&amp;gt;, &amp;lt;class &apos;A&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子类优先于父类。&lt;/li&gt;
&lt;li&gt;多个父类按照从左到右的顺序检查。&lt;/li&gt;
&lt;li&gt;如果出现菱形继承（如上图，B和C都继承A），确保公共基类（A）最后被检查（但在 &lt;code&gt;object&lt;/code&gt; 之前）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 Mixin 模式&lt;/h3&gt;
&lt;p&gt;Mixin（混入）是一种设计模式，利用多重继承给类添加单一功能的“插件”，而不需要建立严格的父子关系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class JsonSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class User(JsonSerializableMixin):
    def __init__(self, name):
        self.name = name

u = User(&quot;Alice&quot;)
print(u.to_json())  # {&quot;name&quot;: &quot;Alice&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 接口与约束：ABC 与 Protocol&lt;/h2&gt;
&lt;p&gt;Python 是动态语言，通常不强制类型。但为了大型项目的健壮性，我们需要接口约束。&lt;/p&gt;
&lt;h3&gt;4.1 抽象基类 (Abstract Base Classes)&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;abc&lt;/code&gt; 模块定义抽象基类，强制子类实现特定方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    
    def area(self):
        return 3.14 * self.r ** 2

# s = Shape() # TypeError: Can&apos;t instantiate abstract class
c = Circle(5) # OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 Protocol (鸭子类型的静态检查)&lt;/h3&gt;
&lt;p&gt;Python 3.8 引入了 &lt;code&gt;typing.Protocol&lt;/code&gt;。它不需要继承，只要类实现了协议规定的方法，类型检查器（如 MyPy）就认为它符合要求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import Protocol

class Flyer(Protocol):
    def fly(self) -&amp;gt; None:
        ...

class Bird:
    def fly(self): print(&quot;Bird flying&quot;)

class Plane:
    def fly(self): print(&quot;Plane flying&quot;)

def lift_off(obj: Flyer):
    obj.fly()

# Bird 和 Plane 不需要显式继承 Flyer
lift_off(Bird())
lift_off(Plane())
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 元编程 (Metaprogramming)&lt;/h2&gt;
&lt;p&gt;元编程是“编写写代码的代码”。在 Python 中，类也是对象，&lt;strong&gt;元类 (Metaclass)&lt;/strong&gt; 就是用来创建类的类。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;type&lt;/code&gt; 是所有类的元类。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 关键字背后的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# class MyClass: pass
# 等价于：
MyClass = type(&apos;MyClass&apos;, (), {})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义元类&lt;/h3&gt;
&lt;p&gt;自定义元类通常继承自 &lt;code&gt;type&lt;/code&gt;，并重写 &lt;code&gt;__new__&lt;/code&gt; 或 &lt;code&gt;__init__&lt;/code&gt;。可以在类创建时修改类的定义（自动添加方法、验证属性等）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AutoDebugMeta(type):
    &quot;&quot;&quot;自动给类中的所有方法添加打印调试信息的元类&quot;&quot;&quot;
    def __new__(mcs, name, bases, attrs):
        new_attrs = {}
        for key, value in attrs.items():
            if callable(value) and not key.startswith(&quot;__&quot;):
                # 包装函数
                def wrapper(*args, **kwargs):
                    print(f&quot;Calling {key}...&quot;)
                    return value(*args, **kwargs)
                new_attrs[key] = wrapper
            else:
                new_attrs[key] = value
        
        return super().__new__(mcs, name, bases, new_attrs)

class MyService(metaclass=AutoDebugMeta):
    def process(self):
        print(&quot;Processing...&quot;)

s = MyService()
s.process()
# 输出:
# Calling process...
# Processing...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 魔术方法大全&lt;/h2&gt;
&lt;p&gt;除了常见的 &lt;code&gt;__init__&lt;/code&gt;, &lt;code&gt;__str__&lt;/code&gt;，Python 提供了极其丰富的魔术方法。&lt;/p&gt;
&lt;h3&gt;属性访问控制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__getattr__(self, name)&lt;/code&gt;: 访问&lt;strong&gt;不存在&lt;/strong&gt;的属性时调用（兜底）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__getattribute__(self, name)&lt;/code&gt;: 访问&lt;strong&gt;任何&lt;/strong&gt;属性时都会调用（拦截所有访问，慎用，易递归）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__setattr__(self, name, value)&lt;/code&gt;: 设置属性时调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容器模拟&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__len__(self)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__getitem__(self, key)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__setitem__(self, key, value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__delitem__(self, key)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__iter__(self)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__contains__(self, item)&lt;/code&gt;: &lt;code&gt;in&lt;/code&gt; 操作符。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;上下文管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__enter__&lt;/code&gt;, &lt;code&gt;__exit__&lt;/code&gt;: &lt;code&gt;with&lt;/code&gt; 语句支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;调用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__call__&lt;/code&gt;: 让实例像函数一样被调用 &lt;code&gt;instance()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 内存管理与垃圾回收&lt;/h2&gt;
&lt;p&gt;Python 使用&lt;strong&gt;引用计数 (Reference Counting)&lt;/strong&gt; 为主，&lt;strong&gt;标记-清除 (Mark and Sweep)&lt;/strong&gt; 和 &lt;strong&gt;分代回收 (Generational Collection)&lt;/strong&gt; 为辅的垃圾回收机制。&lt;/p&gt;
&lt;h3&gt;7.1 &lt;code&gt;__del__&lt;/code&gt; 析构方法&lt;/h3&gt;
&lt;p&gt;当对象的引用计数降为 0 时，&lt;code&gt;__del__&lt;/code&gt; 会被调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;：尽量不要依赖 &lt;code&gt;__del__&lt;/code&gt; 来进行资源释放（如关闭文件），因为在循环引用等复杂情况下，它可能不会被立即调用，甚至不会被调用。应使用上下文管理器 (&lt;code&gt;with&lt;/code&gt;)。&lt;/p&gt;
&lt;h3&gt;7.2 弱引用 (Weak Reference)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;weakref&lt;/code&gt; 模块允许创建不增加引用计数的引用。常用于缓存实现，避免对象无法被回收。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import weakref

class Data:
    def __del__(self):
        print(&quot;Data died&quot;)

d = Data()
r = weakref.ref(d) # 创建弱引用

print(r()) # 获取对象: &amp;lt;__main__.Data object ...&amp;gt;
del d      # 删除唯一强引用，对象立即被回收，输出 &quot;Data died&quot;
print(r()) # None
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;Python 的面向对象远比表面看起来深奥。从简单的 &lt;code&gt;class&lt;/code&gt; 定义，到背后的元类机制、描述符协议以及 C3 算法，Python 提供了一套逻辑自洽且极具扩展性的对象模型。&lt;/p&gt;
&lt;p&gt;掌握这些细节，不仅能让你写出更高效、更健壮的代码，更能让你在阅读 Django, SQLAlchemy 等顶级框架源码时游刃有余。&lt;/p&gt;
</content:encoded></item><item><title>Python面向对象进阶：属性管理与魔术方法</title><link>https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BF%9B%E9%98%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/python/python%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BF%9B%E9%98%B6/</guid><description>深入探讨 Python 面向对象编程中的进阶话题，包括类属性与实例属性的区别、三种方法类型（实例/类/静态）、@property 封装以及常用的魔术方法。</description><pubDate>Mon, 19 Jan 2026 22:53:40 GMT</pubDate><content:encoded>&lt;p&gt;在掌握了 Python 面向对象的基础之后，我们需要进一步了解如何编写更“Pythonic”的类。本文将涵盖属性管理、方法类型以及强大的魔术方法。&lt;/p&gt;
&lt;h2&gt;1. 属性与方法的进阶&lt;/h2&gt;
&lt;h3&gt;1.1 类属性 vs 实例属性&lt;/h3&gt;
&lt;p&gt;这是新手最容易混淆的地方。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实例属性&lt;/strong&gt;：定义在 &lt;code&gt;__init__&lt;/code&gt; 或其他方法中，使用 &lt;code&gt;self.variable&lt;/code&gt;。属于&lt;strong&gt;单个对象&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类属性&lt;/strong&gt;：直接定义在类体中。属于&lt;strong&gt;类本身&lt;/strong&gt;，所有实例共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Dog:
    species = &quot;Canis&quot;  # 类属性：所有狗都是犬科

    def __init__(self, name):
        self.name = name  # 实例属性：每只狗名字不同

d1 = Dog(&quot;Buddy&quot;)
d2 = Dog(&quot;Charlie&quot;)

# 访问类属性
print(d1.species)  # Canis (通过实例访问)
print(Dog.species) # Canis (推荐：通过类名访问)

# 修改类属性
Dog.species = &quot;Wolf&quot;
print(d1.species)  # Wolf (所有实例感知变化)

# 【坑点预警】通过实例修改类属性
d1.species = &quot;Cat&quot; 
# 这一步并没有修改类属性！而是在 d1 对象上创建了一个同名的实例属性 &apos;species&apos;
# 屏蔽了对类属性的访问。

print(d1.species)   # Cat (d1 的实例属性)
print(d2.species)   # Wolf (依然是类属性)
print(Dog.species)  # Wolf (类属性未变)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 三种方法类型&lt;/h3&gt;
&lt;p&gt;Python 的类中可以定义三种方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实例方法 (Instance Method)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个参数是 &lt;code&gt;self&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;可以访问实例属性和类属性。&lt;/li&gt;
&lt;li&gt;最常用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类方法 (Class Method)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;@classmethod&lt;/code&gt; 装饰器。&lt;/li&gt;
&lt;li&gt;第一个参数是 &lt;code&gt;cls&lt;/code&gt;（代表类本身）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能&lt;/strong&gt;访问实例属性，只能访问类属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：常用于实现“工厂模式”或修改类状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态方法 (Static Method)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;@staticmethod&lt;/code&gt; 装饰器。&lt;/li&gt;
&lt;li&gt;不需要 &lt;code&gt;self&lt;/code&gt; 或 &lt;code&gt;cls&lt;/code&gt; 参数。&lt;/li&gt;
&lt;li&gt;就像一个普通函数放在了类里面，逻辑上属于这个类，但在运行时与类/实例无关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：工具函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # 实例方法
    def format(self):
        return f&quot;{self.year}-{self.month}-{self.day}&quot;

    # 类方法：作为构造函数的一种替代（工厂模式）
    @classmethod
    def from_string(cls, date_str):
        # date_str 格式 &quot;2023-10-01&quot;
        year, month, day = map(int, date_str.split(&apos;-&apos;))
        return cls(year, month, day)  # 返回一个新的实例

    # 静态方法：不需要访问类或实例的数据
    @staticmethod
    def is_valid(date_str):
        return &apos;-&apos; in date_str

# 使用
d1 = Date(2023, 10, 1)
d2 = Date.from_string(&quot;2023-12-25&quot;)  # 调用类方法
print(d2.format())

print(Date.is_valid(&quot;2023-10-1&quot;)) # True
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 封装与访问控制&lt;/h2&gt;
&lt;h3&gt;2.1 私有属性与名称改写&lt;/h3&gt;
&lt;p&gt;Python 没有像 Java 那样严格的 &lt;code&gt;private&lt;/code&gt; 关键字。它通过&lt;strong&gt;命名约定&lt;/strong&gt;来实现封装。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;：&lt;code&gt;self.name&lt;/code&gt;，公有，随处可访问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected&lt;/code&gt;：&lt;code&gt;self._age&lt;/code&gt;（单下划线），&lt;strong&gt;约定&lt;/strong&gt;视为内部使用，但解释器不强制限制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt;：&lt;code&gt;self.__money&lt;/code&gt;（双下划线），解释器会进行&lt;strong&gt;名称改写 (Name Mangling)&lt;/strong&gt;，防止子类意外覆盖或外部直接访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Account:
    def __init__(self, balance):
        self.__balance = balance # 私有属性

    def get_balance(self):
        return self.__balance

acc = Account(100)
# print(acc.__balance) # AttributeError
print(acc.get_balance()) # 100

# 强行访问（不推荐，除非调试）
print(acc._Account__balance) # 100 (Python 将其改名为 _ClassName__variable)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 使用 &lt;code&gt;@property&lt;/code&gt; 装饰器&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@property&lt;/code&gt; 是 Pythonic 的封装方式。它允许你像访问属性一样调用方法，实现对属性的&lt;strong&gt;获取&lt;/strong&gt;、&lt;strong&gt;设置&lt;/strong&gt;和&lt;strong&gt;删除&lt;/strong&gt;的控制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name):
        self._name = name

    # Getter
    @property
    def name(self):
        return self._name

    # Setter
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise ValueError(&quot;Name must be a string&quot;)
        self._name = value

    # Deleter
    @name.deleter
    def name(self):
        print(&quot;Deleting name...&quot;)
        del self._name

p = Person(&quot;Alice&quot;)
print(p.name)  # 自动调用 getter
p.name = &quot;Bob&quot; # 自动调用 setter
# p.name = 123 # 抛出 ValueError
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 魔术方法 (Magic Methods)&lt;/h2&gt;
&lt;p&gt;魔术方法（Dunder Methods，双下划线方法）允许你的对象模拟内置类型的行为（如算术运算、长度获取、索引访问等）。&lt;/p&gt;
&lt;h3&gt;3.1 字符串表示：&lt;code&gt;__str__&lt;/code&gt; vs &lt;code&gt;__repr__&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__str__&lt;/code&gt;：面向用户，打印时 (&lt;code&gt;print()&lt;/code&gt;) 调用，力求可读性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__repr__&lt;/code&gt;：面向开发者，调试时 (&lt;code&gt;repl&lt;/code&gt; 环境) 调用，力求准确性（最好能用来重建对象）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f&quot;Vector({self.x}, {self.y})&quot;
    
    def __repr__(self):
        return f&quot;Vector(x={self.x}, y={self.y})&quot;

v = Vector(1, 2)
print(v)      # 调用 __str__
print([v])    # 列表内的元素会调用 __repr__
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 运算符重载&lt;/h3&gt;
&lt;p&gt;让你的对象支持 &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt; 等操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    # 接上面的 Vector 类
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2  # 自动调用 v1.__add__(v2)
print(v3)     # Vector(4, 6)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 其他常用魔术方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__len__(self)&lt;/code&gt;: &lt;code&gt;len(obj)&lt;/code&gt; 时调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__getitem__(self, key)&lt;/code&gt;: &lt;code&gt;obj[key]&lt;/code&gt; 时调用，实现索引或切片访问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__call__(self)&lt;/code&gt;: 让对象像函数一样被调用 &lt;code&gt;obj()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__enter__&lt;/code&gt; / &lt;code&gt;__exit__&lt;/code&gt;: 实现上下文管理器（&lt;code&gt;with&lt;/code&gt; 语句）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Python 的 OOP 既简洁又强大。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;区分清楚&lt;/strong&gt;类属性与实例属性，防止数据污染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;善用&lt;/strong&gt; &lt;code&gt;@property&lt;/code&gt; 和魔法方法，写出 Pythonic 的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解&lt;/strong&gt;鸭子类型，不要过分纠结于类型检查，而要关注行为（接口）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;掌握&lt;/strong&gt; &lt;code&gt;super()&lt;/code&gt;，为编写可维护的继承结构打好基础。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>scoop配置国内源</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/scoop%E9%85%8D%E7%BD%AE%E5%9B%BD%E5%86%85%E6%BA%90/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/scoop%E9%85%8D%E7%BD%AE%E5%9B%BD%E5%86%85%E6%BA%90/</guid><description>scoop配置国内源</description><pubDate>Mon, 19 Jan 2026 20:49:49 GMT</pubDate><content:encoded>&lt;h1&gt;更换scoop主仓库&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# 南京大学
scoop config SCOOP_REPO &quot;https://mirrors.nju.edu.cn/git/scoop-installer/Scoop.git&quot;



# 添加南京大学extras仓库
scoop bucket add extras https://mirrors.nju.edu.cn/git/scoop-extras.git

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>开发日记/springboot项目logback配置</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/springboot%E9%A1%B9%E7%9B%AElogback%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/springboot%E9%A1%B9%E7%9B%AElogback%E9%85%8D%E7%BD%AE/</guid><pubDate>Sun, 18 Jan 2026 19:24:15 GMT</pubDate><content:encoded>&lt;h1&gt;文件内容 可通用&lt;/h1&gt;
&lt;p&gt;可以直接复制到项目的 &lt;code&gt;src/main/resources/logback-spring.xml&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vuaftq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!--debug=&quot;false&quot; 表示关闭 Logback 框架自身的内部状态信息打印。--&amp;gt;
&amp;lt;configuration scan=&quot;true&quot; scanPeriod=&quot;10 seconds&quot; debug=&quot;false&quot;&amp;gt;
    &amp;lt;!--    引入 spring boot 默认日志颜色和基础配置--&amp;gt;
    &amp;lt;include resource=&quot;org/springframework/boot/logging/logback/defaults.xml&quot;/&amp;gt;
    &amp;lt;!--    定义变量 APP_NAME，从 Spring 环境变量中获取 spring.application.name 的值--&amp;gt;
    &amp;lt;springProperty scope=&quot;context&quot; name=&quot;APP_NAME&quot; source=&quot;spring.application.name&quot;/&amp;gt;

&amp;lt;!--    定义时间格式，yyyy-MM 表示年-月，yyyy-MM-dd 表示年-月-日--&amp;gt;
    &amp;lt;timestamp key=&quot;time-month&quot; datePattern=&quot;yyyy-MM&quot;/&amp;gt;
    &amp;lt;timestamp key=&quot;time-month-day&quot; datePattern=&quot;yyyy-MM-dd&quot;/&amp;gt;
    &amp;lt;!--    定义变量 LOG_FILE_PATH，默认值为 ./logs/${APP_NAME}，可以通过环境变量 LOG_PATH 覆盖 日志存储路径--&amp;gt;
    &amp;lt;property name=&quot;LOG_FILE_PATH&quot; value=&quot;${LOG_PATH:-./logs/${APP_NAME}}&quot;/&amp;gt;
    &amp;lt;!--    定义日志格式
    格式说明：
    %d{yyyy-MM-dd HH:mm:ss.SSS}：日志记录时间，格式为年-月-日 时:分:秒.毫秒
    [%thread]：日志记录线程名称
    %-5level：日志级别，左对齐，占用 5 个字符宽度
    %logger{50}：日志记录器名称，最多显示 50 个字符
    -[%X{traceId:-}]：从 MDC 中获取 traceId 变量值，如果不存在则显示为空
    %msg%n：日志消息内容，换行符
    --&amp;gt;
    &amp;lt;property name=&quot;FILE_LOG_PATTERN&quot;
              value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n&quot;/&amp;gt;

    &amp;lt;!--    appender 控制台输出--&amp;gt;
    &amp;lt;!--
    &amp;lt;appender&amp;gt;: 这是 Logback 配置的根标签之一，用于定义一个日志输出目的地。
    name=&quot;CONSOLE&quot;: 为这个 Appender 赋予一个唯一的名称，方便在 &amp;lt;root&amp;gt; 或 &amp;lt;logger&amp;gt; 标签中引用它。
    class=&quot;ch.qos.logback.core.ConsoleAppender&quot;: 指定使用的实现类。
    ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
    这是我们在本地开发和测试时最常用的 Appender。
    CONSOLE_LOG_PATTERN 是上面include的默认日志格式，这里直接引用即可
    --&amp;gt;
    &amp;lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;${CONSOLE_LOG_PATTERN}&amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!--    全量info日志--&amp;gt;
    &amp;lt;appender name=&quot;INFO_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;file&amp;gt;${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log&amp;lt;/file&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;maxFileSize&amp;gt;100MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;maxHistory&amp;gt;31&amp;lt;/maxHistory&amp;gt;
            &amp;lt;totalSizeCap&amp;gt;100GB&amp;lt;/totalSizeCap&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;${FILE_LOG_PATTERN}&amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
        &amp;lt;/encoder&amp;gt;
        &amp;lt;filter class=&quot;ch.qos.logback.classic.filter.ThresholdFilter&quot;&amp;gt;
            &amp;lt;!-- 只记录 INFO 级别以及以上的日志的日志 --&amp;gt;
            &amp;lt;level&amp;gt;INFO&amp;lt;/level&amp;gt;
        &amp;lt;/filter&amp;gt;
    &amp;lt;/appender&amp;gt;
    &amp;lt;appender name=&quot;ERROR_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;file&amp;gt;${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log&amp;lt;/file&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;maxFileSize&amp;gt;100MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;maxHistory&amp;gt;31&amp;lt;/maxHistory&amp;gt;
            &amp;lt;totalSizeCap&amp;gt;100GB&amp;lt;/totalSizeCap&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;${FILE_LOG_PATTERN}&amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
        &amp;lt;/encoder&amp;gt;
        &amp;lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&amp;gt;
            &amp;lt;!-- 只记录 ERROR 级别以及以上的日志的日志 --&amp;gt;
            &amp;lt;level&amp;gt;ERROR&amp;lt;/level&amp;gt;
            &amp;lt;onMatch&amp;gt;ACCEPT&amp;lt;/onMatch&amp;gt;
            &amp;lt;onMismatch&amp;gt;DENY&amp;lt;/onMismatch&amp;gt;
        &amp;lt;/filter&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!--    异步写入日志--&amp;gt;
    &amp;lt;appender name=&quot;ASYNC_INFO&quot;
              class=&quot;ch.qos.logback.classic.AsyncAppender&quot;&amp;gt;
        &amp;lt;discardingThreshold&amp;gt;0&amp;lt;/discardingThreshold&amp;gt;
        &amp;lt;queueSize&amp;gt;512&amp;lt;/queueSize&amp;gt;
        &amp;lt;appender-ref ref=&quot;INFO_FILE&quot;/&amp;gt;
    &amp;lt;/appender&amp;gt;
    &amp;lt;appender name=&quot;ASYNC_ERROR&quot; class=&quot;ch.qos.logback.classic.AsyncAppender&quot;&amp;gt;
        &amp;lt;discardingThreshold&amp;gt;0&amp;lt;/discardingThreshold&amp;gt;
        &amp;lt;queueSize&amp;gt;512&amp;lt;/queueSize&amp;gt;
        &amp;lt;appender-ref ref=&quot;ERROR_FILE&quot;/&amp;gt;
    &amp;lt;/appender&amp;gt;


    &amp;lt;!--    =========== 环境配置 打印到控制台 ===========--&amp;gt;
    &amp;lt;springProfile name=&quot;dev&quot;&amp;gt;
        &amp;lt;root level=&quot;INFO&quot;&amp;gt;
            &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
            &amp;lt;appender-ref ref=&quot;ASYNC_ERROR&quot;/&amp;gt;
            &amp;lt;appender-ref ref=&quot;ASYNC_INFO&quot;/&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;

    &amp;lt;springProfile name=&quot;prod&quot;&amp;gt;
        &amp;lt;root level=&quot;INFO&quot;&amp;gt;
            &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
            &amp;lt;appender-ref ref=&quot;ASYNC_ERROR&quot;/&amp;gt;
            &amp;lt;appender-ref ref=&quot;ASYNC_INFO&quot;/&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置详解&lt;/h2&gt;
&lt;h3&gt;1. 根节点配置 (&lt;code&gt;&amp;lt;configuration&amp;gt;&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration scan=&quot;true&quot; scanPeriod=&quot;10 seconds&quot; debug=&quot;false&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;scan=&quot;true&quot;&lt;/strong&gt;: 配置文件如果发生改变，将会被重新加载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scanPeriod=&quot;10 seconds&quot;&lt;/strong&gt;: 监测配置文件是否有修改的时间间隔，默认单位是毫秒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;debug=&quot;false&quot;&lt;/strong&gt;: 关闭 Logback 框架自身的内部状态信息打印，设置为 &lt;code&gt;true&lt;/code&gt; 时可以在控制台看到 Logback 的加载过程，有助于排查 Logback 配置错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 基础引用与变量定义&lt;/h3&gt;
&lt;h4&gt;引入 Spring Boot 默认配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;include resource=&quot;org/springframework/boot/logging/logback/defaults.xml&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码引入了 Spring Boot 预定义的日志配置，包含了控制台输出的彩色日志格式 &lt;code&gt;CONSOLE_LOG_PATTERN&lt;/code&gt; 等常用变量。&lt;/p&gt;
&lt;h4&gt;获取 Spring 配置属性&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;springProperty scope=&quot;context&quot; name=&quot;APP_NAME&quot; source=&quot;spring.application.name&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;springProperty&amp;gt;&lt;/code&gt;: 允许从 Spring 的 &lt;code&gt;Environment&lt;/code&gt; 中读取属性并暴露给 Logback。&lt;/li&gt;
&lt;li&gt;这里读取了 &lt;code&gt;spring.application.name&lt;/code&gt;（应用名称）赋值给变量 &lt;code&gt;APP_NAME&lt;/code&gt;，用于后续生成日志文件路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;定义时间戳变量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;timestamp key=&quot;time-month&quot; datePattern=&quot;yyyy-MM&quot;/&amp;gt;
&amp;lt;timestamp key=&quot;time-month-day&quot; datePattern=&quot;yyyy-MM-dd&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义了两个时间戳变量，用于构建按月或按天归档的目录结构。&lt;/p&gt;
&lt;h4&gt;定义日志路径&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property name=&quot;LOG_FILE_PATH&quot; value=&quot;${LOG_PATH:-./logs/${APP_NAME}}&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;${LOG_PATH:-./logs/${APP_NAME}}&lt;/code&gt;: 这是一个默认值语法。如果环境变量 &lt;code&gt;LOG_PATH&lt;/code&gt; 存在，则使用它；否则使用 &lt;code&gt;./logs/${APP_NAME}&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 日志格式 (&lt;code&gt;FILE_LOG_PATTERN&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property name=&quot;FILE_LOG_PATTERN&quot;
          value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%d&lt;/code&gt;: 日期时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[%thread]&lt;/code&gt;: 线程名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%-5level&lt;/code&gt;: 日志级别（左对齐，5字符宽）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%logger{50}&lt;/code&gt;: 类名（最大长度50，超长会智能缩写）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[%X{traceId:-}]&lt;/code&gt;: 这是一个 MDC (Mapped Diagnostic Context) 变量。用于分布式链路追踪，如果 MDC 中有 &lt;code&gt;traceId&lt;/code&gt; 则显示，否则显示 &lt;code&gt;-&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%msg&lt;/code&gt;: 日志具体内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%n&lt;/code&gt;: 换行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 输出源 (Appenders)&lt;/h3&gt;
&lt;h4&gt;控制台输出 (&lt;code&gt;CONSOLE&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;ConsoleAppender&lt;/code&gt; 将日志输出到标准输出，并直接复用了 Spring Boot 默认的 &lt;code&gt;CONSOLE_LOG_PATTERN&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;滚动文件输出 (&lt;code&gt;INFO_FILE&lt;/code&gt; / &lt;code&gt;ERROR_FILE&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;RollingFileAppender&lt;/code&gt; 实现日志文件的滚动策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;滚动策略 (&lt;code&gt;SizeAndTimeBasedRollingPolicy&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按时间滚动&lt;/strong&gt;: 每天生成一个新的日志文件 (&lt;code&gt;%d{yyyy-MM-dd}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按大小滚动&lt;/strong&gt;: 如果单天日志超过 &lt;code&gt;100MB&lt;/code&gt; (&lt;code&gt;%i&lt;/code&gt;)，会切分出新文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;历史保留&lt;/strong&gt;: &lt;code&gt;&amp;lt;maxHistory&amp;gt;31&amp;lt;/maxHistory&amp;gt;&lt;/code&gt; 保留最近 31 天的日志。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总大小限制&lt;/strong&gt;: &lt;code&gt;&amp;lt;totalSizeCap&amp;gt;100GB&amp;lt;/totalSizeCap&amp;gt;&lt;/code&gt; 限制所有日志文件总大小不超过 100GB。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;过滤器 (Filter)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INFO_FILE&lt;/code&gt; 使用 &lt;code&gt;ThresholdFilter&lt;/code&gt;: 记录 &lt;code&gt;INFO&lt;/code&gt; 及以上级别（INFO, WARN, ERROR）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ERROR_FILE&lt;/code&gt; 使用 &lt;code&gt;LevelFilter&lt;/code&gt;: &lt;strong&gt;只&lt;/strong&gt;记录 &lt;code&gt;ERROR&lt;/code&gt; 级别。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onMatch=ACCEPT&lt;/code&gt;: 匹配 ERROR 则记录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onMismatch=DENY&lt;/code&gt;: 不匹配则丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 异步处理 (&lt;code&gt;AsyncAppender&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;appender name=&quot;ASYNC_INFO&quot; class=&quot;ch.qos.logback.classic.AsyncAppender&quot;&amp;gt;
    &amp;lt;discardingThreshold&amp;gt;0&amp;lt;/discardingThreshold&amp;gt;
    &amp;lt;queueSize&amp;gt;512&amp;lt;/queueSize&amp;gt;
    &amp;lt;appender-ref ref=&quot;INFO_FILE&quot;/&amp;gt;
&amp;lt;/appender&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 将日志写入操作放入独立线程执行，避免高并发下 IO 操作阻塞业务线程，提高应用性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;queueSize&lt;/strong&gt;: 异步队列深度，默认为 256，这里调整为 512。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;discardingThreshold&lt;/strong&gt;: 默认为队列剩余 20% 容量时丢弃 TRACE/DEBUG/INFO 日志。设置为 &lt;code&gt;0&lt;/code&gt; 表示&lt;strong&gt;不丢弃任何日志&lt;/strong&gt;，即使队列满了也阻塞等待，保证日志不丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 环境隔离 (&lt;code&gt;&amp;lt;springProfile&amp;gt;&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;springProfile name=&quot;dev&quot;&amp;gt; ... &amp;lt;/springProfile&amp;gt;
&amp;lt;springProfile name=&quot;prod&quot;&amp;gt; ... &amp;lt;/springProfile&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logback 支持 Spring 的 Profile 功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;spring.profiles.active=dev&lt;/code&gt; 时，激活 dev 块内的配置。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;spring.profiles.active=prod&lt;/code&gt; 时，激活 prod 块内的配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当前配置中，&lt;code&gt;dev&lt;/code&gt; 和 &lt;code&gt;prod&lt;/code&gt; 都输出了 &lt;code&gt;CONSOLE&lt;/code&gt;、&lt;code&gt;ASYNC_ERROR&lt;/code&gt; 和 &lt;code&gt;ASYNC_INFO&lt;/code&gt;，在实际生产环境中，通常会移除 &lt;code&gt;CONSOLE&lt;/code&gt; Appender 以减少不必要的控制台输出性能损耗。&lt;/p&gt;
</content:encoded></item><item><title>开发日记/python包管理工具uv使用</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/python%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7uv%E4%BD%BF%E7%94%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/python%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7uv%E4%BD%BF%E7%94%A8/</guid><description>uv 是一个极速的 Python 包管理和项目管理工具，由 Rust 编写。本文介绍了 uv 的安装、基础用法以及如何使用它来替代 pip、poetry 和 pyenv。</description><pubDate>Sun, 18 Jan 2026 19:23:07 GMT</pubDate><content:encoded>&lt;h1&gt;uv 使用指南&lt;/h1&gt;
&lt;p&gt;在 Python 的开发生态中，包管理和环境管理一直是一个让人头疼的话题。从 &lt;code&gt;pip&lt;/code&gt; 到 &lt;code&gt;pipenv&lt;/code&gt;，再到 &lt;code&gt;poetry&lt;/code&gt; 和 &lt;code&gt;pdm&lt;/code&gt;，工具层出不穷。而最近，由 Astral（Ruff 的开发者）推出的 &lt;strong&gt;uv&lt;/strong&gt; 横空出世，凭借其&lt;strong&gt;极快的速度&lt;/strong&gt;和&lt;strong&gt;全能的特性&lt;/strong&gt;，迅速成为了 Python 开发者的新宠。&lt;/p&gt;
&lt;p&gt;本文将带你快速上手 uv，体验这个&quot;终结者&quot;级别的工具。&lt;/p&gt;
&lt;h2&gt;什么是 uv？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; 是一个用 Rust 编写的 Python 包安装器和解析器。它的设计初衷是替代 &lt;code&gt;pip&lt;/code&gt;、&lt;code&gt;pip-tools&lt;/code&gt; 和 &lt;code&gt;virtualenv&lt;/code&gt;，但随着版本的迭代，它现在已经具备了替代 &lt;code&gt;poetry&lt;/code&gt;（项目管理）、&lt;code&gt;pyenv&lt;/code&gt;（Python 版本管理）和 &lt;code&gt;pipx&lt;/code&gt;（工具管理）的能力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极速&lt;/strong&gt;：比 pip 快 10-100 倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全能&lt;/strong&gt;：一个工具搞定 Python 安装、虚拟环境、依赖管理、工具运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容&lt;/strong&gt;：兼容 &lt;code&gt;pyproject.toml&lt;/code&gt; 标准。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. 安装 uv&lt;/h2&gt;
&lt;p&gt;uv 提供了多种安装方式，推荐使用官方的独立安装脚本，这样升级和管理更方便。&lt;/p&gt;
&lt;h3&gt;macOS / Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -LsSf https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows (PowerShell)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;powershell -ExecutionPolicy ByPass -c &quot;irm https://astral.sh/uv/install.ps1 | iex&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 pip 安装&lt;/h3&gt;
&lt;p&gt;如果你只是想尝鲜，也可以通过 pip 安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install uv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，可以通过以下命令验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 项目管理 (Modern Workflow)&lt;/h2&gt;
&lt;p&gt;这是 uv 目前最推荐的使用方式，类似于 &lt;code&gt;poetry&lt;/code&gt; 或 &lt;code&gt;npm&lt;/code&gt; 的体验。&lt;/p&gt;
&lt;h3&gt;初始化项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建一个新项目
uv init my-project
cd my-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vvzidl-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vwl6mu-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这将创建一个 &lt;code&gt;pyproject.toml&lt;/code&gt; 文件和一个 &lt;code&gt;.python-version&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;p&gt;不再需要手动激活虚拟环境，&lt;code&gt;uv add&lt;/code&gt; 会自动处理虚拟环境的创建和依赖的安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 添加依赖
uv add requests

# 添加开发依赖 (例如 pytest)
uv add --dev pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vwrk27-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vx4n39-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vxz3kj-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;配置国内镜像源&lt;/h3&gt;
&lt;h4&gt;配置项目镜像&lt;/h4&gt;
&lt;p&gt;可以参考这个 &lt;code&gt;https://uv.oaix.tech/blog/2025/06/17/quickly-set-uv-package-index-is-china-mirror/&lt;/code&gt; 来配置国内镜像源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vygekp-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vyls9m-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt; pyproject.toml&lt;/code&gt; 里面添加下面的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[tool.uv.index]]
name = &quot;tencent&quot;
url = &quot;https://mirrors.cloud.tencent.com/pypi/simple/&quot; # 腾讯云镜像源
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/vz77xi-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w7ixxh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w7uppx-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到里面已经用上镜像了&lt;/p&gt;
&lt;h4&gt;配置全局镜像源&lt;/h4&gt;
&lt;p&gt;参考这个 &lt;code&gt;https://www.cnblogs.com/ljbguanli/p/19357762&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;全局配置后，所有项目默认使用指定镜像，无需重复设置。&lt;/p&gt;
&lt;p&gt;步骤 1：找到配置文件路径&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux/macOS：~/.config/uv/config.toml&lt;/li&gt;
&lt;li&gt;Windows：%USERPROFILE%.config\uv\config.toml（如 C:\Users\你的用户名.config\uv\config.toml）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;步骤 2：创建/编辑配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux/macOS：用 vim 打开（若文件不存在会自动创建）
vim ~/.config/uv/config.toml
# Windows：用记事本打开
Write-Host $env:USERPROFILE


notepad %USERPROFILE%\.config\uv\config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 阿里云镜像（推荐，稳定性好）
[registries.pypi]
index = &quot;https://mirrors.aliyun.com/pypi/simple/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w2n47v-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w3acq4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w3ngws-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w3qyvu-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w47xz8-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过网上推荐的都是直接设置环境变量 &lt;code&gt;UV_DEFAULT_INDEX&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For Linux Users:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 推荐使用清华源
echo &apos;export UV_DEFAULT_INDEX=&quot;https://pypi.tuna.tsinghua.edu.cn/simple&quot;&apos;&amp;gt;&amp;gt; ~/.bashrc

# 或者用阿里源
# echo &apos;export UV_DEFAULT_INDEX=&quot;https://mirrors.aliyun.com/pypi/simple/&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc

# 让配置立即生效
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Windows Users:&lt;/p&gt;
&lt;p&gt;这个只在当前会话生效，关闭会话后就会失效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$env:UV_DEFAULT_INDEX = &quot;https://pypi.tuna.tsinghua.edu.cn/simple&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想永久生效就
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w63ik2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;环境变量不知道去哪儿找可以直接 windows 搜索 &lt;code&gt;环境变量&lt;/code&gt; 就可以找到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w6bb65-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w91iwr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如果是已经有的项目，如何用 uv 同步包呢？&lt;/h3&gt;
&lt;p&gt;如果是已经有的项目，你可以使用 &lt;code&gt;uv sync&lt;/code&gt; 命令来同步项目的依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将根据 &lt;code&gt;pyproject.toml&lt;/code&gt; 中的配置，安装所有必要的依赖。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w06627-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/w0aosh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;运行代码&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;uv run&lt;/code&gt; 可以在项目的虚拟环境中执行命令，无需显式激活环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 运行脚本
uv run main.py

# 运行工具
uv run pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/wbir6y-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/wbml7j-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;同步环境&lt;/h3&gt;
&lt;p&gt;如果你拉取了别人的代码，或者手动修改了 &lt;code&gt;pyproject.toml&lt;/code&gt;，可以使用 &lt;code&gt;sync&lt;/code&gt; 命令同步环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv sync
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Python 版本管理&lt;/h2&gt;
&lt;p&gt;uv 内置了 Python 版本管理功能，这意味着你不再需要安装 &lt;code&gt;pyenv&lt;/code&gt; 或 &lt;code&gt;conda&lt;/code&gt; 来管理不同的 Python 版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 列出可用的 Python 版本
uv python list

# 安装特定版本的 Python
uv python install 3.12

# 为当前项目指定 Python 版本
uv python pin 3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你运行 &lt;code&gt;uv run&lt;/code&gt; 或 &lt;code&gt;uv sync&lt;/code&gt; 时，uv 会自动下载并使用项目指定的 Python 版本。&lt;/p&gt;
&lt;p&gt;这个也可以用镜像，不然走github国内很慢
很简单&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/x4rjp3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UV_PYTHON_INSTALL_MIRROR

https://mirror.nju.edu.cn/github-release/astral-sh/python-build-standalone/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;linux的话&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export UV_PYTHON_INSTALL_MIRROR=&quot;https://mirror.nju.edu.cn/github-release/astral-sh/python-build-standalone/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/18/x627ey-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4. 脚本支持 (Script Support)&lt;/h2&gt;
&lt;p&gt;uv 对单文件脚本的支持非常出色。你可以在脚本顶部声明依赖，uv 会自动下载并运行，且不会污染全局环境。&lt;/p&gt;
&lt;p&gt;创建一个 &lt;code&gt;example.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /// script
# requires-python = &quot;&amp;gt;=3.11&quot;
# dependencies = [
#     &quot;requests&amp;lt;3&quot;,
#     &quot;rich&quot;,
# ]
# ///

import requests
from rich.pretty import pprint

resp = requests.get(&quot;https://peps.python.org/api/peps.json&quot;)
data = resp.json()
pprint([(k, v[&quot;title&quot;]) for k, v in data.items()][:10])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run example.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;uv 会自动解析头部元数据，创建一个临时环境并安装依赖，然后执行脚本。&lt;/p&gt;
&lt;h2&gt;5. 工具管理 (Tool Management)&lt;/h2&gt;
&lt;p&gt;类似于 &lt;code&gt;pipx&lt;/code&gt;，uv 可以安装和运行全局的 Python 命令行工具。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 临时运行一个工具 (例如 ruff)
uvx ruff check .
# 或者
uv tool run ruff check .

# 安装一个工具到全局
uv tool install black
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 兼容 pip 的接口 (Legacy Interface)&lt;/h2&gt;
&lt;p&gt;如果你不想改变现有的工作流，只想利用 uv 的速度，可以使用它的 pip 兼容接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建虚拟环境
uv venv

# 创建指定版本的虚拟环境
uv venv --python 3.12

# 激活环境 (Windows)
.venv\Scripts\activate
# 激活环境 (macOS/Linux)
source .venv/bin/activate

# 安装依赖 (替代 pip install)
uv pip install requests

# 从 requirements.txt 安装
uv pip install -r requirements.txt

# 生成锁定文件 (替代 pip-compile)
uv pip compile pyproject.toml -o requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;uv 正在以惊人的速度重塑 Python 的开发体验。它不仅解决了&quot;慢&quot;的问题，更重要的是它试图统一碎片化的 Python 工具链。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;迁移建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新项目&lt;/strong&gt;：直接使用 &lt;code&gt;uv init&lt;/code&gt; 和 &lt;code&gt;uv add&lt;/code&gt; 的工作流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;老项目&lt;/strong&gt;：可以先用 &lt;code&gt;uv pip&lt;/code&gt; 替代 &lt;code&gt;pip&lt;/code&gt; 加速安装，时机成熟后迁移到 &lt;code&gt;pyproject.toml&lt;/code&gt; 管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脚本&lt;/strong&gt;：强烈推荐使用 &lt;code&gt;uv run&lt;/code&gt; 运行带依赖声明的单文件脚本。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>开发日记/SpringCloud项目配合maven动态启用不同配置文件设计</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/springcloud%E9%A1%B9%E7%9B%AE%E9%85%8D%E5%90%88maven%E5%8A%A8%E6%80%81%E5%90%AF%E7%94%A8%E4%B8%8D%E5%90%8C%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AE%BE%E8%AE%A1/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/springcloud%E9%A1%B9%E7%9B%AE%E9%85%8D%E5%90%88maven%E5%8A%A8%E6%80%81%E5%90%AF%E7%94%A8%E4%B8%8D%E5%90%8C%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AE%BE%E8%AE%A1/</guid><pubDate>Sun, 11 Jan 2026 12:53:19 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在多模块（父工程 + 多个子模块）的 SpringCloud / SpringBoot 项目里，我们通常会有多套环境配置（dev/test/prod），比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库、Redis、MQ 地址不同&lt;/li&gt;
&lt;li&gt;Nacos / Config Server 的命名空间、group 不同&lt;/li&gt;
&lt;li&gt;日志级别、监控开关不同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题在于：&lt;strong&gt;子模块是可独立启动的&lt;/strong&gt;，但它们的配置又希望能跟随父模块选择的 Maven Profile 自动切换，而不是每次都手动改 &lt;code&gt;application.yml&lt;/code&gt; 或 IDE 的 VM Options。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Maven Profile 统一管理环境（dev/test/prod），&lt;strong&gt;一处配置，多模块复用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;打包时根据 &lt;code&gt;-Pdev/-Pprod&lt;/code&gt; 自动写入激活的 Spring Profile&lt;/li&gt;
&lt;li&gt;子模块的 &lt;code&gt;application.yml&lt;/code&gt; 不硬编码环境，而是“动态注入”&lt;/li&gt;
&lt;li&gt;本地运行、CI 打包、Docker 部署都能保持同一套切换逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;思路总览（核心点）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;父模块（parent/pom.xml）用 &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt; 维护环境变量&lt;/strong&gt;，比如 &lt;code&gt;profile.active=dev&lt;/code&gt;、&lt;code&gt;config.server.url=...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子模块开启资源过滤（resource filtering）&lt;/strong&gt;，让 &lt;code&gt;application.yml&lt;/code&gt; 支持占位符替换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application.yml&lt;/code&gt; 用占位符写 &lt;code&gt;spring.profiles.active&lt;/code&gt;，让它随 Maven Profile 注入（例如 &lt;code&gt;@profile.active@&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;运行 / 打包时只需要切换 Maven Profile：&lt;code&gt;mvn -Pdev package&lt;/code&gt;、&lt;code&gt;mvn -Pprod package&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面重点讲两件事：&lt;strong&gt;父模块 profiles 怎么写&lt;/strong&gt;，以及 &lt;strong&gt;子模块 application.yml 怎么实现动态配置&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;父模块：Maven Profiles 统一管理环境&lt;/h2&gt;
&lt;p&gt;在父模块 &lt;code&gt;pom.xml&lt;/code&gt; 中定义环境 Profile（dev/test/prod），每个 profile 只负责两类事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供“环境变量”（properties）&lt;/li&gt;
&lt;li&gt;参与资源过滤（让子模块能读到这些变量）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例（父模块 &lt;code&gt;pom.xml&lt;/code&gt;，只保留关键段落）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project&amp;gt;
  &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
  &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;

  &amp;lt;properties&amp;gt;
    &amp;lt;maven.resources.filtered&amp;gt;true&amp;lt;/maven.resources.filtered&amp;gt;
    &amp;lt;profile.active&amp;gt;dev&amp;lt;/profile.active&amp;gt;
  &amp;lt;/properties&amp;gt;

  &amp;lt;profiles&amp;gt;
    &amp;lt;profile&amp;gt;
      &amp;lt;id&amp;gt;dev&amp;lt;/id&amp;gt;
      &amp;lt;properties&amp;gt;
        &amp;lt;profile.active&amp;gt;dev&amp;lt;/profile.active&amp;gt;
        &amp;lt;config.server.url&amp;gt;http://127.0.0.1:8888&amp;lt;/config.server.url&amp;gt;
      &amp;lt;/properties&amp;gt;
    &amp;lt;/profile&amp;gt;

    &amp;lt;profile&amp;gt;
      &amp;lt;id&amp;gt;test&amp;lt;/id&amp;gt;
      &amp;lt;properties&amp;gt;
        &amp;lt;profile.active&amp;gt;test&amp;lt;/profile.active&amp;gt;
        &amp;lt;config.server.url&amp;gt;http://test-config:8888&amp;lt;/config.server.url&amp;gt;
      &amp;lt;/properties&amp;gt;
    &amp;lt;/profile&amp;gt;

    &amp;lt;profile&amp;gt;
      &amp;lt;id&amp;gt;prod&amp;lt;/id&amp;gt;
      &amp;lt;properties&amp;gt;
        &amp;lt;profile.active&amp;gt;prod&amp;lt;/profile.active&amp;gt;
        &amp;lt;config.server.url&amp;gt;http://prod-config:8888&amp;lt;/config.server.url&amp;gt;
      &amp;lt;/properties&amp;gt;
    &amp;lt;/profile&amp;gt;
  &amp;lt;/profiles&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么把 Profile 写在父模块&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;多模块下每个子模块都依赖 parent，&lt;strong&gt;配置天然继承&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;切换环境只需控制父模块 profile，CI/CD 更好统一&lt;/li&gt;
&lt;li&gt;对团队协作友好：大家不需要各自维护一份“本地 dev 配置”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用方式（命令）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# dev 打包
mvn -Pdev clean package

# prod 打包
mvn -Pprod clean package
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 IDE（例如 IntelliJ IDEA）里，也可以在 Maven 面板里勾选对应 profile，然后运行子模块的启动类即可（前提是子模块启用了资源过滤，后面会讲）。&lt;/p&gt;
&lt;h2&gt;子模块：application.yml 如何实现动态配置&lt;/h2&gt;
&lt;p&gt;核心就是一句话：&lt;strong&gt;让 &lt;code&gt;application.yml&lt;/code&gt; 里的值由 Maven 过滤替换&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1）子模块开启资源过滤（resource filtering）&lt;/h3&gt;
&lt;p&gt;在子模块（或统一放在父模块的 &lt;code&gt;&amp;lt;build&amp;gt;&amp;lt;pluginManagement&amp;gt;&lt;/code&gt; 里让子模块继承）配置 resources 过滤。&lt;/p&gt;
&lt;p&gt;以子模块 &lt;code&gt;pom.xml&lt;/code&gt; 为例（关键段落）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;build&amp;gt;
  &amp;lt;resources&amp;gt;
    &amp;lt;resource&amp;gt;
      &amp;lt;directory&amp;gt;src/main/resources&amp;lt;/directory&amp;gt;
      &amp;lt;filtering&amp;gt;true&amp;lt;/filtering&amp;gt;
    &amp;lt;/resource&amp;gt;
  &amp;lt;/resources&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 Maven 在 &lt;code&gt;process-resources&lt;/code&gt; 阶段会对 &lt;code&gt;src/main/resources&lt;/code&gt; 下的文件做占位符替换。&lt;/p&gt;
&lt;h3&gt;2）在 application.yml 里引用父模块 Profile 变量&lt;/h3&gt;
&lt;p&gt;在子模块 &lt;code&gt;src/main/resources/application.yml&lt;/code&gt; 里写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  profiles:
    active: &quot;@profile.active@&quot;

app:
  config-server-url: &quot;@config.server.url@&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用的是 Maven 的 &lt;code&gt;@...@&lt;/code&gt; 占位符格式（它比 &lt;code&gt;${...}&lt;/code&gt; 在 YAML 中更不容易和 Spring 本身的占位符混淆）。&lt;/p&gt;
&lt;p&gt;当你执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn -Pprod clean package
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包后的 &lt;code&gt;target/classes/application.yml&lt;/code&gt; 会变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  profiles:
    active: &quot;prod&quot;

app:
  config-server-url: &quot;http://prod-config:8888&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3）配合多套配置文件：application-{profile}.yml&lt;/h3&gt;
&lt;p&gt;建议把“环境差异”尽量放到 &lt;code&gt;application-dev.yml&lt;/code&gt; / &lt;code&gt;application-prod.yml&lt;/code&gt; 中，让 &lt;code&gt;application.yml&lt;/code&gt; 只负责“选择环境”与公共配置。&lt;/p&gt;
&lt;p&gt;结构示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/main/resources/
  application.yml
  application-dev.yml
  application-test.yml
  application-prod.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;application.yml&lt;/code&gt;（只写公共 + 激活环境）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  profiles:
    active: &quot;@profile.active@&quot;

server:
  port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;application-dev.yml&lt;/code&gt;（写 dev 差异）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/app_dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;application-prod.yml&lt;/code&gt;（写 prod 差异）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/app_prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一来：&lt;strong&gt;父模块 Maven Profile 决定 &lt;code&gt;spring.profiles.active&lt;/code&gt;&lt;/strong&gt;，SpringBoot 再根据 active profile 自动加载对应的 &lt;code&gt;application-{profile}.yml&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;常见坑与建议&lt;/h2&gt;
&lt;h3&gt;1）本地直接运行为什么不生效&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@profile.active@&lt;/code&gt; 只会在 &lt;strong&gt;Maven 资源处理&lt;/strong&gt; 时被替换。&lt;/p&gt;
&lt;p&gt;如果你直接用 IDE “运行启动类”，但没有触发 Maven 的 &lt;code&gt;process-resources&lt;/code&gt;，就会出现占位符未替换的情况。&lt;/p&gt;
&lt;p&gt;推荐做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 IDE 使用 Maven Profile，并确保运行前执行了 &lt;code&gt;process-resources&lt;/code&gt;（常见方式是先 &lt;code&gt;mvn -Pdev -DskipTests package&lt;/code&gt; 一次）&lt;/li&gt;
&lt;li&gt;或者在 Run Configuration 里临时加 &lt;code&gt;-Dspring.profiles.active=dev&lt;/code&gt;（但这会绕过“父模块统一管理”，不建议作为团队默认方案）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2）过滤导致 application.yml 被破坏&lt;/h3&gt;
&lt;p&gt;如果你在 &lt;code&gt;application.yml&lt;/code&gt; 里本身也使用 &lt;code&gt;${...}&lt;/code&gt;（比如 Spring 的占位符），Maven 过滤可能会误处理。&lt;/p&gt;
&lt;p&gt;建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;@...@&lt;/code&gt; 作为 Maven 注入占位符&lt;/li&gt;
&lt;li&gt;Spring 自己的占位符继续用 &lt;code&gt;${...}&lt;/code&gt;，避免混用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3）CI/CD 建议&lt;/h3&gt;
&lt;p&gt;CI 里只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn -Pprod clean package -DskipTests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;产物里 profile 已经写死为 prod，运行时不需要额外设置。&lt;/p&gt;
&lt;p&gt;如果你希望“同一包多环境运行”，那就不要在构建期写死 &lt;code&gt;spring.profiles.active&lt;/code&gt;，改为运行期用环境变量/启动参数控制（这是另一条路线，和本文目标不同）。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;父模块 &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt; 用来统一维护环境变量（重点：&lt;code&gt;profile.active&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;子模块开启资源过滤，让 &lt;code&gt;application.yml&lt;/code&gt; 能读取父模块的变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application.yml&lt;/code&gt; 用 &lt;code&gt;spring.profiles.active: &quot;@profile.active@&quot;&lt;/code&gt; 达到“动态切换配置”的效果&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>开发日记/记一次springcloud项目启动显示端口被占用但是查不到占用进程的问题</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/%E8%AE%B0%E4%B8%80%E6%AC%A1springcloud%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8%E6%98%BE%E7%A4%BA%E7%AB%AF%E5%8F%A3%E8%A2%AB%E5%8D%A0%E7%94%A8%E4%BD%86%E6%98%AF%E6%9F%A5%E4%B8%8D%E5%88%B0%E5%8D%A0%E7%94%A8%E8%BF%9B%E7%A8%8B%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/%E8%AE%B0%E4%B8%80%E6%AC%A1springcloud%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8%E6%98%BE%E7%A4%BA%E7%AB%AF%E5%8F%A3%E8%A2%AB%E5%8D%A0%E7%94%A8%E4%BD%86%E6%98%AF%E6%9F%A5%E4%B8%8D%E5%88%B0%E5%8D%A0%E7%94%A8%E8%BF%9B%E7%A8%8B%E7%9A%84%E9%97%AE%E9%A2%98/</guid><pubDate>Sun, 11 Jan 2026 12:27:39 GMT</pubDate><content:encoded>&lt;h1&gt;开发日记/记一次springcloud项目启动显示端口被占用但是查不到占用进程的问题&lt;/h1&gt;
&lt;p&gt;换了台电脑，自己写的项目都跑不起来了。。。。网上说要拿netstat -ano | findstr 端口号 来查看占用进程，但是我查不到占用进程。。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/11/kb4rrh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/11/kbeze4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;解决办法&lt;/h1&gt;
&lt;p&gt;查了下，windows是有预留端口的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netsh interface ipv4 show excludedportrange protocol=tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/11/kc0rzr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;奥原来是在预留端口范围里面。。。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/11/kck57k-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;只能把项目端口号改到预留端口范围之外了，比如我这里改成8081就可以了&lt;/p&gt;
</content:encoded></item><item><title>开发日记/GitFlow学习</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/gitflow%E5%AD%A6%E4%B9%A0/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/gitflow%E5%AD%A6%E4%B9%A0/</guid><pubDate>Sat, 10 Jan 2026 00:21:42 GMT</pubDate><content:encoded>&lt;h1&gt;GitFlow 学习：一套分支协作“规矩”的来龙去脉&lt;/h1&gt;
&lt;p&gt;这两天为了把团队协作流程梳理清楚，我系统看了一遍 GitFlow。它不是某个命令，也不是 Git 的内置功能，而是一套“怎么分支、怎么合并、怎么发版、怎么修线上”的协作约定。&lt;/p&gt;
&lt;p&gt;它的优点是清晰、可控、可复用；缺点是流程偏重、分支偏多。适不适合，取决于团队规模、发版节奏和项目类型。&lt;/p&gt;
&lt;h2&gt;GitFlow 解决的核心问题&lt;/h2&gt;
&lt;p&gt;在多人协作里，常见的冲突不是代码冲突，而是“节奏冲突”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你在开发新功能，另一个人要紧急修线上 bug&lt;/li&gt;
&lt;li&gt;产品要在本周发一个稳定版本，但下周的大需求已经在开发中&lt;/li&gt;
&lt;li&gt;版本需要打 Tag、回滚、追溯某次发布包含哪些改动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitFlow 做的事情就是把这些“节奏”通过分支模型固定下来，让每个人都知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪个分支代表线上稳定版本&lt;/li&gt;
&lt;li&gt;哪个分支代表日常集成&lt;/li&gt;
&lt;li&gt;新功能、发版、紧急修复分别从哪里来、往哪里去&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分支模型：两条主干 + 三类临时分支&lt;/h2&gt;
&lt;h3&gt;主分支&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;（或 &lt;code&gt;master&lt;/code&gt;）：线上稳定分支。每一次正式发布通常都来自这里，并通过 Tag 标记版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;develop&lt;/code&gt;：日常集成分支。新功能开发完成后会合回 &lt;code&gt;develop&lt;/code&gt;，用于持续集成与联调。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;临时分支&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feature/*&lt;/code&gt;：功能分支，从 &lt;code&gt;develop&lt;/code&gt; 拉出，完成后合回 &lt;code&gt;develop&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release/*&lt;/code&gt;：发布分支，从 &lt;code&gt;develop&lt;/code&gt; 拉出，用于发版前的收尾（修 bug、改版本号、补文档等），最终合到 &lt;code&gt;main&lt;/code&gt; 并回灌 &lt;code&gt;develop&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotfix/*&lt;/code&gt;：紧急修复分支，从 &lt;code&gt;main&lt;/code&gt; 拉出，修复线上问题，最终合到 &lt;code&gt;main&lt;/code&gt; 并回灌 &lt;code&gt;develop&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;标准流程：从开发到发布，再到紧急修复&lt;/h2&gt;
&lt;h3&gt;1) 开发新功能：feature 分支&lt;/h3&gt;
&lt;p&gt;目标：不污染 &lt;code&gt;develop&lt;/code&gt;，开发完成后再合并。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch develop
git pull
git switch -c feature/user-profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;功能完成后发起 PR 合并到 &lt;code&gt;develop&lt;/code&gt;。合并完成后可以删除 feature 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch develop
git pull
git branch -d feature/user-profile
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) 准备发版：release 分支&lt;/h3&gt;
&lt;p&gt;目标：冻结需求，专注发版质量；同时不阻塞 &lt;code&gt;develop&lt;/code&gt; 的后续开发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch develop
git pull
git switch -c release/1.3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;release/1.3.0&lt;/code&gt; 上通常会做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修复发版相关 bug&lt;/li&gt;
&lt;li&gt;调整版本号&lt;/li&gt;
&lt;li&gt;补发版说明（changelog）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确认发布后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;合并到 &lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main&lt;/code&gt; 上打 tag&lt;/li&gt;
&lt;li&gt;把 release 的改动回灌到 &lt;code&gt;develop&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git switch main
git pull
git merge --no-ff release/1.3.0
git tag -a v1.3.0 -m &quot;release v1.3.0&quot;
git push --follow-tags

git switch develop
git pull
git merge --no-ff release/1.3.0
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布完成后删除发布分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -d release/1.3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3) 线上紧急修复：hotfix 分支&lt;/h3&gt;
&lt;p&gt;目标：以最短路径修复线上，不等待 &lt;code&gt;develop&lt;/code&gt; 的合并节奏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch main
git pull
git switch -c hotfix/1.3.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修复完成后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;合并回 &lt;code&gt;main&lt;/code&gt; 并打 tag&lt;/li&gt;
&lt;li&gt;回灌 &lt;code&gt;develop&lt;/code&gt;（否则下次发布可能把 bug 又带回来）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git switch main
git pull
git merge --no-ff hotfix/1.3.1
git tag -a v1.3.1 -m &quot;hotfix v1.3.1&quot;
git push --follow-tags

git switch develop
git pull
git merge --no-ff hotfix/1.3.1
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;团队落地建议：不然 GitFlow 会变成“形式主义”&lt;/h2&gt;
&lt;h3&gt;分支命名约定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feature/&amp;lt;ticket&amp;gt;-&amp;lt;desc&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release/&amp;lt;version&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotfix/&amp;lt;version&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;明确 ticket（Jira/禅道/飞书任务）能减少“这分支是干嘛的”的沟通成本。&lt;/p&gt;
&lt;h3&gt;合并策略建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;日常 feature 合并建议走 PR + review&lt;/li&gt;
&lt;li&gt;合并时倾向使用 &lt;code&gt;--no-ff&lt;/code&gt; 保留分支合并节点，方便追溯一个 feature 的整体生命周期&lt;/li&gt;
&lt;li&gt;需要线性历史（rebase）也可以，但要团队一致，且注意不要 rebase 已推送且多人协作的分支&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tag 与版本号&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;发布分支/热修分支最终都应该落到 &lt;code&gt;main&lt;/code&gt;，并在 &lt;code&gt;main&lt;/code&gt; 打 tag&lt;/li&gt;
&lt;li&gt;tag 建议使用 &lt;code&gt;vX.Y.Z&lt;/code&gt;（语义化版本），长期会非常省事：定位问题、回滚、对比版本都更直观&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;这种流程适合谁？不适合谁？&lt;/h2&gt;
&lt;h3&gt;适合&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;有明确“发版”概念的软件：需要版本号、发布窗口、变更可追溯&lt;/li&gt;
&lt;li&gt;需要同时并行多个功能开发，还要保证某个版本稳定交付&lt;/li&gt;
&lt;li&gt;线上问题需要快速热修，且修复必须被未来版本继承&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不太适合&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;小团队/个人项目：分支成本大于收益&lt;/li&gt;
&lt;li&gt;强 Trunk-Based（主干开发）文化的团队：更倾向短分支 + 快速合并到主干 + 高频发布&lt;/li&gt;
&lt;li&gt;非常高频发版（比如一天多次发布）：GitFlow 的 release 分支会显得偏重&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见踩坑点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;develop&lt;/code&gt; 长期不稳定：如果大家把“坏掉也没关系”的心态带进 &lt;code&gt;develop&lt;/code&gt;，那 release 就会变成灾难收尾现场&lt;/li&gt;
&lt;li&gt;release 分支拖太久：发版窗口越长，回灌冲突越大；release 建议短周期&lt;/li&gt;
&lt;li&gt;hotfix 没回灌 develop：短期看修好了，长期会在下次发布被“复活”&lt;/li&gt;
&lt;li&gt;过度流程化：所有改动都走 release/hotfix，导致流程负担极高，最后大家反而绕流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;和 GitHub Flow / Trunk-Based 的简单对比&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;GitFlow：强调“版本管理”和“并行节奏”，清晰但偏重&lt;/li&gt;
&lt;li&gt;GitHub Flow：通常只有 &lt;code&gt;main&lt;/code&gt; + feature 分支，合并即部署，适合持续交付&lt;/li&gt;
&lt;li&gt;Trunk-Based：极短生命周期分支（甚至直接主干），依赖 CI/CD 与 feature toggle，效率高但要求工程化更强&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;GitFlow 的本质不是“分支越多越专业”，而是把团队协作里最难的三个问题拆开解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日常集成（&lt;code&gt;develop&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;稳定发布（&lt;code&gt;release/*&lt;/code&gt; -&amp;gt; &lt;code&gt;main&lt;/code&gt; + tag）&lt;/li&gt;
&lt;li&gt;线上热修（&lt;code&gt;hotfix/*&lt;/code&gt; -&amp;gt; &lt;code&gt;main&lt;/code&gt; + 回灌 &lt;code&gt;develop&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果团队发版节奏明确、需要追溯和稳定性，GitFlow 很好用；如果项目更追求快速交付与高频发布，GitHub Flow 或 Trunk-Based 可能更适合。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://blog.csdn.net/sunyctf/article/details/130587970&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;/h1&gt;
</content:encoded></item><item><title>开发日记/记一次mysql数据库恢复引发的慢查询问题</title><link>https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/%E8%AE%B0%E4%B8%80%E6%AC%A1mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E6%81%A2%E5%A4%8D%E5%BC%95%E5%8F%91%E7%9A%84%E6%85%A2%E6%9F%A5%E8%AF%A2%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%BC%80%E5%8F%91%E6%97%A5%E8%AE%B0/%E8%AE%B0%E4%B8%80%E6%AC%A1mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E6%81%A2%E5%A4%8D%E5%BC%95%E5%8F%91%E7%9A%84%E6%85%A2%E6%9F%A5%E8%AF%A2%E9%97%AE%E9%A2%98/</guid><pubDate>Sat, 10 Jan 2026 00:11:06 GMT</pubDate><content:encoded>&lt;h1&gt;记一次 MySQL 数据库恢复引发的慢查询问题&lt;/h1&gt;
&lt;h2&gt;案发经过&lt;/h2&gt;
&lt;p&gt;今天在公司遇到个诡异的问题。另一位实习生同事对测试环境的 MySQL 数据库进行了恢复操作。原本以为只是常规操作，结果恢复完成后，昨天还能在 2 秒内跑完的查询，今天跑了 10 分钟都没出结果。&lt;/p&gt;
&lt;p&gt;此时测试服务器的情况非常糟糕：CPU 占用率直接飙升到 100%，内存占用也居高不下。&lt;/p&gt;
&lt;h2&gt;排查过程&lt;/h2&gt;
&lt;h3&gt;1. 初步检查&lt;/h3&gt;
&lt;p&gt;找技术经理用 Root 账号登录数据库查看状态，发现那几个查询语句一直在执行中，且耗时都已经超过了 10 分钟。
尝试 Kill 掉这些查询进程后，服务器的 CPU 和内存瞬间恢复正常。但只要业务端再次触发那个 SQL 查询，服务器立马又“瘫痪”了。&lt;/p&gt;
&lt;h3&gt;2. 尝试重启&lt;/h3&gt;
&lt;p&gt;起初怀疑是 SQL 导致了死锁，或者有什么隐藏进程阻塞了数据库。于是简单粗暴地重启了几次数据库服务，但问题依旧，没有任何改善。&lt;/p&gt;
&lt;h3&gt;3. 分析执行计划&lt;/h3&gt;
&lt;p&gt;既然重启无效，只能深入分析了。
查看慢查询日志，确认就是那几个业务查询在拖后腿。
接着查看这些 SQL 的执行计划（&lt;code&gt;EXPLAIN&lt;/code&gt;），惊讶地发现：&lt;strong&gt;很多查询竟然都不走索引了！&lt;/strong&gt; 全表扫描导致了查询时间无限拉长。&lt;/p&gt;
&lt;p&gt;这就很离谱了。这些 SQL 在昨天（恢复数据前）还能正常运行，且正式环境也是完全相同的 SQL 和索引结构，一直运行良好。这说明问题不在 SQL 写法或索引缺失上。&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;技术经理建议尝试优化 SQL 或添加索引，但我认为既然正式环境没问题，且昨天也没问题，说明结构本身是合理的。
结合“刚做过数据库恢复”这个操作，我推测可能是&lt;strong&gt;数据库的统计信息（Statistics）在恢复过程中丢失或失效了&lt;/strong&gt;，导致 MySQL 优化器误判，选择了全表扫描而不是走索引。&lt;/p&gt;
&lt;p&gt;于是，我尝试对相关表执行了分析命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANALYZE TABLE 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结果立竿见影！&lt;/strong&gt;
执行完分析后，再次查看执行计划，查询终于重新走了索引。业务查询速度也恢复到了秒级，服务器负载瞬间降了下来。&lt;/p&gt;
&lt;h2&gt;💡 深度解析：关于 ANALYZE TABLE&lt;/h2&gt;
&lt;h3&gt;1. 它到底干了什么？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ANALYZE TABLE&lt;/code&gt; 的主要作用是&lt;strong&gt;重新统计索引的基数（Cardinality）&lt;/strong&gt;。
MySQL 的优化器（Optimizer）在决定是否使用索引时，依据的是成本模型（Cost Model）。而成本计算的核心输入就是&lt;strong&gt;索引区分度&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 会随机采样数据页（InnoDB 默认采样 8 个页，可配置）。&lt;/li&gt;
&lt;li&gt;估算每个索引中有多少个不同的值（基数）。&lt;/li&gt;
&lt;li&gt;如果采样结果显示某个索引的基数太小（重复值太多），优化器就会认为“反正大部分数据都要读，不如直接全表扫描快”，从而放弃索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在数据恢复或大量导入后，数据分布发生了剧烈变化，但统计信息可能还是旧的（或者为空），导致优化器拿着错误的情报做出了错误的决策。&lt;/p&gt;
&lt;h3&gt;2. 各个 MySQL 版本的表现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MySQL 5.5 及以前&lt;/strong&gt;：统计信息是&lt;strong&gt;非持久化&lt;/strong&gt;的。重启数据库后，统计信息会丢失，MySQL 会在第一次访问表时重新计算。这通常不会导致一直不走索引，但可能会导致数据库刚启动时产生瞬间的性能抖动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL 5.6 及以后（主流）&lt;/strong&gt;：引入了&lt;strong&gt;持久化统计信息（Persistent Optimizer Statistics）&lt;/strong&gt;，默认开启（&lt;code&gt;innodb_stats_persistent=ON&lt;/code&gt;）。统计信息存储在磁盘的 &lt;code&gt;mysql.innodb_table_stats&lt;/code&gt; 表中。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：重启后统计信息不丢失，执行计划稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：这也意味着，如果你通过非标准方式（如直接拷贝文件、某些恢复工具）恢复数据，或者大量数据变动后自动更新机制（&lt;code&gt;innodb_stats_auto_recalc&lt;/code&gt;）没有及时触发，统计信息就会长期处于“过时”状态，必须手动 &lt;code&gt;ANALYZE&lt;/code&gt; 才能纠正。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 这种问题出现的概率大吗？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;非常大，尤其是逻辑备份恢复后。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑恢复（如 source .sql 文件）&lt;/strong&gt;：本质是执行成千上万条 &lt;code&gt;INSERT&lt;/code&gt; 语句。虽然 InnoDB 默认在表数据变更超过 10% 时会自动重新计算统计信息，但在高负载恢复过程中，这个异步动作可能会滞后，或者因为锁竞争而失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理恢复（如 XtraBackup）&lt;/strong&gt;：通常会连同统计信息表文件一起恢复，出现概率相对较小，但也存在统计信息损坏的可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：在生产环境中，每当进行&lt;strong&gt;全量数据迁移、大批量数据导入/删除&lt;/strong&gt;操作后，建议把 &lt;code&gt;ANALYZE TABLE&lt;/code&gt; 作为标准收尾动作执行一遍，以确保“情报”准确。&lt;/p&gt;
&lt;h2&gt;📝 总结&lt;/h2&gt;
&lt;p&gt;数据库恢复或大量数据导入导出后，可能会导致索引统计信息不准确。MySQL 优化器依赖这些统计信息来决定执行计划。当统计信息偏差过大时，优化器可能会放弃索引而选择全表扫描。&lt;/p&gt;
&lt;p&gt;下次遇到这种“昨天好好的，今天突然不走索引”的情况，且 SQL 没变动，不妨先 &lt;code&gt;ANALYZE TABLE&lt;/code&gt; 一下，强制更新统计信息。&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>祝自己生日快乐</title><link>https://blog.meowrain.cn/posts/%E7%94%9F%E6%B4%BB/%E7%A5%9D%E8%87%AA%E5%B7%B1%E7%94%9F%E6%97%A5%E5%BF%AB%E4%B9%90/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E7%94%9F%E6%B4%BB/%E7%A5%9D%E8%87%AA%E5%B7%B1%E7%94%9F%E6%97%A5%E5%BF%AB%E4%B9%90/</guid><pubDate>Mon, 05 Jan 2026 22:22:28 GMT</pubDate><content:encoded>&lt;h1&gt;生日蛋糕&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10sz5vn-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;💕💕💕💕💕💕姐姐买的，很好吃💕💕💕&lt;/p&gt;
&lt;p&gt;明天吃
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10wbwh6-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;小物件&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;把去年看洛天依演唱会买的盒子拆了（可爱的天依0-0）
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10wwmyz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10u4sop-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10uitt5-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10utg32-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有立牌&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10v65kf-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;亚克力牌
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10v8oew-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;很硬的卡纸
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10vus92-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Bash查找命令顺序</title><link>https://blog.meowrain.cn/posts/linux/bash%E6%9F%A5%E6%89%BE%E9%A1%BA%E5%BA%8F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/linux/bash%E6%9F%A5%E6%89%BE%E9%A1%BA%E5%BA%8F/</guid><pubDate>Mon, 05 Jan 2026 22:06:25 GMT</pubDate><content:encoded>&lt;h1&gt;Bash命令查找顺序&lt;/h1&gt;
&lt;h1&gt;1. 绝对路径或者相对路径&lt;/h1&gt;
&lt;p&gt;优先级最高，如果输入的命令用 &apos;/&apos;或者&apos;./&apos;开头，bash会直接访问指定路径下的文件去执行
比如： 输入 &apos;/bin/ls&apos;或者&apos;./script.sh&apos; bash会直接执行这个路径下面的文件，跳过后续所有的查找步骤&lt;/p&gt;
&lt;h1&gt;2. 别名&lt;/h1&gt;
&lt;p&gt;比如: &lt;code&gt;alias ll = &apos;ls -l&apos;&lt;/code&gt; 输入ll会被替换为&apos;ls -l &apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ps: 买了不少vps,发现有的vps 的ls命令，可执行文件和目录的颜色和普通文件的文件名颜色不一样，之前一直不知道为什么。后来看了下才知道是用到了别名优先级比较高的特性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10muvzx-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;能看到上面有配置&lt;code&gt;alias ls = &apos;ls --color=auto&apos;&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;Shell内置命令&lt;/h1&gt;
&lt;p&gt;如果别名没有匹配,bash会去检查是不是内置命令（比如cd,echo这种）&lt;/p&gt;
&lt;h1&gt;哈希表&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10pax8l-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10q01ct-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10pyvd2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;环境变量 path中的目录&lt;/h1&gt;
&lt;p&gt;最后一步，Bash按照PATH定义的目录顺序从左到右搜索可执行文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2026/01/05/10qkdcl-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>开发工具笔记合集</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/tool/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/tool/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Git&lt;/h1&gt;
&lt;h2&gt;Git概述&lt;/h2&gt;
&lt;h3&gt;版本系统&lt;/h3&gt;
&lt;p&gt;SVN 是集中式版本控制系统，版本库是集中放在中央服务器的，而开发人员工作的时候，用的都是自己的电脑，所以首先要从中央服务器下载最新的版本，然后开发，开发完后，需要把自己开发的代码提交到中央服务器。&lt;/p&gt;
&lt;p&gt;集中式版本控制工具缺点：服务器单点故障、容错性差&lt;/p&gt;
&lt;p&gt;Git 是分布式版本控制系统（Distributed Version Control System，简称 DVCS） ，分为两种类型的仓库：&lt;/p&gt;
&lt;p&gt;本地仓库和远程仓库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地仓库：是在开发人员自己电脑上的 Git 仓库&lt;/li&gt;
&lt;li&gt;远程仓库：是在远程服务器上的 Git 仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;p&gt;1．从远程仓库中克隆代码到本地仓库&lt;/p&gt;
&lt;p&gt;2．从本地仓库中 checkout 代码然后进行代码修改&lt;/p&gt;
&lt;p&gt;3．在提交前先将代码提交到&lt;strong&gt;暂存区&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;4．提交到本地仓库。本地仓库中保存修改的各个历史版本&lt;/p&gt;
&lt;p&gt;5．修改完成后，需要和团队成员共享代码时，将代码 push 到远程仓库&lt;/p&gt;
&lt;h3&gt;Git安装&lt;/h3&gt;
&lt;p&gt;下载地址： &lt;a href=&quot;https://git-scm.com/download&quot;&gt;https://git-scm.com/download&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;代码托管&lt;/h3&gt;
&lt;p&gt;Git 中存在两种类型的仓库，即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢？我们可以借助互联网上提供的一些代码托管服务来实现，其中比较常用的有 GitHub、码云、GitLab 等。&lt;/p&gt;
&lt;p&gt;GitHub（地址：https://github.com/）是一个面向开源及私有软件项目的托管平台，因为只支持 Git 作为唯一的版本库格式进行托管，故名 GitHub&lt;/p&gt;
&lt;p&gt;码云（地址： https://gitee.com/）是国内的一个代码托管平台，由于服务器在国内，所以相比于 GitHub，码云速度会更快&lt;/p&gt;
&lt;p&gt;GitLab（地址： https://about.gitlab.com/ ）是一个用于仓库管理系统的开源项目，使用 Git 作为代码管理工具，并在此基础上搭建起来的 web 服务&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;环境配置&lt;/h2&gt;
&lt;p&gt;安装 Git 后首先要设置用户名称和 email 地址，因为每次 Git 提交都会使用该用户信息，此信息和注册的代码托管平台的信息无关&lt;/p&gt;
&lt;p&gt;设置用户信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;git config --global user.name “Seazean”&lt;/li&gt;
&lt;li&gt;git config --global user.email &quot;imseazean@gmail.com&quot;  //用户名和邮箱可以随意填写，不会校对&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查看配置信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;git config --list&lt;/li&gt;
&lt;li&gt;git config user.name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过上面的命令设置的信息会保存在用户目录下 /.gitconfig 文件中&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;本地仓库&lt;/h2&gt;
&lt;h3&gt;获取仓库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;本地仓库初始化&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在电脑的任意位置创建一个空目录（例如 repo1）作为本地 Git 仓库&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入这个目录中，点击右键打开 Git bash 窗口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行命令 &lt;strong&gt;git init&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果在当前目录中看到 .git 文件夹（此文件夹为隐藏文件夹）则说明 Git 仓库创建成功&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;远程仓库克隆&lt;/strong&gt;
通过 Git 提供的命令从远程仓库进行克隆，将远程仓库克隆到本地&lt;/p&gt;
&lt;p&gt;命令：git clone 远程 Git 仓库地址（HTTPS 或者 SSH）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成 SSH 公钥步骤&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置账户&lt;/li&gt;
&lt;li&gt;cd ~/.ssh（查看是否生成过 SSH 公钥）user 目录下&lt;/li&gt;
&lt;li&gt;生成 SSH 公钥：&lt;code&gt;ssh-keygen -t rsa -C &quot;email&quot;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;-t 指定密钥类型，默认是 rsa ，可以省略&lt;/li&gt;
&lt;li&gt;-C 设置注释文字，比如邮箱&lt;/li&gt;
&lt;li&gt;-f 指定密钥文件存储文件名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;查看命令：cat ~/.ssh/id_rsa.pub&lt;/li&gt;
&lt;li&gt;公钥测试命令：ssh -T git@github.com&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;工作过程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Git%E5%9F%BA%E6%9C%AC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;版本库：.git 隐藏文件夹就是版本库，版本库中存储了很多配置信息、日志信息和文件版本信息等&lt;/p&gt;
&lt;p&gt;工作目录（工作区）：包含 .git 文件夹的目录就是工作目录，主要用于存放开发的代码&lt;/p&gt;
&lt;p&gt;暂存区：.git 文件夹中有很多文件，其中有一个 index 文件就是暂存区，也可以叫做 stage，暂存区是一个临时保存修改文件的地方&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E6%96%87%E4%BB%B6%E6%B5%81%E7%A8%8B%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文件操作&lt;/h3&gt;
&lt;h4&gt;常用命令&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;git status&lt;/td&gt;
&lt;td&gt;查看 git 状态 （文件是否进行了添加、提交操作）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git add filename&lt;/td&gt;
&lt;td&gt;添加，将指定文件添加到暂存区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git commit -m &apos;message&apos;&lt;/td&gt;
&lt;td&gt;提交，将暂存区文件提交到本地仓库，删除暂存区的该文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git commit --amend&lt;/td&gt;
&lt;td&gt;修改 commit 的 message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git rm filename&lt;/td&gt;
&lt;td&gt;删除，删除工作区的文件，不是仓库，需要提交&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git mv filename&lt;/td&gt;
&lt;td&gt;移动或重命名工作区文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git reset filename&lt;/td&gt;
&lt;td&gt;使用当前分支上的修改覆盖暂存区，&lt;strong&gt;将暂存区的文件取消暂存&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git checkout filename&lt;/td&gt;
&lt;td&gt;使用暂存区的修改覆盖工作目录，用来撤销本次修改(危险)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git log&lt;/td&gt;
&lt;td&gt;查看日志（ git 提交的历史日志）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git reflog&lt;/td&gt;
&lt;td&gt;可以查看所有分支的所有操作记录（包括已经被删除的 commit 记录的操作）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;其他指令&lt;/strong&gt;：可以跳过暂存区域直接从分支中取出修改，或者直接提交修改到分支中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;git commit -a 直接把所有文件的修改添加到暂存区然后执行提交&lt;/li&gt;
&lt;li&gt;git checkout HEAD -- files 取出最后一次修改，可以用来进行回滚操作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;文件状态&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Git 工作目录下的文件存在两种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;untracked 未跟踪（未被纳入版本控制）&lt;/li&gt;
&lt;li&gt;tracked 已跟踪（被纳入版本控制）
&lt;ul&gt;
&lt;li&gt;Unmodified 未修改状态&lt;/li&gt;
&lt;li&gt;Modified 已修改状态&lt;/li&gt;
&lt;li&gt;Staged 已暂存状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看文件状态：文件的状态会随着我们执行 Git 的命令发生变化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;git status 查看文件状态&lt;/li&gt;
&lt;li&gt;git status –s 查看更简洁的文件状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;文件忽略&lt;/h4&gt;
&lt;p&gt;一般我们总会有些文件无需纳入Git 的管理，也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件，比如日志文件，或者编译过程中创建的临时文件等。 在这种情况下，我们可以在工作目录中创建一个名为 .gitignore 的文件（文件名称固定），列出要忽略的文件模式。下面是一个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# no .a files
*.a
# but do track lib.a, even though you&apos;re ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;远程仓库&lt;/h2&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;p&gt;Git 有四个工作空间的概念，分别为 工作空间、暂存区、本地仓库、远程仓库。&lt;/p&gt;
&lt;p&gt;pull = fetch + merge&lt;/p&gt;
&lt;p&gt;fetch 是从远程仓库更新到本地仓库，pull是从远程仓库直接更新到工作空间中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E5%9B%BE%E8%A7%A3%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;查看仓库&lt;/h3&gt;
&lt;p&gt;git remote：显示所有远程仓库的简写&lt;/p&gt;
&lt;p&gt;git remote -v：显示所有远程仓库&lt;/p&gt;
&lt;p&gt;git remote show &amp;lt;shortname&amp;gt;：显示某个远程仓库的详细信息&lt;/p&gt;
&lt;h3&gt;添加仓库&lt;/h3&gt;
&lt;p&gt;git remote add &amp;lt;shortname&amp;gt;&amp;lt;url&amp;gt;：添加一个新的远程仓库，并指定一个可以引用的简写&lt;/p&gt;
&lt;h3&gt;克隆仓库&lt;/h3&gt;
&lt;p&gt;git clone &amp;lt;url&amp;gt;(HTTPS or SSH)：克隆远程仓库&lt;/p&gt;
&lt;p&gt;Git 克隆的是该 Git 仓库服务器上的几乎所有数据（包括日志信息、历史记录等），而不仅仅是复制工作所需要的文件，当你执行 git clone 命令的时候，默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。&lt;/p&gt;
&lt;h3&gt;删除仓库&lt;/h3&gt;
&lt;p&gt;git remote rm &amp;lt;shortname&amp;gt;：移除远程仓库，从本地移除远程仓库的记录，并不会影响到远程仓库&lt;/p&gt;
&lt;h3&gt;拉取仓库&lt;/h3&gt;
&lt;p&gt;git fetch  &amp;lt;shortname&amp;gt;：从远程仓库获取最新版本到本地仓库，不会自动 merge&lt;/p&gt;
&lt;p&gt;git pull &amp;lt;shortname&amp;gt; &amp;lt;branchname&amp;gt;：从远程仓库获取最新版本并 merge 到本地仓库&lt;/p&gt;
&lt;p&gt;注意：如果当前本地仓库不是从远程仓库克隆，而是本地创建的仓库，并且&lt;strong&gt;仓库中存在文件&lt;/strong&gt;，此时再从远程仓库拉取文件的时候会报错（fatal: refusing to merge unrelated histories ），解决此问题可以在 git pull 命令后加入参数 --allow-unrelated-histories&lt;/p&gt;
&lt;h3&gt;推送仓库&lt;/h3&gt;
&lt;p&gt;git push &amp;lt;shortname&amp;gt;&amp;lt;branchname&amp;gt;：上传本地指定分支到远程仓库&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;版本管理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%89%88%E6%9C%AC%E5%88%87%E6%8D%A2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;命令：git reset --hard 版本唯一索引值&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;分支管理&lt;/h2&gt;
&lt;h3&gt;查看分支&lt;/h3&gt;
&lt;p&gt;git branch：列出所有本地分支&lt;/p&gt;
&lt;p&gt;git branch -r：列出所有远程分支&lt;/p&gt;
&lt;p&gt;git branch -a：列出所有本地分支和远程分支&lt;/p&gt;
&lt;h3&gt;创建分支&lt;/h3&gt;
&lt;p&gt;git branch  branch-name：新建一个分支，但依然停留在当前分支&lt;/p&gt;
&lt;p&gt;git checkout -b branch-name：新建一个分支，并切换到该分支&lt;/p&gt;
&lt;h3&gt;推送分支&lt;/h3&gt;
&lt;p&gt;git push origin branch-name：推送到远程仓库，origin 是引用名&lt;/p&gt;
&lt;h3&gt;切换分支&lt;/h3&gt;
&lt;p&gt;git checkout branch-name：切换到 branch-name 分支&lt;/p&gt;
&lt;h3&gt;合并分支&lt;/h3&gt;
&lt;p&gt;git merge branch-name：合并指定分支到当前分支&lt;/p&gt;
&lt;p&gt;有时候合并操作不会如此顺利。 如果你在两个不同的分支中，对同一个文件的同一个部分进行了不同的修改，Git 就没办法合并它们，同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容，最后执行 git add 命令来标识冲突已解决&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E5%90%88%E5%B9%B6%E5%88%86%E6%94%AF%E5%86%B2%E7%AA%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;删除分支&lt;/h3&gt;
&lt;p&gt;git branch -d branch-name：删除分支&lt;/p&gt;
&lt;p&gt;git push origin –d branch-name：删除远程仓库中的分支   （origin 是引用名）&lt;/p&gt;
&lt;p&gt;如果要删除的分支中进行了开发动作，此时执行删除命令并不会删除分支，如果坚持要删除此分支，可以将命令中的 -d 参数改为 -D：git branch -D branch-name&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;标签管理&lt;/h2&gt;
&lt;h3&gt;查看标签&lt;/h3&gt;
&lt;p&gt;git tag：列出所有 tag&lt;/p&gt;
&lt;p&gt;git show tag-name：查看 tag 详细信息&lt;/p&gt;
&lt;p&gt;标签作用：在开发的一些关键时期，使用标签来记录这些关键时刻，保存快照，例如发布版本、有重大修改、升级的时候、会使用标签记录这些时刻，来永久标记项目中的关键历史时刻&lt;/p&gt;
&lt;h3&gt;新建标签&lt;/h3&gt;
&lt;p&gt;git tag tag-name：新建标签，如（git tag v1.0.1）&lt;/p&gt;
&lt;h3&gt;推送标签&lt;/h3&gt;
&lt;p&gt;git push [remotename] [tagname]：推送到远程仓库&lt;/p&gt;
&lt;p&gt;git push [remotename] --tags：推送所有的标签&lt;/p&gt;
&lt;h3&gt;切换标签&lt;/h3&gt;
&lt;p&gt;git checkout tag-name：切换标签&lt;/p&gt;
&lt;h3&gt;删除标签&lt;/h3&gt;
&lt;p&gt;git tag -d tag-name：删除本地标签&lt;/p&gt;
&lt;p&gt;git push origin :refs/tags/ tag-name：删除远程标签&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;IDEA操作&lt;/h2&gt;
&lt;h3&gt;环境配置&lt;/h3&gt;
&lt;p&gt;File → Settings 打开设置窗口，找到 Version Control 下的 git 选项&lt;/p&gt;
&lt;p&gt;选择 git 的安装目录后可以点击 Test 按钮测试是否正确配置：D:\Program Files\Git\cmd\git.exe&lt;/p&gt;
&lt;h3&gt;创建仓库&lt;/h3&gt;
&lt;p&gt;1、VCS → Import into Version Control → Create Git Repository&lt;/p&gt;
&lt;p&gt;2、选择工程所在的目录,这样就创建好本地仓库了&lt;/p&gt;
&lt;p&gt;3、点击git后边的对勾,将当前项目代码提交到本地仓库&lt;/p&gt;
&lt;p&gt;​	注意: 项目中的配置文件不需要提交到本地仓库中,提交时,忽略掉即可&lt;/p&gt;
&lt;h3&gt;文件操作&lt;/h3&gt;
&lt;p&gt;右键项目名打开菜单 Git → Add → commit&lt;/p&gt;
&lt;h3&gt;版本管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;版本对比
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%89%88%E6%9C%AC%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;版本切换方式一：控制台 Version Control → Log → 右键 Reset Current Branch → Reset，这种切换会抛弃原来的提交记录
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%89%88%E6%9C%AC%E5%88%87%E6%8D%A2%E6%96%B9%E5%BC%8F%E4%B8%80.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;版本切换方式二：控制台 Version Control → Log → Revert Commit → Merge → 处理代码 → commit，这种切换会当成一个新的提交记录，之前的提交记录也都保留
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%89%88%E6%9C%AC%E5%88%87%E6%8D%A2%E6%96%B9%E5%BC%8F%E4%BA%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​           &lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%89%88%E6%9C%AC%E5%88%87%E6%8D%A2%E6%96%B9%E5%BC%8F%E4%BA%8C(1).png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;分支管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;创建分支：VCS → Git → Branches → New Branch → 给分支起名字 → ok&lt;/li&gt;
&lt;li&gt;切换分支：idea 右下角 Git → 选择要切换的分支 → checkout&lt;/li&gt;
&lt;li&gt;合并分支：VCS → Git → Merge changes → 选择要合并的分支 → merge&lt;/li&gt;
&lt;li&gt;删除分支：idea 右下角 → 选中要删除的分支 → Delete&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;推送仓库&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;VCS → Git → Push → 点击 master Define remote&lt;/li&gt;
&lt;li&gt;将远程仓库的 url 路径复制过来 → Push
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93%E6%8E%A8%E9%80%81%E5%88%B0%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;克隆仓库&lt;/h3&gt;
&lt;p&gt;File → Close Project → Checkout from Version Control → Git → 指定远程仓库的路径 → 指定本地存放的路径 → clone&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93%E5%85%8B%E9%9A%86%E5%88%B0%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Linux&lt;/h1&gt;
&lt;h2&gt;操作系统&lt;/h2&gt;
&lt;p&gt;操作系统（Operation System），是管理计算机硬件与软件资源的计算机程序，同时也是计算机系统的内核与基石。操作系统需要处理管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务，操作系统也提供一个让用户与系统交互的操作界面&lt;/p&gt;
&lt;p&gt;操作系统作为接口的示意图：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/操作系统.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;移动设备操作系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%A7%BB%E5%8A%A8%E8%AE%BE%E5%A4%87%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Linux系统&lt;/h2&gt;
&lt;h3&gt;系统介绍&lt;/h3&gt;
&lt;p&gt;从内到位依次是硬件 → 内核层 → Shell 层 → 应用层 → 用户
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux%E7%B3%BB%E7%BB%9F.png&quot; alt=&quot;Linux&quot; /&gt;&lt;/p&gt;
&lt;p&gt;内核层：核心和基础，附着在硬件平台上，控制和管理系统内的各种资源，有效的组织进程的运行，扩展硬件的功能，提高资源利用效率，为用户提供安全可靠的应用环境。&lt;/p&gt;
&lt;p&gt;Shell 层：与用户直接交互的界面。用户可以在提示符下输入命令行，由 Shell 解释执行并输出相应结果或者有关信息，所以我们也把  Shell 称作命令解释器，利用系统提供的丰富命令可以快捷而简便地完成许多工作。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文件系统&lt;/h3&gt;
&lt;p&gt;Linux 文件系统目录结构和熟知的 windows 系统有较大区别，没有各种盘符的概念。根目录只有一个/，采用层级式的树状目录结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F.png&quot; alt=&quot;Linux文件系统&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;远程连接&lt;/h2&gt;
&lt;h3&gt;设置IP&lt;/h3&gt;
&lt;h4&gt;NAT&lt;/h4&gt;
&lt;p&gt;首先设置虚拟机中 NAT 模式的选项，打开 VMware，点击编辑下的虚拟网络编辑器，设置 NAT 参数
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E9%85%8D%E7%BD%AENAT.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：VMware Network Adapter VMnet8 保证是启用状态&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E6%9C%AC%E5%9C%B0%E4%B8%BB%E6%9C%BA%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;静态IP&lt;/h4&gt;
&lt;p&gt;在普通用户下不能修改网卡的配置信息；所以我们要切换到 root 用户进行 ip 配置：su root/su&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改网卡配置文件：&lt;code&gt;vim /etc/sysconfig/network-scripts/ifcfg-ens33&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改文件内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=static
IPADDR=10.2.111.62
NETMASK=255.255.252.0
GATEWAY=10.2.111.254
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=ens33
UUID=2c2371f1-ef29-4514-a568-c4904bd11c82
DEVICE=ens33
ONBOOT=true
###########################
BOOTPROTO设置为静态static
IPADDR设置ip地址
NETMASK设置子网掩码
GATEWAY设置网关
ONBOOT设置为true在系统启动时是否激活网卡
执行保存 :wq!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启网络：systemctl restart network&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看IP：ifconfig&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;宿主机 ping 虚拟机，虚拟机 ping 宿主机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在虚拟机中访问网络，需要增加一块 NAT 网卡&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;【虚拟机】--【设置】--【添加】&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/两块NAT网卡.jpg&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;远程登陆&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;服务器维护工作&lt;/strong&gt; 都是在 远程 通过 SSH 客户端 来完成的， 并没有图形界面， 所有的维护工作都需要通过命令来完成，Linux 服务器需要安装 SSH 相关服务&lt;/p&gt;
&lt;p&gt;首先执行 sudo apt-get install openssh-server 指令，接下来用 xshell 连接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E8%BF%9C%E7%A8%8B%E8%BF%9E%E6%8E%A5Linux.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先用普通用户登录，然后转成 root&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;用户管理&lt;/h2&gt;
&lt;p&gt;Linux 系统是一个多用户、多任务的操作系统。多用户是指在 Linux 操作系统中可以创建多个用户，而这些多用户又可以同时执行各自不同的任务，而互不影响&lt;/p&gt;
&lt;p&gt;在 Linux 系统中，会存在着以下几个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户名：用户的名称&lt;/li&gt;
&lt;li&gt;用户所属的组：当前用户所属的组&lt;/li&gt;
&lt;li&gt;用户的家目录：当前账号登录成功之后的目录，就叫做该用户的家目录&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户管理&lt;/h3&gt;
&lt;h4&gt;当前用户&lt;/h4&gt;
&lt;p&gt;logname：用于显示目前用户的名称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;--help：在线帮助&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;--vesion：显示版本信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;切换用户&lt;/h4&gt;
&lt;p&gt;su UserName：切换用户&lt;/p&gt;
&lt;p&gt;su -c comman root：切换用户为 root 并在执行 comman 指令后退出返回原使用者&lt;/p&gt;
&lt;p&gt;su：切换到 root 用户&lt;/p&gt;
&lt;h4&gt;用户添加&lt;/h4&gt;
&lt;p&gt;命令：useradd  [options]  用户名&lt;/p&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-c comment 指定一段注释性描述&lt;/li&gt;
&lt;li&gt;-d 指定用户主目录，如果此目录不存在，则同时使用 -m 选项，可以创建主目录&lt;/li&gt;
&lt;li&gt;-m 创建用户的主目录&lt;/li&gt;
&lt;li&gt;-g 用户组，指定用户所属的用户组&lt;/li&gt;
&lt;li&gt;-G 用户组，用户组 指定用户所属的附加组&lt;/li&gt;
&lt;li&gt;-s Shell 文件 指定用户的登录 Shell&lt;/li&gt;
&lt;li&gt;-u 用户号，指定用户的用户号，如果同时有 -o 选项，则可以重复使用其他用户的标识号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何知道添加用户成功呢？ 通过指令 cat /etc/passwd 查看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seazean:x:  1000:1000:Seazean:/home/seazean:/bin/bash
用户名 密码  用户ID 组ID   注释    家目录        shell程序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;useradd -m Username 新建用户成功之后，会建立 home 目录，但是此时有问题没有指定 shell 的版本，不是我们熟知的 bash，功能上有很多限制，进行 &lt;strong&gt;sudo useradd -m -s /bin/bash Username&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;用户密码&lt;/h4&gt;
&lt;p&gt;系统安装好默认的 root 用户是没有密码的，需要给 root 设置一个密码 &lt;strong&gt;sudo passwd root&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;普通用户：&lt;strong&gt;sudo passwd UserName&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理员用户：passwd [options] UserName&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-l：锁定密码，即禁用账号&lt;/li&gt;
&lt;li&gt;-u：密码解锁&lt;/li&gt;
&lt;li&gt;-d：使账号无密码&lt;/li&gt;
&lt;li&gt;-f：强迫用户下次登录时修改密码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;用户权限&lt;/h4&gt;
&lt;p&gt;usermod 命令通过修改系统帐户文件来修改用户账户信息&lt;/p&gt;
&lt;p&gt;修改用户账号就是根据实际情况更改用户的有关属性，如用户号、主目录、用户组、登录 Shell 等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;普通用户：sudo usermod [options] Username&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理员用户：usermod [options] Username&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;usermod &lt;strong&gt;-l&lt;/strong&gt; newName Username&lt;/li&gt;
&lt;li&gt;-l 新的登录名称&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;用户删除&lt;/h4&gt;
&lt;p&gt;删除用户账号就是要将 /etc/passwd 等系统文件中的该用户记录删除，必要时还删除用户的主目录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;普通用户：sudo userdel [options] Username&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理员用户：userdel [options] Username&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-f：强制删除用户，即使用户当前已登录&lt;/li&gt;
&lt;li&gt;-r：删除用户的同时，删除与用户相关的所有文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;用户组管理&lt;/h3&gt;
&lt;h4&gt;组管理&lt;/h4&gt;
&lt;p&gt;添加组：&lt;strong&gt;groupadd 组名&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;创建用户的时加入组：useradd -m  -g 组名 用户名
​&lt;/p&gt;
&lt;h4&gt;添加用户组&lt;/h4&gt;
&lt;p&gt;新增一个用户组（组名可见名知意，符合规范即可），然后将用户添加到组中，需要使用管理员权限&lt;/p&gt;
&lt;p&gt;命令：groupadd  [options] Groupname&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-g GID 指定新用户组的组标识号（GID）&lt;/li&gt;
&lt;li&gt;-o 一般与 -g 选项同时使用，表示新用户组的 GID 可以与系统已有用户组的 GID 相同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新增用户组 Seazean：groupadd Seazean&lt;/p&gt;
&lt;h4&gt;修改用户组&lt;/h4&gt;
&lt;p&gt;需要使用管理员权限&lt;/p&gt;
&lt;p&gt;命令：groupmod [options] Groupname&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-g GID 为用户组指定新的组标识号。&lt;/li&gt;
&lt;li&gt;-o 与 -g 选项同时使用，用户组的新 GID 可以与系统已有用户组的 GID 相同&lt;/li&gt;
&lt;li&gt;-n 新用户组 将用户组的名字改为新名字&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修改 Seazean 组名为 zhy：groupmod -n zhy Seazean&lt;/p&gt;
&lt;h4&gt;删除用户组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;普通用户：sudo groupdel Groupname&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理员用户：groupdel Groupname&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-f  用户的主组也继续删除&lt;/li&gt;
&lt;li&gt;-h  显示帮助信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;用户所属组&lt;/h4&gt;
&lt;p&gt;查询用户所属组：groups Username&lt;/p&gt;
&lt;p&gt;查看用户及组信息：id Username&lt;/p&gt;
&lt;p&gt;创建用户的时加入组：useradd -m  -g Groupname Username&lt;/p&gt;
&lt;p&gt;修改用户所属组：usermod -g Groupname Username&lt;/p&gt;
&lt;p&gt;usermod常用选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-d 用户的新主目录&lt;/li&gt;
&lt;li&gt;-l  新的登录名称&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;gpasswd&lt;/h4&gt;
&lt;p&gt;gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具，用于将一个用户添加到组或从组中删除&lt;/p&gt;
&lt;p&gt;命令：gpasswd  选项  Username  Groupname&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-a 向组 GROUP 中添加用户 USER&lt;/li&gt;
&lt;li&gt;-d 从组 GROUP 中添加或删除用户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;查看用户组下所有用户（所有用户）&lt;/strong&gt;：grep &apos;Groupname&apos; /etc/group&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;系统管理&lt;/h2&gt;
&lt;h3&gt;man&lt;/h3&gt;
&lt;p&gt;在控制台输入：命令名 -h/  -help/   --h  /空&lt;/p&gt;
&lt;p&gt;可以看到命令的帮助文档&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;man&lt;/strong&gt; [指令名称]：查看帮助文档，比如 man ls，退出方式 q&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;date&lt;/h3&gt;
&lt;p&gt;date 可以用来显示或设定系统的日期与时间&lt;/p&gt;
&lt;p&gt;命令：date [options]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-d&amp;lt;字符串&amp;gt;：显示字符串所指的日期与时间，字符串前后必须加上双引号；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-s&amp;lt;字符串&amp;gt;：根据字符串来设置日期与时间，字符串前后必须加上双引号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-u：显示 GMT&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;--version：显示版本信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查看时间：date → 2020年 11月 30日 星期一 17:10:54 CST&lt;/p&gt;
&lt;p&gt;查看指定格式时间：date &quot;+%Y-%m-%d %H:%M:%S&quot; → 2020-11-30 17:11:44&lt;/p&gt;
&lt;p&gt;设置日期指令：date -s “2019-12-23 19:21:00”&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;id&lt;/h3&gt;
&lt;p&gt;id 会显示用户以及所属群组的实际与有效 ID，若两个 ID 相同则仅显示实际 ID；若仅指定用户名称，则显示目前用户的 ID&lt;/p&gt;
&lt;p&gt;命令：id [-gGnru] [--help] [--version] [用户名称] //参数的顺序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-g 或--group：显示用户所属群组的 ID&lt;/li&gt;
&lt;li&gt;-G 或--groups：显示用户所属附加群组的 ID&lt;/li&gt;
&lt;li&gt;-n 或--name：显示用户，所属群组或附加群组的名称。&lt;/li&gt;
&lt;li&gt;-r 或--real：显示实际 ID&lt;/li&gt;
&lt;li&gt;-u 或--user：显示用户 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;id 命令参数虽然很多，但是常用的是不带参数的 id 命令，主要看 uid 和组信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;sudo&lt;/h3&gt;
&lt;p&gt;sudo：控制用户对系统命令的使用权限，通过 sudo 可以提高普通用户的操作权限&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-V 显示版本编号&lt;/li&gt;
&lt;li&gt;-h 会显示版本编号及指令的使用方式说明&lt;/li&gt;
&lt;li&gt;-l  显示出自己（执行 sudo 的使用者）的权限&lt;/li&gt;
&lt;li&gt;-command 要以系统管理者身份（或以 -u 更改为其他人）执行的指令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;sudo -u root command  -l&lt;/strong&gt;：指定 root 用户执行指令 command&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;top&lt;/h3&gt;
&lt;p&gt;top：用于实时显示 process 的动态&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-c：command 属性进行了命令补全&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-p 进程号：显示指定 pid 的进程信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-d 秒数：表示进程界面更新时间（每几秒刷新一次）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-H 表示线程模式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;top -Hp 进程 id&lt;/code&gt;：分析该进程内各线程的 CPU 使用情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/top%E5%91%BD%E4%BB%A4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;各进程（任务）的状态监控属性解释说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PID — 进程 id&lt;/li&gt;
&lt;li&gt;TID — 线程 id&lt;/li&gt;
&lt;li&gt;USER — 进程所有者&lt;/li&gt;
&lt;li&gt;PR — 进程优先级&lt;/li&gt;
&lt;li&gt;NI — nice 值，负值表示高优先级，正值表示低优先级&lt;/li&gt;
&lt;li&gt;VIRT — 进程使用的虚拟内存总量，单位 kb，VIRT=SWAP+RES&lt;/li&gt;
&lt;li&gt;RES — 进程使用的、未被换出的物理内存大小，单位 kb，RES=CODE+DATA&lt;/li&gt;
&lt;li&gt;SHR — 共享内存大小，单位 kb&lt;/li&gt;
&lt;li&gt;S — 进程状态，D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程&lt;/li&gt;
&lt;li&gt;%CPU — 上次更新到现在的 CPU 时间占用百分比&lt;/li&gt;
&lt;li&gt;%MEM — 进程使用的物理内存百分比&lt;/li&gt;
&lt;li&gt;TIME+ — 进程使用的 CPU 时间总计，单位 1/100 秒&lt;/li&gt;
&lt;li&gt;COMMAND — 进程名称（命令名/命令行）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ps&lt;/h3&gt;
&lt;p&gt;Linux 系统中查看进程使用情况的命令是 ps 指令&lt;/p&gt;
&lt;p&gt;命令：ps&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-e:  显示所有进程&lt;/li&gt;
&lt;li&gt;-f:  全格式&lt;/li&gt;
&lt;li&gt;a:  显示终端上的所有进程&lt;/li&gt;
&lt;li&gt;u:  以用户的格式来显示进程信息&lt;/li&gt;
&lt;li&gt;x:  显示后台运行的进程&lt;/li&gt;
&lt;li&gt;-T：开启线程查看&lt;/li&gt;
&lt;li&gt;-p：指定线程号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般常用格式为 ps -ef 或者 ps aux 两种。显示的信息大体一致，略有区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果想查看进程的 CPU 占用率和内存占用率，可以使用 aux&lt;/li&gt;
&lt;li&gt;如果想查看进程的父进程 ID 和完整的 COMMAND 命令，可以使用 ef&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ps -T -p &amp;lt;pid&amp;gt;&lt;/code&gt;：显示某个进程的线程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ps 和 top 区别：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ps 命令：可以查看进程的瞬间信息，是系统在过去执行的进程的静态快照&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;top 命令：可以持续的监视进程的动态信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;kill&lt;/h3&gt;
&lt;p&gt;Linux kill 命令用于删除执行中的程序或工作，并不是让进程直接停止，而是给进程发一个信号，可以进入终止逻辑&lt;/p&gt;
&lt;p&gt;命令：kill [-s &amp;lt;信息名称或编号&amp;gt;] [程序]　或　kill [-l &amp;lt;信息编号&amp;gt;]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-l &amp;lt;信息编号&amp;gt;：若不加&amp;lt;信息编号&amp;gt;选项，则-l参数会列出全部的信息名称&lt;/li&gt;
&lt;li&gt;-s &amp;lt;信息名称或编号&amp;gt;：指定要送出的信息&lt;/li&gt;
&lt;li&gt;-KILL：强制杀死进程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-9：彻底杀死进程（常用）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;[程序]  程序的 PID、PGID、工作编号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kill 15642 &lt;/code&gt;.   &lt;code&gt;kill -KILL 15642&lt;/code&gt;.    &lt;code&gt;kill -9 15642&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;杀死指定用户所有进程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;过滤出 user 用户进程 ：&lt;code&gt;kill -9 $(ps -ef | grep user) &lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接杀死：&lt;code&gt;kill -u user&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;shutdown&lt;/h3&gt;
&lt;p&gt;shutdown 命令可以用来进行关闭系统，并且在关机以前传送讯息给所有使用者正在执行的程序，shutdown 也可以用来重开机&lt;/p&gt;
&lt;p&gt;普通用户：sudo shutdown [-t seconds] [-rkhncfF] time [message]&lt;/p&gt;
&lt;p&gt;管理员用户：shutdown [-t seconds] [-rkhncfF] time [message]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-t seconds：设定在几秒钟之后进行关机程序&lt;/li&gt;
&lt;li&gt;-k：并不会真的关机，只是将警告讯息传送给所有使用者&lt;/li&gt;
&lt;li&gt;-r：关机后重新开机&lt;/li&gt;
&lt;li&gt;-h：关机后停机&lt;/li&gt;
&lt;li&gt;-n：不采用正常程序来关机，用强迫的方式杀掉所有执行中的程序后自行关机&lt;/li&gt;
&lt;li&gt;-c：取消目前已经进行中的关机动作&lt;/li&gt;
&lt;li&gt;-f：关机时，不做 fcsk 动作（检查 Linux 档系统）&lt;/li&gt;
&lt;li&gt;-F：关机时，强迫进行 fsck 动作&lt;/li&gt;
&lt;li&gt;time：设定关机的时间&lt;/li&gt;
&lt;li&gt;message：传送给所有使用者的警告讯息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;立即关机：&lt;code&gt;shutdown -h now&lt;/code&gt;   或者   &lt;code&gt;shudown now&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;指定 1 分钟后关机并显示警告信息：&lt;code&gt;shutdown +1 &quot;System will shutdown after 1 minutes&quot; &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;指定 1 分钟后重启并发出警告信息：&lt;code&gt;shutdown –r +1 &quot;1分钟后关机重启&quot;&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;reboot&lt;/h3&gt;
&lt;p&gt;reboot 命令用于用来重新启动计算机&lt;/p&gt;
&lt;p&gt;命令：reboot [-n] [-w] [-d] [-f] [-i]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-n：在重开机前不做将记忆体资料写回硬盘的动作&lt;/li&gt;
&lt;li&gt;-w：并不会真的重开机，只是把记录写到 /var/log/wtmp 档案里&lt;/li&gt;
&lt;li&gt;-d：不把记录写到 /var/log/wtmp 档案里（-n 这个参数包含了 -d）&lt;/li&gt;
&lt;li&gt;-f：强迫重开机，不呼叫 shutdown 这个指令&lt;/li&gt;
&lt;li&gt;-i：在重开机之前先把所有网络相关的装置先停止&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;who&lt;/h3&gt;
&lt;p&gt;who 命令用于显示系统中有哪些使用者正在上面，显示的资料包含了使用者 ID、使用的终端机、上线时间、CPU 使用量、动作等等&lt;/p&gt;
&lt;p&gt;命令：who - [husfV] [user]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-H 或 --heading：显示各栏位的标题信息列（常用 &lt;code&gt;who -H&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;-i 或 -u 或 --idle：显示闲置时间，若该用户在前一分钟之内有进行任何动作，将标示成 &lt;code&gt;.&lt;/code&gt; 号，如果该用户已超过 24 小时没有任何动作，则标示出 &lt;code&gt;old&lt;/code&gt; 字符串&lt;/li&gt;
&lt;li&gt;-m：此参数的效果和指定 &lt;code&gt;am i&lt;/code&gt; 字符串相同&lt;/li&gt;
&lt;li&gt;-q 或--count：只显示登入系统的帐号名称和总人数&lt;/li&gt;
&lt;li&gt;-s：此参数将忽略不予处理，仅负责解决who指令其他版本的兼容性问题&lt;/li&gt;
&lt;li&gt;-w 或-T或--mesg或--message或--writable：显示用户的信息状态栏&lt;/li&gt;
&lt;li&gt;--help：在线帮助&lt;/li&gt;
&lt;li&gt;--version：显示版本信息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;systemctl&lt;/h3&gt;
&lt;p&gt;命令：systemctl [command] [unit]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;--version  查看版本号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;start：立刻启动后面接的 unit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;stop：立刻关闭后面接的 unit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;restart：立刻关闭后启动后面接的 unit，亦即执行 stop 再 start 的意思&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reload：不关闭 unit 的情况下，重新载入配置文件，让设置生效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;status：目前后面接的这个 unit 的状态，会列出有没有正在执行、开机时是否启动等信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enable：设置下次开机时，后面接的 unit 会被启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;disable：设置下次开机时，后面接的 unit 不会被启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;is-active：目前有没有正在运行中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;is-enable：开机时有没有默认要启用这个 unit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kill ：不要被 kill 这个名字吓着了，它其实是向运行 unit 的进程发送信号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;show：列出 unit 的配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mask：注销 unit，注销后你就无法启动这个 unit 了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unmask：取消对 unit 的注销&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;timedatectl&lt;/h3&gt;
&lt;p&gt;timedatectl用于控制系统时间和日期。可以查询和更改系统时钟于设定，同时可以设定和修改时区信息。在实际开发过程中，系统时间的显示会和实际出现不同步；我们为了校正服务器时间、时区会使用timedatectl命令&lt;/p&gt;
&lt;p&gt;timedatectl：显示系统的时间信息&lt;/p&gt;
&lt;p&gt;timedatectl status：显示系统的当前时间和日期&lt;/p&gt;
&lt;p&gt;timedatectl | grep Time：查看当前时区&lt;/p&gt;
&lt;p&gt;timedatectl list-timezones：查看所有可用的时区&lt;/p&gt;
&lt;p&gt;timedatectl set-timezone &quot;Asia/Shanghai&quot;：设置本地时区为上海&lt;/p&gt;
&lt;p&gt;timedatectl set-ntp true/false：启用/禁用时间同步&lt;/p&gt;
&lt;p&gt;timedatectl set-time &quot;2020-12-20 20:45:00&quot;：时间同步关闭后可以设定时间&lt;/p&gt;
&lt;p&gt;NTP 即 Network Time Protocol（网络时间协议），是一个互联网协议，用于同步计算机之间的系统时钟，timedatectl 实用程序可以自动同步你的Linux系统时钟到使用NTP的远程服务器&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;clear&lt;/h3&gt;
&lt;p&gt;clear 命令用于清除屏幕&lt;/p&gt;
&lt;p&gt;通过执行 clear 命令，就可以把缓冲区的命令全部清理干净&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;exit&lt;/h3&gt;
&lt;p&gt;exit 命令用于退出目前的 shell&lt;/p&gt;
&lt;p&gt;执行 exit 可使 shell 以指定的状态值退出。若不设置状态值参数，则 shell 以预设值退出。状态值 0 代表执行成功，其他值代表执行失败；exit 也可用在 script，离开正在执行的 script，回到 shell&lt;/p&gt;
&lt;p&gt;命令：exit [状态值]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;0 表示成功（Zero - Success）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非 0 表示失败（Non-Zero  - Failure）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 表示用法不当（Incorrect Usage）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;127 表示命令没有找到（Command Not Found）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;126 表示不是可执行的（Not an executable）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大于等于 128 信号产生&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;文件管理&lt;/h2&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;h4&gt;ls&lt;/h4&gt;
&lt;p&gt;ls命令相当于我们在Windows系统中打开磁盘、或者打开文件夹看到的目录以及文件的明细。&lt;/p&gt;
&lt;p&gt;命令：ls [options]  目录名称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-a ：全部的文件，连同隐藏档( 开头为 . 的文件) 一起列出来(常用)&lt;/li&gt;
&lt;li&gt;-d ：仅列出目录本身，而不是列出目录内的文件数据(常用)&lt;/li&gt;
&lt;li&gt;-l  ：显示不隐藏的文件与文件夹的详细信息；(常用)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ls -al = ll 命令&lt;/strong&gt;：显示所有文件与文件夹的详细信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;pwd&lt;/h4&gt;
&lt;p&gt;pwd 是 Print Working Directory 的缩写，也就是显示目前所在当前目录的命令&lt;/p&gt;
&lt;p&gt;命令：pwd 选项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-L	打印 $PWD 变量的值，如果它包含了当前的工作目录&lt;/li&gt;
&lt;li&gt;-P	打印当前的物理路径，不带有任何的符号链接&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;cd&lt;/h4&gt;
&lt;p&gt;cd 是 Change Directory 的缩写，这是用来变换工作目录的命令&lt;/p&gt;
&lt;p&gt;命令：cd [相对路径或绝对路径]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cd ~ ：表示回到根目录&lt;/li&gt;
&lt;li&gt;cd .. ：返回上级目录&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;相对路径&lt;/strong&gt; 在输入路径时, 最前面不是以 &lt;code&gt;/&lt;/code&gt; 开始的 , 表示相对&lt;strong&gt;当前目录&lt;/strong&gt;所在的目录位置
&lt;ul&gt;
&lt;li&gt;例如： /usr/share/doc&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绝对路径&lt;/strong&gt; 在输入路径时, 最前面是以 &lt;code&gt;/&lt;/code&gt;  开始的, 表示从&lt;strong&gt;根目录&lt;/strong&gt;开始的具体目录位置
&lt;ul&gt;
&lt;li&gt;由 /usr/share/doc 到 /usr/share/man 时，可以写成： cd ../man&lt;/li&gt;
&lt;li&gt;优点：定位准确, 不会因为 工作目录变化 而变化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;mkdir&lt;/h4&gt;
&lt;p&gt;mkdir命令用于建立名称为 dirName 之子目录&lt;/p&gt;
&lt;p&gt;命令：mkdir [-p] dirName&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-p 确保目录名称存在，不存在的就建一个，用来创建多级目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;mkdir -p aaa/bbb&lt;/code&gt;：在 aaa 目录下，创建一个 bbb 的子目录。 若 aaa 目录原本不存在，则建立一个&lt;/p&gt;
&lt;h4&gt;rmdir&lt;/h4&gt;
&lt;p&gt;rmdir命令删除空的目录&lt;/p&gt;
&lt;p&gt;命令：rmdir [-p] dirName&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-p 是当子目录被删除后使它也成为空目录的话，则顺便一并删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;rmdir  -p aaa/bbb&lt;/code&gt;：在 aaa 目录中，删除名为 bbb 的子目录。若 bbb 删除后，aaa 目录成为空目录，则 aaa 同时也会被删除&lt;/p&gt;
&lt;h4&gt;cp&lt;/h4&gt;
&lt;p&gt;cp 命令主要用于复制文件或目录&lt;/p&gt;
&lt;p&gt;命令：cp  [options]  source... directory&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-a：此选项通常在复制目录时使用，它保留链接、文件属性，并复制目录下的所有内容。其作用等于dpR参数组合&lt;/li&gt;
&lt;li&gt;-d：复制时保留链接。这里所说的链接相当于Windows系统中的快捷方式&lt;/li&gt;
&lt;li&gt;-f：覆盖已经存在的目标文件而不给出提示&lt;/li&gt;
&lt;li&gt;-i：与 -f 选项相反，在覆盖目标文件之前给出提示，要求用户确认是否覆盖，回答 y 时目标文件将被覆盖&lt;/li&gt;
&lt;li&gt;-p：除复制文件的内容外，还把修改时间和访问权限也复制到新文件中&lt;/li&gt;
&lt;li&gt;-r/R：若给出的源文件是一个目录文件，此时将复制该目录下所有的&lt;strong&gt;子目录&lt;/strong&gt;和文件&lt;/li&gt;
&lt;li&gt;-l：不复制文件，只是生成链接文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;cp –r aaa/*  ccc&lt;/code&gt;：复制 aaa 下的所有文件到 ccc，不加参数 -r 或者 -R，只复制文件，而略过目录&lt;/p&gt;
&lt;h4&gt;rm&lt;/h4&gt;
&lt;p&gt;rm命令用于删除一个文件或者目录。&lt;/p&gt;
&lt;p&gt;命令：rm [options] name...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-i 删除前逐一询问确认。&lt;/li&gt;
&lt;li&gt;-f 即使原档案属性设为唯读，亦直接删除，无需逐一确认&lt;/li&gt;
&lt;li&gt;-r 将目录及以下之档案亦逐一删除，递归删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：文件一旦通过 rm 命令删除，则无法恢复，所以必须格外小心地使用该命令&lt;/p&gt;
&lt;h4&gt;mv&lt;/h4&gt;
&lt;p&gt;mv 命令用来为文件或目录改名、或将文件或目录移入其它位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv [options] source dest
mv [options] source... directory
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-i：若指定目录已有同名文件，则先询问是否覆盖旧文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-f：在 mv 操作要覆盖某已有的目标文件时不给任何指示&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令格式&lt;/th&gt;
&lt;th&gt;运行结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mv  文件名  文件名&lt;/td&gt;
&lt;td&gt;将源文件名改为目标文件名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mv  文件名  目录名&lt;/td&gt;
&lt;td&gt;将文件移动到目标目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mv  目录名  目录名&lt;/td&gt;
&lt;td&gt;目标目录已存在，将源目录移动到目标目录。目标目录不存在则改名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mv  目录名  文件名&lt;/td&gt;
&lt;td&gt;出错&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;文件属性&lt;/h3&gt;
&lt;h4&gt;基本属性&lt;/h4&gt;
&lt;p&gt;Linux 系统是一种典型的多用户系统，不同的用户处于不同的地位，拥有不同的权限。为了保护系统的安全性，Linux系统对不同的用户访问同一文件（包括目录文件）的权限做了不同的规定&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%94%A8%E6%88%B7%E7%9B%AE%E5%BD%95%E4%B8%8B%E7%9A%84%E6%96%87%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在Linux中第一个字符代表这个文件是目录、文件或链接文件等等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当为 d 则是目录&lt;/li&gt;
&lt;li&gt;当为 - 则是文件&lt;/li&gt;
&lt;li&gt;若是 l 则表示为链接文档 link file&lt;/li&gt;
&lt;li&gt;若是 b 则表示为装置文件里面的可供储存的接口设备(可随机存取装置)&lt;/li&gt;
&lt;li&gt;若是 c 则表示为装置文件里面的串行端口设备，例如键盘、鼠标(一次性读取装置)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来的字符，以三个为一组，均为[rwx] 的三个参数组合。其中，[ r ]代表可读(read)、[ w ]代表可写(write)、[ x ]代表可执行(execute)。 要注意的是，这三个权限的位置不会改变，如果没有权限，就会出现[ - ]。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/文件权限.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;从左至右用 0-9 这些数字来表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 0 位确定文件类型&lt;/li&gt;
&lt;li&gt;第 1-3 位确定属主拥有该文件的权限&lt;/li&gt;
&lt;li&gt;第 4-6 位确定属组拥有该文件的权限&lt;/li&gt;
&lt;li&gt;第 7-9 位确定其他用户拥有该文件的权限&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;文件信息&lt;/h4&gt;
&lt;p&gt;对于一个文件，都有一个特定的所有者，也就是对该文件具有所有权的用户（属主）；还有这个文件是属于哪个组的（属组）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件的【属主】有一套【读写执行权限rwx】&lt;/li&gt;
&lt;li&gt;文件的【属组】有一套【读写执行权限rwx】&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E5%88%97%E5%87%BA%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ls -l&lt;/code&gt; 可以查看文件夹下文件的详细信息, 从左到右 依次是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;权限（A 区域）： 第一个字符如果是 &lt;code&gt;d&lt;/code&gt; 表示目录&lt;/li&gt;
&lt;li&gt;硬链接数（B 区域）：通俗的讲就是有多少种方式, 可以访问当前目录和文件&lt;/li&gt;
&lt;li&gt;属主（C 区域）：文件是所有者、或是叫做属主&lt;/li&gt;
&lt;li&gt;属组（D 区域）： 文件属于哪个组&lt;/li&gt;
&lt;li&gt;大小（E 区域）：文件大小&lt;/li&gt;
&lt;li&gt;时间（F 区域）：最后一次访问时间&lt;/li&gt;
&lt;li&gt;名称（G 区域）：文件的名称&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;更改权限&lt;/h4&gt;
&lt;h5&gt;权限概述&lt;/h5&gt;
&lt;p&gt;Linux 文件属性有两种设置方法，一种是数字，一种是符号&lt;/p&gt;
&lt;p&gt;Linux 的文件调用权限分为三级 : 文件属主、属组、其他，利用 chmod 可以控制文件如何被他人所调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod [-cfvR] [--help] [--version] mode file...
mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;u 表示档案的拥有者，g 表示与该档案拥有者属于同一个 group 者，o 表示其他的人，a 表示这三者皆是&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;+表示增加权限、- 表示取消权限、= 表示唯一设定权限&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;r 表示可读取，w 表示可写入，x 表示可执行，X 表示只有该档案是个子目录或者该档案已经被设定过为可执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;数字权限&lt;/h5&gt;
&lt;p&gt;命令：chmod [-R] xyz 文件或目录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xyz : 就是刚刚提到的数字类型的权限属性，为 rwx 属性数值的相加&lt;/li&gt;
&lt;li&gt;-R : 进行递归（recursive）的持续变更，亦即连同次目录下的所有文件都会变更&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件的权限字符为：[-rwxrwxrwx]， 这九个权限是三三一组的，我们使用数字来代表各个权限&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/权限数字表.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;各权限的数字对照表：[r]:4、[w]:2、[x]:1、[-]:0&lt;/p&gt;
&lt;p&gt;每种身份（owner/group/others）的三个权限（r/w/x）分数是需要累加的，例如权限为：[-rwxrwx---] 分数是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;owner = rwx = 4+2+1 = 7&lt;/li&gt;
&lt;li&gt;group = rwx = 4+2+1 = 7&lt;/li&gt;
&lt;li&gt;others= --- = 0+0+0 = 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表示为：&lt;code&gt;chmod -R 770 文件名&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;符号权限&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E6%9D%83%E9%99%90%E7%AC%A6%E5%8F%B7%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user     属主权限&lt;/li&gt;
&lt;li&gt;group  属组权限&lt;/li&gt;
&lt;li&gt;others  其他权限&lt;/li&gt;
&lt;li&gt;all  全部的身份&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们就可以使用 &lt;strong&gt;u g o a&lt;/strong&gt; 来代表身份的权限，读写的权限可以写成 &lt;strong&gt;r w x&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chmod u=rwx,g=rx,o=r  a.txt&lt;/code&gt;：将as.txt的权限设置为 &lt;strong&gt;-rwxr-xr--&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; chmod a-r a.txt&lt;/code&gt;：将文件的所有权限去除 &lt;strong&gt;r&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;更改属组&lt;/h4&gt;
&lt;p&gt;chgrp 命令用于变更文件或目录的所属群组&lt;/p&gt;
&lt;p&gt;文件或目录权限的的拥有者由所属群组来管理，可以使用 chgrp 指令去变更文件与目录的所属群组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chgrp [-cfhRv][--help][--version][所属群组][文件或目录...]
chgrp [-cfhRv][--help][--reference=&amp;lt;参考文件或目录&amp;gt;][--version][文件或目录...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;chgrp -v root aaa：将文件 aaa 的属组更改成 root（其他也可以）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;更改属主&lt;/h4&gt;
&lt;p&gt;利用 chown 可以将档案的拥有者加以改变。&lt;/p&gt;
&lt;p&gt;使用权限 : 管理员账户&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chown [–R] 属主名 文件名
chown [-R] 属主名:属组名 文件名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;chown root aaa：将文件aaa的属主更改成root&lt;/p&gt;
&lt;p&gt;chown seazean:seazean aaa：将文件aaa的属主和属组更改为seazean&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文件操作&lt;/h3&gt;
&lt;h4&gt;touch&lt;/h4&gt;
&lt;p&gt;touch 命令用于创建文件、修改文件或者目录的时间属性，包括存取时间和更改时间。若文件不存在，系统会建立一个新的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch [-acfm][-d&amp;lt;日期时间&amp;gt;][-r&amp;lt;参考文件或目录&amp;gt;] [-t&amp;lt;日期时间&amp;gt;][--help][--version][文件或目录…]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-a  改变档案的读取时间记录&lt;/li&gt;
&lt;li&gt;-m 改变档案的修改时间记录&lt;/li&gt;
&lt;li&gt;-c  假如目的档案不存在，不会建立新的档案。与 --no-create 的效果一样&lt;/li&gt;
&lt;li&gt;-f  不使用，是为了与其他 unix 系统的相容性而保留&lt;/li&gt;
&lt;li&gt;-r  使用参考档的时间记录，与 --file 的效果一样&lt;/li&gt;
&lt;li&gt;-d 设定时间与日期，可以使用各种不同的格式&lt;/li&gt;
&lt;li&gt;-t  设定档案的时间记录，格式与 date 指令相同&lt;/li&gt;
&lt;li&gt;--no-create 不会建立新档案&lt;/li&gt;
&lt;li&gt;--help 列出指令格式&lt;/li&gt;
&lt;li&gt;--version 列出版本讯息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;touch t.txt&lt;/code&gt;：创建 t.txt 文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;touch t{1..10}.txt&lt;/code&gt;：创建10 个名为 t1.txt 到 t10.txt 的空文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;touch t.txt&lt;/code&gt;：更改 t.txt 的访问时间为现在&lt;/p&gt;
&lt;h4&gt;stat&lt;/h4&gt;
&lt;p&gt;stat 命令用于显示 inode 内容&lt;/p&gt;
&lt;p&gt;命令：stat [文件或目录]&lt;/p&gt;
&lt;h4&gt;cat&lt;/h4&gt;
&lt;p&gt;cat 是一个文本文件查看和连接工具，&lt;strong&gt;用于小文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;命令：cat [-AbeEnstTuv] [--help] [--version] Filename&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-n  显示文件加上行号&lt;/li&gt;
&lt;li&gt;-b  和 -n 相似，只不过对于空白行不编号&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;less&lt;/h4&gt;
&lt;p&gt;less 用于查看文件，但是 less 在查看之前不会加载整个文件，&lt;strong&gt;用于大文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;命令：less [options] Filename&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-N  显示每行行号&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;tail&lt;/h4&gt;
&lt;p&gt;tail 命令可用于查看文件的内容，有一个常用的参数 &lt;strong&gt;-f&lt;/strong&gt; 常用于查阅正在改变的日志文件&lt;/p&gt;
&lt;p&gt;命令：tail  [options]  Filename&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-f  循环读取,动态显示文档的最后内容&lt;/li&gt;
&lt;li&gt;-n  显示文件的尾部 n 行内容&lt;/li&gt;
&lt;li&gt;-c 显示字节数&lt;/li&gt;
&lt;li&gt;-nf 查看最后几行日志信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;tail -f filename&lt;/code&gt;：动态显示最尾部的内容&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tail -n +2  txtfile.txt&lt;/code&gt;：显示文件 txtfile.txt 的内容，从第 2 行至文件末尾&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tail -n 2  txtfile.txt&lt;/code&gt;：显示文件 txtfile.txt 的内容，最后 2 行&lt;/p&gt;
&lt;h4&gt;head&lt;/h4&gt;
&lt;p&gt;head 命令可用于查看文件的开头部分的内容，有一个常用的参数 &lt;strong&gt;-n&lt;/strong&gt; 用于显示行数，默认为 10&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-q 隐藏文件名&lt;/li&gt;
&lt;li&gt;-v 显示文件名&lt;/li&gt;
&lt;li&gt;-c 显示的字节数&lt;/li&gt;
&lt;li&gt;-n 显示的行数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;head -n Filename&lt;/code&gt;：查看文件的前一部分&lt;/p&gt;
&lt;p&gt;&lt;code&gt;head -n 20 Filename&lt;/code&gt;：查看文件的前 20 行&lt;/p&gt;
&lt;h4&gt;grep&lt;/h4&gt;
&lt;p&gt;grep 指令用于查找内容包含指定的范本样式的文件，若不指定任何文件名称，或是所给予的文件名为 -，则 grep 指令会从标准输入设备读取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep [-abcEFGhHilLnqrsvVwxy][-A&amp;lt;显示列数&amp;gt;][-B&amp;lt;显示列数&amp;gt;][-C&amp;lt;显示列数&amp;gt;][-d&amp;lt;进行动作&amp;gt;][-e&amp;lt;范本样式&amp;gt;][-f&amp;lt;范本文件&amp;gt;][--help][范本样式][文件或目录...]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-c 只输出匹配行的计数&lt;/li&gt;
&lt;li&gt;-i 不区分大小写&lt;/li&gt;
&lt;li&gt;-h 查询多文件时不显示文件名&lt;/li&gt;
&lt;li&gt;-l 查询多文件时只输出包含匹配字符的文件名&lt;/li&gt;
&lt;li&gt;-n 显示匹配行及行号&lt;/li&gt;
&lt;li&gt;-s 不显示不存在或无匹配文本的错误信息&lt;/li&gt;
&lt;li&gt;-v 显示不包含匹配文本的所有行&lt;/li&gt;
&lt;li&gt;--color=auto 可以将找到的关键词部分加上颜色的显示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;管道符 |&lt;/strong&gt;：表示将前一个命令处理的结果传递给后面的命令处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grep aaaa Filename &lt;/code&gt;：显示存在关键字 aaaa 的行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -n aaaa Filename&lt;/code&gt;：显示存在关键字 aaaa 的行，且显示行号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -i aaaa Filename&lt;/code&gt;：忽略大小写，显示存在关键字 aaaa 的行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -v aaaa Filename&lt;/code&gt;：显示存在关键字 aaaa 的所有行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps -ef | grep  sshd&lt;/code&gt;：查找包含 sshd 进程的进程信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt; ps -ef | grep -c sshd&lt;/code&gt;：查找 sshd 相关的进程个数&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;echo&lt;/h4&gt;
&lt;p&gt;将字符串输出到控制台 ,  通常和重定向联合使用&lt;/p&gt;
&lt;p&gt;命令：echo string，如果字符串有空格, 为了避免歧义 请增加 双引号 或者 单引号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;命令 &amp;gt; 文件&lt;/code&gt;  将命令的成功结果覆盖指定文件内容&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;命令 &amp;gt;&amp;gt; 文件&lt;/code&gt;   将命令的成功结果追加指定文件的后面&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;命令 &amp;amp;&amp;gt;&amp;gt; 文件&lt;/code&gt; 将 命令的失败结果追加指定文件的后面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;echo &quot;程序员&quot; &amp;gt;&amp;gt; a.txt&lt;/code&gt;：将程序员追加到 a.txt 后面&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cat 不存在的目录 &amp;amp;&amp;gt;&amp;gt; error.log&lt;/code&gt;：将错误信息追加到 error.log 文件&lt;/p&gt;
&lt;h4&gt;awk&lt;/h4&gt;
&lt;p&gt;AWK 是一种处理文本文件的语言，是一个强大的文本分析工具&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;awk [options] &apos;script&apos; var=value file(s)
awk [options] -f scriptfile var=value file(s)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-F fs：指定输入文件折分隔符，fs 是一个字符串或者是一个正则表达式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-v：var=value 赋值一个用户定义变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-f：从脚本文件中读取 awk 命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$n：获取&lt;strong&gt;第几段&lt;/strong&gt;内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$0：获取&lt;strong&gt;当前行&lt;/strong&gt; 内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NF：表示当前行共有多少个字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$NF：代表最后一个字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$(NF-1)：代表倒数第二个字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NR：代表处理的是第几行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;命令：awk &apos;BEGIN{初始化操作}{每行都执行} END{结束时操作}&apos;   
文件名BEGIN{ 这里面放的是执行前的语句 }{这里面放的是处理每一行时要执行的语句}
END {这里面放的是处理完所有的行后要执行的语句 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//准备数据
zhangsan 68 99 26
lisi 98 66 96
wangwu 38 33 86
zhaoliu 78 44 36
maq 88 22 66
zhouba 98 44 46
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cat a.txt | awk  &apos;/zhang|li/&apos;&lt;/code&gt;：搜索含有 zhang  和 li 的学生成绩&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk &quot;/zhang|li/&quot; a.txt &lt;/code&gt;：同上一个命令，效果一样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zhangsan 68 99 26
lisi 98 66 96
zhaoliu 78 44 36
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cat a.txt | awk -F &apos; &apos; &apos;{print $1,$2,$3}&apos;&lt;/code&gt;：按照空格分割，打印 一二三列内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk -F &apos; &apos; &apos;{OFS=&quot;\t&quot;}{print $1,$2,$3}&apos;&lt;/code&gt;：按照制表符 tab 进行分割，打印一二三列
\b：退格      \f：换页      \n：换行      \r：回车      \t：制表符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zhangsan	68	99
lisi	98	66
wangwu	38	33
zhaoliu	78	44
maq	88	22
zhouba	98	44
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk -F &apos;,&apos; &apos;{print  toupper($1)}&apos; a.txt&lt;/code&gt;：根据逗号分割，打印内容，第一段大写&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;toupper()&lt;/td&gt;
&lt;td&gt;upper&lt;/td&gt;
&lt;td&gt;字符 转成 大写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tolower()&lt;/td&gt;
&lt;td&gt;lower&lt;/td&gt;
&lt;td&gt;字符 转成小写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;length()&lt;/td&gt;
&lt;td&gt;length&lt;/td&gt;
&lt;td&gt;返回 字符长度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk -F &apos; &apos; &apos;BEGIN{}{total=total+$4} END{print total}&apos; a.txt&lt;/code&gt;：计算的是第4列的总分&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk -F &apos; &apos; &apos;BEGIN{}{total=total+$4} END{print total, NR}&apos; a.txt&lt;/code&gt; ：查看总分, 总人数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;awk -F &apos; &apos; &apos;BEGIN{}{total=total+$4} END{print total, NR, (total/NR)}&apos; a.txt&lt;/code&gt;：查看总分, 总人数，平均数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cat a.txt | awk -F &apos; &apos; &apos;BEGIN{}{total=total+$4} END{print total}&apos; &lt;/code&gt;：可以这样写&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;find&lt;/h4&gt;
&lt;p&gt;find 命令用来在指定目录下查找文件，如果使用该命令不设置任何参数，将在当前目录下查找子目录与文件，并且将查找到的子目录和文件全部进行显示&lt;/p&gt;
&lt;p&gt;命令：find &amp;lt;指定目录&amp;gt; &amp;lt;指定条件&amp;gt; &amp;lt;指定内容&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find . -name &quot;*.gz&quot;&lt;/code&gt;：将目前目录及其子目录下所有延伸档名是 gz 的文件查询出来&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find . -ctime -1&lt;/code&gt;：将目前目录及其子目录下所有最近 1 天内更新过的文件查询出来&lt;/li&gt;
&lt;li&gt;&lt;code&gt; find / -name  &apos;seazean&apos;&lt;/code&gt;：全局搜索 seazean&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;read&lt;/h4&gt;
&lt;p&gt;read 命令用于从标准输入读取数值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sort&lt;/h4&gt;
&lt;p&gt;Linux sort 命令用于将文本文件内容加以排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort [-bcdfimMnr][文件]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-n 依照数值的大小排序&lt;/li&gt;
&lt;li&gt;-r 以相反的顺序来排序（sort 默认的排序方式是&lt;strong&gt;升序&lt;/strong&gt;，改成降序，加 -r）&lt;/li&gt;
&lt;li&gt;-u 去掉重复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试题：一列数字，输出最大的 4 个不重复的数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort -ur a.txt | head -n 4
sort -r a.txt | uniq |  head -n 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;uniq&lt;/h4&gt;
&lt;p&gt;uniq 用于重复数据处理，使用前先 sort 排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uniq [OPTION]... [INPUT [OUTPUT]]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-c 在数据行前出现的次数&lt;/li&gt;
&lt;li&gt;-d 只打印重复的行，重复的行只显示一次&lt;/li&gt;
&lt;li&gt;-D 只打印重复的行，重复的行出现多少次就显示多少次&lt;/li&gt;
&lt;li&gt;-f 忽略行首的几个字段&lt;/li&gt;
&lt;li&gt;-i 忽略大小写&lt;/li&gt;
&lt;li&gt;-s 忽略行首的几个字母&lt;/li&gt;
&lt;li&gt;-u 只打印唯一的行&lt;/li&gt;
&lt;li&gt;-w 比较不超过 n 个字母&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;文件压缩&lt;/h3&gt;
&lt;h4&gt;tar&lt;/h4&gt;
&lt;p&gt;tar 的主要功能是打包、压缩和解压文件，tar 本身不具有压缩功能，是调用压缩功能实现的。&lt;/p&gt;
&lt;p&gt;命令：tar  [必要参数]   [选择参数]   [文件]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-c   产生 .tar 文件&lt;/li&gt;
&lt;li&gt;-v   显示详细信息&lt;/li&gt;
&lt;li&gt;-z   打包同时压缩&lt;/li&gt;
&lt;li&gt;-f   指定压缩后的文件名&lt;/li&gt;
&lt;li&gt;-x   解压 .tar 文件&lt;/li&gt;
&lt;li&gt;-t   列出 tar 文件中包含的文件的信息&lt;/li&gt;
&lt;li&gt;-r   附加新的文件到tar文件中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;tar -cvf txt.tar txtfile.txt &lt;/code&gt;：将 txtfile.txt 文件打包（仅打包，不压缩）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tar -zcvf combine.tar.gz 1.txt 2.txt 3.txt&lt;/code&gt;：将 123.txt 文件打包压缩（gzip）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tar -ztvf txt.tar.gz&lt;/code&gt;：查看 tar 中有哪些文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tar -zxvf Filename -C 目标路径&lt;/code&gt;：解压&lt;/p&gt;
&lt;h4&gt;gzip&lt;/h4&gt;
&lt;p&gt;gzip命令用于压缩文件。&lt;/p&gt;
&lt;p&gt;gzip是个使用广泛的压缩程序，文件经它压缩过后，其名称后面会多出&quot;.gz&quot;的扩展名&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gzip * ：压缩目录下的所有文件，删除源文件。不支持直接压缩目录&lt;/li&gt;
&lt;li&gt;gzip -rv 目录名：递归压缩目录&lt;/li&gt;
&lt;li&gt;gzip -dv *：解压文件并列出详细信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;gunzip&lt;/h4&gt;
&lt;p&gt;gunzip命令用于解压文件。用于解开被gzip压缩过的文件&lt;/p&gt;
&lt;p&gt;命令：gunzip  [options]  [文件或者目录]&lt;/p&gt;
&lt;p&gt;gunzip 001.gz ：解压001.gz文件&lt;/p&gt;
&lt;h4&gt;zip&lt;/h4&gt;
&lt;p&gt;zip 命令用于压缩文件。&lt;/p&gt;
&lt;p&gt;zip 是个使用广泛的压缩程序，文件经它压缩后会另外产生具有 &lt;code&gt;.zip&lt;/code&gt; 扩展名的压缩文件&lt;/p&gt;
&lt;p&gt;命令：zip  [必要参数]  [选择参数]  [文件]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-q 不显示指令执行过程&lt;/li&gt;
&lt;li&gt;-r 递归处理，将指定目录下的所有文件和子目录一并处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;zip -q -r z.zip *&lt;/code&gt;：将该目录的文件全部压缩&lt;/p&gt;
&lt;h4&gt;unzip&lt;/h4&gt;
&lt;p&gt;unzip 命令用于解压缩 zip 文件，unzip 为 &lt;code&gt;.zip&lt;/code&gt; 压缩文件的解压缩程序&lt;/p&gt;
&lt;p&gt;命令：unzip  [必要参数]  [选择参数]  [文件]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-l  查看压缩文件内所包含的文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-d&amp;lt;目录&amp;gt; 指定文件解压缩后所要存储的目录。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;unzip -l z.zip&lt;/code&gt; ：查看压缩文件中包含的文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;unzip -d ./unFiles z.zip&lt;/code&gt;：把文件解压到指定的目录下&lt;/p&gt;
&lt;h4&gt;bzip2&lt;/h4&gt;
&lt;p&gt;bzip2 命令是 &lt;code&gt;.bz2&lt;/code&gt; 文件的压缩程序。&lt;/p&gt;
&lt;p&gt;bzip2 采用新的压缩演算法，压缩效果比传统的 LZ77/LZ78 压缩演算法好，若不加任何参数，bzip2 压缩完文件后会产生 .bz2 的压缩文件，并删除原始的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 压缩等级][要压缩的文件]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;压缩：bzip2 a.txt&lt;/p&gt;
&lt;h4&gt;bunzip2&lt;/h4&gt;
&lt;p&gt;bunzip2 命令是 &lt;code&gt;.bz2&lt;/code&gt; 文件的解压缩程序。&lt;/p&gt;
&lt;p&gt;命令：bunzip2  [-fkLsvV]  [.bz2压缩文件]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-v　解压缩文件时，显示详细的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解压：bunzip2 -v a.bz2&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文件编辑&lt;/h3&gt;
&lt;h4&gt;Vim&lt;/h4&gt;
&lt;p&gt;vim：是从 vi 发展出来的一个文本编辑器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令模式：在 Linux 终端中输入&lt;code&gt;vim 文件名&lt;/code&gt; 就进入了命令模式，但不能输入文字&lt;/li&gt;
&lt;li&gt;编辑模式：在命令模式下按 &lt;code&gt;i&lt;/code&gt; 就会进入编辑模式，此时可以写入程式，按 Esc 可回到命令模式&lt;/li&gt;
&lt;li&gt;末行模式：在命令模式下按 &lt;code&gt;:&lt;/code&gt; 进入末行模式，左下角会有一个冒号，可以敲入命令并执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;打开文件&lt;/h4&gt;
&lt;p&gt;Ubuntu 默认没有安装 vim，需要先安装 vim，安装命令：&lt;strong&gt;sudo apt-get install vim&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vim 有三种模式：命令模式（Command mode）、插入模式（Insert mode）、末行模式（Last Line mode）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vim 使用的选项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;常用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vim filename&lt;/td&gt;
&lt;td&gt;打开或新建一个文件，将光标置于第一行首部&lt;/td&gt;
&lt;td&gt;常用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim -r filename&lt;/td&gt;
&lt;td&gt;恢复上次vim打开时崩溃的文件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim -R filename&lt;/td&gt;
&lt;td&gt;把指定的文件以只读的方式放入Vim编辑器&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim + filename&lt;/td&gt;
&lt;td&gt;打开文件，将光标置于最后一行的首部&lt;/td&gt;
&lt;td&gt;常用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim +n filename&lt;/td&gt;
&lt;td&gt;打开文件，将光标置于n行的首部&lt;/td&gt;
&lt;td&gt;常用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim +/pattern filename&lt;/td&gt;
&lt;td&gt;打开文件，将光标置于第一个与pattern匹配的位置&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vim -c command filename&lt;/td&gt;
&lt;td&gt;对文件编辑前，先执行指定的命令&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;插入模式&lt;/h4&gt;
&lt;p&gt;在命令模式下，通过按下 i、I、a、A、o、O 这 6 个字母进入插入模式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;在光标所在位置插入文本，光标后的文本向右移动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;在光标所在行的行首插入文本，行首是该行的第一个非空白字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;在光标所在行的下面插入新的一行，光标停在空行首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;在光标所在行的上面插入新的一行，光标停在空行首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;在光标所在位置之后插入文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;在光标所在行的行尾插入文本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;按下 ESC 键，离开插入模式，进入命令模式&lt;/p&gt;
&lt;p&gt;因为我们是一个空文件，所以使用【I】或者【i】都可以&lt;/p&gt;
&lt;p&gt;如果里面的文本很多，要使用【A】进入编辑模式，即在行末添加文本&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;命令模式&lt;/h4&gt;
&lt;p&gt;Vim 打开一个文件（文件可以存在，也可以不存在），默认进入命令模式。在该模式下， 输入的字符会被当做指令，而不会被当做要输入的文字&lt;/p&gt;
&lt;h5&gt;移动光标&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;w&lt;/td&gt;
&lt;td&gt;光标移动至下一个单词的单词首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;光标移动至上一个单词的单词首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e&lt;/td&gt;
&lt;td&gt;光标移动至下一个单词的单词尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;光标移动至当前行的行首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^&lt;/td&gt;
&lt;td&gt;行首, 第一个不是空白字符的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;光标移动至当前行的行尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gg&lt;/td&gt;
&lt;td&gt;光标移动至文件开头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;光标移动至文件末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ngg&lt;/td&gt;
&lt;td&gt;光标移动至第n行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nG&lt;/td&gt;
&lt;td&gt;光标移动至第n行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:n&lt;/td&gt;
&lt;td&gt;光标移动至第n行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;选中文本&lt;/h5&gt;
&lt;p&gt;在 vi/vim 中要选择文本，需要显示 visual 命令切换到&lt;strong&gt;可视模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;vi/vim 中提供了三种可视模式，方便程序员的选择&lt;strong&gt;选中文本的方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按 ESC 可以放弃选中, 返回到&lt;strong&gt;命令模式&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v&lt;/td&gt;
&lt;td&gt;可视模式&lt;/td&gt;
&lt;td&gt;从光标位置开始按照正常模式选择文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;可视化模式&lt;/td&gt;
&lt;td&gt;选中光标经过的完整行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl + v&lt;/td&gt;
&lt;td&gt;可是块模式&lt;/td&gt;
&lt;td&gt;垂直方向选中文本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;撤销删除&lt;/h5&gt;
&lt;p&gt;在学习编辑命令之前,先要知道怎样撤销之前一次错误的编辑操作&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;英文&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u&lt;/td&gt;
&lt;td&gt;undo&lt;/td&gt;
&lt;td&gt;撤销上次的命令(ctrl + z)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl + r&lt;/td&gt;
&lt;td&gt;uredo&lt;/td&gt;
&lt;td&gt;恢复撤销的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;删除的内容此时并没有真正的被删除，在剪切板中，按下 p 键，可以将删除的内容粘贴回来&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;删除光标所在位置的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;d&lt;/td&gt;
&lt;td&gt;删除移动命令对应的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dd&lt;/td&gt;
&lt;td&gt;删除光标所在行的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;删除光标位置到行尾的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:n1,n2&lt;/td&gt;
&lt;td&gt;删除从 a1 到 a2 行的文本内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;删除命令可以和移动命令连用, 以下是常见的组合命令(扩展):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dw&lt;/td&gt;
&lt;td&gt;删除从光标位置到单词末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;d}&lt;/td&gt;
&lt;td&gt;删除从光标位置到段落末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dG&lt;/td&gt;
&lt;td&gt;删除光标所行到文件末尾的所有内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ndd&lt;/td&gt;
&lt;td&gt;删除当前行（包括此行）到后 n 行内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;复制粘贴&lt;/h5&gt;
&lt;p&gt;vim 中提供有一个 被复制文本的缓冲区&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复制命令会将选中的文字保存在缓冲区&lt;/li&gt;
&lt;li&gt;删除命令删除的文字会被保存在缓冲区&lt;/li&gt;
&lt;li&gt;在需要的位置，使用粘贴命令可以将缓冲对的文字插入到光标所在的位置&lt;/li&gt;
&lt;li&gt;vim 中的文本缓冲区只有一个，如果后续做过复制、剪切操作，之前缓冲区中的内容会被替换&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;td&gt;复制已选中的文本到剪切板&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yy&lt;/td&gt;
&lt;td&gt;将光标所在行复制到剪切板&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nyy&lt;/td&gt;
&lt;td&gt;复制从光标所在行到向下n行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p&lt;/td&gt;
&lt;td&gt;将剪切板中的内容粘贴到光标后&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;td&gt;将剪切板中的内容粘贴到光标前&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意：&lt;strong&gt;vim 中的文本缓冲区和系统的剪切板不是同一个&lt;/strong&gt;，在其他软件中使用 Ctrl + C 复制的内容，不能在 vim 中通过 &lt;code&gt;p&lt;/code&gt; 命令粘贴，可以在编辑模式下使用鼠标右键粘贴&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;查找替换&lt;/h5&gt;
&lt;p&gt;查找&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/abc&lt;/td&gt;
&lt;td&gt;从光标所在位置向后查找字符串 abc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/^abc&lt;/td&gt;
&lt;td&gt;查找以 abc 为行首的行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/abc$&lt;/td&gt;
&lt;td&gt;查找以 abc 为行尾的行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;?abc&lt;/td&gt;
&lt;td&gt;从光标所在位置向前查找字符串 abc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;向后查找当前光标所在单词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#&lt;/td&gt;
&lt;td&gt;向前查找当前光标所在单词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n&lt;/td&gt;
&lt;td&gt;查找下一个，向同一方向重复上次的查找指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;查找上一个，向相反方向重复上次的查找指令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;替换：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;工作模式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;r&lt;/td&gt;
&lt;td&gt;替换当前字符&lt;/td&gt;
&lt;td&gt;命令模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;替换当前行光标后的字符&lt;/td&gt;
&lt;td&gt;替换模式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;光标选中要替换的字符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R&lt;/code&gt; 命令可以进入替换模式，替换完成后，按下 ESC 可以回到命令模式&lt;/li&gt;
&lt;li&gt;替换命令的作用就是不用进入编辑模式，对文件进行轻量级的修改&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;末行模式&lt;/h4&gt;
&lt;p&gt;在命令模式下，按下 &lt;code&gt;:&lt;/code&gt; 键进入末行模式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:wq&lt;/td&gt;
&lt;td&gt;保存并退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:wq!&lt;/td&gt;
&lt;td&gt;保存并强制退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:q&lt;/td&gt;
&lt;td&gt;不保存且退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:q!&lt;/td&gt;
&lt;td&gt;不保存且强制退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:w&lt;/td&gt;
&lt;td&gt;保存但是不退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:w!&lt;/td&gt;
&lt;td&gt;强制保存但是不退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:w filename&lt;/td&gt;
&lt;td&gt;另存到 filename 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x!&lt;/td&gt;
&lt;td&gt;保存文本，退出保存但是不退出 Vim 编辑器，更通用的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZZ&lt;/td&gt;
&lt;td&gt;直接退出保存但是不退出 Vim 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:n&lt;/td&gt;
&lt;td&gt;光标移动至第 n 行行首&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;异常处理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 vim 异常退出, 在磁盘上可能会保存有 交换文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下次再使用 vim 编辑文件时，会看到以下屏幕信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim%E5%BC%82%E5%B8%B8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ls -a 一下，会看到隐藏的 .swp 文件，删除了此文件即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ln [-sf] source_filename dist_filename
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-s：默认是实体链接，加 -s 为符号链接&lt;/li&gt;
&lt;li&gt;-f：如果目标文件存在时，先删除目标文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/文件链接.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实体链接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在目录下创建一个条目，记录着文件名与 inode 编号，这个 inode 就是源文件的 inode&lt;/li&gt;
&lt;li&gt;删除任意一个条目，文件还是存在，只要引用数量不为 0&lt;/li&gt;
&lt;li&gt;不能跨越文件系统、不能对目录进行链接&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ln /etc/crontab .
ll
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;符号链接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;符号链接文件保存着源文件所在的绝对路径，在读取时会定位到源文件上，可以理解为 Windows 的快捷方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当源文件被删除了，链接文件就打不开了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录的是路径，所以可以为目录建立符号链接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -&amp;gt; /etc/crontab
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;进程管理&lt;/h2&gt;
&lt;h3&gt;查看进程&lt;/h3&gt;
&lt;p&gt;ps 指令：查看某个时间点的进程信息&lt;/p&gt;
&lt;p&gt;top 指令：实时显示进程信息&lt;/p&gt;
&lt;p&gt;pstree：查看进程树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pstree -A	#查看所有进程树
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;进程 ID&lt;/h3&gt;
&lt;p&gt;进程号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进程号为 0 的进程通常是调度进程，常常被称为交换进程（swapper），该进程是内核的一部分，它并不执行任何磁盘上的程序，因此也被称为系统进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程号为 1 是 init 进程，是一个守护进程，在自举过程结束时由内核调用，init 进程绝不会终止，是一个普通的用户进程，但是它以超级用户特权运行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;父进程 ID 为 0 的进程通常是内核进程，作为系统&lt;strong&gt;自举过程&lt;/strong&gt;的一部分而启动，init 进程是个例外，它的父进程是 0，但它是用户进程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主存 = RAM + BIOS 部分的 ROM&lt;/li&gt;
&lt;li&gt;DISK：存放 OS 和 Bootloader&lt;/li&gt;
&lt;li&gt;BIOS：基于 I/O 处理系统&lt;/li&gt;
&lt;li&gt;Bootloader：加载 OS，将 OS 放入内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自举程序存储在内存中 ROM，&lt;strong&gt;用来加载操作系统&lt;/strong&gt;，初始化 CPU、寄存器、内存等。CPU 的程序计数器指自举程序第一条指令，当计算机&lt;strong&gt;通电&lt;/strong&gt;，CPU 开始读取并执行自举程序，将操作系统（不是全部，只是启动计算机的那部分程序）装入 RAM 中，这个过程是自举过程。装入完成后程序计数器设置为 RAM 中操作系统的&lt;strong&gt;第一条指令&lt;/strong&gt;，接下来 CPU 将开始执行（启动）操作系统的指令&lt;/p&gt;
&lt;p&gt;存储在 ROM 中保留很小的自举装入程序，完整功能的自举程序保存在磁盘的启动块上，启动块位于磁盘的固定位，拥有启动分区的磁盘称为启动磁盘或系统磁盘（C 盘）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;进程状态&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;running or runnable (on run queue) 正在执行或者可执行，此时进程位于执行队列中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;uninterruptible sleep (usually I/O) 不可中断阻塞，通常为 IO 阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;interruptible sleep (waiting for an event to complete) 可中断阻塞，此时进程正在等待某个事件完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;zombie (terminated but not reaped by its parent) 僵死，进程已经终止但是尚未被其父进程获取信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;stopped (either by a job control signal or because it is being traced) 结束，进程既可以被作业控制信号结束，也可能是正在被追踪&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;孤儿进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个父进程退出，而它的一个或多个子进程还在运行，那么这些子进程将成为孤儿进程&lt;/li&gt;
&lt;li&gt;孤儿进程将被 init 进程所收养，并由 init 进程对它们完成状态收集工作，所以孤儿进程不会对系统造成危害&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;僵尸进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个子进程的进程描述符在子进程退出时不会释放，只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出，而父进程并没有调用 wait() 或 waitpid()，那么子进程的进程描述符仍然保存在系统中，这种进程称之为僵尸进程&lt;/li&gt;
&lt;li&gt;僵尸进程通过 ps 命令显示出来的状态为 Z（zombie）&lt;/li&gt;
&lt;li&gt;系统所能使用的进程号是有限的，产生大量僵尸进程，会导致系统没有可用的进程号而不能产生新的进程&lt;/li&gt;
&lt;li&gt;要消灭系统中大量的僵尸进程，只需要将其父进程杀死，此时僵尸进程就会变成孤儿进程，从而被 init 进程所收养，这样 init 进程就会释放所有的僵尸进程所占有的资源，从而结束僵尸进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;守护进程(daemon)是一类在后台运行的特殊进程，用于执行特定的系统任务。&lt;/li&gt;
&lt;li&gt;守护进程是&lt;strong&gt;脱离于终端&lt;/strong&gt;并且在后台运行的进程，脱离终端是为了避免在执行的过程中的信息在终端上显示，并且进程也不会被任何终端所产生的终端信息所打断&lt;/li&gt;
&lt;li&gt;很多守护进程在系统引导的时候启动，并且一直运行直到系统关闭；另一些只在需要的时候才启动，完成任务后就自动结束&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;状态改变&lt;/h3&gt;
&lt;h4&gt;SIGCHLD&lt;/h4&gt;
&lt;p&gt;当一个子进程改变了它的状态时（停止运行，继续运行或者退出），有两件事会发生在父进程中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;得到 SIGCHLD 信号&lt;/li&gt;
&lt;li&gt;waitpid() 或者 wait() 调用会返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;子进程发送的 SIGCHLD 信号包含了子进程的信息，比如进程 ID、进程状态、进程使用 CPU 的时间等；在子进程退出时进程描述符不会立即释放，父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息，释放子进程的 PCB&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;wait&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pid_t wait(int *status)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：status 用来保存被收集的子进程退出时的状态，如果不关心子进程&lt;strong&gt;如何&lt;/strong&gt;销毁，可以设置这个参数为 NULL&lt;/p&gt;
&lt;p&gt;父进程调用 wait() 会阻塞等待，直到收到一个子进程退出的 SIGCHLD 信号，wait() 函数就会销毁子进程并返回&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成功，返回被收集的子进程的进程 ID&lt;/li&gt;
&lt;li&gt;失败，返回 -1，同时 errno 被置为 ECHILD（如果调用进程没有子进程，调用就会失败）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;waitpid&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pid_t waitpid(pid_t pid, int *status, int options)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用和 wait() 完全相同，只是多了两个可控制的参数 pid 和 options&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pid：指示一个子进程的 ID，表示只关心这个子进程退出的 SIGCHLD 信号；如果 pid=-1 时，那么和 wait() 作用相同，都是关注所有子进程退出的 SIGCHLD 信号&lt;/li&gt;
&lt;li&gt;options：主要有 WNOHANG 和 WUNTRACED 两个，WNOHANG 可以使 waitpid() 调用变成非阻塞的，就是会立即返回，父进程可以继续执行其它任务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;网络管理&lt;/h2&gt;
&lt;h3&gt;network&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动：service network start&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止：service network stop&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启：service network restart&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ifconfig&lt;/h3&gt;
&lt;p&gt;ifconfig 是 Linux 中用于显示或配置网络设备的命令，英文全称是 network interfaces configuring&lt;/p&gt;
&lt;p&gt;ifconfig 命令用于显示或设置网络设备。ifconfig 可设置网络设备的状态，或是显示目前的设置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig [网络设备][down up -allmulti -arp -promisc][add&amp;lt;地址&amp;gt;][del&amp;lt;地址&amp;gt;][&amp;lt;hw&amp;lt;网络设备类型&amp;gt;&amp;lt;硬件地址&amp;gt;][io_addr&amp;lt;I/O地址&amp;gt;][irq&amp;lt;IRQ地址&amp;gt;][media&amp;lt;网络媒介类型&amp;gt;][mem_start&amp;lt;内存地址&amp;gt;][metric&amp;lt;数目&amp;gt;][mtu&amp;lt;字节&amp;gt;][netmask&amp;lt;子网掩码&amp;gt;][tunnel&amp;lt;地址&amp;gt;][-broadcast&amp;lt;地址&amp;gt;][-pointopoint&amp;lt;地址&amp;gt;][IP地址]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ifconfig&lt;/code&gt;：显示激活的网卡信息  ens
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/网卡信息.png&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;ens33（或 eth0）表示第一块网卡，IP地址是 192.168.0.137，广播地址 broadcast 192.168.0.255，掩码地址netmask 255.255.255.0 ，inet6 对应的是 ipv6&lt;/p&gt;
&lt;p&gt;lo 是表示主机的&lt;strong&gt;回坏地址&lt;/strong&gt;，用来测试一个网络程序，但又不想让局域网或外网的用户能够查看，只能在此台主机上运行和查看所用的网络接口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ifconfig ens33 down：关闭网卡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ifconfig ens33 up：启用网卡&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ping&lt;/h3&gt;
&lt;p&gt;ping 命令用于检测主机&lt;/p&gt;
&lt;p&gt;执行 ping 指令会使用 ICMP 传输协议，发出要求回应的信息，若远端主机的网络功能没有问题，就会回应该信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ping [-dfnqrRv][-c&amp;lt;完成次数&amp;gt;][-i&amp;lt;间隔秒数&amp;gt;][-I&amp;lt;网络界面&amp;gt;][-l&amp;lt;前置载入&amp;gt;][-p&amp;lt;范本样式&amp;gt;][-s&amp;lt;数据包大小&amp;gt;][-t&amp;lt;存活数值&amp;gt;][主机名称或IP地址]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-c&amp;lt;完成次数&amp;gt;：设置完成要求回应的次数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ping -c 2 www.baidu.com&lt;/code&gt;
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/ping%E7%99%BE%E5%BA%A6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;icmp_seq：ping 序列，从1开始&lt;/p&gt;
&lt;p&gt;ttl：IP 生存时间值&lt;/p&gt;
&lt;p&gt;time：响应时间,数值越小，联通速度越快&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;netstat&lt;/h3&gt;
&lt;p&gt;netstat 命令用于显示网络状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netstat [-acCeFghilMnNoprstuvVwx][-A&amp;lt;网络类型&amp;gt;][--ip]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-a   显示所有连线中的 Socket，显示详细的连接状况&lt;/li&gt;
&lt;li&gt;-i    显示网络界面信息表单，显示网卡列表&lt;/li&gt;
&lt;li&gt;-p  显示正在使用 Socket 的程序识别码和程序名称&lt;/li&gt;
&lt;li&gt;-n  显示使用 IP 地址，而不通过域名服务器&lt;/li&gt;
&lt;li&gt;-t   显示 TCP 传输协议的连线状况。&lt;/li&gt;
&lt;li&gt;-u  显示 UDP 传输协议的连线状况&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-aptn：查看所有 TCP 开启端口&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-apun：查看所有 UDP 开启端口&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;netstat -apn | grep port：查看指定端口号&lt;/li&gt;
&lt;li&gt;lsof -i:port ：查看指定端口号&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;磁盘管理&lt;/h2&gt;
&lt;h3&gt;挂载概念&lt;/h3&gt;
&lt;p&gt;在安装 Linux 系统时设立的各个分区，如根分区、/boot 分区等都是自动挂载的，也就是说不需要人为操作，开机就会自动挂载。但是光盘、U 盘等存储设备如果需要使用，就必须人为的进行挂载&lt;/p&gt;
&lt;p&gt;在 Windows 下插入 U 盘也是需要挂载（分配盘符）的，只不过 Windows 下分配盘符是自动的。其实挂载可以理解为 Windows 当中的分配盘符，只不过 Windows 当中是以英文字母 ABCD 等作为盘符，而 Linux 是拿系统目录作为盘符，当然 Linux 当中也不叫盘符，而是称为挂载点，而把为分区或者光盘等存储设备分配一个挂载点的过程称为挂载&lt;/p&gt;
&lt;p&gt;Linux 中的根目录以外的文件要想被访问，需要将其关联到根目录下的某个目录来实现，这种关联操作就是挂载，这个目录就是挂载点，解除次关联关系的过程称之为卸载&lt;/p&gt;
&lt;p&gt;挂载点的目录需要以下几个要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目录要先存在，可以用 mkdir 命令新建目录&lt;/li&gt;
&lt;li&gt;挂载点目录不可被其他进程使用到&lt;/li&gt;
&lt;li&gt;挂载点下原有文件将被隐藏&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;lsblk&lt;/h3&gt;
&lt;p&gt;lsblk 命令的英文是 list block，即用于列出所有可用块设备的信息，而且还能显示他们之间的依赖关系，但是不会列出 RAM 盘的信息&lt;/p&gt;
&lt;p&gt;命令：lsblk [参数]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lsblk&lt;/code&gt;：以树状列出所有块设备
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E5%8F%AF%E7%94%A8%E5%9D%97%E8%AE%BE%E5%A4%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;NAME：这是块设备名&lt;/p&gt;
&lt;p&gt;MAJ：MIN : 本栏显示主要和次要设备号&lt;/p&gt;
&lt;p&gt;RM：本栏显示设备是否可移动设备，在上面设备 sr0 的 RM 值等于 1，这说明他们是可移动设备&lt;/p&gt;
&lt;p&gt;SIZE：本栏列出设备的容量大小信息&lt;/p&gt;
&lt;p&gt;RO：该项表明设备是否为只读，在本案例中，所有设备的 RO 值为 0，表明他们不是只读的&lt;/p&gt;
&lt;p&gt;TYPE：本栏显示块设备是否是磁盘或磁盘上的一个分区。在本例中，sda 和 sdb 是磁盘，而 sr0 是只读存储（rom）。&lt;/p&gt;
&lt;p&gt;MOUNTPOINT：本栏指出设备挂载的挂载点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lsblk -f&lt;/code&gt;：不会列出所有空设备
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E4%B8%8D%E5%8C%85%E5%90%AB%E7%A9%BA%E8%AE%BE%E5%A4%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;NAME表示设备名称&lt;/p&gt;
&lt;p&gt;FSTYPE表示文件类型&lt;/p&gt;
&lt;p&gt;LABEL表示设备标签&lt;/p&gt;
&lt;p&gt;UUID设备编号&lt;/p&gt;
&lt;p&gt;MOUNTPOINT表示设备的挂载点&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;df&lt;/h3&gt;
&lt;p&gt;df 命令用于显示目前在 Linux 系统上的文件系统的磁盘使用情况统计。&lt;/p&gt;
&lt;p&gt;命令：df [options]... [FILE]...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-h 使用人类可读的格式(预设值是不加这个选项的...)&lt;/li&gt;
&lt;li&gt;--total 计算所有的数据之和&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E7%A3%81%E7%9B%98%E7%AE%A1%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一列指定文件系统的名称；第二列指定一个特定的文件系统，1K 是 1024 字节为单位的总容量；已用和可用列分别指定的容量；最后一个已用列指定使用的容量的百分比；最后一栏指定的文件系统的挂载点&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;mount&lt;/h3&gt;
&lt;p&gt;mount 命令是经常会使用到的命令，它用于挂载 Linux 系统外的文件&lt;/p&gt;
&lt;p&gt;使用者权限：所有用户，设置级别的需要管理员&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount [-hV]
mount -a [-fFnrsvw] [-t vfstype]
mount [-fnrsvw] [-o options [,...]] device | dir
mount [-fnrsvw] [-t vfstype] [-o options] device dir
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-t：指定档案系统的型态，通常不必指定。mount 会自动选择正确的型态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过挂载的方式查看 Linux CD/DVD 光驱，查看 ubuntu-20.04.1-desktop-amd64.iso 的文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进入【虚拟机】--【设置】，设置 CD/DVD 的内容，ubuntu-20.04.1-desktop-amd64.iso&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建挂载点（注意：一般用户无法挂载 cdrom，只有 root 用户才可以操作）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mkdir -p /mnt/cdrom &lt;/code&gt;：切换到 root 下创建一个挂载点（其实就是创建一个目录）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开始挂载
&lt;code&gt;mount -t auto /dev/cdrom /mnt/cdrom&lt;/code&gt;：通过挂载点的方式查看上面的【ISO文件内容】
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/%E6%8C%82%E8%BD%BD%E6%88%90%E5%8A%9F.png&quot; alt=&quot;挂载成功&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看挂载内容：&lt;code&gt;ls -l -a ./mnt/cdrom/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;卸载 cdrom：&lt;code&gt;umount /mnt/cdrom/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;防火墙&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备，帮助计算机网络于其内、外网之间构建一道相对隔绝的保护屏障，以保护用户资料与信息安全性的一种技术。在默认情况下，Linux 系统的防火墙状态是打开的&lt;/p&gt;
&lt;h3&gt;状态&lt;/h3&gt;
&lt;p&gt;启动语法：service  name status&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看防火墙状态：&lt;code&gt;service iptables status&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;临时开启：&lt;code&gt;service iptables start&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;临时关闭：&lt;code&gt;service iptables stop&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开机启动：&lt;code&gt;chkconfig iptables on&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开机关闭：&lt;code&gt;chkconfig iptables off&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;放行&lt;/h3&gt;
&lt;p&gt;设置端口防火墙放行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改配置文件：&lt;code&gt;vim /etc/sysconfig/iptables&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;添加放行端口：&lt;code&gt;-A INPUT -m state --state NEW -m tcp -p tcp --dport 端口号 -j ACCEPT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重新加载防火墙规则：&lt;code&gt;service iptables reload&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;备注：默认情况下 22 端口号是放行的&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Shell&lt;/h2&gt;
&lt;h3&gt;入门&lt;/h3&gt;
&lt;h4&gt;概念&lt;/h4&gt;
&lt;p&gt;Shell 脚本（shell script），是一种为 shell 编写的脚本程序，又称 Shell 命令稿、程序化脚本，是一种计算机程序使用的文本文件，内容由一连串的 shell 命令组成，经由 Unix Shell 直译其内容后运作&lt;/p&gt;
&lt;p&gt;Shell 被当成是一种脚本语言来设计，其运作方式与解释型语言相当，由 Unix shell 扮演命令行解释器的角色，在读取 shell 脚本之后，依序运行其中的 shell 命令，之后输出结果&lt;/p&gt;
&lt;h4&gt;环境&lt;/h4&gt;
&lt;p&gt;Shell 编程跟 JavaScript、php 编程一样，只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cat /etc/shells&lt;/code&gt;：查看解释器
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/shell%E7%8E%AF%E5%A2%83.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Linux 的 Shell 种类众多，常见的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bourne Shell（/usr/bin/sh或/bin/sh）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bourne Again Shell（/bin/bash）：Bash 是大多数Linux 系统默认的 Shell&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C Shell（/usr/bin/csh）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;K Shell（/usr/bin/ksh）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shell for Root（/sbin/sh）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等等……&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第一个shell&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建 s.sh 文件：touch s.sh&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑 s.sh 文件：vim s.sh&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash  --- 指定脚本解释器
echo &quot;你好，shell !&quot;   ---向窗口输入文本

:&amp;lt;&amp;lt;!
写shell的习惯 第一行指定解释器
文件是sh为后缀名
括号成对书写
注释的时候尽量不用中文注释。不友好。
[] 括号两端要要有空格。  [ neirong ]
习惯代码索引，增加阅读性
写语句的时候，尽量写全了，比如if。。。
!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看 s.sh文件：ls -l    s.sh文件权限是【-rw-rw-r--】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;chmod a+x s.sh         s.sh文件权限是【-rwxrwxr-x】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行文件：./s.sh&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或者直接  &lt;code&gt;bash s.sh&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#!&lt;/strong&gt; 是一个约定的标记，告诉系统这个脚本需要什么解释器来执行，即使用哪一种 Shell&lt;/p&gt;
&lt;p&gt;echo 命令用于向窗口输出文本&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单行注释：以 &lt;strong&gt;#&lt;/strong&gt; 开头的行就是注释，会被解释器忽略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多行注释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:&amp;lt;&amp;lt;EOF
注释内容...
注释内容...
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;:&amp;lt;&amp;lt;!      -----这里的符号要和结尾处的一样
注释内容...
注释内容...
注释内容...
!        
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;变量&lt;/h3&gt;
&lt;h4&gt;定义变量&lt;/h4&gt;
&lt;p&gt;变量名和等号之间不能有空格，这可能和你熟悉的所有编程语言都不一样。同时，变量名的命名须遵循如下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命名只能使用英文字母，数字和下划线，首个字符不能以数字开头。&lt;/li&gt;
&lt;li&gt;中间不能有空格，可以使用下划线（_）。&lt;/li&gt;
&lt;li&gt;不能使用标点符号。&lt;/li&gt;
&lt;li&gt;不能使用bash里的关键字（可用help命令查看保留关键字）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;使用变量&lt;/h4&gt;
&lt;p&gt;使用一个定义过的变量，只要在变量名前面加美元符号$即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name=&quot;seazean&quot;
echo $name
echo ${name}
name=&quot;zhy&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;已定义的变量，可以被重新定义变量名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;外面的花括号是可选的，加不加都行，加花括号是为了帮助解释器识别变量的边界。推荐加！！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;比如：echo &quot;I am good at ${shell-t}Script&quot;
通过上面的脚本我们发现，如果不给shell-t变量加花括号，写成echo &quot;I am good at $shell-tScript&quot;，解释器shell就会把$shell-tScript当成一个变量，由于我们前面没有定义shell-t变量，那么解释器执行执行的结果自然就为空了。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;只读变量&lt;/h4&gt;
&lt;p&gt;使用 readonly 命令可以将变量定义为只读变量，只读变量的值不能被改变。(类似于final)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
myUrl=&quot;https://www.baidu.com&quot;
readonly myUrl
myUrl=&quot;https://cn.bing.com/&quot;  
#报错 myUrl readonly
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除变量&lt;/h4&gt;
&lt;p&gt;使用 unset 命令可以删除变量，变量被删除后不能再次使用。&lt;/p&gt;
&lt;p&gt;语法：&lt;code&gt;unset variable_name&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
myUrl=&quot;https://www.baidu.com&quot;
unset myUrl
echo $myUrl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义myUrl变量，通过unset删除变量，然后通过echo进行输出，&lt;strong&gt;结果是为空&lt;/strong&gt;，没有任何的结果输出。&lt;/p&gt;
&lt;h4&gt;字符变量&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;字符串是shell编程中最常用也是最有用的数据类型，字符串可以用单引号，也可以用双引号，也可以不用引号，在Java SE中我们定义一个字符串通过Stirng  s=“abc&quot; 双引号的形式进行定义，而在shell中也是可以的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;引号&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;单引号&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;str=&apos;this is a string variable&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单引号字符串的限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单引号里的任何字符都会原样输出，单引号字符串中的&lt;strong&gt;变量是无效的&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;单引号字串中不能出现单独一个的单引号（对单引号使用转义符后也不行），但可成对出现，作为字符串拼接使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双引号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;your_name=&apos;frank&apos;
str=&quot;Hello,\&quot;$your_name\&quot;! \n&quot;
echo -e $str     #Hello, &quot;frank&quot;!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双引号的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双引号里可以有变量&lt;/li&gt;
&lt;li&gt;双引号里可以出现转义字符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;拼接字符串&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;your_name=&quot;frank&quot;
# 使用双引号拼接
greeting=&quot;hello, &quot;$your_name&quot; !&quot;
greeting_1=&quot;hello, ${your_name} !&quot;
echo $greeting  $greeting_1
#hello,frank! hello,frank
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;获取字符串长度&lt;/h5&gt;
&lt;p&gt;命令：&lt;code&gt;${#variable_name}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string=&quot;seazean&quot;
echo ${#string} #7
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;提取字符串&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;string=&quot;abcdefghijklmn&quot;
echo ${string:1:4} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为【bcde】，通过截取我们发现，它的下标和我们在java中的读取方式是一样的，下标也是从0开始。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;bash支持一维数组（不支持多维数组），并且没有限定数组的大小。&lt;/p&gt;
&lt;h4&gt;定义数组&lt;/h4&gt;
&lt;p&gt;在 Shell 中，用括号来表示数组，数组元素用&quot;空格&quot;符号分割开&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数组名=(值1 值2 ... 值n)
array_name=(value0 value1 value2 value3) 
array_name=(
value0
value1
value2
value3
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下标定义数组中的其中一个元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以不使用连续的下标，而且下标的范围没有限制&lt;/p&gt;
&lt;h4&gt;读取数组&lt;/h4&gt;
&lt;p&gt;读取数组元素值的一般格式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${数组名[下标]}

value=${array_name[n]}
echo ${value}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;strong&gt;@&lt;/strong&gt; 符号可以获取数组中的所有元素，例如：&lt;code&gt;echo ${array_name[@]}&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;获取长度&lt;/h4&gt;
&lt;p&gt;获取数组长度的方法与获取字符串长度的方法相同，数组前加#&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#! /bin/bash
g=(a b c d e f)
echo &quot;数组下标为2的数据为:&quot; ${g[2]}  #c
echo  &quot;数组所有数据为:&quot;  ${#g[@]}   #6
echo  &quot;数组所有数据为:&quot;   ${#g[*]}  #6
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;运算符&lt;/h3&gt;
&lt;p&gt;Shell 和其他编程一样，&lt;strong&gt;支持&lt;/strong&gt;包括：算术、关系、布尔、字符串等运算符。原生 bash **不支持 **简单的数学运算，但是可以通过其他命令来实现，例如expr。expr 是一款表达式计算工具，使用它能完成表达式的求值操作。&lt;/p&gt;
&lt;h4&gt;规则&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;表达式和运算符之间要有空格&lt;/strong&gt;，例如 2+2 是不对的，必须写成 2 + 2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完整的表达式要被 `` 包含，注意不是单引号&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件表达式要放在方括号之间，并且要有空格&lt;/strong&gt;，例如: &lt;code&gt;[$a==$b]&lt;/code&gt; 是错误的，必须写成 &lt;code&gt;[ $a == $b ]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(())双括号里可以跟表达式&lt;/strong&gt;，例如((i++))，((a+b))&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;算术运算符&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;运算符&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;举例&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;加法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expr $a + $b&lt;/code&gt; 结果为 30。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;减法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expr $a - $b&lt;/code&gt; 结果为 -10。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;乘法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expr $a \* $b&lt;/code&gt; 结果为  200。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;除法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expr $b / $a&lt;/code&gt; 结果为 2。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%&lt;/td&gt;
&lt;td&gt;取余&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expr $b % $a&lt;/code&gt; 结果为 0。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;赋值&lt;/td&gt;
&lt;td&gt;a=$b 将把变量 b 的值赋给 a。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;==&lt;/td&gt;
&lt;td&gt;相等。用于比较两个数字，相同则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a == $b ] &lt;/code&gt;返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!=&lt;/td&gt;
&lt;td&gt;不相等。用于比较两个数字，不相同则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a != $b ] &lt;/code&gt;返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;#! /bin/bash
a=4
b=20
echo &quot;加法运算&quot;  `expr $a + $b` 
echo &quot;乘法运算，注意*号前面需要反斜杠&quot; ` expr $a \* $b`
echo &quot;加法运算&quot;  `expr  $b / $a`
((a++))
echo &quot;a = $a&quot;
c=$((a + b)) 
d=$[a + b]
echo &quot;c = $c&quot;
echo &quot;d = $d&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//结果
加法运算 24
减法运算 -16
乘法运算，注意*号前面需要反斜杠 80
加法运算 5
a = 5
c = 25
d = 25
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字符运算符&lt;/h4&gt;
&lt;p&gt;假定变量 a 为 &quot;abc&quot;，变量 b 为 &quot;efg&quot;，true=0，false=1。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;举例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;检测两个字符串是否相等，相等返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a = $b ]&lt;/code&gt; 返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!=&lt;/td&gt;
&lt;td&gt;检测两个字符串是否相等，不相等返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a != $b ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-z&lt;/td&gt;
&lt;td&gt;检测字符串长度是否为0，为0返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ -z $a ]&lt;/code&gt; 返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-n&lt;/td&gt;
&lt;td&gt;检测字符串长度是否为0，不为0返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ -n &quot;$a&quot; ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;检测字符串是否为空，不为空返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;a=&quot;abc&quot;
b=&quot;efg&quot;

if [ $a = $b ]
then
   echo &quot;$a = $b : a 等于 b&quot;
else
   echo &quot;$a = $b: a 不等于 b&quot;
fi
if [ $a != $b ]
then
   echo &quot;$a != $b : a 不等于 b&quot;
else
   echo &quot;$a != $b: a 等于 b&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;关系运算符&lt;/h4&gt;
&lt;p&gt;关系运算符只支持数字，不支持字符串，除非字符串的值是数字。&lt;/p&gt;
&lt;p&gt;下表列出了常用的关系运算符，假定变量 a 为 10，变量 b 为 20：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;举例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-eq&lt;/td&gt;
&lt;td&gt;检测两个数是否相等，相等返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -eq $b ]&lt;/code&gt; 返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ne&lt;/td&gt;
&lt;td&gt;检测两个数是否不相等，不相等返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -ne $b ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gt&lt;/td&gt;
&lt;td&gt;检测左边的数是否大于右边的，如果是，则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -gt $b ]&lt;/code&gt; 返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-lt&lt;/td&gt;
&lt;td&gt;检测左边的数是否小于右边的，如果是，则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -lt $b ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ge&lt;/td&gt;
&lt;td&gt;检测左边的数是否大于等于右边的，如果是，则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -ge $b ]&lt;/code&gt; 返回 false。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-le&lt;/td&gt;
&lt;td&gt;检测左边的数是否小于等于右边的，如果是，则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -le $b ]&lt;/code&gt; 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;a=10
b=20

if [ $a -eq $b ]
then
   echo &quot;$a -eq $b : a 等于 b&quot;
else
   echo &quot;$a -eq $b: a 不等于 b&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;布尔运算符&lt;/h4&gt;
&lt;p&gt;下表列出了常用的布尔运算符，假定变量 a 为 10，变量 b 为 20：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;举例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;!&lt;/td&gt;
&lt;td&gt;非运算，表达式为 true 则返回 false，否则返回 true。&lt;/td&gt;
&lt;td&gt;[ ! false ] 返回 true。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-o&lt;/td&gt;
&lt;td&gt;或运算，有一个表达式为 true 则返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -lt 20 -o $b -gt 100 ]&lt;/code&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-a&lt;/td&gt;
&lt;td&gt;与运算，两个表达式都为 true 才返回 true。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[ $a -lt 20 -a $b -gt 100 ]&lt;/code&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;逻辑运算符&lt;/h4&gt;
&lt;p&gt;假定变量 a 为 10，变量 b 为 20:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;举例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;逻辑的 AND&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ $a -lt 100 &amp;amp;&amp;amp; $b -gt 100 ]]&lt;/code&gt; 返回 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;||&lt;/td&gt;
&lt;td&gt;逻辑的 OR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[[ $a -lt 100 || $b -gt 100 ]]&lt;/code&gt; 返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;流程控制&lt;/h3&gt;
&lt;h4&gt;if&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;if condition
then
    command1 
    command2
    ...
    commandN 
fi
#末尾的fi就是if倒过来拼写
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;if condition
then
    command1 
    command2
    ...
    commandN
else
    command
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;if condition1
then
    command1
elif condition2 
then 
    command2
else
    commandN
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查找一个进程，如果进程存在就打印true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if [ $(ps -ef | grep -c &quot;ssh&quot;) -gt 1 ]]
then 
	echo &quot;true&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断两个变量是否相等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a=10
b=20
if [ $a == $b ]
then
   echo &quot;a 等于 b&quot;
elif [ $a -gt $b ]
then
   echo &quot;a 大于 b&quot;
elif [ $a -lt $b ]
then
   echo &quot;a 小于 b&quot;
else
   echo &quot;没有符合的条件&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;for&lt;/h4&gt;
&lt;p&gt;for循环格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺序输出当前列表中的字母：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for loop in A B C D E F G 
do
    echo &quot;顺序输出字母为: $loop&quot;
done

顺序输出字母为:A
顺序输出字母为:B
....
顺序输出字母为:G
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;while&lt;/h4&gt;
&lt;p&gt;while循环用于不断执行一系列命令，也用于从输入文件中读取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while condition
do
    command
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需求：如果int小于等于10，那么条件返回真。int从0开始，每次循环处理时，int加1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
a=1
while [ &quot;${a}&quot; -le 10 ]
do
    echo &quot;输出的值为：&quot; $a
    ((a++))
done
输出的值为：1
输出的值为：2
...
输出的值为：10
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;case...esac&lt;/h4&gt;
&lt;p&gt;与 switch ... case 语句类似，是一种多分枝选择结构，每个 case 分支用右圆括号开始，用两个分号 &lt;strong&gt;;;&lt;/strong&gt; 表示 break，即执行结束，跳出整个 case ... esac 语句，esac（就是 case 反过来）作为结束标记。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case 值 in  
模式1)
    command1
    command2
    command3
    ;;
模式2）
    command1
    command2
    command3
    ;;
*)
    command1
    command2
    command3
    ;;
esac  #case反过来
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;case 后为取值，值可以为变量或常数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;值后为关键字 in，接下来是匹配的各种模式，每一模式最后必须以右括号结束，模式支持正则表达式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;v=&quot;czbk&quot;

case &quot;$v&quot; in
&quot;czbk&quot;) 
	echo &quot;传智播客&quot;
   	;;
&quot;baidu&quot;) 
	echo &quot;baidu 搜索&quot;
	;;
&quot;google&quot;) 
	echo &quot;google 搜索&quot;
   	;;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;函数&lt;/h3&gt;
&lt;h4&gt;输入&lt;/h4&gt;
&lt;p&gt;函数语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ function ] funname [()]
{
    action;
    [return int;]

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1、可以使用function fun() 定义函数，也可以直接fun() 定义,不带任何参数。&lt;/li&gt;
&lt;li&gt;2、函数参数返回，可以显示加：return 返回，如果不加，将以最后一条命令运行结果，作为返回值。 return后跟数值n(0-255&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#无参无返回值的方法
method(){
	echo &quot;函数执行了!&quot;
}

#方法的调用
#method



#有参无返回值的方法
method2(){
	echo &quot;接收到的第一个参数$1&quot;
	echo &quot;接收到的第二个参数$2&quot;
}

#方法的调用
#method2 1 2

#有参有返回值方法的定义
method3(){
	echo &quot;接收到的第一个参数$1&quot;
	echo &quot;接收到的第二个参数$2&quot;
	return $(($1 + $2))
}

#方法的调用
method3 10 20
echo $?
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读取&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;read 变量名&lt;/code&gt; --- 表示把键盘录入的数据复制给这个变量&lt;/p&gt;
&lt;p&gt;需求：在方法中键盘录入两个整数,返回这两个整数的和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;method(){
	echo &quot;请录入第一个数&quot;
	read number1
	echo &quot;请录入第二个数&quot;
	read number2
	echo &quot;两个数字分别为${number1},${number2}&quot;
	return $((number1+number2))
}

method
echo $?
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Docker&lt;/h1&gt;
&lt;h2&gt;基本概述&lt;/h2&gt;
&lt;p&gt;Docker 是一个开源的应用容器引擎，诞生于 2013 年初，基于 Go 语言实现， dotCloud 公司出品&lt;/p&gt;
&lt;p&gt;Docker 让开发者打包开发应用以及依赖包到一个轻量级、可移植的容器中，可以发布到任何Linux机器上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容器是完全使用沙箱机制，相互隔离&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器性能开销极低。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker 架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**镜像（Image）：**Docker 镜像，就相当于一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;容器（Container）&lt;/strong&gt;：镜像（Image）和容器（Container）的关系，就像是面向对象程序设计中的类和对象一样，镜像是静态的定义，容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;仓库（Repository）&lt;/strong&gt;：仓库可看成一个代码控制中心，用来保存镜像&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-docker%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;安装步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# step 1: 安装必要的一些系统工具
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
# step 2: 安装GPG证书
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# Step 3: 写入软件源信息
sudo add-apt-repository &quot;deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable&quot;
# Step 4: 更新并安装Docker-CE
sudo apt-get -y update
sudo apt-get -y install docker-ce
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置镜像加速器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json &amp;lt;&amp;lt;-&apos;EOF&apos;
{
  &quot;registry-mirrors&quot;: [&quot;https://hicqe4pi.mirror.aliyuncs.com&quot;]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;操作命令&lt;/h2&gt;
&lt;h3&gt;进程相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动docker服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止docker服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启doker服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看doker服务状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置开机启动docker服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;镜像相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看镜像：查看本地所有的镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
docker images –q # 查看所用镜像的id
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搜索镜像：从网络中查找需要的镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search 镜像名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拉取镜像：从Docker仓库下载镜像到本地，镜像名称格式为 名称:版本号，如果版本号不指定则是最新的版本。如果不知道镜像版本，可以去docker hub 搜索对应镜像查看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull 镜像名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除镜像：删除本地镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rmi 镜像id # 删除指定本地镜像
docker rmi `docker images -q`  # 删除所有本地镜像 tab上面的键
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容器相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps # 查看正在运行的容器
docker ps –a # 查看所有容器
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建并启动容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run 参数  --name=... /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-i：保持容器运行，通常与 -t 同时使用，加入it这两个参数后，容器创建后自动进入容器中，退出容器后，容器自动关闭&lt;/li&gt;
&lt;li&gt;-t：为容器重新分配一个伪输入终端，通常与 -i 同时使用&lt;/li&gt;
&lt;li&gt;-d：以守护（后台）模式运行容器。创建一个容器在后台运行，需要使用docker exec 进入容器。退出后，容器不会关闭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-it 创建的容器一般称为交互式容器，-id 创建的容器一般称为守护式容器&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--name：为创建的容器命名&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec 参数 # 退出容器，容器不会关闭
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stop 容器名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker start 容器名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除容器：如果容器是运行状态则删除失败，需要停止容器才能删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rm 容器名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看容器信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker inspect 容器名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;数据卷&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker 容器删除后，在容器中产生的数据也会随之销毁
Docker 容器和外部机器可以直接交换文件吗？
容器之间想要进行数据交互？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-容器的数据卷.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据卷&lt;/strong&gt;：数据卷是宿主机中的一个目录或文件，当容器目录和数据卷目录绑定后，对方的修改会立即同步&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个数据卷可以被多个容器同时挂载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个容器也可以被挂载多个数据卷&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据卷的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;容器数据持久化&lt;/li&gt;
&lt;li&gt;外部机器和容器间接通信&lt;/li&gt;
&lt;li&gt;容器之间数据交换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置数据卷&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建启动容器时，使用-v参数设置数据卷&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run ... –v 宿主机目录(文件):容器内目录(文件) ... 
docker run -it --name=c1 -v /root(or~)/data:/root/data_container centos:7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;目录必须是绝对路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果目录不存在，会自动创建&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以挂载多个数据卷&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多容器进行数据交换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个容器挂载同一个数据卷&lt;/li&gt;
&lt;li&gt;数据卷容器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-多容器数据交换.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建启动c3数据卷容器，使用 –v 参数设置数据卷&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run –it --name=c3 –v /volume centos:7 /bin/bash 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建启动 c1 c2 容器，使用 –-volumes-from 参数设置数据卷&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run –it --name=c1 --volumes-from c3 centos:7 /bin/bash
docker run –it --name=c2 --volumes-from c3 centos:7 /bin/bash  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;应用部署&lt;/h2&gt;
&lt;h3&gt;MySQL&lt;/h3&gt;
&lt;p&gt;在Docker容器中部署MySQL，通过外部mysql客户端操作MySQL Server&lt;/p&gt;
&lt;p&gt;端口映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容器内的网络服务和外部机器不能直接通信，外部机器和宿主机可以直接通信，宿主机和容器可以直接通信&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当容器中的网络服务需要被外部机器访问时，可以将容器中提供服务的端口映射到宿主机的端口上。外部机器访问宿主机的该端口，从而间接访问容器的服务。这种操作称为：&lt;strong&gt;端口映射&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-MySQL%E9%83%A8%E7%BD%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL部署步骤：搜索mysql镜像，拉取mysql镜像，创建容器，操作容器中的mysql&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;搜索mysql镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拉取mysql镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull mysql:5.6
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建容器，设置端口映射、目录映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在/root目录下创建mysql目录用于存储mysql数据信息
mkdir ~/mysql
cd ~/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -id \
-p 3307:3306 \
--name=c_mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:5.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p 3307:3306&lt;/code&gt;：将容器的 3306 端口映射到宿主机的 3307 端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v $PWD/conf:/etc/mysql/conf.d&lt;/code&gt;：将主机当前目录下的 conf/my.cnf 挂载到容器的 /etc/mysql/my.cnf，配置目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v $PWD/logs:/logs&lt;/code&gt;：将主机当前目录下的 logs目录挂载到容器的 /logs，日志目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v $PWD/data:/var/lib/mysql&lt;/code&gt; ：将主机当前目录下的data目录挂载到容器的 /var/lib/mysql 。数据目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e MYSQL_ROOT_PASSWORD=123456&lt;/code&gt;**：**初始化 root 用户的密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入容器，操作mysql&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec –it c_mysql /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用外部机器连接容器中的mysql&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Tomcat&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;搜索tomcat镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search tomcat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拉取tomcat镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull tomcat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建容器，设置端口映射、目录映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在/root目录下创建tomcat目录用于存储tomcat数据信息
mkdir ~/tomcat
cd ~/tomcat
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -id --name=c_tomcat \
-p 8080:8080 \
-v $PWD:/usr/local/tomcat/webapps \
tomcat 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-p 8080:8080&lt;/code&gt;：将容器的8080端口映射到主机的8080端口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-v $PWD:/usr/local/tomcat/webapps&lt;/code&gt;：将主机中当前目录挂载到容器的webapps&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用外部机器访问tomcat&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Nginx&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;搜索nginx镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拉取nginx镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建容器，设置端口映射、目录映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在/root目录下创建nginx目录用于存储nginx数据信息
mkdir ~/nginx
cd ~/nginx
mkdir conf
cd conf
# 在~/nginx/conf/下创建nginx.conf文件,粘贴下面内容
vim nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  &apos;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &apos;
                      &apos;$status $body_bytes_sent &quot;$http_referer&quot; &apos;
                      &apos;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&apos;;

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -id --name=c_nginx \
-p 80:80 \
-v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf \
-v $PWD/logs:/var/log/nginx \
-v $PWD/html:/usr/share/nginx/html \
nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p 80:80&lt;/code&gt;：将容器的 80端口映射到宿主机的 80 端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf&lt;/code&gt;：将主机当前目录下的 /conf/nginx.conf 挂载到容器的 :/etc/nginx/nginx.conf，配置目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v $PWD/logs:/var/log/nginx&lt;/code&gt;：将主机当前目录下的 logs 目录挂载到容器的/var/log/nginx，日志目录&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用外部机器访问nginx&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Redis&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;搜索redis镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search redis
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拉取redis镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull redis:5.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建容器，设置端口映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -id --name=c_redis -p 6379:6379 redis:5.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用外部机器连接redis&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./redis-cli.exe -h 192.168.149.135 -p 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;镜像原理&lt;/h2&gt;
&lt;h3&gt;底层原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker 镜像本质是什么？
Docker 中一个centos镜像为什么只有200MB，而一个centos操作系统的iso文件要几个个G？
Docker 中一个tomcat镜像为什么有500MB，而一个tomcat安装包只有70多MB？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;操作系统的组成部分：进程调度子系统、进程通信子系统、内存管理子系统、设备管理子系统、文件管理子系统、网络通信子系统、作业控制子系统&lt;/p&gt;
&lt;p&gt;Linux文件系统由bootfs和rootfs两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;bootfs：包含bootloader（引导加载程序）和 kernel（内核）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rootfs： root文件系统，包含的就是典型 Linux 系统中的/dev，/proc，/bin，/etc等标准目录和文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不同的linux发行版，bootfs基本一样，而rootfs不同，如ubuntu，centos&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker镜像原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Docker镜像是一个&lt;strong&gt;分层文件系统&lt;/strong&gt;，是由特殊的文件系统叠加而成，最底端是 bootfs，并复用宿主机的bootfs ，第二层是 root文件系统rootfs称为base image，然后再往上可以叠加其他的镜像文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统一文件系统（Union File System）技术能够将不同的层整合成一个文件系统，为这些层提供了一个统一的视角，这样就隐藏了多层的存在，在用户的角度看来，只存在一个文件系统&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个镜像可以放在另一个镜像的上面。位于下面的镜像称为父镜像，最底部的镜像成为基础镜像。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当从一个镜像启动容器时，Docker会在最顶层加载一个读写文件系统作为容器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 中一个Ubuntu镜像为什么只有200MB，而一个Ubuntu操作系统的iso文件要几个个G？
Ubuntu的iso镜像文件包含bootfs和rootfs，而docker的Ubuntu镜像复用操作系统的bootfs，只有rootfs和其他镜像层&lt;/li&gt;
&lt;li&gt;Docker 中一个tomcat镜像为什么有500MB，而一个tomcat安装包只有70多MB？
由于docker中镜像是分层的，tomcat虽然只有70多MB，但他需要依赖于父镜像和基础镜像，所有整个对外暴露的tomcat镜像大小500多MB&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;镜像制作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Docker%E9%95%9C%E5%83%8F%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Dockerfile&lt;/h3&gt;
&lt;h4&gt;基本概述&lt;/h4&gt;
&lt;p&gt;Dockerfile是一个文本文件，包含一条条的指令，每一条指令构建一层，基于基础镜像最终构建出新的镜像&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于开发人员：可以为开发团队提供一个完全一致的开发环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于测试人员：可以直接拿开发时所构建的镜像或者通过Dockerfile文件构建一个新的镜像开始工作了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于运维人员：在部署时，可以实现应用的无缝移植&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关键字&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FROM&lt;/td&gt;
&lt;td&gt;指定父镜像&lt;/td&gt;
&lt;td&gt;指定dockerfile基于那个image构建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAINTAINER&lt;/td&gt;
&lt;td&gt;作者信息&lt;/td&gt;
&lt;td&gt;用来标明这个dockerfile谁写的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LABEL&lt;/td&gt;
&lt;td&gt;标签&lt;/td&gt;
&lt;td&gt;用来标明dockerfile的标签 可以使用Label代替Maintainer 最终都是在docker image基本信息中可以查看&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RUN&lt;/td&gt;
&lt;td&gt;执行命令&lt;/td&gt;
&lt;td&gt;执行一段命令 默认是/bin/sh 格式: RUN command 或者 RUN [&quot;command&quot; , &quot;param1&quot;,&quot;param2&quot;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMD&lt;/td&gt;
&lt;td&gt;容器启动命令&lt;/td&gt;
&lt;td&gt;提供启动容器时候的默认命令 和ENTRYPOINT配合使用.格式 CMD command param1 param2 或者 CMD [&quot;command&quot; , &quot;param1&quot;,&quot;param2&quot;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENTRYPOINT&lt;/td&gt;
&lt;td&gt;入口&lt;/td&gt;
&lt;td&gt;一般在制作一些执行就关闭的容器中会使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COPY&lt;/td&gt;
&lt;td&gt;复制文件&lt;/td&gt;
&lt;td&gt;build的时候复制文件到image中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADD&lt;/td&gt;
&lt;td&gt;添加文件&lt;/td&gt;
&lt;td&gt;build的时候添加文件到image中 不仅仅局限于当前build上下文 可以来源于远程服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENV&lt;/td&gt;
&lt;td&gt;环境变量&lt;/td&gt;
&lt;td&gt;指定build时候的环境变量 可以在启动的容器的时候 通过-e覆盖 格式ENV name=value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARG&lt;/td&gt;
&lt;td&gt;构建参数&lt;/td&gt;
&lt;td&gt;构建参数 只在构建的时候使用的参数 如果有ENV 那么ENV的相同名字的值始终覆盖arg的参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VOLUME&lt;/td&gt;
&lt;td&gt;定义外部可以挂载的数据卷&lt;/td&gt;
&lt;td&gt;指定build的image那些目录可以启动的时候挂载到文件系统中 启动容器的时候使用 -v 绑定 格式 VOLUME [&quot;目录&quot;]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPOSE&lt;/td&gt;
&lt;td&gt;暴露端口&lt;/td&gt;
&lt;td&gt;定义容器运行的时候监听的端口 启动容器的使用-p来绑定暴露端口 格式: EXPOSE 8080 或者 EXPOSE 8080/udp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WORKDIR&lt;/td&gt;
&lt;td&gt;工作目录&lt;/td&gt;
&lt;td&gt;指定容器内部的工作目录 如果没有创建则自动创建 如果指定/ 使用的是绝对地址 如果不是/开头那么是在上一条workdir的路径的相对路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USER&lt;/td&gt;
&lt;td&gt;指定执行用户&lt;/td&gt;
&lt;td&gt;指定build或者启动的时候 用户 在RUN CMD ENTRYPONT执行的时候的用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEALTHCHECK&lt;/td&gt;
&lt;td&gt;健康检查&lt;/td&gt;
&lt;td&gt;指定监测当前容器的健康监测的命令 基本上没用 因为很多时候 应用本身有健康监测机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONBUILD&lt;/td&gt;
&lt;td&gt;触发器&lt;/td&gt;
&lt;td&gt;当存在ONBUILD关键字的镜像作为基础镜像的时候 当执行FROM完成之后 会执行 ONBUILD的命令 但是不影响当前镜像 用处也不怎么大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STOPSIGNAL&lt;/td&gt;
&lt;td&gt;发送信号量到宿主机&lt;/td&gt;
&lt;td&gt;该STOPSIGNAL指令设置将发送到容器的系统调用信号以退出。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHELL&lt;/td&gt;
&lt;td&gt;指定执行脚本的shell&lt;/td&gt;
&lt;td&gt;指定RUN CMD ENTRYPOINT 执行命令的时候 使用的shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Centos&lt;/h4&gt;
&lt;p&gt;自定义centos7镜像：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;默认登录路径为 /usr&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用vim&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义父镜像：FROM centos:7&lt;/li&gt;
&lt;li&gt;定义作者信息：MAINTAINER  seazean &amp;lt; zhyzhyang@sina.com&amp;gt;&lt;/li&gt;
&lt;li&gt;执行安装vim命令： RUN yum install -y vim&lt;/li&gt;
&lt;li&gt;定义默认的工作目录：WORKDIR /usr&lt;/li&gt;
&lt;li&gt;定义容器启动执行的命令：CMD /bin/bash&lt;/li&gt;
&lt;li&gt;通过dockerfile构建镜像：docker bulid –f dockerfile文件路径 –t 镜像名称:版本&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Boot&lt;/h4&gt;
&lt;p&gt;定义dockerfile，发布springboot项目：&lt;/p&gt;
&lt;p&gt;实现步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义父镜像：FROM java:8&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义作者信息：MAINTAINER seazean &amp;lt; zhyzhyang@sina.com&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将jar包添加到容器： ADD springboot.jar app.jar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义容器启动执行的命令：CMD java–jar app.jar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过dockerfile构建镜像：docker bulid –f dockerfile文件路径 –t 镜像名称:版本&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;服务编排&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;微服务架构的应用系统中一般包含若干个微服务，每个微服务一般都会部署多个实例，如果每个微服务都要手动启停，维护的工作量会很大。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从Dockerfile build image 或者去dockerhub拉取image；&lt;/li&gt;
&lt;li&gt;创建多个container，管理这些container（启动停止删除）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;服务编排&lt;/strong&gt;：按照一定的业务规则批量管理容器&lt;/p&gt;
&lt;p&gt;Docker Compose是一个编排多容器分布式部署的工具，提供命令集管理容器化应用的完整开发周期，包括服务构建，启动和停止。使用步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;利用 Dockerfile 定义运行环境镜像&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 docker-compose.yml 定义组成应用的各服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行 docker-compose up 启动应用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Compose%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;p&gt;使用docker compose编排nginx+springboot项目&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Docker Compose&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建docker-compose目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir ~/docker-compose
cd ~/docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写 docker-compose.yml 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3&apos;
services:
  nginx:
   image: nginx
   ports:
    - 80:80
   links:
    - app
   volumes:
    - ./nginx/conf.d:/etc/nginx/conf.d
  app:
    image: app
    expose:
      - &quot;8080&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建./nginx/conf.d目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ./nginx/conf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在./nginx/conf.d目录下编写***.conf文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    access_log off;

    location / {
        proxy_pass http://app:8080;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在~/docker-compose 目录下使用docker-compose启动容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.0.137/hello
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;私有仓库&lt;/h2&gt;
&lt;p&gt;Docker官方的Docker hub（https://hub.docker.com）是一个用于管理公共镜像的仓库，我们可以从上面拉取镜像 到本地，也可以把我们自己的镜像推送上去。但是当服务器无法访问互联网，或者不希望将自己的镜像放到公网当中，那么我们就需要搭建自己的私有仓库来存储和管理自己的镜像&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;私有仓库搭建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1、拉取私有仓库镜像 
docker pull registry
# 2、启动私有仓库容器 
docker run -id --name=registry -p 5000:5000 registry
# 3、输入地址http://私有仓库服务器ip:5000/v2/_catalog，显示{&quot;repositories&quot;:[]} 
# 4、修改daemon.json   
vim /etc/docker/daemon.json    
# 在上述文件中添加一个key，保存退出。此步用于让 docker 信任私有仓库地址；注意将私有仓库服务器ip修改为自己私有仓库服务器真实ip 
{&quot;insecure-registries&quot;:[&quot;192.168.0.137:5000&quot;]} 
# 5、重启docker 服务 
systemctl restart docker
docker start registry
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将镜像上传至私有仓库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1、标记镜像为私有仓库的镜像     
docker tag centos:7 私有仓库服务器IP:5000/centos:7
 
# 2、上传标记的镜像     
docker push 私有仓库服务器IP:5000/centos:7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从私有仓库拉取镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#拉取镜像 
docker pull 私有仓库服务器ip:5000/centos:7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;虚拟机&lt;/h2&gt;
&lt;p&gt;容器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容器是将软件打包成标准化单元，以用于开发、交付和部署&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器镜像是轻量的、可执行的独立软件包 ，包含软件运行所需的所有内容：代码、运行时环境、系统工具、系统库和设置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器化软件在任何环境中都能够始终如一地运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器赋予了软件独立性，使其免受外在环境差异的影响，从而有助于减少团队间在相同基础设施上运行不同软件时的冲突&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器和虚拟机对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;相同：容器和虚拟机具有相似的资源隔离和分配优势&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;容器虚拟化的是操作系统，虚拟机虚拟化的是硬件。&lt;/li&gt;
&lt;li&gt;传统虚拟机可以运行不同的操作系统，容器只能运行同一类型操作系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-%E5%AE%B9%E5%99%A8%E5%92%8C%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;容器&lt;/th&gt;
&lt;th&gt;虚拟机&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;启动&lt;/td&gt;
&lt;td&gt;秒级&lt;/td&gt;
&lt;td&gt;分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;硬盘使用&lt;/td&gt;
&lt;td&gt;一般为MB&lt;/td&gt;
&lt;td&gt;一般为GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;接近原生&lt;/td&gt;
&lt;td&gt;弱于原生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统支持量&lt;/td&gt;
&lt;td&gt;单机支持上千个容器&lt;/td&gt;
&lt;td&gt;一般几十个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>前端技术笔记合集</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/web/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/web/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;HTML&lt;/h1&gt;
&lt;h2&gt;HTML入门&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;HTML（超文本标记语言—HyperText Markup Language）是构成 Web 世界的基础，是一种用来告知浏览器如何组织页面的标记语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;超文本 Hypertext，是指连接单个或者多个网站间的网页的链接。通过链接，就能访问互联网中的内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标记 Markup ，是用来注明文本，图片等内容，以便于在浏览器中显示，例如 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;网页的构成&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML&quot;&gt;HTML&lt;/a&gt;：通常用来定义网页内容的含义和基本结构&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS&quot;&gt;CSS&lt;/a&gt;：通常用来描述网页的表现与展示效果&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript&quot;&gt;JavaScript&lt;/a&gt;：通常用来执行网页的功能与行为&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1Qf4y1T7Hx&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;组成&lt;/h3&gt;
&lt;h4&gt;标签&lt;/h4&gt;
&lt;p&gt;HTML 页面由一系列的&lt;strong&gt;元素（elements）&lt;/strong&gt; 组成，而元素是使用&lt;strong&gt;标签&lt;/strong&gt;创建的&lt;/p&gt;
&lt;p&gt;一对标签（tags）可以设置一段文字样式，添加一张图片或者添加超链接等等&lt;/p&gt;
&lt;p&gt;在 HTML 中，&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 标签表示&lt;strong&gt;标题&lt;/strong&gt;，我们可以使用&lt;strong&gt;开始标签&lt;/strong&gt;和&lt;strong&gt;结束标签&lt;/strong&gt;包围文本内容，这样其中的内容就以标题的形式显示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;开始学习JavaWeb&amp;lt;/h1&amp;gt;
&amp;lt;h2&amp;gt;二级标题&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;属性&lt;/h4&gt;
&lt;p&gt;HTML 标签可以拥有属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性是属于标签的，修饰标签，让标签有更多的效果&lt;/li&gt;
&lt;li&gt;属性一般定义在起始标签里面&lt;/li&gt;
&lt;li&gt;属性一般以&lt;strong&gt;属性=属性值&lt;/strong&gt;的形式出现&lt;/li&gt;
&lt;li&gt;属性值一般用 &lt;code&gt;&apos;&apos;&lt;/code&gt; 或者 &lt;code&gt;&quot;&quot;&lt;/code&gt; 括起来。 不加引号也是可以的(不建议使用)。比如：name=&apos;value&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1 align=&quot;center&quot;&amp;gt;开始学习JavaWeb&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 HTML 标签中，&lt;code&gt;align&lt;/code&gt;  属性表示&lt;strong&gt;水平对齐方式&lt;/strong&gt;，我们可以赋值为 &lt;code&gt;center&lt;/code&gt;  表示 &lt;strong&gt;居中&lt;/strong&gt; 。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E7%BB%93%E6%9E%84.png&quot; alt=&quot;HTML结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;文档结构介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档声明：用于声明当前 HTML 的版本，这里的&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/code&gt;是 HTML5 的声明&lt;/li&gt;
&lt;li&gt;html 根标签：除文档声明以外，其它内容全部要放在根标签 html 内部&lt;/li&gt;
&lt;li&gt;文档头部配置：head 标签，是当前页面的配置信息，外部引入文件, 例如网页标签、字符集等
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;&lt;/code&gt;：这个标签是页面的元数据信息，设置文档使用 utf-8 字符集编码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;：这个标签定义文档标题，位置出现在浏览器标签。在收藏页面时，它可用来描述页面&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;文档显示内容：body 标签，里边的内容会显示到浏览器页面上&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;HTML语法&lt;/h2&gt;
&lt;h3&gt;注释方式&lt;/h3&gt;
&lt;p&gt;将一段 HTML 中的内容置为注释，你需要将其用特殊的记号 &amp;lt;!----&amp;gt; 包括起来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;我在注释外！&amp;lt;/p&amp;gt;

&amp;lt;!-- &amp;lt;p&amp;gt;我在注释内！&amp;lt;/p&amp;gt; --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;基本元素&lt;/h3&gt;
&lt;h4&gt;空元素&lt;/h4&gt;
&lt;p&gt;一些元素只有一个标签，叫做空元素。它是在开始标签中进行关闭的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一行文档&amp;lt;br/&amp;gt; 
第二行文档&amp;lt;br/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;嵌套元素&lt;/h4&gt;
&lt;p&gt;把元素放到其它元素之中——这被称作嵌套。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h2&amp;gt;&amp;lt;u&amp;gt;二级标题&amp;lt;/u&amp;gt;&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;块元素&lt;/h4&gt;
&lt;p&gt;在HTML中有两种重要元素类别，块级元素和内联元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;块级元素：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;独占一行&lt;/strong&gt;。块级元素（block）在页面中以块的形式展现。相对于其前面的内容它会出现在新的一行，其后的内容也会被挤到下一行展现。比如&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; ，&lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; ，&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行内元素&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;行内显示&lt;/strong&gt;。行内元素不会导致换行。通常出现在块级元素中并环绕文档内容的一小部分，而不是一整个段落或者一组内容。比如&lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;i&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; 等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：一个块级元素不会被嵌套进行内元素中，但可以嵌套在其它块级元素中。&lt;/p&gt;
&lt;p&gt;常用的两个标签：（&lt;strong&gt;重要&lt;/strong&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 是一个通用的内容容器，并没有任何特殊语义。它可以被用来对其它元素进行分组，一般用于样式化相关的需求。它是一个&lt;strong&gt;块级元素&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;属性：id、style、class&lt;/li&gt;
&lt;li&gt;&lt;code&gt; &amp;lt;span&amp;gt;&lt;/code&gt; 是短语内容的通用行内容器，并没有任何特殊语义。它可以被用来编组元素以达到某种样式。它是一个&lt;strong&gt;行内元素&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;基本属性&lt;/h3&gt;
&lt;p&gt;标签属性，主要用于拓展标签。属性包含元素的额外信息，这些信息不会出现在实际的内容中。但是可以改变标签的一些行为或者提供数据，属性总是以&lt;code&gt;name = value&quot;&lt;/code&gt;的格式展现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属性名：同一个标签中，属性名不得重复。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大小写：属性和属性值对大小写不敏感。不过W3C标准中，推荐使用小写的属性/属性值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引号：双引号是最常用的，不过使用单引号也没有问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;class&lt;/td&gt;
&lt;td&gt;定义元素类名，用来选择和访问特定的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;定义元素&lt;strong&gt;唯一&lt;/strong&gt;标识符，在整个文档中必须是唯一的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;定义元素名称，可以用于提交服务器的表单字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;value&lt;/td&gt;
&lt;td&gt;定义在元素内显示的默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;style&lt;/td&gt;
&lt;td&gt;定义CSS样式，这些样式会覆盖之前设置的样式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;特殊字符&lt;/h3&gt;
&lt;p&gt;在HTML中，字符 &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;,&lt;code&gt;&quot;&lt;/code&gt;,&lt;code&gt;&apos;&lt;/code&gt; 和 &lt;code&gt;&amp;amp;&lt;/code&gt; 是特殊字符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原义字符&lt;/th&gt;
&lt;th&gt;等价字符引用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&apos;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空格&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;nbsp;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;文本标签&lt;/h3&gt;
&lt;p&gt;使用文本内容标签设置文字基本样式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p&lt;/td&gt;
&lt;td&gt;表示文本的一个段落&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h&lt;/td&gt;
&lt;td&gt;表示文档标题，&lt;code&gt;&amp;lt;h1&amp;gt;–&amp;lt;h6&amp;gt;&lt;/code&gt; ，呈现了六个不同的级别的标题，&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 级别最高，而 &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt; 级别最低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hr&lt;/td&gt;
&lt;td&gt;表示段落级元素之间的主题转换，一般显示为水平线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;li&lt;/td&gt;
&lt;td&gt;表示列表里的条目。（常用在ul ol 中）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ul&lt;/td&gt;
&lt;td&gt;表示一个无序列表，可含多个元素，无编号显示。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ol&lt;/td&gt;
&lt;td&gt;表示一个有序列表，通常渲染为有带编号的列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;em&lt;/td&gt;
&lt;td&gt;表示文本着重，一般用斜体显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;td&gt;表示文本重要，一般用粗体显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;font&lt;/td&gt;
&lt;td&gt;表示字体，可以设置样式（已过时）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;表示斜体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;表示加粗文本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;文本标签演示&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!--段落标签：&amp;lt;p&amp;gt;--&amp;gt;
    &amp;lt;p&amp;gt;这些年&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;支付宝的诞生就是为了解决淘宝网的客户们的买卖问题&amp;lt;/p&amp;gt;
    
    &amp;lt;!-- 标题标签：&amp;lt;h1&amp;gt; ~ &amp;lt;h6&amp;gt; --&amp;gt;
    &amp;lt;h1&amp;gt;一级标题&amp;lt;/h1&amp;gt;
    &amp;lt;h2&amp;gt;二级标题&amp;lt;/h2&amp;gt;
    &amp;lt;h3&amp;gt;三级标题&amp;lt;/h3&amp;gt;
    &amp;lt;h4&amp;gt;四级标题&amp;lt;/h4&amp;gt;
    &amp;lt;h5&amp;gt;五级标题&amp;lt;/h5&amp;gt;
    &amp;lt;h6&amp;gt;六级标题&amp;lt;/h6&amp;gt;

    &amp;lt;!--水平线标签：&amp;lt;hr/&amp;gt;
        属性：
            size-大小
            color-颜色
	--&amp;gt;
    &amp;lt;hr size=&quot;4&quot; color=&quot;red&quot;/&amp;gt;

    &amp;lt;!--
        无序列表：&amp;lt;ul&amp;gt;
        属性：type-列表样式(disc实心圆、circle空心圆、square实心方块)
        列表项：&amp;lt;li&amp;gt;
    --&amp;gt;
    &amp;lt;ul type=&quot;circle&quot;&amp;gt;
        &amp;lt;li&amp;gt;javaEE&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;HTML&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;

    &amp;lt;!--
        有序列表：&amp;lt;ol&amp;gt;
        属性：type-列表样式(1数字、A或a字母、I或i罗马字符)   start-起始位置
        列表项：&amp;lt;li&amp;gt;
    --&amp;gt;
    &amp;lt;ol type=&quot;1&quot; start=&quot;10&quot;&amp;gt;
        &amp;lt;li&amp;gt;传智播客&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;黑马程序员&amp;lt;/li&amp;gt;
    &amp;lt;/ol&amp;gt;

    &amp;lt;!--
        斜体标签：&amp;lt;i&amp;gt;    &amp;lt;em&amp;gt;
    --&amp;gt;
    &amp;lt;i&amp;gt;我倾斜了&amp;lt;/i&amp;gt;
    &amp;lt;em&amp;gt;我倾斜了&amp;lt;/em&amp;gt;
    &amp;lt;br/&amp;gt;

    &amp;lt;!--
        加粗标签：&amp;lt;strong&amp;gt;  &amp;lt;b&amp;gt;
    --&amp;gt;
    &amp;lt;strong&amp;gt;加粗文本&amp;lt;/strong&amp;gt;
    &amp;lt;b&amp;gt;加粗文本&amp;lt;/b&amp;gt;
    &amp;lt;br/&amp;gt;
    &amp;lt;!--
        文字标签：&amp;lt;font&amp;gt;
        属性：
            size-大小
            color-颜色
    --&amp;gt;
    &amp;lt;font size=&quot;5&quot; color=&quot;yellow&quot;&amp;gt;这是一段文字&amp;lt;/font&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果如下&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E6%96%87%E6%9C%AC%E6%A0%87%E7%AD%BE%E6%95%88%E6%9E%9C%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;图片标签&lt;/h3&gt;
&lt;p&gt;img标签中的img其实是英文image的缩写, img标签的作用, 就是告诉浏览器我们需要显示一张图片&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;../img/b.jpg&quot; width=&quot;400px&quot; height=&quot;200px&quot; alt=&quot;&quot; title=&quot;&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;src&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;图片路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;title&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;鼠标悬停（hover）时显示文本。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;alt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;图片描述，图形不显示时的替换文本。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;height&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;图像的高度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;图像的宽度。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;超链接&lt;/h3&gt;
&lt;p&gt;超链接标签的作用: 就是用于控制页面与页面(服务器资源)之间跳转的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;指定需要跳转的目标路径&quot; target=&quot;打开的方式&quot;&amp;gt;需要展现给用户的内容&amp;lt;/a&amp;gt;
target属性取值: 
	_blank：新起页面
	_self：当前页面（默认）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;超链接标签演示&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        a{
            /*去掉超链接的下划线*/
            text-decoration: none;
            /*超链接的颜色*/
            color: black;
        }

        /*鼠标悬浮的样式控制*/
        a:hover{
            color: red;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!--
        超链接标签：&amp;lt;a&amp;gt;
        属性：
            href-跳转的地址
            target-跳转的方式(_self当前页面、_blank新标签页)
    --&amp;gt;
    &amp;lt;a href=&quot;01案例二：样式演示.html&quot; target=&quot;_blank&quot;&amp;gt;点我跳转到样式演示&amp;lt;/a&amp;gt;  &amp;lt;br/&amp;gt;
    &amp;lt;a href=&quot;http://www.itcast.cn&quot; target=&quot;_blank&quot;&amp;gt;传智播客&amp;lt;/a&amp;gt;  &amp;lt;br/&amp;gt;
    &amp;lt;a href=&quot;http://www.itheima.com&quot; target=&quot;_self&quot;&amp;gt;黑马程序员&amp;lt;/a&amp;gt;  &amp;lt;br/&amp;gt;
    &amp;lt;a href=&quot;http://www.itheima.com&quot; target=&quot;_blank&quot;&amp;gt;&amp;lt;img src=&quot;../img/itheima.png&quot; width=&quot;150px&quot; height=&quot;50px&quot;/&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML超链接效果图.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;表单标签&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;form&lt;/strong&gt;  表示表单，是用来&lt;strong&gt;收集用户输入信息并向 Web 服务器提交&lt;/strong&gt;的一个容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form &amp;gt;
    //表单元素
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;action&lt;/td&gt;
&lt;td&gt;处理此表单信息的Web服务器的URL地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;method&lt;/td&gt;
&lt;td&gt;提交此表单信息到Web服务器的方式，可能的值有get和post，默认为get&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autocomplete&lt;/td&gt;
&lt;td&gt;自动补全，指示表单元素是否能够拥有一个默认值，配合input标签使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;get与post区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;post：指的是 HTTP &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5&quot;&gt;POST 方法&lt;/a&gt;；表单数据会包含在表单体内然后发送给服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get：指的是 HTTP &lt;a href=&quot;http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3&quot;&gt;GET 方法&lt;/a&gt;；表单数据会附加在 &lt;code&gt;action&lt;/code&gt; 属性的URI中，并以 &apos;?&apos; 作为分隔符，然后这样得到的 URI 再发送给服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;地址栏可见&lt;/th&gt;
&lt;th&gt;数据安全&lt;/th&gt;
&lt;th&gt;数据大小&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;可见&lt;/td&gt;
&lt;td&gt;不安全&lt;/td&gt;
&lt;td&gt;有限制（取决于浏览器）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;不可见&lt;/td&gt;
&lt;td&gt;相对安全&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;表单元素&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;label&lt;/td&gt;
&lt;td&gt;表单元素的说明，配合表单元素使用&lt;/td&gt;
&lt;td&gt;for属性值为相关表单元素id属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;td&gt;表单中输入控件，多种输入类型，用于接受来自用户数据&lt;/td&gt;
&lt;td&gt;type属性值决定输入类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;button&lt;/td&gt;
&lt;td&gt;页面中可点击的按钮，可以配合表单进行提交&lt;/td&gt;
&lt;td&gt;type属性值决定按钮类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;select&lt;/td&gt;
&lt;td&gt;表单的控件，下拉选项菜单&lt;/td&gt;
&lt;td&gt;与option配合实用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;optgroup&lt;/td&gt;
&lt;td&gt;option的分组标签&lt;/td&gt;
&lt;td&gt;与option配合实用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;option&lt;/td&gt;
&lt;td&gt;select的子标签，表示一个选项&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;textarea&lt;/td&gt;
&lt;td&gt;表示多行纯文本编辑控件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fieldset&lt;/td&gt;
&lt;td&gt;用来对表单中的控制元素进行分组(也包括 label 元素)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;legend&lt;/td&gt;
&lt;td&gt;用于表示它的fieldset内容的标题。&lt;/td&gt;
&lt;td&gt;fieldset 的子元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;按键控件&lt;/h4&gt;
&lt;p&gt;button标签：表示按钮&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;type属性：表示按钮类型，submit值为提交按钮。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;button&lt;/td&gt;
&lt;td&gt;无行为按钮，用于结合JavaScript实现自定义动态效果&lt;/td&gt;
&lt;td&gt;同 &lt;code&gt;&amp;lt;input type=&quot;submit&quot;/&amp;gt; &lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;submit&lt;/td&gt;
&lt;td&gt;提交按钮，用于提交表单数据到服务器。&lt;/td&gt;
&lt;td&gt;同 &lt;code&gt;&amp;lt;input type=&quot;submit&quot;/&amp;gt; &lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reset&lt;/td&gt;
&lt;td&gt;重置按钮，用于将表单中内容恢复为默认值。&lt;/td&gt;
&lt;td&gt;同&lt;code&gt;&amp;lt;input type=&quot;reset&quot;&lt;/code&gt;/&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;输入控件&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;label标签：表单的说明。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;for属性值：匹配input标签的id属性值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;input标签：输入控件。&lt;/p&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;type：表示输入类型，text值为普通文本框&lt;/li&gt;
&lt;li&gt;id：表示标签唯一标识&lt;/li&gt;
&lt;li&gt;name：表示标签名称，提交服务器的标识&lt;/li&gt;
&lt;li&gt;value：表示标签的默认数据值&lt;/li&gt;
&lt;li&gt;placeholder：默认的提示信息，仅适用于当type 属性为text, search, tel, url or email时;&lt;/li&gt;
&lt;li&gt;required：是否必须为该元素填充值，当type属性是hidden,image或者button类型时不可使用&lt;/li&gt;
&lt;li&gt;readonly：是否只读,可以让用户不修改这个输入框的值,就使用value属性设置默认值&lt;/li&gt;
&lt;li&gt;disabled：是否可用,如果某个输入框有disabled那么它的数据不能提交到服务器通常是使用在有的页面中，让一些按钮不能点击&lt;/li&gt;
&lt;li&gt;autocomplete：自动补全，规定表单或输入字段是否应该自动完成。当自动完成开启，浏览器会基于用户之前的输入值自动填写值。可以设置指定的字段为off，关闭自动补全&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;#&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
        &amp;lt;label for=&quot;username&quot;&amp;gt;用户名：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot; value=&quot;&quot; placeholder=&quot; 请在此处输入用户名&quot; required/&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;提交&amp;lt;/button&amp;gt;
        &amp;lt;button type=&quot;reset&quot;&amp;gt;重置&amp;lt;/button&amp;gt;
        &amp;lt;button type=&quot;button&quot;&amp;gt;按钮&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图：
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;表单项标签&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;form action=&quot;#&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
&amp;lt;label for=&quot;username&quot;&amp;gt;用户名：&amp;lt;/label&amp;gt;
&amp;lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot; value=&quot;&quot; placeholder=&quot; 请在此处输入用户名&quot; required/&amp;gt;
&amp;lt;button type=&quot;submit&quot;&amp;gt;提交&amp;lt;/button&amp;gt;
&amp;lt;button type=&quot;reset&quot;&amp;gt;重置&amp;lt;/button&amp;gt;
&amp;lt;button type=&quot;button&quot;&amp;gt;按钮&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/p&gt;
&lt;h5&gt;n-v属性&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;的名字，在提交整个表单数据时，可以用于区分属于不同&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;这个&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;元素当前的值，允许用户通过页面输入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;使用方式：以name属性值作为键，value属性值作为值，构成键值对提交到服务器，多个键值对浏览器使用&lt;code&gt;&amp;amp;&lt;/code&gt;进行分隔。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E6%A0%87%E7%AD%BEinput%E5%B1%9E%E6%80%A7-name-value.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;type属性&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;单行文本字段&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;password&lt;/td&gt;
&lt;td&gt;单行文本字段，值被遮盖&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;用于编辑 e-mail 的字段，可以对e-mail地址进行简单校验&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;radio&lt;/td&gt;
&lt;td&gt;单选按钮。 1. 在同一个”单选按钮组“中，所有单选按钮的 name 属性使用同一个值；一个单选按钮组中是，同一时间只有一个单选按钮可以被选择。 2. 必须使用 value 属性定义此控件被提交时的值。 3. 使用checked 必须指示控件是否缺省被选择。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checkbox&lt;/td&gt;
&lt;td&gt;复选框。 1. 必须使用 value 属性定义此控件被提交时的值。 2. 使用 checked 属性指示控件是否被选择。 3. 选中多个值时，所有的值会构成一个数组而提交到Web服务器&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;HTML5 用于输入日期的控件&lt;/td&gt;
&lt;td&gt;年，月，日，不包括时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;HTML5 用于输入时间的控件&lt;/td&gt;
&lt;td&gt;不含时区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;datetime-local&lt;/td&gt;
&lt;td&gt;HTML5 用于输入日期时间的控件&lt;/td&gt;
&lt;td&gt;不包含时区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;HTML5 用于输入浮点数的控件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;range&lt;/td&gt;
&lt;td&gt;HTML5 用于输入不精确值控件&lt;/td&gt;
&lt;td&gt;max-规定最大值min-规定最小值 step-规定步进值 value-规定默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;search&lt;/td&gt;
&lt;td&gt;HTML5 用于输入搜索字符串的单行文本字段&lt;/td&gt;
&lt;td&gt;可以点击&lt;code&gt;x&lt;/code&gt;清除内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tel&lt;/td&gt;
&lt;td&gt;HTML5 用于输入电话号码的控件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;url&lt;/td&gt;
&lt;td&gt;HTML5 用于编辑URL的字段&lt;/td&gt;
&lt;td&gt;可以校验URL地址格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;file&lt;/td&gt;
&lt;td&gt;此控件可以让用户选择文件，用于文件上传。&lt;/td&gt;
&lt;td&gt;使用 accept 属性可以定义控件可以选择的文件类型。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hidden&lt;/td&gt;
&lt;td&gt;此控件用户在页面上不可见，但它的值会被提交到服务器，用于传递隐藏值&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;type属性演示&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;#&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
        &amp;lt;label for=&quot;username&quot;&amp;gt;用户名：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;/&amp;gt;  &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;password&quot;&amp;gt;密码：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;email&quot;&amp;gt;邮箱：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;gender&quot;&amp;gt;性别：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;radio&quot; id=&quot;gender&quot; name=&quot;gender&quot; value=&quot;men&quot;/&amp;gt;男
        &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;women&quot;/&amp;gt;女
        &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;other&quot;/&amp;gt;其他&amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;hobby&quot;&amp;gt;爱好：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; id=&quot;hobby&quot; name=&quot;hobby&quot; value=&quot;music&quot; checked/&amp;gt;音乐
        &amp;lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;game&quot;/&amp;gt;游戏 &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;birthday&quot;&amp;gt;生日：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;date&quot; id=&quot;birthday&quot; name=&quot;birthday&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;time&quot;&amp;gt;当前时间：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;time&quot; id=&quot;time&quot; name=&quot;time&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;insert&quot;&amp;gt;注册时间：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;datetime-local&quot; id=&quot;insert&quot; name=&quot;insert&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;age&quot;&amp;gt;年龄：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; name=&quot;age&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;range&quot;&amp;gt;心情值(1~10)：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;range&quot; id=&quot;range&quot; name=&quot;range&quot; min=&quot;1&quot; max=&quot;10&quot; step=&quot;1&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;search&quot;&amp;gt;可全部清除文本：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;search&quot; id=&quot;search&quot; name=&quot;search&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;tel&quot;&amp;gt;电话：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;tel&quot; id=&quot;tel&quot; name=&quot;tel&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;url&quot;&amp;gt;个人网站：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;url&quot; id=&quot;url&quot; name=&quot;url&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;file&quot;&amp;gt;文件上传：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;file&quot; id=&quot;file&quot; name=&quot;file&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;label for=&quot;hidden&quot;&amp;gt;隐藏信息：&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; id=&quot;hidden&quot; name=&quot;hidden&quot; value=&quot;itheima&quot;/&amp;gt; &amp;lt;br/&amp;gt;

        &amp;lt;button type=&quot;submit&quot;&amp;gt;提交&amp;lt;/button&amp;gt;
        &amp;lt;button type=&quot;reset&quot;&amp;gt;重置&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E6%A0%87%E7%AD%BEinput%E5%B1%9E%E6%80%A7-type.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;选择控件&lt;/h4&gt;
&lt;p&gt;下拉列表标签&amp;lt;select&amp;gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select name=&quot;&quot;&amp;gt;
	&amp;lt;option value=&quot;&quot;&amp;gt;显示的内容&amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;option：选择菜单的选项&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;optgroup：列表项分组标签
属性：label设置分组名称&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;文本域控件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;textarea name=&quot;textarea&quot; rows=&quot;10&quot; cols=&quot;50&quot;&amp;gt;Write something here&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;name-标签名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rows-行数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cols-列数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;#&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
        所在城市：&amp;lt;select name=&quot;city&quot;&amp;gt;
            &amp;lt;option&amp;gt;---请选择城市---&amp;lt;/option&amp;gt;
            &amp;lt;optgroup label=&quot;直辖市&quot;&amp;gt;
                &amp;lt;option&amp;gt;北京&amp;lt;/option&amp;gt;
                &amp;lt;option&amp;gt;上海&amp;lt;/option&amp;gt;
            &amp;lt;/optgroup&amp;gt;
        &amp;lt;optgroup label=&quot;省会市&quot;&amp;gt;
            &amp;lt;option&amp;gt;杭州&amp;lt;/option&amp;gt;
            &amp;lt;option&amp;gt;武汉&amp;lt;/option&amp;gt;
        &amp;lt;/optgroup&amp;gt;
    &amp;lt;/select&amp;gt;
        &amp;lt;br/&amp;gt;
        个人介绍：&amp;lt;textarea name=&quot;desc&quot; rows=&quot;5&quot; cols=&quot;20&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E6%A0%87%E7%AD%BEselect%E5%92%8C%E6%96%87%E6%9C%AC%E5%9F%9F%E5%B1%9E%E6%80%A7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;分组控件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;#&quot; method=&quot;post&quot;&amp;gt;
	&amp;lt;fieldset&amp;gt;
		&amp;lt;legend&amp;gt;是否同意&amp;lt;/legend&amp;gt;
        &amp;lt;input type=&quot;radio&quot; id=&quot;radio_y&quot; name=&quot;agree&quot; value=&quot;y&quot;&amp;gt; 
      	&amp;lt;label for=&quot;radio_y&quot;&amp;gt;同意&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;radio&quot; id=&quot;radio_n&quot; name=&quot;agree&quot; value=&quot;n&quot;&amp;gt; 
      	&amp;lt;label for=&quot;radio_n&quot;&amp;gt;不同意&amp;lt;/label&amp;gt;
	&amp;lt;/fieldset&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;form action=&quot;#&quot; method=&quot;post&quot;&amp;gt;
&amp;lt;fieldset&amp;gt;
&amp;lt;legend&amp;gt;是否同意&amp;lt;/legend&amp;gt;
&amp;lt;input type=&quot;radio&quot; id=&quot;radio_y&quot; name=&quot;agree&quot; value=&quot;y&quot;&amp;gt;
&amp;lt;label for=&quot;radio_y&quot;&amp;gt;同意&amp;lt;/label&amp;gt;
&amp;lt;input type=&quot;radio&quot; id=&quot;radio_n&quot; name=&quot;agree&quot; value=&quot;n&quot;&amp;gt;
&amp;lt;label for=&quot;radio_n&quot;&amp;gt;不同意&amp;lt;/label&amp;gt;
&amp;lt;/fieldset&amp;gt;
&amp;lt;/form&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;表格标签&lt;/h3&gt;
&lt;h4&gt;基本属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; , 表示表格标签，表格是数据单元的行和列的两维表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tr：table row，表示表中单元的行&lt;/li&gt;
&lt;li&gt;td：table data，表示表中一个单元格&lt;/li&gt;
&lt;li&gt;th：table header，表格单元格的表头，通常字体样式加粗居中&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E8%A1%A8%E6%A0%BC%E6%A0%87%E7%AD%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码展示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;th&amp;gt;First name&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Last name&amp;lt;/th&amp;gt;
      &amp;lt;/tr&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;John&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Doe&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Jane&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Doe&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图：&lt;/p&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th&amp;gt;First name&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;Last name&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;John&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Doe&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;Jane&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Doe&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;跨行跨列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table width=&quot;400px&quot; border=&quot;1px&quot; align=&quot;center&quot;&amp;gt;
    &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;姓名&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;性别&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;年龄&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;数学&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;语文&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;

    &amp;lt;tbody&amp;gt;
        &amp;lt;tr align=&quot;center&quot;&amp;gt;
            &amp;lt;td&amp;gt;张三&amp;lt;/td&amp;gt;
            &amp;lt;td rowspan=&quot;2&quot;&amp;gt;男&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;23&amp;lt;/td&amp;gt;
            &amp;lt;td colspan=&quot;2&quot;&amp;gt;90&amp;lt;/td&amp;gt;
            &amp;lt;!--&amp;lt;td&amp;gt;90&amp;lt;/td&amp;gt;--&amp;gt;
        &amp;lt;/tr&amp;gt;

        &amp;lt;tr align=&quot;center&quot;&amp;gt;
            &amp;lt;td&amp;gt;李四&amp;lt;/td&amp;gt;
            &amp;lt;!--&amp;lt;td&amp;gt;男&amp;lt;/td&amp;gt;--&amp;gt;
            &amp;lt;td&amp;gt;24&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;95&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;98&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/tbody&amp;gt;

    &amp;lt;tfoot&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td colspan=&quot;4&quot;&amp;gt;总分数：&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;373&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/tfoot&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E8%A1%A8%E6%A0%BC%E6%A0%87%E7%AD%BE%E8%B7%A8%E8%A1%8C%E8%B7%A8%E5%88%97%E6%95%88%E6%9E%9C%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;表格结构&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;thead&lt;/td&gt;
&lt;td&gt;定义表格的列头的行&lt;/td&gt;
&lt;td&gt;一个表格中仅有一个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tbody&lt;/td&gt;
&lt;td&gt;定义表格的主体&lt;/td&gt;
&lt;td&gt;用来封装一组表行（tr元素）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tfoot&lt;/td&gt;
&lt;td&gt;定义表格的各列汇总行&lt;/td&gt;
&lt;td&gt;一个表格中仅有一个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;样式布局&lt;/h3&gt;
&lt;h4&gt;基本格式&lt;/h4&gt;
&lt;p&gt;在head标签中，通过style标签加入样式。&lt;/p&gt;
&lt;p&gt;基本格式：可以含有多个属性，一个属性名也可以含有多个值，同时设置多样式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
    标签名{
        属性名1:属性值1;
        属性名2:属性值2;
        属性名:属性值1 属性值2 属性值3; 
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;背景格式&lt;/h4&gt;
&lt;p&gt;background属性用来设置背景相关的样式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;背景色
[&lt;code&gt;background-color&lt;/code&gt;]属性定义任何元素的背景色&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  background-color: #567895;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;背景图
该[&lt;code&gt;background-image&lt;/code&gt;]属性允许在元素的背景中显示图像。使用url函数指定图片路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;背景图片&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        body{
            /*添加背景图片*/
            background: url(&quot;../img/bg.png&quot;);
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E8%83%8C%E6%99%AF%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;背景重复&lt;/p&gt;
&lt;p&gt;[&lt;code&gt;background-repeat&lt;/code&gt;]属性用于控制图像的平铺行为。可用值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-repeat&lt;/code&gt; -停止完全重复背景&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repeat-x&lt;/code&gt; —水平重复&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repeat-y&lt;/code&gt; —竖直重复&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repeat&lt;/code&gt;—默认值；双向重复&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;body {
  background-image: url(star.png);
  background-repeat: repeat-x;/*水平重复*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E8%83%8C%E6%99%AF%E8%AE%BE%E8%AE%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;div布局&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;div简单布局：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;broader：边界&lt;/li&gt;
&lt;li&gt;solid：实线&lt;/li&gt;
&lt;li&gt;blue：颜色&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
     div{ border: 1px solid blue;}
&amp;lt;/style&amp;gt;

&amp;lt;div &amp;gt;left&amp;lt;/div&amp;gt;
&amp;lt;div &amp;gt;center&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;right&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div%E7%AE%80%E5%8D%95%E5%B8%83%E5%B1%80.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;class值
可以设置宽度，浮动，背景&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.class值{
    属性名:属性值;
}

&amp;lt;标签名 class=&quot;class值&quot;&amp;gt;  
 提示: class是自定义的值
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;background：背景颜色&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;width：宽度 (npx 或者 n%)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;height：长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;text-align：文本对齐方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;background-image: url(&quot;../img/bg.png&quot;)：背景图&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;float：浮动&lt;/p&gt;
&lt;p&gt;指定一个元素应沿其容器的左侧或右侧放置，允许文本或者内联元素环绕它，该元素从网页的正常流动中移除，其他部分保持正常文档流顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 加入浮动 --&amp;gt;
float：none；不浮动
float：left；左浮动
float：right；右浮动

&amp;lt;!-- 清除浮动 --&amp;gt;
clear：both；清除两侧浮动，此元素不再收浮动元素布局影响。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;div基本布局&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;样式演示&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        /*给div标签添加边框*/
        div{
            border: 1px solid red;
        }

        /*左侧图片的div样式*/
        .left{
            width: 20%;
            float: left;
            height: 500px;
        }

        /*中间正文的div样式*/
        .center{
            width: 59%;
            float: left;
            height: 500px;
        }

        /*右侧广告图片的div样式*/
        .right{
            width: 20%;
            float: left;
            height: 500px;
        }

        /*底部超链接的div样式*/
        .footer{
            /*清除浮动效果*/
            clear: both;
            /*文本对齐方式*/
            text-align: center;
            /*背景颜色*/
            background: blue;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!--顶部登陆注册--&amp;gt;
    &amp;lt;div&amp;gt;top&amp;lt;/div&amp;gt;

    &amp;lt;!--导航条--&amp;gt;
    &amp;lt;div&amp;gt;navibar&amp;lt;/div&amp;gt;

    &amp;lt;!--左侧图片--&amp;gt;
    &amp;lt;div class=&quot;left&quot;&amp;gt;left&amp;lt;/div&amp;gt;

    &amp;lt;!--中间正文--&amp;gt;
    &amp;lt;div class=&quot;center&quot;&amp;gt;center&amp;lt;/div&amp;gt;

    &amp;lt;!--右侧广告图片--&amp;gt;
    &amp;lt;div class=&quot;right&quot;&amp;gt;right&amp;lt;/div&amp;gt;

    &amp;lt;!--底部页脚超链接--&amp;gt;
    &amp;lt;div class=&quot;footer&quot;&amp;gt;footer&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div%E5%9F%BA%E6%9C%AC%E5%B8%83%E5%B1%80.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;语义化标签&lt;/h3&gt;
&lt;p&gt;为了更好的组织文档，HTML5规范中设计了几个语义元素，可以将特殊含义传达给浏览器。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;header&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;标头元素&lt;/td&gt;
&lt;td&gt;表示内容的介绍&lt;/td&gt;
&lt;td&gt;块元素，文档中可以定义多个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;nav&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;导航元素&lt;/td&gt;
&lt;td&gt;表示导航链接&lt;/td&gt;
&lt;td&gt;常见于网站的菜单，目录和索引等，可以嵌套在header中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;article&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;文章元素&lt;/td&gt;
&lt;td&gt;表示独立内容区域&lt;/td&gt;
&lt;td&gt;标签定义的内容本身必须是有意义且必须独立于文档的其他部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;footer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;页脚元素&lt;/td&gt;
&lt;td&gt;表示页面的底部&lt;/td&gt;
&lt;td&gt;块元素，文档中可以定义多个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/%E8%AF%AD%E4%B9%89%E5%8C%96%E6%A0%87%E7%AD%BE%E7%BB%93%E6%9E%84%E5%9B%BE.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;HTML拓展&lt;/h2&gt;
&lt;h3&gt;音频标签&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt;：用于播放声音，比如音乐或其他音频流，是 HTML 5 的新标签。&lt;/p&gt;
&lt;p&gt;常用属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;取值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;src&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;音频资源的路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;音频准备就绪后自动播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;显示控件，比如播放按钮。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;表示循环播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;preload&lt;/td&gt;
&lt;td&gt;preload&lt;/td&gt;
&lt;td&gt;音频在页面加载时进行预加载。&amp;lt;br /&amp;gt;如果使用 &quot;autoplay&quot;，则忽略该属性。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;HTML5媒体标签-音频audio&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;audio src=&quot;media/horse.ogg&quot; controls&amp;gt;
你的浏览器不支持 audio 标签。
&amp;lt;/audio&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;视频标签&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签用于播放视频，比如电影片段或其他视频流，是 HTML 5 的新标签。&lt;/p&gt;
&lt;p&gt;常用属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;取值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;src&lt;/td&gt;
&lt;td&gt;&lt;em&gt;URL&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;要播放的视频的 URL。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;设置视频播放器的宽度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;设置视频播放器的高度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;视频在就绪后自动播放。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;control&lt;/td&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;显示控件，比如播放按钮。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;如果出现该属性，则当媒介文件完成播放后再次开始播放。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;preload&lt;/td&gt;
&lt;td&gt;preload&lt;/td&gt;
&lt;td&gt;视频在页面加载时进行加载。&amp;lt;br /&amp;gt;如果使用 &quot;autoplay&quot;，则忽略该属性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mute&lt;/td&gt;
&lt;td&gt;muted&lt;/td&gt;
&lt;td&gt;规定视频的音频输出应该被静音。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;poste&lt;/td&gt;
&lt;td&gt;&lt;em&gt;URL&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;视频下载时显示的图像，或者视频播放前显示的图像。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;HTML5媒体标签-视频video&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;video src=&quot;media/movie.ogg&quot; controls&amp;gt;
        你的浏览器不支持 video 标签
    &amp;lt;/video&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML%E6%A0%87%E7%AD%BEvideo.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;回到顶部&lt;/h3&gt;
&lt;p&gt;在html里面锚点的作用: 通过a标签跳转到指定的位置.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;#aId&quot;&amp;gt;回到顶部&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;a href=&quot;#aId&quot;&amp;gt;回到顶部&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;详情概要&lt;/h3&gt;
&lt;p&gt;summary标签来描述概要信息, 利用details标签来描述详情信息. 默认情况下是折叠展示, 想看见详情必须点击&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;details&amp;gt;
    &amp;lt;summary&amp;gt;概要信息&amp;lt;/summary&amp;gt;
        详情信息
&amp;lt;/details&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;概要信息&amp;lt;/summary&amp;gt;
详情信息
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;CSS&lt;/h1&gt;
&lt;h2&gt;CSS入门&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;CSS (层叠样式表——Cascading Style Sheets，缩写为 &lt;strong&gt;CSS&lt;/strong&gt;），简单的说，它是用于设置和布局网页的计算机语言。会告知浏览器如何渲染页面元素。例如，调整内容的字体，颜色，大小等样式，设置边框的样式，调整模块的间距等。&lt;/p&gt;
&lt;p&gt;层叠：是指样式表允许以多种方式规定样式信息。可以规定在单个元素中，可以在页面头元素中，也可以在另一个CSS文件中，规定的方式会有次序的差别。&lt;/p&gt;
&lt;p&gt;样式：是指丰富的样式外观。拿边框距离来说，允许任何设置边框，允许设置边框与框内元素的距离，允许设置边框与边框的距离等等。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;组成&lt;/h3&gt;
&lt;p&gt;CSS是一门基于规则的语言—你能定义用于你的网页中&lt;strong&gt;特定元素&lt;/strong&gt;的一组&lt;strong&gt;样式规则&lt;/strong&gt;。这里面提到了两个概念，一是特定元素，二是样式规则。对应CSS的语法，也就是&lt;strong&gt;选择器（&lt;em&gt;selects&lt;/em&gt;）&lt;strong&gt;和&lt;/strong&gt;声明（&lt;em&gt;eclarations&lt;/em&gt;）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择器：指定要添加样式的 HTML元素的方式。可以使用标签名，class值，id值等多种方式。&lt;/li&gt;
&lt;li&gt;声明：形式为&lt;strong&gt;属性(property):值(value)&lt;/strong&gt;，用于设置特定元素的属性信息。
&lt;ul&gt;
&lt;li&gt;属性：指示文体特征，例如&lt;code&gt;font-size&lt;/code&gt;，&lt;code&gt;width&lt;/code&gt;，&lt;code&gt;background-color&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;值：每个指定的属性都有一个值，该值指示您如何更改这些样式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;选择器 {
    属性名:属性值;
    属性名:属性值;
    属性名:属性值;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS%E7%9A%84%E7%BB%84%E6%88%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;title&amp;gt;页面标题&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
h1{
font-size:40px; /* 设置字体大小为100像素*/
}
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;今天开始学CSS&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;CSS语法&lt;/h2&gt;
&lt;h3&gt;注释方式&lt;/h3&gt;
&lt;p&gt;CSS中的注释以&lt;code&gt;/*&lt;/code&gt;和开头&lt;code&gt;*/&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 设置h1的样式 */
h1 {
  color: blue;
  background-color: yellow;
  border: 1px solid black;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;引入方式&lt;/h3&gt;
&lt;h4&gt;内联样式&lt;/h4&gt;
&lt;p&gt;内联样式是CSS声明在元素的&lt;code&gt;style&lt;/code&gt;属性中，仅影响一个元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;标签 style=&quot;属性名:属性值; 属性名:属性值;&quot;&amp;gt;内容&amp;lt;/标签&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1 style=&quot;color: blue;background-color: yellow;border: 1px solid black;&quot;&amp;gt;
    Hello World!
&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;效果：&lt;/p&gt;
&lt;p&gt;&amp;lt;h1 style=&quot;color: blue;background-color: yellow;border: 1px solid black;&quot;&amp;gt;
Hello World!
&amp;lt;/h1&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：格式简单，但是样式作用无法复用到多个元素上，不利于维护&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;内部样式表&lt;/h4&gt;
&lt;p&gt;内部样式表是将CSS样式放在&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/style&quot;&gt;style&lt;/a&gt;标签中，通常style标签编写在HTML 的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/head&quot;&gt;head&lt;/a&gt;标签内部。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
    &amp;lt;style&amp;gt;
        选择器 {
            属性名: 属性值;
            属性名: 属性值;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;head&amp;gt;
    &amp;lt;style&amp;gt;
      h1 {
        color: blue;
        background-color: yellow;
        border: 1px solid black;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：内部样式只能作用在当前页面上，如果是多个页面，就无法复用了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;外部样式表&lt;/h4&gt;
&lt;p&gt;外部样式表是CSS附加到文档中的最常见和最有用的方法，因为您可以将CSS文件链接到多个页面，从而允许您使用相同的样式表设置所有页面的样式。&lt;/p&gt;
&lt;p&gt;外部样式表是指将CSS编写在扩展名为&lt;code&gt;.css&lt;/code&gt; 的单独文件中，并从HTML&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; 元素引用它，通常link标签`编写在HTML 的[head]标签内部。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;css文件&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;rel：表示“关系 (relationship) ”，属性值指链接方式与包含它的文档之间的关系，引入css文件固定值为stylesheet。&lt;/li&gt;
&lt;li&gt;href：属性需要引用某文件系统中的一个文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;举例&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建styles.css文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;h1 {
  color: blue;
  background-color: yellow;
  border: 1px solid black;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;link标签引入文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Hello World!&amp;lt;/h1&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果同上&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了CSS文件的管理，在项目中创建一个&lt;code&gt;css文件夹&lt;/code&gt;，专门保存样式文件，并调整指定的路径以匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;../css/styles.css&quot;&amp;gt;
&amp;lt;!--..代表上一级 相对路径--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;优先级&lt;/h4&gt;
&lt;p&gt;规则层叠于一个样式表中，其中数字 4 拥有最高的优先权：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器缺省设置&lt;/li&gt;
&lt;li&gt;外部样式表&lt;/li&gt;
&lt;li&gt;内部样式表（位于 &amp;lt;head&amp;gt; 标签内部）&lt;/li&gt;
&lt;li&gt;内联样式（在 HTML 元素内部）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;选择器&lt;/h3&gt;
&lt;h4&gt;介绍选择器&lt;/h4&gt;
&lt;p&gt;为了样式化某些元素，我们会通过选择器来选中HTML文档中的这些元素，每个CSS规则都以一个选择器或一组选择器为开始，去告诉浏览器这些规则应该应用到哪些元素上。&lt;/p&gt;
&lt;p&gt;选择器的分类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;基本选择器&lt;/td&gt;
&lt;td&gt;元素选择器&lt;/td&gt;
&lt;td&gt;标签名&lt;/td&gt;
&lt;td&gt;基于标签名匹配元素&lt;/td&gt;
&lt;td&gt;div{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;类选择器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基于class属性值匹配元素&lt;/td&gt;
&lt;td&gt;.center{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ID选择器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基于id属性值匹配元素&lt;/td&gt;
&lt;td&gt;#username{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;通用选择器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配文档中的所有内容&lt;/td&gt;
&lt;td&gt;*{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;属性选择器&lt;/td&gt;
&lt;td&gt;属性选择器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基于某属性匹配元素&lt;/td&gt;
&lt;td&gt;[type]{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;伪类选择器&lt;/td&gt;
&lt;td&gt;伪类选择器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用于向某些选择器添加特殊的效果&lt;/td&gt;
&lt;td&gt;a:hover{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组合选择器&lt;/td&gt;
&lt;td&gt;分组选择器&lt;/td&gt;
&lt;td&gt;,&lt;/td&gt;
&lt;td&gt;使用 , 号结合两个选择器，匹配两个选择器的元素&lt;/td&gt;
&lt;td&gt;span,p{}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;后代选择器&lt;/td&gt;
&lt;td&gt;空格&lt;/td&gt;
&lt;td&gt;使用空格符号结合两个选择器，基于&amp;lt;br /&amp;gt;第一个选择器，匹配第二个选择器的所有后代元素&lt;/td&gt;
&lt;td&gt;.top li{ }&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;基本选择器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;div1&amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;cls&quot;&amp;gt;div2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;cls&quot;&amp;gt;div3&amp;lt;/div&amp;gt;

    &amp;lt;div id=&quot;d1&quot;&amp;gt;div4&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;d2&quot;&amp;gt;div5&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元素选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*选择所有div标签,字体为蓝色*/
div{
	 color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*选择class为cls的,字体为蓝色*/
.cls{
	color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ID选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*id选择器*/
#d1{
    color: green;/*id为d1的字体变成绿色*/
}

#d2{
    color: pink;/*id为d2的字体变成粉色*/
}/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通用选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*所有标签 */
*{
    background-color: aqua;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;属性选择器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    用户名：&amp;lt;input type=&quot;text&quot;/&amp;gt; &amp;lt;br/&amp;gt;
    密码：&amp;lt;input type=&quot;password&quot;/&amp;gt; &amp;lt;br&amp;gt;
    邮箱：&amp;lt;input type=&quot;email&quot;/&amp;gt; &amp;lt;br&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*输入框中输入的字符是红色*/
[type] {
    color: red;
}
/*输入框中输入的字符是蓝色*/
[type=password] {
    color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;伪类选择器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;a href=&quot;https://www.baidu.com&quot; target=&quot;_blank&quot;&amp;gt;百度一下&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪类选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*未访问的状态*/
a:link{
	color: black;
}

/*已访问的状态*/
a:visited{
	color: blue;
}

/*鼠标悬浮的状态*/
a:hover{
	color: red;
}

/*已选中的状态*/
a:active{
	color: yellow;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意：伪类顺序 link ，visited，hover，active，否则有可能失效。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;组合选择器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;span&amp;gt;span&amp;lt;/span&amp;gt; &amp;lt;br/&amp;gt;
    &amp;lt;p&amp;gt;段落&amp;lt;/p&amp;gt;
    
    &amp;lt;div class=&quot;top&quot;&amp;gt;
        &amp;lt;ol&amp;gt;
            &amp;lt;li&amp;gt;aa&amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;bb&amp;lt;/li&amp;gt;
        &amp;lt;/ol&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;center&quot;&amp;gt;
        &amp;lt;ol&amp;gt;
            &amp;lt;li&amp;gt;cc&amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;dd&amp;lt;/li&amp;gt;
        &amp;lt;/ol&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分组选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*span p两个标签下的字体为蓝色*/
span,p{
	color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后代选择器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*class为top下的所有li标签字体颜色为红色*/
.top li{
	color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;优先级&lt;/h4&gt;
&lt;p&gt;选择器优先级&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ID选择器 &amp;gt; 类选择器 &amp;gt; 标签选择器 &amp;gt; 通用选择器&lt;/li&gt;
&lt;li&gt;如果优先级相同，那么就满足就近原则&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;边框样式&lt;/h3&gt;
&lt;h4&gt;单个边框&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单个边框
border：边框
border-top: 上边框
border-left: 左边框
border-bottom: 底边框
border-right:  右边框&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无边框，当border值为none时，可以让边框不显示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
	width: 200px;
    height: 200px;
    border: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;圆角&lt;/p&gt;
&lt;p&gt;通过使用[&lt;code&gt;border-radius&lt;/code&gt;]属性设置盒子的圆角，虽然能分别设置四个角，但是通常我们使用一个值，来设置整体效果&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#d1{
    /*设置所有边框*/
    /*border: 5px solid black;*/

    /*设置上边框*/
    border-top: 5px solid black;
    /*设置左边框*/
    border-left: 5px double red;
    /*设置右边框*/
    border-right: 5px dotted blue;
    /*设置下边框*/
    border-bottom: 5px dashed pink;

    width: 150px;
    height: 150px;
}

#d2{
    border: 5px solid red;
    /*设置边框的弧度*/
    border-radius: 25px;
    width: 150px;
    height: 150px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;d1&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;br/&amp;gt;
    &amp;lt;div id=&quot;d2&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS-边框样式效果图.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;边框轮廓&lt;/h4&gt;
&lt;p&gt;轮廓&lt;strong&gt;outline&lt;/strong&gt;：是绘制于元素周围的一条线，位于边框边缘的外围，可起到突出元素的作用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性值：double：双实线   dotted：圆点   dashed：虚线   none：无&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;样式演示&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        input{
            outline: dotted;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    用户名：&amp;lt;input type=&quot;text&quot;/&amp;gt; &amp;lt;br/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS%E8%BE%B9%E6%A1%86%E8%BD%AE%E5%BB%93%E6%95%88%E6%9E%9C%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;盒子模型&lt;/h4&gt;
&lt;h5&gt;模型介绍&lt;/h5&gt;
&lt;p&gt;盒子模型是通过设置&lt;strong&gt;元素框&lt;/strong&gt;与&lt;strong&gt;元素内容&lt;/strong&gt;和&lt;strong&gt;外部元素&lt;/strong&gt;的边距，而进行布局的方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS%E7%9B%92%E5%AD%90%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;element : 元素。&lt;/li&gt;
&lt;li&gt;padding : 内边距，也有资料将其翻译为填充。&lt;/li&gt;
&lt;li&gt;border : 边框。&lt;/li&gt;
&lt;li&gt;margin : 外边距，也有资料将其翻译为空白或空白边。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;边距&lt;/h5&gt;
&lt;p&gt;内边距、边框和外边距都是可选的，默认值是零。在 CSS 中，width 和 height 指的是内容区域的宽度和高度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;外边距
单独设置边框的外边距，设置上、右、下、左方向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;margin-top
margin-right
margin-bottom
margin-left
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;margin:  auto /*浏览器自动计算外边距，具有居中效果。*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*  所有 4 个外边距都是 10px */
margin:10px;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两个值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;margin:10px 5px;/* 上外边距和下外边距是 10px*/
margin:10px auto;/*右外边距和左外边距是 5px */
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三个值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 上外边距是 10px，右外边距和左外边距是 5px，下外边距是 15px*/
margin:10px 5px 15px;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;四个值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*上外边距是 10px，右外边距是 5px，下外边距是 15px，左外边距是 20px*/
/*上右下外*/
margin:10px 5px 15px 20px;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内边距
与外边距类似，单独设置边框的内边距，设置上、右、下、左方向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;padding-top
padding-right
padding-bottom
padding-left
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;布局&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基本布局&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
       div{
           border: 2px solid blue;
       }
       .big{
           width: 200px;
           height: 200px;
       }
       .small{
           width: 100px;
           height: 100px;
           margin: 30px;/*  外边距 */
       }
&amp;lt;/style&amp;gt;

&amp;lt;div class=&quot;big&quot;&amp;gt;
    &amp;lt;div class=&quot;small&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS%E7%9B%92%E5%AD%90%E6%A8%A1%E5%BC%8F-%E6%95%88%E6%9E%9C%E5%9B%BE1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增加内边距会增加元素框的总尺寸&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;style&amp;gt;
	div{
    	border: 2px solid blue;
	}
	.big{
    	width: 200px;
    	height: 200px;
    	padding: 30px;/*内边距 */
	}
	.small{
		width: 100px;
    	height: 100px;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS%E7%9B%92%E5%AD%90%E6%A8%A1%E5%BC%8F-%E6%95%88%E6%9E%9C%E5%9B%BE2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;文本样式&lt;/h3&gt;
&lt;h4&gt;基本属性&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;属性取值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;宽度&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;高度&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;color&lt;/td&gt;
&lt;td&gt;颜色&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;font-family&lt;/td&gt;
&lt;td&gt;字体样式&lt;/td&gt;
&lt;td&gt;宋体、楷体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;font-size&lt;/td&gt;
&lt;td&gt;字体大小&lt;/td&gt;
&lt;td&gt;px : 像素，文本高度像素绝对数值。&amp;lt;br /&amp;gt;em : 1em等于当前元素的父元素设置的字体大小，是相对数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text-decoration&lt;/td&gt;
&lt;td&gt;下划线&lt;/td&gt;
&lt;td&gt;underline : 下划线  &amp;lt;br/&amp;gt;overline : 上划线 &amp;lt;br/&amp;gt;line-through : 删除线 &amp;lt;br/&amp;gt;none : 不要线条&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text-align&lt;/td&gt;
&lt;td&gt;文本水平对齐&lt;/td&gt;
&lt;td&gt;lef : 左对齐文本&amp;lt;br /&amp;gt;right : 右对齐文本&amp;lt;br /&amp;gt;center : 使文本居中 &amp;lt;br /&amp;gt;justify : 使文本散布，改变单词间的间距，使文本所有行具有相同宽度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;line-height&lt;/td&gt;
&lt;td&gt;行高，行间距&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vertical-align&lt;/td&gt;
&lt;td&gt;文本垂直对齐&lt;/td&gt;
&lt;td&gt;top：居上   bottom：居下  middle：居中   或者百分比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;display&lt;/td&gt;
&lt;td&gt;元素如何显示&lt;/td&gt;
&lt;td&gt;可以设置块级和行内元素的切换，也可以设置元素隐藏&amp;lt;br /&amp;gt;inline：内联元素(无换行、无长宽)   &amp;lt;br /&amp;gt;block：块级元素(有换行)  &amp;lt;br /&amp;gt;inline-block：内联元素(有长宽)  &amp;lt;br /&amp;gt;none：隐藏元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;div{
    color: /*red*/ #ff0000;
    font-family: /*宋体*/ 微软雅黑;
    font-size: 25px;/
    text-decoration: none;
    text-align: center;
    line-height: 60px;
}

span{
    /*文字垂直对齐  top：居上   bottom：居下  middle：居中   百分比*/
    vertical-align: 50%;     /*居中对齐*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
    我是文字
&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;
    我是文字
&amp;lt;/div&amp;gt;

&amp;lt;img src=&quot;../img/wx.png&quot; width=&quot;38px&quot; height=&quot;38px&quot;/&amp;gt;
&amp;lt;span&amp;gt;微信&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS-%E6%96%87%E6%9C%AC%E6%A0%B7%E5%BC%8F%E6%95%88%E6%9E%9C%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;文本显示&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;元素显示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*   把列表项显示为内联元素，无长宽*/
li {
    display:inline;
}
/*   把span元素作为块元素，有换行*/
span {
    display:block;
}
/*   行内块元素，结合的行内和块级的优点，既可以行内显示，又可以设置长宽，*/
li {
    display:inline-block;
}
/*所有div在一行显示*/
div{
    display: inline-block;
    width: 100px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元素隐藏&lt;/p&gt;
&lt;p&gt;当设置为none时，可以隐藏元素。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;CSS案例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/*背景图片*/
body{
    background: url(&quot;../img/bg.png&quot;);
}

/*中间表单样式*/
.center{
    background: white;      /*背景色*/
    width: 40%;             /*宽度*/
    margin: auto;           /*水平居中外边距*/
    margin-top: 100px;      /*上外边距*/
    border-radius: 15px;    /*边框弧度*/
    text-align: center;     /*文本水平居中*/
}

/*表头样式*/
thead th{
    font-size: 30px;    /*字体大小*/
    color: orangered;   /*字体颜色*/
}

/*表体提示信息样式*/
tbody label{
    font-size: 20px;    /*字体大小*/
}

/*表体输入框样式*/
tbody input{
    border: 1px solid gray; /*边框*/
    border-radius: 5px;     /*边框弧度*/
    width: 90%;             /*输入框的宽度*/
    height: 40px;           /*输入框的高度*/
    outline: none;          /*取消轮廓的样式*/
}

/*表底确定按钮样式*/
tfoot button{
    border: 1px solid crimson;  /*边框*/
    border-radius: 5px;         /*边框弧度*/
    width: 95%;                 /*宽度*/
    height: 40px;               /*高度*/
    background: crimson;        /*背景色*/
    color: white;               /*文字的颜色*/
    font-size: 20px;            /*字体大小*/
}

/*表行高度*/
tr{
    line-height: 60px;  /*行高*/
}

/*底部页脚样式*/
.footer{
    width: 35%; /*宽度*/
    margin: auto;   /*水平居中外边距*/
    font-size: 15px;    /*字体大小*/
    color: gray;    /*字体颜色*/
}

/*超链接样式*/
a{
    text-decoration: none;  /*去除超链接的下划线*/
    color: blue;            /*超链接颜色*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;登录页面&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;../css/login.css&quot;/&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!--顶部公司图标--&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;img src=&quot;../img/logo.png&quot;/&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!--中间表单--&amp;gt;
    &amp;lt;div class=&quot;center&quot;&amp;gt;
        &amp;lt;form action=&quot;#&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
            &amp;lt;table width=&quot;100%&quot;&amp;gt;
                &amp;lt;thead&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;th colspan=&quot;2&quot;&amp;gt;账&amp;amp;nbsp;密&amp;amp;nbsp;登&amp;amp;nbsp;录&amp;lt;hr/&amp;gt;&amp;lt;/th&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/thead&amp;gt;

                &amp;lt;tbody&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;label for=&quot;username&quot;&amp;gt;账号&amp;lt;/label&amp;gt;
                        &amp;lt;/td&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot; placeholder=&quot; 请输入账号&quot; required/&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;label for=&quot;password&quot;&amp;gt;密码&amp;lt;/label&amp;gt;
                        &amp;lt;/td&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; placeholder=&quot; 请输入密码&quot; required/&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/tbody&amp;gt;

                &amp;lt;tfoot&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td colspan=&quot;2&quot;&amp;gt;
                            &amp;lt;button type=&quot;submit&quot;&amp;gt;确&amp;amp;nbsp;定&amp;lt;/button&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/tfoot&amp;gt;
            &amp;lt;/table&amp;gt;
        &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!--底部页脚--&amp;gt;
    &amp;lt;div class=&quot;footer&quot;&amp;gt;
        &amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
        登录/注册即表示您同意&amp;amp;nbsp;&amp;amp;nbsp;
        &amp;lt;a href=&quot;#&quot; target=&quot;_blank&quot;&amp;gt;用户协议&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
        和&amp;amp;nbsp;&amp;amp;nbsp;
        &amp;lt;a href=&quot;#&quot; target=&quot;_blank&quot;&amp;gt;隐私条款&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;
        &amp;lt;a href=&quot;#&quot; target=&quot;_blank&quot;&amp;gt;忘记密码?&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;HTTP&lt;/h1&gt;
&lt;h2&gt;相关概念&lt;/h2&gt;
&lt;p&gt;HTTP：Hyper Text Transfer Protocol，意为超文本传输协议，是建立在 &lt;strong&gt;TCP/IP 协议&lt;/strong&gt;基础上，指的是服务器和客户端之间交互必须遵循的一问一答的规则，形容这个规则：问答机制、握手机制&lt;/p&gt;
&lt;p&gt;HTTP 协议是&lt;strong&gt;一个无状态的面向连接的协议&lt;/strong&gt;，指的是协议对于事务处理没有记忆能力，服务器不知道客户端是什么状态。所以打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系&lt;/p&gt;
&lt;p&gt;注意：无状态并不是代表 HTTP 就是 UDP，面向连接也不是代表 HTTP 就是TCP&lt;/p&gt;
&lt;p&gt;HTTP 作用：用于定义 WEB 浏览器与 WEB 服务器之间交换数据的过程和数据本身的内容&lt;/p&gt;
&lt;p&gt;浏览器和服务器交互过程：浏览器请求，服务请求响应&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求（请求行、请求头、请求体）&lt;/li&gt;
&lt;li&gt;响应（响应行、响应头、响应体）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;URL 和 URI&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;URL：统一资源定位符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式：http://127.0.0.1:8080/request/servletDemo01&lt;/li&gt;
&lt;li&gt;详解：http：协议；127.0.0.1：域名；8080：端口；request/servletDemo01：请求资源路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;URI：统一资源标志符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式：/request/servletDemo01&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;区别：&lt;code&gt;URL - HOST = URI&lt;/code&gt;，URI 是抽象的定义，URL 用地址定位，URI 用名称定位。&lt;strong&gt;只要能唯一标识资源的是 URI，在 URI 的基础上给出其资源的访问方式的是 URL&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;从浏览器地址栏输入 URL 到请求返回发生了什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进行 URL 解析，进行编码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DNS 解析，顺序是先查 hosts 文件是否有记录，有的话就会把相对应映射的 IP 返回，然后去本地 DNS 缓存中寻找，然后依次向本地域名服务器、根域名服务器、顶级域名服务器、权限域名服务器发起查询请求，最终返回 IP 地址给本地域名服务器&lt;/p&gt;
&lt;p&gt;本地域名服务器将得到的 IP 地址返回给操作系统，同时将 IP 地址缓存起来；操作系统将 IP 地址返回给浏览器，同时自己也将 IP 地址缓存起来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找到 IP 之后，进行 TCP 协议的三次握手建立连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发出 HTTP 请求，取文件指令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器处理请求，返回响应&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放 TCP 连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器解析渲染页面&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐阅读：https://xiaolincoding.com/network/&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;版本区别&lt;/h2&gt;
&lt;p&gt;版本介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/0.9 仅支持 GET 请求，不支持请求头&lt;/li&gt;
&lt;li&gt;HTTP/1.0 默认短连接（一次请求建议一次 TCP 连接，请求完就断开），支持 GET、POST、 HEAD 请求&lt;/li&gt;
&lt;li&gt;HTTP/1.1 默认长连接（一次 TCP 连接可以多次请求）；支持 PUT、DELETE、PATCH 等六种请求；增加 HOST 头，支持虚拟主机；支持&lt;strong&gt;断点续传&lt;/strong&gt;功能&lt;/li&gt;
&lt;li&gt;HTTP/2.0 多路复用，降低开销（一次 TCP 连接可以处理多个请求）；服务器主动推送（相关资源一个请求全部推送）；解析基于二进制，解析错误少，更高效（HTTP/1.X 解析基于文本）；报头压缩，降低开销&lt;/li&gt;
&lt;li&gt;HTTP/3.0 QUIC (Quick UDP Internet Connections)，快速 UDP 互联网连接，基于 UDP 协议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTTP 1.0 和 HTTP 1.1 的主要区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;长短连接：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在HTTP/1.0中，默认使用的是短连接&lt;/strong&gt;，每次请求都要重新建立一次连接，比如获取 HTML 和 CSS 文件，需要两次请求。HTTP 基于 TCP/IP 协议的，每一次建立或者断开连接都需要三次握手四次挥手，开销会比较大&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP 1.1起，默认使用长连接&lt;/strong&gt; ，默认开启 &lt;code&gt;Connection: keep-alive&lt;/code&gt;，Keep-Alive 有一个保持时间，不会永久保持连接。持续连接有非流水线方式和流水线方式 ，流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文，非流水线方式是客户端在收到前一个响应后才能发送下一个请求&lt;/p&gt;
&lt;p&gt;HTTP 协议的长连接和短连接，实质上是 TCP 协议的长连接和短连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;错误状态响应码：在 HTTP1.1 中新增了 24 个错误状态响应码，如 409（Conflict）表示请求的资源与资源的当前状态发生冲突，410（Gone）表示服务器上的某个资源被永久性的删除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存处理：在 HTTP1.0 中主要使用 header 里的 If-Modified-Since，Expires 来做为缓存判断的标准，HTTP1.1 则引入了更多的缓存控制策略，例如 Entity tag，If-Unmodified-Since，If-Match，If-None-Match等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;带宽优化及网络连接的使用：HTTP1.0 存在一些浪费带宽的现象，例如客户端只需要某个对象的一部分，而服务器却将整个对象送过来了，并且不支持&lt;strong&gt;断点续传&lt;/strong&gt;功能，HTTP1.1 则在请求头引入了 range 头域，允许只&lt;strong&gt;请求资源的某个部分&lt;/strong&gt;，即返回码是 206（Partial Content），这样就方便了开发者自由的选择以便于充分利用带宽和连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HOST 头处理：在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址，因此请求消息中的 URL 并没有传递主机名。HTTP1.1 时代虚拟主机技术发展迅速，在一台物理服务器上可以存在多个虚拟主机，并且共享一个 IP 地址，故 HTTP1.1 增加了 HOST 信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTTP 1.1 和 HTTP 2.0 的主要区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新的二进制格式：HTTP1.1 基于文本格式传输数据，HTTP2.0 采用二进制格式传输数据，解析更高效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多路复用&lt;/strong&gt;：在一个连接里，允许同时发送多个请求或响应，并且这些请求或响应能够并行的传输而不被阻塞，避免 HTTP1.1 出现的队头堵塞问题&lt;/li&gt;
&lt;li&gt;头部压缩，HTTP1.1 的 header 带有大量信息，而且每次都要重复发送；HTTP2.0 把 header 从数据中分离，并封装成头帧和数据帧，使用特定算法压缩头帧。并且 HTTP2.0 在客户端和服务器端记录了之前发送的键值对，对于相同的数据不会重复发送。比如请求 A 发送了所有的头信息字段，请求 B 则只需要发送差异数据，这样可以减少冗余数据，降低开销&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端推送&lt;/strong&gt;：HTTP2.0 允许服务器向客户端推送资源，无需客户端发送请求到服务器获取&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;安全请求&lt;/h2&gt;
&lt;p&gt;HTTP 和 HTTPS 的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端口 ：HTTP 默认使用端口 80，HTTPS 默认使用端口 443&lt;/li&gt;
&lt;li&gt;安全性：HTTP 协议运行在 TCP 之上，所有传输的内容都是明文，客户端和服务器端都无法验证对方的身份；HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议，SSL/TLS 运行在 TCP 之上，所有传输的内容都经过加密，加密采用对称加密，但对称加密的密钥用服务器方的证书进行了非对称加密&lt;/li&gt;
&lt;li&gt;资源消耗：HTTP 安全性没有 HTTPS 高，但是 HTTPS 比 HTTP 耗费更多服务器资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对称加密和非对称加密&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对称加密：加密和解密使用同一个秘钥，把密钥转发给需要发送数据的客户机，中途会被拦截（类似于把带锁的箱子和钥匙给别人，对方打开箱子放入数据，上锁后发送），私钥用来解密数据，典型的对称加密算法有 DES、AES 等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：运算速度快&lt;/li&gt;
&lt;li&gt;缺点：无法安全的将密钥传输给通信方&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非对称加密：加密和解密使用不同的秘钥，一把作为公开的公钥，另一把作为私钥，&lt;strong&gt;公钥公开给任何人&lt;/strong&gt;（类似于把锁和箱子给别人，对方打开箱子放入数据，上锁后发送），典型的非对称加密算法有 RSA、DSA 等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公钥加密，私钥解密：为了&lt;strong&gt;保证内容传输的安全&lt;/strong&gt;，因为被公钥加密的内容，其他人是无法解密的，只有持有私钥的人，才能解密出实际的内容&lt;/li&gt;
&lt;li&gt;私钥加密，公钥解密：为了&lt;strong&gt;保证消息不会被冒充&lt;/strong&gt;，因为私钥是不可泄露的，如果公钥能正常解密出私钥加密的内容，就能证明这个消息是来源于持有私钥身份的人发送的&lt;/li&gt;
&lt;li&gt;可以更安全地将公开密钥传输给通信发送方，但是运算速度慢&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用对称加密和非对称加密的方式传送数据&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用非对称密钥加密方式，传输对称密钥加密方式所需要的 Secret Key，从而保证安全性&lt;/li&gt;
&lt;li&gt;获取到 Secret Key 后，再使用对称密钥加密方式进行通信，从而保证效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;思想：锁上加锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;名词解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;哈希算法：通过哈希函数计算出内容的哈希值，传输到对端后会重新计算内容的哈希，进行哈希比对来校验内容的完整性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数字签名：附加在报文上的特殊加密校验码，可以防止报文被篡改。一般是通过私钥对内容的哈希值进行加密，公钥正常解密并对比哈希值后，可以确保该内容就是对端发出的，防止出现中间人替换的问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数字证书：由权威机构给某网站颁发的一种认可凭证&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTTPS 工作流程：服务器端的公钥和私钥，用来进行非对称加密，客户端生成的随机密钥，用来进行对称加密&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP-HTTPS%E5%8A%A0%E5%AF%86%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端向服务器发起 HTTPS 请求，连接到服务器的 443 端口，请求携带了浏览器支持的加密算法和哈希算法，协商加密算法&lt;/li&gt;
&lt;li&gt;服务器端会向数字证书认证机构注册公开密钥，认证机构&lt;strong&gt;用 CA 私钥&lt;/strong&gt;对公开密钥做数字签名后绑定在数字证书（又叫公钥证书，内容有公钥，网站地址，证书颁发机构，失效日期等）&lt;/li&gt;
&lt;li&gt;服务器将数字证书发送给客户端，私钥由服务器持有&lt;/li&gt;
&lt;li&gt;客户端收到服务器端的数字证书后&lt;strong&gt;通过 CA 公钥&lt;/strong&gt;（事先置入浏览器或操作系统）对证书进行检查，验证其合法性。如果公钥合格，那么客户端会生成一个随机值，这个随机值就是用于进行对称加密的密钥，将该密钥称之为 client key（客户端密钥、会话密钥）。用服务器的公钥对客户端密钥进行非对称加密，这样客户端密钥就变成密文，HTTPS 中的第一次 HTTP 请求结束&lt;/li&gt;
&lt;li&gt;客户端会发起 HTTPS 中的第二个 HTTP 请求，将加密之后的客户端密钥发送给服务器&lt;/li&gt;
&lt;li&gt;服务器接收到客户端发来的密文之后，会用自己的私钥对其进行非对称解密，解密之后的明文就是客户端密钥，然后用客户端密钥对数据进行对称加密，这样数据就变成了密文&lt;/li&gt;
&lt;li&gt;服务器将加密后的密文发送给客户端&lt;/li&gt;
&lt;li&gt;客户端收到服务器发送来的密文，用客户端密钥对其进行对称解密，得到服务器发送的数据，这样 HTTPS 中的第二个 HTTP 请求结束，整个 HTTPS 传输完成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/linianhui/p/security-https-workflow.html&lt;/p&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/14cd2c9d2cd2&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;请求部分&lt;/h2&gt;
&lt;p&gt;请求行： 永远位于请求的第一行&lt;/p&gt;
&lt;p&gt;请求头： 从第二行开始，到第一个空行结束&lt;/p&gt;
&lt;p&gt;请求体： 从第一个空行后开始，到正文的结束（GET 没有）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;POST&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP%E8%AF%B7%E6%B1%82%E9%83%A8%E5%88%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GET&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【请求行】
GET /myApp/success.html?username=zs&amp;amp;password=123456 HTTP/1.1

【请求头】
Accept: text/html, application/xhtml+xml, */*; X-HttpWatch-RID: 41723-10011
Referer: http://localhost:8080/myApp/login.html
Accept-Language: zh-Hans-CN,zh-Hans;q=0.5
User-Agent: Mozilla/5.0 (MSIE 9.0; qdesk 2.4.1266.203; Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GET 和 POST 比较&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：GET 用于获取资源，而 POST 用于传输实体主体&lt;/p&gt;
&lt;p&gt;参数：GET 和 POST 的请求都能使用额外的参数，但是 GET 的参数是以查询字符串出现在 URL 中，而 POST 的参数存储在实体主体中（GET 也有请求体，POST 也可以通过 URL 传输参数）。不能因为 POST 参数存储在实体主体中就认为它的安全性更高，因为照样可以通过一些抓包工具（Fiddler）查看&lt;/p&gt;
&lt;p&gt;安全：安全的 HTTP 方法不会改变服务器状态，也就是说它只是可读的。GET 方法是安全的，而 POST 不是，因为 POST 的目的是传送实体主体内容&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全的方法除了 GET 之外还有：HEAD、OPTIONS&lt;/li&gt;
&lt;li&gt;不安全的方法除了 POST 之外还有 PUT、DELETE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;幂等性：同样的请求&lt;strong&gt;被执行一次与连续执行多次的效果是一样的&lt;/strong&gt;，服务器的状态也是一样的，所有的安全方法也都是幂等的。在正确实现条件下，GET，HEAD，PUT 和 DELETE 等方法都是幂等的，POST 方法不是&lt;/p&gt;
&lt;p&gt;可缓存：如果要对响应进行缓存，需要满足以下条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求报文的 HTTP 方法本身是可缓存的，包括 GET 和 HEAD，但是 PUT 和 DELETE 不可缓存，POST 在多数情况下不可缓存&lt;/li&gt;
&lt;li&gt;响应报文的状态码是可缓存的，包括：200、203、204、206、300、301、404、405、410、414 and 501&lt;/li&gt;
&lt;li&gt;响应报文的 Cache-Control 首部字段没有指定不进行缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PUT 和 POST 的区别&lt;/p&gt;
&lt;p&gt;PUT 请求：如果两个请求相同，后一个请求会把第一个请求覆盖掉（幂等），所以 PUT 用来修改资源&lt;/p&gt;
&lt;p&gt;POST 请求：后一个请求不会把第一个请求覆盖掉（非幂等），所以 POST 用来创建资源&lt;/p&gt;
&lt;p&gt;PATCH 方法 是新引入的，是对 PUT 方法的补充，用来对已知资源进行&lt;strong&gt;局部更新&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求行详解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET  /myApp/success.html?username=zs&amp;amp;password=123456 HTTP/1.1	
POST /myApp/success.html HTTP/1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET/POST&lt;/td&gt;
&lt;td&gt;请求的方式。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/myApp/success.html&lt;/td&gt;
&lt;td&gt;请求的资源。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;td&gt;使用的协议，及协议的版本。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求头详解&lt;/p&gt;
&lt;p&gt;从第 2 行到空行处，都叫请求头，以键值对的形式存在，但存在一个 key 对应多个值的请求头&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accept&lt;/td&gt;
&lt;td&gt;告知服务器，客户浏览器支持的 MIME 类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-Agent&lt;/td&gt;
&lt;td&gt;浏览器相关信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Charset&lt;/td&gt;
&lt;td&gt;告诉服务器，客户浏览器支持哪种字符集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Encoding&lt;/td&gt;
&lt;td&gt;告知服务器，客户浏览器支持的压缩编码格式，常用 gzip 压缩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Language&lt;/td&gt;
&lt;td&gt;告知服务器，客户浏览器支持的语言，zh_CN 或 en_US 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;初始 URL 中的主机和端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referer&lt;/td&gt;
&lt;td&gt;告知服务器，当前请求的来源。只有当前请求有来源，才有这个消息头。&amp;lt;br/&amp;gt;作用：1 投放广告  2 防盗链&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;告知服务器，请求正文的 MIME 类型，文件传输的类型，&amp;lt;br/&amp;gt;application/x-www-form-urlencoded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Length&lt;/td&gt;
&lt;td&gt;告知服务器，请求正文的长度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection&lt;/td&gt;
&lt;td&gt;表示是否需要持久连接，一般是 &lt;code&gt;Keep -Alive&lt;/code&gt;（HTTP 1.1 默认进行持久连接 )&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Modified-Since&lt;/td&gt;
&lt;td&gt;告知服务器，客户浏览器缓存文件的最后修改时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;会话管理相关（非常的重要）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求体详解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只有 POST 请求方式，才有请求的正文，GET 方式的正文是在地址栏中的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表单的输入域有 name 属性的才会被提交，不分 GET 和 POST 的请求方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表单的 enctype 属性取值决定了请求正文的体现形式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;enctype取值&lt;/th&gt;
&lt;th&gt;请求正文体现形式&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;application/x-www-form-urlencoded&lt;/td&gt;
&lt;td&gt;key=value&amp;amp;key=value&lt;/td&gt;
&lt;td&gt;username=test&amp;amp;password=1234&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multipart/form-data&lt;/td&gt;
&lt;td&gt;此时变成了多部分表单数据。多部分是靠分隔符分隔的。&lt;/td&gt;
&lt;td&gt;-----------------------------7df23a16c0210&amp;lt;br/&amp;gt;Content-Disposition: form-data; name=&quot;username&quot;&amp;lt;br/&amp;gt;test&amp;lt;br/&amp;gt;-----------------------------7df23a16c0210&amp;lt;br/&amp;gt;Content-Disposition: form-data; name=&quot;password&quot;&amp;lt;br/&amp;gt;1234&amp;lt;br/&amp;gt;-------------------------------7df23a16c0210&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;响应部分&lt;/h2&gt;
&lt;p&gt;响应部分图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP%E5%93%8D%E5%BA%94%E9%83%A8%E5%88%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;响应行&lt;/p&gt;
&lt;p&gt;HTTP/1.1：使用协议的版本&lt;/p&gt;
&lt;p&gt;200：响应状态码&lt;/p&gt;
&lt;p&gt;OK：状态码描述&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;响应状态码：
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP%E7%8A%B6%E6%80%81%E5%93%8D%E5%BA%94%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;一切都 OK，与服务器连接成功，发送请求成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;302/307&lt;/td&gt;
&lt;td&gt;请求重定向（客户端行为，两次请求，地址栏发生改变）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;请求资源未改变，使用缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;客户端错误，请求错误，最常见的就是请求参数有问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;客户端错误，但 forbidden 权限不够，拒绝处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;客户端错误，请求资源未找到&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;服务器错误，服务器运行内部错误&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;转移：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;301 redirect：301 代表永久性转移 (Permanently Moved)&lt;/li&gt;
&lt;li&gt;302 redirect：302 代表暂时性转移 (Temporarily Moved )&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应头：以 key:vaue 存在，可能多个 value 情况&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;消息头&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Location&lt;/td&gt;
&lt;td&gt;请求重定向的地址，常与 302，307 配合使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;服务器相关信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;告知客户浏览器，响应正文的MIME类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Length&lt;/td&gt;
&lt;td&gt;告知客户浏览器，响应正文的长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Encoding&lt;/td&gt;
&lt;td&gt;告知客户浏览器，响应正文使用的压缩编码格式，常用的 gzip 压缩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Language&lt;/td&gt;
&lt;td&gt;告知客户浏览器，响应正文的语言，zh_CN 或 en_US 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Disposition&lt;/td&gt;
&lt;td&gt;告知客户浏览器，以下载的方式打开响应正文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh&lt;/td&gt;
&lt;td&gt;客户端的刷新频率，单位是秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last-Modified&lt;/td&gt;
&lt;td&gt;服务器资源的最后修改时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set-Cookie&lt;/td&gt;
&lt;td&gt;服务器端发送的 Cookie，会话管理相关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expires:-1&lt;/td&gt;
&lt;td&gt;服务器资源到客户浏览器后的缓存时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catch-Control: no-catch&lt;/td&gt;
&lt;td&gt;不要缓存，//针对http协议1.1版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pragma:no-catch&lt;/td&gt;
&lt;td&gt;不要缓存，//针对http协议1.0版本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应体：页面展示内容, 类似网页的源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;css.css&quot; type=&quot;text/css&quot;&amp;gt;
        &amp;lt;script type=&quot;text/javascript&quot; src=&quot;demo.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;img src=&quot;1.jpg&quot; /&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Servlet&lt;/h1&gt;
&lt;h2&gt;JavaEE&lt;/h2&gt;
&lt;h3&gt;JavaEE规范&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;JavaEE&lt;/code&gt; 规范是 &lt;code&gt;J2EE&lt;/code&gt; 规范的新名称，早期被称为 &lt;code&gt;J2EE&lt;/code&gt; 规范，其全称是 &lt;code&gt;Java 2 Platform Enterprise Edition&lt;/code&gt;，它是由 SUN 公司领导、各厂家共同制定并得到广泛认可的工业标准（&lt;code&gt;JCP&lt;/code&gt;组织成员）。之所以改名为&lt;code&gt;JavaEE&lt;/code&gt;，目的还是让大家清楚 &lt;code&gt;J2EE&lt;/code&gt; 只是 &lt;code&gt;Java&lt;/code&gt; 企业应用。在 2004 年底中国软件技术大会 &lt;code&gt;Ioc&lt;/code&gt; 微容器（也就是 &lt;code&gt;Jdon&lt;/code&gt; 框架的实现原理）演讲中指出：我们需要一个跨 &lt;code&gt;J2SE/WEB/EJB&lt;/code&gt; 的微容器，保护我们的业务核心组件，以延续它的生命力，而不是依赖 &lt;code&gt;J2SE/J2EE&lt;/code&gt; 版本。此次 &lt;code&gt;J2EE&lt;/code&gt; 改名为 &lt;code&gt;Java EE&lt;/code&gt;，实际也反映出业界这种共同心声&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaEE&lt;/code&gt; 规范是很多 Java 开发技术的总称。这些技术规范都是沿用自 &lt;code&gt;J2EE&lt;/code&gt; 的。一共包括了 13 个技术规范，例如：&lt;code&gt;jsp/servlet&lt;/code&gt;，&lt;code&gt;jndi&lt;/code&gt;，&lt;code&gt;jaxp&lt;/code&gt;，&lt;code&gt;jdbc&lt;/code&gt;，&lt;code&gt;jni&lt;/code&gt;，&lt;code&gt;jaxb&lt;/code&gt;，&lt;code&gt;jmf&lt;/code&gt;，&lt;code&gt;jta&lt;/code&gt;，&lt;code&gt;jpa&lt;/code&gt;，&lt;code&gt;EJB&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;JCP&lt;/code&gt; 组织的全称是 Java Community Process，是一个开放的国际组织，主要由 Java 开发者以及被授权者组成，职能是发展和更新。成立于 1998 年。官网是：&lt;a href=&quot;https://jcp.org/en/home/index&quot;&gt;JCP&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaEE&lt;/code&gt; 的版本是延续了 &lt;code&gt;J2EE&lt;/code&gt; 的版本，但是没有继续采用其命名规则。&lt;code&gt;J2EE&lt;/code&gt; 的版本从 1.0 开始到 1.4 结束，而 &lt;code&gt;JavaEE&lt;/code&gt; 版本是从 &lt;code&gt;JavaEE 5&lt;/code&gt; 版本开始，目前最新的的版本是 &lt;code&gt;JavaEE 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;详情请参考：&lt;a href=&quot;https://www.oracle.com/technetwork/cn/java/javaee/overview/index.html&quot;&gt;JavaEE8 规范概览&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Web 概述&lt;/h3&gt;
&lt;p&gt;Web，在计算机领域指网络。像我们接触的 &lt;code&gt;WWW&lt;/code&gt;，它是由 3 个单词组成的，即：&lt;code&gt;World Wide Web &lt;/code&gt;，中文含义是&amp;lt;b&amp;gt;万维网&amp;lt;/b&amp;gt;。而我们前面学的 HTML 的参考文档《W3School 全套教程》中的 &lt;code&gt;W3C&lt;/code&gt; 就是万维网联盟，他们的出现都是为了让我们在网络的世界中获取资源，这些资源的存放之处，我们称之为网站。我们通过输入网站的地址（网址），就可以访问网站中提供的资源。在网上我们能访问到的内容全是资源（不区分局域网还是广域网），只不过不同类型的资源展示的效果不一样&lt;/p&gt;
&lt;p&gt;资源分为静态资源和动态资源&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态资源指的是，网站中提供给人们展示的资源是一成不变的，也就是说不同人或者在不同时间，看到的内容都是一样的。例如：我们看到的新闻，网站的使用手册，网站功能说明文档等等。而作为开发者，我们编写的 &lt;code&gt;html&lt;/code&gt;、&lt;code&gt;css&lt;/code&gt;、&lt;code&gt;js&lt;/code&gt; 图片，多媒体等等都可以称为静态资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态资源它指的是，网站中提供给人们展示的资源是由程序产生的，在不同的时间或者用不同的人员由于身份的不同，所看到的内容是不一样的。例如：我们在CSDN上下载资料，只有登录成功后，且积分足够时才能下载。否则就不能下载，这就是访客身份和会员身份的区别。作为开发人员，我们编写的 &lt;code&gt;JSP&lt;/code&gt;，&lt;code&gt;servlet&lt;/code&gt;，&lt;code&gt;php&lt;/code&gt;，&lt;code&gt;ASP&lt;/code&gt; 等都是动态资源。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于广域网和局域网的划分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;广域网指的就是万维网，也就是我们说的互联网。&lt;/li&gt;
&lt;li&gt;局域网是指的是在一定范围之内可以访问的网络，出了这个范围，就不能再使用的网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;系统结构&lt;/h3&gt;
&lt;p&gt;基础结构划分：C/S结构，B/S结构两类。&lt;/p&gt;
&lt;p&gt;技术选型划分：Model1模型，Model2模型，MVC模型和三层架构+MVC模型。&lt;/p&gt;
&lt;p&gt;部署方式划分：一体化架构，垂直拆分架构，分布式架构，流动计算架构，微服务架构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C/S结构：客户端—服务器的方式。其中C代表Client，S代表服务器。C/S结构的系统设计图如下：
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JavaEE-CS结构图.jpg&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B/S结构是浏览器—服务器的方式。B代表Browser，S代表服务器。B/S结构的系统设计图如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JavaEE-BS结构图.jpg&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两种结构的区别及优劣&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一：硬件环境不同，C/S通常是建立在专用的网络或小范围的网络环境上（即局域网），且必须要安装客户端。而B/S是建立在广域网上的，适应范围强，通常有操作系统和浏览器就行。&lt;/li&gt;
&lt;li&gt;第二：C/S结构比B/S结构更安全，因为用户群相对固定，对信息的保护更强。&lt;/li&gt;
&lt;li&gt;第三：B/S结构维护升级比较简单，而C/S结构维护升级相对困难。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优劣&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C/S：能充分发挥客户端PC的处理能力，很多工作可以在客户端处理后再提交给服务器。对应的优点就是客户端响应速度快。&lt;/li&gt;
&lt;li&gt;B/S：总体拥有成本低、维护方便、 分布性强、开发简单，可以不用安装任何专门的软件就能实现在任何地方进行操作，客户端零维护，系统的扩展非常容易，只要有一台能上网的电脑就能使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们的课程中涉及的系统结构都是是基于B/S结构&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Tomcat&lt;/h2&gt;
&lt;h3&gt;服务器&lt;/h3&gt;
&lt;p&gt;服务器的概念非常的广泛，它可以指代一台特殊的计算机（相比普通计算机运行更快、负载更高、价格更贵），也可以指代用于部署网站的应用。我们这里说的服务器，其实是web服务器，或者应用服务器。它本质就是一个软件，一个应用。作用就是发布我们的应用（工程），让用户可以通过浏览器访问我们的应用。&lt;/p&gt;
&lt;p&gt;常见的应用服务器，请看下表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;服务器名称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;weblogic&lt;/td&gt;
&lt;td&gt;实现了 JavaEE 规范，重量级服务器，又称为 JavaEE 容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;websphereAS&lt;/td&gt;
&lt;td&gt;实现了 JavaEE 规范，重量级服务器。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JBOSSAS&lt;/td&gt;
&lt;td&gt;实现了 JavaEE 规范，重量级服务器，免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat&lt;/td&gt;
&lt;td&gt;实现了 jsp/servlet 规范，是一个轻量级服务器，开源免费&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;h4&gt;Windows安装&lt;/h4&gt;
&lt;p&gt;下载地址：http://tomcat.apache.org/&lt;/p&gt;
&lt;p&gt;目录结构详解：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%E8%AF%A6%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Linux安装&lt;/h4&gt;
&lt;p&gt;解压apache-tomcat-8.5.32.tar.gz。&lt;/p&gt;
&lt;p&gt;防火墙设置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式1：service iptables stop  关闭防火墙(不建议); 用到哪一个端口号就放行哪一个(80,8080,3306...)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式2：放行8080 端口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改配置文件&lt;code&gt;cd /etc/sysconfig&lt;/code&gt;--&amp;gt;&lt;code&gt;vi iptables&lt;/code&gt;
&lt;code&gt;-A INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -j ACCEPT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启加载防火墙或者重启防火墙
&lt;code&gt;service iptables reload&lt;/code&gt; 或者&lt;code&gt;service iptables restart&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;启动停止&lt;/h4&gt;
&lt;p&gt;Tomcat服务器的启动文件在二进制文件目录bin中：startup.bat，startup.sh&lt;/p&gt;
&lt;p&gt;Tomcat服务器的停止文件也在二进制文件目录bin中：shutdown.bat，shutdown.sh  （推荐直接关闭控制台）&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;.bat&lt;/code&gt;文件是针对windows系统的运行程序，&lt;code&gt;.sh&lt;/code&gt;文件是针对linux系统的运行程序。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;常见问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动一闪而过&lt;/p&gt;
&lt;p&gt;没有配置环境变量，配置上 JAVA_HOME 环境变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tomcat 启动后控制台输出乱码&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;/conf/logging.properties&lt;/code&gt;，设置 gbk &lt;code&gt;java.util.logging.ConsoleHandler.encoding = gbk&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Address already in use : JVM_Bind：端口被占用，找到占用该端口的应用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进程不重要：使用cmd命令：netstat -a -o 查看 pid  在任务管理器中结束占用端口的进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程很重要：修改自己的端口号。修改的是 Tomcat 目录下&lt;code&gt;\conf\server.xml&lt;/code&gt;中的配置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-server.xml%E7%AB%AF%E5%8F%A3%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;IDEA集成&lt;/h4&gt;
&lt;p&gt;Run -&amp;gt; Edit Configurations -&amp;gt; Templates -&amp;gt; Tomcat Server -&amp;gt; Local&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA%E9%85%8D%E7%BD%AETomcat.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;发布应用&lt;/h3&gt;
&lt;h4&gt;虚拟目录&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;server.xml&lt;/code&gt; 的 &lt;code&gt;&amp;lt;Host&amp;gt;&lt;/code&gt; 元素中加一个 &lt;code&gt;&amp;lt;Context path=&quot;&quot; docBase=&quot;&quot;/&amp;gt;&lt;/code&gt; 元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt;：访问资源URI，URI名称可以随便起，但是必须在前面加上一个/&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docBase&lt;/code&gt;：资源所在的磁盘物理地址&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;虚拟主机&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;&amp;lt;Engine&amp;gt;&lt;/code&gt;元素中添加一个&lt;code&gt;&amp;lt;Host name=&quot;&quot; appBase=&quot;&quot; unparkWARs=&quot;&quot; autoDeploy=&quot;&quot; /&amp;gt;&lt;/code&gt;，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：指定主机的名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appBase&lt;/code&gt;：当前主机的应用发布目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unparkWARs&lt;/code&gt;：启动时是否自动解压war包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;autoDeploy&lt;/code&gt;：是否自动发布&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Host name=&quot;www.itcast.cn&quot; appBase=&quot;D:\itcastapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;/&amp;gt;

&amp;lt;Host name=&quot;www.itheima.com&quot; appBase=&quot;D:\itheimaapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;IDEA部署&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建工程
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IEDA新建工程.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发布工程
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA%E5%8F%91%E5%B8%83%E5%B7%A5%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;IDEA发布&lt;/h4&gt;
&lt;p&gt;把资源移动到 Tomcat 工程下 web 目录中，两种访问方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接访问：http://localhost:8080/Tomcat/login/login.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 web.xml 中配置默认主页&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;welcome-file-list&amp;gt;
    &amp;lt;welcome-file&amp;gt;/默认主页&amp;lt;/welcome-file&amp;gt;
&amp;lt;/welcome-file-list&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;执行原理&lt;/h3&gt;
&lt;h4&gt;整体架构&lt;/h4&gt;
&lt;p&gt;Tomcat 核心组件架构图如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E6%9E%B6%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;组件介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GlobalNamingResources：实现 JNDI，指定一些资源的配置信息&lt;/li&gt;
&lt;li&gt;Server：Tomcat 是一个 Servlet 容器，一个 Tomcat 对应一个 Server，一个 Server 可以包含多个 Service&lt;/li&gt;
&lt;li&gt;Service：核心服务是 Catalina，用来对请求进行处理，一个 Service 包含多个 Connector 和一个 Container&lt;/li&gt;
&lt;li&gt;Connector：连接器，负责处理客户端请求，解析不同协议及 I/O 方式&lt;/li&gt;
&lt;li&gt;Executor：线程池&lt;/li&gt;
&lt;li&gt;Container：容易包含 Engine，Host，Context，Wrapper 等组件&lt;/li&gt;
&lt;li&gt;Engine：服务交给引擎处理请求，Container 容器中顶层的容器对象，一个 Engine 可以包含多个 Host 主机&lt;/li&gt;
&lt;li&gt;Host：Engine 容器的子容器，一个 Host 对应一个网络域名，一个 Host 包含多个 Context&lt;/li&gt;
&lt;li&gt;Context：Host 容器的子容器，表示一个 Web 应用&lt;/li&gt;
&lt;li&gt;Wrapper：Tomcat 中的最小容器单元，表示 Web 应用中的 Servlet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心类库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coyote：Tomcat 连接器的名称，封装了底层的网络通信，为 Catalina 容器提供了统一的接口，使容器与具体的协议以及 I/O 解耦&lt;/li&gt;
&lt;li&gt;EndPoint：Coyote 通信端点，即通信监听的接口，是 Socket 接收和发送处理器，是对传输层的抽象，用来实现 TCP/IP 协议&lt;/li&gt;
&lt;li&gt;Processor ： Coyote 协议处理接口，用来实现 HTTP 协议，Processor 接收来自 EndPoint 的 Socket，读取字节流解析成 Tomcat 的 Request 和 Response 对象，并通过 Adapter 将其提交到容器处理，Processor 是对应用层协议的抽象&lt;/li&gt;
&lt;li&gt;CoyoteAdapter：适配器，连接器调用 CoyoteAdapter 的 sevice 方法，传入的是 TomcatRequest 对象，CoyoteAdapter 负责将TomcatRequest 转成 ServletRequest，再调用容器的 service 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/7c9401b85704&lt;/p&gt;
&lt;p&gt;参考文章：https://www.yuque.com/yinhuidong/yu877c/ktq82e&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;启动过程&lt;/h4&gt;
&lt;p&gt;Tomcat 的启动入口是 Bootstrap#main 函数，首先通过调用 &lt;code&gt;bootstrap.init()&lt;/code&gt; 初始化相关组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;initClassLoaders()&lt;/code&gt;：初始化三个类加载器，commonLoader 的父类加载器是启动类加载器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Thread.currentThread().setContextClassLoader(catalinaLoader)&lt;/code&gt;：自定义类加载器加载 Catalina 类，&lt;strong&gt;打破双亲委派&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object startupInstance = startupClass.getConstructor().newInstance()&lt;/code&gt;：反射创建 Catalina 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method.invoke(startupInstance, paramValues)&lt;/code&gt;：反射调用方法，设置父类加载器是 sharedLoader&lt;/li&gt;
&lt;li&gt;&lt;code&gt;catalinaDaemon = startupInstance&lt;/code&gt;：引用 Catalina 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;daemon.load(args)&lt;/code&gt; 方法反射调用 Catalina 对象的 load 方法，对&lt;strong&gt;服务器的组件进行初始化&lt;/strong&gt;，并绑定了 ServerSocket 的端口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parseServerXml(true)&lt;/code&gt;：解析 XML 配置文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getServer().init()&lt;/code&gt;：服务器执行初始化，采用责任链的执行方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LifecycleBase.init()&lt;/code&gt;：生命周期接口的初始化方法，开始链式调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardServer.initInternal()&lt;/code&gt;：Server 的初始化，遍历所有的 Service 进行初始化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardService.initInternal()&lt;/code&gt;：Service 的初始化，对 Engine、Executor、listener、Connector 进行初始化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardEngine.initInternal()&lt;/code&gt;：Engine 的初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getRealm()&lt;/code&gt;：创建一个 Realm 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContainerBase.initInternal()&lt;/code&gt;：容器的初始化，设置处理容器内组件的启动和停止事件的线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Connector.initInternal()&lt;/code&gt;：Connector 的初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Connector() {
    this(&quot;HTTP/1.1&quot;); //默认无参构造方法，会创建出 Http11NioProtocol 的协议处理器
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;adapter = new CoyoteAdapter(this)&lt;/code&gt;：实例化 CoyoteAdapter 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;protocolHandler.setAdapter(adapter)&lt;/code&gt;：设置到 ProtocolHandler 协议处理器中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ProtocolHandler.init()&lt;/code&gt;：协议处理器的初始化，底层调用 &lt;code&gt;AbstractProtocol#init&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;endpoint.init()&lt;/code&gt;：端口的初始化，底层调用 &lt;code&gt;AbstractEndpoint#init&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NioEndpoint.bind()&lt;/code&gt;：绑定方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;initServerSocket()&lt;/code&gt;：&lt;strong&gt;初始化 ServerSocket&lt;/strong&gt;，以 NIO 的方式监听端口
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;serverSock = ServerSocketChannel.open()&lt;/code&gt;：&lt;strong&gt;NIO 的方式打开通道&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serverSock.bind(addr, getAcceptCount())&lt;/code&gt;：通道绑定连接端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serverSock.configureBlocking(true)&lt;/code&gt;：切换为阻塞模式（没懂，为什么阻塞）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initialiseSsl()&lt;/code&gt;：初始化 SSL 连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;selectorPool.open(getName())&lt;/code&gt;：打开选择器，类似 NIO 的多路复用器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初始化完所有的组件，调用 &lt;code&gt;daemon.start()&lt;/code&gt; 进行&lt;strong&gt;组件的启动&lt;/strong&gt;，底层反射调用 Catalina 对象的 start 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getServer().start()&lt;/code&gt;：启动组件，也是责任链的模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LifecycleBase.start()&lt;/code&gt;：生命周期接口的初始化方法，开始链式调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardServer.startInternal()&lt;/code&gt;：Server 服务的启动&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;globalNamingResources.start()&lt;/code&gt;：启动 JNDI 服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (Service service : services)&lt;/code&gt;：遍历所有的 Service 进行启动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardService.startInternal()&lt;/code&gt;：Service 的启动，对所有 Executor、listener、Connector 进行启&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StandardEngine.startInternal()&lt;/code&gt;：启动引擎，部署项目&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ContainerBase.startInternal()&lt;/code&gt;：容器的启动
&lt;ul&gt;
&lt;li&gt;启动集群、Realm 组件，并且创建子容器，提交给线程池&lt;/li&gt;
&lt;li&gt;&lt;code&gt;((Lifecycle) pipeline).start()&lt;/code&gt;：遍历所有的管道进行启动
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Valve current = first&lt;/code&gt;：获取第一个阀门&lt;/li&gt;
&lt;li&gt;&lt;code&gt;((Lifecycle) current).start()&lt;/code&gt;：启动阀门，底层 &lt;code&gt;ValveBase#startInternal&lt;/code&gt; 中设置启动的状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current = current.getNext()&lt;/code&gt;：获取下一个阀门&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Connector.startInternal()&lt;/code&gt;：Connector 的初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;protocolHandler.start()&lt;/code&gt;：协议处理器的启动&lt;/p&gt;
&lt;p&gt;&lt;code&gt;endpoint.start()&lt;/code&gt;：端点启动&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NioEndpoint.startInternal()&lt;/code&gt;：启动 NIO 的端点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createExecutor()&lt;/code&gt;：创建 Worker 线程组，10 个线程，用来进行任务处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initializeConnectionLatch()&lt;/code&gt;：用来进行连接限流，&lt;strong&gt;最大 8*1024 条连接&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;poller = new Poller()&lt;/code&gt;：&lt;strong&gt;创建 Poller 对象&lt;/strong&gt;，开启了一个多路复用器 Selector&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Thread pollerThread = new Thread(poller, getName() + &quot;-ClientPoller&quot;)&lt;/code&gt;：创建并启动 Poller 线程，Poller 实现了 Runnable 接口，是一个任务对象，&lt;strong&gt;线程 start 后进入 Poller#run 方法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pollerThread.setDaemon(true)&lt;/code&gt;：设置为守护线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startAcceptorThread()&lt;/code&gt;：启动接收者线程
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;acceptor = new Acceptor&amp;lt;&amp;gt;(this)&lt;/code&gt;：&lt;strong&gt;创建 Acceptor 对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Thread t = new Thread(acceptor, threadName)&lt;/code&gt;：创建并启动 Acceptor 接受者线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;处理过程&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Acceptor 监听客户端套接字，每 50ms 调用一次 &lt;strong&gt;&lt;code&gt;serverSocket.accept&lt;/code&gt;&lt;/strong&gt;，获取 Socket 后把封装成 NioSocketWrapper（是 SocketWrapperBase 的子类），并设置为非阻塞模式，把 NioSocketWrapper 封装成 PollerEvent 放入同步队列中&lt;/li&gt;
&lt;li&gt;Poller 循环判断同步队列中是否有就绪的事件，如果有则通过 &lt;code&gt;selector.selectedKeys()&lt;/code&gt; 获取就绪事件，获取 SocketChannel 中携带的 attachment（NioSocketWrapper），在 processKey 方法中根据事件类型进行 processSocket，将 Wrapper 对象封装成 SocketProcessor 对象，该对象是一个任务对象，提交到 Worker 线程池进行执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SocketProcessorBase.run()&lt;/code&gt; 加锁调用 &lt;code&gt;SocketProcessor#doRun&lt;/code&gt;，保证线程安全，从协议处理器 ProtocolHandler 中获取 AbstractProtocol，然后&lt;strong&gt;创建 Http11Processor 对象处理请求&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Http11Processor#service&lt;/code&gt; 中调用 &lt;code&gt;CoyoteAdapter#service&lt;/code&gt; ，把生成的 Tomcat 下的 Request 和 Response 对象通过方法 postParseRequest 匹配到对应的 Servlet 的请求响应，将请求传递到对应的 Engine 容器中调用 Pipeline，管道中包含若干个 Valve，执行完所有的 Valve 最后执行 StandardEngineValve，继续调用 Host 容器的 Pipeline，执行 Host 的 Valve，再传递给 Context 的 Pipeline，最后传递到 Wrapper 容器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StandardWrapperValve#invoke&lt;/code&gt; 中创建了 Servlet 对象并执行初始化，并为当前请求准备一个 FilterChain 过滤器链执行 doFilter 方法，&lt;code&gt;ApplicationFilterChain#doFilter&lt;/code&gt; 是一个&lt;strong&gt;责任链的驱动方法&lt;/strong&gt;，通过调用 internalDoFilter 来获取过滤器链的下一个过滤器执行 doFilter，执行完所有的过滤器后执行 &lt;code&gt;servlet.service&lt;/code&gt; 的方法&lt;/li&gt;
&lt;li&gt;最后调用 HttpServlet#service()，根据请求的方法来调用 doGet、doPost 等，执行到自定义的业务方法&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Servlet&lt;/h2&gt;
&lt;h3&gt;Socket&lt;/h3&gt;
&lt;p&gt;Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传输的技术，是网络编程的基础&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Servlet 是使用 HTTP 协议在服务器与客户端之间通信的技术，是 Socket 的一种应用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 协议：是在 TCP/IP 协议之上进一步封装的一层协议，关注数据传输的格式是否规范，底层的数据传输还是运用了 Socket 和 TCP/IP&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tomcat 和 Servlet 的关系：Servlet 的运行环境叫做 Web 容器或 Servlet 服务器，&lt;strong&gt;Tomcat 是 Web 应用服务器，是一个 Servlet/JSP 容器&lt;/strong&gt;。Tomcat 作为 Servlet 容器，负责处理客户请求，把请求传送给 Servlet，并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持 Java 语言的服务器上的组件，Servlet 用来扩展 Java Web 服务器功能，提供非常安全的、可移植的、易于使用的 CGI 替代品
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat%E4%B8%8EServlet%E7%9A%84%E5%85%B3%E7%B3%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;h4&gt;Servlet类&lt;/h4&gt;
&lt;p&gt;Servlet是SUN公司提供的一套规范，名称就叫Servlet规范，它也是JavaEE规范之一。通过API来使用Servlet。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Servlet是一个运行在web服务端的java小程序，用于接收和响应客户端的请求。一个服务器包含多个Servlet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过实现Servlet接口，继承GenericServlet或者HttpServlet，实现Servlet功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次请求都会执行service方法，在service方法中还有参数ServletRequest和ServletResponse&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持配置相关功能&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet%E7%B1%BB%E5%85%B3%E7%B3%BB%E6%80%BB%E8%A7%86%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;执行流程&lt;/h4&gt;
&lt;p&gt;创建 Web 工程 → 编写普通类继承 Servlet 相关类 → 重写方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet%E5%85%A5%E9%97%A8%E6%A1%88%E4%BE%8B%E6%89%A7%E8%A1%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Servlet执行过程分析：&lt;/p&gt;
&lt;p&gt;通过浏览器发送请求，请求首先到达Tomcat服务器，由服务器解析请求URL，然后在部署的应用列表中找到应用。然后找到web.xml配置文件，在web.xml中找到FirstServlet的配置（&amp;lt;url-pattern&amp;gt;/&amp;lt;url-pattern&amp;gt;），找到后执行service方法，最后由FirstServlet响应客户浏览器。整个过程如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E5%9B%BE.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现方式&lt;/h4&gt;
&lt;p&gt;实现 Servlet 功能时，可以选择以下三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一种：实现 Servlet 接口，接口中的方法必须全部实现。
使用此种方式，表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二种：继承 GenericServlet，service 方法必须重写，其他方可根据需求，选择性重写。
使用此种方式，表示只在接收和响应客户端请求这方面有重写的需求，而其他方法可根据实际需求选择性重写，使我们的开发Servlet变得简单。但是，此种方式是和 HTTP 协议无关的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三种：继承 HttpServlet，它是 javax.servlet.http 包下的一个抽象类，是 GenericServlet 的子类。选择继承 HttpServlet 时，&lt;strong&gt;需要重写 doGet 和 doPost 方法&lt;/strong&gt;，来接收 get 方式和 post 方式的请求，不要覆盖 service 方法。使用此种方式，表示我们的请求和响应需要和 HTTP 协议相关，我们是通过 HTTP 协议来访问。每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式（GET POST PUT DELETE TRACE OPTIONS HEAD )。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;相关问题&lt;/h3&gt;
&lt;h4&gt;异步处理&lt;/h4&gt;
&lt;p&gt;Servlet 3.0 中的异步处理指的是允许Servlet重新发起一条新线程去调用 耗时业务方法，这样就可以避免等待&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet3.0%E7%9A%84%E5%BC%82%E6%AD%A5%E5%A4%84%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;p&gt;servlet从创建到销毁的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;出生：（初始化）请求第一次到达 Servlet 时，创建对象，并且初始化成功。Only one time&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;活着：（服务）服务器提供服务的整个过程中，该对象一直存在，每次只是执行 service 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;死亡：（销毁）当服务停止时，或者服务器宕机时，对象删除，&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;serrvlet生命周期方法:
&lt;code&gt;init(ServletConfig config)&lt;/code&gt; → &lt;code&gt;service(ServletRequest req, ServletResponse res)&lt;/code&gt; → &lt;code&gt;destroy()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;默认情况下, 有了第一次请求, 会调用 init() 方法进行初始化【调用一次】，任何一次请求，都会调用 service() 方法处理这个请求，服务器正常关闭或者项目从服务器移除, 调用 destory() 方法进行销毁【调用一次】&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩展&lt;/strong&gt;：servlet 是单例多线程的，尽量不要在 servlet 里面使用全局(成员)变量，可能会导致线程不安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单例：Servlet 对象只会创建一次，销毁一次，Servlet 对象只有一个实例。&lt;/li&gt;
&lt;li&gt;多线程：服务器会针对每次请求, 开启一个线程调用 service() 方法处理这个请求&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;线程安全&lt;/h4&gt;
&lt;p&gt;Servlet运用了单例模式，整个应用中只有一个实例对象，所以需要分析这个唯一的实例中的类成员是否线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServletDemo extends HttpServlet{
    //1.定义用户名成员变量
    //private String username = null;
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = null;
        //synchronized (this) {
            //2.获取用户名
            username = req.getParameter(&quot;username&quot;);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //3.获取输出流对象
            PrintWriter pw = resp.getWriter();
            //4.响应给客户端浏览器
            pw.print(&quot;Welcome:&quot; + username);
            //5.关流
            pw.close();
        //}
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动两个浏览器，输入不同的参数(http://localhost:8080/ServletDemo/username=aaa 或者bbb)，访问之后发现输出的结果都是一样，所以出现线程安全问题。&lt;/p&gt;
&lt;p&gt;在Servlet中定义了类成员之后，多个浏览器都会共享类成员的数据，其中任何一个线程修改了数据，都会影响其他线程。因此，我们可以认为Servlet它不是线程安全的。因为Servlet是单例，单例对象的类成员只会随类实例化时初始化一次，之后的操作都是改变，而不会重新初始化。&lt;/p&gt;
&lt;p&gt;解决办法：如果类成员是共用的，只在初始化时赋值，其余时间都是获取。或者加锁synchronized&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;映射方式&lt;/h4&gt;
&lt;p&gt;Servlet支持三种映射方式，三种映射方式的优先级为：第一种&amp;gt;第二种&amp;gt;第三种。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;具体名称方式
这种方式，只有和映射配置一模一样时，Servlet才会接收和响应来自客户端的请求。
访问URL：http://localhost:8080/servlet/servletDemo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/servletDemo&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/开头+通配符的方式
这种方式，只要符合目录结构即可，不用考虑结尾是什么
访问URL：http://localhost:8080/servlet/ + 任何字符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/servlet/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通配符+固定格式结尾
这种方式，只要符合固定结尾格式即可，其前面的访问URI无须关心（注意协议，主机和端口必须正确）
访问URL：http://localhost:8080/任何字符任何目录 + .do (http://localhost:8080/seazean/i.do)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo05&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo05&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo05&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;*.do&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;多路径映射&lt;/h4&gt;
&lt;p&gt;一个Servlet的多种路径配置的支持。给一个Servlet配置多个访问映射，从而根据不同请求的URL实现不同的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*多路映射*/
public class ServletDemo06 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int money = 1000;
        //获取访问的资源路径
        String name = req.getRequestURI();
        name = name.substring(name.lastIndexOf(&quot;/&quot;));

        if(&quot;/vip&quot;.equals(name)) {
            //如果访问资源路径是/vip 商品价格为9折
            System.out.println(&quot;商品原价为：&quot; + money + &quot;。优惠后是：&quot; + (money*0.9));
        } else if(&quot;/svip&quot;.equals(name)) {
            //如果访问资源路径是/svip 商品价格为5折
            System.out.println(&quot;商品原价为：&quot; + money + &quot;。优惠后是：&quot; + (money*0.5));
        } else {
            //如果访问资源路径是其他  商品价格原样显示
            System.out.println(&quot;商品价格为：&quot; + money);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--演示Servlet多路径映射--&amp;gt;
&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;vip&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo06&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;vip&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/vip&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;svip&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo06&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;svip&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/svip&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;other&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.servlet.ServletDemo06&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;other&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/other&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以根据不同的网页显示不同的数据。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;启动时创建&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;第一种：应用加载时创建Servlet，它的优势是在服务器启动时，就把需要的对象都创建完成了，从而在使用的时候减少了创建对象的时间，提高了首次执行的效率。它的弊端是在应用加载时就创建了Servlet对象，因此，导致内存中充斥着大量用不上的Servlet对象，造成了内存的浪费。&lt;/li&gt;
&lt;li&gt;第二种：请求第一次访问是创建Servlet，它的优势就是减少了对服务器内存的浪费，因为一直没有被访问过的Servlet对象都没有创建，因此也提高了服务器的启动时间。而它的弊端就是要在应用加载时就做的初始化操作，它都没法完成，从而要考虑其他技术实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在web.xml中是支持对Servlet的创建时机进行配置的，配置的方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置ServletDemo3--&amp;gt;
&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.web.servlet.ServletDemo&amp;lt;/servlet-class&amp;gt;
    &amp;lt;!--配置Servlet的创建顺序，当配置此标签时，Servlet就会改为应用加载时创建
        配置项的取值只能是正整数（包括0），数值越小，表明创建的优先级越高--&amp;gt;
    &amp;lt;load-on-startup&amp;gt;1&amp;lt;/load-on-startup&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/servletDemo&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;默认Servlet&lt;/h4&gt;
&lt;p&gt;默认 Servlet 是由服务器提供的一个 Servlet，它配置在 Tomcat 的 conf 目录下的 web.xml 中。&lt;/p&gt;
&lt;p&gt;它的映射路径是&lt;code&gt;&amp;lt;url-pattern&amp;gt;/&amp;lt;url-pattern&amp;gt;&lt;/code&gt;，我们在发送请求时，首先会在我们应用中的 web.xml 中查找映射配置。但是当找不到对应的 Servlet 路径时，就去找默认的 Servlet，由默认 Servlet 处理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ServletConfig&lt;/h3&gt;
&lt;p&gt;ServletConfig 是 Servlet 的配置参数对象。在 Servlet 规范中，允许为每个 Servlet 都提供一些初始化配置，每个 Servlet 都有自己的ServletConfig，作用是&lt;strong&gt;在 Servlet 初始化期间，把一些配置信息传递给 Servlet&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;生命周期：在初始化阶段读取了 web.xml 中为 Servlet 准备的初始化配置，并把配置信息传递给 Servlet，所以生命周期与 Servlet 相同。如果 Servlet 配置了 &lt;code&gt;&amp;lt;load-on-startup&amp;gt;1&amp;lt;/load-on-startup&amp;gt;&lt;/code&gt;，ServletConfig 也会在应用加载时创建。&lt;/p&gt;
&lt;p&gt;获取 ServletConfig：在 init 方法中为 ServletConfig 赋值&lt;/p&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String getInitParameter(String name)&lt;/code&gt;：根据初始化参数的名称获取参数的值，根据&amp;lt;param-name&amp;gt;，获取&amp;lt;param-value&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Enumeration&amp;lt;String&amp;gt; getInitParameterNames()&lt;/code&gt; : 获取所有初始化参数名称的枚举(遍历方式看例子)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ServletContext getServletContext()&lt;/code&gt; : 获取&lt;strong&gt;ServletContext&lt;/strong&gt;对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getServletName()&lt;/code&gt; : 获取Servlet名称&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;web.xml 配置：
初始化参数使用 &lt;code&gt;&amp;lt;servlet&amp;gt;&lt;/code&gt; 标签中的 &lt;code&gt;&amp;lt;init-param&amp;gt; &lt;/code&gt;标签来配置，并且每个 Servlet 都支持有多个初始化参数，并且初始化参数都是以键值对的形式存在的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置ServletDemo8--&amp;gt;
&amp;lt;servlet&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo8&amp;lt;/servlet-name&amp;gt;
    &amp;lt;servlet-class&amp;gt;com.itheima.web.servlet.ServletDemo8&amp;lt;/servlet-class&amp;gt;
    &amp;lt;!--配置初始化参数--&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;!--用于获取初始化参数的key--&amp;gt;
        &amp;lt;param-name&amp;gt;encoding&amp;lt;/param-name&amp;gt;
        &amp;lt;!--初始化参数的值--&amp;gt;
        &amp;lt;param-value&amp;gt;UTF-8&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
    &amp;lt;!--每个初始化参数都需要用到init-param标签--&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;servletInfo&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;This is Demo8&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
    &amp;lt;servlet-name&amp;gt;servletDemo8&amp;lt;/servlet-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/servletDemo8&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//演示Servlet的初始化参数对象
public class ServletDemo8 extends HttpServlet {
	//定义Servlet配置对象ServletConfig
    private ServletConfig servletConfig;

    //在初始化时为ServletConfig赋值
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.servletConfig = config;
    }
    /**
       * doGet方法输出一句话
       */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.输出ServletConfig
        System.out.println(servletConfig);
        //2.获取Servlet的名称
        String servletName= servletConfig.getServletName();
        System.out.println(servletName);
        //3.获取字符集编码
        String encoding = servletConfig.getInitParameter(&quot;encoding&quot;);
        System.out.println(encoding);
        //4.获取所有初始化参数名称的枚举
        Enumeration&amp;lt;String&amp;gt; names = servletConfig.getInitParameterNames();
        //遍历names
        while(names.hasMoreElements()){
            //取出每个name
            String name = names.nextElement();
            //根据key获取value
            String value = servletConfig.getInitParameter(name);
            System.out.println(&quot;name:&quot;+name+&quot;,value:&quot;+value);
        }
        //5.获取ServletContext对象
        ServletContext servletContext = servletConfig.getServletContext();
        System.out.println(servletContext);
    }

    //调用doGet方法
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/ServletConfig%E6%BC%94%E7%A4%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ServletContext&lt;/h3&gt;
&lt;p&gt;ServletContext 对象是应用上下文对象。服务器为每一个应用都创建了一个 ServletContext 对象，ServletContext 属于整个应用，不局限于某个 Servlet，可以实现让应用中所有 Servlet 间的数据共享。&lt;/p&gt;
&lt;p&gt;上下文代表了程序当下所运行的环境，联系整个应用的生命周期与资源调用，是程序可以访问到的所有资源的总和，资源可以是一个变量，也可以是一个对象的引用&lt;/p&gt;
&lt;p&gt;生命周期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出生：应用一加载，该对象就被创建出来。一个应用只有一个实例对象（Servlet 和 ServletContext 都是单例的）&lt;/li&gt;
&lt;li&gt;活着：只要应用一直提供服务，该对象就一直存在。&lt;/li&gt;
&lt;li&gt;死亡：应用被卸载（或者服务器停止），该对象消亡。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;域对象：指的是对象有作用域，即有作用范围，可以&lt;strong&gt;实现数据共享&lt;/strong&gt;，不同作用范围的域对象，共享数据的能力不一样。&lt;/p&gt;
&lt;p&gt;Servlet 规范中，共有4个域对象，ServletContext 是其中一个，web 应用中最大的作用域，叫 application 域，可以实现整个应用间的数据共享功能。&lt;/p&gt;
&lt;p&gt;数据共享：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/ServletContext共享数据.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;获取ServletContext：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java 项目继承 HttpServlet，HttpServlet 继承 GenericServlet，GenericServlet 中有一个方法可以直接使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ServletContext getServletContext() {
        return this.getServletConfig().getServletContext();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServletRequest 类方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ServletContext getServletContext()//获取ServletContext对象
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String getInitParameter(String name)&lt;/code&gt; : 根据名称获取全局配置的参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getContextPath&lt;/code&gt; : 获取当前应用访问的虚拟目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getRealPath(String path)&lt;/code&gt; : 根据虚拟目录获取应用部署的磁盘绝对路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void setAttribute(String name, Object object)&lt;/code&gt; : 向应用域对象中存储数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object getAttribute(String name)&lt;/code&gt; : 根据名称获取域对象中的数据，没有则返回null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void removeAttribute(String name)&lt;/code&gt; : 根据名称移除应用域对象中的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;web.xml配置：
配置的方式，需要在&lt;code&gt;&amp;lt;web-app&amp;gt;&lt;/code&gt;标签中使用&lt;code&gt;&amp;lt;context-param&amp;gt;&lt;/code&gt;来配置初始化参数，它的配置是针对整个应用的配置，被称为应用的初始化参数配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置应用初始化参数--&amp;gt;
&amp;lt;context-param&amp;gt;
    &amp;lt;!--用于获取初始化参数的key--&amp;gt;
    &amp;lt;param-name&amp;gt;servletContextInfo&amp;lt;/param-name&amp;gt;
    &amp;lt;!--初始化参数的值--&amp;gt;
    &amp;lt;param-value&amp;gt;This is application scope&amp;lt;/param-value&amp;gt;
&amp;lt;/context-param&amp;gt;
&amp;lt;!--每个应用初始化参数都需要用到context-param标签--&amp;gt;
&amp;lt;context-param&amp;gt;
    &amp;lt;param-name&amp;gt;globalEncoding&amp;lt;/param-name&amp;gt;
    &amp;lt;param-value&amp;gt;UTF-8&amp;lt;/param-value&amp;gt;
&amp;lt;/context-param&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServletContextDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取ServletContext对象
        ServletContext context = getServletContext();

        //获取全局配置的globalEncoding
        String value = context.getInitParameter(&quot;globalEncoding&quot;);
        System.out.println(value);//UTF-8

        //获取应用的访问虚拟目录
        String contextPath = context.getContextPath();
        System.out.println(contextPath);//servlet

        //根据虚拟目录获取应用部署的磁盘绝对路径
        //获取b.txt文件的绝对路径 web目录下
        String b = context.getRealPath(&quot;/b.txt&quot;);
        System.out.println(b);

        //获取c.txt文件的绝对路径  /WEB-INF目录下
        String c = context.getRealPath(&quot;/WEB-INF/c.txt&quot;);
        System.out.println(c);

        //获取a.txt文件的绝对路径 //src目录下
        String a = context.getRealPath(&quot;/WEB-INF/classes/a.txt&quot;);
        System.out.println(a);

        //向域对象中存储数据
        context.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);

        //移除域对象中username的数据
        //context.removeAttribute(&quot;username&quot;);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

//E:\Database\Java\Project\JavaEE\out\artifacts\Servlet_war_exploded\b.txt
//E:\Database\Java\Project\JavaEE\out\artifacts\Servlet_war_exploded\WEB-INF\c.txt
//E:\Database\Java\Project\JavaEE\out\artifacts\Servlet_war_exploded\WEB-INF\classes\a.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解开发&lt;/h3&gt;
&lt;p&gt;Servlet3.0 版本！不需要配置 web.xml&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注解案例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo1&quot;)
public class ServletDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;Servlet Demo1 Annotation&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WebServlet注解（@since Servlet 3.0 (Section 8.1.1)）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    //指定Servlet的名称。相当于xml配置中&amp;lt;servlet&amp;gt;标签下的&amp;lt;servlet-name&amp;gt;
    String name() default &quot;&quot;;

    //用于映射Servlet访问的url映射，相当于xml配置时的&amp;lt;url-pattern&amp;gt;
    String[] value() default {};

    //相当于xml配置时的&amp;lt;url-pattern&amp;gt;
    String[] urlPatterns() default {};

	//用于配置Servlet的启动时机，相当于xml配置的&amp;lt;load-on-startup&amp;gt;
    int loadOnStartup() default -1;

    //用于配置Servlet的初始化参数，相当于xml配置的&amp;lt;init-param&amp;gt;
    WebInitParam[] initParams() default {};

    //用于配置Servlet是否支持异步，相当于xml配置的&amp;lt;async-supported&amp;gt;
    boolean asyncSupported() default false;

    //用于指定Servlet的小图标
    String smallIcon() default &quot;&quot;;

    //用于指定Servlet的大图标
    String largeIcon() default &quot;&quot;;

    //用于指定Servlet的描述信息
    String description() default &quot;&quot;;

    //用于指定Servlet的显示名称
    String displayName() default &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;手动创建容器：（了解）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Request&lt;/h2&gt;
&lt;h3&gt;请求响应&lt;/h3&gt;
&lt;p&gt;Web服务器收到客户端的http请求，会针对每一次请求，分别创建一个用于代表请求的request对象、和代表响应的response对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet%E8%AF%B7%E6%B1%82%E5%93%8D%E5%BA%94%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;请求对象&lt;/h3&gt;
&lt;p&gt;请求：客户机希望从服务器端索取一些资源，向服务器发出询问&lt;/p&gt;
&lt;p&gt;请求对象：在 JavaEE 工程中，用于发送请求的对象，常用的对象是 ServletRequest 和 HttpServletRequest ，它们的区是是否与 HTTP 协议有关&lt;/p&gt;
&lt;p&gt;Request 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作请求三部分(行,头,体)&lt;/li&gt;
&lt;li&gt;请求转发&lt;/li&gt;
&lt;li&gt;作为域对象存数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Request%E8%AF%B7%E6%B1%82%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%B1%BB%E8%A7%86%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;请求路径&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String  getLocalAddr()&lt;/td&gt;
&lt;td&gt;获取本机（服务器）地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getLocalName()&lt;/td&gt;
&lt;td&gt;获取本机（服务器）名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getLocalPort()&lt;/td&gt;
&lt;td&gt;获取本机（服务器）端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getRemoteAddr()&lt;/td&gt;
&lt;td&gt;获取访问者IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getRemoteHost&lt;/td&gt;
&lt;td&gt;获取访问者主机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getRemotePort()&lt;/td&gt;
&lt;td&gt;获取访问者端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getMethod();&lt;/td&gt;
&lt;td&gt;获得请求方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getRequestURI()&lt;/td&gt;
&lt;td&gt;获取统一资源标识符（/request/servletDemo01）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getRequestURL()&lt;/td&gt;
&lt;td&gt;获取统一资源定位符（http://localhost:8080/request/servletDemo01）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getQueryString()&lt;/td&gt;
&lt;td&gt;获取请求消息的数据&amp;lt;br /&amp;gt;（GET方式 URL中带参字符串：username=aaa&amp;amp;password=123）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getContextPath()&lt;/td&gt;
&lt;td&gt;获取虚拟目录名称（/request）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getServletPath&lt;/td&gt;
&lt;td&gt;获取Servlet映射路径&amp;lt;br /&amp;gt;（&amp;lt;url-pattern&amp;gt;或@WebServlet值: /servletDemo01）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getRealPath(String path)&lt;/td&gt;
&lt;td&gt;根据虚拟目录获取应用部署的磁盘绝对路径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;URL = URI + HOST&lt;/p&gt;
&lt;p&gt;URL = HOST + ContextPath + ServletPath&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;获取请求头&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String  getHeader(String name)&lt;/td&gt;
&lt;td&gt;获得指定请求头的值。&amp;lt;br /&amp;gt;如果没有该请求头返回null，有多个值返回第一个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enumeration&amp;lt;String&amp;gt; getHeaders(String name)&lt;/td&gt;
&lt;td&gt;获取指定请求头的多个值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enumeration&amp;lt;String&amp;gt; getHeaderNames()&lt;/td&gt;
&lt;td&gt;获取所有请求头名称的枚举&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo02&quot;)
public class ServletDemo02 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.根据请求头名称获取一个值
        String connection = req.getHeader(&quot;connection&quot;);
        System.out.println(connection);//keep-alive

        //2.根据请求头名称获取多个值
        Enumeration&amp;lt;String&amp;gt; values = req.getHeaders(&quot;accept-encoding&quot;);
        while(values.hasMoreElements()) {
            String value = values.nextElement();
            System.out.println(value);//gzip, deflate, br
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;请求参数&lt;/h3&gt;
&lt;h4&gt;请求参数&lt;/h4&gt;
&lt;p&gt;请求参数是正文部分&amp;lt;input&amp;gt;标签内容，&amp;lt;form&amp;gt;标签属性action=&quot;/request/servletDemo08&quot;，服务器URI&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;法名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getParameter(String name)&lt;/td&gt;
&lt;td&gt;获得指定参数名的值&amp;lt;br /&amp;gt;如果没有该参数则返回null，如果有多个获得第一个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String[] getParameterValues(String name)&lt;/td&gt;
&lt;td&gt;获得指定参数名所有的值。此方法为复选框提供的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enumeration&amp;lt;String&amp;gt; getParameterNames()&lt;/td&gt;
&lt;td&gt;获得所有参数名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map&amp;lt;String,String[]&amp;gt; getParameterMap()&lt;/td&gt;
&lt;td&gt;获得所有的请求参数键值对（key=value）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;封装参数&lt;/h4&gt;
&lt;p&gt;封装请求参数到类对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接封装：有参构造或者set方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo04&quot;)
public class ServletDemo04 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取所有的数据
        String username = req.getParameter(&quot;username&quot;);
        String password = req.getParameter(&quot;password&quot;);
        String[] hobbies = req.getParameterValues(&quot;hobby&quot;);

        //2.封装学生对象
        Student stu = new Student(username,password,hobbies);

        //3.输出对象
        System.out.println(stu);

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private String username;
    private String password;
    private String[] hobby;
        
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--register.html--&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;注册页面&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;/request/servletDemo05&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
        姓名：&amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt; &amp;lt;br&amp;gt;
        密码：&amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt; &amp;lt;br&amp;gt;
        爱好：&amp;lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;study&quot;&amp;gt;学习
              &amp;lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;game&quot;&amp;gt;游戏 &amp;lt;br&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;注册&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反射方式：&lt;/p&gt;
&lt;p&gt;表单&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;标签的name属性取值，必须和实体类中定义的属性名称一致&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //1.获取请求正文的映射关系
    Map&amp;lt;String, String[]&amp;gt; map = req.getParameterMap();
    //2.封装学生对象
    Student stu = new Student();
    //2.1遍历集合
    for(String name : map.keySet()) {
        String[] value = map.get(name);
        try {
            //2.2获取Student对象的属性描述器
            //参数一：指定获取xxx属性的描述器
            //参数二：指定字节码文件
            PropertyDescriptor pd = new PropertyDescriptor(name,stu.getClass());
            //2.3获取对应的setXxx方法
            Method writeMethod = pd.getWriteMethod();
            //2.4执行方法
            if(value.length &amp;gt; 1) {
                writeMethod.invoke(stu,(Object)value);
            }else {
                writeMethod.invoke(stu,value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //3.输出对象
    System.out.println(stu);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;commons-beanutils封装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取所有的数据
        Map&amp;lt;String, String[]&amp;gt; map = req.getParameterMap();
        //2.封装学生对象
        Student stu = new Student();
        try {
            BeanUtils.populate(stu,map);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.输出对象
        System.out.println(stu);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;流获取数据&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ServletInputStream getInputStream()&lt;/code&gt; : 获取请求字节输入流对象
&lt;code&gt;BufferedReader getReader()  &lt;/code&gt; : 获取请求缓冲字符输入流对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo07&quot;)
public class ServletDemo07 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //字符流(必须是post方式)
        /*BufferedReader br = req.getReader();
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }*/
        //br.close();
        //字节流
        ServletInputStream is = req.getInputStream();
        byte[] arr = new byte[1024];
        int len;
        while((len = is.read(arr)) != -1) {
            System.out.println(new String(arr,0,len));
        }
        //is.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;/request/servletDemo07&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;请求域&lt;/h3&gt;
&lt;h4&gt;请求域&lt;/h4&gt;
&lt;p&gt;request 域：可以在一次请求范围内进行共享数据&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void setAttribute(String name, Object value)&lt;/td&gt;
&lt;td&gt;向请求域对象中存储数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object getAttribute(String name)&lt;/td&gt;
&lt;td&gt;通过名称获取请求域对象的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void removeAttribute(String name)&lt;/td&gt;
&lt;td&gt;通过名称移除请求域对象的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;请求转发&lt;/h4&gt;
&lt;p&gt;请求转发：客户端的一次请求到达后，需要借助其他 Servlet 来实现功能，进行请求转发。特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器地址栏不变&lt;/li&gt;
&lt;li&gt;域对象中的数据不丢失&lt;/li&gt;
&lt;li&gt;负责转发的 Servlet 转发前后响应正文会丢失&lt;/li&gt;
&lt;li&gt;由转发目的地来响应客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HttpServletRequest 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RequestDispatcher getRequestDispatcher(String path) &lt;/code&gt; : 获取任务调度对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RequestDispatcher 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void forward(ServletRequest request, ServletResponse response)&lt;/code&gt; : 实现转发，将请求从 Servlet 转发到服务器上的另一个资源（Servlet，JSP 文件或 HTML 文件）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;过程：浏览器访问 http://localhost:8080/request/servletDemo09，/servletDemo10也会执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo09&quot;)
public class ServletDemo09 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置共享数据
        req.setAttribute(&quot;encoding&quot;,&quot;gbk&quot;);
        //获取请求调度对象
        RequestDispatcher rd = req.getRequestDispatcher(&quot;/servletDemo10&quot;);
        //实现转发功能
        rd.forward(req,resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo10&quot;)
public class ServletDemo10 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取共享数据
        Object encoding = req.getAttribute(&quot;encoding&quot;);
        System.out.println(encoding);//gbk

        System.out.println(&quot;servletDemo10执行了...&quot;);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;请求包含&lt;/h4&gt;
&lt;p&gt;请求包含：合并其他的 Servlet 中的功能一起响应给客户端。特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器地址栏不变&lt;/li&gt;
&lt;li&gt;域对象中的数据不丢失&lt;/li&gt;
&lt;li&gt;被包含的 Servlet 响应头会丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请求转发的注意事项：负责转发的 Servlet，转发前后的响应正文丢失，由转发目的地来响应浏览器&lt;/p&gt;
&lt;p&gt;请求包含的注意事项：被包含者的响应消息头丢失，因为它被包含者包含起来了&lt;/p&gt;
&lt;p&gt;HttpServletRequest 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RequestDispatcher getRequestDispatcher(String path) &lt;/code&gt; : 获取任务调度对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RequestDispatcher 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void include(ServletRequest request, ServletResponse response) &lt;/code&gt; : 实现包含。包括响应中资源的内容（servlet，JSP页面，HTML文件）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo11&quot;)
public class ServletDemo11 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;servletDemo11执行了...&quot;);//执行了
        //获取请求调度对象
        RequestDispatcher rd = req.getRequestDispatcher(&quot;/servletDemo12&quot;);
        //实现包含功能
        rd.include(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
**********************************************************************************
@WebServlet(&quot;/servletDemo12&quot;)
public class ServletDemo12 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;servletDemo12执行了...&quot;);//输出了
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;乱码问题&lt;/h3&gt;
&lt;p&gt;请求体&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;POST：&lt;code&gt;void setCharacterEncoding(String env)&lt;/code&gt;：设置请求体的编码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo08&quot;)
public class ServletDemo08 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置编码格式
        req.setCharacterEncoding(&quot;UTF-8&quot;);

        String username = req.getParameter(&quot;username&quot;);
        System.out.println(username);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GET：Tomcat8.5 版本及以后，Tomcat 服务器已经帮我们解决&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Response&lt;/h2&gt;
&lt;h3&gt;响应对象&lt;/h3&gt;
&lt;p&gt;响应，服务器把请求的处理结果告知客户端&lt;/p&gt;
&lt;p&gt;响应对象：在 JavaEE 工程中，用于发送响应的对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协议无关的对象标准是：ServletResponse 接口&lt;/li&gt;
&lt;li&gt;协议相关的对象标准是：HttpServletResponse 接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Response 的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作响应的三部分(行, 头, 体)&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;请求重定向&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response%E5%93%8D%E5%BA%94%E7%B1%BB%E8%A7%86%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;操作响应行&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;int getStatus()&lt;/td&gt;
&lt;td&gt;Gets the current status code of this response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setStatus(int sc)&lt;/td&gt;
&lt;td&gt;Sets the status code for this response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;状态码：（HTTP--&amp;gt;相应部分）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1xx&lt;/td&gt;
&lt;td&gt;消息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;重定向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;客户端错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;服务器错误&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;操作响应体&lt;/h3&gt;
&lt;h4&gt;字节流响应&lt;/h4&gt;
&lt;p&gt;响应体对应&lt;strong&gt;乱码问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;项目中常用的编码格式是UTF-8，而浏览器默认使用的编码是gbk。导致乱码！&lt;/p&gt;
&lt;p&gt;解决方式：
一：修改浏览器的编码格式(不推荐，不能让用户做修改的动作)
二：通过输出流写出一个标签：&amp;lt;meta http-equiv=&apos;content-type&apos;content=&apos;text/html;charset=UTF-8&apos;&amp;gt;
三：指定响应头信息：response.setHeader(&quot;Content-Type&quot;,&quot;text/html;charset=UTF-8&quot;)
四：response.setContentType(&quot;text/html;charset=UTF-8&quot;)&lt;/p&gt;
&lt;p&gt;常用API：
&lt;code&gt;ServletOutputStream getOutputStream()&lt;/code&gt; : 获取响应字节输出流对象
&lt;code&gt;void setContenType(&quot;text/html;charset=UTF-8&quot;)&lt;/code&gt; : 设置响应内容类型，解决中文乱码问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo01&quot;)
public class ServletDemo01 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.设置响应内容类型
		resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
        //2.通过响应对象获取字节输出流对象
        ServletOutputStream sos = resp.getOutputStream();
        //3.定义消息
        String str = &quot;你好&quot;;
        //4.通过字节流输出对象
        sos.write(str.getBytes(&quot;UTF-8&quot;));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;字符流响应&lt;/h4&gt;
&lt;p&gt;response得到的字符流和字节流互斥，只能选其一，response获取的流不用关闭，由服务器关闭即可。&lt;/p&gt;
&lt;p&gt;常用API：
&lt;code&gt;PrintWriter getWriter()&lt;/code&gt; : 获取响应字节输出流对象，可以发送标签
&lt;code&gt;void setContenType(&quot;text/html;charset=UTF-8&quot;)&lt;/code&gt; : 设置响应内容类型，解决中文乱码问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String str = &quot;你好&quot;;
    //解决中文乱码
    resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
    //获取字符流对象
    PrintWriter pw = resp.getWriter();
    pw.write(str);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;响应图片&lt;/h4&gt;
&lt;p&gt;响应图片到浏览器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo03&quot;)
public class ServletDemo03 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.通过文件的相对路径来获取文件的绝对路径
        String realPath = getServletContext().getRealPath(&quot;/img/hm.png&quot;);
        //E:\Project\JavaEE\out\artifacts\Response_war_exploded\img\hm.png
        System.out.println(realPath);
        //2.创建字节输入流对象，关联图片路径
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(realPath));

        //3.通过响应对象获取字节输出流对象
        ServletOutputStream sos = resp.getOutputStream();

        //4.循环读写
        byte[] arr = new byte[1024];
        int len;
        while((len = bis.read(arr)) != -1) {
            sos.write(arr,0,len);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;操作响应头&lt;/h3&gt;
&lt;h4&gt;常用方法&lt;/h4&gt;
&lt;p&gt;响应头: 是服务器指示浏览器去做什么&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getHeader(String name)&lt;/td&gt;
&lt;td&gt;获取指定响应头的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collection&amp;lt;String&amp;gt; getHeaders(String name)&lt;/td&gt;
&lt;td&gt;获取指定响应头的多个值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collection&amp;lt;String&amp;gt; getHeaderNames()&lt;/td&gt;
&lt;td&gt;获取所有响应头名称的枚举&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setHeader(String name, String value)&lt;/td&gt;
&lt;td&gt;设置响应头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setDateHeader(String name, long date)&lt;/td&gt;
&lt;td&gt;设置具有给定名称和日期值的响应消息头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void sendRedirect(String location)&lt;/td&gt;
&lt;td&gt;设置重定向&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;setHeader常用响应头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expires：设置缓存时间&lt;/li&gt;
&lt;li&gt;Refresh：定时跳转&lt;/li&gt;
&lt;li&gt;Location：重定向地址&lt;/li&gt;
&lt;li&gt;Content-Disposition: 告诉浏览器下载&lt;/li&gt;
&lt;li&gt;Content-Type：设置响应内容的MIME类型(服务器告诉浏览器内容的类型)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;控制缓存&lt;/h4&gt;
&lt;p&gt;缓存：对于不经常变化的数据，我们可以设置合理的缓存时间，防止浏览器频繁的请求服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo04&quot;)
public class ServletDemo04 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String news = &quot;设置缓存时间&quot;;
        //设置缓存时间，缓存一小时
        resp.setDateHeader(&quot;Expires&quot;,System.currentTimeMillis()+1*60*60*1000L);
        //设置编码格式
        resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
        //写出数据
        resp.getWriter().write(news);
        System.out.println(&quot;aaa&quot;);//只输出一次，不能刷新，必须从网址直接进入
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response%E8%AE%BE%E7%BD%AE%E7%BC%93%E5%AD%98%E6%97%B6%E9%97%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;定时刷新&lt;/h4&gt;
&lt;p&gt;定时刷新：过了指定时间后，页面进行自动跳转&lt;/p&gt;
&lt;p&gt;格式：&lt;code&gt;setHeader(&quot;Refresh&quot;, &quot;3;URL=https://www.baidu.com&quot;&quot;);&lt;/code&gt;
Refresh设置的时间单位是秒，如果刷新到其他地址，需要在时间后面拼接上地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo05&quot;)
public class ServletDemo05 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String news = &quot;您的用户名或密码错误，3秒后自动跳转到登录页面...&quot;;
        //设置编码格式
        resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
        //写出数据
        resp.getWriter().write(news);

        //设置响应消息头定时刷新
        resp.setHeader(&quot;Refresh&quot;,&quot;3;URL=/response/login.html&quot;);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;下载文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo06&quot;)
public class ServletDemo06 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.创建字节输入流对象，关联读取的文件
        String realPath = getServletContext().getRealPath(&quot;/img/hm.png&quot;);//绝对路径
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(realPath));

        //2.设置响应头支持的类型  应用支持的类型为字节流
        /*
            Content-Type 消息头名称   支持的类型
            application/octet-stream   消息头参数  应用类型为字节流
         */
        resp.setHeader(&quot;Content-Type&quot;,&quot;application/octet-stream&quot;);

        //3.设置响应头以下载方式打开  以附件形式处理内容
        /*
            Content-Disposition  消息头名称  处理的形式
            attachment;filename=  消息头参数  附件形式进行处理
         */
        resp.setHeader(&quot;Content-Disposition&quot;,&quot;attachment;filename=&quot; + System.currentTimeMillis() + &quot;.png&quot;);

        //4.获取字节输出流对象
        ServletOutputStream sos = resp.getOutputStream();

        //5.循环读写文件
        byte[] arr = new byte[1024];
        int len;
        while((len = bis.read(arr)) != -1) {
            sos.write(arr,0,len);
        }

        //6.释放资源
        bis.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;重定向&lt;/h4&gt;
&lt;h5&gt;实现重定向&lt;/h5&gt;
&lt;p&gt;请求重定向：客户端的一次请求到达后，需要借助其他 Servlet 来实现功能。特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重定向两次请求&lt;/li&gt;
&lt;li&gt;重定向的地址栏路径改变&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重定向的路径写绝对路径&lt;/strong&gt;（带域名 /ip 地址，如果是同一个项目，可以省略域名 /ip 地址）&lt;/li&gt;
&lt;li&gt;重定向的路径可以是项目内部的,也可以是项目以外的（百度）&lt;/li&gt;
&lt;li&gt;重定向不能重定向到 WEB-INF 下的资源&lt;/li&gt;
&lt;li&gt;把数据存到 request 域里面，重定向不可用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置响应状态码：&lt;code&gt;resp.setStatus(302)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置重定向的路径（响应到哪里，通过响应头 location 来指定）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;response.setHeader(&quot;Location&quot;,&quot;http://www.baidu.com&quot;);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.setHeader(&quot;Location&quot;,&quot;/response/servletDemo08);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt; resp.sendRedirect(&quot;重定向的路径&quot;);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo07&quot;)
public class ServletDemo07 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置请求域数据
        req.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);

        //设置重定向
        resp.sendRedirect(req.getContextPath() + &quot;/servletDemo07&quot;);
		// resp.sendRedirect(&quot;https://www.baidu.com&quot;);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo08&quot;)
public class ServletDemo08 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;servletDemo08执行了...&quot;);
        Object username = req.getAttribute(&quot;username&quot;);
        System.out.println(username);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;重定向和转发&lt;/h5&gt;
&lt;p&gt;请求重定向跳转的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重定向是由&lt;strong&gt;浏览器发起&lt;/strong&gt;的，在这个过程中浏览器会发起&lt;strong&gt;两次请求&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;重定向可以跳转到任意服务器的资源，但是&lt;strong&gt;无法跳转到WEB-INF中的资源&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;重定向不能和请求域对象共享数据，数据会丢失&lt;/li&gt;
&lt;li&gt;重定向浏览器的地址栏中的地址会变成跳转到的路径&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请求转发跳转的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请求转发是由&lt;strong&gt;服务器发起&lt;/strong&gt;的，在这个过程中浏览器只会发起&lt;strong&gt;一次请求&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;请求转发只能跳转到本项目的资源，但是&lt;strong&gt;可以跳转到WEB-INF中的资源&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;请求转发可以和请求域对象共享数据，数据不会丢失&lt;/li&gt;
&lt;li&gt;请求转发浏览器地址栏不变&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/%E9%87%8D%E5%AE%9A%E5%90%91%E5%92%8C%E8%AF%B7%E6%B1%82%E8%BD%AC%E5%8F%91%E5%AF%B9%E6%AF%94%E5%9B%BE.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;路径问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;完整URL地址：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;协议：http://&lt;/li&gt;
&lt;li&gt;服务器主机地址：127.0.0.1  or localhost&lt;/li&gt;
&lt;li&gt;服务器端口号：8080&lt;/li&gt;
&lt;li&gt;项目的虚拟路径(部署路径)：/response&lt;/li&gt;
&lt;li&gt;具体的项目上资源路径   /login.html      or     Demo 的Servlet映射路径&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;相对路径：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不以&quot;/&quot;开头的路径写法，它是以目标路径相对当前文件的路径，其中&quot;..&quot;表示上一级目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;hello world....&amp;lt;/h1&amp;gt;
    &amp;lt;!--
        目标资源的url: http://localhost:8080/response/demo05
        当前资源的url: http://localhost:8080/response/pages/demo.html
        相对路径的优劣:
            1. 优势: 无论部署的项目名怎么改变，我的路径都不需要改变
            2. 劣势: 如果当前资源的位置发生改变，那么相对路径就必定要发生改变--&amp;gt;
    &amp;lt;a href=&quot;../demo05&quot;&amp;gt;访问ServletDemo05&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;绝对路径：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;绝对路径就是以&quot;/&quot;开头的路径写法，项目部署的路径&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Cookie&lt;/h2&gt;
&lt;h3&gt;会话技术&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;会话&lt;/strong&gt;：浏览器和服务器之间的多次请求和响应&lt;/p&gt;
&lt;p&gt;浏览器和服务器可能产生多次的请求和响应，从浏览器访问服务器开始，到访问服务器结束（关闭浏览器、到了过期时间），这期间产生的多次请求和响应加在一起称为浏览器和服务器之间的一次对话&lt;/p&gt;
&lt;p&gt;作用：保存用户各自的数据（以浏览器为单位），在多次请求间实现数据共享&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用的会话管理技术&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cookie：客户端会话管理技术，用户浏览的信息以键值对（key=value）的形式保存在浏览器上。如果没有关闭浏览器，再次访问服务器，会把 cookie 带到服务端，服务端就可以做相应的处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session：服务端会话管理技术。当客户端第一次请求 session 对象时，服务器为每一个浏览器开辟一块内存空间，并将通过特殊算法算出一个 session 的 ID，用来标识该 session 对象。由于内存空间是每一个浏览器独享的，所有用户在访问的时候，可以把信息保存在 session 对象中，同时服务器会把 sessionId 写到 cookie 中，再次访问的时候，浏览器会把 cookie(sessionId) 带过来，找到对应的 session 对象即可&lt;/p&gt;
&lt;p&gt;tomcat 生成的 sessionID 叫做 jsessionID&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cookie 存储在客户端中，而 Session 存储在服务器上，相对来说 Session 安全性更高。如果要在 Cookie 中存储一些敏感信息，不要直接写入 Cookie，应该将 Cookie 信息加密然后使用到的时候再去服务器端解密&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie 一般用来保存用户信息，在 Cookie 中保存已经登录过得用户信息，下次访问网站的时候就不需要重新登录，因为用户登录的时候可以存放一个 Token 在 Cookie 中，下次登录的时候只需要根据 Token 值来查找用户即可（为了安全考虑，重新登录一般要将 Token 重写），所以登录一次网站后访问网站其他页面不需要重新登录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session 通过服务端记录用户的状态，服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie 只能存储 ASCII 码，而 Session 可以存储任何类型的数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/weixin_43625577/article/details/92393581&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;Cookie：客户端会话管理技术，把要共享的数据保存到了客户端（也就是浏览器端）。每次请求时，把会话信息带到服务器，从而实现多次请求的数据共享。&lt;/p&gt;
&lt;p&gt;作用：保存客户浏览器访问网站的相关内容（需要客户端不禁用 Cookie），从而在每次访问同一个内容时，先从本地缓存获取，使资源共享，提高效率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Cookie%E7%B1%BB%E8%AE%B2%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;基本使用&lt;/h3&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cookie属性：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名称&lt;/th&gt;
&lt;th&gt;属性作用&lt;/th&gt;
&lt;th&gt;是否重要&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;cookie的名称&lt;/td&gt;
&lt;td&gt;必要属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;value&lt;/td&gt;
&lt;td&gt;cookie的值（不能是中文）&lt;/td&gt;
&lt;td&gt;必要属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;cookie的路径&lt;/td&gt;
&lt;td&gt;重要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;domain&lt;/td&gt;
&lt;td&gt;cookie的域名&lt;/td&gt;
&lt;td&gt;重要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxAge&lt;/td&gt;
&lt;td&gt;cookie的生存时间&lt;/td&gt;
&lt;td&gt;重要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;version&lt;/td&gt;
&lt;td&gt;cookie的版本号&lt;/td&gt;
&lt;td&gt;不重要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comment&lt;/td&gt;
&lt;td&gt;cookie的说明&lt;/td&gt;
&lt;td&gt;不重要&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意：Cookie 有大小，个数限制。每个网站最多只能存20个 Cookie，且大小不能超过 4kb。同时所有网站的 Cookie 总数不超过300个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cookie类API：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Cookie(String name, String value)&lt;/code&gt; : 构造方法创建 Cookie 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie 属性对应的 set 和 get 方法，name 属性被 final 修饰，没有 set 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpServletResponse 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void addCookie(Cookie cookie)&lt;/code&gt;：向客户端添加 Cookie，Adds cookie to the response&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpServletRequest类API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cookie[] getCookies()&lt;/code&gt;：获取所有的 Cookie 对象，client sent with this request&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;有效期&lt;/h4&gt;
&lt;p&gt;如果不设置过期时间，表示这个 Cookie 生命周期为浏览器会话期间，只要关闭浏览器窗口 Cookie 就消失，这种生命期为浏览会话期的 Cookie 被称为会话 Cookie，会话 Cookie 一般不保存在硬盘上而是保存在内存里。&lt;/p&gt;
&lt;p&gt;如果设置过期时间，浏览器就会把 Cookie 保存到硬盘上，关闭后再次打开浏览器，这些 Cookie 依然有效直到超过设定的过期时间。存储在硬盘上的 Cookie 可以在&lt;strong&gt;不同的浏览器进程间共享&lt;/strong&gt;，比如两个 IE 窗口，而对于保存在内存的 Cookie，不同的浏览器有不同的处理方式&lt;/p&gt;
&lt;p&gt;设置 Cookie 存活时间 API：&lt;code&gt;void setMaxAge(int expiry)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-1：默认，代表 Cookie 数据存到浏览器关闭（保存在浏览器文件中）&lt;/li&gt;
&lt;li&gt;0：代表删除 Cookie，如果要删除 Cookie 要确保&lt;strong&gt;路径一致&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;正整数：以秒为单位保存数据有有效时间（把缓存数据保存到磁盘中）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo01&quot;)
public class ServletDemo01 extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.通过响应对象写出提示信息
        resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
        PrintWriter pw = resp.getWriter();
        pw.write(&quot;欢迎访问本网站，您的最后访问时间为：&amp;lt;br&amp;gt;&quot;);

        //2.创建Cookie对象，用于记录最后访问时间
        Cookie cookie = new Cookie(&quot;time&quot;,System.currentTimeMillis()+&quot;&quot;);

        //3.设置最大存活时间
        cookie.setMaxAge(3600);
        //cookie.setMaxAge(0);    // 立即清除

        //4.将cookie对象添加到客户端
        resp.addCookie(cookie);

        //5.获取cookie
        Cookie[] cookies = req.getCookies();
        for(Cookie c : cookies) {
            if(&quot;time&quot;.equals(c.getName())) {
                //6.获取cookie对象中的value，进行写出
                String value = c.getValue();
                SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
                pw.write(sdf.format(Long.parseLong(value)));
            }
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;有效路径&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;setPath(String url)&lt;/code&gt; : Cookie 设置有效路径&lt;/p&gt;
&lt;p&gt;有效路径作用 :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保证不会携带别的网站/项目里面的 Cookie 到我们自己的项目&lt;/li&gt;
&lt;li&gt;路径不一样，Cookie 的 key 可以相同&lt;/li&gt;
&lt;li&gt;保证自己的项目可以合理的利用自己项目的 Cookie&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;判断路径是否携带 Cookie：请求资源 URI.startWith(cookie的path)，返回 true 就带&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;访问URL&lt;/th&gt;
&lt;th&gt;URI部分&lt;/th&gt;
&lt;th&gt;Cookie的Path&lt;/th&gt;
&lt;th&gt;是否携带Cookie&lt;/th&gt;
&lt;th&gt;能否取到Cookie&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://localhost:8080/servlet/servletDemo02&quot;&gt;servletDemo02&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;/servlet/servletDemo02&lt;/td&gt;
&lt;td&gt;/servlet/&lt;/td&gt;
&lt;td&gt;带&lt;/td&gt;
&lt;td&gt;能取到&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://localhost:8080/servlet/servletDemo03&quot;&gt;servletDemo03&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;/servlet/servletDemo03&lt;/td&gt;
&lt;td&gt;/servlet/&lt;/td&gt;
&lt;td&gt;带&lt;/td&gt;
&lt;td&gt;能取到&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://localhost:8080/servlet/aaa/servletDemo03&quot;&gt;servletDemo04&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;/servlet/aaa/servletDemo04&lt;/td&gt;
&lt;td&gt;/servlet/&lt;/td&gt;
&lt;td&gt;带&lt;/td&gt;
&lt;td&gt;能取到&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;http://localhost:8080/bbb/servletDemo03&quot;&gt;servletDemo05&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;/bbb/servletDemo04&lt;/td&gt;
&lt;td&gt;/servlet/&lt;/td&gt;
&lt;td&gt;不带&lt;/td&gt;
&lt;td&gt;不能取到&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只有当访问资源的 url 包含此 cookie 的有效 path 的时候，才会携带这个 cookie&lt;/p&gt;
&lt;p&gt;想要当前项目下的 Servlet 可以使用该 cookie，一般设置：&lt;code&gt;cookie.setPath(request.getContextPath())&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;安全性&lt;/h4&gt;
&lt;p&gt;如果 Cookie 中设置了 HttpOnly 属性，通过 js 脚本将无法读取到 cookie 信息，这样能有效的防止 XSS 攻击，窃取 cookie 内容，这样就增加了安全性，即便是这样，也不要将重要信息存入cookie。&lt;/p&gt;
&lt;p&gt;XSS 全称 Cross SiteScript，跨站脚本攻击，是Web程序中常见的漏洞，XSS 属于被动式且用于客户端的攻击方式，所以容易被忽略其危害性。其原理是攻击者向有 XSS 漏洞的网站中输入(传入)恶意的 HTML 代码，当其它用户浏览该网站时，这段HTML代码会自动执行，从而达到攻击的目的。如盗取用户 Cookie、破坏页面结构、重定向到其它网站等。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Session&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;Session：服务器端会话管理技术，本质也是采用客户端会话管理技术，不过在客户端保存的是一个特殊标识，共享的数据保存到了服务器的内存对象中。每次请求时，会将特殊标识带到服务器端，根据标识来找到对应的内存空间，从而实现数据共享。简单说它就是一个服务端会话对象，用于存储用户的会话数据&lt;/p&gt;
&lt;p&gt;Session 域（会话域）对象是 Servlet 规范中四大域对象之一，并且它也是用于实现数据共享的&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;域对象&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;创建&lt;/th&gt;
&lt;th&gt;销毁&lt;/th&gt;
&lt;th&gt;使用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ServletContext&lt;/td&gt;
&lt;td&gt;应用域&lt;/td&gt;
&lt;td&gt;服务器启动&lt;/td&gt;
&lt;td&gt;服务器关闭&lt;/td&gt;
&lt;td&gt;在整个应用之间实现数据共享&amp;lt;br /&amp;gt;（记录网站访问次数，聊天室）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServletRequest&lt;/td&gt;
&lt;td&gt;请求域&lt;/td&gt;
&lt;td&gt;请求到来&lt;/td&gt;
&lt;td&gt;响应了这个请求&lt;/td&gt;
&lt;td&gt;在当前请求或者请求转发之间实现数据共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HttpSession&lt;/td&gt;
&lt;td&gt;会话域&lt;/td&gt;
&lt;td&gt;getSession()&lt;/td&gt;
&lt;td&gt;session过期，调用invalidate()，服务器关闭&lt;/td&gt;
&lt;td&gt;在当前会话范围中实现数据共享，可以在多次请求中实现数据共享。&amp;lt;br /&amp;gt;（验证码校验, 保存用户登录状态等）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;基本使用&lt;/h3&gt;
&lt;h4&gt;获取会话&lt;/h4&gt;
&lt;p&gt;HttpServletRequest类获取Session：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HttpSession getSession()&lt;/td&gt;
&lt;td&gt;获取HttpSession对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HttpSession getSession(boolean creat)&lt;/td&gt;
&lt;td&gt;获取HttpSession对象，未获取到是否自动创建&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Session获取的两个方法.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void setAttribute(String name, Object value)&lt;/td&gt;
&lt;td&gt;设置会话域中的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object getAttribute(String name)&lt;/td&gt;
&lt;td&gt;获取指定名称的会话域数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enumeration&amp;lt;String&amp;gt; getAttributeNames()&lt;/td&gt;
&lt;td&gt;获取所有会话域所有属性的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void removeAttribute(String name)&lt;/td&gt;
&lt;td&gt;移除会话域中指定名称的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getId()&lt;/td&gt;
&lt;td&gt;获取唯一标识名称，Jsessionid的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void invalidate()&lt;/td&gt;
&lt;td&gt;立即失效session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;实现会话&lt;/h4&gt;
&lt;p&gt;通过第一个Servlet设置共享的数据用户名，并在第二个Servlet获取到&lt;/p&gt;
&lt;p&gt;项目执行完以后，去浏览器抓包，Request Headers 中的 Cookie JSESSIONID的值是一样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo01&quot;)
public class ServletDemo01 extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取请求的用户名
        String username = req.getParameter(&quot;username&quot;);
        //2.获取HttpSession的对象
        HttpSession session = req.getSession();
        System.out.println(session);
        System.out.println(session.getId());
        //3.将用户名信息添加到共享数据中
        session.setAttribute(&quot;username&quot;,username);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo02&quot;)
public class ServletDemo02 extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取HttpSession对象
        HttpSession session = req.getSession();
        //2.获取共享数据
        Object username = session.getAttribute(&quot;username&quot;);
        //3.将数据响应给浏览器
        resp.getWriter().write(username+&quot;&quot;);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;p&gt;Session 的创建：一个常见的错误是以为 Session 在有客户端访问时就被创建，事实是直到某 server 端程序（如 Servlet）调用 &lt;code&gt;HttpServletRequest.getSession(true)&lt;/code&gt; 这样的语句时才会被创建&lt;/p&gt;
&lt;p&gt;Session 在以下情况会被删除：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序调用 HttpSession.invalidate()&lt;/li&gt;
&lt;li&gt;距离上一次收到客户端发送的 session id 时间间隔超过了 session 的最大有效时间&lt;/li&gt;
&lt;li&gt;服务器进程被停止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端只保存 sessionID 到 cookie 中，而不会保存 session&lt;/li&gt;
&lt;li&gt;关闭浏览器只会使存储在客户端浏览器内存中的 cookie 失效，不会使服务器端的 session 对象失效，同样也不会使已经保存到硬盘上的持久化cookie消失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开两个浏览器窗口访问应用程序会使用的是不同的session，通常 session cookie 是不能跨窗口使用，当新开了一个浏览器窗口进入相同页面时，系统会赋予一个新的 session id，实现跨窗口信息共享：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先把 session id 保存在 persistent cookie 中（通过设置session的最大有效时间）&lt;/li&gt;
&lt;li&gt;在新窗口中读出来，就可以得到上一个窗口的 session id，这样通过 session cookie 和 persistent cookie 的结合就可以实现跨窗口的会话跟踪&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;会话问题&lt;/h3&gt;
&lt;h4&gt;禁用Cookie&lt;/h4&gt;
&lt;p&gt;浏览器禁用Cookie解决办法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：通过提示信息告知用户&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo03&quot;)
public class ServletDemo03 extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取HttpSession对象
        HttpSession session = req.getSession(false);
        System.out.println(session);
        if(session == null) {
            resp.setContentType(&quot;text/html;charset=UTF-8&quot;);
            resp.getWriter().write(&quot;为了不影响正常的使用，请不要禁用浏览器的Cookie~&quot;);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：访问时拼接 jsessionid 标识，通过 encodeURL() 方法&lt;strong&gt;重写地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    HttpSession session = req.getSession();
    //实现url重写  相当于在地址栏后面拼接了一个jsessionid
    resp.getWriter().write(&quot;&amp;lt;a href=&apos;&quot;+ resp.encodeURL
                           (&quot;http://localhost:8080/session/servletDemo03&quot;) +
                           &quot;&apos;&amp;gt;go servletDemo03&amp;lt;/a&amp;gt;&quot;);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;钝化活化&lt;/h4&gt;
&lt;p&gt;Session 存放在服务器端的内存中，可以做持久化管理。&lt;/p&gt;
&lt;p&gt;钝化：序列化，持久态。把长时间不用，但还不到过期时间的 HttpSession 进行序列化写到磁盘上。&lt;/p&gt;
&lt;p&gt;活化：相反的状态&lt;/p&gt;
&lt;p&gt;何时钝化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当访问量很大时，服务器会根据getLastAccessTime来进行排序，对长时间不用，但是还没到过期时间的HttpSession进行序列化（持久化）&lt;/li&gt;
&lt;li&gt;当服务器进行重启的时候，为了保持客户HttpSession中的数据，也要对HttpSession进行序列化（持久化）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HttpSession的持久化由服务器来负责管理，我们不用关心&lt;/li&gt;
&lt;li&gt;只有实现了序列化接口的类才能被序列化&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;JSP&lt;/h2&gt;
&lt;h3&gt;JSP概述&lt;/h3&gt;
&lt;p&gt;JSP(Java Server Page)：是一种动态网页技术标准。（页面技术）&lt;/p&gt;
&lt;p&gt;JSP是基于Java语言的，它的本质就是Servlet，一个特殊的Servlet。&lt;/p&gt;
&lt;p&gt;JSP部署在服务器上，可以处理客户端发送的请求，并根据请求内容动态的生成HTML、XML或其他格式文档的Web网页，然后响应给客户端。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;开发静态资源，不能包含java代码，无法添加动态数据。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;美化页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;给网页添加动态效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servlet&lt;/td&gt;
&lt;td&gt;编写java代码，实现后台功能处理，但是很不方便，开发效率低。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSP&lt;/td&gt;
&lt;td&gt;包括了显示页面技术，同时具备Servlet输出动态资源的能力。但是不适合作为控制器来用。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;执行原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建JavaEE工程，编写index.jsp文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;JSP的入门&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
      这是第一个JSP页面
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行过程：&lt;/p&gt;
&lt;p&gt;客户端提交请求——Tomcat服务器解析请求地址——找到JSP页面——Tomcat将JSP页面翻译成Servlet的java文件——将翻译好的.java文件编译成.class文件——返回到客户浏览器上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSP%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;溯源，打开JSP翻译后的Java文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase&lt;/code&gt;，&lt;code&gt;public abstract class HttpJspBase extends HttpServlet implements HttpJspPage&lt;/code&gt;，HttpJspBase是个抽象类继承HttpServlet，所以JSP本质上继承HttpServlet&lt;/p&gt;
&lt;p&gt;在文件中找到了输出页面的代码，本质都是用out.write()输出的JSP语句&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Jsp%E7%9A%84%E6%9C%AC%E8%B4%A8%E8%AF%B4%E6%98%8E.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总结：
JSP它是一个特殊的Servlet，主要是用于展示动态数据。它展示的方式是用流把数据输出出来，而我们在使用JSP时，涉及HTML的部分，都与HTML的用法一致，这部分称为jsp中的模板元素，决定了页面的外观。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;JSP语法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSP注释：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注释类型&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSP注释&lt;/td&gt;
&lt;td&gt;&amp;lt;%--注释内容--%&amp;gt;&lt;/td&gt;
&lt;td&gt;被jsp注释的部分不会被翻译成.java文件，不会在浏览器上显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML注释&lt;/td&gt;
&lt;td&gt;&amp;lt;!--HTML注释--&amp;gt;&lt;/td&gt;
&lt;td&gt;在Jsp中可以使用html的注释，但是只能注释html元素&amp;lt;br /&amp;gt;被html注释部分会参与翻译，并且会在浏览器上显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java注释&lt;/td&gt;
&lt;td&gt;//; /* */&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java代码块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;% 此处写java代码 %&amp;gt;
&amp;lt;%--由tomcat负责翻译，翻译之后是service方法的成员变量--%&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JSP表达式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%=表达式%&amp;gt;
&amp;lt;%--翻译成Service()方法里面的内容,相当于调用out.print()--%&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JSP声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%! 声明的变量或方法 %&amp;gt;
&amp;lt;%--翻译成Servlet类里面的内容--%&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语法示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;jsp语法&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--1. 这是注释--%&amp;gt;

    &amp;lt;%--
        2.java代码块
        System.out.println(&quot;Hello JSP&quot;); 普通输出语句，输出在控制台!!
        out.println(&quot;Hello JSP&quot;);out是JspWriter对象，输出在页面上
    --%&amp;gt;
    &amp;lt;%
        System.out.println(&quot;Hello JSP&quot;);
        out.println(&quot;Hello JSP&amp;lt;br&amp;gt;&quot;);
        String str = &quot;hello&amp;lt;br&amp;gt;&quot;;
        out.println(str);
    %&amp;gt;

    &amp;lt;%--
        3.jsp表达式,相当于 out.println(&quot;Hello&quot;);
    --%&amp;gt;
    &amp;lt;%=&quot;Hello&amp;lt;br&amp;gt;&quot;%&amp;gt;

    &amp;lt;%--
        4.jsp中的声明(变量或方法)
        如果加!  代表的是声明的是成员变量
        如果不加!  代表的是声明的是局部变量,页面显示abc
    --%&amp;gt;
    &amp;lt;%! String s = &quot;abc&quot;;%&amp;gt;
    &amp;lt;% String s = &quot;def&quot;;%&amp;gt;
    &amp;lt;%=s%&amp;gt;
    
    &amp;lt;%! public void getSum(){}%&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;控制台输出：Hello JSP
页面输出：
	Hello JSP
	hello
	Hello
	def
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;JSP指令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;page指令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page  属性名=属性值 属性名=属性值... %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;contentType&lt;/td&gt;
&lt;td&gt;设置响应正文支持的MIME类型和编码格式：contentType=&quot;text/html;charset=UTF-8&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;language&lt;/td&gt;
&lt;td&gt;告知引擎，脚本使用的语言，默认为Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;errorPage&lt;/td&gt;
&lt;td&gt;当前页面出现异常后跳转的页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isErrorPage&lt;/td&gt;
&lt;td&gt;是否抓住异常。值为true页面中就能使用exception对象，打印异常信息。默认值false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;import&lt;/td&gt;
&lt;td&gt;导入哪些包（类）&amp;lt;%@ page import=&quot;java.util.ArrayList&quot; %&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;是否创建HttpSession对象，默认是true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buffer&lt;/td&gt;
&lt;td&gt;设定JspWriter用s输出jsp内容的缓存大小。默认8kb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pageEncoding&lt;/td&gt;
&lt;td&gt;翻译jsp时所用的编码格式，pageEncoding=&quot;UTF-8&quot;相当于用UTF-8读取JSP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isELIgnored&lt;/td&gt;
&lt;td&gt;是否忽略EL表达式，默认值是false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note：当使用全局错误页面，就无须配置errorPage实现跳转错误页面，而是由服务器负责跳转到错误页面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置全局错误页面：web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;error-page&amp;gt;    
    &amp;lt;exception-type&amp;gt;java.lang.Exception&amp;lt;/exception-type&amp;gt;    			
    &amp;lt;location&amp;gt;/error.jsp&amp;lt;/location&amp;gt;
&amp;lt;/error-page&amp;gt;
&amp;lt;error-page&amp;gt;
    &amp;lt;error-code&amp;gt;404&amp;lt;/error-code&amp;gt;
    &amp;lt;location&amp;gt;/404.html&amp;lt;/location&amp;gt;
&amp;lt;/error-page&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**include指令：**包含其他页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@include file=&quot;被包含的页面&quot; %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：file，以/开头，就代表当前应用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**taglib指令：**引入外部标签库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%taglib uri=&quot;标签库的地址&quot; prefix=&quot;前缀名称&quot;%&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;html标签和jsp标签不用引入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;隐式对象&lt;/h3&gt;
&lt;h4&gt;九大隐式对象&lt;/h4&gt;
&lt;p&gt;隐式对象：在jsp中可以不声明就直接使用的对象。它只存在于jsp中，因为java类中的变量必须要先声明再使用。
jsp中的隐式对象也并不是未声明，它是在翻译成.java文件时声明的，所以我们在jsp中可以直接使用。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隐式对象名称&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;request&lt;/td&gt;
&lt;td&gt;javax.servlet.http.HttpServletRequest&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;response&lt;/td&gt;
&lt;td&gt;javax.servlet.http.HttpServletResponse&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;javax.servlet.http.HttpSession&lt;/td&gt;
&lt;td&gt;Page指令可以控制开关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;application&lt;/td&gt;
&lt;td&gt;javax.servlet.ServletContext&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;Java.lang.Object&lt;/td&gt;
&lt;td&gt;当前jsp对应的servlet引用实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;config&lt;/td&gt;
&lt;td&gt;javax.servlet.ServletConfig&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exception&lt;/td&gt;
&lt;td&gt;java.lang.Throwable&lt;/td&gt;
&lt;td&gt;page指令有开关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;out&lt;/td&gt;
&lt;td&gt;javax.servlet.jsp.JspWriter&lt;/td&gt;
&lt;td&gt;字符输出流，相当于printwriter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pageContext&lt;/td&gt;
&lt;td&gt;javax.servlet.jsp.PageContext&lt;/td&gt;
&lt;td&gt;很重要，页面域&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;PageContext&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PageContext对象特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PageContextd对象是JSP独有的对象，Servlet中没有&lt;/li&gt;
&lt;li&gt;PageContextd对象是一个&lt;strong&gt;页面域（作用范围）对象&lt;/strong&gt;，还可以操作其他三个域对象中的属性&lt;/li&gt;
&lt;li&gt;PageContextd对象&lt;strong&gt;可以获取其他八个隐式对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;PageContextd对象是一个局部变量，它的生命周期随着JSP的创建而诞生，随着JSP的结束而消失。每个JSP页面都有一个独立的PageContext&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PageContext方法如下，页面域操作的方法定义在了PageContext的父类JspContext中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/PageContext%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;四大域对象&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;域对象名称&lt;/th&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;级别&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PageContext&lt;/td&gt;
&lt;td&gt;页面范围&lt;/td&gt;
&lt;td&gt;最小，只能在当前页面用&lt;/td&gt;
&lt;td&gt;因范围太小，开发中用的很少&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServletRequest&lt;/td&gt;
&lt;td&gt;请求范围&lt;/td&gt;
&lt;td&gt;一次请求或当期请求转发用&lt;/td&gt;
&lt;td&gt;当请求转发之后，再次转发时请求域丢失&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HttpSession&lt;/td&gt;
&lt;td&gt;会话范围&lt;/td&gt;
&lt;td&gt;多次请求数据共享时使用&lt;/td&gt;
&lt;td&gt;多次请求共享数据，但不同的客户端不能共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServletContext&lt;/td&gt;
&lt;td&gt;应用范围&lt;/td&gt;
&lt;td&gt;最大，整个应用都可以使用&lt;/td&gt;
&lt;td&gt;尽量少用，如果对数据有修改需要做同步处理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;MVC模型&lt;/h3&gt;
&lt;p&gt;M : model， 通常用于封装数据，封装的是数据模型
V :  view，通常用于展示数据。动态展示用jsp页面，静态数据展示用html
C :  controller，通常用于处理请求和响应，一般指的是Servlet&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/MVC%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;EL&lt;/h2&gt;
&lt;h3&gt;EL概述&lt;/h3&gt;
&lt;p&gt;EL表达式：Expression Language，意为表达式语言。它是Servlet规范中的一部分，是JSP2.0规范加入的内容。&lt;/p&gt;
&lt;p&gt;EL表达式作用：在JSP页面中获取数据，让JSP脱离java代码块和JSP表达式&lt;/p&gt;
&lt;p&gt;EL表达式格式： &lt;code&gt;${表达式内容}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;EL表达式特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有明确的&lt;strong&gt;返回值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;把内容输出到&lt;strong&gt;页面&lt;/strong&gt;上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只能在四大域对象中获取数据&lt;/strong&gt;，不在四大域对象中的数据取不到。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;EL用法&lt;/h3&gt;
&lt;h4&gt;多种类型&lt;/h4&gt;
&lt;p&gt;EL表达式可以获取不同类型数据，前提是数据放入四大域对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page import=&quot;bean.Student&quot; %&amp;gt;
&amp;lt;%@ page import=&quot;java.util.ArrayList&quot; %&amp;gt;
&amp;lt;%@ page import=&quot;java.util.HashMap&quot; %&amp;gt;
&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式获取不同类型数据&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--1.获取基本数据类型--%&amp;gt;
    &amp;lt;% pageContext.setAttribute(&quot;num&quot;,10); %&amp;gt;
    基本数据类型：${num} &amp;lt;br&amp;gt;

    &amp;lt;%--2.获取自定义对象类型--%&amp;gt;
    &amp;lt;%
        Student stu = new Student(&quot;张三&quot;,23);
        pageContext.setAttribute(&quot;stu&quot;,stu);
    %&amp;gt;
    自定义对象：${stu} &amp;lt;br&amp;gt;
    &amp;lt;%--stu.name 实现原理 getName()--%&amp;gt;
    学生姓名：${stu.name} &amp;lt;br&amp;gt;
    学生年龄：${stu.age} &amp;lt;br&amp;gt;

    &amp;lt;%--3.获取数组类型--%&amp;gt;
    &amp;lt;%
        String[] arr = {&quot;hello&quot;,&quot;world&quot;};
        pageContext.setAttribute(&quot;arr&quot;,arr);
    %&amp;gt;
    数组：${arr}  &amp;lt;br&amp;gt;
    0索引元素：${arr[0]} &amp;lt;br&amp;gt;
    1索引元素：${arr[1]} &amp;lt;br&amp;gt;

    &amp;lt;%--4.获取List集合--%&amp;gt;
    &amp;lt;%
        ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;aaa&quot;);
        list.add(&quot;bbb&quot;);
        pageContext.setAttribute(&quot;list&quot;,list);
    %&amp;gt;
    List集合：${list} &amp;lt;br&amp;gt;
    0索引元素：${list[0]} &amp;lt;br&amp;gt;

    &amp;lt;%--5.获取Map集合--%&amp;gt;
    &amp;lt;%
        HashMap&amp;lt;String,Student&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;hm01&quot;,new Student(&quot;张三&quot;,23));
        map.put(&quot;hm02&quot;,new Student(&quot;李四&quot;,24));
        pageContext.setAttribute(&quot;map&quot;,map);
    %&amp;gt;
    Map集合：${map}  &amp;lt;br&amp;gt;
    第一个学生对象：${map.hm01}  &amp;lt;br&amp;gt;
    第一个学生对象的姓名：${map.hm01.name}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;--页面输出效果
基本数据类型：10
自定义对象：bean.Student@5f8da92c   (地址)
学生姓名：张三
学生年龄：23
数组：[Ljava.lang.String;@4b3bd520
0索引元素：hello
1索引元素：world
List集合：[aaa, bbb]
0索引元素：aaa
Map集合：{hm01=bean.Student@4768d250, hm02=bean.Student@67f237d9}
第一个学生对象：bean.Student@4768d250
第一个学生对象的姓名：张三
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;异常问题&lt;/h4&gt;
&lt;p&gt;EL表达式的注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EL表达式没有空指针异常&lt;/li&gt;
&lt;li&gt;EL表达式没有数组下标越界&lt;/li&gt;
&lt;li&gt;EL表达式没有字符串拼接&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式的注意事项&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    第一个：没有空指针异常&amp;lt;br/&amp;gt;
    &amp;lt;% String str = null;
        request.setAttribute(&quot;testNull&quot;,str);
    %&amp;gt;
    str：${testNull}
    &amp;lt;hr/&amp;gt;
    第二个：没有数组下标越界&amp;lt;br/&amp;gt;
    &amp;lt;% String[] strs = new String[]{&quot;a&quot;,&quot;b&quot;,&quot;c&quot;};
        request.setAttribute(&quot;strs&quot;,strs);
    %&amp;gt;
    取第一个元素：${strs[0]}&amp;lt;br/&amp;gt;
    取第六个元素：${strs[5]}&amp;lt;br/&amp;gt;
    &amp;lt;hr/&amp;gt;
    第三个：没有字符串拼接&amp;lt;br/&amp;gt;
    &amp;lt;%--${strs[0]+strs[1]}--%&amp;gt;
    拼接：${strs[0]}+${strs[1]} &amp;lt;%--注意拼接--%&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;--页面输出效果
第一个：没有空指针异常
str：
第二个：没有数组下标越界
取第一个元素：a
取第六个元素：
第三个：没有字符串拼接
拼接：a+b
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;运算符&lt;/h4&gt;
&lt;p&gt;EL表达式中运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;关系运算符：&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%85%B3%E7%B3%BB%E8%BF%90%E7%AE%97%E7%AC%A6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑运算符：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;逻辑运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&amp;amp; 或 and&lt;/td&gt;
&lt;td&gt;交集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|| 或 or&lt;/td&gt;
&lt;td&gt;并集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;! 或 not&lt;/td&gt;
&lt;td&gt;非&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他运算符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;1. 判断对象是否为null&amp;lt;br /&amp;gt;2. 判断字符串是否为空字符串&amp;lt;br /&amp;gt;3. 判断容器元素是否为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;条件 ? 表达式1 : 表达式2&lt;/td&gt;
&lt;td&gt;三元运算符，条件?真:假&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式运算符&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--empty--%&amp;gt;
    &amp;lt;%
        String str1 = null;
        String str2 = &quot;&quot;;
        int[] arr = {};
    %&amp;gt;
    ${empty str1} &amp;lt;br&amp;gt;
    ${empty str2} &amp;lt;br&amp;gt;
    ${empty arr} &amp;lt;br&amp;gt;

    &amp;lt;%--三元运算符。获取性别的数据，在对应的按钮上进行勾选--%&amp;gt;
    &amp;lt;% pageContext.setAttribute(&quot;gender&quot;,&quot;women&quot;); %&amp;gt;
    &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;men&quot; ${gender==&quot;men&quot;?&quot;checked&quot;:&quot;&quot;}&amp;gt;男
    &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;women&quot; ${gender==&quot;women&quot;?&quot;checked&quot;:&quot;&quot;}&amp;gt;女
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%BF%90%E7%AE%97%E7%AC%A6%E6%95%88%E6%9E%9C%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;四大域数据&lt;/h4&gt;
&lt;p&gt;EL表达式只能从从四大域中获取数据，调用的就是&lt;code&gt;findAttribute(name,value);&lt;/code&gt;方法，根据名称由小到大在域对象中查找，找到就返回，找不到就什么都不显示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式使用细节&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--获取四大域对象中的数据--%&amp;gt;
    &amp;lt;%
        //pageContext.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);
        request.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);
        //session.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);
        //application.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);
    %&amp;gt;
    ${username} &amp;lt;br&amp;gt;

    &amp;lt;%--获取JSP中其他八个隐式对象  获取虚拟目录名称--%&amp;gt;
    &amp;lt;%= request.getContextPath()%&amp;gt;
    ${pageContext.request.contextPath}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;EL隐式对象&lt;/h3&gt;
&lt;h4&gt;EL表达式隐式对象&lt;/h4&gt;
&lt;p&gt;EL表达式也为我们提供隐式对象，可以让我们不声明直接来使用，需要注意的是，它和JSP的隐式对象不是同一种事物。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;EL中的隐式对象&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;对应JSP隐式对象&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PageContext&lt;/td&gt;
&lt;td&gt;Javax.serlvet.jsp.PageContext&lt;/td&gt;
&lt;td&gt;PageContext&lt;/td&gt;
&lt;td&gt;完全一样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ApplicationScope&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;应用层范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SessionScope&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;会话范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RequestScope&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;请求范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageScope&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;页面层范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;请求消息头key，值是value（一个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HeaderValues&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;请求消息头key，值是数组（一个头多个值）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Param&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;请求参数key，值是value（一个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ParamValues&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;请求参数key，值是数组（一个名称多个值）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InitParam&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;全局参数，key是参数名称，value是参数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;Java.util.Map&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;Key是cookie的名称，value是cookie对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式11个隐式对象&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--pageContext对象 可以获取其他三个域对象和JSP中八个隐式对象--%&amp;gt;
    ${pageContext.request.contextPath} &amp;lt;br&amp;gt;

    &amp;lt;%--applicationScope sessionScope requestScope pageScope 操作四大域对象中的数据--%&amp;gt;
    &amp;lt;% request.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;); %&amp;gt;
    ${username} &amp;lt;br&amp;gt;
    ${requestScope.username} &amp;lt;br&amp;gt;

    &amp;lt;%--header headerValues  获取请求头数据--%&amp;gt;
    ${header[&quot;connection&quot;]} &amp;lt;br&amp;gt;
    ${headerValues[&quot;connection&quot;][0]} &amp;lt;br&amp;gt;

    &amp;lt;%--param paramValues 获取请求参数数据--%&amp;gt;
    ${param.username} &amp;lt;br&amp;gt;
    ${paramValues.hobby[0]} &amp;lt;br&amp;gt;
    ${paramValues.hobby[1]} &amp;lt;br&amp;gt;

    &amp;lt;%--initParam 获取全局配置参数--%&amp;gt;
    ${initParam[&quot;pname&quot;]}  &amp;lt;br&amp;gt;

    &amp;lt;%--cookie 获取cookie信息--%&amp;gt;
    ${cookie}  &amp;lt;br&amp;gt; &amp;lt;%--获取Map集合--%&amp;gt;
    ${cookie.JSESSIONID}  &amp;lt;br&amp;gt; &amp;lt;%--获取map集合中第二个元素--%&amp;gt;
    ${cookie.JSESSIONID.name}  &amp;lt;br&amp;gt; &amp;lt;%--获取cookie对象的名称--%&amp;gt;
    ${cookie.JSESSIONID.value} &amp;lt;%--获取cookie对象的值--%&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&amp;lt;--页面显示
/el
zhangsan
zhangsan
keep-alive
keep-alive

bbb
{JSESSIONID=javax.servlet.http.Cookie@435c8431, Idea-5a5d203e=javax.servlet.http.Cookie@46be0b58, Idea-be3279e7=javax.servlet.http.Cookie@4ef6e8e8}
javax.servlet.http.Cookie@435c8431
JSESSIONID
E481B2A845A448AD88A71FD43611FF02    
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在web.xml配置全局参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;web-app ******&amp;gt;
    &amp;lt;!--配置全局参数--&amp;gt;
    &amp;lt;context-param&amp;gt;
        &amp;lt;param-name&amp;gt;pname&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;bbb&amp;lt;/param-value&amp;gt;
    &amp;lt;/context-param&amp;gt;
&amp;lt;/web-app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取JSP隐式对象&lt;/h4&gt;
&lt;p&gt;通过获取页面域对象，获取其他JSP八个隐式对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;EL表达式使用细节&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--获取虚拟目录名称--%&amp;gt;
    &amp;lt;%= request.getContextPath()%&amp;gt;
    ${pageContext.request.contextPath}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&amp;lt;--页面显示
/el /el
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;JSTL&lt;/h3&gt;
&lt;p&gt;JSTL：Java Server Pages Standarded Tag Library，JSP中标准标签库。&lt;/p&gt;
&lt;p&gt;作用：提供给开发人员一个标准的标签库，开发人员可以利用这些标签取代JSP页面上的Java代码，从而提高程序的可读性，降低程序的维护难度。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组成&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;核心标签库&lt;/td&gt;
&lt;td&gt;通用逻辑处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fmt&lt;/td&gt;
&lt;td&gt;国际化有关&lt;/td&gt;
&lt;td&gt;需要不同地域显示不同语言时使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Functions&lt;/td&gt;
&lt;td&gt;EL函数&lt;/td&gt;
&lt;td&gt;EL表达式可以使用的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;操作数据库&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML&lt;/td&gt;
&lt;td&gt;操作XML&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;使用：添加jar包，通过taglib导入，prefix属性表示程序调用标签使用的引用名&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签名称&lt;/th&gt;
&lt;th&gt;功能分类&lt;/th&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;`&amp;lt;c:if test=&quot;${A==B&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;C==D}&quot;&amp;gt;`&lt;/td&gt;
&lt;td&gt;流程控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;c:choose&amp;gt; ,&amp;lt;c:when&amp;gt;,&amp;lt;c:otherwise&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;流程控制&lt;/td&gt;
&lt;td&gt;核心标签库&lt;/td&gt;
&lt;td&gt;用于多个条件判断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;c:foreache&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;迭代操作&lt;/td&gt;
&lt;td&gt;核心标签库&lt;/td&gt;
&lt;td&gt;用于循环遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;流程控制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;%@taglib uri=&quot;http://java.sun.com/jsp/jstl/core&quot; prefix=&quot;c&quot;%&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;流程控制&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--向域对象中添加成绩数据--%&amp;gt;
    ${pageContext.setAttribute(&quot;score&quot;,&quot;T&quot;)}

    &amp;lt;%--对成绩进行判断--%&amp;gt;
    &amp;lt;c:if test=&quot;${score eq &apos;A&apos;}&quot;&amp;gt;
        优秀
    &amp;lt;/c:if&amp;gt;

    &amp;lt;%--对成绩进行多条件判断--%&amp;gt;
    &amp;lt;c:choose&amp;gt;
        &amp;lt;c:when test=&quot;${score eq &apos;A&apos;}&quot;&amp;gt;优秀&amp;lt;/c:when&amp;gt;
        &amp;lt;c:when test=&quot;${score eq &apos;B&apos;}&quot;&amp;gt;良好&amp;lt;/c:when&amp;gt;
        &amp;lt;c:when test=&quot;${score eq &apos;C&apos;}&quot;&amp;gt;及格&amp;lt;/c:when&amp;gt;
        &amp;lt;c:when test=&quot;${score eq &apos;D&apos;}&quot;&amp;gt;较差&amp;lt;/c:when&amp;gt;
        &amp;lt;c:otherwise&amp;gt;成绩非法&amp;lt;/c:otherwise&amp;gt;
    &amp;lt;/c:choose&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;迭代操作
c:forEach：用来遍历集合，属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;items&lt;/td&gt;
&lt;td&gt;指定要遍历的集合，它可以是用EL表达式取出来的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;var&lt;/td&gt;
&lt;td&gt;把当前遍历的元素放入指定的page域中。var的值是key，遍历的元素是value&amp;lt;br /&amp;gt;注意：var不支持EL表达式，只能是字符串常量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;begin&lt;/td&gt;
&lt;td&gt;开始遍历的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;end&lt;/td&gt;
&lt;td&gt;结束遍历的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;step&lt;/td&gt;
&lt;td&gt;步长，i+=step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;varStatus&lt;/td&gt;
&lt;td&gt;它是一个计数器对象，有两个属性，一个是用于记录索引，一个是用于计数。索引是从0开始，计数是从1开始&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page import=&quot;java.util.ArrayList&quot; %&amp;gt;
&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;%@taglib uri=&quot;http://java.sun.com/jsp/jstl/core&quot; prefix=&quot;c&quot;%&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;循环&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;%--向域对象中添加集合--%&amp;gt;
    &amp;lt;%
        ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;aa&quot;);
        list.add(&quot;bb&quot;);
        list.add(&quot;cc&quot;);
        list.add(&quot;dd&quot;);
        pageContext.setAttribute(&quot;list&quot;,list);
    %&amp;gt;
    &amp;lt;%--遍历集合--%&amp;gt;
    &amp;lt;c:forEach items=&quot;${list}&quot; var=&quot;str&quot;&amp;gt;
        ${str} &amp;lt;br&amp;gt;
    &amp;lt;/c:forEach&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Filter&lt;/h2&gt;
&lt;h3&gt;过滤器&lt;/h3&gt;
&lt;p&gt;Filter：过滤器，是 JavaWeb 三大组件之一，另外两个是 Servlet 和 Listener&lt;/p&gt;
&lt;p&gt;工作流程：在程序访问服务器资源时，当一个请求到来，服务器首先判断是否有过滤器与去请求资源相关联，如果有过滤器可以将请求拦截下来，完成一些特定的功能，再由过滤器决定是否交给请求资源，如果没有就直接请求资源，响应同理&lt;/p&gt;
&lt;p&gt;作用：过滤器一般用于完成通用的操作，例如：登录验证、统一编码处理、敏感字符过滤等&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;相关类&lt;/h3&gt;
&lt;h4&gt;Filter&lt;/h4&gt;
&lt;p&gt;Filter是一个接口，如果想实现过滤器的功能，必须实现该接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void init(FilterConfig filterConfig)&lt;/td&gt;
&lt;td&gt;初始化，开启过滤器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)&lt;/td&gt;
&lt;td&gt;对请求资源和响应资源过滤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void destroy()&lt;/td&gt;
&lt;td&gt;销毁过滤器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置方式&lt;/p&gt;
&lt;p&gt;注解方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(&quot;/*&quot;)
()内填拦截路径，/*代表全部路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo01&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo01&amp;lt;/filter-class&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo01&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;FilterChain&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;FilterChain 是一个接口，代表过滤器对象。由Servlet容器提供实现类对象，直接使用即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过滤器可以定义多个，就会组成过滤器链&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心方法：&lt;code&gt;void doFilter(ServletRequest request, ServletResponse response)&lt;/code&gt; 用来放行方法&lt;/p&gt;
&lt;p&gt;如果有多个过滤器，在第一个过滤器中调用下一个过滤器，以此类推，直到到达最终访问资源。
如果只有一个过滤器，放行时就会直接到达最终访问资源。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;FilterConfig&lt;/h4&gt;
&lt;p&gt;FilterConfig 是一个接口，代表过滤器的配置对象，可以加载一些初始化参数&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getFilterName()&lt;/td&gt;
&lt;td&gt;获取过滤器对象名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getInitParameter(String name)&lt;/td&gt;
&lt;td&gt;获取指定名称的初始化参数的值，不存在返回null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enumeration&amp;lt;String&amp;gt; getInitParameterNames()&lt;/td&gt;
&lt;td&gt;获取所有参数的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServletContext getServletContext()&lt;/td&gt;
&lt;td&gt;获取应用上下文对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;Filter使用&lt;/h3&gt;
&lt;h4&gt;设置页面编码&lt;/h4&gt;
&lt;p&gt;请求先被过滤器拦截进行相关操作&lt;/p&gt;
&lt;p&gt;过滤器放行之后执行完目标资源，仍会回到过滤器中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Filter 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(&quot;/*&quot;)
public class FilterDemo01 implements Filter{
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo01拦截到请求...&quot;);
        //处理乱码
        servletResponse.setContentType(&quot;text/html;charset=UTF-8&quot;);
        //过滤器放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println(&quot;filterDemo1放行之后，又回到了doFilter方法&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Servlet 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/servletDemo01&quot;)
public class ServletDemo01 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;servletDemo01执行了...&quot;);
        resp.getWriter().write(&quot;servletDemo01执行了...&quot;);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;filterDemo01拦截到请求...
servletDemo01执行了...
filterDemo1放行之后，又回到了doFilter方法  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;多过滤器顺序&lt;/h4&gt;
&lt;p&gt;多个过滤器使用的顺序，取决于过滤器映射的顺序。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;两个 Filter 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class FilterDemo01 implements Filter{
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo01执行了...&quot;);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}
public class FilterDemo02 implements Filter{
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo02执行了...&quot;);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Servlet代码：&lt;code&gt;System.out.println(&quot;servletDemo02执行了...&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;web.xml配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo01&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo01&amp;lt;/filter-class&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo01&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo02&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo02&amp;lt;/filter-class&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo02&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;filterDemo01执行了
filterDemo02执行了
servletDemo02执行了...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在过滤器的配置中，有过滤器的声明和过滤器的映射两部分，到底是声明决定顺序，还是映射决定顺序呢？&lt;/p&gt;
&lt;p&gt;答案是：&lt;code&gt;&amp;lt;filter-mapping&amp;gt;&lt;/code&gt;的配置前后顺序决定过滤器的调用顺序，也就是由映射配置顺序决定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Filter生命周期&lt;/h4&gt;
&lt;p&gt;**创建：**当应用加载时实例化对象并执行init()初始化方法&lt;/p&gt;
&lt;p&gt;**服务：**对象提供服务的过程，执行doFilter()方法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;销毁&lt;/strong&gt;：当应用卸载时或服务器停止时对象销毁，执行destroy()方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Filter代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(&quot;/*&quot;)
public class FilterDemo03 implements Filter{
    /*
        初始化方法
     */
    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println(&quot;对象初始化成功了...&quot;);
    }
    /*
        提供服务方法
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo03执行了...&quot;);
        //过滤器放行
        filterChain.doFilter(servletRequest,servletResponse);
    }
    /*
        对象销毁方法，关闭Tomcat服务器
     */
    @Override
    public void destroy() {
        System.out.println(&quot;对象销毁了...&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Servlet 代码：&lt;code&gt;System.out.println(&quot;servletDemo03执行了...&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;对象初始化成功了...
filterDemo03执行了...
servletDemo03执行了...
对象销毁了
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;FilterConfig使用&lt;/h4&gt;
&lt;p&gt;Filter初始化函数init的参数是FilterConfig 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Filter代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class FilterDemo04 implements Filter{

	//初始化方法
    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println(&quot;对象初始化成功了...&quot;);

        //获取过滤器名称
        String filterName = filterConfig.getFilterName();
        System.out.println(filterName);

        //根据name获取value
        String username = filterConfig.getInitParameter(&quot;username&quot;);
        System.out.println(username);
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo04执行了...&quot;);
        filterChain.doFilter(servletRequest,servletResponse);
    }
    @Override
    public void destroy() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;web.xml配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo04&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo04&amp;lt;/filter-class&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;username&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;zhangsan&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;filterDemo04&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;对象初始化成功了...
filterDemo04
zhangsan
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Filter案例&lt;/h3&gt;
&lt;p&gt;在访问html，js，image时，不需要每次都重新发送请求读取资源，就可以通过设置响应消息头的方式，设置缓存时间。但是如果每个Servlet都编写相同的代码，显然不符合我们统一调用和维护的理念。&lt;/p&gt;
&lt;p&gt;静态资源设置缓存时间：html设置为1小时，js设置为2小时，css设置为3小时&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置过滤器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;StaticResourceNeedCacheFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.StaticResourceNeedCacheFilter&amp;lt;/filter-class&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;html&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;3&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;js&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;4&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;css&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;5&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;StaticResourceNeedCacheFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;*.html&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;StaticResourceNeedCacheFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;*.js&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;StaticResourceNeedCacheFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;*.css&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写过滤器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class StaticResourceNeedCacheFilter implements Filter {
	private FilterConfig filterConfig;//获取初始化参数
    @Override
	public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        //1.把doFilter的请求和响应对象转换成跟http协议有关的对象
        HttpServletRequest  request;
        HttpServletResponse response;
        try {
            request = (HttpServletRequest) req;
            response = (HttpServletResponse) res;
        } catch (ClassCastException e) {
            throw new ServletException(&quot;non-HTTP request or response&quot;);
        }
        //2.获取请求资源URI
        String uri = request.getRequestURI();
        //3.得到请求资源到底是什么类型
        String extend = uri.substring(uri.lastIndexOf(&quot;.&quot;)+1);//我们只需要判断它是不是html,css,js。其他的不管
        //4.判断到底是什么类型的资源
        long time = 60*60*1000;
        if(&quot;html&quot;.equals(extend)){
            //html 缓存1小时
            String html = filterConfig.getInitParameter(&quot;html&quot;);
            time = time*Long.parseLong(html);
        }else if(&quot;js&quot;.equals(extend)){
            //js 缓存2小时
            String js = filterConfig.getInitParameter(&quot;js&quot;);
            time = time*Long.parseLong(js);
        }else if(&quot;css&quot;.equals(extend)){
            //css 缓存3小时
            String css = filterConfig.getInitParameter(&quot;css&quot;);
            time = time*Long.parseLong(css);

        }
        //5.设置响应消息头
        response.setDateHeader(&quot;Expires&quot;, System.currentTimeMillis()+time);
        //6.放行
        chain.doFilter(request, response);
    }
    
    @Override
    public void destroy() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;拦截行为&lt;/h3&gt;
&lt;p&gt;Filter过滤器默认拦截的是请求，但是在实际开发中，我们还有请求转发和请求包含，以及由服务器触发调用的全局错误页面。默认情况下过滤器是不参与过滤的，需要配置web.xml&lt;/p&gt;
&lt;p&gt;开启功能后，当访问页面发生相关行为后，会执行过滤器的操作&lt;/p&gt;
&lt;p&gt;五种拦截行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置过滤器--&amp;gt;
&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;FilterDemo5&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo5&amp;lt;/filter-class&amp;gt;
    &amp;lt;!--配置开启异步支持，当dispatcher配置ASYNC时，需要配置此行--&amp;gt;
    &amp;lt;async-supported&amp;gt;true&amp;lt;/async-supported&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;FilterDemo5&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/error.jsp&amp;lt;/url-pattern&amp;gt;
    &amp;lt;!--&amp;lt;url-pattern&amp;gt;/index.jsp&amp;lt;/url-pattern&amp;gt;--&amp;gt;
    &amp;lt;!--过滤请求：默认值。--&amp;gt;
    &amp;lt;dispatcher&amp;gt;REQUEST&amp;lt;/dispatcher&amp;gt;
    &amp;lt;!--过滤全局错误页面：开启后，当由服务器调用全局错误页面时，过滤器工作--&amp;gt;
    &amp;lt;dispatcher&amp;gt;ERROR&amp;lt;/dispatcher&amp;gt;
    &amp;lt;!--过滤请求转发：开启后，当请求转发时，过滤器工作。--&amp;gt;
    &amp;lt;dispatcher&amp;gt;FORWARD&amp;lt;/dispatcher&amp;gt;
    &amp;lt;!--过滤请求包含：当请求包含时，过滤器工作。它只能过滤动态包含，jsp的include指令是静态包含--&amp;gt;
    &amp;lt;dispatcher&amp;gt;INCLUDE&amp;lt;/dispatcher&amp;gt;
    &amp;lt;!--过滤异步类型，它要求我们在filter标签中配置开启异步支持--&amp;gt;
    &amp;lt;dispatcher&amp;gt;ASYNC&amp;lt;/dispatcher&amp;gt;
&amp;lt;/filter-mapping&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;web.xml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;FilterDemo5&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;filter.FilterDemo5&amp;lt;/filter-class&amp;gt;
    &amp;lt;!--配置开启异步支持，当dispatcher配置ASYNC时，需要配置此行--&amp;gt;
    &amp;lt;async-supported&amp;gt;true&amp;lt;/async-supported&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;FilterDemo5&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/error.jsp&amp;lt;/url-pattern&amp;gt;
    &amp;lt;dispatcher&amp;gt;ERROR&amp;lt;/dispatcher&amp;gt;
&amp;lt;filter-mapping&amp;gt;    
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServletDemo03：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;servletDemo03执行了...&quot;);
        int i = 1/ 0;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FilterDemo05：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class FilterDemo05 implements Filter{
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;filterDemo05执行了...&quot;);
        //放行
        filterChain.doFilter(servletRequest,servletResponse);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问URL：http://localhost:8080/filter/servletDemo03&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出（注意输出顺序）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;servletDemo03执行了...
filterDemo05执行了...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;对比Servlet&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法/类型&lt;/th&gt;
&lt;th&gt;Servlet&lt;/th&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初始化                                        方法&lt;/td&gt;
&lt;td&gt;void   init(ServletConfig);&lt;/td&gt;
&lt;td&gt;void init(FilterConfig);&lt;/td&gt;
&lt;td&gt;几乎一样，都是在web.xml中配置参数，用该对象的方法可以获取到。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提供服务方法&lt;/td&gt;
&lt;td&gt;void   service(request,response);&lt;/td&gt;
&lt;td&gt;void   dofilter(request,response,FilterChain)&lt;/td&gt;
&lt;td&gt;Filter比Servlet多了一个FilterChain，它不仅能完成Servlet的功能，而且还可以决定程序是否能继续执行。所以过滤器比Servlet更为强大。   在Struts2中，核心控制器就是一个过滤器。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;销毁方法&lt;/td&gt;
&lt;td&gt;void destroy();&lt;/td&gt;
&lt;td&gt;void destroy();&lt;/td&gt;
&lt;td&gt;方法/类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Listener&lt;/h2&gt;
&lt;h3&gt;观察者设计者&lt;/h3&gt;
&lt;p&gt;所有的监听器都是基于观察者设计模式的。&lt;/p&gt;
&lt;p&gt;观察者模式通常由以下三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事件源：触发事件的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件：触发的动作，里面封装了事件源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监听器：当事件源触发事件后，可以完成的功能。一般是一个接口，由使用者来实现。（此处的思想还涉及了一个策略模式）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;监听器分类&lt;/h3&gt;
&lt;p&gt;在程序当中，我们可以对：对象的创建销毁、域对象中属性的变化、会话相关内容进行监听。&lt;/p&gt;
&lt;p&gt;Servlet规范中共计8个监听器，&lt;strong&gt;监听器都是以接口形式提供&lt;/strong&gt;，具体功能需要我们自己完成&lt;/p&gt;
&lt;h4&gt;监听对象&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ServletContextListener：用于监听ServletContext对象的创建和销毁&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void contextInitialized(ServletContextEvent sce)&lt;/td&gt;
&lt;td&gt;对象创建时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void contextDestroyed(ServletContextEvent sce)&lt;/td&gt;
&lt;td&gt;对象销毁时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数ServletContextEvent 代表事件对象，事件对象中封装了事件源ServletContext，真正的事件指的是创建或者销毁ServletContext对象的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpSessionListener：用于监听HttpSession对象的创建和销毁&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void sessionCreated(HttpSessionEvent se)&lt;/td&gt;
&lt;td&gt;对象创建时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void sessionDestroyed(HttpSessionEvent se)&lt;/td&gt;
&lt;td&gt;对象销毁时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数HttpSessionEvent 代表事件对象，事件对象中封装了事件源HttpSession，真正的事件指的是创建或者销毁HttpSession对象的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServletRequestListener：用于监听ServletRequest对象的创建和销毁&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void requestInitialized(ServletRequestEvent sre)&lt;/td&gt;
&lt;td&gt;对象创建时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void requestDestroyed(ServletRequestEvent sre)&lt;/td&gt;
&lt;td&gt;对象销毁时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数ServletRequestEvent 代表事件对象，事件对象中封装了事件源ServletRequest，真正的事件指的是创建或者销毁ServletRequest对象的操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;监听域对象属性&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ServletContextAttributeListener：用于监听ServletContext应用域中属性的变化&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void attributeAdded(ServletContextAttributeEvent event)&lt;/td&gt;
&lt;td&gt;域中添加属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeRemoved(ServletContextAttributeEvent event)&lt;/td&gt;
&lt;td&gt;域中移除属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeReplaced(ServletContextAttributeEvent event)&lt;/td&gt;
&lt;td&gt;域中替换属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数ServletContextAttributeEvent 代表事件对象，事件对象中封装了事件源ServletContext，真正的事件指的是添加、移除、替换应用域中属性的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpSessionAttributeListener：用于监听HttpSession会话域中属性的变化&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void attributeAdded(HttpSessionBindingEvent event)&lt;/td&gt;
&lt;td&gt;域中添加属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeRemoved(HttpSessionBindingEvent event)&lt;/td&gt;
&lt;td&gt;域中移除属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeReplaced(HttpSessionBindingEvent event)&lt;/td&gt;
&lt;td&gt;域中替换属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数HttpSessionBindingEvent 代表事件对象，事件对象中封装了事件源HttpSession，真正的事件指的是添加、移除、替换应用域中属性的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServletRequestAttributeListener：用于监听ServletRequest请求域中属性的变化&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void attributeAdded(ServletRequestAttributeEvent srae)&lt;/td&gt;
&lt;td&gt;域中添加属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeRemoved(ServletRequestAttributeEvent srae)&lt;/td&gt;
&lt;td&gt;域中移除属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void attributeReplaced(ServletRequestAttributeEvent srae)&lt;/td&gt;
&lt;td&gt;域中替换属性时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数ServletRequestAttributeEvent 代表事件对象，事件对象中封装了事件源ServletRequest，真正的事件指的是添加、移除、替换应用域中属性的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面域对象没有监听器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;感知型监听器&lt;/h4&gt;
&lt;p&gt;监听会话相关的感知型监听器，和会话域相关的两个感知型监听器是无需配置（注解）的，可以直接编写代码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HttpSessionBindingListener：用于感知对象和会话域绑定的监听器&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void valueBound(HttpSessionBindingEvent event)&lt;/td&gt;
&lt;td&gt;数据添加到会话域中(绑定)时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void valueUnbound(HttpSessionBindingEvent event)&lt;/td&gt;
&lt;td&gt;数据从会话域中移除(解绑)时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;参数HttpSessionBindingEvent 代表事件对象，事件对象中封装了事件源HttpSession，真正的事件指的是添加、移除、替换应用域中属性的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpSessionActivationListener：用于感知会话域中对象和钝化和活化的监听器&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void sessionWillPassivate(HttpSessionEvent se)&lt;/td&gt;
&lt;td&gt;会话域中数据钝化时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void sessionDidActivate(HttpSessionEvent se)&lt;/td&gt;
&lt;td&gt;会话域中数据活化时执行该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;监听器使用&lt;/h3&gt;
&lt;h4&gt;ServletContextListener&lt;/h4&gt;
&lt;p&gt;ServletContext对象的创建和销毁的监听器&lt;/p&gt;
&lt;p&gt;注解方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebListener
public class ServletContextListenerDemo implements ServletContextListener {
    //创建时执行此方法
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println(&quot;监听到对象的创建....&quot;);//启动服务器就创建

        ServletContext servletContext = sce.getServletContext();
        System.out.println(servletContext);
    }
    //销毁时执行的方法
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println(&quot;监听到对象的销毁...&quot;);//关闭服务器就销毁
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;web-app&amp;gt;
&amp;lt;!--配置监听器--&amp;gt;
    &amp;lt;listener&amp;gt;
        &amp;lt;listener-class&amp;gt;listener.ServletContextAttributeListenerDemo&amp;lt;/listener-class&amp;gt;
    &amp;lt;/listener&amp;gt;
&amp;lt;/web-app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;ServletContextAttributeListener&lt;/h4&gt;
&lt;p&gt;应用域对象中的属性变化的监听器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServletContextAttributeListenerDemo implements ServletContextAttributeListener{
    /*
        向应用域对象中添加属性时执行此方法
     */
    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        System.out.println(&quot;监听到了属性的添加...&quot;);

        //获取应用域对象
        ServletContext servletContext = scae.getServletContext();
        //获取属性
        Object value = servletContext.getAttribute(&quot;username&quot;);
        System.out.println(value);//zhangsan 
    }

    /*
        向应用域对象中替换属性时执行此方法
     */
    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        System.out.println(&quot;监听到了属性的替换...&quot;);

        //获取应用域对象
        ServletContext servletContext = scae.getServletContext();
        //获取属性
        Object value = servletContext.getAttribute(&quot;username&quot;);
        System.out.println(value);//lisi
    }

    /*
        向应用域对象中移除属性时执行此方法
     */
    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        System.out.println(&quot;监听到了属性的移除...&quot;);

        //获取应用域对象
        ServletContext servletContext = scae.getServletContext();
        //获取属性
        Object value = servletContext.getAttribute(&quot;username&quot;);
        System.out.println(value);//null
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ServletContextListenerDemo implements ServletContextListener{
    //ServletContext对象创建的时候执行此方法
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println(&quot;监听到了对象的创建...&quot;);
        //获取对象
        ServletContext servletContext = sce.getServletContext();

        //添加属性
        servletContext.setAttribute(&quot;username&quot;,&quot;zhangsan&quot;);

        //替换属性
        servletContext.setAttribute(&quot;username&quot;,&quot;lisi&quot;);

        //移除属性
        servletContext.removeAttribute(&quot;username&quot;);
    }

    //ServletContext对象销毁的时候执行此方法
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println(&quot;监听到了对象的销毁...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制台输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;监听到了对象的创建...
监听到了属性的添加...
zhangsan
监听到了属性的替换
lisi
监听到属性的移除
null
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;JS&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;JavaScript 是一种客户端脚本语言。运行在客户端浏览器中，每一个浏览器都具备解析 JavaScript 的引擎。&lt;/p&gt;
&lt;p&gt;脚本语言：不需要编译，就可以被浏览器直接解析执行了。&lt;/p&gt;
&lt;p&gt;作用：增强用户和 HTML 页面的交互过程，让页面产生动态效果，增强用户的体验。&lt;/p&gt;
&lt;p&gt;组成部分：ECMAScript、DOM、BOM&lt;/p&gt;
&lt;p&gt;开发环境搭建：安装Node.js，是JavaScript运行环境&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;h3&gt;引入&lt;/h3&gt;
&lt;p&gt;引入HTML文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内部方式：&amp;lt;script&amp;gt;标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;JS快速入门&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!--html语句--&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    // JS语句
&amp;lt;/script&amp;gt;    
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;外部方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建js文件：my.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alert(&quot;Hello&quot;);//js语句
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在html中引用外部js文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;!--html语句--&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/my.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单行注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注释的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多行注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
注释的内容
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;输入输出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输入框：prompt(“提示内容”);&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弹出警告框：alert(“提示内容”);&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制台输出：console.log(“显示内容”);&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面内容输出：document.write(“显示内容”);&lt;/p&gt;
&lt;p&gt;注：&lt;code&gt;document.write(&quot;&amp;lt;br/&amp;gt;&quot;)&lt;/code&gt;换行，通常输出数据后跟br标签&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;变量常量&lt;/h3&gt;
&lt;p&gt;JavaScript 属于弱类型的语言，定义变量时不区分具体的数据类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义局部变量：let 变量名 = 值;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let name = &quot;张三&quot;;
let age = 23;
document.write(name + &quot;,&quot; + age +&quot;&amp;lt;br&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义全局变量：变量名 = 值;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    l2 = &quot;bb&quot;;
}
document.write(l2 + &quot;&amp;lt;br&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义常量：const 常量名 = 值;
常量不能被重新赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const PI = 3.1415926;
//PI = 3.15;  
document.write(PI);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;数据类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;布尔类型，true或false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;声明null值的特殊关键字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;td&gt;代表变量未定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;整数或浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bigint&lt;/td&gt;
&lt;td&gt;大整数，例如：let num = 10n;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;typeof 用于判断变量的数据类型&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let age = 18; 
document.write(typeof(age)); // number
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;运算符&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;算术运算符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算术运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;加法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;减法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;乘法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;除法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%&lt;/td&gt;
&lt;td&gt;取余数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;自增&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;自减&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值运算符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;赋值运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;加法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+=&lt;/td&gt;
&lt;td&gt;减法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-=&lt;/td&gt;
&lt;td&gt;乘法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*=&lt;/td&gt;
&lt;td&gt;除法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/=&lt;/td&gt;
&lt;td&gt;取余数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%=&lt;/td&gt;
&lt;td&gt;自增&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较运算符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;比较运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;==&lt;/td&gt;
&lt;td&gt;判断值是否相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;===&lt;/td&gt;
&lt;td&gt;判断数据类型和值是否相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;=&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;=&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!=&lt;/td&gt;
&lt;td&gt;不等于&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑运算符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;逻辑运算符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;逻辑与，并且的功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;||&lt;/td&gt;
&lt;td&gt;逻辑或，或者的功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!&lt;/td&gt;
&lt;td&gt;取反&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;三元运算符&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三元运算符格式：(比较表达式) ? 表达式1 : 表达式2;&lt;/li&gt;
&lt;li&gt;格式说明：
如果比较表达式为true，则取表达式1
如果比较表达式为false，则取表达式2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;流程控制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;if语句&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let month = 3;
if(month &amp;gt;= 3 &amp;amp;&amp;amp; month &amp;lt;= 5) {
    document.write(&quot;春季&quot;);
}else if(month &amp;gt;= 6 &amp;amp;&amp;amp; month &amp;lt;= 8) {
    document.write(&quot;夏季&quot;);
}else if(month == 12 || month == 1 || month == 2) {
    document.write(&quot;冬季&quot;);
}else {
    document.write(&quot;月份有误&quot;);
}

document.write(&quot;&amp;lt;br&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;switch语句&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch(sex){
    case 1:
        document.write(&quot;男性&quot;);
        break;
    case 2:
        document.write(&quot;女性&quot;);
        break;
    default:
        document.write(&quot;性别有误&quot;);
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;for循环&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(let i = 1; i &amp;lt;= 5; i++) {
    document.write(i + &quot;&amp;lt;br&amp;gt;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;while循环&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let n = 6;
while(n &amp;lt;= 10) {
    document.write(n + &quot;&amp;lt;br&amp;gt;&quot;);
    n++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;数组的使用和 java 中的数组基本一致，在JavaScript 中的数组更加灵活，数据类型和长度都没有限制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let 数组名 = [元素1,元素2,…];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引范围：从 0 开始，最大到数组长度-1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组长度：数组名.length&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [10,20,30]; 
document.write(arr+&quot;&amp;lt;br&amp;gt;&quot;)// 直接输出：10,20,30
for(let i = 0; i &amp;lt; arr.length; i++) {
    document.write(arr[i] + &quot;&amp;lt;br&amp;gt;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组高级运算符：...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数组赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr2 = [...arr];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr3 = [40,50,60];
let arr4 = [...arr2 , ...arr3];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串转数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr5 = [...&quot;JavaScript&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;函数&lt;/h3&gt;
&lt;p&gt;函数类似于 java 中的方法，可以将一些代码进行抽取，达到复用的效果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function 方法名(参数列表) {
    方法体; 
    return 返回值; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let 变量 = 方法名();
方法名();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可变参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function 方法名(... 参数名) {
    方法体; 
    return 返回值; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匿名函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function(参数列表) {
    方法体; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;DOM&lt;/h2&gt;
&lt;h3&gt;DOM介绍&lt;/h3&gt;
&lt;p&gt;DOM(Document Object Model)：文档对象模型。&lt;/p&gt;
&lt;p&gt;将 HTML 文档的各个组成部分，封装为对象。借助这些对象，可以对 HTML 文档进行增删改查的动态操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/DOM%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;元素获取&lt;/h3&gt;
&lt;p&gt;Element元素的获取操作：document接口方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getElementById(id属性值)&lt;/td&gt;
&lt;td&gt;根据id属性值获取元素对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getElementsByTagName(标签名称)&lt;/td&gt;
&lt;td&gt;根据标签名称获取元素对象，返回数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getElementsByClassName(class属性值)&lt;/td&gt;
&lt;td&gt;根据class属性值获取元素对象，返回数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getElementsByName(name属性值)&lt;/td&gt;
&lt;td&gt;根据name属性值获取元素对象，返回数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子元素对象.parentElement属性&lt;/td&gt;
&lt;td&gt;获取当前元素的父元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div1&quot;&amp;gt;div1&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;div2&quot;&amp;gt;div2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;cls&quot;&amp;gt;div3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;cls&quot;&amp;gt;div4&amp;lt;/div&amp;gt;
    &amp;lt;input type=&quot;text&quot; name=&quot;username&quot;/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    let div1 = document.getElementById(&quot;div1&quot;);//根据id属性值获取元素对象
    //alert(div1);//[object HTMLDivElement]

    let body = div1.parentElement;//获取当前元素的父元素
    alert(body);
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;元素增删改&lt;/h3&gt;
&lt;p&gt;Element元素的增删改操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;createElement(标签名)&lt;/td&gt;
&lt;td&gt;创建一个新的标签元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appendChild(子元素)&lt;/td&gt;
&lt;td&gt;将指定子元素添加到父元素中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removeChild(子元素)&lt;/td&gt;
&lt;td&gt;用父元素删除指定子元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replaceChild(新元素, 旧元素)&lt;/td&gt;
&lt;td&gt;用新元素替换子元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;createTextNode(数据)&lt;/td&gt;
&lt;td&gt;创建文本元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;select id=&quot;s&quot;&amp;gt;
        &amp;lt;option&amp;gt;---请选择---&amp;lt;/option&amp;gt;
        &amp;lt;option&amp;gt;北京&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    let option = document.createElement(&quot;option&quot;);//创建新的元素
    option.innerText = &quot;深圳&quot;;//为option添加文本内容
 
    let select = document.getElementById(&quot;s&quot;);
    select.appendChild(option);//将子元素添加到父元素中

    let option2 = document.createElement(&quot;option&quot;);
    option2.innerText = &quot;杭州&quot;;
    select.replaceChild(option2,option);//用新元素替换老元素
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;属性操作&lt;/h3&gt;
&lt;p&gt;Attribute属性的操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;setAttribute(属性名, 属性值)&lt;/td&gt;
&lt;td&gt;设置属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getAttribute(属性名)&lt;/td&gt;
&lt;td&gt;根据属性名获取属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removeAttribute(属性名)&lt;/td&gt;
&lt;td&gt;根据属性名移除指定的属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元素名.style属性&lt;/td&gt;
&lt;td&gt;为元素添加样式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元素名.className属性&lt;/td&gt;
&lt;td&gt;为元素添加指定样式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;.aColor{
    color: blue;
}/*获取写在&amp;lt;style&amp;gt;标签*/
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;a&amp;gt;点我呀&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    let a = document.getElementsByTagName(&quot;a&quot;)[0];//因为是数组
    a.setAttribute(&quot;href&quot;,&quot;https://www.baidu.com&quot;);//添加属性

    let value = a.getAttribute(&quot;href&quot;);//获取属性

    //a.style.color = &quot;red&quot;;//添加样式
    a.className = &quot;aColor&quot;;//添加指定CSS样式
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;文本操作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Text文本的操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;innerText&lt;/td&gt;
&lt;td&gt;元素的文本内容，不解析标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;innerHTML&lt;/td&gt;
&lt;td&gt;元素的文本内容，解析标签&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;类似于赋值操作，同时支持取用该值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    //1. innerText   添加文本内容，不解析标签
    let div = document.getElementById(&quot;div&quot;);
    div.innerText = &quot;我是div&quot;;

    //2. innerHTML   添加文本内容，解析标签
    div.innerHTML = &quot;&amp;lt;b&amp;gt;我是div&amp;lt;/b&amp;gt;&quot;;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入框文本：input元素.value;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;事件&lt;/h2&gt;
&lt;h3&gt;事件介绍&lt;/h3&gt;
&lt;p&gt;事件指的就是当某些组件执行了某些操作后，会触发某些代码的执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常用的事件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS%E5%B8%B8%E7%94%A8%E7%9A%84%E4%BA%8B%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更多的事件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS%E6%9B%B4%E5%A4%9A%E7%9A%84%E4%BA%8B%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;事件操作&lt;/h3&gt;
&lt;p&gt;绑定事件的方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：通过标签中的事件属性进行绑定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：通过 DOM 元素属性绑定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;img id=&quot;img&quot; src=&quot;img/01.png&quot;/&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;!-- &amp;lt;button id=&quot;up&quot; onclick=&quot;up()&quot;&amp;gt;上一张&amp;lt;/button&amp;gt; 
    &amp;lt;button id=&quot;down&quot; onclick=&quot;down()&quot;&amp;gt;下一张&amp;lt;/button&amp;gt; --&amp;gt;
    &amp;lt;button id=&quot;up&quot;&amp;gt;上一张&amp;lt;/button&amp;gt; &amp;lt;!--图片 上一张 下一张  类似百度图库--&amp;gt;
    &amp;lt;button id=&quot;down&quot;&amp;gt;下一张&amp;lt;/button&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    //显示第一张图片的方法
    function up(){
        let img = document.getElementById(&quot;img&quot;);
        img.setAttribute(&quot;src&quot;,&quot;img/01.png&quot;);
    }

    //显示第二张图片的方法
    function down(){
        let img = document.getElementById(&quot;img&quot;);
        img.setAttribute(&quot;src&quot;,&quot;img/02.png&quot;);
    }

    //为上一张按钮绑定单击事件
    let upBtn = document.getElementById(&quot;up&quot;);
    upBtn.onclick = up;

    //为下一张按钮绑定单击事件
    let downBtn = document.getElementById(&quot;down&quot;);
    downBtn.onclick = down;
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;综合案例&lt;/h3&gt;
&lt;p&gt;案例介绍：&lt;/p&gt;
&lt;p&gt;在姓名、年龄、性别三个文本框中填写信息后，添加到“学生信息表”列表（表格），点击删除后，删除该行数据，并且不需刷新&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/%E4%BA%8B%E4%BB%B6%E6%A1%88%E4%BE%8B%E6%95%88%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加功能分析&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为添加按钮绑定单击事件&lt;/li&gt;
&lt;li&gt;创建 tr 元素&lt;/li&gt;
&lt;li&gt;创建 4 个 td 元素&lt;/li&gt;
&lt;li&gt;将 td 添加到 tr 中&lt;/li&gt;
&lt;li&gt;获取文本框输入的信息&lt;/li&gt;
&lt;li&gt;创建 3 个文本元素&lt;/li&gt;
&lt;li&gt;将文本元素添加到对应的 td 中&lt;/li&gt;
&lt;li&gt;创建 a 元素&lt;/li&gt;
&lt;li&gt;将 a 元素添加到对应的 td 中&lt;/li&gt;
&lt;li&gt;将 tr 添加到 table 中&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除功能分析&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为每个删除超链接添加单击事件属性&lt;/li&gt;
&lt;li&gt;定义删除的方法&lt;/li&gt;
&lt;li&gt;获取 table 元素&lt;/li&gt;
&lt;li&gt;获取 tr 元素&lt;/li&gt;
&lt;li&gt;通过 table 删除 tr&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTML&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;动态表格&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;../css/table.css&quot;/&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;name&quot; placeholder=&quot;请输入姓名&quot; autocomplete=&quot;off&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;age&quot;  placeholder=&quot;请输入年龄&quot; autocomplete=&quot;off&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;gender&quot;  placeholder=&quot;请输入性别&quot; autocomplete=&quot;off&quot;&amp;gt;
    &amp;lt;input type=&quot;button&quot; value=&quot;添加&quot; id=&quot;add&quot;&amp;gt;
&amp;lt;/div&amp;gt;

    &amp;lt;table id=&quot;tb&quot;&amp;gt;
        &amp;lt;caption&amp;gt;学生信息表&amp;lt;/caption&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;姓名&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;年龄&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;性别&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;操作&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;张三&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;23&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;男&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;a href=&quot;JavaScript:void(0);&quot;onclick=&quot;drop(this)&quot;&amp;gt;删除&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    //一、添加功能
    //1.为添加按钮绑定单击事件
    document.getElementById(&quot;add&quot;).onclick = function(){
        //2.创建行元素
        let tr = document.createElement(&quot;tr&quot;);
        //3.创建4个单元格元素
        let nameTd = document.createElement(&quot;td&quot;);
        let ageTd = document.createElement(&quot;td&quot;);
        let genderTd = document.createElement(&quot;td&quot;);
        let deleteTd = document.createElement(&quot;td&quot;);
        //4.将td添加到tr中
        tr.appendChild(nameTd);
        tr.appendChild(ageTd);
        tr.appendChild(genderTd);
        tr.appendChild(deleteTd);
        //5.获取输入框的文本信息
        let name = document.getElementById(&quot;name&quot;).value;
        let age = document.getElementById(&quot;age&quot;).value;
        let gender = document.getElementById(&quot;gender&quot;).value;
        //6.根据获取到的信息创建3个文本元素
        let nameText = document.createTextNode(name);
        let ageText = document.createTextNode(age);
        let genderText = document.createTextNode(gender);
        //7.将3个文本元素添加到td中
        nameTd.appendChild(nameText);
        ageTd.appendChild(ageText);
        genderTd.appendChild(genderText);
        //8.创建超链接元素和显示的文本以及添加href属性
        let a = document.createElement(&quot;a&quot;);
        let aText = document.createTextNode(&quot;删除&quot;);
        a.setAttribute(&quot;href&quot;,&quot;JavaScript:void(0);&quot;);
        a.setAttribute(&quot;onclick&quot;,&quot;drop(this)&quot;);
        a.appendChild(aText);
        //9.将超链接元素添加到td中
        deleteTd.appendChild(a);
        //10.获取table元素，将tr添加到table中
        let table = document.getElementById(&quot;tb&quot;);
        table.appendChild(tr);
    }

    //二、删除的功能
    //1.为每个删除超链接标签添加单击事件的属性
    //2.定义删除的方法
    function drop(obj){
        //3.获取table元素
        let table = obj.parentElement.parentElement.parentElement;
        //4.获取tr元素
        let tr = obj.parentElement.parentElement;
        //5.通过table删除tr
        table.removeChild(tr);
    }
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;table{
    border: 1px solid;
    margin: auto;
    width: 500px;
}
td,th{
    text-align: center;
    border: 1px solid;
}
div{
    text-align: center;
    margin: 50px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;对象&lt;/h2&gt;
&lt;h3&gt;类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class 类名{
    constructor(变量列表){
        变量赋值;
    }
    方法名(参数列表) {
        方法体;
        return 返回值;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let 对象名 = new 类名(实际变量值);
对象名.方法名();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字面量类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //定义person
    let person = {
        name : &quot;张三&quot;,
        age : 23,
        hobby : [&quot;听课&quot;,&quot;学习&quot;],

        eat : function() {
            document.write(&quot;吃饭...&quot;);
        }
    };

    //使用person
    document.write(person.name + &quot;,&quot; + person.age + &quot;,&quot; + person.hobby[0]+&quot;&amp;lt;br&amp;gt;&quot;);
    person.eat();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;继承&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;继承：让类与类产生子父类的关系，子类可以使用父类有权限的成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承关键字：extends&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;顶级父类：Object&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //定义Person类
    class Person{
        //构造方法
        constructor(name,age){
            this.name = name;
            this.age = age;
        }
        //eat方法
        eat(){
            document.write(&quot;吃饭...&quot;);
        }
    }
    //定义Worker类继承Person
    class Worker extends Person{
        constructor(name,age,salary){
            super(name,age);
            this.salary = salary;
        }

        show(){
            document.write(this.name + &quot;,&quot; + this.age + &quot;,&quot; + this.salary + &quot;&amp;lt;br&amp;gt;&quot;);
        }
    }
    //使用Worker
    let w = new Worker(&quot;张三&quot;,23,10000);
    w.show();
    w.eat();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;内置对象&lt;/h3&gt;
&lt;p&gt;内置对象是 JavaScript 提供的带有属性和方法的特殊数据类型，常见的有普通类型、JSON和正则表达式&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;普通类型&lt;/h3&gt;
&lt;h4&gt;数字&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Number&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;parseFloat(Sring)&lt;/td&gt;
&lt;td&gt;将传入的字符串转为浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parseInt()&lt;/td&gt;
&lt;td&gt;将传入的字符串整数转为整数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //1. parseFloat()  将传入的字符串浮点数转为浮点数
    document.write(Number.parseFloat(&quot;3.14&quot;) + &quot;&amp;lt;br&amp;gt;&quot;);

    //2. parseInt()    将传入的字符串整数转为整数
    document.write(Number.parseInt(&quot;100&quot;) + &quot;&amp;lt;br&amp;gt;&quot;);
    document.write(Number.parseInt(&quot;200abc&quot;) + &quot;&amp;lt;br&amp;gt;&quot;);//从数字开始转换，直到不是数字
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Math&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ceil(x)&lt;/td&gt;
&lt;td&gt;向上取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;floor(x)&lt;/td&gt;
&lt;td&gt;向下取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;round(x)&lt;/td&gt;
&lt;td&gt;四舍五入为整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random()&lt;/td&gt;
&lt;td&gt;随机数，返回的是0.0-1.0之间的范围（含头不含尾）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pow(x,y)&lt;/td&gt;
&lt;td&gt;幂运算，x的y次方&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;document.write(Math.pow(2,3) + &quot;&amp;lt;br&amp;gt;&quot;); // 8
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日期&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Date构造方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;构造方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Date()&lt;/td&gt;
&lt;td&gt;根据当前时间创建对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date(value)&lt;/td&gt;
&lt;td&gt;根据指定毫秒值创建对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date(year, month, [day, hours, minutes, seconds, milliseconds])&lt;/td&gt;
&lt;td&gt;根据指定字段创建对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Date成员方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;成员方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getFullYear()&lt;/td&gt;
&lt;td&gt;获取年份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getMonth()&lt;/td&gt;
&lt;td&gt;获取月份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDate()&lt;/td&gt;
&lt;td&gt;获取天数，相对于月份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getHours()&lt;/td&gt;
&lt;td&gt;获取小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getMinutes()&lt;/td&gt;
&lt;td&gt;获取分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getSeconds()&lt;/td&gt;
&lt;td&gt;获取秒数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getTime()&lt;/td&gt;
&lt;td&gt;返回据1970年1月1日至今的毫秒数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toLocaleString()&lt;/td&gt;
&lt;td&gt;返回本地日期格式的字符串&amp;lt;br /&amp;gt;2021/2/3下午8:20:20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;字符串&lt;/h4&gt;
&lt;p&gt;String&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;构造方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gengenString(vale)&lt;/td&gt;
&lt;td&gt;根据指定字符串创建对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;let s = &quot;字符串&quot;&lt;/td&gt;
&lt;td&gt;直接赋值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;成员方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;length&lt;/td&gt;
&lt;td&gt;获取字符串长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;charAt(index)&lt;/td&gt;
&lt;td&gt;获取指定索引处的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;indexOf(value)&lt;/td&gt;
&lt;td&gt;获取指定字符串出现的索引位置，找不到为-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substring(start, end)&lt;/td&gt;
&lt;td&gt;根据指定索引范围截取字符串（含头不含尾）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;split(value)&lt;/td&gt;
&lt;td&gt;根据指定规则切割字符串，返回数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replace(old, new)&lt;/td&gt;
&lt;td&gt;使用新字符替换老字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数组集合&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Array&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;push(value)&lt;/td&gt;
&lt;td&gt;添加元素到数组的末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop()&lt;/td&gt;
&lt;td&gt;删除数组末尾的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shift()&lt;/td&gt;
&lt;td&gt;删除数组最前面的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;includes(value)&lt;/td&gt;
&lt;td&gt;判断数组是否包含给定的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reverse()&lt;/td&gt;
&lt;td&gt;反转数组中的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sort()&lt;/td&gt;
&lt;td&gt;对数组元素进行升序排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;length&lt;/td&gt;
&lt;td&gt;返回数组的长度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;降序排序：先sort，再reverse&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set：JavaScript中的Set集合，元素唯一，存取顺序一致&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set()&lt;/td&gt;
&lt;td&gt;创建Set集合对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add(value)&lt;/td&gt;
&lt;td&gt;向集合中添加元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;获取集合长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keys()&lt;/td&gt;
&lt;td&gt;获取迭代器对象（遍历方法看实例）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete(value)&lt;/td&gt;
&lt;td&gt;删除指定元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;let s = new Set();
// add(元素)   添加元素
s.add(&quot;a&quot;);s.add(&quot;b&quot;);s.add(&quot;c&quot;);s.add(&quot;c&quot;);

// keys()      获取迭代器对象
let st = s.keys();
//遍历集合
for(let i = 0; i &amp;lt; s.size; i++){
    document.write(st.next().value + &quot;&amp;lt;br&amp;gt;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Map：JavaScript 中的 Map 集合，key 唯一，存取顺序一致&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Map()&lt;/td&gt;
&lt;td&gt;创建Map集合对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set(key, value)&lt;/td&gt;
&lt;td&gt;向集合添加元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;获取集合长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get(key)&lt;/td&gt;
&lt;td&gt;根据key获取value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entries()&lt;/td&gt;
&lt;td&gt;获取迭代器对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete(key)&lt;/td&gt;
&lt;td&gt;根据key删除键值对&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;JSON&lt;/h3&gt;
&lt;h4&gt;JSON入门&lt;/h4&gt;
&lt;p&gt;JSON(JavaScript Object Notation)：是一种轻量级的数据交换格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 ECMAScript 规范的一个子集，采用完全独立于编程语言的文本格式来存储和表示数据&lt;/li&gt;
&lt;li&gt;简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言，易于人阅读和编写，同时也易于计算机解析和 生成，并有效的提升网络传输效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建格式：
&lt;strong&gt;name是字符串类型&lt;/strong&gt;
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSON%E5%88%9B%E5%BB%BA%E6%A0%BC%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;json常用方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stringify(对象)&lt;/td&gt;
&lt;td&gt;将指定对象转换为json格式字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parse(字符串)&lt;/td&gt;
&lt;td&gt;将指定json格式字符串解析成对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;入门案例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; //定义天气对象
let weather = {
    city : &quot;北京&quot;,
    date : &quot;2088-08-08&quot;,
    wendu : &quot;10° ~ 23°&quot;,
};

//1.将天气对象转换为JSON格式的字符串
let str = JSON.stringify(weather);
document.write(str + &quot;&amp;lt;br&amp;gt;&quot;);

//2.将JSON格式字符串解析成JS对象
let weather2 = JSON.parse(str);
document.write(&quot;城市：&quot; + weather2.city + &quot;&amp;lt;br&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;转换工具&lt;/h4&gt;
&lt;p&gt;我们除了可以在 JavaScript 中来使用 JSON 以外，在 JAVA 中同样也可以使用 JSON。&lt;/p&gt;
&lt;p&gt;JSON 的转换工具是通过 JAVA 封装好的一些 JAR 工具包，可以将 JAVA 对象或集合转换成 JSON 格式的字符串，也可以将 JSON 格式的字符串转成 JAVA 对象。&lt;/p&gt;
&lt;p&gt;Jackson：开源免费的 JSON 转换工具，SpringMVC 转换默认使用 Jackson。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常用类&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ObjectMapper&lt;/td&gt;
&lt;td&gt;是Jackson工具包的核心类，提供方法来实现JSON字符串和对象之间的转换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeReference&lt;/td&gt;
&lt;td&gt;对集合泛型的反序列化，使用TypeReference可以明确的指定反序列化的对象类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ObjectMapper常用方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String writeValueAsString(Object obj)&lt;/td&gt;
&lt;td&gt;将Java对象转换成JSON字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;T&amp;gt; T readValue(String json, Class&amp;lt;T&amp;gt; valueType)&lt;/td&gt;
&lt;td&gt;将JSON字符串转换成Java对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;T&amp;gt; T readValue(String json, TypeReference valueTypeRef)&lt;/td&gt;
&lt;td&gt;将JSON字符串转换成Java对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法练习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象转 JSON，JSON 转对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void test01() throws Exception{
    //User对象转json
    User user = new User(&quot;张三&quot;,23);
    String json = mapper.writeValueAsString(user);
    System.out.println(&quot;json字符串：&quot; + json//json字符串 = {&quot;name&quot;:&quot;张三&quot;,&quot;age&quot;:23}
    //json转User对象
    User user2 = mapper.readValue(json, User.class);
    System.out.println(&quot;user对象：&quot; + user2);//user对象 = User{name=&apos;张三&apos;, age=23}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Map转 JSON，JSON 转 Map&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void test02() throws Exception{
    //map&amp;lt;String,String&amp;gt;转json
    HashMap&amp;lt;String,String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    map.put(&quot;姓名&quot;,&quot;张三&quot;);
    map.put(&quot;性别&quot;,&quot;男&quot;);
    String json = mapper.writeValueAsString(map);
    System.out.println(&quot;json字符串：&quot; + json);

    //json转map&amp;lt;String,String&amp;gt;
    HashMap&amp;lt;String,String&amp;gt; map2 = mapper.readValue(json, HashMap.class);
    System.out.println(&quot;map对象：&quot; + map2);
}
//json字符串 = {&quot;姓名&quot;:&quot;张三&quot;,&quot;性别&quot;:&quot;男&quot;}
//map对象 = {姓名=张三, 性别=男}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Map转 JSON，JSON 转 Map&amp;lt;自定义类&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void test03() throws Exception{
    //map&amp;lt;String,User&amp;gt;转json
    HashMap&amp;lt;String,User&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    map.put(&quot;sea一班&quot;,new User(&quot;张三&quot;,23));
    map.put(&quot;sea二班&quot;,new User(&quot;李四&quot;,24));
    String json = mapper.writeValueAsString(map);
    System.out.println(&quot;json字符串：&quot; + json);

    //json转map&amp;lt;String,User&amp;gt;
    HashMap&amp;lt;String,User&amp;gt; map2=mapper.readValue(json,
                                 new TypeReference&amp;lt;HashMap&amp;lt;String,User&amp;gt;&amp;gt;(){});
    System.out.println(&quot;java对象：&quot; + map2);
}
//json字符串 = {&quot;sea一班&quot;:{&quot;name&quot;:&quot;张三&quot;,&quot;age&quot;:23},&quot;sea二班&quot;:{....}
//map对象 = {sea一班=User{name=&apos;张三&apos;, age=23}, sea二班=User{name=&apos;李四&apos;, age=24}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;List&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void test05() throws Exception{
    //List&amp;lt;User&amp;gt;转json
    ArrayList&amp;lt;User&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
    list.add(new User(&quot;张三&quot;,23));
    list.add(new User(&quot;李四&quot;,24));
    String json = mapper.writeValueAsString(list);
    System.out.println(&quot;json字符串：&quot; + json);

    //json转List&amp;lt;User&amp;gt;
    ArrayList&amp;lt;User&amp;gt; list2 = mapper.readValue(json,
								new TypeReference&amp;lt;ArrayList&amp;lt;User&amp;gt;&amp;gt;(){});
    System.out.println(&quot;java对象：&quot; + list2);
}
//json字符串 = [{&quot;name&quot;:&quot;张三&quot;,&quot;age&quot;:23},{&quot;name&quot;:&quot;李四&quot;,&quot;age&quot;:24}]
//list对象 = [User{name=&apos;张三&apos;, age=23}, User{name=&apos;李四&apos;, age=24}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;正则&lt;/h3&gt;
&lt;h4&gt;正则表达式&lt;/h4&gt;
&lt;p&gt;正则表达式：是一种对字符串进行匹配的规则&lt;/p&gt;
&lt;p&gt;RegExp：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;构造方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RegExp(规则)&lt;/td&gt;
&lt;td&gt;根据指定规则创建对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;let reg = /^规则$/&lt;/td&gt;
&lt;td&gt;直接赋值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;成员方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test(匹配的字符串)&lt;/td&gt;
&lt;td&gt;根据指定规则验证字符串是否符合&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;验证用户&lt;/h4&gt;
&lt;p&gt;使用 onsubmit 表单提交事件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/%E8%A1%A8%E5%8D%95%E6%A0%A1%E9%AA%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form class=&quot;login-form&quot; action=&quot;#&quot; id=&quot;registered&quot; method=&quot;get&quot; autocomplete=&quot;off&quot;&amp;gt;
	&amp;lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;&amp;gt;
	&amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot;&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;注册&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;script&amp;gt;
    //1.为表单绑定提交事件  匿名函数
    document.getElementById(&quot;registered&quot;).onsubmit = function() {
        //2.获取填写的用户名和密码
        let username = document.getElementById(&quot;username&quot;).value;
        let password = document.getElementById(&quot;password&quot;).value;

        //3.判断用户名是否符合规则  4~16位纯字母
        let reg1 = /^[a-zA-Z]{4,16}$/;
        if(!reg1.test(username)) {
            alert(&quot;用户名不符合规则，请输入4到16位的纯字母！&quot;);
            return false;
        }

        //4.判断密码是否符合规则  6位纯数字
        let reg2 = /^[\d]{6}$/;
        if(!reg2.test(password)) {
            alert(&quot;密码不符合规则，请输入6位纯数字的密码！&quot;);
            return false;
        }
        //5.如果所有条件都满足，则提交表单
        return true;
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;BOM&lt;/h2&gt;
&lt;h3&gt;BOM介绍&lt;/h3&gt;
&lt;p&gt;BOM(Browser Object Model)：浏览器对象模型。&lt;/p&gt;
&lt;p&gt;将浏览器的各个组成部分封装成不同的对象，方便我们进行操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/BOM%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Window&lt;/h3&gt;
&lt;p&gt;Windows窗口对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定时器&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;唯一标识 setTimeout(功能，毫秒值)：设置一次性定时器。&lt;/li&gt;
&lt;li&gt;clearTimeout(标识)：取消一次性定时器。&lt;/li&gt;
&lt;li&gt;唯一标识 setInterval(功能，毫秒值)：设置循环定时器。&lt;/li&gt;
&lt;li&gt;clearInterval(标识)：取消循环定时器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;加载事件
&lt;ul&gt;
&lt;li&gt;window.onload：在页面加载完毕后触发此事件的功能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //一、定时器
    function fun(){
        alert(&quot;该起床了！&quot;);
    }

	//设置一次性定时器
    let d1 = setTimeout(&quot;fun()&quot;,3000);
    //取消一次性定时器
    clearTimeout(d1);

    //设置循环定时器，3秒弹出一次
    let d2 = setInterval(&quot;fun()&quot;,3000);
    //取消循环定时器
    clearInterval(d2);

    //加载事件
    let div = document.getElementById(&quot;div&quot;);
    alert(div);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;window窗口对象&amp;lt;/title&amp;gt;
    &amp;lt;script&amp;gt;
        function fun(){
            alert(&quot;该起床了！&quot;);
        }
        //加载事件
        window.onload = function(){
            let div = document.getElementById(&quot;div&quot;);
            alert(div);
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;dddd&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Location&lt;/h3&gt;
&lt;p&gt;Location地址栏对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;href 属性：浏览器的地址栏。我们可以通过为该属性设置新的URL，使浏览器读取并显示新URL的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现效果：秒数会自动变小，倒计时，5，4，3，2，1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;p&amp;gt;
        注册成功！&amp;lt;span id=&quot;time&quot;&amp;gt;5&amp;lt;/span&amp;gt;秒之后自动跳转到首页...
    &amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    //1.定义方法。改变秒数，跳转页面
    let num = 5;
    function showTime() {
        num--;

        if(num &amp;lt;= 0) {
            //跳转首页
            location.href = &quot;index.html&quot;;
        }
        let span = document.getElementById(&quot;time&quot;);
        span.innerText = num;
    }
    //2.设置循环定时器，每1秒钟执行showTime方法
    setInterval(&quot;showTime()&quot;,1000);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;封装&lt;/h2&gt;
&lt;p&gt;封装思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**封装：**将复杂的操作进行封装隐藏，对外提供更加简单的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取元素的方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;document.getElementById(id值)：根据 id 值获取元素&lt;/li&gt;
&lt;li&gt;document.getElementsByName(name值)：根据 name 属性值获取元素们&lt;/li&gt;
&lt;li&gt;document.getElementsByTagName(标签名)：根据标签名获取元素们&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;my.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getById(id){
    return document.getElementById(id);
}

function getByName(name) {
    return document.getElementsByName(name);
}

function getByTag(tag) {
    return document.getElementsByTagName(tag);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;封装.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div1&quot;&amp;gt;div1&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;my.js&quot;&amp;gt;&amp;lt;/script&amp;gt;	&amp;lt;!--引入js文件--&amp;gt;
&amp;lt;script&amp;gt;
    let div1 = getById(&quot;div1&quot;);
    alert(div1);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;JQuery&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;jQuery 是一个 JavaScript 库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所谓的库，就是一个 JS 文件，里面封装了很多预定义的函数，比如获取元素，执行隐藏、移动等，目的就是在使用时直接调用，不需要再重复定义，这样就可以极大地简化了 JavaScript 编程。&lt;/li&gt;
&lt;li&gt;jQuery 官网：https://www.jquery.com&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引入jQ文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--引入 jQuery 文件--&amp;gt;
&amp;lt;script src=&quot;js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    //jQ语句
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;jQuery 的核心语法 $()&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;h4&gt;对象转换&lt;/h4&gt;
&lt;p&gt;jQuery 本质上虽然也是 JS，但二者的 API 方法不能混合使用，若想使用对方的 API，需要进行对象的转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JS 的 DOM 对象转换成 jQuery 对象：$(JS的DOM对象);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// JS方式，通过id属性值获取div元素
let jsDiv = document.getElementById(&quot;div&quot;);
// 将 JS 对象转换为jQuery对象
let jq = $(jsDiv);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jQuery 对象转换成 JS 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jQuery对象[索引];&lt;/li&gt;
&lt;li&gt;jQuery对象.get(索引);&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//jQuery方式，通过id属性值获取div元素
let jqDiv = $(&quot;#div&quot;);
//将 jQuery对象转换为JS对象
let js = jqDiv[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;事件操作&lt;/h4&gt;
&lt;h5&gt;绑定解绑&lt;/h5&gt;
&lt;p&gt;在 jQuery 中将事件封装成了对应的方法。去掉了 JS 中的 .on 语法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;绑定事件：&lt;code&gt;jQuery对象.on(事件名称,执行的功能);&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//给btn1按钮绑定单击事件
$(&quot;#btn1&quot;).on(&quot;click&quot;,function(){
	alert(&quot;点我干嘛?&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解绑事件：&lt;code&gt;jQuery对象.off(事件名称);&lt;/code&gt;
如果不指定事件名称，则会把该对象绑定的所有事件都解绑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//通过btn2解绑btn1的单击事件
$(&quot;#btn2&quot;).on(&quot;click&quot;,function(){
	$(&quot;#btn1&quot;).off(&quot;click&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;事件切换&lt;/h5&gt;
&lt;p&gt;事件切换：需要给同一个对象绑定多个事件，而且多个事件还有先后顺序关系&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：单独定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$(元素).事件方法名1(要执行的功能);&lt;/li&gt;
&lt;li&gt;$(元素).事件方法名2(要执行的功能);&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//将鼠标移到某元素，添加css样式
$(&quot;#div&quot;).mouseover(function(){
    //背景色：红色
    //$(&quot;#div&quot;).css(&quot;background&quot;,&quot;red&quot;);
    $(this).css(&quot;background&quot;,&quot;red&quot;);
});
$(&quot;#div&quot;).mouseout(function(){
    //背景色：蓝色
    $(this).css(&quot;background&quot;,&quot;blue&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：链式定义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$(元素).事件方法名1(要执行的功能) .事件方法名2(要执行的功能);&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$(&quot;#div&quot;).mouseover(function(){
   $(this).css(&quot;background&quot;,&quot;red&quot;);
}).mouseout(function(){
   $(this).css(&quot;background&quot;,&quot;blue&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;遍历操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备，实现按键后遍历无序列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;input type=&quot;button&quot; id=&quot;btn&quot; value=&quot;遍历列表项&quot;&amp;gt;
    &amp;lt;ul&amp;gt;
        &amp;lt;li&amp;gt;传智播客&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;黑马程序员&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;传智专修学院&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(let i = 0; i &amp;lt; 容器对象长度; i++){
		执行功能;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象.each方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;容器对象.each(function(index,ele){
	执行功能;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$(&quot;#btn&quot;).click(function(){
    let lis = $(&quot;li&quot;);
    lis.each(function(index,ele){
        alert(index + &quot;:&quot; + ele.innerHTML);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$.each()方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.each(容器对象,function(index,ele){
	执行功能;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$(&quot;#btn&quot;).click(function(){
    let lis = $(&quot;li&quot;);
    $.each(lis,function(index,ele){
        alert(index + &quot;:&quot; + ele.innerHTML);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for of语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&quot;#btn&quot;).click(function(){
    let lis = $(&quot;li&quot;);
    for(ele of lis){
        alert(ele.innerHTML);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;选择器&lt;/h3&gt;
&lt;h4&gt;基本选择器&lt;/h4&gt;
&lt;p&gt;选择器：类似于 CSS 的选择器，可以帮助我们获取元素。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下面所有的A B均为标签名&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;元素选择器&lt;/td&gt;
&lt;td&gt;$(&quot;元素的名称&quot;)&lt;/td&gt;
&lt;td&gt;根据元素名称获取元素对象（数组）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id选择器&lt;/td&gt;
&lt;td&gt;$(&quot;#id的属性值&quot;)&lt;/td&gt;
&lt;td&gt;根据id属性值获取元素对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类选择器&lt;/td&gt;
&lt;td&gt;$(&quot;.class的属性值&quot;)&lt;/td&gt;
&lt;td&gt;根据class属性值获取元素对象（数组）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;层级选择器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;后代选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A B&quot;)&lt;/td&gt;
&lt;td&gt;A下的所有B，包括B的子级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A &amp;gt; B&quot;)&lt;/td&gt;
&lt;td&gt;A下的所有B，不 包括B的子级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;兄弟选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A + B&quot;)&lt;/td&gt;
&lt;td&gt;A相邻的下一个B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;兄弟选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A ~ B&quot;)&lt;/td&gt;
&lt;td&gt;A相邻的所有B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;属性选择器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;属性名选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A[属性名]&quot;)&lt;/td&gt;
&lt;td&gt;根据指定属性名获取元素对象（数组）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;属性值选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A[属性名=属性值]&quot;)&lt;/td&gt;
&lt;td&gt;根据指定属性名和属性值获取元素对象（数组）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;过滤器选择器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;首元素选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:first&quot;)&lt;/td&gt;
&lt;td&gt;获取选择的元素中的第一个元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;尾元素选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:last&quot;)&lt;/td&gt;
&lt;td&gt;获取选择的元素中的最后一个元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非元素选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:not(B)&quot;)&lt;/td&gt;
&lt;td&gt;不包括指定内容的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;偶数选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:even&quot;)&lt;/td&gt;
&lt;td&gt;偶数，从0开始计数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;奇数选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:odd&quot;)&lt;/td&gt;
&lt;td&gt;奇数，从0开始计数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;等于索引选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:eq(index)&quot;)&lt;/td&gt;
&lt;td&gt;指定索引的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大于索引选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:gt(index)&quot;)&lt;/td&gt;
&lt;td&gt;大于指定索引的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小于索引选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:lt(index)&quot;)&lt;/td&gt;
&lt;td&gt;小于指定索引的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;div1&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;div2&quot;&amp;gt;div2&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;div3&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;div4&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    // 首元素选择器	$(&quot;A:first&quot;);
    let div1 = $(&quot;div:first&quot;);
    //alert(div1.html());

    // 非元素选择器	$(&quot;A:not(B)&quot;);
    let divs1 = $(&quot;div:not(#div2)&quot;);//数组

    // 偶数选择器	    $(&quot;A:even&quot;);
    let divs2 = $(&quot;div:even&quot;);
    alert(divs2.length);
    alert(divs2[0].innerHTML);
    alert(divs2[1].innerHTML);

    // 等于索引选择器	 $(&quot;A:eq(index)&quot;);
    let div3 = $(&quot;div:eq(2)&quot;);
    //alert(div3.html());
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;表单属性选择器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;可用选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:enabled&quot;)&lt;/td&gt;
&lt;td&gt;获得可用元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不可用元素选择器&lt;/td&gt;
&lt;td&gt;$(&quot;A:disabled&quot;)&lt;/td&gt;
&lt;td&gt;获得不可用元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单选/复选框被选中的元素&lt;/td&gt;
&lt;td&gt;$(&quot;A:checked&quot;)&lt;/td&gt;
&lt;td&gt;获取单选/复选框被选中的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;下拉框被选中的元素&lt;/td&gt;
&lt;td&gt;$(&quot;A:selected&quot;)&lt;/td&gt;
&lt;td&gt;获取下拉框被选中的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;input type=&quot;text&quot; disabled&amp;gt;
    &amp;lt;input type=&quot;text&quot; &amp;gt;
    &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;men&quot; checked&amp;gt;男
    &amp;lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;women&quot;&amp;gt;女
    &amp;lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;study&quot; checked&amp;gt;学习
    &amp;lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;sleep&quot; checked&amp;gt;睡觉
    &amp;lt;select&amp;gt;
        &amp;lt;option&amp;gt;---请选择---&amp;lt;/option&amp;gt;
        &amp;lt;option selected&amp;gt;本科&amp;lt;/option&amp;gt;
        &amp;lt;option&amp;gt;专科&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    // 1.可用元素选择器  $(&quot;A:enabled&quot;);
    let ins1 = $(&quot;input:enabled&quot;);

    // 2.不可用元素选择器  $(&quot;A:disabled&quot;);
    let ins2 = $(&quot;input:disabled&quot;);

    // 3.单选/复选框被选中的元素  $(&quot;A:checked&quot;);
    let ins3 = $(&quot;input:checked&quot;);
    alert(ins3.length);
    alert(ins3[0].name);
    alert(ins3[1].value);

    // 4.下拉框被选中的元素   $(&quot;A:selected&quot;);
    let select = $(&quot;select option:selected&quot;);
    alert(select.html());  
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;DOM&lt;/h3&gt;
&lt;h4&gt;文本操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;html()&lt;/td&gt;
&lt;td&gt;获取标签的文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;html(value)&lt;/td&gt;
&lt;td&gt;设置标签的文本内容，解析标签&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;//获取div标签的文本内容
let value = $(&quot;#div&quot;).html();
//设置div标签的文本内容
$(&quot;#div&quot;).html(&quot;&amp;lt;b&amp;gt;我是div&amp;lt;/b&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;对象操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$(&quot;元素&quot;)&lt;/td&gt;
&lt;td&gt;创建指定元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;append(element)&lt;/td&gt;
&lt;td&gt;添加成最后一个子元素，由添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appendTo(element)&lt;/td&gt;
&lt;td&gt;添加成最后一个子元素，由被添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prepend(element)&lt;/td&gt;
&lt;td&gt;添加成第一个子元素，由添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prependTo(element)&lt;/td&gt;
&lt;td&gt;添加成第一个子元素，由被添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;before(element)&lt;/td&gt;
&lt;td&gt;添加到当前元素的前面，两者之间是兄弟关系，由添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after(element)&lt;/td&gt;
&lt;td&gt;添加到当前元素的后面，两者之间是兄弟关系，由添加者对象调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove()&lt;/td&gt;
&lt;td&gt;删除指定元素（自己移除自己）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty()&lt;/td&gt;
&lt;td&gt;清空指定元素的所有子元素（自己还在）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;div id=&quot;div&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;ul id=&quot;city&quot;&amp;gt;
&amp;lt;li id=&quot;bj&quot;&amp;gt;北京&amp;lt;/li&amp;gt;
&amp;lt;li id=&quot;sh&quot;&amp;gt;上海&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;ul id=&quot;desc&quot;&amp;gt;
&amp;lt;li id=&quot;jy&quot;&amp;gt;加油&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 按钮一：添加一个span到div
$(&quot;#btn1&quot;).click(function(){
    let span = $(&quot;&amp;lt;span&amp;gt;span&amp;lt;/span&amp;gt;&quot;);
    $(&quot;#div&quot;).append(span);
});
//按钮二：将加油添加到城市列表最下方
$(&quot;#btn2&quot;).click(function(){
    $(&quot;#city&quot;).append($(&quot;#jy&quot;));
});
//按钮三：将加油添加到城市列表最上方
$(&quot;#btn3&quot;).click(function(){
    $(&quot;#jy&quot;).prependTo($(&quot;#city&quot;));
});
//按钮四：将加油添加到北京下方
$(&quot;#btn4&quot;).click(function(){
    $(&quot;#bj&quot;).after($(&quot;#jy&quot;));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;样式操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;css(name)&lt;/td&gt;
&lt;td&gt;根据样式名称获取css样式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;css(name,value)&lt;/td&gt;
&lt;td&gt;设置css样式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;addClass(value)&lt;/td&gt;
&lt;td&gt;给指定的对象添加样式类名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removeClass(value)&lt;/td&gt;
&lt;td&gt;给指定的对象删除样式类名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toggleClass(value)&lt;/td&gt;
&lt;td&gt;没有样式类名就添加，有就删除，循环如此&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;.cls{
    background: pink;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div style=&quot;border: 1px solid red;&quot; id=&quot;div&quot;&amp;gt;我是div&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 1.css(name)   获取css样式
$(&quot;#btn1&quot;).click(function(){
    alert($(&quot;#div&quot;).css(&quot;border&quot;));  //1px solid rgb(255, 0, 0)
});

// 2.css(name,value)   设置CSS样式
$(&quot;#btn2&quot;).click(function(){
    $(&quot;#div&quot;).css(&quot;background&quot;,&quot;blue&quot;);
});

// 3.addClass(value)   给指定的对象添加样式类名
$(&quot;#btn3&quot;).click(function(){
    $(&quot;#div&quot;).addClass(&quot;cls&quot;);  //cls是一个css样式
});

// 4.toggleClass(value)  如果没有样式类名，则添加。如果有，则删除
$(&quot;#btn5&quot;).click(function(){
    $(&quot;#div&quot;).toggleClass(&quot;cls&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;属性操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;attr(name,[value])&lt;/td&gt;
&lt;td&gt;获得/设置属性的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prop(name,[value])&lt;/td&gt;
&lt;td&gt;获得/设置属性的值（checked, selected）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;操作属性&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;input type=&quot;text&quot; id=&quot;username&quot;&amp;gt; &amp;lt;br&amp;gt;
&amp;lt;input type=&quot;button&quot; id=&quot;btn1&quot; value=&quot;获取输入框的id属性&quot;&amp;gt;    
&amp;lt;input type=&quot;button&quot; id=&quot;btn2&quot; value=&quot;给输入框设置value属性&quot;&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;input type=&quot;radio&quot; id=&quot;gender1&quot; name=&quot;gender&quot;&amp;gt;男
&amp;lt;input type=&quot;radio&quot; id=&quot;gender2&quot; name=&quot;gender&quot;&amp;gt;女&amp;lt;br/&amp;gt;
&amp;lt;input type=&quot;button&quot; id=&quot;btn3&quot; value=&quot;选中女&quot;&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;select&amp;gt;
&amp;lt;option&amp;gt;---请选择---&amp;lt;/option&amp;gt;
&amp;lt;option id=&quot;bk&quot;&amp;gt;本科&amp;lt;/option&amp;gt;
&amp;lt;option id=&quot;zk&quot;&amp;gt;专科&amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;input type=&quot;button&quot; id=&quot;btn4&quot; value=&quot;选中本科&quot;&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    //按钮一：获取输入框的id属性  attr(name,[value])
    $(&quot;#btn1&quot;).click(function(){
        alert($(&quot;#username&quot;).attr(&quot;id&quot;));
    });
    
    //按钮二：给输入框设置value属性  attr(name,[value])
    $(&quot;#btn2&quot;).click(function(){
        $(&quot;#username&quot;).attr(&quot;value&quot;,&quot;hello...&quot;);
    });
    
    //按钮三：选中女   prop(name,[value]) 
    $(&quot;#btn3&quot;).click(function(){
        $(&quot;#gender2&quot;).prop(&quot;checked&quot;,true);
    });

    //按钮四：选中本科  prop(name,[value]) 
    $(&quot;#btn4&quot;).click(function(){
        $(&quot;#bk&quot;).prop(&quot;selected&quot;,true);
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;AJAX&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AJAX(Asynchronous JavaScript And XML)：异步的 JavaScript 和 XML。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不是一种新技术，而是多个技术综合，用于快速创建动态网页的技术。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般的网页如果需要更新内容，必需重新加载个页面。而 AJAX 通过浏览器与服务器进行少量数据交换，就可以使网页实现异步更新。也就是在不重新加载整个页 面的情况下，对网页的部分内容进行&lt;strong&gt;局部更新&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/AJAX%E7%BD%91%E9%A1%B5%E5%B1%80%E9%83%A8%E6%9B%B4%E6%96%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;实现AJAX&lt;/h2&gt;
&lt;h3&gt;JS方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心对象：XMLHttpRequest&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于在后台与服务器交换数据。可以在不重新加载整个网页的情况下，对网页的某部分进行更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开链接：open(method,url,async)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;method：请求的类型 GET 或 POST&lt;/li&gt;
&lt;li&gt;url：请求资源的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;async：true(异步) 或 false(同步)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送请求：send(String params)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;params：请求的参数(POST 专用)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理响应：onreadystatechange&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;readyState：0-请求未初始化，1-服务器连接已建立，2-请求已接收，3-请求处理中，4-请求已完成，且响应已就绪。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;status：200-响应已全部 OK。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获得响应数据形式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;responseText：获得字符串形式的响应数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;responseXML：获得 XML 形式的响应数据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;鼠标移出输入框，判断用户名是否被注册：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Servlet&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/userServlet&quot;)
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置请求和响应的乱码
        req.setCharacterEncoding(&quot;UTF-8&quot;);
        resp.setContentType(&quot;text/html;charset=UTF-8&quot;);

        //1.获取请求参数
        String username = req.getParameter(&quot;username&quot;);
        //模拟服务器处理请求需要1秒钟
        Thread.sleep(5000);
        
        //2.判断姓名是否已注册
        if (&quot;zhangsan&quot;.equals(username)) {
            resp.getWriter().write(&quot;&amp;lt;font color=&apos;red&apos;&amp;gt;用户名已注册&quot;);
        } else {
            resp.getWriter().write(&quot;&amp;lt;font color=&apos;green&apos;&amp;gt;用户名可用&quot;);
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;html文件&lt;/p&gt;
&lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;用户注册&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;form autocomplete=&quot;off&quot;&amp;gt;
姓名：&amp;lt;input type=&quot;text&quot; id=&quot;username&quot;&amp;gt;
&amp;lt;span id=&quot;uSpan&quot;&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;br&amp;gt;
密码：&amp;lt;input type=&quot;password&quot; id=&quot;password&quot;&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;input type=&quot;submit&quot; value=&quot;注册&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //1.为姓名绑定失去焦点事件
    document.getElementById(&quot;username&quot;).onblur = function() {
        //2.创建XMLHttpRequest核心对象
        let xmlHttp = new XMLHttpRequest();

        //3.打开链接
        let username = document.getElementById(&quot;username&quot;).value;
        xmlHttp.open(&quot;GET&quot;, &quot;userServlet?username=&quot; + username, true);

        //4.发送请求
        xmlHttp.send();

        //5.处理响应
        xmlHttp.onreadystatechange = function() {
            //判断请求和响应是否成功
            if(xmlHttp.readyState == 4 &amp;amp;&amp;amp; xmlHttp.status == 200) {
                //将响应的数据显示到span标签
                document.getElementById(&quot;uSpan&quot;).innerHTML = xmlHttp.responseText;
            }
        }
    }
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;JQ方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心语法：&lt;/strong&gt;&lt;code&gt;$.ajax({name:value,name:value,…}); &lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;url：请求的资源路径。&lt;/li&gt;
&lt;li&gt;async：是否异步请求，true-是，false-否 (默认是 true)。&lt;/li&gt;
&lt;li&gt;data：发送到服务器的数据，可以是&lt;strong&gt;键值对或者 js 对象&lt;/strong&gt;形式。&lt;/li&gt;
&lt;li&gt;type：请求方式，POST 或 GET (默认是 GET)。&lt;/li&gt;
&lt;li&gt;dataType：预期的返回数据的类型，取值可以是 xml, html, js, json, text等。&lt;/li&gt;
&lt;li&gt;success：请求成功时调用的回调函数。&lt;/li&gt;
&lt;li&gt;error：请求失败时调用的回调函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    //1.为用户名绑定失去焦点事件
    $(&quot;#username&quot;).blur(function () {
        let username = $(&quot;#username&quot;).val();
        //2.jQuery的通用方式实现AJAX
        $.ajax({
            //请求资源路径
            url:&quot;userServletxxx&quot;,
            //是否异步
            async:true,
            //请求参数
            data:&quot;username=&quot;+username,
            //请求方式
            type:&quot;POST&quot;,
            //数据形式
            dataType:&quot;text&quot;,
            //请求成功后调用的回调函数
            success:function (data) {
                //将响应的数据显示到span标签
                $(&quot;#uSpan&quot;).html(data);
            },
            //请求失败后调用的回调函数
            error:function () {
                alert(&quot;操作失败...&quot;);
            }
        });
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;分页知识&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/%E5%88%86%E9%A1%B5%E7%9F%A5%E8%AF%86.png&quot; alt=&quot;分页知识&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;VUE&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Vue是一套构建用户界面的渐进式前端框架。&lt;/p&gt;
&lt;p&gt;Vue只关注视图层，并且非常容易学习，还可以很方便的与其它库或已有项目整合。
通过尽可能简单的API来实现&lt;strong&gt;响应数据的绑定和组合的视图组件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;易用：在有HTMLCSSJavaScript的基础上，快速上手。&lt;/li&gt;
&lt;li&gt;灵活：简单小巧的核心，渐进式技术栈，足以应付任何规模的应用。&lt;/li&gt;
&lt;li&gt;性能：20kbmin+gzip运行大小、超快虚拟DOM、最省心的优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;基本语法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Vue 核心对象：每一个 Vue 程序都是从一个 Vue 核心对象开始的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let vm = new Vue({
	选项列表;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项列表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;el选项：用于接收获取到页面中的元素（根据常用选择器获取）&lt;/li&gt;
&lt;li&gt;data选项：用于保存当前Vue对象中的数据，在视图中声明的变量需要在此处赋值&lt;/li&gt;
&lt;li&gt;methods选项：用于定义方法，方法可以直接通过对象名调用，this代表当前Vue对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据绑定：在视图部分获取脚本部分的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{遍变量名}}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;!-- 视图 --&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;div&amp;gt;姓名：{{name}}&amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;班级：{{classRoom}}&amp;lt;/div&amp;gt;
        &amp;lt;button onclick=&quot;hi()&quot;&amp;gt;打招呼&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    // 脚本
    let vm = new Vue({
        el:&quot;#div&quot;,
        data:{
            name:&quot;张三&quot;,
            classRoom:&quot;sea程序员&quot;
        },
        methods:{
            study(){
                alert(this.name + &quot;正在&quot; + this.classRoom + &quot;好好学习!&quot;);
            }
        }
    });
    //定义打招呼方法  按一下按钮就弹出
    function hi(){
        vm.study();
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;常用指令&lt;/h2&gt;
&lt;h3&gt;指令介绍&lt;/h3&gt;
&lt;p&gt;指令：是带有 v- 前缀的特殊属性，不同指令具有不同含义&lt;/p&gt;
&lt;p&gt;使用方法：通常编写在标签的属性上，值可以使用 JS 的表达式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue%E6%8C%87%E4%BB%A4%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;文本插值&lt;/h3&gt;
&lt;p&gt;v-html：把文本解析为 HTML 代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;div&amp;gt;{{msg}}&amp;lt;/div&amp;gt;	&amp;lt;!--标签不解析--&amp;gt;
        &amp;lt;div v-html=&quot;msg&quot;&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;!--加粗显示--&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            msg:&quot;&amp;lt;b&amp;gt;Hello Vue&amp;lt;/b&amp;gt;&quot;  
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;绑定属性&lt;/h3&gt;
&lt;p&gt;v-bind：为 HTML 标签绑定属性值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;绑定属性&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        .my{
            border: 1px solid red;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;a v-bind:href=&quot;url&quot;&amp;gt;百度一下&amp;lt;/a&amp;gt; &amp;lt;br&amp;gt;
        &amp;lt;a :href=&quot;url&quot;&amp;gt;百度一下&amp;lt;/a&amp;gt; &amp;lt;br&amp;gt;
        &amp;lt;div :class=&quot;cls&quot;&amp;gt;我是div&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            url:&quot;https://www.baidu.com&quot;,
            cls:&quot;my&quot;
        }
    });
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;条件渲染&lt;/h3&gt;
&lt;p&gt;v-if：条件性的渲染某元素，判定为真时渲染，否则不渲染
v-else：条件性的渲染
v-else-if：条件性的渲染&lt;/p&gt;
&lt;p&gt;v-show：根据条件展示某元素，区别在于切换的是display属性的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;!-- 判断num的值，对3取余  余数为0显示div1  余数为1显示div2  余数为2显示div3 --&amp;gt;
        &amp;lt;div v-if=&quot;num % 3 == 0&quot;&amp;gt;div1&amp;lt;/div&amp;gt;
        &amp;lt;div v-else-if=&quot;num % 3 == 1&quot;&amp;gt;div2&amp;lt;/div&amp;gt;
        &amp;lt;div v-else=&quot;num % 3 == 2&quot;&amp;gt;div3&amp;lt;/div&amp;gt;
        &amp;lt;div v-show=&quot;flag&quot;&amp;gt;div4&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            num:1,
            flag:false
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;列表渲染&lt;/h3&gt;
&lt;p&gt;v-for：列表渲染，遍历容器的元素或者对象的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;ul&amp;gt;
            &amp;lt;li v-for=&quot;name in names&quot;&amp;gt;
                {{name}}
            &amp;lt;/li&amp;gt;
            &amp;lt;li v-for=&quot;value in student&quot;&amp;gt;
                {{value}}
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            names:[&quot;张三&quot;,&quot;李四&quot;,&quot;王五&quot;],
            student:{
                name:&quot;张三&quot;,
                age:23
            }
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;事件绑定&lt;/h3&gt;
&lt;p&gt;v-on：为 HTML 标签绑定事件，有简写方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;div&amp;gt;{{name}}&amp;lt;/div&amp;gt;
        &amp;lt;button v-on:click=&quot;change()&quot;&amp;gt;改变div的内容&amp;lt;/button&amp;gt;  
        &amp;lt;button @click=&quot;change()&quot;&amp;gt;改变div的内容&amp;lt;/button&amp;gt; &amp;lt;!--把sea改成传智播客--&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            name:&quot;sea程序员&quot;
        },
        methods:{
            change(){
                this.name = &quot;传智播客&quot;
            }
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;表单绑定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;表单绑定&lt;/strong&gt;
v-model：在表单元素上创建双向数据绑定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;双向数据绑定&lt;/strong&gt;
更新data数据，页面中的数据也会更新；更新页面数据，data数据也会更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MVVM模型(ModelViewViewModel)：是MVC模式的改进版&lt;/strong&gt;
在前端页面中，JS对象表示Model，页面表示View，两者做到了最大限度的分离。
将Model和View关联起来的就是ViewModel，它是桥梁。
ViewModel负责把Model的数据同步到View显示出来，还负责把View修改的数据同步回Model。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/MVVM%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;form autocomplete=&quot;off&quot;&amp;gt;
            姓名：&amp;lt;input type=&quot;text&quot; name=&quot;username&quot; v-model=&quot;username&quot;&amp;gt; &amp;lt;br&amp;gt;
            年龄：&amp;lt;input type=&quot;number&quot; name=&quot;age&quot; v-model=&quot;age&quot;&amp;gt;
        &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;,
        data:{
            username:&quot;张三&quot;,  //输入框内容从网页更改后，更新为修改后的值
            age:23
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Element&lt;/h2&gt;
&lt;p&gt;Element：网站快速成型工具，是饿了么公司前端开发团队提供的一套基于Vue的网站组件库，使用Element前提必须要有Vue&lt;/p&gt;
&lt;p&gt;组件：组成网页的部件，例如超链接、按钮、图片、表格等等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element官网：https://element.eleme.cn/#/zh-CN&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开发步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载 Element 核心库&lt;/li&gt;
&lt;li&gt;引入 Element 样式文件&lt;/li&gt;
&lt;li&gt;引入 Vue 核心 js 文件&lt;/li&gt;
&lt;li&gt;引入 Element 核心 js 文件&lt;/li&gt;
&lt;li&gt;编写按钮标签&lt;/li&gt;
&lt;li&gt;通过 Vue 核心对象加载元素&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;快速入门&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;element-ui/lib/theme-chalk/index.css&quot;&amp;gt;
    &amp;lt;script src=&quot;js/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;element-ui/lib/index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;button&amp;gt;我是按钮&amp;lt;/button&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;el-row&amp;gt;
            &amp;lt;el-button&amp;gt;默认按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;primary&quot;&amp;gt;主要按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;success&quot;&amp;gt;成功按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;info&quot;&amp;gt;信息按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;warning&quot;&amp;gt;警告按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;danger&quot;&amp;gt;危险按钮&amp;lt;/el-button&amp;gt;
          &amp;lt;/el-row&amp;gt;
          &amp;lt;br&amp;gt;
          &amp;lt;el-row&amp;gt;
            &amp;lt;el-button plain&amp;gt;朴素按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;primary&quot; plain&amp;gt;主要按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;success&quot; plain&amp;gt;成功按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;info&quot; plain&amp;gt;信息按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;warning&quot; plain&amp;gt;警告按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;danger&quot; plain&amp;gt;危险按钮&amp;lt;/el-button&amp;gt;
          &amp;lt;/el-row&amp;gt;
          &amp;lt;br&amp;gt;
          &amp;lt;el-row&amp;gt;
            &amp;lt;el-button round&amp;gt;圆角按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;primary&quot; round&amp;gt;主要按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;success&quot; round&amp;gt;成功按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;info&quot; round&amp;gt;信息按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;warning&quot; round&amp;gt;警告按钮&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;danger&quot; round&amp;gt;危险按钮&amp;lt;/el-button&amp;gt;
          &amp;lt;/el-row&amp;gt;
          &amp;lt;br&amp;gt;
          &amp;lt;el-row&amp;gt;
            &amp;lt;el-button icon=&quot;el-icon-search&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;primary&quot; icon=&quot;el-icon-edit&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;success&quot; icon=&quot;el-icon-check&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;info&quot; icon=&quot;el-icon-message&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;warning&quot; icon=&quot;el-icon-star-off&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
            &amp;lt;el-button type=&quot;danger&quot; icon=&quot;el-icon-delete&quot; circle&amp;gt;&amp;lt;/el-button&amp;gt;
          &amp;lt;/el-row&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el:&quot;#div&quot;
    });
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;自定义&lt;/h2&gt;
&lt;p&gt;对组件的封装&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vue.component(组件名称, {
     props:组件的属性,
     data: 组件的数据函数,
     template: 组件解析的标签模板
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        &amp;lt;my-button&amp;gt;我的按钮&amp;lt;/my-button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    Vue.component(&quot;my-button&quot;,{
        // 属性
        props:[&quot;style&quot;],
        // 数据函数
        data: function(){
            return{
                msg:&quot;我的按钮&quot;
            }
        },
        //解析标签模板
        template:&quot;&amp;lt;button style=&apos;color:red&apos;&amp;gt;{{msg}}&amp;lt;/button&amp;gt;&quot;
    });

    new Vue({
        el:&quot;#div&quot;
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;效果&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;div&quot;&amp;gt;&amp;lt;button style=&quot;color: red;&quot;&amp;gt;我的按钮&amp;lt;/button&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;生命周期&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;生命周期&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生命周期八个阶段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Vue%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9A%84%E5%85%AB%E4%B8%AA%E9%98%B6%E6%AE%B5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;异步操作&lt;/h2&gt;
&lt;p&gt;在Vue中发送异步请求，本质上还是AJAX，使用axios这个插件来简化操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用步骤：
1.引入axios核心js文件
2.调用axios对象的方法来发起异步请求
3.调用axios对象的方法来处理响应的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;axios常用方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;get(请求的资源路径与请求的参数)&lt;/td&gt;
&lt;td&gt;发起GET方式请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;post(请求的资源路径**,** 请求的参数)&lt;/td&gt;
&lt;td&gt;发起POST方式请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;then(response)&lt;/td&gt;
&lt;td&gt;请求成功后的回调函数，通过response获取响应的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;catch(error)&lt;/td&gt;
&lt;td&gt;请求失败后的回调函数，通过error获取错误信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码实现&lt;/p&gt;
&lt;p&gt;Servlet类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebServlet(&quot;/testServlet&quot;)
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp){
        //设置请求响应编码
        req.setCharacterEncoding(&quot;UTF-8&quot;);
        resp.setContentType(&quot;text/html;charset=UTF-8&quot;);

        //获取请求参数
        String name = req.getParameter(&quot;name&quot;);
        System.out.println(name);

        //响应客户端
        resp.getWriter().write(&quot;请求成功&quot;);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp){
        doGet(req, resp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;div&quot;&amp;gt;
        {{name}}
        &amp;lt;button @click=&quot;send()&quot;&amp;gt;发起请求&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    new Vue({
        el: &quot;#div&quot;,
        data:{
            name: &quot;张三&quot;
        },
        methods: {
            send() {
                //GET方式请求
                /*axios.get(&quot;testServlet?name=&quot; + this.name)
                    .then(resp =&amp;gt; {
                        alert(resp.data);
                    })
                    .catch(error =&amp;gt; {
                        alert(error);
                    })*/
                //POST方式请求
                axios.post(&quot;testServlet&quot;, &quot;name=&quot; + this.name)
                    .then(resp =&amp;gt; {
                        alert(resp.data);
                    })
                    .catch(error =&amp;gt; {
                        alert(error);
                    });
            }
        }
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Nginx&lt;/h1&gt;
&lt;h2&gt;安装软件&lt;/h2&gt;
&lt;p&gt;Nginx 是一个高性能的 HTTP 和&lt;a href=&quot;https://baike.baidu.com/item/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/7793488&quot;&gt;反向代理 &lt;/a&gt;Web 服务器，同时也提供了 IMAP/POP3/SMTP 服务&lt;/p&gt;
&lt;p&gt;Nginx 两个最核心的功能：高性能的静态 Web 服务器，反向代理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;安装指令：sudo apt-get install nginx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看版本：nginx -v&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统指令：systemctl / service  start/restart/stop/status nginx&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置文件安装目录：/etc/nginx&lt;/p&gt;
&lt;p&gt;日志文件：/var/log/nginx&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;nginx.conf 文件时 Nginx 的主配置文件&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Nginx配置文件conf.jpg&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;main 部分
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Nginx配置文件main部分.jpg&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;events 部分
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Nginx配置文件events部分.jpg&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;server 部分
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Nginx配置文件server部分.jpg&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;root 设置的路径会拼接上 location 的路径，然后去最终路径寻找对应的文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;发布项目&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建一个 toutiao 目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /home
mkdir toutiao
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将项目上传到 toutiao 目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解压项目 unzip web.zip&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑 Nginx 配置文件 nginx.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
	listen       80;
	server_name  localhost;
	location / {
		root   /home/seazean/toutiao;
		index  index.html index.htm;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启 Nginx 服务：systemctl  restart nginx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器打开网址：http://127.0.0.1:80&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;反向代理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;无法访问 Google，可以配置一个代理服务器，发送请求到代理服务器，代理服务器经过转发，再将请求转发给 Google，返回结果之后，再次转发给用户，这个叫做正向代理，正向代理对于用户来说，是有感知的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;正向代理（forward proxy）&lt;/strong&gt;：是一个位于客户端和目标服务器之间的代理服务器，为了从目标服务器取得内容，客户端向代理服务器发送一个请求并指定目标，然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端，&lt;strong&gt;正向代理，其实是&quot;代理服务器&quot;代理了当前&quot;客户端&quot;，去和&quot;目标服务器&quot;进行交互&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;突破访问限制：通过代理服务器，可以突破自身 IP 访问限制，访问国外网站，教育网等&lt;/li&gt;
&lt;li&gt;提高访问速度：代理服务器都设置一个较大的硬盘缓冲区，会将部分请求的响应保存到缓冲区中，当其他用户再访问相同的信息时， 则直接由缓冲区中取出信息，传给用户，以提高访问速度&lt;/li&gt;
&lt;li&gt;隐藏客户端真实 IP：隐藏自己的 IP，免受攻击&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/正向代理.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;反向代理（reverse proxy）&lt;/strong&gt;：是指以代理服务器来接受 Internet 上的连接请求，然后将请求转发给内部网络上的服务器，并将从服务器上得到的结果返回给 Internet 上请求连接的客户端，此时代理服务器对外就表现为一个反向代理服务器，&lt;strong&gt;反向代理，其实是&quot;代理服务器&quot;代理了&quot;目标服务器&quot;，去和当前&quot;客户端&quot;进行交互&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐藏服务器真实 IP：使用反向代理，可以对客户端隐藏服务器的 IP 地址&lt;/li&gt;
&lt;li&gt;负载均衡：根据所有真实服务器的负载情况，将客户端请求分发到不同的真实服务器上&lt;/li&gt;
&lt;li&gt;提高访问速度：反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务&lt;/li&gt;
&lt;li&gt;提供安全保障：反向代理服务器可以作为应用层防火墙，为网站提供对基于 Web 的攻击行为（例如 DoS/DDoS）的防护，更容易排查恶意软件等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/反向代理.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正向代理其实是客户端的代理，帮助客户端访问其无法访问的服务器资源；反向代理则是服务器的代理，帮助服务器做负载均衡，安全防护等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正向代理一般是客户端架设的，比如在自己的机器上安装一个代理软件；反向代理一般是服务器架设的，比如在自己的机器集群中部署一个反向代理服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正向代理中，服务器不知道真正的客户端到底是谁，以为访问自己的就是真实的客户端；反向代理中，客户端不知道真正的服务器是谁，以为自己访问的就是真实的服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正向代理和反向代理的作用和目的不同。正向代理主要是用来解决访问限制问题；而反向代理则是提供负载均衡、安全防护等作用；二者均能提高访问速度&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>中间件+构建工具合集</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/frame/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/frame/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Maven&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;h3&gt;Mvn介绍&lt;/h3&gt;
&lt;p&gt;Maven：本质是一个项目管理工具，将项目开发和管理过程抽象成一个项目对象模型（POM）&lt;/p&gt;
&lt;p&gt;POM：Project Object Model 项目对象模型。Maven 是用 Java 语言编写的，管理的东西以面向对象的形式进行设计，最终把一个项目看成一个对象，这个对象叫做 POM&lt;/p&gt;
&lt;p&gt;pom.xml：Maven 需要一个  pom.xml 文件，Maven 通过加载这个配置文件可以知道项目的相关信息，这个文件代表就一个项目。如果做 8 个项目，对应的是 8 个 pom.xml 文件&lt;/p&gt;
&lt;p&gt;依赖管理：Maven 对项目所有依赖资源的一种管理，它和项目之间是一种双向关系，即做项目时可以管理所需要的其他资源，当其他项目需要依赖我们项目时，Maven 也会把我们的项目当作一种资源去进行管理。&lt;/p&gt;
&lt;p&gt;管理资源的存储位置：本地仓库，私服，中央仓库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基本作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;项目构建：提供标准的，跨平台的自动化构建项目的方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依赖管理：方便快捷的管理项目依赖的资源（jar 包），避免资源间的版本冲突等问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统一开发结构：提供标准的，统一的项目开发结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E6%A0%87%E5%87%86%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各目录存放资源类型说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;src/main/java：项目 java 源码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/main/resources：项目的相关配置文件（比如 mybatis 配置，xml 映射配置，自定义配置文件等）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/main/webapp：web 资源（比如 html、css、js 等）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/test/java：测试代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/test/resources：测试相关配置文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/pom.xml：项目 pom 文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1Ah411S7ZE&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;基础概念&lt;/h3&gt;
&lt;p&gt;仓库：用于存储资源，主要是各种 jar 包。有本地仓库，私服，中央仓库，私服和中央仓库都是远程仓库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;中央仓库：Maven 团队自身维护的仓库，属于开源的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;私服：各公司/部门等小范围内存储资源的仓库，私服也可以从中央仓库获取资源，作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存具有版权的资源，包含购买或自主研发的 jar&lt;/li&gt;
&lt;li&gt;一定范围内共享资源，能做到仅对内不对外开放&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地仓库：开发者自己电脑上存储资源的仓库，也可从远程仓库获取资源&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;坐标：Maven 中的坐标用于描述仓库中资源的位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;作用：使用唯一标识，唯一性定义资源位置，通过该标识可以将资源的识别与下载工作交由机器完成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://mvnrepository.com：查询 maven 某一个资源的坐标，输入资源名称进行检索&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依赖设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;groupId：定义当前资源隶属组织名称（通常是域名反写，如：org.mybatis）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;artifactId：定义当前资源的名称（通常是项目或模块名称，如：crm、sms）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version：定义当前资源的版本号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;packaging：定义资源的打包方式，取值一般有如下三种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;jar：该资源打成 jar 包，默认是 jar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;war：该资源打成 war 包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom：该资源是一个父资源（表明使用 Maven 分模块管理），打包时只生成一个 pom.xml 不生成 jar 或其他包结构&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;h3&gt;环境配置&lt;/h3&gt;
&lt;p&gt;Maven 的官网：http://maven.apache.org/&lt;/p&gt;
&lt;p&gt;下载安装：Maven 是一个绿色软件，解压即安装&lt;/p&gt;
&lt;p&gt;目录结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bin：可执行程序目录&lt;/li&gt;
&lt;li&gt;boot：Maven 自身的启动加载器&lt;/li&gt;
&lt;li&gt;conf：Maven 配置文件的存放目录&lt;/li&gt;
&lt;li&gt;lib：Maven运行所需库的存放目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置 MAVEN_HOME：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Path 下配置：&lt;code&gt;%MAVEN_HOME%\bin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;环境变量配置好之后需要测试环境配置结果，在 DOS 命令窗口下输入以下命令查看输出：&lt;code&gt;mvn -v&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;仓库配置&lt;/h3&gt;
&lt;p&gt;默认情况 Maven 本地仓库在系统用户目录下的 &lt;code&gt;.m2/repository&lt;/code&gt;，修改 Maven 的配置文件 &lt;code&gt;conf/settings.xml&lt;/code&gt; 来修改仓库位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改本地仓库位置：找到 &amp;lt;localRepository&amp;gt; 标签，修改默认值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- localRepository
| The path to the local repository maven will use to store artifacts.
| Default: ${user.home}/.m2/repository
&amp;lt;localRepository&amp;gt;/path/to/local/repo&amp;lt;/localRepository&amp;gt;
--&amp;gt;
&amp;lt;localRepository&amp;gt;E:\Workspace\Java\Project\.m2\repository&amp;lt;/localRepository&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：在仓库的同级目录即 &lt;code&gt;.m2&lt;/code&gt; 也应该包含一个 &lt;code&gt;settings.xml&lt;/code&gt; 配置文件，局部用户配置优先与全局配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局 setting 定义了 Maven 的公共配置&lt;/li&gt;
&lt;li&gt;用户 setting 定义了当前用户的配置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改远程仓库：在配置文件中找到 &lt;code&gt;&amp;lt;mirrors&amp;gt;&lt;/code&gt; 标签，在这组标签下添加国内镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mirror&amp;gt;
    &amp;lt;id&amp;gt;nexus-aliyun&amp;lt;/id&amp;gt;
    &amp;lt;mirrorOf&amp;gt;central&amp;lt;/mirrorOf&amp;gt;  &amp;lt;!--必须是central--&amp;gt;
    &amp;lt;name&amp;gt;Nexus aliyun&amp;lt;/name&amp;gt;
    &amp;lt;url&amp;gt;http://maven.aliyun.com/nexus/content/groups/public&amp;lt;/url&amp;gt;
&amp;lt;/mirror&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改默认 JDK：在配置文件中找到 &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt; 标签，添加配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;profile&amp;gt; 
    &amp;lt;id&amp;gt;jdk-10&amp;lt;/id&amp;gt; 
    &amp;lt;activation&amp;gt; 
        &amp;lt;activeByDefault&amp;gt;true&amp;lt;/activeByDefault&amp;gt; 
        &amp;lt;jdk&amp;gt;10&amp;lt;/jdk&amp;gt; 
    &amp;lt;/activation&amp;gt;
    &amp;lt;properties&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;10&amp;lt;/maven.compiler.source&amp;gt; 
        &amp;lt;maven.compiler.target&amp;gt;10&amp;lt;/maven.compiler.target&amp;gt;  
    &amp;lt;/properties&amp;gt;  
&amp;lt;/profile&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;项目搭建&lt;/h2&gt;
&lt;h3&gt;手动搭建&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 E 盘下创建目录 mvnproject 进入该目录，作为我们的操作目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建我们的 Maven 项目，创建一个目录 &lt;code&gt;project-java&lt;/code&gt; 作为我们的项目文件夹，并进入到该目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 Java 代码（源代码）所在目录，即创建 &lt;code&gt;src/main/java&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建配置文件所在目录，即创建 &lt;code&gt;src/main/resources&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建测试源代码所在目录，即创建 &lt;code&gt;src/test/java&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建测试存放配置文件存放目录，即 &lt;code&gt;src/test/resources&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;src/main/java&lt;/code&gt; 中创建一个包（注意在 Windos 文件夹下就是创建目录）&lt;code&gt;demo&lt;/code&gt;，在该目录下创建 &lt;code&gt;Demo.java&lt;/code&gt; 文件，作为演示所需 Java 程序，内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package demo;
public class Demo{
	public String say(String name){
		System.out.println(&quot;hello &quot;+name);
		return &quot;hello &quot;+name;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;src/test/java&lt;/code&gt; 中创建一个测试包（目录）&lt;code&gt;demo&lt;/code&gt;，在该包下创建测试程序 &lt;code&gt;DemoTest.java&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package demo;
import org.junit.*;
public class DemoTest{
	@Test
	public void testSay(){
		Demo d = new Demo();
		String ret = d.say(&quot;maven&quot;);
		Assert.assertEquals(&quot;hello maven&quot;,ret);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;project-java/src&lt;/code&gt; 下创建 &lt;code&gt;pom.xml&lt;/code&gt; 文件，格式如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project
    xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 	
                        http://maven.apache.org/maven-v4_0_0.xsd&quot;&amp;gt;
    
    &amp;lt;!--指定pom的模型版本--&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;!--打包方式，web工程打包为war，java工程打包为jar --&amp;gt;
    &amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;
    
    &amp;lt;!--组织id--&amp;gt;
    &amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;
	&amp;lt;!--项目id--&amp;gt;
    &amp;lt;artifactId&amp;gt;project-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;!--版本号:release,snapshot--&amp;gt;
    &amp;lt;version&amp;gt;1.0&amp;lt;/version&amp;gt;
    
    &amp;lt;!--设置当前工程的所有依赖--&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!--具体的依赖--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搭建完成 Maven 的项目结构，通过 Maven 来构建项目。Maven 的构建命令以 &lt;code&gt;mvn&lt;/code&gt; 开头，后面添加功能参数，可以一次性执行多个命令，用空格分离&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mvn compile&lt;/code&gt;：编译&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn clean&lt;/code&gt;：清理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn test&lt;/code&gt;：测试&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn package&lt;/code&gt;：打包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn install&lt;/code&gt;：安装到本地仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：执行某一条命令，则会把前面所有的都执行一遍&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;IDEA搭建&lt;/h3&gt;
&lt;h4&gt;不用原型&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 IDEA 中配置 Maven，选择 maven3.6.1 防止依赖问题
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA配置Maven.png&quot; alt=&quot;IDEA配置Maven&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 Maven，New Module → Maven → 不选中 Create from archetype&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;填写项目的坐标&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GroupId：demo&lt;/li&gt;
&lt;li&gt;ArtifactId：project-java&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看各目录颜色标记是否正确&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E5%88%9B%E5%BB%BAMaven%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IDEA 右侧侧栏有 Maven Project，打开后有 Lifecycle 生命周期&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA-Maven%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义 Maven 命令：Run → Edit Configurations → 左上角 +  → Maven&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E9%85%8D%E7%BD%AEMaven%E5%91%BD%E4%BB%A4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;使用原型&lt;/h4&gt;
&lt;p&gt;普通工程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建 Maven 项目的时候选择使用原型骨架&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E5%88%9B%E5%BB%BAMaven-quickstart.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建完成后发现通过这种方式缺少一些目录，需要手动去补全目录，并且要对补全的目录进行标记&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Web 工程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;选择 Web 对应的原型骨架（选择 Maven 开头的是简化的）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E5%88%9B%E5%BB%BAMaven-webapp.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过原型创建 Web 项目得到的目录结构是不全的，因此需要我们自行补全，同时要标记正确&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Web 工程创建之后需要启动运行，使用 tomcat 插件来运行项目，在 &lt;code&gt;pom.xml&lt;/code&gt; 中添加插件的坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; 		
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; 
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 
                             http://maven.apache.org/maven-v4_0_0.xsd&quot;&amp;gt;

  &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
  &amp;lt;packaging&amp;gt;war&amp;lt;/packaging&amp;gt;

  &amp;lt;name&amp;gt;web01&amp;lt;/name&amp;gt;
  &amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;web01&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

  &amp;lt;dependencies&amp;gt;
  &amp;lt;/dependencies&amp;gt;

  &amp;lt;!--构建--&amp;gt;
  &amp;lt;build&amp;gt;
    &amp;lt;!--设置插件--&amp;gt;
    &amp;lt;plugins&amp;gt;
      &amp;lt;!--具体的插件配置--&amp;gt;
      &amp;lt;plugin&amp;gt;
        &amp;lt;!--https://mvnrepository.com/  搜索--&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.tomcat.maven&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;tomcat7-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1&amp;lt;/version&amp;gt;
        &amp;lt;configuration&amp;gt;
            &amp;lt;port&amp;gt;80&amp;lt;/port&amp;gt; &amp;lt;!--80端口默认不显示--&amp;gt;
            &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
        &amp;lt;/configuration&amp;gt;
      &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
  &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插件配置以后，在 IDEA 右侧 &lt;code&gt;maven-project&lt;/code&gt; 操作面板看到该插件，并且可以利用该插件启动项目，web01 → Plugins → tomcat7 → tomcat7:run&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;依赖管理&lt;/h2&gt;
&lt;h3&gt;依赖配置&lt;/h3&gt;
&lt;p&gt;依赖是指在当前项目中运行所需的 jar，依赖配置的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--设置当前项目所依赖的所有jar--&amp;gt;
&amp;lt;dependencies&amp;gt;
    &amp;lt;!--设置具体的依赖--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;!--依赖所属群组id--&amp;gt;
        &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
        &amp;lt;!--依赖所属项目id--&amp;gt;
        &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
        &amp;lt;!--依赖版本号--&amp;gt;
        &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;依赖传递&lt;/h3&gt;
&lt;p&gt;依赖具有传递性，分两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接依赖：在当前项目中通过依赖配置建立的依赖关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;间接依赖：被依赖的资源如果依赖其他资源，则表明当前项目间接依赖其他资源&lt;/p&gt;
&lt;p&gt;注意：直接依赖和间接依赖其实也是一个相对关系&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;依赖传递的冲突问题：在依赖传递过程中产生了冲突，有三种优先法则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;路径优先：当依赖中出现相同资源时，层级越深，优先级越低，反之则越高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;声明优先：当资源在相同层级被依赖时，配置顺序靠前的覆盖靠后的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特殊优先：当同级配置了相同资源的不同版本时，后配置的覆盖先配置的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;可选依赖&lt;/strong&gt;：对外隐藏当前所依赖的资源，不透明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;    
    &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;    
    &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;    
    &amp;lt;version&amp;gt;4.11&amp;lt;/version&amp;gt;    
    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt; 
    &amp;lt;!--默认是false，true以后就变得不透明--&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;排除依赖&lt;/strong&gt;：主动断开依赖的资源，被排除的资源无需指定版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;    
    &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;    
    &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;    
    &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;    
    &amp;lt;exclusions&amp;gt;        
        &amp;lt;exclusion&amp;gt;            
            &amp;lt;groupId&amp;gt;org.hamcrest&amp;lt;/groupId&amp;gt;  &amp;lt;!--排除这个资源--&amp;gt;            
            &amp;lt;artifactId&amp;gt;hamcrest-core&amp;lt;/artifactId&amp;gt;        
        &amp;lt;/exclusion&amp;gt;    
    &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;依赖范围&lt;/h3&gt;
&lt;p&gt;依赖的 jar 默认情况可以在任何地方可用，可以通过 &lt;code&gt;scope&lt;/code&gt; 标签设定其作用范围，有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主程序范围有效（src/main 目录范围内）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试程序范围内有效（src/test 目录范围内）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否参与打包（package 指令范围内）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;scope&lt;/code&gt; 标签的取值有四种：&lt;code&gt;compile,test,provided,runtime&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;依赖范围的传递性：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4%E7%9A%84%E4%BC%A0%E9%80%92%E6%80%A7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;生命周期&lt;/h2&gt;
&lt;h3&gt;相关事件&lt;/h3&gt;
&lt;p&gt;Maven 的构建生命周期描述的是一次构建过程经历了多少个事件&lt;/p&gt;
&lt;p&gt;最常用的一套流程：compile → test-compile → test → package → install&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;clean：清理工作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pre-clean：执行一些在 clean 之前的工作&lt;/li&gt;
&lt;li&gt;clean：移除上一次构建产生的所有文件&lt;/li&gt;
&lt;li&gt;post-clean：执行一些在 clean 之后立刻完成的工作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;default：核心工作，例如编译，测试，打包，部署等，每个事件在执行之前都会&lt;strong&gt;将之前的所有事件依次执行一遍&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven-default%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;site：产生报告，发布站点等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pre-site：执行一些在生成站点文档之前的工作&lt;/li&gt;
&lt;li&gt;site：生成项目的站点文档&lt;/li&gt;
&lt;li&gt;post-site：执行一些在生成站点文档之后完成的工作，并为部署做准备&lt;/li&gt;
&lt;li&gt;site-deploy：将生成的站点文档部署到特定的服务器上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;执行事件&lt;/h3&gt;
&lt;p&gt;Maven 的插件用来执行生命周期中的相关事件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;插件与生命周期内的阶段绑定，在执行到对应生命周期时执行对应的插件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Maven 默认在各个生命周期上都绑定了预先设定的插件来完成相应功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插件还可以完成一些自定义功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;build&amp;gt;    
    &amp;lt;plugins&amp;gt;        
        &amp;lt;plugin&amp;gt;           
            &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;           
            &amp;lt;artifactId&amp;gt;maven-source-plugin&amp;lt;/artifactId&amp;gt;           
            &amp;lt;version&amp;gt;2.2.1&amp;lt;/version&amp;gt;           
            &amp;lt;!--执行--&amp;gt;          
            &amp;lt;excutions&amp;gt;               
                &amp;lt;!--具体执行位置--&amp;gt;              
                &amp;lt;excution&amp;gt;                   
                    &amp;lt;goals&amp;gt;                      
                        &amp;lt;!--对源码进行打包，打包放在target目录--&amp;gt;                    	
                        &amp;lt;goal&amp;gt;jar&amp;lt;/goal&amp;gt;                      
                        &amp;lt;!--对测试代码进行打包--&amp;gt;                       
                        &amp;lt;goal&amp;gt;test-jar&amp;lt;/goal&amp;gt;                 
                    &amp;lt;/goals&amp;gt;                  
                    &amp;lt;!--执行的生命周期--&amp;gt;                 
                    &amp;lt;phase&amp;gt;generate-test-resources&amp;lt;/phase&amp;gt;                 
                &amp;lt;/excution&amp;gt;         
            &amp;lt;/excutions&amp;gt;       
        &amp;lt;/plugin&amp;gt;   
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;模块开发&lt;/h2&gt;
&lt;h3&gt;拆分&lt;/h3&gt;
&lt;p&gt;工程模块与模块划分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E6%A8%A1%E5%9D%97%E5%88%92%E5%88%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ssm_pojo 拆分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建模块，拷贝原始项目中对应的相关内容到 ssm_pojo 模块中&lt;/li&gt;
&lt;li&gt;实体类（User）&lt;/li&gt;
&lt;li&gt;配置文件（无）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ssm_dao 拆分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建模块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝原始项目中对应的相关内容到 ssm_dao 模块中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据层接口（UserDao）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件：保留与数据层相关配置文件(3 个）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意：分页插件在配置中与 SqlSessionFactoryBean 绑定，需要保留&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom.xml：引入数据层相关坐标即可，删除 SpringMVC 相关坐标&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring&lt;/li&gt;
&lt;li&gt;MyBatis&lt;/li&gt;
&lt;li&gt;Spring 整合 MyBatis&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;druid&lt;/li&gt;
&lt;li&gt;pagehelper&lt;/li&gt;
&lt;li&gt;直接依赖 ssm_pojo（对 ssm_pojo 模块执行 install 指令，将其安装到本地仓库）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;    &amp;lt;!--导入资源文件pojo--&amp;gt;    
    &amp;lt;dependency&amp;gt;       
        &amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;        
        &amp;lt;artifactId&amp;gt;ssm_pojo&amp;lt;/artifactId&amp;gt;      
        &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;  
    &amp;lt;!--spring环境--&amp;gt;   
    &amp;lt;!--mybatis环境--&amp;gt;  
    &amp;lt;!--mysql环境--&amp;gt;  
    &amp;lt;!--spring整合jdbc--&amp;gt;   
    &amp;lt;!--spring整合mybatis--&amp;gt;  
    &amp;lt;!--druid连接池--&amp;gt;  
    &amp;lt;!--分页插件坐标--&amp;gt;   
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ssm_service 拆分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建模块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝原始项目中对应的相关内容到 ssm_service 模块中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;业务层接口与实现类（UserService、UserServiceImpl）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件：保留与数据层相关配置文件(1 个）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom.xml：引入数据层相关坐标即可，删除 SpringMVC 相关坐标&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;spring&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;junit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;spring 整合 junit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接依赖 ssm_dao（对 ssm_dao 模块执行 install 指令，将其安装到本地仓库）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;间接依赖 ssm_pojo（由 ssm_dao 模块负责依赖关系的建立）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 service 模块 Spring 核心配置文件名，添加模块名称，格式：applicationContext-service.xml&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 dao 模块 Spring 核心配置文件名，添加模块名称，格式：applicationContext-dao.xml&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改单元测试引入的配置文件名称，由单个文件修改为多个文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ssm_control 拆分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建模块（使用 webapp 模板）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝原始项目中对应的相关内容到 ssm_controller 模块中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;现层控制器类与相关设置类（UserController、异常相关……）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件：保留与表现层相关配置文件(1 个）、服务器相关配置文件（1 个）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom.xml：引入数据层相关坐标即可，删除 SpringMVC 相关坐标&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;spring&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;springmvc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jackson&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;servlet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tomcat 服务器插件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接依赖 ssm_service（对 ssm_service 模块执行 install 指令，将其安装到本地仓库）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;间接依赖 ssm_dao、ssm_pojo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;   
    &amp;lt;!--导入资源文件service--&amp;gt;    
    &amp;lt;dependency&amp;gt;      
        &amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;         
        &amp;lt;artifactId&amp;gt;ssm_service&amp;lt;/artifactId&amp;gt;   
        &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;     
    &amp;lt;/dependency&amp;gt;  
    &amp;lt;!--springmvc环境--&amp;gt;  
    &amp;lt;!--jackson相关坐标3个--&amp;gt; 
    &amp;lt;!--servlet环境--&amp;gt; 
&amp;lt;/dependencies&amp;gt; 
&amp;lt;build&amp;gt;   
    &amp;lt;!--设置插件--&amp;gt;    
    &amp;lt;plugins&amp;gt;    
        &amp;lt;!--具体的插件配置--&amp;gt;  
        &amp;lt;plugin&amp;gt;
        &amp;lt;/plugin&amp;gt; 
    &amp;lt;/plugins&amp;gt; 
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 web.xml 配置文件中加载 Spring 环境的配置文件名称，使用*通配，加载所有 applicationContext- 开始的配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--加载配置文件--&amp;gt;
&amp;lt;context-param&amp;gt;   
    &amp;lt;param-name&amp;gt;contextConfigLocation&amp;lt;/param-name&amp;gt;  
    &amp;lt;param-value&amp;gt;classpath*:applicationContext-*.xml&amp;lt;/param-value&amp;gt;
&amp;lt;/context-param&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;spring-mvc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mvc:annotation-driven/&amp;gt;&amp;lt;context:component-scan base-package=&quot;controller&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;聚合&lt;/h3&gt;
&lt;p&gt;作用：聚合用于快速构建 Maven 工程，一次性构建多个项目/模块&lt;/p&gt;
&lt;p&gt;制作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个空模块，打包类型定义为 pom&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义当前模块进行构建操作时关联的其他模块名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&amp;lt;project xmlns=&quot;............&quot;&amp;gt;   
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;   
    &amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;  
    &amp;lt;artifactId&amp;gt;ssm&amp;lt;/artifactId&amp;gt; 
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;!--定义该工程用于构建管理--&amp;gt;  
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;   
    &amp;lt;!--管理的工程列表--&amp;gt;   
    &amp;lt;modules&amp;gt;       
        &amp;lt;!--具体的工程名称--&amp;gt;     
        &amp;lt;module&amp;gt;../ssm_pojo&amp;lt;/module&amp;gt;   
        &amp;lt;module&amp;gt;../ssm_dao&amp;lt;/module&amp;gt;  
        &amp;lt;module&amp;gt;../ssm_service&amp;lt;/module&amp;gt;
        &amp;lt;module&amp;gt;../ssm_controller&amp;lt;/module&amp;gt;   
    &amp;lt;/modules&amp;gt;&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：参与聚合操作的模块最终执行顺序与模块间的依赖关系有关，与配置顺序无关&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;继承&lt;/h3&gt;
&lt;p&gt;Maven 中的继承与 Java 中的继承相似，可以实现在子工程中沿用父工程中的配置&lt;/p&gt;
&lt;p&gt;dependencyManagement 里只是声明依赖，并不实现引入，所以子工程需要显式声明需要用的依赖&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果子工程中未声明依赖，则不会从父项目继承下来&lt;/li&gt;
&lt;li&gt;在子工程中声明该依赖项，并且不指定具体版本，才会从父项目中继承该项，version 和 scope 都继承取自父工程 pom 文件&lt;/li&gt;
&lt;li&gt;如果子工程中指定了版本号，那么使用子工程中指定的 jar 版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;制作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在子工程中声明其父工程坐标与对应的位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--定义该工程的父工程--&amp;gt;
&amp;lt;parent&amp;gt;   
    &amp;lt;groupId&amp;gt;com.seazean&amp;lt;/groupId&amp;gt;  
    &amp;lt;artifactId&amp;gt;ssm&amp;lt;/artifactId&amp;gt;   
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;  
    &amp;lt;!--填写父工程的pom文件--&amp;gt;   
    &amp;lt;relativePath&amp;gt;../ssm/pom.xml&amp;lt;/relativePath&amp;gt;
&amp;lt;/parent&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承依赖的定义：在父工程中定义依赖管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--声明此处进行依赖管理，版本锁定--&amp;gt;
&amp;lt;dependencyManagement&amp;gt;   
    &amp;lt;!--具体的依赖--&amp;gt;    
    &amp;lt;dependencies&amp;gt;      
        &amp;lt;!--spring环境--&amp;gt;   
        &amp;lt;dependency&amp;gt;       
            &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;   
            &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;  
            &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;      
        &amp;lt;/dependency&amp;gt;     
        &amp;lt;!--等等所有--&amp;gt;  
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承依赖的使用：在子工程中定义依赖关系，&lt;strong&gt;无需声明依赖版本&lt;/strong&gt;，版本参照父工程中依赖的版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt; 
    &amp;lt;!--spring环境--&amp;gt;  
    &amp;lt;dependency&amp;gt;    
        &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;    
        &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;  
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承的资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groupId：项目组ID，项目坐标的核心元素
version：项目版本，项目坐标的核心因素
description：项目的描述信息
organization：项目的组织信息
inceptionYear：项目的创始年份
url：项目的URL地址
developers：项目的开发者信息
contributors：项目的贡献者信息
distributionManagement：项目的部署配置
issueManagement：项目的缺陷跟踪系统信息
ciManagement：项目的持续集成系统信息
scm：项目的版本控制系统信息
malilingLists：项目的邮件列表信息
properties：自定义的Maven属性
dependencies：项目的依赖配置
dependencyManagement：项目的依赖管理配置
repositories：项目的仓库配置
build：包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
reporting：包括项目的报告输出目录配置、报告插件配置等
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承与聚合：&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合用于快速构建项目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承用于快速配置&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合与继承的 pom.xml 文件打包方式均为 pom，可以将两种关系制作到同一个 pom 文件中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;聚合与继承均属于设计型模块，并无实际的模块内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合是在当前模块中配置关系，聚合可以感知到参与聚合的模块有哪些&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承是在子模块中配置关系，父模块无法感知哪些子模块继承了自己&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;属性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;版本统一的重要性：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E7%89%88%E6%9C%AC%E7%BB%9F%E4%B8%80%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性类别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自定义属性&lt;/li&gt;
&lt;li&gt;内置属性&lt;/li&gt;
&lt;li&gt;setting 属性&lt;/li&gt;
&lt;li&gt;Java 系统属性&lt;/li&gt;
&lt;li&gt;环境变量属性&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义属性：&lt;/p&gt;
&lt;p&gt;作用：等同于定义变量，方便统一维护&lt;/p&gt;
&lt;p&gt;定义格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--定义自定义属性，放在dependencyManagement上方--&amp;gt;
&amp;lt;properties&amp;gt;    
    &amp;lt;spring.version&amp;gt;5.1.9.RELEASE&amp;lt;/spring.version&amp;gt;    
    &amp;lt;junit.version&amp;gt;4.12&amp;lt;/junit.version&amp;gt;
&amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合与继承的 pom.xml 文件打包方式均为 pom，可以将两种关系制作到同一个 pom 文件中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;聚合与继承均属于设计型模块，并无实际的模块内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;    
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;    
    &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;    
    &amp;lt;version&amp;gt;${spring.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内置属性：&lt;/p&gt;
&lt;p&gt;作用：使用 Maven 内置属性，快速配置&lt;/p&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${project.basedir} or ${project.basedir}  &amp;lt;!--../ssm根目录--&amp;gt;${version} or ${project.version}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;vresion 是 1.0-SNAPSHOT&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;ssm&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;setting 属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Maven 配置文件 setting.xml 中的标签属性，用于动态配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${settings.localRepository} 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 系统属性：&lt;/p&gt;
&lt;p&gt;作用：读取 Java 系统属性&lt;/p&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${user.home}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统属性查询方式 cmd 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn help:system 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环境变量属性&lt;/p&gt;
&lt;p&gt;作用：使用 Maven 配置文件 setting.xml 中的标签属性，用于动态配置&lt;/p&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${env.JAVA_HOME} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;环境变量属性查询方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn help:system 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;工程版本&lt;/h3&gt;
&lt;p&gt;SNAPSHOT（快照版本）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;项目开发过程中，为方便团队成员合作，解决模块间相互依赖和时时更新的问题，开发者对每个模块进行构建的时候，输出的临时性版本叫快照版本（测试阶段版本）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快照版本会随着开发的进展不断更新&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RELEASE（发布版本）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目开发到进入阶段里程碑后，向团队外部发布较为稳定的版本，这种版本所对应的构件文件是稳定的，即便进行功能的后续开发，也不会改变当前发布版本内容，这种版本称为发布版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;约定规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;主版本&amp;gt;.&amp;lt;次版本&amp;gt;.&amp;lt;增量版本&amp;gt;.&amp;lt;里程碑版本&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主版本：表示项目重大架构的变更，如：Spring5 相较于 Spring4 的迭代&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;次版本：表示有较大的功能增加和变化，或者全面系统地修复漏洞&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增量版本：表示有重大漏洞的修复&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;里程碑版本：表明一个版本的里程碑（版本内部）。这样的版本同下一个正式版本相比，相对来说不是很稳定，有待更多的测试&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;资源配置&lt;/h3&gt;
&lt;p&gt;作用：在任意配置文件中加载 pom 文件中定义的属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;父文件 pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;properties&amp;gt;    
    &amp;lt;jdbc.url&amp;gt;jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false&amp;lt;/jdbc.url&amp;gt;&amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开启配置文件加载 pom 属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置资源文件对应的信息--&amp;gt;
&amp;lt;resources&amp;gt;  
    &amp;lt;resource&amp;gt;   
        &amp;lt;!--设定配置文件对应的位置目录，支持使用属性动态设定路径--&amp;gt;     
        &amp;lt;directory&amp;gt;${project.basedir}/src/main/resources&amp;lt;/directory&amp;gt; 
        &amp;lt;!--开启对配置文件的资源加载过滤--&amp;gt;  
        &amp;lt;filtering&amp;gt;true&amp;lt;/filtering&amp;gt;   
    &amp;lt;/resource&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;properties 文件中调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jdbc.driver=com.mysql.jdbc.Driverjdbc.url=${jdbc.url}
jdbc.username=rootjdbc.password=123456
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;多环境配置&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环境配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--创建多环境--&amp;gt;
&amp;lt;profiles&amp;gt;   
    &amp;lt;!--定义具体的环境：生产环境--&amp;gt;  
    &amp;lt;profile&amp;gt;     
        &amp;lt;!--定义环境对应的唯一名称--&amp;gt;        
        &amp;lt;id&amp;gt;pro_env&amp;lt;/id&amp;gt;    
        &amp;lt;!--定义环境中专用的属性值--&amp;gt;    
        &amp;lt;properties&amp;gt;           
            &amp;lt;jdbc.url&amp;gt;jdbc:mysql://127.1.1.1:3306/ssm_db&amp;lt;/jdbc.url&amp;gt;     
        &amp;lt;/properties&amp;gt;     
        &amp;lt;!--设置默认启动--&amp;gt;    
        &amp;lt;activation&amp;gt;      
            &amp;lt;activeByDefault&amp;gt;true&amp;lt;/activeByDefault&amp;gt; 
        &amp;lt;/activation&amp;gt;
    &amp;lt;/profile&amp;gt;   
    &amp;lt;!--定义具体的环境：开发环境--&amp;gt;   
    &amp;lt;profile&amp;gt;     
        &amp;lt;id&amp;gt;dev_env&amp;lt;/id&amp;gt;
        ……  
    &amp;lt;/profile&amp;gt;
&amp;lt;/profiles&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加载指定环境&lt;/p&gt;
&lt;p&gt;作用：加载指定环境配置&lt;/p&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn 指令 –P 环境定义id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn install –P pro_env
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;跳过测试&lt;/h2&gt;
&lt;p&gt;命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn 指令 –D skipTests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：执行的指令生命周期必须包含测试环节&lt;/p&gt;
&lt;p&gt;IEDA 界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E4%BD%BF%E7%94%A8%E7%95%8C%E9%9D%A2%E6%93%8D%E4%BD%9C%E8%B7%B3%E8%BF%87%E6%B5%8B%E8%AF%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配置跳过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;plugin&amp;gt;  
    &amp;lt;!--&amp;lt;groupId&amp;gt;org.apache.maven&amp;lt;/groupId&amp;gt;--&amp;gt;  
    &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt; 
    &amp;lt;version&amp;gt;2.22.1&amp;lt;/version&amp;gt; 
    &amp;lt;configuration&amp;gt;     
        &amp;lt;skipTests&amp;gt;true&amp;lt;/skipTests&amp;gt;&amp;lt;!--设置跳过测试--&amp;gt; 
        &amp;lt;includes&amp;gt; &amp;lt;!--包含指定的测试用例--&amp;gt;      
            &amp;lt;include&amp;gt;**/User*Test.java&amp;lt;/include&amp;gt;    
        &amp;lt;/includes&amp;gt;    
        &amp;lt;excludes&amp;gt;&amp;lt;!--排除指定的测试用例--&amp;gt;      
            &amp;lt;exclude&amp;gt;**/User*TestCase.java&amp;lt;/exclude&amp;gt;   
        &amp;lt;/excludes&amp;gt;   
    &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;私服&lt;/h2&gt;
&lt;h3&gt;Nexus&lt;/h3&gt;
&lt;p&gt;Nexus 是 Sonatype 公司的一款 Maven 私服产品&lt;/p&gt;
&lt;p&gt;下载地址：https://help.sonatype.com/repomanager3/download&lt;/p&gt;
&lt;p&gt;启动服务器（命令行启动）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nexus.exe /run nexus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问服务器（默认端口：8081）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改基础配置信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装路径下 etc 目录中 nexus-default.properties 文件保存有 nexus 基础配置信息，例如默认访问端口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修改服务器运行配置信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装路径下 bin 目录中 nexus.vmoptions 文件保存有 nexus 服务器启动的配置信息，例如默认占用内存空间&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;资源操作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven%E7%A7%81%E6%9C%8D%E8%B5%84%E6%BA%90%E8%8E%B7%E5%8F%96.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;仓库分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;宿主仓库 hosted&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存无法从中央仓库获取的资源
&lt;ul&gt;
&lt;li&gt;自主研发&lt;/li&gt;
&lt;li&gt;第三方非开源项目&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代理仓库 proxy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理远程仓库，通过 nexus 访问其他公共仓库，例如中央仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仓库组 group&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将若干个仓库组成一个群组，简化配置&lt;/li&gt;
&lt;li&gt;仓库组不能保存资源，属于设计型仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;资源上传，上传资源时提供对应的信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;保存的位置（宿主仓库）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;资源文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对应坐标&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;IDEA操作&lt;/h3&gt;
&lt;h4&gt;上传下载&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA%E7%8E%AF%E5%A2%83%E4%B8%AD%E8%B5%84%E6%BA%90%E4%B8%8A%E4%BC%A0%E4%B8%8E%E4%B8%8B%E8%BD%BD.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;访问私服&lt;/h4&gt;
&lt;h5&gt;本地访问&lt;/h5&gt;
&lt;p&gt;配置本地仓库访问私服的权限（setting.xml）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;servers&amp;gt;  
    &amp;lt;server&amp;gt;     
        &amp;lt;id&amp;gt;heima-release&amp;lt;/id&amp;gt;      
        &amp;lt;username&amp;gt;admin&amp;lt;/username&amp;gt; 
        &amp;lt;password&amp;gt;admin&amp;lt;/password&amp;gt;   
    &amp;lt;/server&amp;gt;  
    &amp;lt;server&amp;gt;   
        &amp;lt;id&amp;gt;heima-snapshots&amp;lt;/id&amp;gt;  
        &amp;lt;username&amp;gt;admin&amp;lt;/username&amp;gt;   
        &amp;lt;password&amp;gt;admin&amp;lt;/password&amp;gt;  
    &amp;lt;/server&amp;gt;
&amp;lt;/servers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置本地仓库资源来源（setting.xml）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mirrors&amp;gt; 
    &amp;lt;mirror&amp;gt;  
        &amp;lt;id&amp;gt;nexus-heima&amp;lt;/id&amp;gt;  
        &amp;lt;mirrorOf&amp;gt;*&amp;lt;/mirrorOf&amp;gt;    
        &amp;lt;url&amp;gt;http://localhost:8081/repository/maven-public/&amp;lt;/url&amp;gt; 
    &amp;lt;/mirror&amp;gt;
&amp;lt;/mirrors&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;工程访问&lt;/h5&gt;
&lt;p&gt;配置当前项目访问私服上传资源的保存位置（pom.xml）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;distributionManagement&amp;gt; 
    &amp;lt;repository&amp;gt;    
        &amp;lt;id&amp;gt;heima-release&amp;lt;/id&amp;gt;      
        &amp;lt;url&amp;gt;http://localhost:8081/repository/heima-release/&amp;lt;/url&amp;gt; 
    &amp;lt;/repository&amp;gt;
    &amp;lt;snapshotRepository&amp;gt;   
        &amp;lt;id&amp;gt;heima-snapshots&amp;lt;/id&amp;gt; 
        &amp;lt;url&amp;gt;http://localhost:8081/repository/heima-snapshots/&amp;lt;/url&amp;gt; 
    &amp;lt;/snapshotRepository&amp;gt;
&amp;lt;/distributionManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布资源到私服命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;日志&lt;/h2&gt;
&lt;h3&gt;Log4j&lt;/h3&gt;
&lt;p&gt;程序中的日志可以用来记录程序在运行时候的详情，并可以进行永久存储。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;输出语句&lt;/th&gt;
&lt;th&gt;日志技术&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;取消日志&lt;/td&gt;
&lt;td&gt;需要修改代码，灵活性比较差&lt;/td&gt;
&lt;td&gt;不需要修改代码，灵活性比较好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出位置&lt;/td&gt;
&lt;td&gt;只能是控制台&lt;/td&gt;
&lt;td&gt;可以将日志信息写入到文件或者数据库中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多线程&lt;/td&gt;
&lt;td&gt;和业务代码处于一个线程中&lt;/td&gt;
&lt;td&gt;多线程方式记录日志，不影响业务代码的性能&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Log4j 是 Apache 的一个开源项目。使用 Log4j，通过一个配置文件来灵活地进行配置，而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置，也可以控制每一条日志的输出格式。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/日志体系结构.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;配置文件&lt;/h3&gt;
&lt;p&gt;配置文件的三个核心：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置根 Logger&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;格式：log4j.rootLogger=日志级别，appenderName1，appenderName2，…&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;日志级别：常见的五个级别：&lt;strong&gt;DEBUG &amp;lt; INFO &amp;lt; WARN &amp;lt; ERROR &amp;lt; FATAL&lt;/strong&gt;（可以自定义）
Log4j 规则：只输出级别不低于设定级别的日志信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;appenderName1：指定日志信息要输出地址。可以同时指定多个输出目的地，用逗号隔开：&lt;/p&gt;
&lt;p&gt;例如：log4j.rootLogger＝INFO，ca，fa&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Appenders（输出源）：日志要输出的地方，如控制台（Console）、文件（Files）等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Appenders 取值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;org.apache.log4j.ConsoleAppender（控制台）&lt;/li&gt;
&lt;li&gt;org.apache.log4j.FileAppender（文件）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ConsoleAppender 常用参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ImmediateFlush=true&lt;/code&gt;：表示所有消息都会被立即输出，设为 false 则不输出，默认值是 true&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Target=System.err&lt;/code&gt;：默认值是 System.out&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FileAppender常用的选项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ImmediateFlush=true&lt;/code&gt;：表示所有消息都会被立即输出。设为 false 则不输出，默认值是 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Append=false&lt;/code&gt;：true 表示将消息添加到指定文件中，原来的消息不覆盖。默认值是 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File=E:/logs/logging.log4j&lt;/code&gt;：指定消息输出到 logging.log4j 文件中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Layouts (布局)：日志输出的格式，常用的布局管理器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;org.apache.log4j.PatternLayout（可以灵活地指定布局模式）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;org.apache.log4j.SimpleLayout（包含日志信息的级别和信息字符串）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;org.apache.log4j.TTCCLayout（包含日志产生的时间、线程、类别等信息）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PatternLayout 常用的选项
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/日志-PatternLayout常用的选项.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;日志应用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;log4j 的配置文件,名字为 log4j.properties, 放在 src 根目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log4j.rootLogger=I

### direct log messages to my ###
log4j.appender.my=org.apache.log4j.ConsoleAppender
log4j.appender.my.ImmediateFlush = true
log4j.appender.my.Target=System.out
log4j.appender.my.layout=org.apache.log4j.PatternLayout
log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n

# fileAppender演示
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.ImmediateFlush = true
log4j.appender.fileAppender.Append=true
log4j.appender.fileAppender.File=E:/log4j-log.log
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 测试类
public class Log4JTest01 {    
    //使用log4j的api来获取日志的对象   
    //弊端：如果以后我们更换日志的实现类，那么下面的代码就需要跟着改   
    //不推荐使用   
    //private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class);    
    //使用slf4j里面的api来获取日志的对象 
    //好处：如果以后我们更换日志的实现类，那么下面的代码不需要跟着修改   
    //推荐使用    
    private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class);  
    public static void main(String[] args) {        
        //1.导入jar包        
        //2.编写配置文件        
        //3.在代码中获取日志的对象        
        //4.按照日志级别设置日志信息        
        LOGGER.debug(&quot;debug级别的日志&quot;);        
        LOGGER.info(&quot;info级别的日志&quot;);        
        LOGGER.warn(&quot;warn级别的日志&quot;);        
        LOGGER.error(&quot;error级别的日志&quot;);    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Netty&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;p&gt;Netty 是一个异步事件驱动的网络应用程序框架，用于快速开发可维护、高性能的网络服务器和客户端&lt;/p&gt;
&lt;p&gt;Netty 官网：https://netty.io/&lt;/p&gt;
&lt;p&gt;Netty 的对 JDK 自带的 NIO 的 API 进行封装，解决上述问题，主要特点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设计优雅，适用于各种传输类型的统一 API， 阻塞和非阻塞 Socket 基于灵活且可扩展的事件模型&lt;/li&gt;
&lt;li&gt;使用方便，详细记录的 Javadoc、用户指南和示例，没有其他依赖项&lt;/li&gt;
&lt;li&gt;高性能，吞吐量更高，延迟更低，减少资源消耗，最小化不必要的内存复制&lt;/li&gt;
&lt;li&gt;安全，完整的 SSL/TLS 和 StartTLS 支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Netty 的功能特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传输服务：支持 BIO 和 NIO&lt;/li&gt;
&lt;li&gt;容器集成：支持 OSGI、JBossMC、Spring、Guice 容器&lt;/li&gt;
&lt;li&gt;协议支持：HTTP、Protobuf、二进制、文本、WebSocket 等一系列协议都支持，也支持通过实行编码解码逻辑来实现自定义协议&lt;/li&gt;
&lt;li&gt;Core 核心：可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-功能特性.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;线程模型&lt;/h2&gt;
&lt;h3&gt;阻塞模型&lt;/h3&gt;
&lt;p&gt;传统阻塞型 I/O 模式，每个连接都需要独立的线程完成数据的输入，业务处理，数据返回&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-传统阻塞IO服务模型.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;模型缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当并发数较大时，需要创建大量线程来处理连接，系统资源占用较大&lt;/li&gt;
&lt;li&gt;连接建立后，如果当前线程暂时没有数据可读，则线程就阻塞在 read 操作上，造成线程资源浪费&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/2965fca6bb8f&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Reactor&lt;/h3&gt;
&lt;h4&gt;设计思想&lt;/h4&gt;
&lt;p&gt;Reactor 模式，通过一个或多个输入同时传递给服务处理器的&lt;strong&gt;事件驱动处理模式&lt;/strong&gt;。 服务端程序处理传入的多路请求，并将它们同步分派给对应的处理线程，Reactor 模式也叫 Dispatcher 模式，即 I/O 多路复用统一监听事件，收到事件后分发（Dispatch 给某线程）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I/O 复用结合线程池&lt;/strong&gt;，就是 Reactor 模式基本设计思想：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-Reactor模型.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Reactor 模式关键组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reactor：在一个单独的线程中运行，负责&lt;strong&gt;监听和分发事件&lt;/strong&gt;，分发给适当的处理程序来对 I/O 事件做出反应&lt;/li&gt;
&lt;li&gt;Handler：处理程序执行 I/O 要完成的实际事件，Reactor 通过调度适当的处理程序来响应 I/O 事件，处理程序执行&lt;strong&gt;非阻塞操作&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reactor 模式具有如下的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;响应快，不必为单个同步时间所阻塞，虽然 Reactor 本身依然是同步的&lt;/li&gt;
&lt;li&gt;编程相对简单，可以最大程度的避免复杂的多线程及同步问题，并且避免了多线程/进程的切换开销&lt;/li&gt;
&lt;li&gt;可扩展性，可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源&lt;/li&gt;
&lt;li&gt;可复用性，Reactor 模型本身与具体事件处理逻辑无关，具有很高的复用性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据 Reactor 的数量和处理资源池线程的数量不同，有三种典型的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单 Reactor 单线程&lt;/li&gt;
&lt;li&gt;单 Reactor 多线程&lt;/li&gt;
&lt;li&gt;主从 Reactor 多线程&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;单R单线程&lt;/h4&gt;
&lt;p&gt;Reactor 对象通过 select 监控客户端请求事件，收到事件后通过 dispatch 进行分发：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果是建立连接请求事件，则由 Acceptor 通过 accept 处理连接请求，然后创建一个 Handler 对象处理连接完成后的后续业务处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不是建立连接事件，则 Reactor 会分发给连接对应的 Handler 来响应，Handler 会完成 read、业务处理、send 的完整流程&lt;/p&gt;
&lt;p&gt;说明：&lt;strong&gt;Handler 和 Acceptor 属于同一个线程&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-单Reactor单线程.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;模型优点：模型简单，没有多线程、进程通信、竞争的问题，全部都在一个线程中完成&lt;/p&gt;
&lt;p&gt;模型缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性能问题：只有一个线程，无法发挥多核 CPU 的性能，Handler 在处理某个连接上的业务时，整个进程无法处理其他连接事件，很容易导致性能瓶颈&lt;/li&gt;
&lt;li&gt;可靠性问题：线程意外跑飞，或者进入死循环，会导致整个系统通信模块不可用，不能接收和处理外部消息，造成节点故障&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用场景：客户端的数量有限，业务处理非常快速，比如 Redis，业务处理的时间复杂度 O(1)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;单R多线程&lt;/h4&gt;
&lt;p&gt;执行流程通同单 Reactor 单线程，不同的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Handler 只负责响应事件，不做具体业务处理，通过 read 读取数据后，会分发给后面的 Worker 线程池进行业务处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Worker 线程池会分配独立的线程完成真正的业务处理，将响应结果发给 Handler 进行处理，最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-单Reactor多线程.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;模型优点：可以充分利用多核 CPU 的处理能力&lt;/p&gt;
&lt;p&gt;模型缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多线程数据共享和访问比较复杂&lt;/li&gt;
&lt;li&gt;Reactor 承担所有事件的监听和响应，在单线程中运行，高并发场景下容易成为性能瓶颈&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;主从模型&lt;/h4&gt;
&lt;p&gt;采用多个 Reactor ，执行流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Reactor 主线程 MainReactor 通过 select &lt;strong&gt;监控建立连接事件&lt;/strong&gt;，收到事件后通过 Acceptor 接收，处理建立连接事件，处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor（有多个）处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SubReactor 将连接加入连接队列进行监听其他事件，并创建一个 Handler 用于处理该连接的事件，当有新的事件发生时，SubReactor 会调用连接对应的 Handler 进行响应&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler 通过 read 读取数据后，会分发给 Worker 线程池进行业务处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Worker 线程池会分配独立的线程完成真正的业务处理，将响应结果发给 Handler 进行处理，最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-主从Reactor多线程.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;模型优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;父线程与子线程&lt;/strong&gt;的数据交互简单职责明确，父线程只需要接收新连接，子线程完成后续的业务处理&lt;/li&gt;
&lt;li&gt;父线程与子线程的数据交互简单，Reactor 主线程只需要把新连接传给子线程，子线程无需返回数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用场景：Nginx 主从 Reactor 多进程模型，Memcached 主从多线程，Netty 主从多线程模型的支持&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Proactor&lt;/h3&gt;
&lt;p&gt;Reactor 模式中，Reactor 等待某个事件的操作状态发生变化（文件描述符可读写，socket 可读写），然后把事件传递给事先注册的 Handler 来做实际的读写操作，其中的读写操作都需要应用程序同步操作，所以 &lt;strong&gt;Reactor 是非阻塞同步网络模型（NIO）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把 I/O 操作改为异步，交给操作系统来完成就能进一步提升性能，这就是异步网络模型 Proactor（AIO）：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-Proactor模型.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ProactorInitiator 创建 Proactor 和 Handler 对象，并将 Proactor 和 Handler 通过 Asynchronous Operation Processor（AsyOptProcessor）注册到内核&lt;/li&gt;
&lt;li&gt;AsyOptProcessor 处理注册请求，并处理 I/O 操作，完成I/O后通知 Proactor&lt;/li&gt;
&lt;li&gt;Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理，最后由 Handler 完成业务处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比：Reactor 在事件发生时就通知事先注册的处理器（读写在应用程序线程中处理完成）；Proactor 是在事件发生时基于异步 I/O 完成读写操作（内核完成），I/O 完成后才回调应用程序的处理器进行业务处理&lt;/p&gt;
&lt;p&gt;模式优点：异步 I/O 更加充分发挥 DMA（Direct Memory Access 直接内存存取）的优势&lt;/p&gt;
&lt;p&gt;模式缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编程复杂性，由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的，因此开发异步应用程序更加复杂，应用程序还可能因为反向的流控而变得更加难以调试&lt;/li&gt;
&lt;li&gt;内存使用，缓冲区在读或写操作的时间段内必须保持住，可能造成持续的不确定性，并且每个并发操作都要求有独立的缓存，Reactor 模式在 socket 准备好读或写之前是不要求开辟缓存的&lt;/li&gt;
&lt;li&gt;操作系统支持，Windows 下通过 IOCP 实现了真正的异步 I/O，而在 Linux 系统下，Linux2.6 才引入异步 I/O，目前还不完善，所以在 Linux 下实现高并发网络编程都是以 Reactor 模型为主&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Netty&lt;/h3&gt;
&lt;p&gt;Netty 主要基于主从 Reactors 多线程模型做了一定的改进，Netty 的工作架构图：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-工作模型.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接，WorkerGroup 专门负责网络的读写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup，该 Group 相当于一个事件循环组，含有多个事件循环，每一个事件循环是 NioEventLoop，所以可以有多个线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NioEventLoop 表示一个&lt;strong&gt;循环处理任务的线程&lt;/strong&gt;，每个 NioEventLoop 都有一个 Selector，用于监听绑定在其上的 Socket 的通讯&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个 Boss NioEventLoop 循环执行的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轮询 accept 事件&lt;/li&gt;
&lt;li&gt;处理 accept 事件，与 client 建立连接，生成 NioScocketChannel，并将其&lt;strong&gt;注册到某个 Worker 中&lt;/strong&gt;的某个 NioEventLoop 上的 Selector，连接就与 NioEventLoop 绑定&lt;/li&gt;
&lt;li&gt;处理任务队列的任务，即 runAllTasks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个 Worker NioEventLoop 循环执行的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轮询 read、write 事件&lt;/li&gt;
&lt;li&gt;处理 I/O 事件，即 read，write 事件，在对应 NioSocketChannel 处理&lt;/li&gt;
&lt;li&gt;处理任务队列的任务，即 runAllTasks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个 Worker NioEventLoop 处理业务时，会使用 Pipeline（管道），Pipeline 中包含了 Channel，即通过 Pipeline 可以获取到对应通道，管道中维护了很多的处理器 Handler&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-Channel与Pipeline.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;基本实现&lt;/h2&gt;
&lt;p&gt;开发简单的服务器端和客户端，基本介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Channel 理解为数据的通道，把 msg 理解为流动的数据，最开始输入是 ByteBuf，但经过 Pipeline 的加工，会变成其它类型对象，最后输出又变成 ByteBuf&lt;/li&gt;
&lt;li&gt;Handler 理解为数据的处理工序，Pipeline 负责发布事件传播给每个 Handler，Handler 对自己感兴趣的事件进行处理（重写了相应事件处理方法），分 Inbound 和 Outbound 两类&lt;/li&gt;
&lt;li&gt;EventLoop 理解为处理数据的执行者，既可以执行 IO 操作，也可以进行任务处理。每个执行者有任务队列，队列里可以堆放多个 Channel 的待处理任务，任务分为普通任务、定时任务。按照 Pipeline 顺序，依次按照 Handler 的规划（代码）处理数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.netty&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;netty-all&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.1.20.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Server.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloServer {
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup(2);
        // 1. 启动器，负责组装 netty 组件，启动服务器
        new ServerBootstrap()
                // 2. 线程组，boss 只负责【处理 accept 事件】， worker 只【负责 channel 上的读写】
                .group(boss, worker)
           	 	//.option() 		// 给 ServerSocketChannel 配置参数
            	//.childOption()   	// 给 SocketChannel 配置参数
                // 3. 选择服务器的 ServerSocketChannel 实现
                .channel(NioServerSocketChannel.class)
                // 4. boss 负责处理连接，worker(child) 负责处理读写，决定了能执行哪些操作(handler)
                .childHandler(new ChannelInitializer&amp;lt;NioSocketChannel&amp;gt;() {
                    // 5. channel 代表和客户端进行数据读写的通道 Initializer 初始化，负责添加别的 handler
                    // 7. 连接建立后，执行初始化方法
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        // 添加具体的 handler
                        ch.pipeline().addLast(new StringDecoder());// 将 ByteBuf 转成字符串
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { // 自定义 handler
                            // 读事件
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                // 打印转换好的字符串
                                System.out.println(msg);
                            }
                        });
                    }
                })
                // 6. 绑定监听端口
                .bind(8080);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Client.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建启动器类
        new Bootstrap()
                // 2. 添加 EventLoop
                .group(new NioEventLoopGroup())
            	//.option()，给 SocketChannel 配置参数
                // 3. 选择客户端 channel 实现
                .channel(NioSocketChannel.class)
                // 4. 添加处理器
                .handler(new ChannelInitializer&amp;lt;NioSocketChannel&amp;gt;() {
                    // 4.1 连接建立后被调用
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        // 将 Hello World 转为 ByteBuf
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                // 5. 连接到服务器，然后调用 4.1
                .connect(new InetSocketAddress(&quot;127.0.0.1&quot;,8080))
                // 6. 阻塞方法，直到连接建立
                .sync()
                // 7. 代表连接对象
                .channel()
                // 8. 向服务器发送数据
                .writeAndFlush(&quot;Hello World&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1py4y1E7oA&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;组件介绍&lt;/h2&gt;
&lt;h3&gt;EventLoop&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;事件循环对象 EventLoop，&lt;strong&gt;本质是一个单线程执行器同时维护了一个 Selector&lt;/strong&gt;，有 run 方法处理 Channel 上源源不断的 IO 事件&lt;/p&gt;
&lt;p&gt;事件循环组 EventLoopGroup 是一组 EventLoop，Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop，后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理，保证了事件处理时的线程安全&lt;/p&gt;
&lt;p&gt;EventLoopGroup 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;EventLoop next()&lt;/code&gt;：获取集合中下一个 EventLoop，EventLoopGroup 实现了 Iterable 接口提供遍历 EventLoop 的能力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Future&amp;lt;?&amp;gt; shutdownGracefully()&lt;/code&gt;：优雅关闭的方法，会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入，然后在任务队列的任务都处理完成后，停止线程的运行，从而确保整体应用是在正常有序的状态下退出的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Callable&amp;lt;T&amp;gt; task)&lt;/code&gt;：提交任务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; scheduleWithFixedDelay&lt;/code&gt;：提交定时任务&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;任务传递&lt;/h4&gt;
&lt;p&gt;把要调用的代码封装为一个任务对象，由下一个 handler 的线程来调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EventLoopServer {
    public static void main(String[] args) {
        EventLoopGroup group = new DefaultEventLoopGroup();
        new ServerBootstrap()
                .group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer&amp;lt;NioSocketChannel&amp;gt;() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(&quot;handler1&quot;, new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                ByteBuf buf = (ByteBuf) msg;
                                log.debug(buf.toString(Charset.defaultCharset()));
                                ctx.fireChannelRead(msg);   // 让消息【传递】给下一个 handler
                            }
                        }).addLast(group, &quot;handler2&quot;, new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                ByteBuf buf = (ByteBuf) msg;
                                log.debug(buf.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                })
                .bind(8080);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ChannelHandlerContext fireChannelRead(final Object msg) {
    invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
    return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, &quot;msg&quot;), next);
    EventExecutor executor = next.executor();
    // 下一个 handler 的事件循环是否与当前的事件循环是同一个线程
    if (executor.inEventLoop()) {
        // 是，直接调用
        next.invokeChannelRead(m);
    } else {
        // 不是，将要执行的代码作为任务提交给下一个 handler 处理
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Channel&lt;/h3&gt;
&lt;h4&gt;连接操作&lt;/h4&gt;
&lt;p&gt;Channel 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ChannelFuture close()&lt;/code&gt;：关闭通道&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelPipeline pipeline()&lt;/code&gt;：添加处理器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelFuture write(Object msg)&lt;/code&gt;：数据写入缓冲区&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelFuture writeAndFlush(Object msg)&lt;/code&gt;：数据写入缓冲区并且刷出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ChannelFuture 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ChannelFuture sync()&lt;/code&gt;：同步阻塞等待连接成功&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelFuture addListener(GenericFutureListener listener)&lt;/code&gt;：异步等待&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;connect 方法是异步的，不等连接建立完成就返回，因此 channelFuture 对象中不能立刻获得到正确的 Channel 对象，需要等待&lt;/li&gt;
&lt;li&gt;连接未建立 channel 打印为 &lt;code&gt;[id: 0x2e1884dd]&lt;/code&gt;；建立成功打印为 &lt;code&gt;[id: 0x2e1884dd, L:/127.0.0.1:57191 - R:/127.0.0.1:8080]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ChannelClient {
    public static void main(String[] args) throws InterruptedException {
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer&amp;lt;NioSocketChannel&amp;gt;() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                // 1. 连接服务器，【异步非阻塞】，main 调用 connect 方法，真正执行连接的是 nio 线程
                .connect(new InetSocketAddress(&quot;127.0.0.1&quot;, 8080));
        // 2.1 使用 sync 方法【同步】处理结果，阻塞当前线程，直到 nio 线程连接建立完毕
        channelFuture.sync();
        Channel channel = channelFuture.channel();
        System.out.println(channel); // 【打印】
        // 向服务器发送数据
        channel.writeAndFlush(&quot;hello world&quot;);
        
**************************************************************************************二选一
        // 2.2 使用 addListener 方法【异步】处理结果
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            // nio 线程连接建立好以后，回调该方法
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    Channel channel = future.channel();
                	channel.writeAndFlush(&quot;hello, world&quot;);
                } else {
                    // 建立失败，需要关闭
                    future.channel().close();
                }
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;关闭操作&lt;/h4&gt;
&lt;p&gt;关闭 EventLoopGroup 的运行，分为同步关闭和异步关闭&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CloseFutureClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap()
                // ....
                .connect(new InetSocketAddress(&quot;127.0.0.1&quot;, 8080));
        Channel channel = channelFuture.sync().channel();
        // 发送数据
        new Thread(() -&amp;gt; {
            Scanner sc = new Scanner(System.in);
            while (true) {
                String line = sc.nextLine();
                if (line.equals(&quot;q&quot;)) {
                    channel.close();
                    break;
                }
                channel.writeAndFlush(line);
            }
        }, &quot;input&quot;).start();
        // 获取 CloseFuture 对象
        ChannelFuture closeFuture = channel.closeFuture();
        
        // 1. 同步处理关闭
        System.out.println(&quot;waiting close...&quot;);
        closeFuture.sync();
        System.out.println(&quot;处理关闭后的操作&quot;);
****************************************************
        // 2. 异步处理关闭
        closeFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                System.out.println(&quot;处理关闭后的操作&quot;);
                group.shutdownGracefully();
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Future&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Netty 中的 Future 与 JDK 中的 Future 同名，但是功能的实现不同&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package io.netty.util.concurrent;
public interface Future&amp;lt;V&amp;gt; extends java.util.concurrent.Future&amp;lt;V&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Future 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;V get()&lt;/code&gt;：阻塞等待获取任务执行结果&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V getNow()&lt;/code&gt;：非阻塞获取任务结果，还未产生结果时返回 null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Throwable cause()&lt;/code&gt;：非阻塞获取失败信息，如果没有失败，返回 null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Future&amp;lt;V&amp;gt; sync()&lt;/code&gt;：等待任务结束，如果任务失败，抛出异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean cancel(boolean mayInterruptIfRunning)&lt;/code&gt;：取消任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Future&amp;lt;V&amp;gt; addListener(GenericFutureListener listener)&lt;/code&gt;：添加回调，异步接收结果&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isSuccess()&lt;/code&gt;：判断任务是否成功&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isCancellable()&lt;/code&gt;：判断任务是否取消&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class NettyFutureDemo {
    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();
        EventLoop eventLoop = group.next();
        Future&amp;lt;Integer&amp;gt; future = eventLoop.submit(new Callable&amp;lt;Integer&amp;gt;() {
            @Override
            public Integer call() throws Exception {
                System.out.println(&quot;执行计算&quot;);
                Thread.sleep(1000);
                return 70;
            }
        });
        future.getNow();
        System.out.println(new Date() + &quot;等待结果&quot;);
        System.out.println(new Date() + &quot;&quot; + future.get());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;扩展子类&lt;/h4&gt;
&lt;p&gt;Promise 类是 Future 的子类，可以脱离任务独立存在，作为两个线程间传递结果的容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Promise&amp;lt;V&amp;gt; extends Future&amp;lt;V&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Promise 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Promise&amp;lt;V&amp;gt; setSuccess(V result)&lt;/code&gt;：设置成功结果&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Promise&amp;lt;V&amp;gt; setFailure(Throwable cause)&lt;/code&gt;：设置失败结果&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class NettyPromiseDemo {
    public static void main(String[] args) throws Exception {
        // 1. 准备 EventLoop 对象
        EventLoop eventLoop = new NioEventLoopGroup().next();
        // 2. 主动创建 promise
        DefaultPromise&amp;lt;Integer&amp;gt; promise = new DefaultPromise&amp;lt;&amp;gt;(eventLoop);
        // 3. 任意一个线程执行计算，计算完毕后向 promise 填充结果
        new Thread(() -&amp;gt; {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            promise.setSuccess(200);
        }).start();

        // 4. 接受结果的线程
        System.out.println(new Date() + &quot;等待结果&quot;);
        System.out.println(new Date() + &quot;&quot; + promise.get());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Pipeline&lt;/h3&gt;
&lt;p&gt;ChannelHandler 用来处理 Channel 上的各种事件，分为入站出站两种，所有 ChannelHandler 连接成双向链表就是 Pipeline&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入站处理器通常是 ChannelInboundHandlerAdapter 的子类，主要用来读取客户端数据，写回结果&lt;/li&gt;
&lt;li&gt;出站处理器通常是 ChannelOutboundHandlerAdapter 的子类，主要对写回结果进行加工（入站和出站是对于服务端来说的）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    new ServerBootstrap()
        .group(new NioEventLoopGroup())
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer&amp;lt;NioSocketChannel&amp;gt;() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                // 1. 通过 channel 拿到 pipeline
                ChannelPipeline pipeline = ch.pipeline();
                // 2. 添加处理器 head -&amp;gt; h1 -&amp;gt; h2 -&amp;gt; h3 -&amp;gt; h4 -&amp;gt; tail
                pipeline.addLast(&quot;h1&quot;, new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        log.debug(&quot;1&quot;);
                        ByteBuf buf = (ByteBuf) msg;
                        String s = buf.toString(Charset.defaultCharset());
                        // 将数据传递给下一个【入站】handler，如果不调用该方法则链会断开
                        super.channelRead(ctx, s);
                    }
                });
                pipeline.addLast(&quot;h2&quot;, new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        log.debug(&quot;2&quot;);
                        // 从【尾部开始向前触发】出站处理器
                        ch.writeAndFlush(ctx.alloc().buffer().writeBytes(&quot;server&quot;.getBytes()));
                        // 该方法会让管道从【当前 handler 向前】寻找出站处理器
                        // ctx.writeAndFlush();
                    }
                });
                pipeline.addLast(&quot;h3&quot;, new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                        log.debug(&quot;3&quot;);
                        super.write(ctx, msg, promise);
                    }
                });
                pipeline.addLast(&quot;h4&quot;, new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                        log.debug(&quot;4&quot;);
                        super.write(ctx, msg, promise);
                    }
                });
            }
        })
        .bind(8080);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器端依次打印：1 2 4 3 ，所以&lt;strong&gt;入站是按照 addLast 的顺序执行的，出站是按照 addLast 的逆序执行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个 Channel 包含了一个 ChannelPipeline，而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表，并且每个 ChannelHandlerContext 中关联着一个 ChannelHandler&lt;/p&gt;
&lt;p&gt;入站事件和出站事件在一个双向链表中，两种类型的 handler 互不干扰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入站事件会从链表 head 往后传递到最后一个入站的 handler&lt;/li&gt;
&lt;li&gt;出站事件会从链表 tail 往前传递到最前一个出站的 handler&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-ChannelPipeline.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ByteBuf&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;ByteBuf 是对字节数据的封装，优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;池化，可以重用池中 ByteBuf 实例，更节约内存，减少内存溢出的可能&lt;/li&gt;
&lt;li&gt;读写指针分离，不需要像 ByteBuffer 一样切换读写模式&lt;/li&gt;
&lt;li&gt;可以自动扩容&lt;/li&gt;
&lt;li&gt;支持链式调用，使用更流畅&lt;/li&gt;
&lt;li&gt;零拷贝思想，例如 slice、duplicate、CompositeByteBuf&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;创建方法&lt;/h4&gt;
&lt;p&gt;创建方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10)&lt;/code&gt;：创建了一个默认的 ByteBuf，初始容量是 10&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public ByteBuf buffer() {
    if (directByDefault) {
        return directBuffer();
    }
    return heapBuffer();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10)&lt;/code&gt;：创建池化基于堆的 ByteBuf&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10)&lt;/code&gt;：创建池化基于直接内存的 ByteBuf&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;推荐&lt;/strong&gt;的创建方式：在添加处理器的方法中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipeline.addLast(new ChannelInboundHandlerAdapter() {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = ctx.alloc().buffer();
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接内存对比堆内存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接内存创建和销毁的代价昂贵，但读写性能高（少一次内存复制），适合配合池化功能一起用&lt;/li&gt;
&lt;li&gt;直接内存对 GC 压力小，因为这部分内存不受 JVM 垃圾回收的管理，但也要注意及时主动释放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;池化的意义在于可以&lt;strong&gt;重用 ByteBuf&lt;/strong&gt;，高并发时池化功能更节约内存，减少内存溢出的可能，与非池化对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非池化，每次都要创建新的 ByteBuf 实例，这个操作对直接内存代价昂贵，堆内存会增加 GC 压力&lt;/li&gt;
&lt;li&gt;池化，可以重用池中 ByteBuf 实例，并且采用了与 jemalloc 类似的内存分配算法提升分配效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;池化功能的开启，可以通过下面的系统环境变量来设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Dio.netty.allocator.type={unpooled|pooled}	 # VM 参数
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;4.1 以后，非 Android 平台默认启用池化实现，Android 平台启用非池化实现&lt;/li&gt;
&lt;li&gt;4.1 之前，池化功能还不成熟，默认是非池化实现&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;读写操作&lt;/h4&gt;
&lt;p&gt;ByteBuf 由四部分组成，最开始读写指针（&lt;strong&gt;双指针&lt;/strong&gt;）都在 0 位置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-ByteBuf%E7%BB%84%E6%88%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写入方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;writeBoolean(boolean value)&lt;/td&gt;
&lt;td&gt;写入 boolean 值&lt;/td&gt;
&lt;td&gt;用一字节 01|00 代表 true|false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeByte(int value)&lt;/td&gt;
&lt;td&gt;写入 byte 值&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeInt(int value)&lt;/td&gt;
&lt;td&gt;写入 int 值&lt;/td&gt;
&lt;td&gt;Big Endian，即 0x250，写入后 00 00 02 50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeIntLE(int value)&lt;/td&gt;
&lt;td&gt;写入 int 值&lt;/td&gt;
&lt;td&gt;Little Endian，即 0x250，写入后 50 02 00 00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeBytes(ByteBuf src)&lt;/td&gt;
&lt;td&gt;写入 ByteBuf&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeBytes(byte[] src)&lt;/td&gt;
&lt;td&gt;写入 byte[]&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeBytes(ByteBuffer src)&lt;/td&gt;
&lt;td&gt;写入 NIO 的 ByteBuffer&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int writeCharSequence(CharSequence s, Charset c)&lt;/td&gt;
&lt;td&gt;写入字符串&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;这些方法的未指明返回值的，其返回值都是 ByteBuf，意味着可以链式调用&lt;/li&gt;
&lt;li&gt;写入几位写指针后移几位，指向可以写入的位置&lt;/li&gt;
&lt;li&gt;网络传输，默认习惯是 Big Endian&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩容：写入数据时，容量不够了（初始容量是 10），这时会引发&lt;strong&gt;扩容&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果写入后数据大小未超过 512，则选择下一个 16 的整数倍，例如写入后大小为 12 ，则扩容后 capacity 是 16&lt;/li&gt;
&lt;li&gt;如果写入后数据大小超过 512，则选择下一个 2^n，例如写入后大小为 513，则扩容后 capacity 是 2^10 = 1024（2^9=512 不够）&lt;/li&gt;
&lt;li&gt;扩容不能超过 max capacity 会报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读取方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;byte readByte()&lt;/code&gt;：读取一个字节，读指针后移&lt;/li&gt;
&lt;li&gt;&lt;code&gt;byte getByte(int index)&lt;/code&gt;：读取指定索引位置的字节，读指针不动&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ByteBuf markReaderIndex()&lt;/code&gt;：标记读数据的位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ByteBuf resetReaderIndex()&lt;/code&gt;：重置到标记位置，可以重复读取标记位置向后的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;内存释放&lt;/h4&gt;
&lt;p&gt;Netty 中三种内存的回收：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UnpooledHeapByteBuf 使用的是 JVM 内存，只需等 GC 回收内存&lt;/li&gt;
&lt;li&gt;UnpooledDirectByteBuf 使用的就是直接内存了，需要特殊的方法来回收内存&lt;/li&gt;
&lt;li&gt;PooledByteBuf 和子类使用了池化机制，需要更复杂的规则来回收内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Netty 采用了引用计数法来控制回收内存，每个 ByteBuf 都实现了 ReferenceCounted 接口，回收的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 ByteBuf 对象的初始计数为 1&lt;/li&gt;
&lt;li&gt;调用 release 方法计数减 1，如果计数为 0，ByteBuf 内存被回收&lt;/li&gt;
&lt;li&gt;调用 retain 方法计数加 1，表示调用者没用完之前，其它 handler 即使调用了 release 也不会造成回收&lt;/li&gt;
&lt;li&gt;当计数为 0 时，底层内存会被回收，这时即使 ByteBuf 对象还在，其各个方法均无法正常使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ByteBuf buf = .ByteBufAllocator.DEFAULT.buffer(10)
try {
    // 逻辑处理
} finally {
    buf.release();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pipeline 的存在，需要将 ByteBuf 传递给下一个 ChannelHandler，如果在 finally 中 release 了，就失去了传递性，处理规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建 ByteBuf 放入 Pipeline&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;入站 ByteBuf 处理原则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对原始 ByteBuf 不做处理，调用 ctx.fireChannelRead(msg) 向后传递，这时无须 release，反之不传递需要&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将原始 ByteBuf 转换为其它类型的 Java 对象，这时 ByteBuf 就没用了，此时必须 release&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果出现异常，ByteBuf 没有成功传递到下一个 ChannelHandler，必须 release&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假设消息一直向后传，那么 TailContext 会负责释放未处理消息（原始的 ByteBuf）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
protected void onUnhandledInboundMessage(Object msg) {
    try {
        logger.debug();
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
// io.netty.util.ReferenceCountUtil#release(java.lang.Object)
public static boolean release(Object msg) {
    if (msg instanceof ReferenceCounted) {
        return ((ReferenceCounted) msg).release();
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;出站 ByteBuf 处理原则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出站消息最终都会转为 ByteBuf 输出，一直向前传，由 HeadContext flush 后 release&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不确定 ByteBuf 被引用了多少次，但又必须彻底释放，可以循环调用 release 直到返回 true&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;拷贝操作&lt;/h4&gt;
&lt;p&gt;零拷贝方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ByteBuf slice(int index, int length)&lt;/code&gt;：对原始 ByteBuf 进行切片成多个 ByteBuf，切片后的 ByteBuf 并没有发生内存复制，&lt;strong&gt;共用原始 ByteBuf 的内存&lt;/strong&gt;，切片后的 ByteBuf 维护独立的 read，write 指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
    buf.writeBytes(new byte[]{&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;, &apos;f&apos;, &apos;g&apos;, &apos;h&apos;, &apos;i&apos;, &apos;j&apos;});
    // 在切片过程中并没有发生数据复制
    ByteBuf f1 = buf.slice(0, 5);
    f1.retain();
    ByteBuf f2 = buf.slice(5, 5);
    f2.retain();
    // 对 f1 进行相关的操作也会体现在 buf 上
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ByteBuf duplicate()&lt;/code&gt;：截取原始 ByteBuf 所有内容，并且没有 max capacity 的限制，也是与原始 ByteBuf 使用同一块底层内存，只是读写指针是独立的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers)&lt;/code&gt;：合并多个 ByteBuf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer();
    buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});

    ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
    buf1.writeBytes(new byte[]{6, 7, 8, 9, 10});

    CompositeByteBuf buf = ByteBufAllocator.DEFAULT.compositeBuffer();
    // true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0
    buf.addComponents(true, buf1, buf2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CompositeByteBuf 是一个组合的 ByteBuf，内部维护了一个 Component 数组，每个 Component 管理一个 ByteBuf，记录了这个 ByteBuf 相对于整体偏移量等信息，代表着整体中某一段的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：对外是一个虚拟视图，组合这些 ByteBuf 不会产生内存复制&lt;/li&gt;
&lt;li&gt;缺点：复杂了很多，多次操作会带来性能的损耗&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;深拷贝：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ByteBuf copy()&lt;/code&gt;：将底层内存数据进行深拷贝，因此无论读写，都与原始 ByteBuf 无关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;池化相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Unpooled 是一个工具类，提供了非池化的 ByteBuf 创建、组合、复制等操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});

// 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf，零拷贝思想
ByteBuf buf = Unpooled.wrappedBuffer(buf1, buf2);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;粘包半包&lt;/h2&gt;
&lt;h3&gt;现象演示&lt;/h3&gt;
&lt;p&gt;在 TCP 传输中，客户端发送消息时，实际上是将数据写入 TCP 的缓存，此时数据的大小和缓存的大小就会造成粘包和半包&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当数据超过 TCP 缓存容量时，就会被拆分成多个包，通过 Socket 多次发送到服务端，服务端每次从缓存中取数据，产生半包问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当数据小于 TCP 缓存容量时，缓存中可以存放多个包，客户端和服务端一次通信就可能传递多个包，这时候服务端就可能一次读取多个包，产生粘包的问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码演示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorldClient {
    public static void main(String[] args) {
        send();
    }

    private static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        // 【在连接 channel 建立成功后，会触发 active 方法】
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 发送内容随机的数据包
                            Random r = new Random();
                            char c = &apos;0&apos;;
                            ByteBuf buf = ctx.alloc().buffer();
                            for (int i = 0; i &amp;lt; 10; i++) {
                                byte[] bytes = new byte[10];
                                for (int j = 0; j &amp;lt; r.nextInt(9) + 1; j++) {
                                    bytes[j] = (byte) c;
                                }
                                c++;
                                buf.writeBytes(bytes);
                            }
                            ctx.writeAndFlush(buf);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect(&quot;127.0.0.1&quot;, 8080).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            log.error(&quot;client error&quot;, e);
        } finally {
            worker.shutdownGracefully();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorldServer {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            // 调整系统的接受缓冲区【滑动窗口】
            //serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
            // 调整 netty 的接受缓冲区（ByteBuf）
            //serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, 
            //                            new AdaptiveRecvByteBufAllocator(16, 16, 16));
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 【这里可以添加解码器】
                    // LoggingHandler 用来打印消息
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            channelFuture.sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error(&quot;server error&quot;, e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug(&quot;stop&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;粘包效果展示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;09:57:27.140 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xddbaaef6, L:/127.0.0.1:8080 - R:/127.0.0.1:8701] READ: 100B	// 读了 100 字节，发生粘包
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 30 30 30 30 30 00 00 00 00 00 31 00 00 00 00 00 |00000.....1.....|
|00000010| 00 00 00 00 32 32 32 32 00 00 00 00 00 00 33 00 |....2222......3.|
|00000020| 00 00 00 00 00 00 00 00 34 34 00 00 00 00 00 00 |........44......|
|00000030| 00 00 35 35 35 35 00 00 00 00 00 00 36 36 36 00 |..5555......666.|
|00000040| 00 00 00 00 00 00 37 37 37 37 00 00 00 00 00 00 |......7777......|
|00000050| 38 38 38 38 38 00 00 00 00 00 39 39 00 00 00 00 |88888.....99....|
|00000060| 00 00 00 00                                     |....            |
+--------+-------------------------------------------------+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据，滑动窗口的大小仅决定了 Netty 读取的&lt;strong&gt;最小单位&lt;/strong&gt;，实际每次读取的一般是它的整数倍&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;h4&gt;短连接&lt;/h4&gt;
&lt;p&gt;发一个包建立一次连接，这样连接建立到连接断开之间就是消息的边界，缺点就是效率很低&lt;/p&gt;
&lt;p&gt;客户端代码改造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorldClient {
    public static void main(String[] args) {
        // 分 10 次发送
        for (int i = 0; i &amp;lt; 10; i++) {
            send();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;固定长度&lt;/h4&gt;
&lt;p&gt;服务器端加入定长解码器，每一条消息采用固定长度。如果是半包消息，会缓存半包消息并等待下个包到达之后进行拼包合并，直到读取一个完整的消息包；如果是粘包消息，空余的位置会进行补 0，会浪费空间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
        // LoggingHandler 用来打印消息
        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;10:29:06.522 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38a70fbf, L:/127.0.0.1:8080 - R:/127.0.0.1:10144] READ: 10B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 31 00 00 00 00 00 00 00 00                   |11........      |
+--------+-------------------------------------------------+----------------+
10:29:06.522 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38a70fbf, L:/127.0.0.1:8080 - R:/127.0.0.1:10144] READ: 10B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 32 32 32 32 32 32 00 00 00 00                   |222222....      |
+--------+-------------------------------------------------+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;分隔符&lt;/h4&gt;
&lt;p&gt;服务端加入行解码器，默认以 &lt;code&gt;\n&lt;/code&gt; 或 &lt;code&gt;\r\n&lt;/code&gt; 作为分隔符，如果超出指定长度仍未出现分隔符，则抛出异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端在每条消息之后，加入 &lt;code&gt;\n&lt;/code&gt; 分隔符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void channelActive(ChannelHandlerContext ctx) throws Exception {
    Random r = new Random();
    char c = &apos;a&apos;;
    ByteBuf buffer = ctx.alloc().buffer();
    for (int i = 0; i &amp;lt; 10; i++) {
        for (int j = 1; j &amp;lt;= r.nextInt(16)+1; j++) {
            buffer.writeByte((byte) c);
        }
        // 10 代表 &apos;\n&apos;
        buffer.writeByte(10);
        c++;
    }
    ctx.writeAndFlush(buffer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;预设长度&lt;/h4&gt;
&lt;p&gt;LengthFieldBasedFrameDecoder 解码器自定义长度解决 TCP 粘包黏包问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int maxFrameLength		// 数据最大长度
int lengthFieldOffset 	// 长度字段偏移量，从第几个字节开始是内容的长度字段
int lengthFieldLength	// 长度字段本身的长度
int lengthAdjustment 	// 长度字段为基准，几个字节后才是内容
int initialBytesToStrip	// 从头开始剥离几个字节解码后显示
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;lengthFieldOffset   = 1 (= the length of HDR1)
lengthFieldLength   = 2
lengthAdjustment    = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)//解码
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |-----&amp;gt;| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | &quot;HELLO, WORLD&quot; |      | 0xFE | &quot;HELLO, WORLD&quot; |
+------+--------+------+----------------+      +------+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LengthFieldDecoderDemo {
    public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                // int 占 4 字节，版本号一个字节
                new LengthFieldBasedFrameDecoder(1024, 0, 4, 1,5),
                new LoggingHandler(LogLevel.DEBUG)
        );

        // 4 个字节的内容长度， 实际内容
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        send(buffer, &quot;Hello, world&quot;);
        send(buffer, &quot;Hi!&quot;);
        // 写出缓存
        channel.writeInbound(buffer);
    }
    // 写入缓存
    private static void send(ByteBuf buffer, String content) {
        byte[] bytes = content.getBytes();  // 实际内容
        int length = bytes.length;          // 实际内容长度
        buffer.writeInt(length);
        buffer.writeByte(1);                // 表示版本号
        buffer.writeBytes(bytes);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;10:49:59.344 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 12B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64             |Hello, world    |
+--------+-------------------------------------------------+----------------+
10:49:59.344 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 3B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 69 21                                        |Hi!             |
+--------+-------------------------------------------------+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;协议设计&lt;/h3&gt;
&lt;h4&gt;HTTP&lt;/h4&gt;
&lt;p&gt;访问 URL：http://localhost:8080/&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HttpDemo {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new HttpServerCodec());
                    // 只针对某一种类型的请求处理，此处针对 HttpRequest
                    ch.pipeline().addLast(new SimpleChannelInboundHandler&amp;lt;HttpRequest&amp;gt;() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
                            // 获取请求
                            log.debug(msg.uri());

                            // 返回响应
                            DefaultFullHttpResponse response = new DefaultFullHttpResponse(
                                msg.protocolVersion(), HttpResponseStatus.OK);

                            byte[] bytes = &quot;&amp;lt;h1&amp;gt;Hello, world!&amp;lt;/h1&amp;gt;&quot;.getBytes();

                            response.headers().setInt(CONTENT_LENGTH, bytes.length);
                            response.content().writeBytes(bytes);

                            // 写回响应
                            ctx.writeAndFlush(response);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error(&quot;n3.server error&quot;, e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;自定义&lt;/h4&gt;
&lt;p&gt;处理器代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class MessageCodec extends ByteToMessageCodec&amp;lt;Message&amp;gt; {
    // 编码
    @Override
    public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 1 字节的版本,
        out.writeByte(1);
        // 1 字节的序列化方式 jdk 0 , json 1
        out.writeByte(0);
        // 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义，对齐填充, 1 字节
        out.writeByte(0xff);
        // 获取内容的字节数组，msg 对象序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 长度
        out.writeInt(bytes.length);
        // 写入内容
        out.writeBytes(bytes);
    }

    // 解码
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List&amp;lt;Object&amp;gt; out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug(&quot;{}, {}, {}, {}, {}, {}&quot;, magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug(&quot;{}&quot;, message);
        out.add(message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler(), new MessageCodec());
    // encode
    LoginRequestMessage message = new LoginRequestMessage(&quot;zhangsan&quot;, &quot;123&quot;);
    channel.writeOutbound(message);

    // decode
    ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
    new MessageCodec().encode(null, message, buf);
    // 入站
    channel.writeInbound(buf);
}
public class LoginRequestMessage extends Message {
    private String username;
    private String password;
    // set + get 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Sharable&lt;/h4&gt;
&lt;p&gt;@Sharable 注解的添加时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当 handler 不保存状态时，就可以安全地在多线程下被共享&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于编解码器类不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler，它们的构造方法对 @Sharable 有限制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected ByteToMessageCodec(boolean preferDirect) {
    ensureNotSharable();
    outboundMsgMatcher = TypeParameterMatcher.find(this, ByteToMessageCodec.class, &quot;I&quot;);
    encoder = new Encoder(preferDirect);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;protected void ensureNotSharable() {
    // 如果类上有该注解
    if (isSharable()) {
        throw new IllegalStateException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果能确保编解码器不会保存状态，可以继承 MessageToMessageCodec 父类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@ChannelHandler.Sharable
// 必须和 LengthFieldBasedFrameDecoder 一起使用，确保接到的 ByteBuf 消息是完整的
public class MessageCodecSharable extends MessageToMessageCodec&amp;lt;ByteBuf, Message&amp;gt; {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List&amp;lt;Object&amp;gt; outList) throws Exception {
        ByteBuf out = ctx.alloc().buffer();
        // 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
		// ....
        outList.add(out);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List&amp;lt;Object&amp;gt; out) throws Exception {
        //....
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;场景优化&lt;/h2&gt;
&lt;h3&gt;空闲检测&lt;/h3&gt;
&lt;h4&gt;连接假死&lt;/h4&gt;
&lt;p&gt;连接假死就是客户端数据发不出去，服务端也一直收不到数据，保持这种状态，假死的连接占用的资源不能自动释放，而且向假死连接发送数据，得到的反馈是发送超时&lt;/p&gt;
&lt;p&gt;解决方案：每隔一段时间就检查这段时间内是否接收到客户端数据，没有就可以判定为连接假死&lt;/p&gt;
&lt;p&gt;IdleStateHandler 是 Netty 提供的处理空闲状态的处理器，用来判断是不是读空闲时间或写空闲时间过长&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数一 long readerIdleTime：读空闲，表示多长时间没有读&lt;/li&gt;
&lt;li&gt;参数二 long writerIdleTime：写空闲，表示多长时间没有写&lt;/li&gt;
&lt;li&gt;参数三 long allIdleTime：读写空闲，表示多长时间没有读写&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
        ch.pipeline().addLast(new MessageCodec());
        // 5s 内如果没有收到 channel 的数据，会触发一个 IdleState#READER_IDLE 事件，
        ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
        // ChannelDuplexHandler 【可以同时作为入站和出站】处理器
        ch.pipeline().addLast(new ChannelDuplexHandler() {
            // 用来触发特殊事件
            @Override
            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
                IdleStateEvent event = (IdleStateEvent) evt;
                // 触发了读空闲事件
                if (event.state() == IdleState.READER_IDLE) {
                    log.debug(&quot;已经 5s 没有读到数据了&quot;);
                    ctx.channel().close();
                }
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;心跳机制&lt;/h4&gt;
&lt;p&gt;客户端定时向服务器端发送数据，&lt;strong&gt;时间间隔要小于服务器定义的空闲检测的时间间隔&lt;/strong&gt;，就能防止误判连接假死，这就是心跳机制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bootstrap.handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
        ch.pipeline().addLast(new MessageCodec());
        // 3s 内如果没有向服务器写数据，会触发一个 IdleState#WRITER_IDLE 事件
        ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
        // ChannelDuplexHandler 可以同时作为入站和出站处理器
        ch.pipeline().addLast(new ChannelDuplexHandler() {
            // 用来触发特殊事件
            @Override
            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                IdleStateEvent event = (IdleStateEvent) evt;
                // 触发了写空闲事件
                if (event.state() == IdleState.WRITER_IDLE) {
                    // 3s 没有写数据了，【发送一个心跳包】
                    ctx.writeAndFlush(new PingMessage());
                }
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;序列化&lt;/h3&gt;
&lt;h4&gt;普通方式&lt;/h4&gt;
&lt;p&gt;序列化，反序列化主要用在消息正文的转换上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;序列化时，需要将 Java 对象变为要传输的数据（可以是 byte[]，或 json 等，最终都需要变成 byte[]）&lt;/li&gt;
&lt;li&gt;反序列化时，需要将传入的正文数据还原成 Java 对象，便于处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;抽象一个 Serializer 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Serializer {
    // 反序列化方法
    &amp;lt;T&amp;gt; T deserialize(Class&amp;lt;T&amp;gt; clazz, byte[] bytes);
    // 序列化方法
    &amp;lt;T&amp;gt; byte[] serialize(T object);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供两个实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum SerializerAlgorithm implements Serializer {
	// Java 实现
    Java {
        @Override
        public &amp;lt;T&amp;gt; T deserialize(Class&amp;lt;T&amp;gt; clazz, byte[] bytes) {
            try {
                ObjectInputStream in = 
                    new ObjectInputStream(new ByteArrayInputStream(bytes));
                Object object = in.readObject();
                return (T) object;
            } catch (IOException | ClassNotFoundException e) {
                throw new RuntimeException(&quot;SerializerAlgorithm.Java 反序列化错误&quot;, e);
            }
        }

        @Override
        public &amp;lt;T&amp;gt; byte[] serialize(T object) {
            try {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                new ObjectOutputStream(out).writeObject(object);
                return out.toByteArray();
            } catch (IOException e) {
                throw new RuntimeException(&quot;SerializerAlgorithm.Java 序列化错误&quot;, e);
            }
        }
    }, 
    // Json 实现(引入了 Gson 依赖)
    Json {
        @Override
        public &amp;lt;T&amp;gt; T deserialize(Class&amp;lt;T&amp;gt; clazz, byte[] bytes) {
            return new Gson().fromJson(new String(bytes, StandardCharsets.UTF_8), clazz);
        }

        @Override
        public &amp;lt;T&amp;gt; byte[] serialize(T object) {
            return new Gson().toJson(object).getBytes(StandardCharsets.UTF_8);
        }
    };

    // 需要从协议的字节中得到是哪种序列化算法
    public static SerializerAlgorithm getByInt(int type) {
        SerializerAlgorithm[] array = SerializerAlgorithm.values();
        if (type &amp;lt; 0 || type &amp;gt; array.length - 1) {
            throw new IllegalArgumentException(&quot;超过 SerializerAlgorithm 范围&quot;);
        }
        return array[type];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;ProtoBuf&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;Codec（编解码器）的组成部分有两个：Decoder（解码器）和 Encoder（编码器）。Encoder 负责把业务数据转换成字节码数据，Decoder 负责把字节码数据转换成业务数据&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-编码解码.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Protobuf 是 Google 发布的开源项目，全称  Google Protocol Buffers ，是一种轻便高效的结构化数据存储格式，可以用于结构化数据串行化，或者说序列化。很适合做数据存储或 RPC（远程过程调用 remote procedure call）数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ，效率会更高&lt;/p&gt;
&lt;p&gt;Protobuf 是以 message 的方式来管理数据，支持跨平台、跨语言（客户端和服务器端可以是不同的语言编写的），高性能、高可靠性&lt;/p&gt;
&lt;p&gt;工作过程：使用 Protobuf 编译器自动生成代码，Protobuf 是将类的定义使用 .proto 文件进行描述，然后通过 protoc.exe 编译器根据  .proto 自动生成 .java 文件&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;代码实现&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单个 message：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto3&quot;; 								// 版本
option java_outer_classname = &quot;StudentPOJO&quot;;	// 生成的外部类名，同时也是文件名

message Student { 	// 在 StudentPOJO 外部类种生成一个内部类 Student，是真正发送的 POJO 对象
    int32 id = 1; 	// Student 类中有一个属性：名字为 id 类型为 int32(protobuf类型) ，1表示属性序号，不是值
    string name = 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-Protobuf编译文件.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;编译 &lt;code&gt;protoc.exe --java_out=.Student.proto&lt;/code&gt;（cmd 窗口输入） 将生成的 StudentPOJO 放入到项目使用&lt;/p&gt;
&lt;p&gt;Server 端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new ServerBootstrap() //...
    .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {	// 创建一个通道初始化对象
        // 给pipeline 设置处理器
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            // 在pipeline加入ProtoBufDecoder，指定对哪种对象进行解码
            ch.pipeline().addLast(&quot;decoder&quot;, new ProtobufDecoder(
                							StudentPOJO.Student.getDefaultInstance()));
            ch.pipeline().addLast(new NettyServerHandler());
        }
    }); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client 端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Bootstrap().group(group) 			// 设置线程组
    .channel(NioSocketChannel.class) 	// 设置客户端通道的实现类(反射)
    .handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            // 在pipeline中加入 ProtoBufEncoder
            ch.pipeline().addLast(&quot;encoder&quot;, new ProtobufEncoder());
            ch.pipeline().addLast(new NettyClientHandler()); // 加入自定义的业务处理器
        }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个 message：Protobuf 可以使用 message 管理其他的 message。假设某个项目需要传输 20 个对象，可以在一个文件里定义 20 个 message，最后再用一个总的 message 来决定在实际传输时真正需要传输哪一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto3&quot;;
option optimize_for = SPEED; 					// 加快解析
option java_package=&quot;com.atguigu.netty.codec2&quot;;	// 指定生成到哪个包下
option java_outer_classname=&quot;MyDataInfo&quot;; 		// 外部类名, 文件名

message MyMessage {
    // 定义一个枚举类型，DataType 如果是 0 则表示一个 Student 对象实例，DataType 这个名称自定义
    enum DataType {
        StudentType = 0; //在 proto3 要求 enum 的编号从 0 开始
        WorkerType = 1;
    }

    // 用 data_type 来标识传的是哪一个枚举类型，这里才真正开始定义 Message 的数据类型
    DataType data_type = 1;  // 所有后面的数字都只是编号而已

    // oneof 关键字，表示每次枚举类型进行传输时，限制最多只能传输一个对象。
    // dataBody名称也是自定义的
    // MyMessage 里出现的类型只有两个 DataType 类型，Student 或者 Worker 类型，在真正传输的时候只会有一个出现
    oneof dataBody {
        Student student = 2;  //注意这后面的数字也都只是编号而已，上面DataType data_type = 1  占了第一个序号了
        Worker worker = 3;
    }


}

message Student {
    int32 id = 1;		// Student类的属性
    string name = 2; 	//
}
message Worker {
    string name=1;
    int32 age=2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译：&lt;/p&gt;
&lt;p&gt;Server 端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch.pipeline().addLast(&quot;decoder&quot;, new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client 端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipeline.addLast(&quot;encoder&quot;, new ProtobufEncoder());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;长连接&lt;/h3&gt;
&lt;p&gt;HTTP 协议是无状态的，浏览器和服务器间的请求响应一次，下一次会重新创建连接。实现基于 WebSocket 的长连接的全双工的交互，改变 HTTP 协议多次请求的约束&lt;/p&gt;
&lt;p&gt;开发需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现长连接，服务器与浏览器相互通信客户端&lt;/li&gt;
&lt;li&gt;浏览器和服务器端会相互感知，比如服务器关闭了，浏览器会感知，同样浏览器关闭了，服务器会感知&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;WebSocket：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;WebSocket 的数据是&lt;strong&gt;以帧（frame）形式传递&lt;/strong&gt;，WebSocketFrame 下面有六个子类，代表不同的帧格式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器请求 URL：ws://localhost:8080/xxx&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MyWebSocket {
    public static void main(String[] args) throws Exception {
        // 创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            serverBootstrap.childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    // 基于 http 协议，使用 http 的编码和解码器
                    pipeline.addLast(new HttpServerCodec());
                    // 是以块方式写，添加 ChunkedWriteHandler 处理器
                    pipeline.addLast(new ChunkedWriteHandler());

                    // http 数据在传输过程中是分段, HttpObjectAggregator 就是可以将多个段聚合
                    //  这就就是为什么，当浏览器发送大量数据时，就会发出多次 http 请求
                    pipeline.addLast(new HttpObjectAggregator(8192));
        
                    // WebSocketServerProtocolHandler 核心功能是【将 http 协议升级为 ws 协议】，保持长连接
                    pipeline.addLast(new WebSocketServerProtocolHandler(&quot;/hello&quot;));

                    // 自定义的handler ，处理业务逻辑
                    pipeline.addLast(new MyTextWebSocketFrameHandler());
                }
            });

            // 启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler&amp;lt;TextWebSocketFrame&amp;gt; {
    // TextWebSocketFrame 类型，表示一个文本帧(frame)
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println(&quot;服务器收到消息 &quot; + msg.text());
        // 回复消息
        ctx.writeAndFlush(new TextWebSocketFrame(&quot;服务器时间&quot; + LocalDateTime.now() + &quot; &quot; + msg.text()));
    }

    // 当web客户端连接后， 触发方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值，LongText 是唯一的 ShortText 不是唯一
        System.out.println(&quot;handlerAdded 被调用&quot; + ctx.channel().id().asLongText());
        System.out.println(&quot;handlerAdded 被调用&quot; + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println(&quot;handlerRemoved 被调用&quot; + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println(&quot;异常发生 &quot; + cause.getMessage());
        ctx.close(); // 关闭连接
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTML：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Title&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;script&amp;gt;
    var socket;
    // 判断当前浏览器是否支持websocket
    if(window.WebSocket) {
        //go on
        socket = new WebSocket(&quot;ws://localhost:8080/hello&quot;);
        //相当于channelReado, ev 收到服务器端回送的消息
        socket.onmessage = function (ev) {
            var rt = document.getElementById(&quot;responseText&quot;);
            rt.value = rt.value + &quot;\n&quot; + ev.data;
        }

        //相当于连接开启(感知到连接开启)
        socket.onopen = function (ev) {
            var rt = document.getElementById(&quot;responseText&quot;);
            rt.value = &quot;连接开启了..&quot;
        }

        //相当于连接关闭(感知到连接关闭)
        socket.onclose = function (ev) {

            var rt = document.getElementById(&quot;responseText&quot;);
            rt.value = rt.value + &quot;\n&quot; + &quot;连接关闭了..&quot;
        }
    } else {
        alert(&quot;当前浏览器不支持websocket&quot;)
    }

    // 发送消息到服务器
    function send(message) {
        // 先判断socket是否创建好
        if(!window.socket) {
            return;
        }
        if(socket.readyState == WebSocket.OPEN) {
            // 通过socket 发送消息
            socket.send(message)
        } else {
            alert(&quot;连接没有开启&quot;);
        }
    }
&amp;lt;/script&amp;gt;
    &amp;lt;form onsubmit=&quot;return false&quot;&amp;gt;
        &amp;lt;textarea name=&quot;message&quot; style=&quot;height: 300px; width: 300px&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;input type=&quot;button&quot; value=&quot;发生消息&quot; onclick=&quot;send(this.form.message.value)&quot;&amp;gt;
        &amp;lt;textarea id=&quot;responseText&quot; style=&quot;height: 300px; width: 300px&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;input type=&quot;button&quot; value=&quot;清空内容&quot; onclick=&quot;document.getElementById(&apos;responseText&apos;).value=&apos;&apos;&quot;&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;参数调优&lt;/h3&gt;
&lt;h4&gt;CONNECT&lt;/h4&gt;
&lt;p&gt;参数配置方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端通过 .option() 方法配置参数，给 SocketChannel 配置参数&lt;/li&gt;
&lt;li&gt;服务器端：
&lt;ul&gt;
&lt;li&gt;new ServerBootstrap().option()： 给 ServerSocketChannel 配置参数&lt;/li&gt;
&lt;li&gt;new ServerBootstrap().childOption()：给 SocketChannel 配置参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CONNECT_TIMEOUT_MILLIS 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属于 SocketChannal 参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在客户端建立连接时，如果在指定毫秒内无法连接，会抛出 timeout 异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SO_TIMEOUT 主要用在阻塞 IO，阻塞 IO 中 accept，read 等都是无限等待的，如果不希望永远阻塞，可以调整超时时间&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ConnectionTimeoutTest {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                    .channel(NioSocketChannel.class)
                    .handler(new LoggingHandler());
            ChannelFuture future = bootstrap.connect(&quot;127.0.0.1&quot;, 8080);
            future.sync().channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
            log.debug(&quot;timeout&quot;);
        } finally {
            group.shutdownGracefully();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;SO_BACKLOG&lt;/h4&gt;
&lt;p&gt;属于 ServerSocketChannal 参数，通过 &lt;code&gt;option(ChannelOption.SO_BACKLOG, value)&lt;/code&gt; 来设置大小&lt;/p&gt;
&lt;p&gt;在 Linux 2.2 之前，backlog 大小包括了两个队列的大小，在 2.2 之后，分别用下面两个参数来控制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sync queue：半连接队列，大小通过 &lt;code&gt;/proc/sys/net/ipv4/tcp_max_syn_backlog&lt;/code&gt; 指定，在 &lt;code&gt;syncookies&lt;/code&gt; 启用的情况下，逻辑上没有最大值限制&lt;/li&gt;
&lt;li&gt;accept queue：全连接队列，大小通过 &lt;code&gt;/proc/sys/net/core/somaxconn&lt;/code&gt; 指定，在使用 listen 函数时，内核会根据传入的 backlog 参数与系统参数，取二者的较小值。如果 accpet queue 队列满了，server 将&lt;strong&gt;发送一个拒绝连接的错误信息&lt;/strong&gt;到 client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-TCP%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;其他参数&lt;/h4&gt;
&lt;p&gt;ALLOCATOR：属于 SocketChannal 参数，用来分配 ByteBuf， ctx.alloc()&lt;/p&gt;
&lt;p&gt;RCVBUF_ALLOCATOR：属于 SocketChannal 参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制 Netty 接收缓冲区大小&lt;/li&gt;
&lt;li&gt;负责入站数据的分配，决定入站缓冲区的大小（并可动态调整），统一采用 direct 直接内存，具体池化还是非池化由 allocator 决定&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;RocketMQ&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;h3&gt;消息队列&lt;/h3&gt;
&lt;h4&gt;应用场景&lt;/h4&gt;
&lt;p&gt;消息队列是一种先进先出的数据结构，常见的应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;应用解耦：系统的耦合性越高，容错性就越低&lt;/p&gt;
&lt;p&gt;实例：用户创建订单后，耦合调用库存系统、物流系统、支付系统，任何一个子系统出了故障都会造成下单异常，影响用户使用体验。使用消息队列解耦合，比如物流系统发生故障，需要几分钟恢复，将物流系统要处理的数据缓存到消息队列中，用户的下单操作正常完成。等待物流系统正常后处理存在消息队列中的订单消息即可，终端系统感知不到物流系统发生过几分钟故障&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E8%A7%A3%E8%80%A6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;流量削峰：应用系统如果遇到系统请求流量的瞬间猛增，有可能会将系统压垮，使用消息队列可以将大量请求缓存起来，分散到很长一段时间处理，这样可以提高系统的稳定性和用户体验&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%B5%81%E9%87%8F%E5%89%8A%E5%B3%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据分发：让数据在多个系统更加之间进行流通，数据的产生方不需要关心谁来使用数据，只需要将数据发送到消息队列，数据使用方直接在消息队列中直接获取数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%95%B0%E6%8D%AE%E5%88%86%E5%8F%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1L4411y7mn&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;技术选型&lt;/h4&gt;
&lt;p&gt;RocketMQ 对比 Kafka 的优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持 Pull和 Push 两种消息模式&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;支持延时消息、死信队列、消息重试、消息回溯、消息跟踪、事务消息等高级特性&lt;/li&gt;
&lt;li&gt;对消息可靠性做了改进，&lt;strong&gt;保证消息不丢失并且至少消费一次&lt;/strong&gt;，与 Kafka 一样是先写 PageCache 再落盘，并且数据有多副本&lt;/li&gt;
&lt;li&gt;RocketMQ 存储模型是所有的 Topic 都写到同一个 Commitlog 里，是一个 append only 操作，在海量 Topic 下也能将磁盘的性能发挥到极致，并且保持稳定的写入时延。Kafka 的吞吐非常高（零拷贝、操作系统页缓存、磁盘顺序写），但是在多 Topic 下时延不够稳定（顺序写入特性会被破坏从而引入大量的随机 I/O），不适合实时在线业务场景&lt;/li&gt;
&lt;li&gt;经过阿里巴巴多年双 11 验证过、可以支持亿级并发的开源消息队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kafka 比 RocketMQ 吞吐量高：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Kafka 将 Producer 端将多个小消息合并，采用异步批量发送的机制，当发送一条消息时，消息并没有发送到 Broker 而是缓存起来，直接向业务返回成功，当缓存的消息达到一定数量时再批量发送&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;减少了网络 I/O，提高了消息发送的性能，但是如果消息发送者宕机，会导致消息丢失，降低了可靠性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RocketMQ 缓存过多消息会导致频繁 GC，并且为了保证可靠性没有采用这种方式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Topic 的 partition 数量过多时，Kafka 的性能不如 RocketMQ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;两者都使用文件存储，但是 Kafka 是一个分区一个文件，Topic 过多时分区的总量也会增加，过多的文件导致对消息刷盘时出现文件竞争磁盘，造成性能的下降。&lt;strong&gt;一个分区只能被一个消费组中的一个消费线程进行消费&lt;/strong&gt;，因此可以同时消费的消费端也比较少&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RocketMQ 所有队列都存储在一个文件中，每个队列存储的消息量也比较小，因此多 Topic 的对 RocketMQ 的性能的影响较小&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;安装测试&lt;/h3&gt;
&lt;p&gt;安装需要 Java 环境，下载解压后进入安装目录，进行启动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动 NameServer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.启动 NameServer
nohup sh bin/mqnamesrv &amp;amp;
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/namesrv.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RocketMQ 默认的虚拟机内存较大，需要编辑如下两个配置文件，修改 JVM 内存大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 编辑runbroker.sh和runserver.sh修改默认JVM大小
vi runbroker.sh
vi runserver.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考配置：JAVA_OPT=&quot;${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m  -XX:MaxMetaspaceSize=320m&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 Broker&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.启动 Broker
nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true &amp;amp;
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/broker.log 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.设置环境变量
export NAMESRV_ADDR=localhost:9876
# 2.使用安装包的 Demo 发送消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接受消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.设置环境变量
export NAMESRV_ADDR=localhost:9876
# 2.接收消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭 RocketMQ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.关闭 NameServer
sh bin/mqshutdown namesrv
# 2.关闭 Broker
sh bin/mqshutdown broker



&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;相关概念&lt;/h3&gt;
&lt;p&gt;RocketMQ 主要由 Producer、Broker、Consumer 三部分组成，其中 Producer 负责生产消息，Consumer 负责消费消息，Broker 负责存储消息，NameServer 负责管理 Broker&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理服务器（Broker Server）：消息中转角色，负责&lt;strong&gt;存储消息、转发消息&lt;/strong&gt;。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备，也存储消息相关的元数据，包括消费者组、消费进度偏移和主题和队列消息等&lt;/li&gt;
&lt;li&gt;名字服务（Name Server）：充当&lt;strong&gt;路由消息&lt;/strong&gt;的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表&lt;/li&gt;
&lt;li&gt;消息生产者（Producer）：负责&lt;strong&gt;生产消息&lt;/strong&gt;，把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式，同步发送、异步发送、顺序发送、单向发送，同步和异步方式均需要 Broker 返回确认信息，单向发送不需要；可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递，投递的过程支持快速失败并且低延迟&lt;/li&gt;
&lt;li&gt;消息消费者（Consumer）：负责&lt;strong&gt;消费消息&lt;/strong&gt;，一般是后台系统负责异步消费，一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式：
&lt;ul&gt;
&lt;li&gt;拉取式消费（Pull Consumer）：应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息，主动权由应用控制，一旦获取了批量消息，应用就会启动消费过程&lt;/li&gt;
&lt;li&gt;推动式消费（Push Consumer）：该模式下 Broker 收到数据后会主动推送给消费端，实时性较高&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;生产者组（Producer Group）：同一类 Producer 的集合，发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃，&lt;strong&gt;则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;消费者组（Consumer Group）：同一类 Consumer 的集合，消费者实例必须订阅完全相同的 Topic，消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式：
&lt;ul&gt;
&lt;li&gt;集群消费（Clustering）：相同 Consumer Group 的每个 Consumer 实例平均分摊消息&lt;/li&gt;
&lt;li&gt;广播消费（Broadcasting）：相同 Consumer Group 的每个 Consumer 实例都接收全量的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个 Broker 可以存储多个 Topic 的消息，每个 Topic 的消息也可以分片存储于不同的 Broker，Message Queue（消息队列）是用于存储消息的物理地址，每个 Topic 中的消息地址存储于多个 Message Queue 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主题（Topic）：表示一类消息的集合，每个主题包含若干条消息，每条消息只属于一个主题，是 RocketMQ 消息订阅的基本单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息（Message）：消息系统所传输信息的物理载体，生产和消费数据的最小单位，每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID，且可以携带具有业务标识的 Key，系统提供了通过 Message ID 和 Key 查询消息的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标签（Tag）：为消息设置的标志，用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性，并优化 RocketMQ 提供的查询系统，消费者可以根据 Tag 实现对不同子主题的不同消费逻辑，实现更好的扩展性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;普通顺序消息（Normal Ordered Message）：消费者通过同一个消息队列（Topic 分区）收到的消息是有顺序的，不同消息队列收到的消息则可能是无顺序的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;严格顺序消息（Strictly Ordered Message）：消费者收到的所有消息均是有顺序的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/tree/master/docs/cn（基础知识部分的笔记参考官方文档编写）&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;消息操作&lt;/h2&gt;
&lt;h3&gt;基本样例&lt;/h3&gt;
&lt;h4&gt;订阅发布&lt;/h4&gt;
&lt;p&gt;消息的发布是指某个生产者向某个 Topic 发送消息，消息的订阅是指某个消费者关注了某个 Topic 中带有某些 Tag 的消息，进而从该 Topic 消费数据&lt;/p&gt;
&lt;p&gt;导入 MQ 客户端依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.rocketmq&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;rocketmq-client&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.4.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消息发送者步骤分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建消息生产者 Producer，并制定生产者组名&lt;/li&gt;
&lt;li&gt;指定 Nameserver 地址&lt;/li&gt;
&lt;li&gt;启动 Producer&lt;/li&gt;
&lt;li&gt;创建消息对象，指定主题 Topic、Tag 和消息体&lt;/li&gt;
&lt;li&gt;发送消息&lt;/li&gt;
&lt;li&gt;关闭生产者 Producer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;消息消费者步骤分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建消费者 Consumer，制定消费者组名&lt;/li&gt;
&lt;li&gt;指定 Nameserver 地址&lt;/li&gt;
&lt;li&gt;订阅主题 Topic 和 Tag&lt;/li&gt;
&lt;li&gt;设置回调函数，处理消息&lt;/li&gt;
&lt;li&gt;启动消费者 Consumer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;发送消息&lt;/h4&gt;
&lt;h5&gt;同步发送&lt;/h5&gt;
&lt;p&gt;使用 RocketMQ 发送三种类型的消息：同步消息、异步消息和单向消息，其中前两种消息是可靠的，因为会有发送是否成功的应答&lt;/p&gt;
&lt;p&gt;这种可靠性同步地发送方式使用的比较广泛，比如：重要的消息通知，短信通知&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SyncProducer {
	public static void main(String[] args) throws Exception {
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer(&quot;please_rename_unique_group_name&quot;);
    	// 设置NameServer的地址
    	producer.setNamesrvAddr(&quot;localhost:9876&quot;);
    	// 启动Producer实例
        producer.start();
    	for (int i = 0; i &amp;lt; 100; i++) {
    	    // 创建消息，并指定Topic，Tag和消息体
    	    Message msg = new Message(
                &quot;TopicTest&quot; /* Topic */,
                &quot;TagA&quot; /* Tag */,
        		(&quot;Hello RocketMQ &quot; + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
            
        	// 发送消息到一个Broker
            SendResult sendResult = producer.send(msg);
            // 通过sendResult返回消息是否成功送达
            System.out.printf(&quot;%s%n&quot;, sendResult);
    	}
    	// 如果不再发送消息，关闭Producer实例。
    	producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;异步发送&lt;/h5&gt;
&lt;p&gt;异步消息通常用在对响应时间敏感的业务场景，即发送端不能容忍长时间地等待 Broker 的响应&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AsyncProducer {
	public static void main(String[] args) throws Exception {
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer(&quot;please_rename_unique_group_name&quot;);
    	// 设置NameServer的地址
        producer.setNamesrvAddr(&quot;localhost:9876&quot;);
    	// 启动Producer实例
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);
	
        int messageCount = 100;
		// 根据消息数量实例化倒计时计算器
        final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount);
        for (int i = 0; i &amp;lt; messageCount; i++) {
            final int index = i;
            // 创建消息，并指定Topic，Tag和消息体
            Message msg = new Message(&quot;TopicTest&quot;, &quot;TagA&quot;, &quot;OrderID188&quot;,
                                      &quot;Hello world&quot;.getBytes(RemotingHelper.DEFAULT_CHARSET));

            // SendCallback接收异步返回结果的回调
            producer.send(msg, new SendCallback() {
                // 发送成功回调函数
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.printf(&quot;%-10d OK %s %n&quot;, index, sendResult.getMsgId());
                }
                
                @Override
                public void onException(Throwable e) {
                    countDownLatch.countDown();
                    System.out.printf(&quot;%-10d Exception %s %n&quot;, index, e);
                    e.printStackTrace();
                }
            });
        }
        // 等待5s
        countDownLatch.await(5, TimeUnit.SECONDS);
        // 如果不再发送消息，关闭Producer实例。
        producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;单向发送&lt;/h5&gt;
&lt;p&gt;单向发送主要用在不特别关心发送结果的场景，例如日志发送&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class OnewayProducer {
	public static void main(String[] args) throws Exception{
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer(&quot;please_rename_unique_group_name&quot;);
    	// 设置NameServer的地址
        producer.setNamesrvAddr(&quot;localhost:9876&quot;);
    	// 启动Producer实例
        producer.start();
    	for (int i = 0; i &amp;lt; 100; i++) {
        	// 创建消息，并指定Topic，Tag和消息体
        	Message msg = new Message(&quot;TopicTest&quot;,&quot;TagA&quot;,
                          (&quot;Hello RocketMQ &quot; + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
        	// 发送单向消息，没有任何返回结果
        	producer.sendOneway(msg);
    	}
    	// 如果不再发送消息，关闭Producer实例。
    	producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;消费消息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Consumer {
	public static void main(String[] args) throws InterruptedException, MQClientException {
    	// 实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(&quot;please_rename_unique_group_name&quot;);
    	// 设置NameServer的地址
        consumer.setNamesrvAddr(&quot;localhost:9876&quot;);

    	// 订阅一个或者多个Topic，以及Tag来过滤需要消费的消息
        consumer.subscribe(&quot;TopicTest&quot;, &quot;*&quot;);
    	// 注册消息监听器，回调实现类来处理从broker拉取回来的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            // 接受消息内容
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List&amp;lt;MessageExt&amp;gt; msgs, ConsumeConcurrentlyContext context) {
                System.out.printf(&quot;%s Receive New Messages: %s %n&quot;, Thread.currentThread().getName(), msgs);
                // 标记该消息已经被成功消费
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 启动消费者实例
        consumer.start();
        System.out.printf(&quot;Consumer Started.%n&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;顺序消息&lt;/h3&gt;
&lt;h4&gt;原理解析&lt;/h4&gt;
&lt;p&gt;消息有序指的是一类消息消费时，能按照发送的顺序来消费。例如：一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义，但是同时订单之间是可以并行消费的，RocketMQ 可以严格的保证消息有序。&lt;/p&gt;
&lt;p&gt;顺序消息分为全局顺序消息与分区顺序消息，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局顺序：对于指定的一个 Topic，所有消息按照严格的先入先出（FIFO）的顺序进行发布和消费，适用于性能要求不高，所有的消息严格按照 FIFO 原则进行消息发布和消费的场景&lt;/li&gt;
&lt;li&gt;分区顺序：对于指定的一个 Topic，所有消息根据 Sharding key 进行分区，同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段，和普通消息的 Key 是完全不同的概念，适用于性能要求高的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue（分区队列），而消费消息是从多个 queue 上拉取消息，这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中，消费的时候只从这个 queue 上依次拉取，则就保证了顺序。当&lt;strong&gt;发送和消费参与的 queue 只有一个&lt;/strong&gt;，则是全局有序；如果多个queue 参与，则为分区有序，即相对每个 queue，消息都是有序的&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;一个订单的顺序流程是：创建、付款、推送、完成，订单号相同的消息会被先后发送到同一个队列中，消费时同一个 OrderId 获取到的肯定是同一个队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Producer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer(&quot;please_rename_unique_group_name&quot;);
        producer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        producer.start();
		// 标签集合
        String[] tags = new String[]{&quot;TagA&quot;, &quot;TagC&quot;, &quot;TagD&quot;};

        // 订单列表
        List&amp;lt;OrderStep&amp;gt; orderList = new Producer().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        String dateStr = sdf.format(date);
        for (int i = 0; i &amp;lt; 10; i++) {
            // 加个时间前缀
            String body = dateStr + &quot; Hello RocketMQ &quot; + orderList.get(i);
            Message msg = new Message(&quot;OrderTopic&quot;, tags[i % tags.length], &quot;KEY&quot; + i, body.getBytes());
			/**
             * 参数一：消息对象
             * 参数二：消息队列的选择器
             * 参数三：选择队列的业务标识（订单 ID）
             */
            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                /**
                 * mqs：队列集合
                 * msg：消息对象
                 * arg：业务标识的参数
                 */
                public MessageQueue select(List&amp;lt;MessageQueue&amp;gt; mqs, Message msg, Object arg) {
                    Long id = (Long) arg;
                    long index = id % mqs.size(); // 根据订单id选择发送queue
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

            System.out.println(String.format(&quot;SendResult status:%s, queueId:%d, body:%s&quot;,
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }

        producer.shutdown();
    }

    // 订单的步骤
    private static class OrderStep {
        private long orderId;
        private String desc;
        // set + get
    }

    // 生成模拟订单数据
    private List&amp;lt;OrderStep&amp;gt; buildOrders() {
        List&amp;lt;OrderStep&amp;gt; orderList = new ArrayList&amp;lt;OrderStep&amp;gt;();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc(&quot;创建&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc(&quot;创建&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc(&quot;付款&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc(&quot;创建&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc(&quot;付款&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc(&quot;付款&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc(&quot;完成&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc(&quot;推送&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc(&quot;完成&quot;);
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc(&quot;完成&quot;);
        orderList.add(orderDemo);

        return orderList;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 顺序消息消费，带事务方式（应用可控制Offset什么时候提交）
public class ConsumerInOrder {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(&quot;please_rename_unique_group_name_3&quot;);
        consumer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
        // 如果非第一次启动，那么按照上次消费的位置继续消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
		// 订阅三个tag
        consumer.subscribe(&quot;OrderTopic&quot;, &quot;TagA || TagC || TagD&quot;);
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();
            @Override
            public ConsumeOrderlyStatus consumeMessage(List&amp;lt;MessageExt&amp;gt; msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println(&quot;consumeThread=&quot; + Thread.currentThread().getName() + &quot;queueId=&quot; + msg.getQueueId() + &quot;, content:&quot; + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.println(&quot;Consumer Started.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;延时消息&lt;/h3&gt;
&lt;h4&gt;原理解析&lt;/h4&gt;
&lt;p&gt;定时消息（延迟队列）是指消息发送到 Broker 后，不会立即被消费，等待特定时间投递给真正的 Topic&lt;/p&gt;
&lt;p&gt;RocketMQ 并不支持任意时间的延时，需要设置几个固定的延时等级，从 1s 到 2h 分别对应着等级 1 到 18，消息消费失败会进入延时消息队列，消息发送时间与设置的延时等级和重试次数有关，详见代码 &lt;code&gt;SendMessageProcessor.java&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String messageDelayLevel = &quot;1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Broker 可以配置 messageDelayLevel，该属性是 Broker 的属性，不属于某个 Topic&lt;/p&gt;
&lt;p&gt;发消息时，可以设置延迟等级 &lt;code&gt;msg.setDelayLevel(level)&lt;/code&gt;，level 有以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;level == 0：消息为非延迟消息&lt;/li&gt;
&lt;li&gt;1&amp;lt;=level&amp;lt;=maxLevel：消息延迟特定时间，例如 level==1，延迟 1s&lt;/li&gt;
&lt;li&gt;level &amp;gt; maxLevel：则 level== maxLevel，例如 level==20，延迟 2h&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中，并根据 delayTimeLevel 存入特定的 queue，队列的标识 &lt;code&gt;queueId = delayTimeLevel – 1&lt;/code&gt;，即&lt;strong&gt;一个 queue 只存相同延迟的消息&lt;/strong&gt;，保证具有相同发送延迟的消息能够顺序消费。Broker 会为每个延迟级别提交一个定时任务，调度地消费 SCHEDULE_TOPIC_XXXX，将消息写入真实的 Topic&lt;/p&gt;
&lt;p&gt;注意：定时消息在第一次写入和调度写入真实 Topic 时都会计数，因此发送数量、tps 都会变高&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;提交了一个订单就可以发送一个延时消息，1h 后去检查这个订单的状态，如果还是未付款就取消订单释放库存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ScheduledMessageProducer {
    public static void main(String[] args) throws Exception {
        // 实例化一个生产者来产生延时消息
        DefaultMQProducer producer = new DefaultMQProducer(&quot;ExampleProducerGroup&quot;);
        producer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        // 启动生产者
        producer.start();
        int totalMessagesToSend = 100;
        for (int i = 0; i &amp;lt; totalMessagesToSend; i++) {
            Message message = new Message(&quot;DelayTopic&quot;, (&quot;Hello scheduled message &quot; + i).getBytes());
            // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
            message.setDelayTimeLevel(3);
            // 发送消息
            producer.send(message);
        }
        // 关闭生产者
        producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ScheduledMessageConsumer {
   public static void main(String[] args) throws Exception {
      // 实例化消费者
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(&quot;ExampleConsumer&quot;);
      consumer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
      // 订阅Topics
      consumer.subscribe(&quot;DelayTopic&quot;, &quot;*&quot;);
      // 注册消息监听者
      consumer.registerMessageListener(new MessageListenerConcurrently() {
          @Override
          public ConsumeConcurrentlyStatus consumeMessage(List&amp;lt;MessageExt&amp;gt; messages, ConsumeConcurrentlyContext context) {
              for (MessageExt message : messages) {
                  // 打印延迟的时间段
                  System.out.println(&quot;Receive message[msgId=&quot; + message.getMsgId() + &quot;] &quot; + (System.currentTimeMillis() - message.getBornTimestamp()) + &quot;ms later&quot;);}
              return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
          }
      });
      // 启动消费者
      consumer.start();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;批量消息&lt;/h3&gt;
&lt;p&gt;批量发送消息能显著提高传递小消息的性能，限制是这些批量消息应该有相同的 topic，相同的 waitStoreMsgOK，而且不能是延时消息，并且这一批消息的总大小不应超过 4MB&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Producer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer(&quot;ExampleProducerGroup&quot;)
        producer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        //启动producer
        producer.start();

        List&amp;lt;Message&amp;gt; msgs = new ArrayList&amp;lt;Message&amp;gt;();
        // 创建消息对象，指定主题Topic、Tag和消息体
        Message msg1 = new Message(&quot;BatchTopic&quot;, &quot;Tag1&quot;, (&quot;Hello World&quot; + 1).getBytes());
        Message msg2 = new Message(&quot;BatchTopic&quot;, &quot;Tag1&quot;, (&quot;Hello World&quot; + 2).getBytes());
        Message msg3 = new Message(&quot;BatchTopic&quot;, &quot;Tag1&quot;, (&quot;Hello World&quot; + 3).getBytes());

        msgs.add(msg1);
        msgs.add(msg2);
        msgs.add(msg3);

        // 发送消息
        SendResult result = producer.send(msgs);
        System.out.println(&quot;发送结果:&quot; + result);
        // 关闭生产者producer
        producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当发送大批量数据时，可能不确定消息是否超过了大小限制（4MB），所以需要将消息列表分割一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ListSplitter implements Iterator&amp;lt;List&amp;lt;Message&amp;gt;&amp;gt; {
    private final int SIZE_LIMIT = 1024 * 1024 * 4;
    private final List&amp;lt;Message&amp;gt; messages;
    private int currIndex;

    public ListSplitter(List&amp;lt;Message&amp;gt; messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currIndex &amp;lt; messages.size();
    }

    @Override
    public List&amp;lt;Message&amp;gt; next() {
        int startIndex = getStartIndex();
        int nextIndex = startIndex;
        int totalSize = 0;
        for (; nextIndex &amp;lt; messages.size(); nextIndex++) {
            Message message = messages.get(nextIndex);
            int tmpSize = calcMessageSize(message);
            // 单个消息超过了最大的限制
            if (tmpSize + totalSize &amp;gt; SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List&amp;lt;Message&amp;gt; subList = messages.subList(startIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    private int getStartIndex() {
        Message currMessage = messages.get(currIndex);
        int tmpSize = calcMessageSize(currMessage);
        while (tmpSize &amp;gt; SIZE_LIMIT) {
            currIndex += 1;
            Message message = messages.get(curIndex);
            tmpSize = calcMessageSize(message);
        }
        return currIndex;
    }

    private int calcMessageSize(Message message) {
        int tmpSize = message.getTopic().length() + message.getBody().length;
        Map&amp;lt;String, String&amp;gt; properties = message.getProperties();
        for (Map.Entry&amp;lt;String, String&amp;gt; entry : properties.entrySet()) {
            tmpSize += entry.getKey().length() + entry.getValue().length();
        }
        tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节
        return tmpSize;
    }

    public static void main(String[] args) {
        //把大的消息分裂成若干个小的消息
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            try {
                List&amp;lt;Message&amp;gt; listItem = splitter.next();
                producer.send(listItem);
            } catch (Exception e) {
                e.printStackTrace();
                //处理error
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;过滤消息&lt;/h3&gt;
&lt;h4&gt;基本语法&lt;/h4&gt;
&lt;p&gt;RocketMQ 定义了一些基本语法来支持过滤特性，可以很容易地扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数值比较，比如：&amp;gt;，&amp;gt;=，&amp;lt;，&amp;lt;=，BETWEEN，=&lt;/li&gt;
&lt;li&gt;字符比较，比如：=，&amp;lt;&amp;gt;，IN&lt;/li&gt;
&lt;li&gt;IS NULL 或者 IS NOT NULL&lt;/li&gt;
&lt;li&gt;逻辑符号 AND，OR，NOT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常量支持类型为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数值，比如 123，3.1415&lt;/li&gt;
&lt;li&gt;字符，比如 &apos;abc&apos;，必须用单引号包裹起来&lt;/li&gt;
&lt;li&gt;NULL，特殊的常量&lt;/li&gt;
&lt;li&gt;布尔值，TRUE 或 FALSE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有使用 push 模式的消费者才能用使用 SQL92 标准的 sql 语句，接口如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void subscribe(final String topic, final MessageSelector messageSelector)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：消费者接收包含 TAGA 或 TAGB 或 TAGC 的消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(&quot;CID_EXAMPLE&quot;);
consumer.subscribe(&quot;TOPIC&quot;, &quot;TAGA || TAGB || TAGC&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原理解析&lt;/h4&gt;
&lt;p&gt;RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的，所以是在 Broker 端实现的，优点是减少了对于 Consumer 无用消息的网络传输，缺点是增加了 Broker 的负担，而且实现相对复杂&lt;/p&gt;
&lt;p&gt;RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用&lt;strong&gt;分离存储&lt;/strong&gt;的机制实现，Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引，然后再从 CommitLog 里面读取真正的消息实体内容&lt;/p&gt;
&lt;p&gt;ConsumeQueue 的存储结构如下，有 8 个字节存储的 Message Tag 的哈希值，基于 Tag 的消息过滤就是基于这个字段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%B6%88%E8%B4%B9%E9%98%9F%E5%88%97%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Tag 过滤：Consumer 端订阅消息时指定 Topic 和 TAG，然后将订阅请求构建成一个 SubscriptionData，发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter，然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后，会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断，无法精确对 tag 原始字符串进行过滤，所以消费端拉取到消息后，还需要对消息的原始 tag 字符串进行比对，如果不同，则丢弃该消息，不进行消息消费&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL92 过滤：工作流程和 Tag 过滤大致一样，只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责，每次过滤都去执行 SQL 表达式会影响效率，所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;发送消息时，通过 putUserProperty 来设置消息的属性，SQL92 的表达式上下文为消息的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Producer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer(&quot;please_rename_unique_group_name&quot;);
        producer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        producer.start();
        for (int i = 0; i &amp;lt; 10; i++) {
            Message msg = new Message(&quot;FilterTopic&quot;, &quot;tag&quot;,
               (&quot;Hello RocketMQ &quot; + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 设置一些属性
            msg.putUserProperty(&quot;i&quot;, String.valueOf(i));
            SendResult sendResult = producer.send(msg);
        }
        producer.shutdown();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 SQL 筛选过滤消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Consumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(&quot;please_rename_unique_group_name&quot;);
        consumer.setNamesrvAddr(&quot;127.0.0.1:9876&quot;);
        // 过滤属性大于 5  的消息
        consumer.subscribe(&quot;FilterTopic&quot;, MessageSelector.bySql(&quot;i&amp;gt;5&quot;));

        // 设置回调函数，处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            //接受消息内容
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List&amp;lt;MessageExt&amp;gt; msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println(&quot;consumeThread=&quot; + Thread.currentThread().getName() + &quot;,&quot; + new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 启动消费者consumer
        consumer.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;事务消息&lt;/h3&gt;
&lt;h4&gt;工作流程&lt;/h4&gt;
&lt;p&gt;RocketMQ 支持分布式事务消息，采用了 2PC 的思想来实现了提交事务消息，同时增加一个&lt;strong&gt;补偿逻辑&lt;/strong&gt;来处理二阶段超时或者失败的消息，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;事务消息的大致方案分为两个流程：正常事务消息的发送及提交、事务消息的补偿流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;事务消息发送及提交：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;发送消息（Half 消息），服务器将消息的主题和队列改为半消息状态，并放入半消息队列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端响应消息写入结果（如果写入失败，此时 Half 消息对业务不可见）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据发送结果执行本地事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据本地事务状态执行 Commit 或者 Rollback&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E4%BA%8B%E5%8A%A1%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;补偿机制：用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况，比如出现网络问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broker 服务端通过&lt;strong&gt;对比 Half 消息和 Op 消息&lt;/strong&gt;，对未确定状态的消息推进 CheckPoint&lt;/li&gt;
&lt;li&gt;没有 Commit/Rollback 的事务消息，服务端根据根据半消息的生产者组，到 ProducerManager 中获取生产者（同一个 Group 的 Producer）的会话通道，发起一次回查（&lt;strong&gt;单向请求&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;Producer 收到回查消息，检查事务消息状态表内对应的本地事务的状态&lt;/li&gt;
&lt;li&gt;根据本地事务状态，重新 Commit 或者 Rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RocketMQ 并不会无休止的进行事务状态回查，最大回查 15 次，如果 15 次回查还是无法得知事务状态，则默认回滚该消息，&lt;/p&gt;
&lt;p&gt;回查服务：&lt;code&gt;TransactionalMessageCheckService#run&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;两阶段&lt;/h4&gt;
&lt;h5&gt;一阶段&lt;/h5&gt;
&lt;p&gt;事务消息相对普通消息最大的特点就是&lt;strong&gt;一阶段发送的消息对用户是不可见的&lt;/strong&gt;，因为对于 Half 消息，会备份原消息的主题与消息消费队列，然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC，由于消费组未订阅该主题，故消费端无法消费 Half 类型的消息&lt;/p&gt;
&lt;p&gt;RocketMQ 会开启一个&lt;strong&gt;定时任务&lt;/strong&gt;，从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费，根据生产者组获取一个服务提供者发送回查事务状态请求，根据事务状态来决定是提交或回滚消息&lt;/p&gt;
&lt;p&gt;RocketMQ 的具体实现策略：如果写入的是事务消息，对消息的 Topic 和 Queue 等属性进行替换，同时将原来的 Topic 和 Queue 信息存储到&lt;strong&gt;消息的属性&lt;/strong&gt;中，因为消息的主题被替换，所以消息不会转发到该原主题的消息消费队列，消费者无法感知消息的存在，不会消费&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;二阶段&lt;/h5&gt;
&lt;p&gt;一阶段写入不可见的消息后，二阶段操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果执行 Commit 操作，则需要让消息对用户可见，构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic，构建索引时需要读取出 Half 消息，然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue，生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容，在二阶段时恢复出一条完整的普通消息，然后走一遍消息写入流程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是 Rollback 则需要撤销一阶段的消息，因为消息本就不可见，所以并&lt;strong&gt;不需要真正撤销消息&lt;/strong&gt;（实际上 RocketMQ 也无法去删除一条消息，因为是顺序写文件的）。RocketMQ 为了区分这条消息没有确定状态的消息，采用 Op 消息标识已经确定状态的事务消息（Commit 或者 Rollback）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作&lt;/strong&gt;，两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前将原消息的主题和队列恢复。如果一条事务消息没有对应的 Op 消息，说明这个事务的状态还无法确定（可能是二阶段失败了）&lt;/p&gt;
&lt;p&gt;RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中，通过源码中的方法 &lt;code&gt;TransactionalMessageUtil.buildOpTopic()&lt;/code&gt;，这个主题是一个内部的 Topic（像 Half 消息的 Topic 一样），不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset，这样&lt;strong&gt;通过 Op  消息能索引到 Half 消息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-OP%E6%B6%88%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;h5&gt;使用方式&lt;/h5&gt;
&lt;p&gt;事务消息共有三种状态，提交状态、回滚状态、中间状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TransactionStatus.CommitTransaction：提交事务，允许消费者消费此消息。&lt;/li&gt;
&lt;li&gt;TransactionStatus.RollbackTransaction：回滚事务，代表该消息将被删除，不允许被消费&lt;/li&gt;
&lt;li&gt;TransactionStatus.Unknown：中间状态，代表需要检查消息队列来确定状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用限制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;事务消息不支持延时消息和批量消息&lt;/li&gt;
&lt;li&gt;Broker 配置文件中的参数 &lt;code&gt;transactionTimeout&lt;/code&gt; 为特定时间，事务消息将在特定时间长度之后被检查。当发送事务消息时，还可以通过设置用户属性 &lt;code&gt;CHECK_IMMUNITY_TIME_IN_SECONDS&lt;/code&gt; 来改变这个限制，该参数优先于 &lt;code&gt;transactionTimeout&lt;/code&gt; 参数&lt;/li&gt;
&lt;li&gt;为了避免单个消息被检查太多次而导致半队列消息累积，默认将单个消息的检查次数限制为 15 次，开发者可以通过 Broker 配置文件的 &lt;code&gt;transactionCheckMax&lt;/code&gt; 参数来修改此限制。如果已经检查某条消息超过 N 次（N = &lt;code&gt;transactionCheckMax&lt;/code&gt;）， 则 Broker 将丢弃此消息，在默认情况下会打印错误日志。可以通过重写 &lt;code&gt;AbstractTransactionalMessageCheckListener&lt;/code&gt; 类来修改这个行为&lt;/li&gt;
&lt;li&gt;事务性消息可能不止一次被检查或消费&lt;/li&gt;
&lt;li&gt;提交给用户的目标主题消息可能会失败，可以查看日志的记录。事务的高可用性通过 RocketMQ 本身的高可用性机制来保证，如果希望事务消息不丢失、并且事务完整性得到保证，可以使用同步的双重写入机制&lt;/li&gt;
&lt;li&gt;事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同，事务消息允许反向查询，MQ 服务器能通过消息的生产者 ID 查询到消费者&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;代码实现&lt;/h5&gt;
&lt;p&gt;实现事务的监听接口，当发送半消息成功时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;executeLocalTransaction&lt;/code&gt; 方法来执行本地事务，返回三个事务状态之一&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkLocalTransaction&lt;/code&gt; 方法检查本地事务状态，响应消息队列的检查请求，返回三个事务状态之一&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    private ConcurrentHashMap&amp;lt;String, Integer&amp;gt; localTrans = new ConcurrentHashMap&amp;lt;&amp;gt;();

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        // 将事务ID和状态存入 map 集合
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 从 map 集合读出当前事务对应的状态
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;strong&gt;TransactionMQProducer&lt;/strong&gt; 类创建事务性生产者，并指定唯一的 &lt;code&gt;ProducerGroup&lt;/code&gt;，就可以设置自定义线程池来处理这些检查请求，执行本地事务后，需要根据执行结果对消息队列进行回复&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Producer {
	public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建消息生产者
       	TransactionMQProducer producer = new TransactionMQProducer(&quot;please_rename_unique_group_name&quot;);
       	ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS);
        producer.setExecutorService(executorService);
        
        // 创建事务监听器
		TransactionListener transactionListener = new TransactionListenerImpl();
        // 生产者的监听器
        producer.setTransactionListener(transactionListener);
       	// 启动生产者
        producer.start();
        String[] tags = new String[] {&quot;TagA&quot;, &quot;TagB&quot;, &quot;TagC&quot;, &quot;TagD&quot;, &quot;TagE&quot;};
        for (int i = 0; i &amp;lt; 10; i++) {
            try {
                Message msg = new Message(&quot;TransactionTopic&quot;, tags[i % tags.length], &quot;KEY&quot; + i,
                                (&quot;Hello RocketMQ &quot; + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 发送消息
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf(&quot;%s%n&quot;, sendResult);
                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
       	//Thread.sleep(1000000);
        //producer.shutdown();暂时不关闭
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消费者代码和前面的实例相同的&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;系统特性&lt;/h2&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;h4&gt;模块介绍&lt;/h4&gt;
&lt;p&gt;NameServer 是一个简单的 Topic 路由注册中心，支持 Broker 的动态注册与发现，生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表&lt;/p&gt;
&lt;p&gt;NameServer 主要包括两个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broker 管理，NameServer 接受 Broker 集群的注册信息，保存下来作为路由信息的基本数据，提供&lt;strong&gt;心跳检测机制&lt;/strong&gt;检查 Broker 是否还存活，每 10 秒清除一次两小时没有活跃的 Broker&lt;/li&gt;
&lt;li&gt;路由信息管理，每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息，然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息，从而进行消息的投递和消费&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NameServer 特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NameServer 通常是集群的方式部署，&lt;strong&gt;各实例间相互不进行信息通讯&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Broker 向每一台 NameServer（集群）注册自己的路由信息，所以每个 NameServer 实例上面&lt;strong&gt;都保存一份完整的路由信息&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当某个 NameServer 因某种原因下线了，Broker 仍可以向其它 NameServer 同步其路由信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证，在 RocketMQ 系统中接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备，也存储消息相关的元数据，包括消费者组、消费进度偏移和主题和队列消息等&lt;/p&gt;
&lt;p&gt;Broker 包含了以下几个重要子模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Remoting Module：整个 Broker 的实体，负责处理来自 Clients 端的请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Client Manager：负责管理客户端（Producer/Consumer）和维护 Consumer 的 Topic 订阅信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Store Service：提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HA Service：高可用服务，提供 Master Broker 和 Slave Broker 之间的数据同步功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Index Service：根据特定的 Message key 对投递到 Broker 的消息进行索引服务，以提供消息的快速查询&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Broker%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;总体流程&lt;/h4&gt;
&lt;p&gt;RocketMQ 的工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动 NameServer 监听端口，等待 Broker、Producer、Consumer 连上来，相当于一个路由控制中心&lt;/li&gt;
&lt;li&gt;Broker 启动，跟&lt;strong&gt;所有的 NameServer 保持长连接&lt;/strong&gt;，每隔 30s 时间向 NameServer 上报 Topic 路由信息（心跳包）。心跳包中包含当前 Broker 信息（IP、端口等）以及存储所有 Topic 信息。注册成功后，NameServer 集群中就有 Topic 跟 Broker 的映射关系&lt;/li&gt;
&lt;li&gt;收发消息前，先创建 Topic，创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上，也可以在发送消息时自动创建 Topic&lt;/li&gt;
&lt;li&gt;Producer 启动时先跟 NameServer 集群中的&lt;strong&gt;其中一台&lt;/strong&gt;建立长连接，并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上，同时 Producer 会默认每隔 30s 向 NameServer &lt;strong&gt;定时拉取&lt;/strong&gt;一次路由信息&lt;/li&gt;
&lt;li&gt;Producer 发送消息时，根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息，如果没有则会从 NameServer 上重新拉取并更新，轮询队列列表并选择一个队列 MessageQueue，然后与队列所在的 Broker 建立长连接，向 Broker 发消息&lt;/li&gt;
&lt;li&gt;Consumer 跟 Producer 类似，跟其中一台 NameServer 建立长连接，&lt;strong&gt;定时获取路由信息&lt;/strong&gt;，根据当前订阅 Topic 存在哪些 Broker 上，直接跟 Broker 建立连接通道，在完成客户端的负载均衡后，选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;生产消费&lt;/h4&gt;
&lt;p&gt;At least Once：至少一次，指每个消息必须投递一次，Consumer 先 Pull 消息到本地，消费完成后才向服务器返回 ACK，如果没有消费一定不会 ACK 消息&lt;/p&gt;
&lt;p&gt;回溯消费：指 Consumer 已经消费成功的消息，由于业务上需求需要重新消费，Broker 在向 Consumer 投递成功消息后，消息仍然需要保留。并且重新消费一般是按照时间维度，例如由于 Consumer 系统故障，恢复后需要重新消费 1 小时前的数据，RocketMQ 支持按照时间回溯消费，时间维度精确到毫秒&lt;/p&gt;
&lt;p&gt;分布式队列因为有高可靠性的要求，所以数据要进行&lt;strong&gt;持久化存储&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;消息生产者发送消息&lt;/li&gt;
&lt;li&gt;MQ 收到消息，将消息进行持久化，在存储中新增一条记录&lt;/li&gt;
&lt;li&gt;返回 ACK 给生产者&lt;/li&gt;
&lt;li&gt;MQ push 消息给对应的消费者，然后等待消费者返回 ACK&lt;/li&gt;
&lt;li&gt;如果消息消费者在指定时间内成功返回 ACK，那么 MQ 认为消息消费成功，在存储中删除消息；如果 MQ 在指定时间内没有收到 ACK，则认为消息消费失败，会尝试重新 push 消息，重复执行 4、5、6 步骤&lt;/li&gt;
&lt;li&gt;MQ 删除消息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%B6%88%E6%81%AF%E5%AD%98%E5%8F%96.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;存储机制&lt;/h3&gt;
&lt;h4&gt;存储结构&lt;/h4&gt;
&lt;p&gt;RocketMQ 中 Broker 负责存储消息转发消息，所以以下的结构是存储在 Broker Server 上的，生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成，类似于通道&lt;/p&gt;
&lt;p&gt;RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的，CommitLog 是消息真正的&lt;strong&gt;物理存储&lt;/strong&gt;文件，ConsumeQueue 是消息的逻辑队列，类似数据库的&lt;strong&gt;索引节点&lt;/strong&gt;，存储的是指向物理存储的地址。&lt;strong&gt;每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每条消息都会有对应的索引信息，Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CommitLog：消息主体以及元数据的存储主体，存储 Producer 端写入的消息内容，消息内容不是定长的。消息主要是&lt;strong&gt;顺序写入&lt;/strong&gt;日志文件，单个文件大小默认 1G，偏移量代表下一次写入的位置，当文件写满了就继续写入下一个文件&lt;/li&gt;
&lt;li&gt;ConsumerQueue：消息消费队列，存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件，并根据主题 Topic 检索消息，这是非常低效的。引入 ConsumeQueue 作为消费消息的索引，&lt;strong&gt;保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset&lt;/strong&gt;，消息大小 size 和消息 Tag 的 HashCode 值，每个 ConsumeQueue 文件大小约 5.72M&lt;/li&gt;
&lt;li&gt;IndexFile：为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法，通过 IndexFile 来查找消息的方法&lt;strong&gt;不影响发送与消费消息的主流程&lt;/strong&gt;。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构，故 RocketMQ 的索引文件其底层实现为 &lt;strong&gt;hash 索引&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RocketMQ 采用的是混合型的存储结构，即为 Broker 单个实例下所有的队列共用一个日志数据文件（CommitLog）来存储，多个 Topic 的消息实体内容都存储于一个 CommitLog 中。混合型存储结构针对 Producer 和 Consumer 分别采用了&lt;strong&gt;数据和索引部分相分离&lt;/strong&gt;的存储结构，Producer 发送消息至 Broker 端，然后 Broker 端使用同步或者异步的方式对消息刷盘持久化，保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中，Producer 发送的消息就不会丢失，Consumer 也就肯定有机会去消费这条消息&lt;/p&gt;
&lt;p&gt;服务端支持长轮询模式，当消费者无法拉取到消息后，可以等下一次消息拉取，Broker 允许等待 30s 的时间，只要这段时间内有新消息到达，将直接返回给消费端。RocketMQ 的具体做法是，使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue（逻辑消费队列）和 IndexFile（索引文件）数据&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存映射&lt;/h4&gt;
&lt;p&gt;操作系统分为用户态和内核态，文件操作、网络操作需要涉及这两种形态的切换，需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端，分为两个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;read：读取本地文件内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;write：将读取的内容通过网络发送出去&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%96%87%E4%BB%B6%E4%B8%8E%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;补充：Prog → NET → I/O → 零拷贝部分的笔记详解相关内容&lt;/p&gt;
&lt;p&gt;通过使用 mmap 的方式，可以省去向用户态的内存复制，RocketMQ 充分利用&lt;strong&gt;零拷贝技术&lt;/strong&gt;，提高消息存盘和网络发送的速度&lt;/p&gt;
&lt;p&gt;RocketMQ 通过 MappedByteBuffer 对文件进行读写操作，利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中，将对文件的操作转化为直接对内存地址进行操作，从而极大地提高了文件的读写效率&lt;/p&gt;
&lt;p&gt;MappedByteBuffer 内存映射的方式&lt;strong&gt;限制&lt;/strong&gt;一次只能映射 1.5~2G 的文件至用户态的虚拟内存，所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储，方便一次将整个文件映射至内存&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;页面缓存&lt;/h4&gt;
&lt;p&gt;页缓存（PageCache）是 OS 对文件的缓存，每一页的大小通常是 4K，用于加速对文件的读写。因为 OS 将一部分的内存用作 PageCache，所以程序对文件进行顺序读写的速度几乎接近于内存的读写速度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于数据的写入，OS 会先写入至 Cache 内，随后&lt;strong&gt;通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;对于数据的读取，如果一次读取文件时出现未命中 PageCache 的情况，OS 从物理磁盘上访问读取文件的同时，会顺序对其他相邻块的数据文件进行&lt;strong&gt;预读取&lt;/strong&gt;（局部性原理，最大 128K）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 RocketMQ 中，ConsumeQueue 逻辑消费队列存储的数据较少，并且是顺序读取，在 PageCache 机制的预读取作用下，Consume Queue 文件的读性能几乎接近读内存，即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取，严重影响性能。选择合适的系统 IO 调度算法和固态硬盘，比如设置调度算法为 Deadline，随机读的性能也会有所提升&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;刷盘机制&lt;/h4&gt;
&lt;p&gt;两种持久化的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系型数据库 DB：IO 读写性能比较差，如果 DB 出现故障，则 MQ 的消息就无法落盘存储导致线上故障，可靠性不高&lt;/li&gt;
&lt;li&gt;文件系统：消息刷盘至所部署虚拟机/物理机的文件系统来做持久化，分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式，除非部署 MQ 机器本身或是本地磁盘挂了，一般不会出现无法持久化的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RocketMQ 采用文件系统的方式，无论同步还是异步刷盘，都使用&lt;strong&gt;顺序 IO&lt;/strong&gt;，因为磁盘的顺序读写要比随机读写快很多&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步刷盘：只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应，保障 MQ 消息的可靠性，但是性能上会有较大影响，一般适用于金融业务应用该模式较多&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步刷盘：利用 OS 的 PageCache，只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端，降低了读写延迟，提高了 MQ 的性能和吞吐量。消息刷盘采用&lt;strong&gt;后台异步线程&lt;/strong&gt;提交的方式进行，当内存里的消息量积累到一定程度时，触发写磁盘动作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式，可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%88%B7%E7%9B%98%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/blob/master/docs/cn/design.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;集群设计&lt;/h3&gt;
&lt;h4&gt;集群模式&lt;/h4&gt;
&lt;p&gt;常用的以下几种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单 Master 模式：这种方式风险较大，一旦 Broker 重启或者宕机，会导致整个服务不可用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多 Master 模式：一个集群无 Slave，全是 Master&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：配置简单，单个 Master 宕机或重启维护对应用无影响，在磁盘配置为 RAID10 时，即使机器宕机不可恢复情况下，由于 RAID10 磁盘非常可靠，消息也不会丢（异步刷盘丢失少量消息，同步刷盘一条不丢），性能最高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：单台机器宕机期间，这台机器上未被消费的消息在机器恢复之前不可订阅，消息实时性会受到影响&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多 Master 多 Slave 模式（同步）：每个 Master 配置一个 Slave，有多对 Master-Slave，HA 采用&lt;strong&gt;同步双写&lt;/strong&gt;方式，即只有主备都写成功，才向应用返回成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：数据与服务都无单点故障，Master 宕机情况下，消息无延迟，服务可用性与数据可用性都非常高&lt;/li&gt;
&lt;li&gt;缺点：性能比异步复制略低（大约低 10% 左右），发送单个消息的 RT 略高，目前不能实现主节点宕机，备机自动切换为主机&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多 Master 多 Slave 模式（异步）：HA 采用异步复制的方式，会造成主备有短暂的消息延迟（毫秒级别）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：即使磁盘损坏，消息丢失的非常少，且消息实时性不会受影响，同时 Master 宕机后，消费者仍然可以从 Slave 消费，而且此过程对应用透明，不需要人工干预，性能同多 Master 模式几乎一样&lt;/li&gt;
&lt;li&gt;缺点：Master 宕机，磁盘损坏情况下会丢失少量消息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;集群架构&lt;/h4&gt;
&lt;p&gt;RocketMQ 网络部署特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NameServer 是一个几乎&lt;strong&gt;无状态节点&lt;/strong&gt;，节点之间相互独立，无任何信息同步&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Broker 部署相对复杂，Broker 分为 Master 与 Slave，Master 可以部署多个，一个 Master 可以对应多个 Slave，但是一个 Slave 只能对应一个 Master，Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义，BrokerId 为 0 是 Master，非 0 表示 Slave。&lt;strong&gt;每个 Broker 与 NameServer 集群中的所有节点建立长连接&lt;/strong&gt;，定时注册 Topic 信息到所有 NameServer&lt;/p&gt;
&lt;p&gt;说明：部署架构上也支持一 Master 多 Slave，但只有 BrokerId=1 的从服务器才会参与消息的读负载（读写分离）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Producer 与 NameServer 集群中的其中&lt;strong&gt;一个节点（随机选择）建立长连接&lt;/strong&gt;，定期从 NameServer 获取 Topic 路由信息，并向提供 Topic 服务的 Master 建立长连接，且定时向 Master &lt;strong&gt;发送心跳&lt;/strong&gt;。Producer 完全无状态，可集群部署&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Consumer 与 NameServer 集群中的其中一个节点（随机选择）建立长连接，定期从 NameServer 获取 Topic 路由信息，并向提供  Topic 服务的 Master、Slave 建立长连接，且定时向 Master、Slave 发送心跳&lt;/p&gt;
&lt;p&gt;Consumer 既可以从 Master 订阅消息，也可以从 Slave 订阅消息，在向 Master 拉取消息时，Master 服务器会根据拉取偏移量与最大偏移量的距离（判断是否读老消息，产生读 I/O），以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E9%9B%86%E7%BE%A4%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;高可用性&lt;/h4&gt;
&lt;p&gt;NameServer 节点是无状态的，且各个节点直接的数据是一致的，部分 NameServer 不可用也可以保证 MQ 服务正常运行&lt;/p&gt;
&lt;p&gt;BrokerServer 的高可用通过 Master 和 Slave 的配合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Slave 只负责读，当 Master 不可用，对应的 Slave 仍能保证消息被正常消费&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置多组 Master-Slave 组，其他的 Master-Slave 组也会保证消息的正常发送和消费&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;目前不支持把 Slave 自动转成 Master&lt;/strong&gt;，需要手动停止 Slave 角色的 Broker，更改配置文件，用新的配置文件启动 Broker&lt;/p&gt;
&lt;p&gt;所以需要配置多个 Master 保证可用性，否则一个 Master 挂了导致整体系统的写操作不可用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产端的高可用：在创建 Topic 的时候，把 Topic 的&lt;strong&gt;多个 Message Queue 创建在多个 Broker 组&lt;/strong&gt;上（相同 Broker 名称，不同 brokerId 的机器），当一个 Broker 组的 Master 不可用后，其他组的 Master 仍然可用，Producer 仍然可以发送消息&lt;/p&gt;
&lt;p&gt;消费端的高可用：在 Consumer 的配置文件中，并不需要设置是从 Master Broker 读还是从 Slave 读，当 Master 不可用或者繁忙的时候，Consumer 会被自动切换到从 Slave 读。有了自动切换的机制，当一个 Master 机器出现故障后，Consumer 仍然可以从 Slave 读取消息，不影响 Consumer 程序，达到了消费端的高可用性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E9%AB%98%E5%8F%AF%E7%94%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;主从复制&lt;/h4&gt;
&lt;p&gt;如果一个 Broker 组有 Master 和 Slave，消息需要从 Master 复制到 Slave 上，有同步和异步两种复制方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步复制方式：Master 和 Slave 均写成功后才反馈给客户端写成功状态（写 Page Cache）。在同步复制方式下，如果 Master 出故障， Slave 上有全部的备份数据，容易恢复，但是同步复制会增大数据写入延迟，降低系统吞吐量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步复制方式：只要 Master 写成功，即可反馈给客户端写成功状态，系统拥有较低的延迟和较高的吞吐量，但是如果 Master 出了故障，有些数据因为没有被写入 Slave，有可能会丢失&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的，可以设置成 ASYNC_MASTE、RSYNC_MASTER、SLAVE 三个值中的一个&lt;/p&gt;
&lt;p&gt;一般把刷盘机制配置成 ASYNC_FLUSH，主从复制为 SYNC_MASTER，这样即使有一台机器出故障，仍然能保证数据不丢&lt;/p&gt;
&lt;p&gt;RocketMQ 支持消息的高可靠，影响消息可靠性的几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Broker 非正常关闭&lt;/li&gt;
&lt;li&gt;Broker 异常 Crash&lt;/li&gt;
&lt;li&gt;OS Crash&lt;/li&gt;
&lt;li&gt;机器掉电，但是能立即恢复供电情况&lt;/li&gt;
&lt;li&gt;机器无法开机（可能是 CPU、主板、内存等关键设备损坏）&lt;/li&gt;
&lt;li&gt;磁盘设备损坏&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;前四种情况都属于硬件资源可立即恢复情况，RocketMQ 在这四种情况下能保证消息不丢，或者丢失少量数据（依赖刷盘方式）&lt;/p&gt;
&lt;p&gt;后两种属于单点故障，且无法恢复，一旦发生，在此单点上的消息全部丢失。RocketMQ 在这两种情况下，通过主从异步复制，可保证 99% 的消息不丢，但是仍然会有极少量的消息可能丢失。通过&lt;strong&gt;同步双写技术&lt;/strong&gt;可以完全避免单点，但是会影响性能，适合对消息可靠性要求极高的场合，RocketMQ 从 3.0 版本开始支持同步双写&lt;/p&gt;
&lt;p&gt;一般而言，我们会建议采取同步双写 + 异步刷盘的方式，在消息的可靠性和性能间有一个较好的平衡&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;负载均衡&lt;/h3&gt;
&lt;h4&gt;生产端&lt;/h4&gt;
&lt;p&gt;RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡&lt;/p&gt;
&lt;p&gt;Producer 端在发送消息时，会先根据 Topic 找到指定的 TopicPublishInfo，在获取了 TopicPublishInfo 路由信息后，RocketMQ 的客户端在默认方式调用 &lt;code&gt;selectOneMessageQueue()&lt;/code&gt; 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息&lt;/p&gt;
&lt;p&gt;默认会&lt;strong&gt;轮询所有的 Message Queue 发送&lt;/strong&gt;，以让消息平均落在不同的 queue 上，而由于 queue可以散落在不同的 Broker，所以消息就发送到不同的 Broker 下，图中箭头线条上的标号代表顺序，发布方会把第一条消息发送至 Queue 0，然后第二条消息发送至 Queue 1，以此类推：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-producer%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;容错策略均在 MQFaultStrategy 这个类中定义，有一个 sendLatencyFaultEnable 开关变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果开启，会在&lt;strong&gt;随机（只有初始化索引变量时才随机，正常都是递增）递增取模&lt;/strong&gt;的基础上，再过滤掉 not available 的 Broker&lt;/li&gt;
&lt;li&gt;如果关闭，采用随机递增取模的方式选择一个队列（MessageQueue）来发送消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在，对之前失败的，按一定的时间做退避。例如上次请求的 latency 超过 550Lms，就退避 3000Lms；超过 1000L，就退避 60000L&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;消费端&lt;/h4&gt;
&lt;p&gt;在 RocketMQ 中，Consumer 端的两种消费模式（Push/Pull）都是基于拉模式来获取消息的，而在 Push 模式只是对 Pull 模式的一种封装，其本质实现为消息拉取线程在从服务器拉取到一批消息，提交到消息消费线程池后，又继续向服务器再次尝试拉取消息，如果未拉取到消息，则延迟一下又继续拉取&lt;/p&gt;
&lt;p&gt;在两种基于拉模式的消费方式（Push/Pull）中，均需要 Consumer 端在知道从 Broker 端的哪一个消息队列中去获取消息，所以在 Consumer 端来做负载均衡，即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例，所以不存在负载均衡，在实现上，Consumer 分配 queue 时，所有 Consumer 都分到所有的 queue。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在集群消费模式下，每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可，RocketMQ 采用主动拉取的方式拉取并消费消息，在拉取的时候需要明确指定拉取哪一条 Message Queue&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集群模式下，每当消费者实例的数量有变更，都会触发一次所有实例的负载均衡，这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%B9%B3%E5%9D%87%E9%98%9F%E5%88%97%E5%88%86%E9%85%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle，以环状轮流均分 queue 的形式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%B9%B3%E5%9D%87%E9%98%9F%E5%88%97%E8%BD%AE%E6%B5%81%E5%88%86%E9%85%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;集群模式下，&lt;strong&gt;queue 都是只允许分配一个实例&lt;/strong&gt;，如果多个实例同时消费一个 queue 的消息，由于拉取哪些消息是 Consumer 主动控制的，会导致同一个消息在不同的实例下被消费多次&lt;/p&gt;
&lt;p&gt;通过增加 Consumer 实例去分摊 queue 的消费，可以起到水平扩展的消费能力的作用。而当有实例下线时，会重新触发负载均衡，这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话，多出来的 Consumer 实例将无法分到 queue，也就无法消费到消息，也就无法起到分摊负载的作用了，所以需要&lt;strong&gt;控制让 queue 的总数量大于等于 Consumer 的数量&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;原理解析&lt;/h4&gt;
&lt;p&gt;在 Consumer 启动后，会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broker 端在收到 Consumer 的心跳消息后，会将它维护在 ConsumerManager 的本地缓存变量 consumerTable，同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中，为 Consumer 端的负载均衡提供可以依据的元数据信息&lt;/p&gt;
&lt;p&gt;Consumer 端实现负载均衡的核心类 &lt;strong&gt;RebalanceImpl&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例，完成负载均衡服务线程 RebalanceService 的启动（&lt;strong&gt;每隔 20s 执行一次&lt;/strong&gt;负载均衡），RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法，该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据广播模式还是集群模式做不同的逻辑处理。主要看集群模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中，获取该 Topic 主题下的消息消费队列集合 mqSet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据 Topic 和 consumerGroup 为参数调用 &lt;code&gt;mQClientFactory.findConsumerIdList()&lt;/code&gt; 方法向 Broker 端发送获取该消费组下消费者 ID 列表的 RPC 通信请求（Broker 端基于前面 Consumer 端上报的心跳包数据而构建的 consumerTable 做出响应返回，业务请求码 &lt;code&gt;GET_CONSUMER_LIST_BY_GROUP&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;先对 Topic 下的消息消费队列、消费者 ID 排序，然后用消息队列分配策略算法（默认是消息队列的平均分配算法），计算出待拉取的消息队列。平均分配算法类似于分页的算法，将所有 MessageQueue 排好序类似于记录，将所有消费端 Consumer 排好序类似页数，并求出每一页需要包含的平均 size 和每个页面记录的范围 range，最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录（这里即为 MessageQueue）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用 updateProcessQueueTableInRebalance() 方法，先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E9%87%8D%E6%96%B0%E5%B9%B3%E8%A1%A1%E7%AE%97%E6%B3%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processQueueTable 标注的红色部分，表示与分配到的消息队列集合 mqSet 互不包含，将这些队列设置 Dropped 属性为 true，然后查看这些队列是否可以移除出 processQueueTable 缓存变量。具体执行 removeUnnecessaryMessageQueue() 方法，即每隔 1s  查看是否可以获取当前消费处理队列的锁，拿到的话返回 true；如果等待 1s 后，仍然拿不到当前消费处理队列的锁则返回 false。如果返回 true，则从 processQueueTable 缓存变量中移除对应的 Entry&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processQueueTable 的绿色部分，表示与分配到的消息队列集合 mqSet 的交集，判断该 ProcessQueue 是否已经过期了，在 Pull 模式的不用管，如果是 Push 模式的，设置 Dropped 属性为 true，并且调用 removeUnnecessaryMessageQueue() 方法，像上面一样尝试移除 Entry&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为过滤后的消息队列集合 mqSet 中每个 MessageQueue 创建 ProcessQueue 对象存入 RebalanceImpl 的 processQueueTable 队列中（其中调用 RebalanceImpl 实例的 &lt;code&gt;computePullFromWhere(MessageQueue mq)&lt;/code&gt; 方法获取该 MessageQueue 对象的下一个进度消费值 offset，随后填充至接下来要创建的 pullRequest 对象属性中），并&lt;strong&gt;创建拉取请求对象&lt;/strong&gt; pullRequest 添加到拉取列表 pullRequestList 中，最后执行 dispatchPullRequest() 方法，将 Pull 消息的请求对象 PullRequest 放入 PullMessageService 服务线程的&lt;strong&gt;阻塞队列&lt;/strong&gt; pullRequestQueue 中，待该服务线程取出后向 Broker 端发起 Pull 消息的请求&lt;/p&gt;
&lt;p&gt;对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法，RebalancePullImpl 类里面的该方法为空&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消息消费队列在&lt;strong&gt;同一消费组不同消费者之间的负载均衡&lt;/strong&gt;，其核心设计理念是在&lt;strong&gt;一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费，一个消息消费者能同时消费多个消息队列&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;消息查询&lt;/h3&gt;
&lt;h4&gt;查询方式&lt;/h4&gt;
&lt;p&gt;RocketMQ 支持按照两种维度进行消息查询：按照 Message ID 查询消息、按照 Message Key 查询消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RocketMQ 中的 MessageID 的长度总共有 16 字节，其中包含了消息存储主机地址（IP 地址和端口），消息 Commit Log offset&lt;/p&gt;
&lt;p&gt;实现方式：Client 端从 MessageID 中解析出 Broker 的地址（IP 地址和端口）和 Commit Log 的偏移地址，封装成一个 RPC 请求后通过 Remoting 通信层发送（业务请求码 VIEW_MESSAGE_BY_ID）。Broker 端走的是 QueryMessageProcessor，读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按照 Message Key 查询消息，IndexFile 索引文件为提供了通过 Message Key 查询消息的服务&lt;/p&gt;
&lt;p&gt;实现方式：通过 Broker 端的 QueryMessageProcessor 业务处理器来查询，读取消息的过程用 &lt;strong&gt;Topic 和 Key&lt;/strong&gt; 找到 IndexFile 索引文件中的一条记录，根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;索引机制&lt;/h4&gt;
&lt;p&gt;RocketMQ 的索引文件逻辑结构，类似 JDK 中 HashMap 的实现，具体结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-IndexFile%E7%B4%A2%E5%BC%95%E6%96%87%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;IndexFile 文件的存储在 &lt;code&gt;$HOME\store\index${fileName}&lt;/code&gt;，文件名 fileName 是以创建时的时间戳命名，文件大小是固定的，等于 &lt;code&gt;40+500W*4+2000W*20= 420000040&lt;/code&gt; 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性，就用 &lt;code&gt;topic + “#” + UNIQ_KEY&lt;/code&gt; 作为 key 来做写入操作；如果消息设置了 KEYS 属性（多个 KEY 以空格分隔），也会用 &lt;code&gt;topic + “#” + KEY&lt;/code&gt; 来做索引&lt;/p&gt;
&lt;p&gt;整个 Index File 的结构如图，40 Byte 的 Header 用于保存一些总的统计信息，&lt;code&gt;4*500W&lt;/code&gt; 的 Slot Table 并不保存真正的索引数据，而是保存每个槽位对应的单向链表的&lt;strong&gt;头指针&lt;/strong&gt;，即一个 Index File 可以保存 2000W 个索引，&lt;code&gt;20*2000W&lt;/code&gt; 是&lt;strong&gt;真正的索引数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段，一共 20 Byte&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NextIndex offset 即前面读出来的 slotValue，如果有 hash 冲突，就可以用这个字段将所有冲突的索引用链表的方式串起来&lt;/li&gt;
&lt;li&gt;Timestamp 记录的是消息 storeTimestamp 之间的差，并不是一个绝对的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文档：https://github.com/apache/rocketmq/blob/master/docs/cn/design.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;消息重试&lt;/h3&gt;
&lt;h4&gt;消息重投&lt;/h4&gt;
&lt;p&gt;生产者在发送消息时，同步消息和异步消息失败会重投，oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失，但当出现消息量大、网络抖动时，可能会造成消息重复；生产者主动重发、Consumer 负载变化也会导致重复消息&lt;/p&gt;
&lt;p&gt;如下方法可以设置消息重投策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retryTimesWhenSendFailed：同步发送失败重投次数，默认为 2，因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker，尝试向其他 Broker 发送，&lt;strong&gt;最大程度保证消息不丢&lt;/strong&gt;。超过重投次数抛出异常，由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投&lt;/li&gt;
&lt;li&gt;retryTimesWhenSendAsyncFailed：异步发送失败重试次数，异步重试不会选择其他 Broker，仅在同一个 Broker 上做重试，&lt;strong&gt;不保证消息不丢&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;retryAnotherBrokerWhenNotStoreOK：消息刷盘（主或备）超时或 slave 不可用（返回状态非 SEND_OK），是否尝试发送到其他  Broker，默认 false，十分重要消息可以开启&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果同步模式发送失败，则选择到下一个 Broker，如果异步模式发送失败，则&lt;strong&gt;只会在当前 Broker 进行重试&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;发送消息超时时间默认 3000 毫秒，就不会再尝试重试&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;消息重试&lt;/h4&gt;
&lt;p&gt;Consumer 消费消息失败后，提供了一种重试机制，令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于消息本身的原因，例如反序列化失败，消息数据本身无法处理等。这种错误通常需要跳过这条消息，再消费其它消息，而这条失败的消息即使立刻重试消费，99% 也不成功，所以需要提供一种定时重试机制，即过 10 秒后再重试&lt;/li&gt;
&lt;li&gt;由于依赖的下游应用服务不可用，例如 DB 连接不可用，外系统网络不可达等。这种情况即使跳过当前失败的消息，消费其他消息同样也会报错，这种情况建议应用 sleep 30s，再消费下一条消息，这样可以减轻 Broker 重试消息的压力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RocketMQ 会为每个消费组都设置一个 Topic 名称为 &lt;code&gt;%RETRY%+consumerGroup&lt;/code&gt; 的重试队列（这个 Topic 的重试队列是&lt;strong&gt;针对消费组&lt;/strong&gt;，而不是针对每个 Topic 设置的），用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;顺序消息的重试，当消费者消费消息失败后，消息队列 RocketMQ 会自动不断进行消息重试（每次间隔时间为 1 秒），这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时，必须保证应用能够及时监控并处理消费失败的情况，避免阻塞现象的发生&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无序消息（普通、定时、延时、事务消息）的重试，可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效，广播方式不提供失败重试特性，即消费失败后，失败消息不再重试，继续消费新的消息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;无序消息情况下&lt;/strong&gt;，因为异常恢复需要一些时间，会为重试队列设置多个重试级别，每个重试级别都有对应的重新投递延时，重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 &lt;code&gt;SCHEDULE_TOPIC_XXXX&lt;/code&gt; 的延迟队列中，后台定时任务&lt;strong&gt;按照对应的时间进行 Delay 后&lt;/strong&gt;重新保存至 &lt;code&gt;%RETRY%+consumerGroup&lt;/code&gt; 的重试队列中&lt;/p&gt;
&lt;p&gt;消息队列 RocketMQ 默认允许每条消息最多重试 16 次，每次重试的间隔时间如下表示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;第几次重试&lt;/th&gt;
&lt;th&gt;与上次重试的间隔时间&lt;/th&gt;
&lt;th&gt;第几次重试&lt;/th&gt;
&lt;th&gt;与上次重试的间隔时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10 秒&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;7 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;30 秒&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1 分钟&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;9 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2 分钟&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;10 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3 分钟&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;20 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4 分钟&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;30 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5 分钟&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;1 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6 分钟&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果消息重试 16 次后仍然失败，消息将&lt;strong&gt;不再投递&lt;/strong&gt;，如果严格按照上述重试时间间隔计算，某条消息在一直消费失败的前提下，将会在接下来的 4 小时 46 分钟之内进行 16 次重试，超过这个时间范围消息将不再重试投递&lt;/p&gt;
&lt;p&gt;时间间隔不支持自定义配置，最大重试次数可通过自定义参数 &lt;code&gt;MaxReconsumeTimes&lt;/code&gt; 取值进行配置，若配置超过 16 次，则超过的间隔时间均为 2 小时&lt;/p&gt;
&lt;p&gt;说明：一条消息无论重试多少次，&lt;strong&gt;消息的 Message ID 是不会改变的&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;重试操作&lt;/h4&gt;
&lt;p&gt;集群消费方式下，消息消费失败后期望消息重试，需要在消息监听器接口的实现中明确进行配置（三种方式任选一种）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回 Action.ReconsumeLater （推荐）&lt;/li&gt;
&lt;li&gt;返回 null&lt;/li&gt;
&lt;li&gt;抛出异常&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        // 处理消息
        doConsumeMessage(message);
        //方式1：返回 Action.ReconsumeLater，消息将重试
        return Action.ReconsumeLater;
        //方式2：返回 null，消息将重试
        return null;
        //方式3：直接抛出异常， 消息将重试
        throw new RuntimeException(&quot;Consumer Message exceotion&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群消费方式下，消息失败后期望消息不重试，需要捕获消费逻辑中可能抛出的异常，最终返回 Action.CommitMessage，此后这条消息将不会再重试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        try {
            doConsumeMessage(message);
        } catch (Throwable e) {
            // 捕获消费逻辑中的所有异常，并返回 Action.CommitMessage;
            return Action.CommitMessage;
        }
        //消息处理正常，直接返回 Action.CommitMessage;
        return Action.CommitMessage;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义消息最大重试次数，RocketMQ 允许 Consumer 启动的时候设置最大重试次数，重试时间间隔将按照如下策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大重试次数小于等于 16 次，则重试时间间隔同上表描述&lt;/li&gt;
&lt;li&gt;最大重试次数大于 16 次，超过 16 次的重试时间间隔均为每次 2 小时&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Properties properties = new Properties();
// 配置对应 Group ID 的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,&quot;20&quot;);
Consumer consumer = ONSFactory.createConsumer(properties);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。例如只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes，那么该配置对两个 Consumer 实例均生效&lt;/li&gt;
&lt;li&gt;配置采用覆盖的方式生效，即最后启动的 Consumer 实例会覆盖之前的启动实例的配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者收到消息后，可按照如下方式获取消息的重试次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        // 获取消息的重试次数
        System.out.println(message.getReconsumeTimes());
        return Action.CommitMessage;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;死信队列&lt;/h3&gt;
&lt;p&gt;正常情况下无法被消费的消息称为死信消息（Dead-Letter Message），存储死信消息的特殊队列称为死信队列（Dead-Letter Queue）&lt;/p&gt;
&lt;p&gt;当一条消息初次消费失败，消息队列 RocketMQ 会自动进行消息重试，达到最大重试次数后，若消费依然失败，则表明消费者在正常情况下无法正确地消费该消息，此时 RocketMQ 不会立刻将消息丢弃，而是将其发送到该消费者对应的死信队列中&lt;/p&gt;
&lt;p&gt;死信消息具有以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不会再被消费者正常消费&lt;/li&gt;
&lt;li&gt;有效期与正常消息相同，均为 3 天，3 天后会被自动删除，所以请在死信消息产生后的 3 天内及时处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;死信队列具有以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个死信队列对应一个 Group ID， 而不是对应单个消费者实例&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果一个 Group ID 未产生死信消息，消息队列 RocketMQ 不会为其创建相应的死信队列&lt;/li&gt;
&lt;li&gt;一个死信队列包含了对应 Group ID 产生的所有死信消息，不论该消息属于哪个 Topic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一条消息进入死信队列，需要排查可疑因素并解决问题后，可以在消息队列 RocketMQ 控制台重新发送该消息，让消费者重新消费一次&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;高可靠性&lt;/h3&gt;
&lt;p&gt;RocketMQ 消息丢失可能发生在以下三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产阶段：消息在 Producer 发送端创建出来，经过网络传输发送到 Broker 存储端
&lt;ul&gt;
&lt;li&gt;生产者得到一个成功的响应，就可以认为消息的存储和消息的消费都是可靠的&lt;/li&gt;
&lt;li&gt;消息重投机制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储阶段：消息在 Broker 端存储，如果是主备或者多副本，消息会在这个阶段被复制到其他的节点或者副本上
&lt;ul&gt;
&lt;li&gt;单点：刷盘机制（同步或异步）&lt;/li&gt;
&lt;li&gt;主从：消息同步机制（异步复制或同步双写，主从复制章节详解）&lt;/li&gt;
&lt;li&gt;过期删除：操作 CommitLog、ConsumeQueue 文件是基于文件内存映射机制，并且在启动的时候会将所有的文件加载，为了避免内存与磁盘的浪费，让磁盘能够循环利用，防止磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平，最终保证新写入消息的可靠存储&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消费阶段：Consumer 消费端从 Broker存储端拉取消息，经过网络传输发送到 Consumer 消费端上
&lt;ul&gt;
&lt;li&gt;消息重试机制来最大限度的保证消息的消费&lt;/li&gt;
&lt;li&gt;消费失败的进行消息回退，重试次数过多的消息放入死信队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐文章：https://cdn.modb.pro/db/394751&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;幂等消费&lt;/h3&gt;
&lt;p&gt;消息队列 RocketMQ 消费者在接收到消息以后，需要根据业务上的唯一 Key 对消息做幂等处理&lt;/p&gt;
&lt;p&gt;At least Once 机制保证消息不丢失，但是可能会造成消息重复，RocketMQ 中无法避免消息重复（Exactly-Once），在互联网应用中，尤其在网络不稳定的情况下，几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;发送时消息重复：当一条消息已被成功发送到服务端并完成持久化，此时出现了网络闪断或客户端宕机，导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息，消费者后续会收到两条内容相同并且 Message ID 也相同的消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;投递时消息重复：消息消费的场景下，消息已投递到消费者并完成业务处理，当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次，消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息，消费者后续会收到两条内容相同并且 Message ID 也相同的消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负载均衡时消息重复：当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时，会触发 Rebalance，此时消费者可能会收到重复消息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为 Message ID 有可能出现冲突（重复）的情况，所以真正安全的幂等处理，不建议以 Message ID 作为处理依据，最好的方式是以业务唯一标识作为幂等处理的关键依据，而业务的唯一标识可以通过消息 Key 进行设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Message message = new Message();
message.setKey(&quot;ORDERID_100&quot;);
SendResult sendResult = producer.send(message);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;订阅方收到消息时可以根据消息的 Key 进行幂等处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;consumer.subscribe(&quot;ons_test&quot;, &quot;*&quot;, new MessageListener() {
    public Action consume(Message message, ConsumeContext context) {
        String key = message.getKey()
        // 根据业务唯一标识的 key 做幂等处理
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;流量控制&lt;/h3&gt;
&lt;p&gt;生产者流控，因为 Broker 处理能力达到瓶颈；消费者流控，因为消费能力达到瓶颈&lt;/p&gt;
&lt;p&gt;生产者流控：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CommitLog 文件被锁时间超过 osPageCacheBusyTimeOutMills 时，参数默认为 1000ms，返回流控&lt;/li&gt;
&lt;li&gt;如果开启 transientStorePoolEnable == true，且 Broker 为异步刷盘的主机，且 transientStorePool 中资源不足，拒绝当前 send 请求，返回流控&lt;/li&gt;
&lt;li&gt;Broker 每隔 10ms 检查 send 请求队列头部请求的等待时间，如果超过 waitTimeMillsInSendQueue，默认 200ms，拒绝当前 send 请求，返回流控。&lt;/li&gt;
&lt;li&gt;Broker 通过拒绝 send 请求方式实现流量控制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：生产者流控，不会尝试消息重投&lt;/p&gt;
&lt;p&gt;消费者流控：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费者本地缓存消息数超过 pullThresholdForQueue 时，默认 1000&lt;/li&gt;
&lt;li&gt;消费者本地缓存消息大小超过 pullThresholdSizeForQueue 时，默认 100MB&lt;/li&gt;
&lt;li&gt;消费者本地缓存消息跨度超过 consumeConcurrentlyMaxSpan 时，默认 2000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者流控的结果是降低拉取频率&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;原理解析&lt;/h2&gt;
&lt;h3&gt;Namesrv&lt;/h3&gt;
&lt;h4&gt;服务启动&lt;/h4&gt;
&lt;h5&gt;启动方法&lt;/h5&gt;
&lt;p&gt;NamesrvStartup 类中有 Namesrv 服务的启动方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    // 如果启动时 使用 -c  -p  设置参数了，这些参数存储在 args 中
    main0(args);
}

public static NamesrvController main0(String[] args) {
    try {
        // 创建 namesrv 控制器，用来初始化 namesrv 启动 namesrv 关闭 namesrv
        NamesrvController controller = createNamesrvController(args);
		// 启动 controller
        start(controller);
        return controller;
    } catch (Throwable e) {
        // 出现异常，停止系统
        System.exit(-1);
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NamesrvStartup#createNamesrvController：读取配置信息，初始化 Namesrv 控制器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ServerUtil.parseCmdLine(&quot;mqnamesrv&quot;, args, buildCommandlineOptions(options)，..)&lt;/code&gt;：解析启动时的参数信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;namesrvConfig = new NamesrvConfig()&lt;/code&gt;：创建 Namesrv 配置对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private String rocketmqHome&lt;/code&gt;：获取 ROCKETMQ_HOME 值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private boolean orderMessageEnable = false&lt;/code&gt;：&lt;strong&gt;顺序消息&lt;/strong&gt;功能是否开启&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nettyServerConfig = new NettyServerConfig()&lt;/code&gt;：Netty 的服务器配置对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nettyServerConfig.setListenPort(9876)&lt;/code&gt;：Namesrv 服务器的&lt;strong&gt;监听端口设置为 9876&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (commandLine.hasOption(&apos;c&apos;))&lt;/code&gt;：读取命令行 -c 的参数值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;in = new BufferedInputStream(new FileInputStream(file))&lt;/code&gt;：读取指定目录的配置文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;properties.load(in)&lt;/code&gt;：将配置文件信息加载到 properties 对象，相关属性会复写到 Namesrv 配置和 Netty 配置对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;namesrvConfig.setConfigStorePath(file)&lt;/code&gt;：将配置文件的路径保存到配置保存字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == namesrvConfig.getRocketmqHome())&lt;/code&gt;：检查 ROCKETMQ_HOME 配置是否是空，是空就报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lc = (LoggerContext) LoggerFactory.getILoggerFactory()&lt;/code&gt;：创建日志对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;controller = new NamesrvController(namesrvConfig, nettyServerConfig)&lt;/code&gt;：&lt;strong&gt;创建 Namesrv 控制器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NamesrvStartup#start：启动 Namesrv 控制器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean initResult = controller.initialize()&lt;/code&gt;：初始化方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; Runtime.getRuntime().addShutdownHook(new ShutdownHookThread())&lt;/code&gt;：JVM HOOK 平滑关闭的逻辑， 当 JVM 被关闭时，主动调用 controller.shutdown() 方法，让服务器平滑关机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;controller.start()&lt;/code&gt;：启动服务器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源码解析参考视频：https://space.bilibili.com/457326371&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;控制器类&lt;/h5&gt;
&lt;p&gt;NamesrvController 用来初始化和启动 Namesrv 服务器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ScheduledExecutorService scheduledExecutorService;	// 调度线程池，用来执行定时任务
private final RouteInfoManager routeInfoManager;					// 管理【路由信息】的对象
private RemotingServer remotingServer;								// 【网络层】封装对象
private BrokerHousekeepingService brokerHousekeepingService;		// 用于监听 channel 状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;private ExecutorService remotingExecutor&lt;/code&gt;：业务线程池，&lt;strong&gt;netty 线程解析报文成 RemotingCommand 对象，然后将该对象交给业务线程池再继续处理&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean initialize() {
    // 加载本地kv配置（我还不明白 kv 配置是啥）
    this.kvConfigManager.load();
    // 创建网络服务器对象，【将 netty 的配置和监听器传入】
    // 监听器监听 channel 状态的改变，会向事件队列发起事件，最后交由 service 处理
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
    // 【创建业务线程池，默认线程数 8】
    this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().);

    // 注册协议处理器（缺省协议处理器），【处理器是 DefaultRequestProcessor】，线程使用的是刚创建的业务的线程池
    this.registerProcessor();

    // 定时任务1：每 10 秒钟检查 broker 存活状态，将 IDLE 状态的 broker 移除【扫描机制，心跳检测】
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            // 扫描 brokerLiveTable 表，将两小时没有活动的 broker 关闭，
            // 通过 next.getKey() 获取 broker 的地址，然后【关闭服务器与broker物理节点的 channel】
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);

    // 定时任务2：每 10 分钟打印一遍 kv 配置。
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            NamesrvController.this.kvConfigManager.printAllPeriodically();
        }
    }, 1, 10, TimeUnit.MINUTES);

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() throws Exception {
    // 服务器网络层启动。
    this.remotingServer.start();

    if (this.fileWatchService != null) {
        this.fileWatchService.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;网络通信&lt;/h4&gt;
&lt;h5&gt;通信原理&lt;/h5&gt;
&lt;p&gt;RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库，同样也遵循了 Reactor 多线程模型，NettyRemotingServer 类负责框架的通信服务，同时又在这之上做了一些扩展和优化&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Reactor%E8%AE%BE%E8%AE%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个 Reactor 主线程（eventLoopGroupBoss）负责监听 TCP 网络连接请求，建立好连接创建 SocketChannel（RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll，也可以通过参数配置），并注册到 Selector 上，然后监听真正的网络数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拿到网络数据交给 Worker 线程池（eventLoopGroupSelector，默认设置为 3），在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理，这些工作交给 defaultEventExecutorGroup（默认设置为 8）去做&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理业务操作放在业务线程池中执行，根据 RomotingCommand 的&lt;strong&gt;业务请求码 code&lt;/strong&gt; 去 processorTable 这个本地缓存变量中找到对应的 processor，封装成 task 任务提交给对应的 processor 处理线程池来执行（sendMessageExecutor，以发送消息为例）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从入口到业务逻辑的几个步骤中线程池一直再增加，这跟每一步逻辑复杂性相关，越复杂，需要的并发通道越宽&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;线程数&lt;/th&gt;
&lt;th&gt;线程名&lt;/th&gt;
&lt;th&gt;线程具体说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NettyBoss_%d&lt;/td&gt;
&lt;td&gt;Reactor 主线程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;NettyServerEPOLLSelector_%d_%d&lt;/td&gt;
&lt;td&gt;Reactor 线程池&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M1&lt;/td&gt;
&lt;td&gt;NettyServerCodecThread_%d&lt;/td&gt;
&lt;td&gt;Worker 线程池&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M2&lt;/td&gt;
&lt;td&gt;RemotingExecutorThread_%d&lt;/td&gt;
&lt;td&gt;业务 processor 处理线程池&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RocketMQ 的异步通信流程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%BC%82%E6%AD%A5%E9%80%9A%E4%BF%A1%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;==todo：后期对 Netty 有了更深的认知后会进行扩充，现在暂时 copy 官方文档==&lt;/p&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;NettyRemotingServer 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器相关属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ServerBootstrap serverBootstrap;				// netty 服务端启动对象
private final EventLoopGroup eventLoopGroupSelector;		// netty worker 组线程池，【默认 3 个线程】
private final EventLoopGroup eventLoopGroupBoss;			// netty boss 组线程池，【一般是 1 个线程】
private final NettyServerConfig nettyServerConfig;			// netty 服务端网络配置
private int port = 0;										// 服务器绑定的端口
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公共线程池：注册处理器时如果未指定线程池，则业务处理使用公共线程池，线程数量默认是 4&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ExecutorService publicExecutor;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件监听器：Nameserver 使用 BrokerHouseKeepingService，Broker 使用 ClientHouseKeepingService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ChannelEventListener channelEventListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件处理线程池：默认是 8&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private DefaultEventExecutorGroup defaultEventExecutorGroup;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时器：执行循环任务，并且将定时器线程设置为守护线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; private final Timer timer = new Timer(&quot;ServerHouseKeepingService&quot;, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理器：多个 Channel 共享的处理器 Handler，多个通道使用同一个对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Netty 配置对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class NettyServerConfig implements Cloneable {
    // 服务端启动时监听的端口号
    private int listenPort = 8888;
    // 【业务线程池】 线程数量
    private int serverWorkerThreads = 8;
    // 根据该值创建 remotingServer 内部的一个 publicExecutor
    private int serverCallbackExecutorThreads = 0;
    // netty 【worker】线程数
    private int serverSelectorThreads = 3;
    // 【单向访问】时的并发限制
    private int serverOnewaySemaphoreValue = 256;
    // 【异步访问】时的并发限制
    private int serverAsyncSemaphoreValue = 64;
    // channel 最大的空闲存活时间 默认是 2min
    private int serverChannelMaxIdleTimeSeconds = 120;
    // 发送缓冲区大小 65535
    private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    // 接收缓冲区大小 65535
    private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    // 是否启用 netty 内存池 默认开启
    private boolean serverPooledByteBufAllocatorEnable = true;

    // 默认 linux 会启用 【epoll】
    private boolean useEpollNativeSelector = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无监听器构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public NettyRemotingServer(final NettyServerConfig nettyServerConfig) {
    this(nettyServerConfig, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有参构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
                           final ChannelEventListener channelEventListener) {
    // 服务器对客户端主动发起请求时并发限制。【单向请求和异步请求】的并发限制
    super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());
	// Netty 的启动器，负责组装 netty 组件
    this.serverBootstrap = new ServerBootstrap();
    // 成员变量的赋值
    this.nettyServerConfig = nettyServerConfig;
    this.channelEventListener = channelEventListener;

    // 公共线程池的线程数量，默认给的0，这里最终修改为4.
    int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
    if (publicThreadNums &amp;lt;= 0) {
        publicThreadNums = 4;
    }
    // 创建公共线程池，指定线程工厂，设置线程名称前缀：NettyServerPublicExecutor_[数字]
    this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory(){.});

    // 创建两个 netty 的线程组，一个是boss组，一个是worker组，【linux 系统默认启用 epoll】
    if (useEpoll()) {...} else {...}
	// SSL 相关
    loadSslContext();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;启动方法&lt;/h5&gt;
&lt;p&gt;核心方法的解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法，&lt;strong&gt;创建 BootStrap，并添加 NettyServerHandler 处理器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() {
    // Channel Pipeline 内的 handler 使用的线程资源，【线程分配给 handler 处理事件】
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...);

    // 创建通用共享的处理器 handler，【非常重要的 NettyServerHandler】
    prepareSharableHandlers();

    ServerBootstrap childHandler =
        // 配置工作组 boss（数量1） 和 worker（数量3） 组
        this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
        // 设置服务端 ServerSocketChannel 类型， Linux 用 epoll
        .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
        // 设置服务端 channel 选项
        .option(ChannelOption.SO_BACKLOG, 1024)
        // 客户端 channel 选项
        .childOption(ChannelOption.TCP_NODELAY, true)
        // 设置服务器端口
        .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
        // 向 channel pipeline 添加了很多 handler，【包括 NettyServerHandler】
        .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {});
            
	// 客户端开启 内存池，使用的内存池是  PooledByteBufAllocator.DEFAULT
    if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
        childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }

    try {
        // 同步等待建立连接，并绑定端口。
        ChannelFuture sync = this.serverBootstrap.bind().sync();
        InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
        // 将服务器成功绑定的端口号赋值给字段 port。
        this.port = addr.getPort();
    } catch (InterruptedException e1) {}

    // housekeepingService 不为空，则创建【网络异常事件处理器】
    if (this.channelEventListener != null) {
        // 线程一直轮询 nettyEvent 状态，根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型
        // CONNECT 不做操作，其余都是回调 onChannelDestroy 【关闭服务器与 Broker 物理节点的 Channel】
        this.nettyEventExecutor.start();
    }

    // 提交定时任务，每一秒 执行一次。扫描 responseTable 表，将过期的数据移除
    this.timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
       		NettyRemotingServer.this.scanResponseTable();
        }
    }, 1000 * 3, 1000);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;registerProcessor()：注册业务处理器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) {
    ExecutorService executorThis = executor;
    if (null == executor) {
        // 未指定线程池资源，将公共线程池赋值
        executorThis = this.publicExecutor;
    }
    // pair 对象，第一个参数代表的是处理器， 第二个参数是线程池，默认是公共的线程池
    Pair&amp;lt;NettyRequestProcessor, ExecutorService&amp;gt; pair = new Pair&amp;lt;NettyRequestProcessor, ExecutorService&amp;gt;(processor, executorThis);

    // key 是请求码，value 是 Pair 对象
    this.processorTable.put(requestCode, pair);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getProcessorPair()：&lt;strong&gt;根据请求码获取对应的处理器和线程池资源&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Pair&amp;lt;NettyRequestProcessor, ExecutorService&amp;gt; getProcessorPair(int requestCode) {
    return processorTable.get(requestCode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;请求方法&lt;/h5&gt;
&lt;p&gt;在 RocketMQ 消息队列中支持通信的方式主要有同步（sync）、异步（async）、单向（oneway）三种，其中单向通信模式相对简单，一般用在发送心跳包场景下，无需关注其 Response&lt;/p&gt;
&lt;p&gt;服务器主动向客户端发起请求时，使用三种方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;invokeSync()： 同步调用，&lt;strong&gt;服务器需要阻塞等待调用的返回结果&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int opaque = request.getOpaque()&lt;/code&gt;：获取请求 ID（与请求码不同）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture = new ResponseFuture(...)&lt;/code&gt;：&lt;strong&gt;创建响应对象&lt;/strong&gt;，没有回调函数和 Once&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.responseTable.put(opaque, responseFuture)&lt;/code&gt;：&lt;strong&gt;加入到响应映射表中&lt;/strong&gt;，key 为请求 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SocketAddress addr = channel.remoteAddress()&lt;/code&gt;：获取客户端的地址信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;channel.writeAndFlush(request).addListener(...)&lt;/code&gt;：将&lt;strong&gt;业务 Command 信息&lt;/strong&gt;写入通道，业务线程将数据交给 Netty ，Netty 的 IO 线程接管写刷数据的操作，&lt;strong&gt;监听器由 IO 线程在写刷后回调&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (f.isSuccess())&lt;/code&gt;：写入成功会将响应对象设置为成功状态直接 return，写入失败设置为失败状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseTable.remove(opaque)&lt;/code&gt;：将当前请求的 responseFuture &lt;strong&gt;从映射表移除&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture.setCause(f.cause())&lt;/code&gt;：设置错误的信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture.putResponse(null)&lt;/code&gt;：响应 Command 设置为 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseCommand = responseFuture.waitResponse(timeoutMillis)&lt;/code&gt;：当前线程设置超时时间挂起，&lt;strong&gt;同步等待响应&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (null == responseCommand)&lt;/code&gt;：超时或者出现异常，直接报错&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return responseCommand&lt;/code&gt;：返回响应 Command 信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invokeAsync()：异步调用，有回调对象，无返回值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)&lt;/code&gt;：获取信号量的许可证，信号量用来&lt;strong&gt;限制异步请求&lt;/strong&gt;的数量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (acquired)&lt;/code&gt;：许可证获取失败说明并发较高，会抛出异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)&lt;/code&gt;：Once 对象封装了释放信号量的操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;costTime = System.currentTimeMillis() - beginStartTime&lt;/code&gt;：计算一下耗费的时间，超时不再发起请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture = new ResponseFuture()&lt;/code&gt;：&lt;strong&gt;创建响应对象，包装了回调函数和 Once 对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.responseTable.put(opaque, responseFuture)&lt;/code&gt;：加入到响应映射表中，key 为请求 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;channel.writeAndFlush(request).addListener(...)&lt;/code&gt;：写刷数据
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (f.isSuccess())&lt;/code&gt;：写刷成功，设置 responseFuture 发生状态为 true&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requestFail(opaque)&lt;/code&gt;：写入失败，使用 publicExecutor &lt;strong&gt;公共线程池异步执行回调对象的函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture.release()&lt;/code&gt;：出现异常会释放信号量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invokeOneway()：单向调用，不关注响应结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request.markOnewayRPC()&lt;/code&gt;：设置单向标记，对端检查标记可知该请是单向请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)&lt;/code&gt;：获取信号量的许可证，信号量用来&lt;strong&gt;限制单向请求&lt;/strong&gt;的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;处理器类&lt;/h4&gt;
&lt;h5&gt;协议设计&lt;/h5&gt;
&lt;p&gt;在 Client 和 Server 之间完成一次消息发送时，需要对发送的消息进行一个协议约定，所以自定义 RocketMQ 的消息协议。在 RocketMQ 中，为了高效地在网络中传输消息和对收到的消息读取，就需要对消息进行编解码，RemotingCommand 这个类在消息传输过程中对所有数据内容的封装，不但包含了所有的数据结构，还包含了编码解码操作&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header字段&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;Request 说明&lt;/th&gt;
&lt;th&gt;Response 说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;请求操作码，应答方根据不同的请求码进行不同的处理&lt;/td&gt;
&lt;td&gt;应答响应码，0 表示成功，非 0 则表示各种错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;language&lt;/td&gt;
&lt;td&gt;LanguageCode&lt;/td&gt;
&lt;td&gt;请求方实现的语言&lt;/td&gt;
&lt;td&gt;应答方实现的语言&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;version&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;请求方程序的版本&lt;/td&gt;
&lt;td&gt;应答方程序的版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opaque&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;相当于 requestId，在同一个连接上的不同请求标识码，与响应消息中的相对应&lt;/td&gt;
&lt;td&gt;应答不做修改直接返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flag&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;区分是普通 RPC 还是 onewayRPC 的标志&lt;/td&gt;
&lt;td&gt;区分是普通 RPC 还是 onewayRPC的标志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remark&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;传输自定义文本信息&lt;/td&gt;
&lt;td&gt;传输自定义文本信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extFields&lt;/td&gt;
&lt;td&gt;HashMap&amp;lt;String, String&amp;gt;&lt;/td&gt;
&lt;td&gt;请求自定义扩展信息&lt;/td&gt;
&lt;td&gt;响应自定义扩展信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E6%B6%88%E6%81%AF%E5%8D%8F%E8%AE%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传输内容主要可以分为以下四部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;消息长度：总长度，四个字节存储，占用一个 int 类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;序列化类型&amp;amp;消息头长度：同样占用一个 int 类型，第一个字节表示序列化类型，后面三个字节表示消息头长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息头数据：经过序列化后的消息头数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息主体数据：消息主体的二进制字节数据内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档：https://github.com/apache/rocketmq/blob/master/docs/cn/design.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;处理方法&lt;/h5&gt;
&lt;p&gt;NettyServerHandler 类用来处理 Channel 上的事件，在 NettyRemotingServer 启动时注册到 Netty 中，可以处理 RemotingCommand 相关的数据，针对某一种类型的&lt;strong&gt;请求处理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class NettyServerHandler extends SimpleChannelInboundHandler&amp;lt;RemotingCommand&amp;gt; {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        // 服务器处理接受到的请求信息
        processMessageReceived(ctx, msg);
    }
}
public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    final RemotingCommand cmd = msg;
    if (cmd != null) {
		// 根据请求的类型进行处理
        switch (cmd.getType()) {
            case REQUEST_COMMAND:// 客户端发起的请求，走这里
                processRequestCommand(ctx, cmd);
                break;
            case RESPONSE_COMMAND:// 客户端响应的数据，走这里【当前类本身是服务器类也是客户端类】
                processResponseCommand(ctx, cmd);
                break;
            default:
                break;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NettyRemotingAbstract#processRequestCommand：&lt;strong&gt;处理请求的数据&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;matched = this.processorTable.get(cmd.getCode())&lt;/code&gt;：根据业务请求码获取 Pair 对象，包含&lt;strong&gt;处理器和线程池资源&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pair = null == matched ? this.defaultRequestProcessor : matched&lt;/code&gt;：未找到处理器则使用缺省处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int opaque = cmd.getOpaque()&lt;/code&gt;：获取请求 ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Runnable run = new Runnable()&lt;/code&gt;：创建任务对象，任务在提交到线程池后开始执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;doBeforeRpcHooks()&lt;/code&gt;：RPC HOOK 前置处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;callback = new RemotingResponseCallback()&lt;/code&gt;：&lt;strong&gt;封装响应客户端的逻辑&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doAfterRpcHooks()&lt;/code&gt;：RPC HOOK 后置处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!cmd.isOnewayRPC())&lt;/code&gt;：条件成立说明不是单向请求，需要结果&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.setOpaque(opaque)&lt;/code&gt;：将请求 ID 设置到 response&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.markResponseType()&lt;/code&gt;：&lt;strong&gt;设置当前请求是响应&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctx.writeAndFlush(response)&lt;/code&gt;： &lt;strong&gt;将响应数据交给 Netty IO 线程，完成数据写和刷&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (pair.getObject1() instanceof AsyncNettyRequestProcessor)&lt;/code&gt;：Nameserver 默认使用 DefaultRequestProcessor 处理器，是一个 AsyncNettyRequestProcessor 子类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processor = (AsyncNettyRequestProcessor)pair.getObject1()&lt;/code&gt;：获取处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processor.asyncProcessRequest(ctx, cmd, callback)&lt;/code&gt;：异步调用，首先 processRequest，然后 callback 响应客户端&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DefaultRequestProcessor.processRequest&lt;/code&gt;：&lt;strong&gt;根据业务码处理请求，执行对应的操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ClientRemotingProcessor.processRequest&lt;/code&gt;：处理事务回查消息，或者回执消息，需要消费者回执一条消息给生产者&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;requestTask = new RequestTask(run, ctx.channel(), cmd)&lt;/code&gt;：将任务对象、通道、请求封装成 RequestTask 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pair.getObject2().submit(requestTask)&lt;/code&gt;：&lt;strong&gt;获取处理器对应的线程池，将 task 提交，从 IO 线程切换到业务线程&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NettyRemotingAbstract#processResponseCommand：&lt;strong&gt;处理响应的数据&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int opaque = cmd.getOpaque()&lt;/code&gt;：获取请求 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture = responseTable.get(opaque)&lt;/code&gt;：&lt;strong&gt;从响应映射表中获取对应的对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture.setResponseCommand(cmd)&lt;/code&gt;：设置响应的 Command 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseTable.remove(opaque)&lt;/code&gt;：从映射表中移除对象，代表处理完成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (responseFuture.getInvokeCallback() != null)&lt;/code&gt;：包含回调对象，异步执行回调对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responseFuture.putResponse(cmd)&lt;/code&gt;：不包含回调对象，&lt;strong&gt;同步调用时，唤醒等待的业务线程&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流程：客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;路由信息&lt;/h4&gt;
&lt;h5&gt;信息管理&lt;/h5&gt;
&lt;p&gt;RouteInfoManager 类负责管理路由信息，NamesrvController 的构造方法中创建该类的实例对象，管理服务端的路由数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RouteInfoManager {
    // Broker 两个小时不活跃，视为离线，被定时任务删除
    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
    // 读写锁，保证线程安全
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 主题队列数据，一个主题对应多个队列
    private final HashMap&amp;lt;String/* topic */, List&amp;lt;QueueData&amp;gt;&amp;gt; topicQueueTable;
    // Broker 数据列表
    private final HashMap&amp;lt;String/* brokerName */, BrokerData&amp;gt; brokerAddrTable;
    // 集群
    private final HashMap&amp;lt;String/* clusterName */, Set&amp;lt;String/* brokerName */&amp;gt;&amp;gt; clusterAddrTable;
    // Broker 存活信息
    private final HashMap&amp;lt;String/* brokerAddr */, BrokerLiveInfo&amp;gt; brokerLiveTable;
    // 服务过滤
    private final HashMap&amp;lt;String/* brokerAddr */, List&amp;lt;String&amp;gt;/* Filter Server */&amp;gt; filterServerTable;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;路由注册&lt;/h5&gt;
&lt;p&gt;DefaultRequestProcessor REGISTER_BROKER 方法解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) {
    // 创建响应请求的对象，设置为响应类型，【先设置响应的状态码时系统错误码】
    // 反射创建 RegisterBrokerResponseHeader 对象设置到 response.customHeader 属性中
    final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);

    // 获取出反射创建的 RegisterBrokerResponseHeader 用户自定义header对象。
    final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();

    // 反射创建 RegisterBrokerRequestHeader 对象，并且将 request.extFields 中的数据写入到该对象中
    final RegisterBrokerRequestHeader requestHeader = request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);

    // CRC 校验，计算请求中的 CRC 值和请求头中包含的是否一致
    if (!checksum(ctx, request, requestHeader)) {
        response.setCode(ResponseCode.SYSTEM_ERROR);
        response.setRemark(&quot;crc32 not match&quot;);
        return response;
    }

    TopicConfigSerializeWrapper topicConfigWrapper;
    if (request.getBody() != null) {
        // 【解析请求体 body】，解码出来的数据就是当前机器的主题信息
        topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class);
    } else {
        topicConfigWrapper = new TopicConfigSerializeWrapper();
        topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0));
        topicConfigWrapper.getDataVersion().setTimestamp(0);
    }

    // 注册方法
    // 参数1 集群、参数2：节点ip地址、参数3：brokerName、参数4：brokerId 注意brokerId=0的节点为主节点
    // 参数5：ha节点ip地址、参数6当前节点主题信息、参数7：过滤服务器列表、参数8：当前服务器和客户端通信的channel
    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(..);

    // 将结果信息 写到 responseHeader 中
    responseHeader.setHaServerAddr(result.getHaServerAddr());
    responseHeader.setMasterAddr(result.getMasterAddr());
    // 获取 kv配置，写入 response body 中，【kv 配置是顺序消息相关的】
    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC);
    response.setBody(jsonValue);

    // code 设置为 SUCCESS
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
	// 返回 response ，【返回的 response 由 callback 对象处理】
    return response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RouteInfoManager#registerBroker：注册 Broker 的信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RegisterBrokerResult result = new RegisterBrokerResult()&lt;/code&gt;：返回结果的封装对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.lock.writeLock().lockInterruptibly()&lt;/code&gt;：加写锁后&lt;strong&gt;同步执行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerNames = this.clusterAddrTable.get(clusterName)&lt;/code&gt;：获取当前集群上的 Broker 名称列表，是空就新建列表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerNames.add(brokerName)&lt;/code&gt;：将当前 Broker 名字加入到集群列表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerData = this.brokerAddrTable.get(brokerName)&lt;/code&gt;：获取当前 Broker 的 brokerData，是空就新建放入映射表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerAddrsMap = brokerData.getBrokerAddrs()&lt;/code&gt;：获取当前 Broker 的物理节点 map 表，进行遍历，如果物理节点角色发生变化（slave → master），先将旧数据从物理节点 map 中移除，然后重写放入，&lt;strong&gt;保证节点的唯一性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null != topicConfigWrapper &amp;amp;&amp;amp; MixAll.MASTER_ID == brokerId)&lt;/code&gt;：Broker 上的 Topic 不为 null，并且当前物理节点是  Broker 上的 master 节点&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tcTable = topicConfigWrapper.getTopicConfigTable()&lt;/code&gt;：获取当前 Broker 信息中的主题映射表&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (tcTable != null)&lt;/code&gt;：映射表不空就加入或者更新到 Namesrv 内&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr)&lt;/code&gt;：添加&lt;strong&gt;当前节点的 BrokerLiveInfo&lt;/strong&gt; ，返回上一次心跳时当前 Broker 节点的存活对象数据。&lt;strong&gt;NamesrvController  中的定时任务会扫描映射表 brokerLiveTable&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo(
    System.currentTimeMillis(),topicConfigWrapper.getDataVersion(), channel,haServerAddr));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (MixAll.MASTER_ID != brokerId)&lt;/code&gt;：当前 Broker 不是 master 节点，&lt;strong&gt;获取主节点的信息&lt;/strong&gt;设置到结果对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.lock.writeLock().unlock()&lt;/code&gt;：释放写锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Broker&lt;/h3&gt;
&lt;h4&gt;MappedFile&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;MappedFile 类是最基础的存储类，继承自 ReferenceResource 类，用来&lt;strong&gt;保证线程安全&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MappedFile 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内存相关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小：默认是 4k
private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY;	// 当前进程下所有的 mappedFile 占用的总虚拟内存大小
private AtomicInteger TOTAL_MAPPED_FILES;		// 当前进程下所有的 mappedFile 个数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据位点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final AtomicInteger wrotePosition;	// 当前 mappedFile 的数据写入点
protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点
private final AtomicInteger flushedPosition;	// 数据落盘位点，在这之前的数据是持久化的安全数据
												// flushedPosition-wrotePosition 之间的数据属于脏页
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件相关：CL 是 CommitLog，CQ 是 ConsumeQueue&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String fileName;	// 文件名称，CL和CQ文件名是【第一条消息的物理偏移量】，索引文件是【年月日时分秒】
private long fileFromOffset;// 文件名转long，代表该对象的【起始偏移量】	
private File file;			// 文件对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;MF 中以物理偏移量作为文件名，可以更好的寻址和进行判断&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存映射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected FileChannel fileChannel;			// 文件通道
private MappedByteBuffer mappedByteBuffer;	// 内存映射缓冲区，访问虚拟内存
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ReferenceResource 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引用数量：当 &lt;code&gt;refCount &amp;lt;= 0&lt;/code&gt; 时，表示该资源可以释放了，没有任何其他程序依赖它了，用原子类保证线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final AtomicLong refCount = new AtomicLong(1);	// 初始值为 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存活状态：表示资源的存活状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected volatile boolean available = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否清理：默认值 false，当执行完子类对象的 cleanup() 清理方法后，该值置为 true ，表示资源已经全部释放&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected volatile boolean cleanupOver = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一次关闭资源的时间：用来记录超时时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long firstShutdownTimestamp = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;MappedFile 类核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;appendMessage()：提供上层向内存映射中追加消息的方法，消息如何追加由 AppendMessageCallback 控制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：消息     参数二：追加消息回调
public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 将字节数组写入到文件通道
public boolean appendMessage(final byte[] data)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flush()：刷盘接口，参数 flushLeastPages  代表刷盘的最小页数 ，等于 0 时属于强制刷盘；&amp;gt; 0 时需要脏页（计算方法在数据位点）达到该值才进行物理刷盘；文件写满时强制刷盘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int flush(final int flushLeastPages)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;selectMappedBuffer()：该方法以 pos 为开始位点 ，到有效数据为止，创建一个切片 ByteBuffer 作为数据副本，供业务访问数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SelectMappedBufferResult selectMappedBuffer(int pos)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;destroy()：销毁映射文件对象，并删除关联的系统文件，参数是强制关闭资源的时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean destroy(final long intervalForcibly)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cleanup()：&lt;strong&gt;释放堆外内存&lt;/strong&gt;，更新总虚拟内存和总内存映射文件数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean cleanup(final long currentRef)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;warmMappedFile()：内存预热，当要新建的 MappedFile 对象大于 1g 时执行该方法。mappedByteBuffer 已经通过mmap映射，此时操作系统中只是记录了该文件和该 Buffer 的映射关系，而并没有映射到物理内存中，对该 MappedFile 的每个 Page Cache 进行写入一个字节分配内存，&lt;strong&gt;将映射文件全部加载到内存&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void warmMappedFile(FlushDiskType type, int pages)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mlock()：锁住指定的内存区域避免被操作系统调到 swap 空间，减少了缺页异常的产生&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void mlock()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;swap space 是磁盘上的一块区域，可以是一个分区或者一个文件或者是组合。当系统物理内存不足时，Linux 会将内存中不常访问的数据保存到 swap 区域上，这样系统就可以有更多的物理内存为各个进程服务，而当系统需要访问 swap 上存储的内容时，需要通过&lt;strong&gt;缺页中断&lt;/strong&gt;将 swap 上的数据加载到内存中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ReferenceResource 类核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;hold()：增加引用记数 refCount，方法加锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public synchronized boolean hold()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：关闭资源，参数代表强制关闭资源的时间间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 系统当前时间 - firstShutdownTimestamp 时间  &amp;gt; intervalForcibly 进行【强制关闭】
public void shutdown(final long intervalForcibly)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;release()：引用计数减 1，当 refCount  为 0 时，调用子类的 cleanup 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void release()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;MapQueue&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;MappedFileQueue 用来管理 MappedFile 文件&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;管理目录：CommitLog 是 &lt;code&gt;../store/commitlog&lt;/code&gt;， ConsumeQueue 是 &lt;code&gt;../store/xxx_topic/0&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final String storePath;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int mappedFileSize;	// 目录下每个文件大小，CL文件默认 1g，CQ文件 默认 600w字节
private final CopyOnWriteArrayList&amp;lt;MappedFile&amp;gt; mappedFiles;	//目录下的每个 mappedFile 都加入该集合
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据位点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long flushedWhere = 0;		// 目录的刷盘位点，值为 mf.fileName + mf.wrotePosition
private long committedWhere = 0;	// 目录的提交位点
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long storeTimestamp = 0;	// 当前目录下最后一条 msg 的存储时间
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建服务：新建 MappedFile 实例，继承自 ServiceThread 是一个任务对象，run 方法用来创建实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AllocateMappedFileService allocateMappedFileService;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;load()：Broker 启动时，加载本地磁盘数据，该方法读取 storePath 目录下的文件，创建 MappedFile 对象放入集合内&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean load()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getLastMappedFile()：获取当前正在顺序写入的 MappedFile 对象，如果最后一个 MappedFile 写满了，或者不存在 MappedFile 对象，则创建新的 MappedFile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：文件起始偏移量；参数二：当list为空时，是否新建 MappedFile
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flush()：根据 flushedWhere 属性查找合适的 MappedFile，调用该 MappedFile 的落盘方法，并更新全局的 flushedWhere&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//参数：0 表示强制刷新， &amp;gt; 0 脏页数据必须达到 flushLeastPages 才刷新
public boolean flush(final int flushLeastPages)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;findMappedFileByOffset()：根据偏移量查询对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFileByTime()：CL 删除过期文件，根据文件的保留时长决定是否删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：过期时间； 参数二：删除两个文件之间的时间间隔； 参数三：mf.destory传递的参数； 参数四：true 强制删除
public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFileByOffset()：CQ 删除过期文件，遍历每个 MF 文件，获取当前文件最后一个数据单元的物理偏移量，小于 offset 说明当前 MF 文件内都是过期数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：consumeLog 目录下最小物理偏移量，就是第一条消息的 offset； 
// 参数二：ConsumerQueue 文件内每个数据单元固定大小
public int deleteExpiredFileByOffset(long offset, int unitSize)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;CommitLog&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;魔数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final static int MESSAGE_MAGIC_CODE = -626843481;	// 消息的第一个字段是大小，第二个字段就是魔数	
protected final static int BLANK_MAGIC_CODE = -875286124;	// 文件尾消息的魔法值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MappedFileQueue：用于管理 &lt;code&gt;../store/commitlog&lt;/code&gt; 目录下的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final MappedFileQueue mappedFileQueue;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存储服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final DefaultMessageStore defaultMessageStore;	// 存储模块对象，上层服务
private final FlushCommitLogService flushCommitLogService;	// 刷盘服务，默认实现是异步刷盘
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回调器：控制消息的哪些字段添加到 MappedFile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AppendMessageCallback appendMessageCallback;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;队列偏移量字典表：key 是主题队列 id，value 是偏移量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected HashMap&amp;lt;String, Long&amp;gt; topicQueueTable = new HashMap&amp;lt;String, Long&amp;gt;(1024);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁相关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long beginTimeInLock = 0;		 	// 写数据时加锁的开始时间
protected final PutMessageLock putMessageLock;		// 写锁，两个实现类：自旋锁和重入锁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为发送消息是需要持久化的，在 Broker 端持久化时会获取该锁，&lt;strong&gt;保证发送的消息的线程安全&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CommitLog(final DefaultMessageStore defaultMessageStore) {
    // 创建 MappedFileQueue 对象
    // 参数1：../store/commitlog； 参数2：【1g】； 参数3：allocateMappedFileService
    this.mappedFileQueue = new MappedFileQueue(...);
    // 默认 异步刷盘，创建这个对象
   	this.flushCommitLogService = new FlushRealTimeService();
    // 控制消息哪些字段追加到 mappedFile，【消息最大是 4M】
   	this.appendMessageCallback = new DefaultAppendMessageCallback(...);
    // 默认使用自旋锁
    this.putMessageLock = ...;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;CommitLog 类核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：会启动刷盘服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：关闭刷盘服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;load()：加载 CommitLog 目录下的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean load()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getMessage()：根据 offset 查询单条信息，返回的结果对象内部封装了一个 ByteBuffer，该 Buffer 表示 &lt;code&gt;[offset, offset + size]&lt;/code&gt; 区间的 MappedFile 的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SelectMappedBufferResult getMessage(final long offset, final int size)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFile()：删除过期文件，方法由 DefaultMessageStore 的定时任务调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int deleteExpiredFile()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;asyncPutMessage()：&lt;strong&gt;存储消息&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CompletableFuture&amp;lt;PutMessageResult&amp;gt; asyncPutMessage(final MessageExtBrokerInner msg)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;msg.setStoreTimestamp(System.currentTimeMillis())&lt;/code&gt;：设置存储时间，后面获取到写锁后这个事件会重写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msg.setBodyCRC(UtilAll.crc32(msg.getBody()))&lt;/code&gt;：获取消息的 CRC 值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;topic、queueId&lt;/code&gt;：获取主题和队列 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (msg.getDelayTimeLevel() &amp;gt; 0) &lt;/code&gt;：&lt;strong&gt;获取消息的延迟级别，这里是延迟消息实现的关键&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC&lt;/code&gt;：&lt;strong&gt;修改消息的主题为 &lt;code&gt;SCHEDULE_TOPIC_XXXX&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queueId = ScheduleMessageService.delayLevel2QueueId()&lt;/code&gt;：&lt;strong&gt;队列 ID 为延迟级别 -1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageAccessor.putProperty&lt;/code&gt;：&lt;strong&gt;将原来的消息主题和 ID 存入消息的属性 &lt;code&gt;REAL_TOPIC&lt;/code&gt; 中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mappedFile = this.mappedFileQueue.getLastMappedFile()&lt;/code&gt;：获取当前顺序写的 MappedFile 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;putMessageLock.lock()&lt;/code&gt;：&lt;strong&gt;获取写锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msg.setStoreTimestamp(beginLockTimestamp)&lt;/code&gt;：设置消息的存储时间为获取锁的时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (null == mappedFile || mappedFile.isFull())&lt;/code&gt;：文件写满了创建新的 MF 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result = mappedFile.appendMessage(msg, this.appendMessageCallback)&lt;/code&gt;：&lt;strong&gt;消息追加&lt;/strong&gt;，核心逻辑在回调器类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;putMessageLock.unlock()&lt;/code&gt;：释放写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.defaultMessageStore.unlockMappedFile(..)&lt;/code&gt;：将 MappedByteBuffer 从 lock 切换为 unlock 状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result)&lt;/code&gt;：结果封装&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flushResultFuture = submitFlushRequest(result, msg)&lt;/code&gt;：&lt;strong&gt;唤醒刷盘线程&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replicaResultFuture = submitReplicaRequest(result, msg)&lt;/code&gt;：HA 消息同步&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recoverNormally()：正常关机时的恢复方法，存储模块启动时&lt;strong&gt;先恢复所有的 ConsumeQueue 数据，再恢复 CommitLog 数据&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数表示恢复阶段 ConsumeQueue 中已知的最大的消息 offset
public void recoverNormally(long maxPhyOffsetOfConsumeQueue)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int index = mappedFiles.size() - 3&lt;/code&gt;：从倒数第三个 file 开始向后恢复&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dispatchRequest = this.checkMessageAndReturnSize()&lt;/code&gt;：每次从切片内解析出一条 msg 封装成 DispatchRequest 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;size = dispatchRequest.getMsgSize()&lt;/code&gt;：获取消息的大小，检查 DispatchRequest 对象的状态&lt;/p&gt;
&lt;p&gt;情况 1：正常数据，则 &lt;code&gt;mappedFileOffset += size&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;情况 2：文件尾数据，处理下一个文件，mappedFileOffset 置为 0，magic_code 表示文件尾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processOffset += mappedFileOffset&lt;/code&gt;：计算出正确的数据存储位点，并设置 MappedFileQueue 的目录刷盘位点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mappedFileQueue.truncateDirtyFiles(processOffset)&lt;/code&gt;：调整 MFQ 中文件的刷盘位点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (maxPhyOffsetOfConsumeQueue &amp;gt;= processOffset)&lt;/code&gt;：删除冗余数据，将超过全局位点的 CQ 下的文件删除，将包含全局位点的 CQ 下的文件重新定位&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recoverAbnormally()：异常关机时的恢复方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int index = mappedFiles.size() - 1&lt;/code&gt;：从尾部开始遍历 MFQ，验证 MF 的第一条消息，找到第一个验证通过的文件对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dispatchRequest = this.checkMessageAndReturnSize()&lt;/code&gt;：每次解析出一条 msg 封装成 DispatchRequest 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.defaultMessageStore.doDispatch(dispatchRequest)&lt;/code&gt;：&lt;strong&gt;重建 ConsumerQueue 和 Index，避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;剩余逻辑与正常关机的恢复方法相似&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;服务线程&lt;/h5&gt;
&lt;p&gt;AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;doAppend()：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public AppendMessageResult doAppend()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;long wroteOffset = fileFromOffset + byteBuffer.position()&lt;/code&gt;：消息写入的位置，物理偏移量 phyOffset&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String msgId&lt;/code&gt;：&lt;strong&gt;消息 ID，规则是客户端 IP + 消息偏移量 phyOffset&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;byte[] topicData&lt;/code&gt;：序列化消息，将消息的字段压入到  msgStoreItemMemory 这个 Buffer 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)&lt;/code&gt;：将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中，数据还没落盘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AppendMessageResult result&lt;/code&gt;：构造结果对象，包括存储位点、是否成功、队列偏移量等信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommitLog.this.topicQueueTable.put(key, ++queueOffset)&lt;/code&gt;：更新队列偏移量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FlushRealTimeService 刷盘 CL 数据，默认是异步刷盘类 FlushRealTimeService&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (!this.isStopped())&lt;/code&gt;：stopped为 true 才跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean flushCommitLogTimed&lt;/code&gt;：控制线程的休眠方式，默认是 false，使用 &lt;code&gt;CountDownLatch.await()&lt;/code&gt; 休眠，设置为 true 时使用 &lt;code&gt;Thread.sleep()&lt;/code&gt; 休眠&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int interval&lt;/code&gt;：获取配置中的刷盘时间间隔&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushPhysicQueueLeastPages&lt;/code&gt;：&lt;strong&gt;获取最小刷盘页数，默认是 4 页&lt;/strong&gt;，脏页达到指定页数才刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushPhysicQueueThoroughInterval&lt;/code&gt;：获取强制刷盘周期，默认是 10 秒，达到周期后强制刷盘，不考虑脏页&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (flushCommitLogTimed)&lt;/code&gt;：&lt;strong&gt;休眠逻辑&lt;/strong&gt;，避免 CPU 占用太长时间，导致无法执行其他更紧急的任务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)&lt;/code&gt;：&lt;strong&gt;刷盘&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; RETRY_TIMES_OVER &amp;amp;&amp;amp; !result; i++)&lt;/code&gt;：stopped 停止标记为 true 时，需要确保所有的数据都已经刷盘，所以此处尝试 10 次强制刷盘，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result = CommitLog.this.mappedFileQueue.flush(0)&lt;/code&gt;：&lt;strong&gt;强制刷盘&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步刷盘类 GroupCommitService&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (!this.isStopped())&lt;/code&gt;：stopped为 true 才跳出循环&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.waitForRunning(10)&lt;/code&gt;：线程休眠 10 毫秒，最后调用 &lt;code&gt;onWaitEnd()&lt;/code&gt; 进行&lt;strong&gt;请求的交换&lt;/strong&gt; &lt;code&gt;swapRequests()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.doCommit()&lt;/code&gt;：做提交逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!this.requestsRead.isEmpty()) &lt;/code&gt;：读请求集合不为空&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for (GroupCommitRequest req : this.requestsRead)&lt;/code&gt;：遍历所有的读请求，请求中的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private final long nextOffset&lt;/code&gt;：本条消息存储之后，下一条消息开始的 offset&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private CompletableFuture&amp;lt;PutMessageStatus&amp;gt; flushOKFuture&lt;/code&gt;：Future 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;boolean flushOK = ...&lt;/code&gt;：当前请求关注的数据是否全部落盘，&lt;strong&gt;落盘成功唤醒消费者线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 2 &amp;amp;&amp;amp; !flushOK; i++)&lt;/code&gt;：尝试进行两次强制刷盘，保证刷盘成功&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommitLog.this.mappedFileQueue.flush(0)&lt;/code&gt;：强制刷盘&lt;/p&gt;
&lt;p&gt;&lt;code&gt;req.wakeupCustomer(flushOK ? ...)&lt;/code&gt;：设置 Future 结果，在 Future 阻塞的线程在这里会被唤醒&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.requestsRead.clear()&lt;/code&gt;：清理 reqeustsRead 列表，方便交换时成为 requestsWrite 使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：读请求集合为空&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommitLog.this.mappedFileQueue.flush(0)&lt;/code&gt;：强制刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.swapRequests()&lt;/code&gt;：交换读写请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.doCommit()&lt;/code&gt;：交换后做一次提交&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;ConsQueue&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ConsumerQueue 是消息消费队列，存储消息在 CommitLog 的索引，便于快速定位消息&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据单元：ConsumerQueueData 数据单元的固定大小是 20 字节，默认申请 20 字节的缓冲区&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static final int CQ_STORE_UNIT_SIZE = 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件管理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MappedFileQueue mappedFileQueue;	// 文件管理器，管理 CQ 目录下的文件
private final String storePath;					// 目录，比如../store/consumequeue/xxx_topic/0
private final int mappedFileSize;				// 每一个 CQ 存储文件大小，默认 20 * 30w = 600w byte
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存储主模块：上层的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final DefaultMessageStore defaultMessageStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final String topic;					// CQ 主题
private final int queueId;					// CQ 队列，每一个队列都有一个 ConsumeQueue 对象进行管理
private final ByteBuffer byteBufferIndex;	// 临时缓冲区，插新的 CQData 时使用
private long maxPhysicOffset = -1;			// 当前ConsumeQueue内存储的最大消息物理偏移量
private volatile long minLogicOffset = 0;	// 当前ConsumeQueue内存储的最小消息物理偏移量
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConsumeQueue() {
    // 申请了一个 20 字节大小的 临时缓冲区
    this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;ConsumeQueue 启动阶段方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;load()：第一步，加载 storePath 目录下的文件，初始化 MappedFileQueue&lt;/li&gt;
&lt;li&gt;recover()：第二步，恢复 ConsumeQueue 数据
&lt;ul&gt;
&lt;li&gt;从倒数第三个 MF 文件开始向后遍历，依次读取 MF 中 20 个字节的 CQData 数据，检查 offset 和 size 是否是有效数据&lt;/li&gt;
&lt;li&gt;找到无效的 CQData 的位点，该位点就是 CQ 的刷盘点和数据顺序写入点&lt;/li&gt;
&lt;li&gt;删除无效的 MF 文件，调整当前顺序写的 MF 文件的数据位点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;truncateDirtyLogicFiles()：CommitLog 恢复阶段调用，将 ConsumeQueue 有效数据文件与 CommitLog 对齐，将超出部分的数据文删除掉，并调整当前文件的数据位点。Broker 启动阶段先恢复 CQ 的数据，再恢复 CL 数据，但是&lt;strong&gt;数据要以 CL 为基准&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数是最大消息物理偏移量
public void truncateDirtyLogicFiles(long phyOffet)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flush()：刷盘，调用 MFQ 的刷盘方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean flush(final int flushLeastPages)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFile()：删除过期文件，将小于 offset 的所有 MF 文件删除，offset 是 CommitLog 目录下最小的物理偏移量，小于该值的 CL 文件已经没有了，所以 CQ 也没有存在的必要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int deleteExpiredFile(long offset)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;putMessagePositionInfoWrapper()：&lt;strong&gt;向 CQ 中追加 CQData 数据&lt;/strong&gt;，由存储主模块 DefaultMessageStore 内部的异步线程调用，负责构建 ConsumeQueue 文件和 Index 文件的，该线程会持续关注 CommitLog 文件，当 CommitLog 文件内有新数据写入，就读出来封装成 DispatchRequest 对象，转发给 ConsumeQueue 或者 IndexService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void putMessagePositionInfoWrapper(DispatchRequest request)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getIndexBuffer()：转换 startIndex 为 offset，获取包含该 offset 的 MappedFile 文件，读取 &lt;code&gt;[offset%maxSize, mfPos]&lt;/code&gt; 范围的数据，包装成结果对象返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public SelectMappedBufferResult getIndexBuffer(final long startIndex)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;IndexFile&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;IndexFile 类成员属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;哈希：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int hashSlotSize = 4;	// 每个 hash 桶的大小是 4 字节，【用来存放索引的编号】
private final int hashSlotNum;			// hash 桶的个数，默认 500 万
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int indexSize = 20;		// 每个 index 条目的大小是 20 字节
private static int invalidIndex = 0;	// 无效索引编号：0 特殊值
private final int indexNum;				// 默认值：2000w
private final IndexHeader indexHeader;	// 索引头
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;映射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MappedFile mappedFile;			// 【索引文件使用的 MF 文件】
private final FileChannel fileChannel;			// 文件通道
private final MappedByteBuffer mappedByteBuffer;// 从 MF 中获取的内存映射缓冲区
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有参构造&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// endPhyOffset 上个索引文件 最后一条消息的 物理偏移量
// endTimestamp 上个索引文件 最后一条消息的 存储时间
public IndexFile(final String fileName, final int hashSlotNum, final int indexNum,
                 final long endPhyOffset, final long endTimestamp) throws IOException {
    // 文件大小 40 + 500w * 4 + 2000w * 20
    int fileTotalSize =
        IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize);
    // 创建 mf 对象，会在disk上创建文件
    this.mappedFile = new MappedFile(fileName, fileTotalSize);
    // 创建 索引头对象，传递 索引文件mf 的切片数据
    this.indexHeader = new IndexHeader(byteBuffer);
	//...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;IndexFile 类方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;load()：加载 IndexHeader&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void load()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flush()：MappedByteBuffer 内的数据强制落盘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void flush()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;isWriteFull()：检查当前的 IndexFile 已写索引数是否 &amp;gt;= indexNum，达到该值则当前 IndexFile 不能继续追加 IndexData 了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean isWriteFull()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;destroy()：删除文件时使用的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean destroy(final long intervalForcibly)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;putKey()：添加索引数据，解决哈希冲突使用&lt;strong&gt;头插法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：消息的 key，uniq_key 或者 keys=&quot;aaa bbb ccc&quot; 会分别为 aaa bbb ccc 创建索引
// 参数二：消息的物理偏移量；  参数三：消息存储时间
public boolean putKey(final String key, final long phyOffset, final long storeTimestamp)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int slotPos = keyHash % this.hashSlotNum&lt;/code&gt;：对 key 计算哈希后，取模得到对应的哈希槽 slot 下标，然后计算出哈希槽的存储位置 absSlotPos&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int slotValue = this.mappedByteBuffer.getInt(absSlotPos)&lt;/code&gt;：获取槽中的值，如果是无效值说明没有哈希冲突&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeDiff = timeDiff / 1000&lt;/code&gt;：计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值，转化为秒进行存储&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int absIndexPos&lt;/code&gt;：计算当前索引数据存储的位置，开始填充索引数据到对应的位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue)&lt;/code&gt;：&lt;strong&gt;hash 桶的原值，头插法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader...)&lt;/code&gt;：在 slot 放入当前索引的索引编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (this.indexHeader.getIndexCount() &amp;lt;= 1)&lt;/code&gt;：索引文件插入的第一条数据，需要设置起始偏移量和存储时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (invalidIndex == slotValue)&lt;/code&gt;：没有哈希冲突，说明占用了一个新的 hash slot&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.indexHeader&lt;/code&gt;：设置索引头的相关属性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;selectPhyOffset()：从索引文件查询消息的物理偏移量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：查询结果全部放到该list内； 参数二：查询key； 参数三：结果最大数限制； 参数四五：时间范围
public void selectPhyOffset(final List&amp;lt;Long&amp;gt; phyOffsets, final String key, final int maxNum,final long begin, final long end, boolean lock)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (this.mappedFile.hold())&lt;/code&gt;： MF 的引用记数 +1，查询期间 MF 资源&lt;strong&gt;不能被释放&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int slotValue = this.mappedByteBuffer.getInt(absSlotPos)&lt;/code&gt;：获取槽中的值，可能是无效值或者索引编号，如果是无效值说明查询未命中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int absIndexPos&lt;/code&gt;：计算出索引编号对应索引数据的开始位点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mappedByteBuffer&lt;/code&gt;：读取索引数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff&lt;/code&gt;：计算出准确的存储时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean timeMatched = (timeRead &amp;gt;= begin) &amp;amp;&amp;amp; (timeRead &amp;lt;= end)&lt;/code&gt;：时间范围的匹配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;phyOffsets.add(phyOffsetRead)&lt;/code&gt;：将命中的消息索引的消息偏移量加入到 list 集合中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nextIndexToRead = prevIndexRead&lt;/code&gt;：遍历前驱节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;IndexServ&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;IndexService 类用来管理 IndexFile 文件&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存储主模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final DefaultMessageStore defaultMessageStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引文件存储目录：&lt;code&gt;../store/index&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final String storePath;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引对象集合：目录下的每个文件都有一个 IndexFile 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ArrayList&amp;lt;IndexFile&amp;gt; indexFileList = new ArrayList&amp;lt;IndexFile&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int hashSlotNum;		// 每个索引文件包含的 哈希桶数量 ：500w
private final int indexNum;			// 每个索引文件包含的 索引条目数量 ：2000w
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;load()：加载 storePath 目录下的文件，为每个文件创建一个 IndexFile 实例对象，并加载 IndexHeader 信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean load(final boolean lastExitOK)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFile()：删除过期索引文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数 offset 表示 CommitLog 内最早的消息的 phyOffset
public void deleteExpiredFile(long offset)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.readWriteLock.readLock().lock()&lt;/code&gt;：加锁判断&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset()&lt;/code&gt;：获取目录中第一个文件的结束偏移量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (endPhyOffset &amp;lt; offset)&lt;/code&gt;：索引目录内存在过期的索引文件，并且当前的 IndexFile 都是过期的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (int i = 0; i &amp;lt; (files.length - 1); i++)&lt;/code&gt;：遍历文件列表，删除过期的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;buildIndex()：存储主模块 DefaultMessageStore 内部的异步线程调用，构建 Index 数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void buildIndex(DispatchRequest req)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;indexFile = retryGetAndCreateIndexFile()&lt;/code&gt;：获取或者创建顺序写的索引文件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;buildKey(topic, req.getUniqKey())&lt;/code&gt;：&lt;strong&gt;构建索引 key，&lt;code&gt;topic + # + uniqKey&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;indexFile = putKey()&lt;/code&gt;：插入索引文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (keys != null &amp;amp;&amp;amp; keys.length() &amp;gt; 0)&lt;/code&gt;：消息存在自定义索引 keys&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; keyset.length; i++)&lt;/code&gt;：遍历每个索引，为每个 key 调用一次 putKey&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getAndCreateLastIndexFile()：获取当前顺序写的 IndexFile，没有就创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public IndexFile getAndCreateLastIndexFile()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;HAService&lt;/h4&gt;
&lt;h5&gt;HAService&lt;/h5&gt;
&lt;h6&gt;Service&lt;/h6&gt;
&lt;p&gt;HAService 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主节点属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// master 节点当前有多少个 slave 节点与其进行数据同步
private final AtomicInteger connectionCount = new AtomicInteger(0);
// master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection，【控制 master 端向 slave 端传输数据】
private final List&amp;lt;HAConnection&amp;gt; connectionList = new LinkedList&amp;lt;&amp;gt;();
// master 向 slave 节点推送的最大的 offset，表示数据同步的进度
private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部类属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 封装了绑定服务器指定端口，监听 slave 的连接的逻辑，没有使用 Netty，使用了原生态的 NIO 去做
private final AcceptSocketService acceptSocketService;
// 控制生产者线程阻塞等待的逻辑
private final GroupTransferService groupTransferService;
// slave 节点的客户端对象，【slave 端才会正常运行该实例】
private final HAClient haClient;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程通信对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动高可用服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void  start() throws Exception {
    // 监听从节点
    this.acceptSocketService.beginAccept();
    // 启动监听服务
    this.acceptSocketService.start();
    // 启动转移服务
    this.groupTransferService.start();
    // 启动从节点客户端实例
    this.haClient.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;Accept&lt;/h6&gt;
&lt;p&gt;AcceptSocketService 类用于&lt;strong&gt;监听从节点的连接&lt;/strong&gt;，创建 HAConnection 连接对象&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;端口信息：Master 绑定监听的端口信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final SocketAddress socketAddressListen;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ServerSocketChannel serverSocketChannel;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多路复用器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Selector selector;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;beginAccept()：开始监听连接，&lt;strong&gt;NIO&lt;/strong&gt; 标准模板&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void beginAccept()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;run()：服务启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.selector.select(1000)&lt;/code&gt;：多路复用器阻塞获取就绪的通道，最多等待 1 秒钟&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set&amp;lt;SelectionKey&amp;gt; selected = this.selector.selectedKeys()&lt;/code&gt;：获取选择器中所有注册的通道中已经就绪好的事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (SelectionKey k : selected)&lt;/code&gt;：遍历所有就绪的事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if ((k.readyOps() &amp;amp; SelectionKey.OP_ACCEPT) != 0)&lt;/code&gt;：说明 &lt;code&gt;OP_ACCEPT&lt;/code&gt; 事件就绪&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SocketChannel sc = ((ServerSocketChannel) k.channel()).accept()&lt;/code&gt;：&lt;strong&gt;获取到客户端连接的通道&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAConnection conn = new HAConnection(HAService.this, sc)&lt;/code&gt;：&lt;strong&gt;为每个连接 master 服务器的 slave 创建连接对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conn.start()&lt;/code&gt;：&lt;strong&gt;启动 HAConnection 对象&lt;/strong&gt;，内部启动两个服务为读数据服务、写数据服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAService.this.addConnection(conn)&lt;/code&gt;：加入到 HAConnection 集合内&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;Group&lt;/h6&gt;
&lt;p&gt;GroupTransferService 用来控制数据同步&lt;/p&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;doWaitTransfer()：等待主从数据同步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doWaitTransfer()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (!this.requestsRead.isEmpty())&lt;/code&gt;：读请求不为空&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean transferOK = HAService.this.push2SlaveMaxOffset... &amp;gt;= req.getNextOffset()&lt;/code&gt;：&lt;strong&gt;主从同步是否完成&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;req.wakeupCustomer(transferOK ? ...)&lt;/code&gt;：唤醒消费者&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.requestsRead.clear()&lt;/code&gt;：清空读请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;swapRequests()：交换读写请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void swapRequests()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;HAClient&lt;/h5&gt;
&lt;h6&gt;成员属性&lt;/h6&gt;
&lt;p&gt;HAClient 是 slave 端运行的代码，用于&lt;strong&gt;和 master 服务器建立长连接&lt;/strong&gt;，上报本地同步进度，消费服务器发来的 msg 数据&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;缓冲区：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4;	// 默认大小：4 MB
private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主节点地址：格式为 &lt;code&gt;ip:port&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AtomicReference&amp;lt;String&amp;gt; masterAddress = new AtomicReference&amp;lt;&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NIO 属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ByteBuffer reportOffset;	// 通信使用NIO，所以消息使用块传输，上报 slave offset 使用
private SocketChannel socketChannel;	// 客户端与 master 的会话通道				
private Selector selector;				// 多路复用器
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通信时间：上次会话通信时间，用于控制 socketChannel 是否关闭的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long lastWriteTimestamp = System.currentTimeMillis();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进度信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long currentReportedOffset = 0;	// slave 当前的进度信息
private int dispatchPosition = 0;		// 控制 byteBufferRead position 指针
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;成员方法&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.connectMaster())&lt;/code&gt;：连接主节点，连接失败会休眠 5 秒&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String addr = this.masterAddress.get()&lt;/code&gt;：获取 master 暴露的 HA 地址端口信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.socketChannel = RemotingUtil.connect(socketAddress)&lt;/code&gt;：建立连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.socketChannel.register(this.selector, SelectionKey.OP_READ)&lt;/code&gt;：注册到多路复用器，&lt;strong&gt;关注读事件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.currentReportedOffset&lt;/code&gt;： 初始化上报进度字段为 slave 的 maxPhyOffset&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.isTimeToReportOffset())&lt;/code&gt;：slave 每 5 秒会上报一次 slave 端的同步进度信息给 master&lt;/p&gt;
&lt;p&gt;&lt;code&gt;boolean result = this.reportSlaveMaxOffset()&lt;/code&gt;：&lt;strong&gt;上报同步信息&lt;/strong&gt;，上报失败关闭连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.selector.select(1000)&lt;/code&gt;：多路复用器阻塞获取就绪的通道，最多等待 1 秒钟，&lt;strong&gt;获取到就绪事件或者超时后结束&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean ok = this.processReadEvent()&lt;/code&gt;：处理读事件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!reportSlaveMaxOffsetPlus())&lt;/code&gt;：检查是否重新上报同步进度&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reportSlaveMaxOffset()：上报 slave 同步进度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean reportSlaveMaxOffset(final long maxOffset)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先向缓冲区写入 slave 端最大偏移量，写完以后切换为指定置为初始状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 3 &amp;amp;&amp;amp; this.reportOffset.hasRemaining(); i++)&lt;/code&gt;：尝试三次写数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.socketChannel.write(this.reportOffset)&lt;/code&gt;：&lt;strong&gt;写数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return !this.reportOffset.hasRemaining()&lt;/code&gt;：写成功之后 pos = limit&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processReadEvent()：处理 master 发送给 slave 数据，返回 true 表示处理成功   false 表示 Socket 处于半关闭状态，需要上层重建 haClient&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean processReadEvent()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int readSizeZeroTimes = 0&lt;/code&gt;：控制 while 循环的一个条件变量，当值为 3 时跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (this.byteBufferRead.hasRemaining())&lt;/code&gt;：byteBufferRead 有空间可以去 Socket 读缓冲区加载数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int readSize = this.socketChannel.read(this.byteBufferRead)&lt;/code&gt;：&lt;strong&gt;从通道读数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (readSize &amp;gt; 0)&lt;/code&gt;：加载成功，有新数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;readSizeZeroTimes = 0&lt;/code&gt;：置为 0&lt;/p&gt;
&lt;p&gt;&lt;code&gt;boolean result = this.dispatchReadRequest()&lt;/code&gt;：处理数据的核心逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (readSize == 0) &lt;/code&gt;：连续无新数据 3 次，跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：readSize = -1 就表示 Socket 处于半关闭状态，对端已经关闭了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dispatchReadRequest()：&lt;strong&gt;处理数据的核心逻辑&lt;/strong&gt;，master 与 slave 传输的数据格式 &lt;code&gt;{[phyOffset][size][data...]}&lt;/code&gt;，phyOffset 表示数据区间的开始偏移量，data 代表数据块，最大 32kb，可能包含多条消息的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean dispatchReadRequest()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final int msgHeaderSize = 8 + 4&lt;/code&gt;：协议头大小 12&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int readSocketPos = this.byteBufferRead.position()&lt;/code&gt;：记录缓冲区处理数据前的 pos 位点，用于恢复指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int diff = ...&lt;/code&gt;：当前 byteBufferRead 还剩多少 byte 未处理，每处理一条帧数据都会更新 dispatchPosition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (diff &amp;gt;= msgHeaderSize)&lt;/code&gt;：缓冲区还有完整的协议头 header 数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (diff &amp;gt;= (msgHeaderSize + bodySize))&lt;/code&gt;：说明&lt;strong&gt;缓冲区内是包含当前帧的全部数据的&lt;/strong&gt;，开始处理帧数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HAService...appendToCommitLog(masterPhyOffset, bodyData)&lt;/code&gt;：&lt;strong&gt;存储数据到 CommitLog&lt;/strong&gt;，并构建 Index 和 CQ&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.byteBufferRead.position(readSocketPos)&lt;/code&gt;：恢复 byteBufferRead 的 pos 指针&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.dispatchPosition += msgHeaderSize + bodySize&lt;/code&gt;：加一帧数据长度，处理下一条数据使用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (!reportSlaveMaxOffsetPlus())&lt;/code&gt;：上报 slave 同步信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!this.byteBufferRead.hasRemaining())&lt;/code&gt;：缓冲区写满了，重新分配缓冲区&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reallocateByteBuffer()：重新分配缓冲区&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void reallocateByteBuffer()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition&lt;/code&gt;：表示缓冲区尚未处理过的字节数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (remain &amp;gt; 0)&lt;/code&gt;：条件成立，说明缓冲区&lt;strong&gt;最后一帧数据是半包数据&lt;/strong&gt;，但是不能丢失数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.byteBufferBackup.put(this.byteBufferRead)&lt;/code&gt;：&lt;strong&gt;将半包数据拷贝到 backup 缓冲区&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.swapByteBuffer()&lt;/code&gt;：交换 backup 成为 read&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.byteBufferRead.position(remain)&lt;/code&gt;：设置 pos 为 remain ，后续加载数据 pos 从remain 开始向后移动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.dispatchPosition = 0&lt;/code&gt;：当前缓冲区交换之后，相当于是一个全新的 byteBuffer，所以分配指针归零&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;HAConn&lt;/h5&gt;
&lt;h6&gt;Connection&lt;/h6&gt;
&lt;p&gt;HAConnection 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;会话通道：master 和 slave 之间通信的 SocketChannel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final SocketChannel socketChannel;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final String clientAddr;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private WriteSocketService writeSocketService;	// 写数据服务
private ReadSocketService readSocketService;	// 读数据服务
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求位点：在 slave 上报本地的进度之后被赋值，该值大于 0 后同步逻辑才会运行，master 如果不知道 slave 节点当前消息的存储进度，就无法给 slave 推送数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long slaveRequestOffset = -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应答位点： 保存最新的 slave 上报的 offset 信息，slaveAckOffset 之前的数据都可以认为 slave 已经同步完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long slaveAckOffset = -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public HAConnection(final HAService haService, final SocketChannel socketChannel) {
    // 初始化一些东西
    // 设置 socket 读写缓冲区为 64kb 大小
    this.socketChannel.socket().setReceiveBufferSize(1024 * 64);
    this.socketChannel.socket().setSendBufferSize(1024 * 64);
    // 创建读写服务
    this.writeSocketService = new WriteSocketService(this.socketChannel);
    this.readSocketService = new ReadSocketService(this.socketChannel);
    // 自增
    this.haService.getConnectionCount().incrementAndGet();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() {
    this.readSocketService.start();
    this.writeSocketService.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;ReadSocket&lt;/h6&gt;
&lt;p&gt;ReadSocketService 类是一个任务对象，slave 向 master 传输的帧格式为 &lt;code&gt;[long][long][long]&lt;/code&gt;，上报的是 slave 本地的同步进度，同步进度是一个 long 值&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;读缓冲：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024;	// 默认大小 1MB
private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NIO 属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Selector selector;			// 多路复用器
private final SocketChannel socketChannel;	// master 与 slave 之间的会话 SocketChannel
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理位点：缓冲区处理位点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int processPosition = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上次读操作的时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile long lastReadTimestamp = System.currentTimeMillis();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ReadSocketService(final SocketChannel socketChannel)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.socketChannel.register(this.selector, SelectionKey.OP_READ)&lt;/code&gt;：通道注册到多路复用器，关注读事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.setDaemon(true)&lt;/code&gt;：设置为守护线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.selector.select(1000)&lt;/code&gt;：多路复用器阻塞获取就绪的通道，最多等待 1 秒钟，获取到就绪事件或者超时后结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean ok = this.processReadEvent()&lt;/code&gt;：&lt;strong&gt;读数据的核心方法&lt;/strong&gt;，返回 true 表示处理成功   false 表示 Socket 处于半关闭状态，需要上层重建 HAConnection 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int readSizeZeroTimes = 0&lt;/code&gt;：控制 while 循环，当连续从 Socket 读取失败 3 次（未加载到数据）跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!this.byteBufferRead.hasRemaining())&lt;/code&gt;：byteBufferRead 已经全部使用完，需要清理数据并更新位点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (this.byteBufferRead.hasRemaining())&lt;/code&gt;：byteBufferRead 有空间可以去 Socket 读缓冲区加载数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int readSize = this.socketChannel.read(this.byteBufferRead)&lt;/code&gt;：&lt;strong&gt;从通道读数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (readSize &amp;gt; 0)&lt;/code&gt;：加载成功，有新数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if ((byteBufferRead.position() - processPosition) &amp;gt;= 8)&lt;/code&gt;：缓冲区的可读数据最少包含一个数据帧&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int pos = ...&lt;/code&gt;：&lt;strong&gt;获取可读帧数据中最后一个完整的帧数据的位点，后面的数据丢弃&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long readOffset = ...byteBufferRead.getLong(pos - 8)&lt;/code&gt;：读取最后一帧数据，slave 端当前的同步进度信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.processPosition = pos&lt;/code&gt;：更新处理位点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAConnection.this.slaveAckOffset = readOffset&lt;/code&gt;：更新应答位点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (HAConnection.this.slaveRequestOffset &amp;lt; 0)&lt;/code&gt;：条件成立&lt;strong&gt;给 slaveRequestOffset 赋值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAConnection...notifyTransferSome(slaveAckOffset)&lt;/code&gt;：&lt;strong&gt;唤醒阻塞的生产者线程&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (readSize == 0) &lt;/code&gt;：读取 3 次无新数据跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：readSize = -1 就表示 Socket 处于半关闭状态，对端已经关闭了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (interval &amp;gt; 20)&lt;/code&gt;：超过 20 秒未发生通信，直接结束循环&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;WriteSocket&lt;/h6&gt;
&lt;p&gt;WriteSocketService 类是一个任务对象，master 向 slave 传输的数据帧格式为 &lt;code&gt;{[phyOffset][size][data...]}{[phyOffset][size][data...]}&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;phyOffset：数据区间的开始偏移量，并不表示某一条具体的消息，表示的数据块开始的偏移量位置&lt;/li&gt;
&lt;li&gt;size：同步的数据块的大小&lt;/li&gt;
&lt;li&gt;data：数据块，最大 32kb，可能包含多条消息的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;协议头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int headerSize = 8 + 4;		// 协议头大小：12
private final ByteBuffer byteBufferHeader;	// 帧头缓冲区
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NIO 属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Selector selector;			// 多路复用器
private final SocketChannel socketChannel;	// master 与 slave 之间的会话 SocketChannel
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理位点：下一次传输同步数据的位置信息，master 给当前 slave 同步的位点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long nextTransferFromWhere = -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上次写操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean lastWriteOver = true;							// 上一轮数据是否传输完毕
private long lastWriteTimestamp = System.currentTimeMillis();	// 上次写操作的时间
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public WriteSocketService(final SocketChannel socketChannel)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.socketChannel.register(this.selector, SelectionKey.OP_WRITE)&lt;/code&gt;：通道注册到多路复用器，关注写事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.setDaemon(true)&lt;/code&gt;：设置为守护线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.selector.select(1000)&lt;/code&gt;：多路复用器阻塞获取就绪的通道，最多等待 1 秒钟，获取到就绪事件或者超时后结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (-1 == HAConnection.this.slaveRequestOffset)&lt;/code&gt;：&lt;strong&gt;等待 slave 同步完数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (-1 == this.nextTransferFromWhere)&lt;/code&gt;：条件成立，需要初始化该变量&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (0 == HAConnection.this.slaveRequestOffset)&lt;/code&gt;：slave 是一个全新节点，从正在顺序写的 MF 开始同步数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;long masterOffset = ...&lt;/code&gt;：获取 master 最大的 offset，并计算归属的 mappedFile 文件的开始 offset&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.nextTransferFromWhere = masterOffset&lt;/code&gt;：&lt;strong&gt;赋值给下一次传输同步数据的位置信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset&lt;/code&gt;：大部分情况走这个赋值逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.lastWriteOver)&lt;/code&gt;：上一次待发送数据全部发送完成&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (interval &amp;gt; 5)&lt;/code&gt;：&lt;strong&gt;超过 5 秒未同步数据，发送一个 header 心跳数据包，维持长连接&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：上一轮的待发送数据未全部发送，需要同步数据到 slave 节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SelectMappedBufferResult selectResult&lt;/code&gt;：&lt;strong&gt;到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (size &amp;gt; 32k)&lt;/code&gt;：一次最多同步 32k 数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.nextTransferFromWhere += size&lt;/code&gt;：增加 size，下一轮传输跳过本帧数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;selectResult.getByteBuffer().limit(size)&lt;/code&gt;：设置 byteBuffer 可访问数据区间为 [pos, size]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.selectMappedBufferResult = selectResult&lt;/code&gt;：&lt;strong&gt;待发送的数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.byteBufferHeader.put&lt;/code&gt;：&lt;strong&gt;构建帧头数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.lastWriteOver = this.transferData()&lt;/code&gt;：处理数据，返回是否处理完成&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同步方法：&lt;strong&gt;同步数据到 slave 节点&lt;/strong&gt;，返回 true 表示本轮数据全部同步完成，false 表示本轮同步未完成（Header 和 Body 其中一个未同步完成都会返回 false）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean transferData()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int writeSizeZeroTimes= 0&lt;/code&gt;：控制 while 循环，当写失败连续 3 次时，跳出循环）跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (this.byteBufferHeader.hasRemaining())&lt;/code&gt;：&lt;strong&gt;帧头数据缓冲区有待发送的数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int writeSize = this.socketChannel.write(this.byteBufferHeader)&lt;/code&gt;：向通道写帧头数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == this.selectMappedBufferResult)&lt;/code&gt;：说明是心跳数据，返回心跳数据是否发送完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!this.byteBufferHeader.hasRemaining())&lt;/code&gt;：&lt;strong&gt;Header写成功之后，才进行写 Body&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (this.selectMappedBufferResult.getByteBuffer().hasRemaining())&lt;/code&gt;：&lt;strong&gt;数据缓冲区有待发送的数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int writeSize = this.socketChannel.write(this.selectMappedBufferResult...)&lt;/code&gt;：向通道写帧头数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (writeSize &amp;gt; 0)&lt;/code&gt;：写数据成功，但是不代表 SMBR 中的数据全部写完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean result&lt;/code&gt;：判断是否发送完成，返回该值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;MesStore&lt;/h4&gt;
&lt;h5&gt;生命周期&lt;/h5&gt;
&lt;p&gt;DefaultMessageStore 类核心是整个存储服务的调度类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public DefaultMessageStore()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.allocateMappedFileService.start()&lt;/code&gt;：启动&lt;strong&gt;创建 MappedFile 文件服务&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.indexService.start()&lt;/code&gt;：启动索引服务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;load()：先加载 CommitLog，再加载 ConsumeQueue，最后加载 IndexFile，加载完进入恢复阶段，先恢复 CQ，在恢复 CL&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean load()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;start()：核心启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lock = lockFile.getChannel().tryLock(0, 1, false)&lt;/code&gt;：获取文件锁，获取失败说明当前目录已经启动过 Broker&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()&lt;/code&gt;：遍历全部的 CQ 对象，获取 CQ 中消息的最大偏移量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.reputMessageService.start()&lt;/code&gt;：设置分发服务的分发位点，启动&lt;strong&gt;分发服务&lt;/strong&gt;，构建 ConsumerQueue 和 IndexFile&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (dispatchBehindBytes() &amp;lt;= 0)&lt;/code&gt;：线程等待分发服务将分发数据全部处理完毕&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.recoverTopicQueueTable()&lt;/code&gt;：因为修改了 CQ 数据，所以再次构建队列偏移量字段表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.haService.start()&lt;/code&gt;：启动 &lt;strong&gt;HA 服务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.handleScheduleMessageService()&lt;/code&gt;：启动&lt;strong&gt;消息调度服务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.flushConsumeQueueService.start()&lt;/code&gt;：启动 CQ &lt;strong&gt;消费队列刷盘服务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.commitLog.start()&lt;/code&gt;：启动 &lt;strong&gt;CL 刷盘服务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.storeStatsService.start()&lt;/code&gt;：启动状态存储服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.createTempFile()&lt;/code&gt;：创建 AbortFile，正常关机时 JVM HOOK 会删除该文件，&lt;strong&gt;异常宕机时该文件不会删除&lt;/strong&gt;，开机数据恢复阶段根据是否存在该文件，执行不同的恢复策略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.addScheduleTask()&lt;/code&gt;：添加定时任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore.this.cleanFilesPeriodically()&lt;/code&gt;：&lt;strong&gt;定时清理过期文件&lt;/strong&gt;，周期是 10 秒&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.cleanCommitLogService.run()&lt;/code&gt;：启动清理过期的 CL 文件服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.cleanConsumeQueueService.run()&lt;/code&gt;：启动清理过期的 CQ 文件服务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore.this.checkSelf()&lt;/code&gt;：每 10 分种进行健康检查&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()&lt;/code&gt;：&lt;strong&gt;磁盘预警定时任务&lt;/strong&gt;，每 10 秒一次&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (physicRatio &amp;gt; this.diskSpaceWarningLevelRatio)&lt;/code&gt;：检查磁盘是否到达 waring 阈值，默认 90%&lt;/p&gt;
&lt;p&gt;&lt;code&gt;boolean diskok = ...runningFlags.getAndMakeDiskFull()&lt;/code&gt;：设置磁盘写满标记&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean diskok = ...this.runningFlags.getAndMakeDiskOK()&lt;/code&gt;：设置磁盘可写标记&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.shutdown = false&lt;/code&gt;：刚启动，设置为 false&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：关闭各种服务和线程资源，设置存储模块状态为关闭状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;destroy()：销毁 Broker 的工作目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void destroy()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;服务线程&lt;/h5&gt;
&lt;p&gt;ServiceThread 类被很多服务继承，本身是一个 Runnable 任务对象，继承者通过重写 run 方法来实现服务的逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：一般实现方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    while (!this.isStopped()) {
        // 业务逻辑
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过参数 stopped 控制服务的停止，使用 volatile 修饰保证可见性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected volatile boolean stopped = false
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：停止线程，首先设置 stopped 为 true，然后进行唤醒，默认不直接打断线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;waitForRunning()：挂起线程，设置唤醒标记 hasNotified 为 false&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void waitForRunning(long interval)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wakeup()：唤醒线程，设置 hasNotified 为 true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void wakeup()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;构建服务&lt;/h5&gt;
&lt;p&gt;AllocateMappedFileService &lt;strong&gt;创建 MappedFile 服务&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;mmapOperation()：核心服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean mmapOperation()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;req = this.requestQueue.take()&lt;/code&gt;： &lt;strong&gt;从 requestQueue 阻塞队列（优先级）中获取 AllocateRequest 任务&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (...isTransientStorePoolEnable())&lt;/code&gt;：条件成立使用直接内存写入数据， 从直接内存中 commit 到 FileChannel 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())&lt;/code&gt;：根据请求的路径和大小创建对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mappedFile.warmMappedFile()&lt;/code&gt;：判断 mappedFile 大小，只有 CommitLog 才进行文件预热&lt;/li&gt;
&lt;li&gt;&lt;code&gt;req.setMappedFile(mappedFile)&lt;/code&gt;：将创建好的 MF 对象的赋值给请求对象的成员属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;req.getCountDownLatch().countDown()&lt;/code&gt;：&lt;strong&gt;唤醒请求的阻塞线程&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;putRequestAndReturnMappedFile()：MappedFileQueue 中用来创建 MF 对象的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AllocateRequest nextReq = new AllocateRequest(...)&lt;/code&gt;：创建 nextFilePath 的 AllocateRequest 对象，放入请求列表和阻塞队列，然后创建 nextNextFilePath 的 AllocateRequest 对象，放入请求列表和阻塞队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AllocateRequest result = this.requestTable.get(nextFilePath)&lt;/code&gt;：从请求列表获取 nextFilePath 的请求对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result.getCountDownLatch().await(...)&lt;/code&gt;：&lt;strong&gt;线程挂起&lt;/strong&gt;，直到超时或者 nextFilePath 对应的 MF 文件创建完成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return result.getMappedFile()&lt;/code&gt;：返回创建好的 MF 文件对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ReputMessageService 消息分发服务，用于构&lt;strong&gt;建 ConsumerQueue 和 IndexFile 文件&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：&lt;strong&gt;循环执行 doReput 方法&lt;/strong&gt;，&lt;strong&gt;所以发送的消息存储进 CL 就可以产生对应的 CQ&lt;/strong&gt;，每执行一次线程休眠 1 毫秒&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;doReput()：实现分发的核心逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doReput()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (boolean doNext = true; this.isCommitLogAvailable() &amp;amp;&amp;amp; doNext; )&lt;/code&gt;：循环遍历&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SelectMappedBufferResult result&lt;/code&gt;： 从 CommitLog 拉取数据，数据范围 &lt;code&gt;[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]&lt;/code&gt;，封装成结果对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DispatchRequest dispatchRequest&lt;/code&gt;：从结果对象读取出一条 DispatchRequest 数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DefaultMessageStore.this.doDispatch(dispatchRequest)&lt;/code&gt;：将数据交给分发器进行分发，用于&lt;strong&gt;构建 CQ 和索引文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.reputFromOffset += size&lt;/code&gt;：更新数据范围&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;刷盘服务&lt;/h5&gt;
&lt;p&gt;FlushConsumeQueueService 刷盘 CQ 数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：每隔 1 秒执行一次刷盘服务，跳出循环后还会执行一次强制刷盘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;doFlush()：刷盘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doFlush(int retryTimes)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushConsumeQueueLeastPages&lt;/code&gt;：脏页阈值，默认是 2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (retryTimes == RETRY_TIMES_OVER)&lt;/code&gt;：&lt;strong&gt;重试次数是 3&lt;/strong&gt; 时设置强制刷盘，设置脏页阈值为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushConsumeQueueThoroughInterval&lt;/code&gt;：两次刷新的&lt;strong&gt;时间间隔超过 60 秒&lt;/strong&gt;会强制刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (ConsumeQueue cq : maps.values())&lt;/code&gt;：遍历所有的 CQ，进行刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore.this.getStoreCheckpoint().flush()&lt;/code&gt;：强制刷盘时将 StoreCheckpoint 瞬时数据刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FlushCommitLogService 刷盘 CL 数据，默认是异步刷盘&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (!this.isStopped())&lt;/code&gt;：stopped为 true 才跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean flushCommitLogTimed&lt;/code&gt;：控制线程的休眠方式，默认是 false，使用 &lt;code&gt;CountDownLatch.await()&lt;/code&gt; 休眠，设置为 true 时使用 &lt;code&gt;Thread.sleep()&lt;/code&gt; 休眠&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int interval&lt;/code&gt;：获取配置中的刷盘时间间隔&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushPhysicQueueLeastPages&lt;/code&gt;：获取最小刷盘页数，默认是 4 页，脏页达到指定页数才刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int flushPhysicQueueThoroughInterval&lt;/code&gt;：获取强制刷盘周期，默认是 10 秒，达到周期后强制刷盘，不考虑脏页&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (flushCommitLogTimed)&lt;/code&gt;：休眠逻辑，避免 CPU 占用太长时间，导致无法执行其他更紧急的任务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)&lt;/code&gt;：&lt;strong&gt;刷盘&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; RETRY_TIMES_OVER &amp;amp;&amp;amp; !result; i++)&lt;/code&gt;：stopped 停止标记为 true 时，需要确保所有的数据都已经刷盘，所以此处尝试 10 次强制刷盘，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result = CommitLog.this.mappedFileQueue.flush(0)&lt;/code&gt;：&lt;strong&gt;强制刷盘&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;清理服务&lt;/h5&gt;
&lt;p&gt;CleanCommitLogService 清理过期的 CL 数据，定时任务 10 秒调用一次，&lt;strong&gt;先清理 CL，再清理 CQ&lt;/strong&gt;，因为 CQ 依赖于 CL 的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFiles()：删除过期 CL 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void deleteExpiredFiles()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;long fileReservedTime&lt;/code&gt;：默认 72，代表文件的保留时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean timeup = this.isTimeToDelete()&lt;/code&gt;：当前时间是否是凌晨 4 点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean spacefull = this.isSpaceToDelete()&lt;/code&gt;：CL 或者 CQ 的目录磁盘使用率达到阈值标准 85%&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean manualDelete = this.manualDeleteFileSeveralTimes &amp;gt; 0&lt;/code&gt;：手动删除文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fileReservedTime *= 60 * 60 * 1000&lt;/code&gt;：默认保留 72 小时&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()&lt;/code&gt;：&lt;strong&gt;调用 MFQ 对象的删除方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CleanConsumeQueueService 清理过期的 CQ 数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deleteExpiredFiles()：删除过期 CQ 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void deleteExpiredFiles()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int deleteLogicsFilesInterval&lt;/code&gt;：清理 CQ 的时间间隔，默认 100 毫秒&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()&lt;/code&gt;：获取 CL 文件中最小的物理偏移量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (minOffset &amp;gt; this.lastPhysicalMinOffset)&lt;/code&gt;：CL 最小的偏移量大于 CQ 最小的，说明有过期数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lastPhysicalMinOffset = minOffset&lt;/code&gt;：更新 CQ 的最小偏移量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (ConsumeQueue logic : maps.values())&lt;/code&gt;：遍历所有的 CQ 文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logic.deleteExpiredFile(minOffset)&lt;/code&gt;：&lt;strong&gt;调用 MFQ 对象的删除方法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)&lt;/code&gt;：&lt;strong&gt;删除过期的索引文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;获取消息&lt;/h5&gt;
&lt;p&gt;DefaultMessageStore#getMessage 用于获取消息，在 PullMessageProcessor#processRequest 方法中被调用 （提示：建议学习消费者源码时再阅读）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// offset: 客户端拉消息使用位点；   maxMsgNums: 32；  messageFilter: 一般这里是 tagCode 过滤 
public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.shutdown)&lt;/code&gt;：检查运行状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GetMessageResult getResult&lt;/code&gt;：创建查询结果对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final long maxOffsetPy = this.commitLog.getMaxOffset()&lt;/code&gt;：&lt;strong&gt;获取 CommitLog 最大物理偏移量&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId)&lt;/code&gt;：根据主题和队列 ID 获取 ConsumeQueue对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;minOffset, maxOffset&lt;/code&gt;：获取当前 ConsumeQueue 的最小 offset 和 最大 offset，&lt;strong&gt;判断是否满足本次 Pull 的 offset&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (maxOffset == 0)&lt;/code&gt;：说明队列内无数据，设置状态为 NO_MESSAGE_IN_QUEUE，外层进行长轮询&lt;/p&gt;
&lt;p&gt;&lt;code&gt;else if (offset &amp;lt; minOffset)&lt;/code&gt;：说明 offset 太小了，设置状态为 OFFSET_TOO_SMALL&lt;/p&gt;
&lt;p&gt;&lt;code&gt;else if (offset == maxOffset)&lt;/code&gt;：消费进度持平，设置状态为 OFFSET_OVERFLOW_ONE，外层进行长轮询&lt;/p&gt;
&lt;p&gt;&lt;code&gt;else if (offset &amp;gt; maxOffset)&lt;/code&gt;：说明 offset 越界了，设置状态为 OFFSET_OVERFLOW_BADLY&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SelectMappedBufferResult bufferConsumeQueue&lt;/code&gt;：查询 CQData &lt;strong&gt;获取包含该 offset 的 MappedFile 文件&lt;/strong&gt;，如果该文件不是顺序写的文件，就读取 &lt;code&gt;[offset%maxSize, 文件尾]&lt;/code&gt; 范围的数据，反之读取 &lt;code&gt;[offset%maxSize, 文件名+wrotePosition尾]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;先查 CQ 的原因：因为 CQ 时 CL 的索引，通过 CQ 查询 CL 更加快捷&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (bufferConsumeQueue != null)&lt;/code&gt;：只有再 CQ 删除过期数据的逻辑执行时，条件才不成立，一般都是成立的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long nextPhyFileStartOffset = Long.MIN_VALUE&lt;/code&gt;：下一个 commitLog 物理文件名，初始值为最小值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long maxPhyOffsetPulling = 0&lt;/code&gt;：本次拉消息最后一条消息的物理偏移量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for ()&lt;/code&gt;：&lt;strong&gt;处理数据&lt;/strong&gt;，每次处理 20 字节处理字节数大于 16000 时跳出循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;offsetPy, sizePy, tagsCode&lt;/code&gt;：读取 20 个字节后，获取消息物理偏移量、消息大小、消息 tagCode&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean isInDisk = checkInDiskByCommitOffset(...)&lt;/code&gt;：&lt;strong&gt;检查消息是热数据还是冷数据&lt;/strong&gt;，false 为热数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;long memory&lt;/code&gt;：Broker 系统 40% 内存的字节数，写数据时内存不够会使用 LRU 算法淘汰数据，将淘汰数据持久化到磁盘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return (maxOffsetPy - offsetPy) &amp;gt; memory&lt;/code&gt;：返回 true 说明数据已经持久化到磁盘，为冷数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.isTheBatchFull())&lt;/code&gt;：&lt;strong&gt;控制是否跳出循环&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (0 == bufferTotal || 0 == messageTotal)&lt;/code&gt;：本次 pull 消息未拉取到任何东西，需要外层 for 循环继续，返回 false&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (maxMsgNums &amp;lt;= messageTotal)&lt;/code&gt;：结果对象内消息数已经超过了最大消息数量，可以结束循环了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (isInDisk)&lt;/code&gt;：冷数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if ((bufferTotal + sizePy) &amp;gt; ...)&lt;/code&gt;：冷数据一次 pull 请求最大允许获取 64kb 的消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (messageTotal &amp;gt; ...)&lt;/code&gt;：冷数据一次 pull 请求最大允许获取8 条消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：热数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if ((bufferTotal + sizePy) &amp;gt; ...)&lt;/code&gt;：热数据一次 pull 请求最大允许获取 256kb 的消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (messageTotal &amp;gt; ...)&lt;/code&gt;：冷数据一次 pull 请求最大允许获取 32 条消息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (messageFilter != null)&lt;/code&gt;：按照消息 tagCode 进行过滤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;selectResult = this.commitLog.getMessage(offsetPy, sizePy)&lt;/code&gt;：根据 CQ 消息物理偏移量和消息大小&lt;strong&gt;到 commitLog 中查询这条 msg&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == selectResult)&lt;/code&gt;：条件成立说明 commitLog 执行了删除过期文件的定时任务，因为是先清理的 CL，所以 CQ 还有该索引数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy)&lt;/code&gt;：获取包含该 offsetPy 的下一个数据文件的文件名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getResult.addMessage(selectResult)&lt;/code&gt;：&lt;strong&gt;将本次循环查询出来的 msg 加入到 getResult 内&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;status = GetMessageStatus.FOUND&lt;/code&gt;：查询状态设置为 FOUND&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nextPhyFileStartOffset = Long.MIN_VALUE&lt;/code&gt;：设置为最小值，跳过期 CQData 数据的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE)&lt;/code&gt;：计算客户端下一次 pull 时使用的位点信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getResult.setSuggestPullingFromSlave(diff &amp;gt; memory)&lt;/code&gt;：&lt;strong&gt;选择主从节点的建议&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;diff &amp;gt; memory =&amp;gt; true&lt;/code&gt;：表示本轮查询最后一条消息为冷数据，Broker 建议客户端下一次 pull 时到 slave 节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;diff &amp;gt; memory =&amp;gt; false&lt;/code&gt;：表示本轮查询最后一条消息为热数据，Broker 建议客户端下一次 pull 时到 master 节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getResult.setStatus(status)&lt;/code&gt;：设置结果状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getResult.setNextBeginOffset(nextBeginOffset)&lt;/code&gt;：设置客户端下一次 pull 时的 offset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getResult.setMaxOffset(maxOffset)&lt;/code&gt;：设置 queue 的最大 offset 和最小 offset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return getResult&lt;/code&gt;：返回结果对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Broker&lt;/h4&gt;
&lt;p&gt;BrokerStartup 启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    start(createBrokerController(args));
}
public static BrokerController start(BrokerController controller) {
    controller.start();	// 启动
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BrokerStartup#createBrokerController：构造控制器，并初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final BrokerController controller()&lt;/code&gt;：创建实例对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean initResult = controller.initialize()&lt;/code&gt;：控制器初始化
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.registerProcessor()&lt;/code&gt;：&lt;strong&gt;注册了处理器，包括发送消息、拉取消息、查询消息等核心处理器&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initialTransaction()&lt;/code&gt;：初始化了事务服务，用于进行&lt;strong&gt;事务回查&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BrokerController#start：核心启动方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.messageStore.start()&lt;/code&gt;：&lt;strong&gt;启动存储服务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.remotingServer.start()&lt;/code&gt;：启动 Netty 通信服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.fileWatchService.start()&lt;/code&gt;：启动文件监听服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;startProcessorByHa(messageStoreConfig.getBrokerRole())&lt;/code&gt;：&lt;strong&gt;启动事务回查&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.scheduledExecutorService.scheduleAtFixedRate()&lt;/code&gt;：每隔 30s 向 NameServer 上报 Topic 路由信息，&lt;strong&gt;心跳机制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Producer&lt;/h3&gt;
&lt;h4&gt;生产者类&lt;/h4&gt;
&lt;h5&gt;生产者类&lt;/h5&gt;
&lt;p&gt;DefaultMQProducer 是生产者的默认实现类&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;生产者实现类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final transient DefaultMQProducerImpl defaultMQProducerImpl
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生产者组：发送事务消息，Broker 端进行事务回查（补偿机制）时，选择当前生产者组的下一个生产者进行事务回查&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String producerGroup;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认主题：isAutoCreateTopicEnable 开启时，当发送消息指定的 Topic 在 Namesrv 未找到路由信息，使用该值创建 Topic 信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC;
// 值为【TBW102】，Just for testing or demo program
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息重投：系统特性消息重试部分详解了三个参数的作用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int retryTimesWhenSendFailed = 2;		// 同步发送失败后重试的发送次数，加上第一次发送，一共三次
private int retryTimesWhenSendAsyncFailed = 2;	// 异步
private boolean retryAnotherBrokerWhenNotStoreOK = false;	// 消息未存储成功，选择其他 Broker 重试
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int defaultTopicQueueNums = 4;		// 默认 Broker 创建的队列数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int sendMsgTimeout = 3000;					// 发送消息的超时限制
private int compressMsgBodyOverHowmuch = 1024 * 4;	// 压缩阈值，当 msg body 超过 4k 后使用压缩
private int maxMessageSize = 1024 * 1024 * 4;		// 消息体的最大限制，默认 4M
private TraceDispatcher traceDispatcher = null;		// 消息轨迹

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) {
    this.namespace = namespace;
    this.producerGroup = producerGroup;
    // 创建生产者实现对象
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() throws MQClientException {
    // 重置生产者组名，如果传递了命名空间，则 【namespace%group】
    this.setProducerGroup(withNamespace(this.producerGroup));
    // 生产者实现对象启动
    this.defaultMQProducerImpl.start();
    if (null != traceDispatcher) {
      	// 消息轨迹的逻辑
   		traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;send()：&lt;strong&gt;发送消息&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SendResult send(Message msg){
    // 校验消息
    Validators.checkMessage(msg, this);
    // 设置消息 Topic
    msg.setTopic(withNamespace(msg.getTopic()));
    return this.defaultMQProducerImpl.send(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;request()：请求方法，&lt;strong&gt;需要消费者回执消息&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Message request(final Message msg, final MessageQueue mq, final long timeout) {
    msg.setTopic(withNamespace(msg.getTopic()));
    return this.defaultMQProducerImpl.request(msg, mq, timeout);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;实现者类&lt;/h5&gt;
&lt;p&gt;DefaultMQProducerImpl 类是默认的生产者实现类&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final DefaultMQProducer defaultMQProducer;	// 持有默认生产者对象，用来获取对象中的配置信息
private MQClientInstance mQClientFactory;			// 客户端实例对象，生产者启动后需要注册到该客户端对象内
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主题发布信息映射表：key 是 Topic，value 是发布信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ConcurrentMap&amp;lt;String, TopicPublishInfo&amp;gt; topicPublishInfoTable = new ConcurrentHashMap&amp;lt;String, TopicPublishInfo&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步发送消息：相关信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final BlockingQueue&amp;lt;Runnable&amp;gt; asyncSenderThreadPoolQueue;// 异步发送消息，异步线程池使用的队列
private final ExecutorService defaultAsyncSenderExecutor;	// 异步发送消息默认使用的线程池
private ExecutorService asyncSenderExecutor;				// 异步消息发送线程池，指定后就不使用默认线程池了
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时器：执行定时任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Timer timer = new Timer(&quot;RequestHouseKeepingService&quot;, true);	// 守护线程
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;状态信息：服务的状态，默认创建状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ServiceState serviceState = ServiceState.CREATE_JUST;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;压缩等级：ZIP 压缩算法的等级，默认是 5，越高压缩效果好，但是压缩的更慢&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int zipCompressLevel = Integer.parseInt(System.getProperty..., &quot;5&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容错策略：选择队列的容错策略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;钩子：用来进行前置或者后置处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ArrayList&amp;lt;SendMessageHook&amp;gt; sendMessageHookList;			// 发送消息的钩子，留给用户扩展使用
ArrayList&amp;lt;CheckForbiddenHook&amp;gt; checkForbiddenHookList;	// 对比上面的钩子，可以抛异常，控制消息是否可以发送
private final RPCHook rpcHook;						 	// 传递给 NettyRemotingClient
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;默认构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) {
    // 默认 RPC HOOK 是空
    this(defaultMQProducer, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {
    // 属性赋值
    this.defaultMQProducer = defaultMQProducer;
    this.rpcHook = rpcHook;

    // 创建【异步消息线程池任务队列】，长度是 5w
    this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(50000);
    // 创建默认的异步消息任务线程池
    this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(
        // 核心线程数和最大线程数都是 系统可用的计算资源（8核16线程的系统就是 16）...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;实现方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法，参数默认是 true，代表正常的启动路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start(final boolean startFactory)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.serviceState = ServiceState.START_FAILED&lt;/code&gt;：先修改为启动失败，成功后再修改，这种思想很常见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.checkConfig()&lt;/code&gt;：判断生产者组名不能是空，也不能是 default_PRODUCER&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))&lt;/code&gt;：条件成立说明当前生产者不是内部产生者，内部生产者是&lt;strong&gt;处理消息回退&lt;/strong&gt;的这种情况使用的生产者&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.defaultMQProducer.changeInstanceNameToPID()&lt;/code&gt;：修改生产者实例名称为当前进程的 PID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; this.mQClientFactory = ...&lt;/code&gt;：获取当前进程的 MQ 客户端实例对象，从 factoryTable 中获取 key 为 客户端 ID，格式是&lt;code&gt;ip@pid&lt;/code&gt;，&lt;strong&gt;一个 JVM 进程只有一个 PID，也只有一个 MQClientInstance&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean registerOK = mQClientFactory.registerProducer(...)&lt;/code&gt;：将生产者注册到 RocketMQ 客户端实例内&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.topicPublishInfoTable.put(...)&lt;/code&gt;：添加一个主题发布信息，key 是 &lt;strong&gt;TBW102&lt;/strong&gt; ，value 是一个空对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mQClientFactory.start()&lt;/code&gt;：启动 RocketMQ 客户端实例对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()&lt;/code&gt;：RocketMQ &lt;strong&gt;客户端实例向已知的 Broker 节点发送一次心跳&lt;/strong&gt;（也是定时任务）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.timer.scheduleAtFixedRate()&lt;/code&gt;： request 发送的消息需要消费着回执信息，启动定时任务每秒一次删除超时请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者 msg 添加信息关联 ID 发送到 Broker&lt;/li&gt;
&lt;li&gt;消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息，处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker，Broker 判断为回执消息，会根据客户端ID 找到 channel 推送给生产者&lt;/li&gt;
&lt;li&gt;生产者拿到回执消息后，读取出来关联 ID 找到对应的 RequestFuture，将阻塞线程唤醒&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendDefaultImpl()：发送消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//参数1：消息；参数2：发送模式（同步异步单向）；参数3：回调函数，异步发送时需要；参数4：发送超时时间, 默认 3 秒
private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.makeSureStateOK()&lt;/code&gt;：校验生产者状态是运行中，否则抛出异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())&lt;/code&gt;：&lt;strong&gt;获取当前消息主题的发布信息&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.topicPublishInfoTable.get(topic)&lt;/code&gt;：先尝试从本地主题发布信息映射表获取信息，获取不到继续执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mQClientFactory.update...FromNameServer(topic)&lt;/code&gt;：然后从 Namesrv 更新该 Topic 的路由数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mQClientFactory.update...FromNameServer(...)&lt;/code&gt;：&lt;strong&gt;路由数据是空，获取默认 TBW102 的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return topicPublishInfo&lt;/code&gt;：返回 TBW102 主题的发布信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String[] brokersSent = new String[timesTotal]&lt;/code&gt;：下标索引代表第几次发送，值代表这次发送选择 Broker name&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (; times &amp;lt; timesTotal; times++)&lt;/code&gt;：循环发送，&lt;strong&gt;发送成功或者发送尝试次数达到上限，结束循环&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String lastBrokerName = null == mq ? null : mq.getBrokerName()&lt;/code&gt;：获取上次发送失败的 BrokerName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)&lt;/code&gt;：从发布信息中选择一个队列，生产者的&lt;strong&gt;负载均衡策略&lt;/strong&gt;，参考系统特性章节&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokersSent[times] = mq.getBrokerName()&lt;/code&gt;：将本次选择的 BrokerName 存入数组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))&lt;/code&gt;：&lt;strong&gt;产生重投，重投消息需要加上标记&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sendResult = this.sendKernelImpl&lt;/code&gt;：核心发送方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (communicationMode)&lt;/code&gt;：异步或者单向消息直接返回 null，异步通过回调函数处理，同步发送进入逻辑判断&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (sendResult.getSendStatus() != SendStatus.SEND_OK)&lt;/code&gt;：&lt;strong&gt;服务端 Broker 存储失败，需要重试其他 Broker&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;throw new MQClientException()&lt;/code&gt;：未找到当前主题的路由数据，无法发送消息，抛出异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendKernelImpl()：&lt;strong&gt;核心发送方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//参数1：消息；参数2：选择的队列；参数3：发送模式（同步异步单向）；参数4：回调函数，异步发送时需要；参数5：主题发布信息；参数6：剩余超时时间限制
private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerAddr = this.mQClientFactory(...)&lt;/code&gt;：&lt;strong&gt;获取指定 BrokerName 对应的 mater 节点的地址&lt;/strong&gt;，master 节点的 ID 为 0，集群模式下，&lt;strong&gt;发送消息要发到主节点&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;brokerAddr = MixAll.brokerVIPChannel()&lt;/code&gt;：Broker 启动时会绑定两个服务器端口，一个是普通端口，一个是 VIP 端口，服务器端根据不同端口创建不同的的 NioSocketChannel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;byte[] prevBody = msg.getBody()&lt;/code&gt;：获取消息体&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!(msg instanceof MessageBatch))&lt;/code&gt;：非批量消息，需要重新设置消息 ID&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MessageClientIDSetter.setUniqID(msg)&lt;/code&gt;：&lt;strong&gt;msg id 由两部分组成&lt;/strong&gt;，一部分是 ip 地址、进程号、Classloader 的 hashcode，另一部分是时间差（当前时间减去当月一号的时间）和计数器的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.tryToCompressMessage(msg))&lt;/code&gt;：判断消息是否压缩，压缩需要设置压缩标记&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hasCheckForbiddenHook、hasSendMessageHook&lt;/code&gt;：执行钩子方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;requestHeader = new SendMessageRequestHeader()&lt;/code&gt;：设置发送消息的消息头&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))&lt;/code&gt;：重投的发送消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (communicationMode)&lt;/code&gt;：异步发送一种处理方式，单向和同步同样的处理逻辑&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()&lt;/code&gt;：&lt;strong&gt;发送消息&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request = RemotingCommand.createRequestCommand()&lt;/code&gt;：创建一个 RequestCommand 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;request.setBody(msg.getBody())&lt;/code&gt;：&lt;strong&gt;将消息放入请求体&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;switch (communicationMode)&lt;/code&gt;：&lt;strong&gt;根据不同的模式 invoke 不同的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;request()：请求方法，消费者回执消息，这种消息是异步消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)&lt;/code&gt;：创建请求响应对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getRequestFutureTable().put(correlationId, requestResponseFuture)&lt;/code&gt;：放入RequestFutureTable 映射表中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())&lt;/code&gt;：&lt;strong&gt;发送异步消息，有回调函数&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return waitResponse(msg, timeout, requestResponseFuture, cost)&lt;/code&gt;：用来挂起请求的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Message waitResponseMessage(final long timeout) throws InterruptedException {
    // 请求挂起
    this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS);
    return this.responseMsg;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当消息被消费后，客户端处理响应时通过消息的关联 ID，从映射表中获取消息的 RequestResponseFuture，执行下面的方法唤醒挂起线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void putResponseMessage(final Message responseMsg) {
    this.responseMsg = responseMsg;
    this.countDownLatch.countDown();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;路由信息&lt;/h4&gt;
&lt;p&gt;TopicPublishInfo 类用来存储路由信息&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;顺序消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean orderTopic = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private List&amp;lt;MessageQueue&amp;gt; messageQueueList = new ArrayList&amp;lt;&amp;gt;();			// 主题全部的消息队列
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();	// 消息队列索引
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 【消息队列类】
public class MessageQueue implements Comparable&amp;lt;MessageQueue&amp;gt;, Serializable {
    private String topic;
    private String brokerName;
    private int queueId;// 队列 ID
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;路由数据：主题对应的路由数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private TopicRouteData topicRouteData;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class TopicRouteData extends RemotingSerializable {
    private String orderTopicConf;
    private List&amp;lt;QueueData&amp;gt; queueDatas;		// 队列数据
    private List&amp;lt;BrokerData&amp;gt; brokerDatas;	// Broker 数据
    private HashMap&amp;lt;String/* brokerAddr */, List&amp;lt;String&amp;gt;/* Filter Server */&amp;gt; filterServerTable;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class QueueData implements Comparable&amp;lt;QueueData&amp;gt; {
    private String brokerName;	// 节点名称
    private int readQueueNums;	// 读队列数
    private int writeQueueNums;	// 写队列数
    private int perm;			// 权限
    private int topicSynFlag;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class BrokerData implements Comparable&amp;lt;BrokerData&amp;gt; {
    private String cluster;		// 集群名
    private String brokerName;	// Broker节点名称
    private HashMap&amp;lt;Long/* brokerId */, String/* broker address */&amp;gt; brokerAddrs;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;selectOneMessageQueue()：&lt;strong&gt;选择消息队列&lt;/strong&gt;使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数是上次失败时的 brokerName，可以为 null
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    if (lastBrokerName == null) {
        return selectOneMessageQueue();
    } else {
        // 遍历消息队列
        for (int i = 0; i &amp;lt; this.messageQueueList.size(); i++) {
            // 【获取队列的索引，+1】
            int index = this.sendWhichQueue.getAndIncrement();
            // 获取队列的下标位置
            int pos = Math.abs(index) % this.messageQueueList.size();
            if (pos &amp;lt; 0)
                pos = 0;
            // 获取消息队列
            MessageQueue mq = this.messageQueueList.get(pos);
            // 与上次选择的不同就可以返回
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        return selectOneMessageQueue();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;公共配置&lt;/h4&gt;
&lt;p&gt;公共的配置信息类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ClientConfig 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClientConfig {
    // Namesrv 地址配置
    private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses();
    // 客户端的 IP 地址
    private String clientIP = RemotingUtil.getLocalAddress();
    // 客户端实例名称
    private String instanceName = System.getProperty(&quot;rocketmq.client.name&quot;, &quot;DEFAULT&quot;);
    // 客户端回调线程池的数量，平台核心数，8核16线程的电脑返回16
    private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors();
    // 命名空间
    protected String namespace;
    protected AccessChannel accessChannel = AccessChannel.LOCAL;

    // 获取路由信息的间隔时间 30s
    private int pollNameServerInterval = 1000 * 30;
    // 客户端与 broker 之间的心跳周期 30s
    private int heartbeatBrokerInterval = 1000 * 30;
    // 消费者持久化消费的周期 5s
    private int persistConsumerOffsetInterval = 1000 * 5;
    private long pullTimeDelayMillsWhenException = 1000;
    private boolean unitMode = false;
    private String unitName;
    // vip 通道，broker 启动时绑定两个端口，其中一个是 vip 通道
    private boolean vipChannelEnabled = Boolean.parseBoolean();
    // 语言，默认是 Java
    private LanguageCode language = LanguageCode.JAVA;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NettyClientConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class NettyClientConfig {
    // 客户端工作线程数
    private int clientWorkerThreads = 4;
    // 回调处理线程池 线程数：平台核心数
    private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors();
    // 单向请求并发数，默认 65535
    private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE;
    // 异步请求并发数，默认 65535
    private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE;
    // 客户端连接服务器的超时时间限制 3秒
    private int connectTimeoutMillis = 3000;
    // 客户端未激活周期，60s（指定时间内 ch 未激活，需要关闭）
    private long channelNotActiveInterval = 1000 * 60;
    // 客户端与服务器 ch 最大空闲时间 2分钟
    private int clientChannelMaxIdleTimeSeconds = 120;

    // 底层 Socket 写和收 缓冲区的大小 65535  64k
    private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    // 客户端 netty 是否启动内存池
    private boolean clientPooledByteBufAllocatorEnable = false;
    // 客户端是否超时关闭 Socket 连接
    private boolean clientCloseSocketIfTimeout = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;客户端类&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;MQClientInstance 是 RocketMQ 客户端实例，在一个 JVM 进程中只有一个客户端实例，&lt;strong&gt;既服务于生产者，也服务于消费者&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int instanceIndex;			// 索引一般是 0，因为客户端实例一般都是一个进程只有一个
private final String clientId;				// 客户端 ID ip@pid
private final long bootTimestamp;			// 客户端的启动时间
private ServiceState serviceState;			// 客户端状态
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生产者消费者的映射表：key 是组名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ConcurrentMap&amp;lt;String, MQProducerInner&amp;gt; producerTable
private final ConcurrentMap&amp;lt;String, MQConsumerInner&amp;gt; consumerTable
private final ConcurrentMap&amp;lt;String, MQAdminExtInner&amp;gt; adminExtTable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络层配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final NettyClientConfig nettyClientConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心功能的实现：负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象，使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法，完成网络 IO（同步、异步、单向）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MQClientAPIImpl mQClientAPIImpl;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地路由数据：key 是主题名称，value 路由信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ConcurrentMap&amp;lt;String, TopicRouteData&amp;gt; topicRouteTable = new ConcurrentHashMap&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁信息：两把锁，锁不同的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Lock lockNamesrv = new ReentrantLock();
private final Lock lockHeartbeat = new ReentrantLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调度线程池：单线程，执行定时任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ScheduledExecutorService scheduledExecutorService;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Broker 映射表：key 是 BrokerName&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 物理节点映射表，value：Long 是 brokerID，【ID=0 的是主节点，其他是从节点】，String 是地址 ip:port
private final ConcurrentMap&amp;lt;String, HashMap&amp;lt;Long, String&amp;gt;&amp;gt; brokerAddrTable;
// 物理节点版本映射表，String 是地址 ip:port，Integer 是版本
ConcurrentMap&amp;lt;String, HashMap&amp;lt;String, Integer&amp;gt;&amp;gt; brokerVersionTable;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;客户端的协议处理器&lt;/strong&gt;：用于处理 IO 事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ClientRemotingProcessor clientRemotingProcessor;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final PullMessageService pullMessageService;		// 拉消息服务
private final RebalanceService rebalanceService;			// 消费者负载均衡服务
private final ConsumerStatsManager consumerStatsManager;	// 消费者状态管理
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部生产者实例：处理消费端&lt;strong&gt;消息回退&lt;/strong&gt;，用该生产者发送回退消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final DefaultMQProducer defaultMQProducer;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;心跳次数统计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MQClientInstance 有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
    this.clientConfig = clientConfig;
    this.instanceIndex = instanceIndex;
    // Netty 相关的配置信息
    this.nettyClientConfig = new NettyClientConfig();
    // 平台核心数
    this.nettyClientConfig.setClientCallbackExecutorThreads(...);
    this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS());
    // 【创建客户端协议处理器】
    this.clientRemotingProcessor = new ClientRemotingProcessor(this);
    // 创建 API 实现对象
    // 参数一：客户端网络配置
    // 参数二：客户端协议处理器，注册到客户端网络层
    // 参数三：rpcHook，注册到客户端网络层
    // 参数四：客户端配置
    this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig);

    //...
    // 内部生产者，指定内部生产者的组
    this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MQClientAPIImpl 有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) {
    this.clientConfig = clientConfig;
    topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName());
    // 创建网络层对象，参数二为 null 说明客户端并不关心 channel event
    this.remotingClient = new NettyRemotingClient(nettyClientConfig, null);
    // 业务处理器
    this.clientRemotingProcessor = clientRemotingProcessor;
    // 注册 RpcHook
    this.remotingClient.registerRPCHook(rpcHook);
	// ...
    // 注册回退消息的请求码
    this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;synchronized (this)&lt;/code&gt;：加锁保证线程安全，保证只有一个实例对象启动&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientAPIImpl.start()&lt;/code&gt;：启动客户端网络层，底层调用 RemotingClient 类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.startScheduledTask()&lt;/code&gt;：启动定时任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.pullMessageService.start()&lt;/code&gt;：启动拉取消息服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.rebalanceService.start()&lt;/code&gt;：启动负载均衡服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.defaultMQProducer...start(false)&lt;/code&gt;：启动内部生产者，参数为 false 代表不启动实例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;startScheduledTask()：&lt;strong&gt;启动定时任务&lt;/strong&gt;，调度线程池是单线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == this.clientConfig.getNamesrvAddr())&lt;/code&gt;：Namesrv 地址是空，需要两分钟拉取一次 Namesrv 地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 1：&lt;strong&gt;从 Namesrv 更新客户端本地的路由数据&lt;/strong&gt;，周期 30 秒一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取生产者和消费者订阅的主题集合，遍历集合，对比从 namesrv 拉取最新的主题路由数据和本地数据，是否需要更新
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 2：周期 30 秒一次，两个任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;清理下线的 Broker 节点&lt;/strong&gt;，遍历客户端的 Broker 物理节点映射表，将所有主题数据都不包含的 Broker 物理节点清理掉，如果被清理的 Broker 下所有的物理节点都没有了，就将该 Broker 的映射数据删除掉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向在线的所有的 Broker 发送心跳数据&lt;/strong&gt;，同步发送的方式，返回值是 Broker 物理节点的版本号，更新版本映射表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;MQClientInstance.this.cleanOfflineBroker();
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 心跳数据
public class HeartbeatData extends RemotingSerializable {
    // 客户端 ID  ip@pid
    private String clientID;
    // 存储客户端所有生产者数据
    private Set&amp;lt;ProducerData&amp;gt; producerDataSet = new HashSet&amp;lt;ProducerData&amp;gt;();
    // 存储客户端所有消费者数据
    private Set&amp;lt;ConsumerData&amp;gt; consumerDataSet = new HashSet&amp;lt;ConsumerData&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 3：消费者持久化消费数据，周期 5 秒一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MQClientInstance.this.persistAllConsumerOffset();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 4：动态调整消费者线程池，周期 1 分钟一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MQClientInstance.this.adjustThreadPool();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;updateTopicRouteInfoFromNameServer()：&lt;strong&gt;更新路由数据&lt;/strong&gt;，通过加锁保证当前实例只有一个线程去更新&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (isDefault &amp;amp;&amp;amp; defaultMQProducer != null)&lt;/code&gt;：需要默认数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()&lt;/code&gt;：从 Namesrv 获取默认的 TBW102 的路由数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;topicRouteData = ...getTopicRouteInfoFromNameServer(topic)&lt;/code&gt;：需要&lt;strong&gt;从 Namesrv 获取&lt;/strong&gt;路由数据（同步）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;old = this.topicRouteTable.get(topic)&lt;/code&gt;：获取客户端实例本地的该主题的路由数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean changed = topicRouteDataIsChange(old, topicRouteData)&lt;/code&gt;：对比本地和最新下拉的数据是否一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (changed)&lt;/code&gt;：不一致进入更新逻辑&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.brokerAddrTable.put(...)&lt;/code&gt;：更新客户端 broker 物理&lt;strong&gt;节点映射表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Update Pub info&lt;/code&gt;：更新生产者信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)&lt;/code&gt;：将主题路由数据转化为发布数据，会&lt;strong&gt;创建消息队列 MQ&lt;/strong&gt;，放入发布数据对象的集合中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;impl.updateTopicPublishInfo(topic, publishInfo)&lt;/code&gt;：生产者将主题的发布数据保存到它本地，方便发送消息使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Update sub info&lt;/code&gt;：更新消费者信息，创建 MQ 队列，更新订阅信息，用于负载均衡&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.topicRouteTable.put(topic, cloneTopicRouteData)&lt;/code&gt;：&lt;strong&gt;将数据放入本地路由表&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;网络通信&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;NettyRemotingClient 类负责客户端的网络通信&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Netty 服务相关属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final NettyClientConfig nettyClientConfig;			// 客户端的网络层配置
private final Bootstrap bootstrap = new Bootstrap();		// 客户端网络层启动对象
private final EventLoopGroup eventLoopGroupWorker;			// 客户端网络层 Netty IO 线程组
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Channel 映射表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ConcurrentMap&amp;lt;String, ChannelWrapper&amp;gt; channelTables;// key 是服务器的地址，value 是通道对象
private final Lock lockChannelTables = new ReentrantLock();		  // 锁，控制并发安全
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时器：启动定时任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Timer timer = new Timer(&quot;ClientHouseKeepingService&quot;, true)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ExecutorService publicExecutor;		// 公共线程池
private ExecutorService callbackExecutor; 	// 回调线程池，客户端发起异步请求，服务器的响应数据由回调线程池处理
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件监听器：客户端这里是 null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ChannelEventListener channelEventListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public NettyRemotingClient(final NettyClientConfig nettyClientConfig) {
    this(nettyClientConfig, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public NettyRemotingClient(nettyClientConfig, channelEventListener) {
    // 父类创建了2个信号量，1、控制单向请求的并发度，2、控制异步请求的并发度
    super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue());
    this.nettyClientConfig = nettyClientConfig;
    this.channelEventListener = channelEventListener;

    // 创建公共线程池
    int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads();
    if (publicThreadNums &amp;lt;= 0) {
        publicThreadNums = 4;
    }
    this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,);

    // 创建 Netty IO 线程，1个线程
    this.eventLoopGroupWorker = new NioEventLoopGroup(1, );

    if (nettyClientConfig.isUseTLS()) {
  		sslContext = TlsHelper.buildSslContext(true);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() {
    // channel pipeline 内的 handler 使用的线程资源，默认 4 个
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup();
    // 配置 netty 客户端启动类对象
    Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)
        //...
        .handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                // 加几个handler
                pipeline.addLast(
                    // 服务端的数据，都会来到这个
                    new NettyClientHandler());
            }
        });
    // 注意 Bootstrap 只是配置好客户端的元数据了，【在这里并没有创建任何 channel 对象】
    // 定时任务 扫描 responseTable 中超时的 ResponseFuture，避免客户端线程长时间阻塞
    this.timer.scheduleAtFixedRate(() -&amp;gt; {
     	NettyRemotingClient.this.scanResponseTable();
    }, 1000 * 3, 1000);
    // 这里是 null，不启动
    if (this.channelEventListener != null) {
        this.nettyEventExecutor.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单向通信：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) {
    // 开始时间
    long beginStartTime = System.currentTimeMillis();
    // 获取或者创建客户端与服务端（addr）的通道 channel
    final Channel channel = this.getAndCreateChannel(addr);
    // 条件成立说明客户端与服务端 channel 通道正常，可以通信
    if (channel != null &amp;amp;&amp;amp; channel.isActive()) {
        try {
            // 执行 rpcHook 拓展点
            doBeforeRpcHooks(addr, request);
            // 计算耗时，如果当前耗时已经超过 timeoutMillis 限制，则直接抛出异常，不再进行系统通信
            long costTime = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis &amp;lt; costTime) {
                throw new RemotingTimeoutException(&quot;invokeSync call timeout&quot;);
            }
            // 参数1：客户端-服务端通道channel
            // 参数二：网络层传输对象，封装着请求数据
            // 参数三：剩余的超时限制
            RemotingCommand response = this.invokeSyncImpl(channel, request, ...);
            // 后置处理
            doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
            // 返回响应数据
            return response;
        } catch (RemotingSendRequestException e) {}
    } else {
        this.closeChannel(addr, channel);
        throw new RemotingConnectException(addr);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;延迟消息&lt;/h4&gt;
&lt;h5&gt;消息处理&lt;/h5&gt;
&lt;p&gt;BrokerStartup 初始化 BrokerController 调用 &lt;code&gt;registerProcessor()&lt;/code&gt; 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中，对应的请求 ID 为 &lt;code&gt;SEND_MESSAGE = 10&lt;/code&gt;，NettyServerHandler 在处理请求时通过 CMD 会获取处理器执行 processRequest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：处理通道的事件；   参数二：客户端
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)  {
    RemotingCommand response = null;
   	response = asyncProcessRequest(ctx, request).get();
    return response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SendMessageProcessor#asyncConsumerSendMsgBack：异步发送消费者的回调消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final RemotingCommand response&lt;/code&gt;：创建一个服务器响应对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final ConsumerSendMsgBackRequestHeader requestHeader&lt;/code&gt;：解析出客户端请求头信息，几个&lt;strong&gt;核心字段&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private Long offset&lt;/code&gt;：回退消息的 CommitLog offset&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private Integer delayLevel&lt;/code&gt;：延迟级别，一般是 0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private String originMsgId, originTopic&lt;/code&gt;：原始的消息 ID，主题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private Integer maxReconsumeTimes&lt;/code&gt;：最大重试次数，默认是 16 次&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if ()&lt;/code&gt;：鉴权，是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String newTopic = MixAll.getRetryTopic(...)&lt;/code&gt;：&lt;strong&gt;获取消费者组的重试主题&lt;/strong&gt;，规则是 &lt;code&gt;%RETRY%GroupName&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int queueIdInt = Math.abs()&lt;/code&gt;：&lt;strong&gt;重试主题下的队列 ID 是 0&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TopicConfig topicConfig&lt;/code&gt;：获取重试主题的配置信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageExt msgExt&lt;/code&gt;：根据消息的物理 offset 到存储模块查询，内部先查询出这条消息的 size，然后再根据 offset 和 size 查询出整条 msg&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final String retryTopic&lt;/code&gt;：获取消息的原始主题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == retryTopic)&lt;/code&gt;：条件成立说明&lt;strong&gt;当前消息是第一次被回退&lt;/strong&gt;， 添加 &lt;code&gt;RETRY_TOPIC&lt;/code&gt; 属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msgExt.setWaitStoreMsgOK(false)&lt;/code&gt;：异步刷盘&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (msgExt...() &amp;gt;= maxReconsumeTimes || delayLevel &amp;lt; 0)&lt;/code&gt;：消息重试次数超过最大次数，不支持重试&lt;/p&gt;
&lt;p&gt;&lt;code&gt;newTopic = MixAll.getDLQTopic()&lt;/code&gt;：&lt;strong&gt;获取消费者的死信队列&lt;/strong&gt;，规则是 &lt;code&gt;%DLQ%GroupName&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;queueIdInt, topicConfig&lt;/code&gt;：死信队列 ID 为 0，创建死信队列的配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (0 == delayLevel)&lt;/code&gt;：说明延迟级别由 Broker 控制&lt;/p&gt;
&lt;p&gt;&lt;code&gt;delayLevel = 3 + msgExt.getReconsumeTimes()&lt;/code&gt;：&lt;strong&gt;延迟级别默认从 3 级开始&lt;/strong&gt;，每重试一次，延迟级别 +1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msgExt.setDelayTimeLevel(delayLevel)&lt;/code&gt;：&lt;strong&gt;将延迟级别设置进消息属性&lt;/strong&gt;，存储时会检查该属性，该属性值 &amp;gt; 0 会&lt;strong&gt;将消息的主题和队列修改为调度主题和调度队列 ID&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageExtBrokerInner msgInner&lt;/code&gt;：创建一条空消息，消息属性从 offset 查询出来的 msg 中拷贝&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msgInner.setReconsumeTimes)&lt;/code&gt;：重试次数设置为原 msg 的次数 +1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UtilAll.isBlank(originMsgId)&lt;/code&gt;：判断消息是否是初次返回到服务器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;true：说明 msgExt 消息是第一次被返回到服务器，此时使用该 msg 的 id 作为 originMessageId&lt;/li&gt;
&lt;li&gt;false：说明原始消息已经被重试不止 1 次，此时使用 offset 查询出来的 msg 中的 originMessageId&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CompletableFuture putMessageResult = ..asyncPutMessage(msgInner)&lt;/code&gt;：调用存储模块存储消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore#asyncPutMessage&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PutMessageResult result = this.commitLog.asyncPutMessage(msg)&lt;/code&gt;：&lt;strong&gt;将新消息存储到 CommitLog 中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;调度服务&lt;/h5&gt;
&lt;p&gt;DefaultMessageStore 中有成员属性 ScheduleMessageService，在 start 方法中会启动该调度服务&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;延迟级别属性表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 存储延迟级别对应的 延迟时间长度 （单位：毫秒）
private final ConcurrentMap&amp;lt;Integer /* level */, Long/* delay timeMillis */&amp;gt; delayLevelTable;
// 存储延迟级别 queue 的消费进度 offset，该 table 每 10 秒钟，会持久化一次，持久化到本地磁盘
private final ConcurrentMap&amp;lt;Integer /* level */, Long/* offset */&amp;gt; offsetTable;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最大延迟级别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int maxDelayLevel;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块启动状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AtomicBoolean started = new AtomicBoolean(false);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时器：内部有线程资源，可执行调度任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Timer timer;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;load()：加载调度消息，&lt;strong&gt;初始化 delayLevelTable 和 offsetTable&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean load()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;start()：启动消息调度服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (started.compareAndSet(false, true))&lt;/code&gt;：将启动状态设为 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.timer&lt;/code&gt;：创建定时器对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (... : this.delayLevelTable.entrySet())&lt;/code&gt;：为&lt;strong&gt;每个延迟级别创建一个延迟任务&lt;/strong&gt;提交到 timer ，周期执行，这样就可以&lt;strong&gt;将延迟消息得到及时的消费&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.timer.scheduleAtFixedRate()&lt;/code&gt;：提交周期型任务，延迟 10 秒执行，周期为 10 秒，持久化延迟队列消费进度任务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScheduleMessageService.this.persist()&lt;/code&gt;：持久化消费进度&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;调度任务&lt;/h5&gt;
&lt;p&gt;DeliverDelayedMessageTimerTask 是一个任务类&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;延迟级别：延迟队列任务处理的延迟级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int delayLevel;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费进度：延迟队列任务处理的延迟队列的消费进度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final long offset;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：执行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    if (isStarted()) {
        this.executeOnTimeup();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;executeOnTimeup()：执行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void executeOnTimeup()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConsumeQueue cq&lt;/code&gt;：获取出该延迟队列任务处理的&lt;strong&gt;延迟队列 ConsumeQueue&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SelectMappedBufferResult bufferCQ&lt;/code&gt;：根据消费进度查询出 SMBR 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (; i &amp;lt; bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE)&lt;/code&gt;：每次读取 20 各字节的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;offsetPy, sizePy&lt;/code&gt;：延迟消息的物理偏移量和消息大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long tagsCode&lt;/code&gt;：延迟消息的交付时间，在 ReputMessageService 转发时根据消息的 DELAY 属性是否 &amp;gt;0 ，会在 tagsCode 字段存储交付时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long deliver... = this.correctDeliverTimestamp(..)&lt;/code&gt;：&lt;strong&gt;校准交付时间&lt;/strong&gt;，延迟时间过长会调整为当前时间立刻执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long countdown = deliverTimestamp - now&lt;/code&gt;：计算差值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (countdown &amp;lt;= 0)&lt;/code&gt;：&lt;strong&gt;消息已经到达交付时间了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MessageExt msgExt&lt;/code&gt;：根据物理偏移量和消息大小获取这条消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MessageExtBrokerInner msgInner&lt;/code&gt;：&lt;strong&gt;构建一条新消息&lt;/strong&gt;，将原消息的属性拷贝过来&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;long tagsCodeValue&lt;/code&gt;：不再是交付时间了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageAccessor.clearProperty(msgInner, DELAY..)&lt;/code&gt;：清理新消息的 DELAY 属性，避免存储时重定向到延迟队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msgInner.setTopic()&lt;/code&gt;：&lt;strong&gt;修改主题为原始的主题 &lt;code&gt;%RETRY%GroupName&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String queueIdStr&lt;/code&gt;：修改队列 ID 为原始的 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;PutMessageResult putMessageResult&lt;/code&gt;：&lt;strong&gt;将新消息存储到 CommitLog&lt;/strong&gt;，消费者订阅的是目标主题，会再次消费该消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：消息还未到达交付时间&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScheduleMessageService.this.timer.schedule()&lt;/code&gt;：创建该延迟级别的任务，延迟 countDown 毫秒之后再执行&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScheduleMessageService.this.updateOffset()&lt;/code&gt;：更新延迟级别队列的消费进度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PutMessageResult putMessageResult&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bufferCQ == null&lt;/code&gt;：说明通过消费进度没有获取到数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (offset &amp;lt; cqMinOffset)&lt;/code&gt;：如果消费进度比最小位点都小，说明是过期数据，重置为最小位点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ScheduleMessageService.this.timer.schedule()&lt;/code&gt;：重新提交该延迟级别对应的延迟队列任务，延迟 100 毫秒后执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;事务消息&lt;/h4&gt;
&lt;h5&gt;生产者类&lt;/h5&gt;
&lt;p&gt;TransactionMQProducer 类发送事务消息时使用&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事务回查线程池资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ExecutorService executorService;

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务监听器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private TransactionListener transactionListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.defaultMQProducerImpl.initTransactionEnv()&lt;/code&gt;：初始化生产者实例和回查线程池资源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;super.start()&lt;/code&gt;：启动生产者实例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendMessageInTransaction()：发送事务消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) {
    msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
    // 调用实现类的发送方法
    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TransactionListener transactionListener = getCheckListener()&lt;/code&gt;：获取监听器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == localTransactionExecuter &amp;amp;&amp;amp; null == transactionListener)&lt;/code&gt;：两者都为 null 抛出异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, &quot;true&quot;)&lt;/code&gt;：&lt;strong&gt;设置事务标志&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sendResult = this.send(msg)&lt;/code&gt;：发送消息，同步发送&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (sendResult.getSendStatus())&lt;/code&gt;：&lt;strong&gt;判断发送消息的结果状态&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case SEND_OK&lt;/code&gt;：消息发送成功&lt;/p&gt;
&lt;p&gt;&lt;code&gt;msg.setTransactionId(transactionId)&lt;/code&gt;：&lt;strong&gt;设置事务 ID 为消息的 UNIQ_KEY 属性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;localTransactionState = ...executeLocalTransactionBranch(msg, arg)&lt;/code&gt;：&lt;strong&gt;执行本地事务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case SLAVE_NOT_AVAILABLE&lt;/code&gt;：其他情况都需要回滚事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE&lt;/code&gt;：&lt;strong&gt;事务状态设置为回滚&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.endTransaction(sendResult, ...)&lt;/code&gt;：结束事务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EndTransactionRequestHeader requestHeader&lt;/code&gt;：构建事务结束头对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()&lt;/code&gt;：向 Broker 发起事务结束的单向请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;接受消息&lt;/h5&gt;
&lt;p&gt;SendMessageProcessor 是服务端处理客户端发送来的消息的处理器，&lt;code&gt;processRequest()&lt;/code&gt; 方法处理请求&lt;/p&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;asyncProcessRequest()&lt;/code&gt;：处理请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CompletableFuture&amp;lt;RemotingCommand&amp;gt; asyncProcessRequest(ChannelHandlerContext ctx,
                                                              RemotingCommand request) {
    final SendMessageContext mqtraceContext;
    switch (request.getCode()) {
            // 回调消息回退
        case RequestCode.CONSUMER_SEND_MSG_BACK:
            return this.asyncConsumerSendMsgBack(ctx, request);
        default:
            // 解析出请求头对象
            SendMessageRequestHeader requestHeader = parseRequestHeader(request);
            if (requestHeader == null) {
                return CompletableFuture.completedFuture(null);
            }
            // 创建上下文对象
            mqtraceContext = buildMsgContext(ctx, requestHeader);
            // 前置处理器
            this.executeSendMessageHookBefore(ctx, request, mqtraceContext);
            // 判断是否是批量消息
            if (requestHeader.isBatch()) {
                return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader);
            } else {
                return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader);
            }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;asyncSendMessage()：异步处理发送消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private CompletableFuture&amp;lt;RemotingCommand&amp;gt; asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RemotingCommand response&lt;/code&gt;：创建响应对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageExtBrokerInner msgInner = new MessageExtBrokerInner()&lt;/code&gt;：创建 msgInner 对象，并赋值相关的属性，主题和队列 ID 都是请求头中的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String transFlag&lt;/code&gt;：&lt;strong&gt;获取事务属性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (transFlag != null &amp;amp;&amp;amp; Boolean.parseBoolean(transFlag))&lt;/code&gt;：判断事务属性是否是 true，走事务消息的存储流程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;putMessageResult = ...asyncPrepareMessage(msgInner)&lt;/code&gt;：&lt;strong&gt;事务消息处理流程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CompletableFuture&amp;lt;PutMessageResult&amp;gt; asyncPutHalfMessage(MessageExtBrokerInner messageInner) {
    // 调用存储模块，将修改后的 msg 存储进 Broker(CommitLog)
    return store.asyncPutMessage(parseHalfMessageInner(messageInner));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TransactionalMessageBridge#parseHalfMessageInner：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MessageAccessor.putProperty(...)&lt;/code&gt;：&lt;strong&gt;将消息的原主题和队列 ID 放入消息的属性中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msgInner.setSysFlag(...)&lt;/code&gt;：消息设置为非事务状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())&lt;/code&gt;：&lt;strong&gt;消息主题设置为半消息主题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msgInner.setQueueId(0)&lt;/code&gt;：&lt;strong&gt;队列 ID 设置为 0&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：普通消息存储&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;回查处理&lt;/h5&gt;
&lt;p&gt;ClientRemotingProcessor 是客户端用于处理请求，创建 MQClientAPIImpl 时将该处理器注册到 Netty 中，&lt;code&gt;processRequest()&lt;/code&gt; 方法根据请求的命令码，进行不同的处理，事务回查的处理命令码为 &lt;code&gt;CHECK_TRANSACTION_STATE&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Broker 端有定时任务发送回查请求&lt;/p&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;checkTransactionState()：检查事务状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final CheckTransactionStateRequestHeader requestHeader&lt;/code&gt;：解析出请求头对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final MessageExt messageExt&lt;/code&gt;：从请求 body 中解析出服务器回查的事务消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String transactionId&lt;/code&gt;：提取 UNIQ_KEY 字段属性值赋值给事务 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final String group&lt;/code&gt;：提取生产者组名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MQProducerInner producer = this...selectProducer(group)&lt;/code&gt;：根据生产者组获取生产者对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String addr = RemotingHelper.parseChannelRemoteAddr()&lt;/code&gt;：解析出要回查的 Broker 服务器的地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;producer.checkTransactionState(addr, messageExt, requestHeader)&lt;/code&gt;：生产者的事务回查
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Runnable request = new Runnable()&lt;/code&gt;：&lt;strong&gt;创建回查事务状态任务对象&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;获取生产者的 TransactionCheckListener 和 TransactionListener，选择一个不为 null 的监听器进行事务状态回查&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.processTransactionState()&lt;/code&gt;：处理回查状态
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EndTransactionRequestHeader thisHeader&lt;/code&gt;：构建 EndTransactionRequestHeader 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DefaultMQProducerImpl...endTransactionOneway()&lt;/code&gt;：向 Broker 发起结束事务单向请求，&lt;strong&gt;二阶段提交&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.checkExecutor.submit(request)&lt;/code&gt;：提交到线程池运行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考图：https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0&lt;/p&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/457326371&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;事务提交&lt;/h5&gt;
&lt;p&gt;EndTransactionProcessor 类是服务端用来处理客户端发来的提交或者回滚请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;processRequest()：处理请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;EndTransactionRequestHeader requestHeader&lt;/code&gt;：从请求中解析出 EndTransactionRequestHeader&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (MessageSysFlag.TRANSACTION_COMMIT_TYPE)&lt;/code&gt;：&lt;strong&gt;事务提交&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result = this.brokerController...commitMessage(requestHeader)&lt;/code&gt;：根据 commitLogOffset 提取出 halfMsg 消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MessageExtBrokerInner msgInner&lt;/code&gt;：根据 result 克隆出一条新消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msgInner.setTopic(msgExt.getUserProperty(...))&lt;/code&gt;：&lt;strong&gt;设置回原主题&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))&lt;/code&gt;：&lt;strong&gt;设置回原队列 ID&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageAccessor.clearProperty()&lt;/code&gt;：清理上面的两个属性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MessageAccessor.clearProperty(msgInner, ...)&lt;/code&gt;：&lt;strong&gt;清理事务属性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RemotingCommand sendResult = sendFinalMessage(msgInner)&lt;/code&gt;：调用存储模块存储至 Broker&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.brokerController...deletePrepareMessage(result.getPrepareMessage())&lt;/code&gt;：&lt;strong&gt;向删除（OP）队列添加消息&lt;/strong&gt;，消息体的数据是 halfMsg 的 queueOffset，&lt;strong&gt;表示半消息队列指定的 offset 的消息已被删除&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))&lt;/code&gt;：添加一条 OP 数据
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MessageQueue messageQueue&lt;/code&gt;：新建一个消息队列，OP 队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return addRemoveTagInTransactionOp(messageExt, messageQueue)&lt;/code&gt;：添加数据
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Message message&lt;/code&gt;：创建 OP 消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeOp(message, messageQueue)&lt;/code&gt;：写入 OP 消息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE)&lt;/code&gt;：&lt;strong&gt;事务回滚&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.brokerController...deletePrepareMessage(result.getPrepareMessage())&lt;/code&gt;：&lt;strong&gt;也需要向 OP 队列添加消息&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Consumer&lt;/h3&gt;
&lt;h4&gt;消费者类&lt;/h4&gt;
&lt;h5&gt;默认消费&lt;/h5&gt;
&lt;p&gt;DefaultMQPushConsumer 类是默认的消费者类&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;消费者实现类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String consumerGroup;									// 消费者组
private MessageModel messageModel = MessageModel.CLUSTERING;	// 消费模式，默认集群模式
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;订阅信息：key 是主题，value 是过滤表达式，一般是 tag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Map&amp;lt;String, String &amp;gt; subscription = new HashMap&amp;lt;String, String&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息监听器：&lt;strong&gt;消息处理逻辑&lt;/strong&gt;，并发消费 MessageListenerConcurrently，顺序（分区）消费 MessageListenerOrderly&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private MessageListener messageListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费位点：当从 Broker 获取当前组内该 queue 的 offset 不存在时，consumeFromWhere 才有效，默认值代表从队列的最后 offset 开始消费，当队列内再有一条新的 msg 加入时，消费者才会去消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费时间戳：当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时，并且服务器 Group 内不存在该 queue 的 offset 时，会使用该时间戳进行消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒，转换成 格式： 年月日小时分钟秒，比如 20220203171201
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;队列分配策略：主题下的队列分配策略，RebalanceImpl 对象依赖该算法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费进度存储器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private OffsetStore offsetStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动消费者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：关闭消费者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;registerMessageListener()：注册消息监听器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void registerMessageListener(MessageListener messageListener) 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;subscribe()：添加订阅信息，&lt;strong&gt;将订阅信息放入负载均衡对象的 subscriptionInner 中&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void subscribe(String topic, String subExpression)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unsubscribe()：删除订阅指定主题的信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unsubscribe(String topic)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;suspend()：停止消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void suspend()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resume()：恢复消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void resume()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;默认实现&lt;/h5&gt;
&lt;p&gt;DefaultMQPushConsumerImpl 是默认消费者的实现类&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端实例：整个进程内只有一个客户端实例对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private MQClientInstance mQClientFactory;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费者实例：门面对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; private final DefaultMQPushConsumer defaultMQPushConsumer;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;负载均衡&lt;/strong&gt;：分配订阅主题的队列给当前消费者，20 秒钟一个周期执行 Rebalance 算法（客户端实例触发）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费者信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final long consumerStartTimestamp;	// 消费者启动时间
private volatile ServiceState serviceState;	// 消费者状态
private volatile boolean pause = false;		// 是否暂停
private boolean consumeOrderly = false;		// 是否顺序消费
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拉取消息&lt;/strong&gt;：封装拉消息的 API，服务器 Broker 返回结果中包含下次 Pull 时推荐的 BrokerId，根据本次请求数据的冷热程度进行推荐&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private PullAPIWrapper pullAPIWrapper;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;消息消费&lt;/strong&gt;服务：并发消费和顺序消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ConsumeMessageService consumeMessageService;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;流控：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long queueFlowControlTimes = 0;			// 队列流控次数，默认每1000次流控，进行一次日志打印
private long queueMaxSpanFlowControlTimes = 0;	// 流控使用，控制打印日志
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HOOK：钩子方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 过滤消息 hook
private final ArrayList&amp;lt;FilterMessageHook&amp;gt; filterMessageHookList;
// 消息执行hook，在消息处理前和处理后分别执行 hook.before  hook.after 系列方法
private final ArrayList&amp;lt;ConsumeMessageHook&amp;gt; consumeMessageHookList;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：加锁保证线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public synchronized void start() 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.checkConfig()&lt;/code&gt;：检查配置，包括组名、消费模式、订阅信息、消息监听器等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.copySubscription()&lt;/code&gt;：拷贝订阅信息到 RebalanceImpl 对象
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)&lt;/code&gt;：将订阅信息加入 rbl 的 map 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.messageListenerInner = ...getMessageListener()&lt;/code&gt;：将消息监听器保存到实例对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;switch (this.defaultMQPushConsumer.getMessageModel())&lt;/code&gt;：判断消费模式，广播模式下直接返回&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final String retryTopic&lt;/code&gt;：创建当前&lt;strong&gt;消费者组重试的主题名&lt;/strong&gt;，规则 &lt;code&gt;%RETRY%ConsumerGroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()&lt;/code&gt;：创建重试主题的订阅数据对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)&lt;/code&gt;：将创建的重试主题加入到 rbl 对象的 map 中，&lt;strong&gt;消息重试时会加入到该主题，消费者订阅这个主题之后，就有机会再次拿到该消息进行消费处理&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientFactory = ...getOrCreateMQClientInstance()&lt;/code&gt;：获取客户端实例对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.rebalanceImpl.&lt;/code&gt;：初始化负载均衡对象，设置&lt;strong&gt;队列分配策略对象&lt;/strong&gt;到属性中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.pullAPIWrapper = new PullAPIWrapper()&lt;/code&gt;：创建拉消息 API 对象，内部封装了查询推荐主机算法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)&lt;/code&gt;：将过滤 Hook 列表注册到该对象内，消息拉取下来之后会执行该 Hook，&lt;strong&gt;再进行一次自定义的消息过滤&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.offsetStore = new RemoteBrokerOffsetStore()&lt;/code&gt;：默认集群模式下创建消息进度存储器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.consumeMessageService = ...&lt;/code&gt;：根据消息监听器的类型创建消费服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.consumeMessageService.start()&lt;/code&gt;：启动消费服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean registerOK = mQClientFactory.registerConsumer()&lt;/code&gt;：&lt;strong&gt;将消费者注册到客户端实例中&lt;/strong&gt;，客户端提供的服务：
&lt;ul&gt;
&lt;li&gt;心跳服务：把订阅数据同步到订阅主题的 Broker&lt;/li&gt;
&lt;li&gt;拉消息服务：内部 PullMessageService 启动线程，基于 PullRequestQueue 工作，消费者负载均衡分配到队列后会向该队列提交 PullRequest&lt;/li&gt;
&lt;li&gt;队列负载服务：每 20 秒调用一次 &lt;code&gt;consumer.doRebalance()&lt;/code&gt; 接口&lt;/li&gt;
&lt;li&gt;消息进度持久化&lt;/li&gt;
&lt;li&gt;动态调整消费者、消费服务线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mQClientFactory.start()&lt;/code&gt;：启动客户端实例&lt;/li&gt;
&lt;li&gt;&lt;code&gt; this.updateTopic&lt;/code&gt;：从 nameserver 获取主题路由数据，生成主题集合放入 rbl 对象的 table&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientFactory.checkClientInBroker()&lt;/code&gt;：检查服务器是否支持消息过滤模式，一般使用 tag 过滤，服务器默认支持&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()&lt;/code&gt;：向所有已知的 Broker 节点，&lt;strong&gt;发送心跳数据&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.mQClientFactory.rebalanceImmediately()&lt;/code&gt;：唤醒 rbl 线程，触发负载均衡执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;负载均衡&lt;/h4&gt;
&lt;h5&gt;实现方式&lt;/h5&gt;
&lt;p&gt;MQClientInstance#start 中会启动负载均衡服务 RebalanceService：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
	// 检查停止标记
    while (!this.isStopped()) {
        // 休眠 20 秒，防止其他线程饥饿，所以【每 20 秒负载均衡一次】
        this.waitForRunning(waitInterval);
        // 调用客户端实例的负载均衡方法，底层【会遍历所有消费者，调用消费者的负载均衡】
        this.mqClientFactory.doRebalance();
    }	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RebalanceImpl 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分配给当前消费者的处理队列：处理消息队列集合，&lt;strong&gt;ProcessQueue 是 MQ 队列在消费者端的快照&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final ConcurrentMap&amp;lt;MessageQueue, ProcessQueue&amp;gt; processQueueTable;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费者订阅主题的队列信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final ConcurrentMap&amp;lt;String/* topic */, Set&amp;lt;MessageQueue&amp;gt;&amp;gt; topicSubscribeInfoTable;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;订阅数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final ConcurrentMap&amp;lt;String/* topic */, SubscriptionData&amp;gt; subscriptionInner;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;队列分配策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected AllocateMessageQueueStrategy allocateMessageQueueStrategy;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;doRebalance()：负载均衡方法，以每个消费者实例为粒度进行负载均衡&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void doRebalance(final boolean isOrder) {
    // 获取当前消费者的订阅数据
    Map&amp;lt;String, SubscriptionData&amp;gt; subTable = this.getSubscriptionInner();
    if (subTable != null) {
        // 遍历所有的订阅主题
        for (final Entry&amp;lt;String, SubscriptionData&amp;gt; entry : subTable.entrySet()) {
            // 获取订阅的主题
            final String topic = entry.getKey();
            // 按照主题进行负载均衡
            this.rebalanceByTopic(topic, isOrder);
        }
    }
    // 将分配到当前消费者的队列进行过滤，不属于当前消费者订阅主题的直接移除
    this.truncateMessageQueueNotMyTopic();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群模式下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Set&amp;lt;MessageQueue&amp;gt; mqSet = this.topicSubscribeInfoTable.get(topic)&lt;/code&gt;：订阅的主题下的全部队列信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cidAll = this...findConsumerIdList(topic, consumerGroup)&lt;/code&gt;：从服务器获取消费者组下的全部消费者 ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Collections.sort(mqAll)&lt;/code&gt;：主题 MQ 队列和消费者 ID 都进行排序，&lt;strong&gt;保证每个消费者的视图一致性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;allocateResult = strategy.allocate()&lt;/code&gt;： &lt;strong&gt;调用队列分配策略&lt;/strong&gt;，给当前消费者进行分配 MessageQueue（下一节）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean changed = this.updateProcessQueueTableInRebalance(...)&lt;/code&gt;：&lt;strong&gt;更新队列处理集合&lt;/strong&gt;，mqSet 是 rbl 算法分配到当前消费者的 MQ 集合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (it.hasNext())&lt;/code&gt;：遍历当前消费者的所有处理队列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mq.getTopic().equals(topic))&lt;/code&gt;：该 MQ 是 本次 rbl 分配算法计算的主题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!mqSet.contains(mq))&lt;/code&gt;：该 MQ 经过 rbl 计算之后，&lt;strong&gt;被分配到其它 Consumer 节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pq.setDropped(true)&lt;/code&gt;：将删除状态设置为 true&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (this.removeUnnecessaryMessageQueue(mq, pq))&lt;/code&gt;：删除不需要的 MQ 队列&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this...getOffsetStore().persist(mq)&lt;/code&gt;：在 MQ 归属的 Broker 节点持久化消费进度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this...getOffsetStore().removeOffset(mq)&lt;/code&gt;：删除该 MQ 在本地的消费进度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.defaultMQPushConsumerImpl.isConsumeOrderly() &amp;amp;&amp;amp;)&lt;/code&gt;：是否是&lt;strong&gt;顺序消费&lt;/strong&gt;和集群模式&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (pq.getLockConsume().tryLock(1000, ..))&lt;/code&gt;： 获取锁成功，说明顺序消费任务已经停止消费工作&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return this.unlockDelay(mq, pq)&lt;/code&gt;：&lt;strong&gt;释放锁 Broker 端的队列锁，向服务器发起 oneway 的解锁请求&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (pq.hasTempMessage())&lt;/code&gt;：队列中有消息，延迟 20 秒释放队列分布式锁，确保全局范围内只有一个消费任务 运行中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;else&lt;/code&gt;：当前消费者本地该消费任务已经退出，直接释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：顺序消费任务正在消费一批消息，不可打断，增加尝试获取锁的次数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;it.remove()&lt;/code&gt;：从 processQueueTable 移除该 MQ&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (pq.isPullExpired())&lt;/code&gt;：说明当前 MQ 还是被当前 Consumer 消费，此时判断一下是否超过 2 分钟未到服务器 拉消息，如果条件成立进行上述相同的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (MessageQueue mq : mqSet)&lt;/code&gt;：开始处理当前主题&lt;strong&gt;新分配到当前节点的队列&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (isOrder &amp;amp;&amp;amp; !this.lock(mq))&lt;/code&gt;：&lt;strong&gt;顺序消息为了保证有序性，需要获取队列锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProcessQueue pq = new ProcessQueue()&lt;/code&gt;：为每个新分配的消息队列创建快照队列&lt;/p&gt;
&lt;p&gt;&lt;code&gt;long nextOffset = this.computePullFromWhere(mq)&lt;/code&gt;：&lt;strong&gt;从服务端获取新分配的 MQ 的消费进度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)&lt;/code&gt;：保存到处理队列集合&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PullRequest pullRequest = new PullRequest()&lt;/code&gt;：&lt;strong&gt;创建拉取请求对象&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.dispatchPullRequest(pullRequestList)&lt;/code&gt;：放入 PullMessageService 的&lt;strong&gt;本地阻塞队列&lt;/strong&gt;内，用于拉取消息工作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lockAll()：续约锁，对消费者的所有队列进行续约&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void lockAll()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HashMap&amp;lt;String, Set&amp;lt;MessageQueue&amp;gt;&amp;gt; brokerMqs&lt;/code&gt;：将分配给当前消费者的全部 MQ 按照 BrokerName 分组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while (it.hasNext())&lt;/code&gt;：遍历所有的分组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final Set&amp;lt;MessageQueue&amp;gt; mqs&lt;/code&gt;：获取该 Broker 上分配给当前消费者的 queue 集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FindBrokerResult findBrokerResult&lt;/code&gt;：查询 Broker 主节点信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LockBatchRequestBody requestBody&lt;/code&gt;：创建请求对象，填充属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Set&amp;lt;MessageQueue&amp;gt; lockOKMQSet&lt;/code&gt;：&lt;strong&gt;以组为单位向 Broker 发起批量续约锁的同步请求&lt;/strong&gt;，返回成功的队列集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (MessageQueue mq : lockOKMQSet)&lt;/code&gt;：遍历续约锁成功的 MQ&lt;/p&gt;
&lt;p&gt;&lt;code&gt;processQueue.setLocked(true)&lt;/code&gt;：&lt;strong&gt;分布式锁状态设置为 true，表示允许顺序消费&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;processQueue.setLastLockTimestamp(System.currentTimeMillis())&lt;/code&gt;：设置上次获取锁的时间为当前时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (MessageQueue mq : mqs)&lt;/code&gt;：遍历当前 Broker 上的所有队列集合&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (!lockOKMQSet.contains(mq))&lt;/code&gt;：条件成立说明续约锁失败&lt;/p&gt;
&lt;p&gt;&lt;code&gt;processQueue.setLocked(false)&lt;/code&gt;：&lt;strong&gt;分布式锁状态设置为 false，表示不允许顺序消费&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;队列分配&lt;/h5&gt;
&lt;p&gt;AllocateMessageQueueStrategy 类是队列的分配策略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;平均分配：AllocateMessageQueueAveragely 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：消费者组       								参数二：当前消费者id   
// 参数三：主题的全部队列，包括所有 broker 上该主题的 mq  	参数四：全部消费者id集合
public List&amp;lt;MessageQueue&amp;gt; allocate(String consumerGroup, String currentCID, List&amp;lt;MessageQueue&amp;gt; mqAll, List&amp;lt;String&amp;gt; cidAll) {
    // 获取当前消费者在全部消费者中的位置，【全部消费者是已经排序好的，排在前面的优先分配更多的队列】
    int index = cidAll.indexOf(currentCID);
    // 平均分配完以后，还剩余的待分配的 mq 的数量
    int mod = mqAll.size() % cidAll.size();
    // 首先判断整体的 mq 的数量是否小于消费者的数量，小于消费者的数量就说明不够分的，先分一个
    int averageSize = mqAll.size() &amp;lt;= cidAll.size() ? 1 :
    	// 成立需要多分配一个队列，因为更靠前
    	(mod &amp;gt; 0 &amp;amp;&amp;amp; index &amp;lt; mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size());
    // 获取起始的分配位置
    int startIndex = (mod &amp;gt; 0 &amp;amp;&amp;amp; index &amp;lt; mod) ? index * averageSize : index * averageSize + mod;
    // 防止索引越界
    int range = Math.min(averageSize, mqAll.size() - startIndex);
    // 开始分配，【挨着分配，是直接就把当前的 消费者分配完成】
    for (int i = 0; i &amp;lt; range; i++) {
        result.add(mqAll.get((startIndex + i) % mqAll.size()));
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;队列排序后：Q1 → Q2 → Q3，消费者排序后 C1 → C2 → C3&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%B9%B3%E5%9D%87%E9%98%9F%E5%88%97%E5%88%86%E9%85%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;轮流分配：AllocateMessageQueueAveragelyByCircle&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-%E5%B9%B3%E5%9D%87%E9%98%9F%E5%88%97%E8%BD%AE%E6%B5%81%E5%88%86%E9%85%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定机房平均分配：AllocateMessageQueueByMachineRoom，前提是 Broker 的命名规则为 &lt;code&gt;机房名@BrokerName&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;拉取服务&lt;/h4&gt;
&lt;h5&gt;实现方式&lt;/h5&gt;
&lt;p&gt;MQClientInstance#start 中会启动消息拉取服务：PullMessageService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    // 检查停止标记，【循环拉取】
    while (!this.isStopped()) {
        try {
            // 从阻塞队列中获取拉消息请求
            PullRequest pullRequest = this.pullRequestQueue.take();
            // 拉取消息，获取请求对应的使用当前消费者组中的哪个消费者，调用消费者的 pullMessage 方法
            this.pullMessage(pullRequest);
        } catch (Exception e) {
            log.error(&quot;Pull Message Service Run Method exception&quot;, e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DefaultMQPushConsumerImpl#pullMessage：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ProcessQueue processQueue = pullRequest.getProcessQueue()&lt;/code&gt;：获取请求对应的快照队列，并判断是否是删除状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.executePullRequestLater()&lt;/code&gt;：如果当前消费者不是运行状态，则拉消息任务延迟 3 秒后执行，如果是暂停状态延迟 1 秒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流控的逻辑&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;long cachedMessageCount = processQueue.getMsgCount().get()&lt;/code&gt;：获取消费者本地该 queue 快照内缓存的消息数量，如果大于 1000 条，进行流控，延迟 50 毫秒&lt;/p&gt;
&lt;p&gt;&lt;code&gt;long cachedMessageSizeInMiB&lt;/code&gt;： 消费者本地该 queue 快照内缓存的消息容量 size，超过 100m 消息未被消费进行流控&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if(processQueue.getMaxSpan() &amp;gt; 2000)&lt;/code&gt;：消费者本地缓存消息第一条消息最后一条消息跨度超过 2000 进行流控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SubscriptionData subscriptionData&lt;/code&gt;：本次拉消息请求订阅的主题数据，如果调用了 &lt;code&gt;unsubscribe(主题)&lt;/code&gt; 将会获取为 null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PullCallback pullCallback = new PullCallback()&lt;/code&gt;：&lt;strong&gt;拉消息处理回调对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pullResult = ...processPullResult()&lt;/code&gt;：预处理 PullResult 结果，将服务器端指定 MQ 的拉消息&lt;strong&gt;下一次的推荐节点&lt;/strong&gt;保存到 pullFromWhichNodeTable 中，&lt;strong&gt;并进行消息过滤&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case FOUND&lt;/code&gt;：正常拉取到消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pullRequest.setNextOffset(pullResult.getNextBeginOffset())&lt;/code&gt;：更新 pullRequest 对象下一次拉取消息的位点&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (pullResult.getMsgFoundList() == null...)&lt;/code&gt;：消息过滤导致消息全部被过滤掉，需要立马发起下一次拉消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;boolean .. = processQueue.putMessage()&lt;/code&gt;：将服务器拉取的消息集合&lt;strong&gt;加入到消费者本地&lt;/strong&gt;的 processQueue 内&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DefaultMQPushConsumerImpl...submitConsumeRequest()&lt;/code&gt;：&lt;strong&gt;提交消费任务，分为顺序消费和并发消费&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Defaul..executePullRequestImmediately(pullRequest)&lt;/code&gt;：将更新过 nextOffset 字段的 PullRequest 对象，再次放到 pullMessageService 的阻塞队列中，&lt;strong&gt;形成闭环&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case NO_NEW_MSG ||NO_MATCHED_MSG&lt;/code&gt;：&lt;strong&gt;表示本次 pull 没有新的可消费的信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pullRequest.setNextOffset()&lt;/code&gt;：更新更新 pullRequest 对象下一次拉取消息的位点&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Defaul..executePullRequestImmediately(pullRequest)&lt;/code&gt;：再次拉取请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case OFFSET_ILLEGAL&lt;/code&gt;：&lt;strong&gt;本次 pull 时使用的 offset 是无效的&lt;/strong&gt;，即 offset &amp;gt; maxOffset || offset  &amp;lt; minOffset&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pullRequest.setNextOffset()&lt;/code&gt;：调整 pullRequest.nextOffset 为正确的 offset&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pullRequest.getProcessQueue().setDropped(true)&lt;/code&gt;：设置该 processQueue 为删除状态，如果有该 queue 的消费任务，消费任务会马上停止&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DefaultMQPushConsumerImpl.this.executeTaskLater()&lt;/code&gt;：提交异步任务，10 秒后去执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMQPushConsumerImpl...updateOffset()&lt;/code&gt;：更新 offsetStore 该 MQ 的 offset 为正确值，内部直接替换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMQPushConsumerImpl...persist()&lt;/code&gt;：持久化该 messageQueue 的 offset 到 Broker 端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMQPushConsumerImpl...removeProcessQueue()&lt;/code&gt;： 删除该消费者该 messageQueue 对应的 processQueue&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这里没有再次提交 pullRequest 到 pullMessageService 的队列，那该队列不再拉消息了吗？&lt;/p&gt;
&lt;p&gt;负载均衡 rbl 程序会重建该队列的 processQueue，重建完之后会为该队列创建新的 PullRequest 对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int sysFlag = PullSysFlag.buildSysFlag()&lt;/code&gt;：&lt;strong&gt;构建标志对象&lt;/strong&gt;，sysFlag 高 4 位未使用，低 4 位使用，从左到右 0000 0011&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一位：表示是否提交消费者本地该队列的 offset，一般是 1&lt;/li&gt;
&lt;li&gt;第二位：表示是否允许服务器端进行长轮询，一般是 1&lt;/li&gt;
&lt;li&gt;第三位：表示是否提交消费者本地该主题的订阅数据，一般是 0&lt;/li&gt;
&lt;li&gt;第四位：表示是否为类过滤，一般是 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.pullAPIWrapper.pullKernelImpl()&lt;/code&gt;：拉取消息的核心方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;封装对象&lt;/h5&gt;
&lt;p&gt;PullAPIWrapper 类封装了拉取消息的 API&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;推荐拉消息使用的主机 ID：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ConcurrentMap&amp;lt;MessageQueue, AtomicLong/* brokerId */&amp;gt; pullFromWhichNodeTable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pullKernelImpl()：拉消息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FindBrokerResult findBrokerResult&lt;/code&gt;：&lt;strong&gt;本地查询指定 BrokerName 的地址信息&lt;/strong&gt;，推荐节点或者主节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (null == findBrokerResult)&lt;/code&gt;：查询不到，就到 Namesrv 获取指定 topic 的路由数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (findBrokerResult.isSlave())&lt;/code&gt;：成立说明 findBrokerResult 表示的主机为 slave 节点，&lt;strong&gt;slave 不存储 offset 信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner)&lt;/code&gt;：将 sysFlag 标记位中 CommitOffset 的位置为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PullMessageRequestHeader requestHeader&lt;/code&gt;：创建请求头对象，封装所有的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage()&lt;/code&gt;：调用客户端实例的方法，核心逻辑就是&lt;strong&gt;将业务数据转化为 RemotingCommand  通过 NettyRemotingClient 的 IO 进行通信&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RemotingCommand request&lt;/code&gt;：创建网络层传输对象 RemotingCommand 对象，&lt;strong&gt;请求 ID 为 &lt;code&gt;PULL_MESSAGE = 11&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return this.pullMessageSync(...)&lt;/code&gt;：此处是&lt;strong&gt;异步调用，处理结果放入 ResponseFuture 中&lt;/strong&gt;，参考服务端小节的处理器类 &lt;code&gt;NettyServerHandler#processMessageReceived&lt;/code&gt; 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RemotingCommand response = responseFuture.getResponseCommand()&lt;/code&gt;：获取服务器端响应数据 response&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PullResult pullResult&lt;/code&gt;：从 response 内提取出来拉消息结果对象，将响应头 PullMessageResponseHeader 对象中信息&lt;strong&gt;填充到 PullResult 中&lt;/strong&gt;，列出两个重要的字段：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private Long suggestWhichBrokerId&lt;/code&gt;：服务端建议客户端下次 Pull 时选择的 BrokerID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private Long nextBeginOffset&lt;/code&gt;：客户端下次 Pull 时使用的 offset 信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pullCallback.onSuccess(pullResult)&lt;/code&gt;：将 PullResult 交给拉消息结果处理回调对象，调用 onSuccess 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;拉取处理&lt;/h4&gt;
&lt;h5&gt;处理器&lt;/h5&gt;
&lt;p&gt;BrokerStartup#createBrokerController 方法中创建了 BrokerController 并进行初始化，调用 &lt;code&gt;registerProcessor()&lt;/code&gt; 方法将处理器 PullMessageProcessor 注册到 NettyRemotingServer 中，对应的请求 ID 为 &lt;code&gt;PULL_MESSAGE = 11&lt;/code&gt;，NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：服务器与客户端 netty 通道； 参数二：客户端请求； 参数三：是否允许服务器端长轮询，默认 true
private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RemotingCommand response&lt;/code&gt;：创建响应对象，设置为响应类型的请求，响应头是 PullMessageResponseHeader&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final PullMessageResponseHeader responseHeader&lt;/code&gt;：获取响应对象的 header&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final PullMessageRequestHeader requestHeader&lt;/code&gt;：解析出请求头 PullMessageRequestHeader&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;response.setOpaque(request.getOpaque())&lt;/code&gt;：设置 opaque 属性，客户端&lt;strong&gt;根据该字段获取 ResponseFuture&lt;/strong&gt; 进行处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行一些鉴权的逻辑：是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConsumerGroupInfo consumerGroupInfo&lt;/code&gt;：获取消费者组信息，包含全部的消费者和订阅数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;subscriptionData = consumerGroupInfo.findSubscriptionData()&lt;/code&gt;：&lt;strong&gt;获取指定主题的订阅数据&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!ExpressionType.isTagType()&lt;/code&gt;：表达式匹配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageFilter messageFilter&lt;/code&gt;：创建消息过滤器，一般是通过 tagCode 进行过滤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultMessageStore.getMessage()&lt;/code&gt;：&lt;strong&gt;查询消息的核心逻辑，在 Broker 端查询消息&lt;/strong&gt;（存储端笔记详解了该源码）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;response.setRemark()&lt;/code&gt;：设置此次响应的状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;responseHeader.set..&lt;/code&gt;：设置响应头对象的一些字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (this.brokerController.getMessageStoreConfig().getBrokerRole())&lt;/code&gt;：如果当前主机节点角色为 slave 并且&lt;strong&gt;从节点读&lt;/strong&gt;并未开启的话，直接给客户端 一个状态 &lt;code&gt;PULL_RETRY_IMMEDIATELY&lt;/code&gt;，并设置为下次从主节点读&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.brokerController.getBrokerConfig().isSlaveReadEnable())&lt;/code&gt;：消费太慢，&lt;strong&gt;下次从另一台机器拉取&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (getMessageResult.getStatus())&lt;/code&gt;：根据 getMessageResult 的状态设置 response 的 code&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum GetMessageStatus {
    FOUND,					// 查询成功
    NO_MATCHED_MESSAGE,		// 未查询到到消息，服务端过滤 tagCode
    MESSAGE_WAS_REMOVING,	// 查询时赶上 CommitLog 清理过期文件，导致查询失败，立刻尝试
    OFFSET_FOUND_NULL,		// 查询时赶上 ConsumerQueue 清理过期文件，导致查询失败，【进行长轮询】
    OFFSET_OVERFLOW_BADLY,	// pullRequest.offset 越界 maxOffset
    OFFSET_OVERFLOW_ONE,	// pullRequest.offset == CQ.maxOffset，【进行长轮询】
    OFFSET_TOO_SMALL,		// pullRequest.offset 越界 minOffset
    NO_MATCHED_LOGIC_QUEUE,	// 没有匹配到逻辑队列
    NO_MESSAGE_IN_QUEUE,	// 空队列，创建队列也是因为查询导致，【进行长轮询】
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (response.getCode())&lt;/code&gt;：根据 response 状态做对应的业务处理&lt;/p&gt;
&lt;p&gt;&lt;code&gt;case ResponseCode.SUCCESS&lt;/code&gt;：查询成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final byte[] r = this.readGetMessageResult()&lt;/code&gt;：本次 pull 出来的全部消息导入 byte 数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.setBody(r)&lt;/code&gt;：将消息的 byte 数组保存到 response body 字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;case ResponseCode.PULL_NOT_FOUND&lt;/code&gt;：产生这种情况大部分原因是 &lt;code&gt;pullRequest.offset  ==  queue.maxOffset&lt;/code&gt;，说明已经没有需要获取的消息，此时如果直接返回给客户端，客户端会立刻重新请求，还是继续返回该状态，频繁拉取服务器导致服务器压力大，所以此处&lt;strong&gt;需要长轮询&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (brokerAllowSuspend &amp;amp;&amp;amp; hasSuspendFlag)&lt;/code&gt;：brokerAllowSuspend = true，当长轮询结束再次执行 processRequest 时该参数为 false，所以&lt;strong&gt;每次 Pull 请求至多在服务器端长轮询控制一次&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PullRequest pullRequest = new PullRequest()&lt;/code&gt;：创建长轮询 PullRequest 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.brokerController...suspendPullRequest(topic, queueId, pullRequest)&lt;/code&gt;：将长轮询请求对象交给长轮询服务
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String key = this.buildKey(topic, queueId)&lt;/code&gt;：构建一个 &lt;code&gt;topic@queueId&lt;/code&gt; 的 key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ManyPullRequest mpr = this.pullRequestTable.get(key)&lt;/code&gt;：从拉请求表中获取对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mpr.addPullRequest(pullRequest)&lt;/code&gt;：&lt;strong&gt;将 PullRequest 对象放入到长轮询的请求集合中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response = null&lt;/code&gt;：响应设置为 null 内部的 callBack 就不会给客户端发送任何数据，&lt;strong&gt;不进行通信&lt;/strong&gt;，否则就又开始重新请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean storeOffsetEnable&lt;/code&gt;：允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立，才&lt;strong&gt;在 Broker 端存储消费者组内该主题的指定 queue 的消费进度&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return response&lt;/code&gt;：返回 response，不为 null 时外层 processRequestCommand 的 callback 会将数据写给客户端&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;长轮询&lt;/h5&gt;
&lt;p&gt;PullRequestHoldService 类负责长轮询，BrokerController#start 方法中调用了 &lt;code&gt;this.pullRequestHoldService.start()&lt;/code&gt; 启动该服务&lt;/p&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：核心运行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
	// 循环运行
    while (!this.isStopped()) {
        if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
            // 服务器开启长轮询开关：每次循环休眠5秒
            this.waitForRunning(5 * 1000);
        } else {
            // 服务器关闭长轮询开关：每次循环休眠1秒
            this.waitForRunning(...);
        }
        // 检查持有的请求
        this.checkHoldRequest();
        // .....
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;checkHoldRequest()：检查所有的请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (String key : this.pullRequestTable.keySet())&lt;/code&gt;：&lt;strong&gt;处理所有的 topic@queueId 的逻辑&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR)&lt;/code&gt;：key 按照 @ 拆分，得到 topic 和 queueId&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long offset = this...getMaxOffsetInQueue(topic, queueId)&lt;/code&gt;： 到存储模块查询该 ConsumeQueue 的&lt;strong&gt;最大 offset&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.notifyMessageArriving(topic, queueId, offset)&lt;/code&gt;：通知消息到达&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;notifyMessageArriving()：&lt;strong&gt;通知消息到达&lt;/strong&gt;的逻辑，ReputMessageService 消息分发服务也会调用该方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ManyPullRequest mpr = this.pullRequestTable.get(key)&lt;/code&gt;：获取对应的的 manyPullRequest 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;PullRequest&amp;gt; requestList&lt;/code&gt;：获取该队列下的所有 PullRequest，并进行遍历&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;PullRequest&amp;gt; replayList&lt;/code&gt;：当某个 pullRequest 不超时，并且对应的 &lt;code&gt;CQ.maxOffset &amp;lt;= pullRequest.offset&lt;/code&gt;，就将该 PullRequest 再放入该列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long newestOffset&lt;/code&gt;：该值为 CQ 的 maxOffset&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (newestOffset &amp;gt; request.getPullFromThisOffset())&lt;/code&gt;：&lt;strong&gt;请求对应的队列内可以 pull 消息了，结束长轮询&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean match&lt;/code&gt;：进行过滤匹配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.brokerController...executeRequestWhenWakeup()&lt;/code&gt;：将满足条件的 pullRequest 再次提交到线程池内执行
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final RemotingCommand response&lt;/code&gt;：执行 processRequest 方法，并且&lt;strong&gt;不会触发长轮询&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;channel.writeAndFlush(response).addListene()&lt;/code&gt;：&lt;strong&gt;将结果数据发送给客户端&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (System.currentTimeMillis() &amp;gt;= ...)&lt;/code&gt;：判断该 pullRequest 是否超时，超时后的也是重新提交到线程池，并且不进行长轮询&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mpr.addPullRequest(replayList)&lt;/code&gt;：将未满足条件的 PullRequest 对象再次添加到 ManyPullRequest 属性中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;结果类&lt;/h5&gt;
&lt;p&gt;GetMessageResult 类成员信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GetMessageResult {
    // 查询消息时，最底层都是 mappedFile 支持的查询，查询时返回给外层一个 SelectMappedBufferResult，
    // mappedFile 每查询一次都会 refCount++ ，通过SelectMappedBufferResult持有mappedFile，完成资源释放的句柄
    private final List&amp;lt;SelectMappedBufferResult&amp;gt; messageMapedList =
        new ArrayList&amp;lt;SelectMappedBufferResult&amp;gt;(100);

    // 该List内存储消息，每一条消息都被转成 ByteBuffer 表示了
    private final List&amp;lt;ByteBuffer&amp;gt; messageBufferList = new ArrayList&amp;lt;ByteBuffer&amp;gt;(100);
    // 查询结果状态
    private GetMessageStatus status;
    // 客户端下次再向当前Queue拉消息时，使用的 offset
    private long nextBeginOffset;
    // 当前queue最小offset
    private long minOffset;
    // 当前queue最大offset
    private long maxOffset;
    // 消息总byte大小
    private int bufferTotalSize = 0;
    // 服务器建议客户端下次到该 queue 拉消息时是否使用 【从节点】
    private boolean suggestPullingFromSlave = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;队列快照&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ProcessQueue 类是消费队列的快照&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属性字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final AtomicLong msgCount = new AtomicLong();	// 队列中消息数量
private final AtomicLong msgSize = new AtomicLong();	// 消息总大小
private volatile long queueOffsetMax = 0L;				// 快照中最大 offset
private volatile boolean dropped = false;				// 快照是否移除
private volatile long lastPullTimestamp = current;		// 上一次拉消息的时间
private volatile long lastConsumeTimestamp = current;	// 上一次消费消息的时间
private volatile long lastLockTimestamp = current;		// 上一次获取锁的时间
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;消息容器&lt;/strong&gt;：key 是消息偏移量，val 是消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final TreeMap&amp;lt;Long, MessageExt&amp;gt; msgTreeMap = new TreeMap&amp;lt;Long, MessageExt&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;顺序消费临时容器&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final TreeMap&amp;lt;Long, MessageExt&amp;gt; consumingMsgOrderlyTreeMap = new TreeMap&amp;lt;Long, MessageExt&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ReadWriteLock lockTreeMap;		// 读写锁
private final Lock lockConsume;					// 重入锁，【顺序消费使用】
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;顺序消费状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile boolean locked = false;		// 是否是锁定状态
private volatile boolean consuming = false;		// 是否是消费中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;核心成员方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;putMessage()：将 Broker 拉取下来的 msgs 存储到快照队列内，返回为 true 表示提交顺序消费任务，false 表示不提交&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean putMessage(final List&amp;lt;MessageExt&amp;gt; msgs)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.lockTreeMap.writeLock().lockInterruptibly()&lt;/code&gt;：获取写锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (MessageExt msg : msgs)&lt;/code&gt;：遍历 msgs 全部加入 msgTreeMap，key 是消息的 queueOffset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!msgTreeMap.isEmpty() &amp;amp;&amp;amp; !this.consuming)&lt;/code&gt;：&lt;strong&gt;消息容器中存在未处理的消息，并且不是消费中的状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dispatchToConsume = true&lt;/code&gt;：代表需要提交顺序消费任务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.consuming = true&lt;/code&gt;：设置为顺序消费执行中的状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.lockTreeMap.writeLock().unlock()&lt;/code&gt;：释放写锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;removeMessage()：移除已经消费的消息，参数是已经消费的消息集合，并发消费使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public long removeMessage(final List&amp;lt;MessageExt&amp;gt; msgs)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;long result = -1&lt;/code&gt;：结果初始化为 -1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().lockInterruptibly()&lt;/code&gt;：获取写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lastConsumeTimestamp = now&lt;/code&gt;：更新上一次消费消息的时间为现在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!msgTreeMap.isEmpty())&lt;/code&gt;：判断消息容器是否是空，&lt;strong&gt;是空直接返回 -1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result = this.queueOffsetMax + 1&lt;/code&gt;：设置结果，&lt;strong&gt;删除完后消息容器为空时返回&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (MessageExt msg : msgs)&lt;/code&gt;：将已经消费的消息全部从 msgTreeMap 移除&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!msgTreeMap.isEmpty())&lt;/code&gt;：移除后容器内还有待消费的消息，&lt;strong&gt;获取第一条消息 offset 返回&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().unlock()&lt;/code&gt;：释放写锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;takeMessages()：获取一批消息，顺序消费使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public List&amp;lt;MessageExt&amp;gt; takeMessages(final int batchSize)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().lockInterruptibly()&lt;/code&gt;：获取写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lastConsumeTimestamp = now&lt;/code&gt;：更新上一次消费消息的时间为现在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (int i = 0; i &amp;lt; batchSize; i++)&lt;/code&gt;：从头节点开始获取消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result.add(entry.getValue())&lt;/code&gt;：将消息放入结果集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;consumingMsgOrderlyTreeMap.put()&lt;/code&gt;：将消息加入顺序消费容器中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (result.isEmpty())&lt;/code&gt;：条件成立说明顺序消费容器本地快照内的消息全部处理完了，&lt;strong&gt;当前顺序消费任务需要停止&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;consuming = false&lt;/code&gt;：消费状态置为 false&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().unlock()&lt;/code&gt;：释放写锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;commit()：处理完一批消息后调用，顺序消费使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public long commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().lockInterruptibly()&lt;/code&gt;：获取写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Long offset = this.consumingMsgOrderlyTreeMap.lastKey()&lt;/code&gt;：获取顺序消费临时容器最后一条数据的 key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msgCount, msgSize&lt;/code&gt;：更新顺序消费相关的字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.consumingMsgOrderlyTreeMap.clear()&lt;/code&gt;：清空顺序消费容器的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return offset + 1&lt;/code&gt;：&lt;strong&gt;消费者下一条消费的位点&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.lockTreeMap.writeLock().unlock()&lt;/code&gt;：释放写锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cleanExpiredMsg()：清除过期消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) &lt;/code&gt;：顺序消费不执行过期清理逻辑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int loop = msgTreeMap.size() &amp;lt; 16 ? msgTreeMap.size() : 16&lt;/code&gt;：最多循环 16 次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!msgTreeMap.isEmpty() &amp;amp;&amp;amp;)&lt;/code&gt;：如果容器中第一条消息的消费开始时间与当前系统时间差值 &amp;gt; 15min，则取出该消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;else&lt;/code&gt;：直接跳出循环，因为&lt;strong&gt;快照队列内的消息是有顺序的&lt;/strong&gt;，第一条消息不过期，其他消息都不过期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pushConsumer.sendMessageBack(msg, 3)&lt;/code&gt;：&lt;strong&gt;消息回退&lt;/strong&gt;到服务器，设置该消息的延迟级别为 3&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!msgTreeMap.isEmpty() &amp;amp;&amp;amp; msg.getQueueOffset() == msgTreeMap.firstKey())&lt;/code&gt;：条件成立说明消息回退期间，该目标消息并没有被消费任务成功消费&lt;/li&gt;
&lt;li&gt;&lt;code&gt;removeMessage(Collections.singletonList(msg))&lt;/code&gt;：从 treeMap 将该回退成功的 msg 删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;并发消费&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ConsumeMessageConcurrentlyService 负责并发消费服务&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;消息监听器：封装处理消息的逻辑，该监听器由开发者实现，并注册到 defaultMQPushConsumer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MessageListenerConcurrently messageListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final BlockingQueue&amp;lt;Runnable&amp;gt; consumeRequestQueue;	// 消费任务队列
private final String consumerGroup;							// 消费者组
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ThreadPoolExecutor consumeExecutor;				// 消费任务线程池，默认 20
private final ScheduledExecutorService scheduledExecutorService;// 调度线程池，延迟提交消费任务
private final ScheduledExecutorService cleanExpireMsgExecutors;	// 清理过期消息任务线程池，15min 一次
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;ConsumeMessageConcurrentlyService 并发消费核心方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动消费服务，DefaultMQPushConsumerImpl 启动时会调用该方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start() {
    // 提交“清理过期消息任务”任务，延迟15min之后执行，之后每15min执行一次
    this.cleanExpireMsgExecutors.scheduleAtFixedRate(() -&amp;gt;  cleanExpireMsg()}, 
                                                     15, 15, TimeUnit.MINUTES);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cleanExpireMsg()：清理过期消息任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void cleanExpireMsg()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Iterator&amp;lt;Map.Entry&amp;lt;MessageQueue, ProcessQueue&amp;gt;&amp;gt; it &lt;/code&gt;：获取分配给当前消费者的队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;while (it.hasNext())&lt;/code&gt;：遍历所有的队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pq.cleanExpiredMsg(this.defaultMQPushConsumer)&lt;/code&gt;：调用队列快照 ProcessQueue 清理过期消息的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;submitConsumeRequest()：提交消费请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：从服务器 pull 下来的这批消息
// 参数二：消息归属 mq 在消费者端的 processQueue，提交消费任务之前，msgs已经加入到该pq内了
// 参数三：消息归属队列
// 参数四：并发消息此参数无效
public void submitConsumeRequest(List&amp;lt;MessageExt&amp;gt; msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final int consumeBatchSize&lt;/code&gt;：&lt;strong&gt;一个消费任务可消费的消息数量&lt;/strong&gt;，默认为 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (msgs.size() &amp;lt;= consumeBatchSize)&lt;/code&gt;：判断一个消费任务是否可以提交&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ConsumeRequest consumeRequest&lt;/code&gt;：封装为消费请求&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.consumeExecutor.submit(consumeRequest)&lt;/code&gt;：提交消费任务，异步执行消息的处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：说明消息较多，需要多个消费任务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for (int total = 0; total &amp;lt; msgs.size(); )&lt;/code&gt;：将消息拆分成多个消费任务&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processConsumeResult()：处理消费结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数一：消费结果状态；  参数二：消费上下文；  参数三：当前消费任务
public void processConsumeResult(status, context, consumeRequest)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (status)&lt;/code&gt;：根据消费结果状态进行处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case CONSUME_SUCCESS&lt;/code&gt;：消费成功&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (ackIndex &amp;gt;= consumeRequest.getMsgs().size())&lt;/code&gt;：消费成功的话，ackIndex 设置成 &lt;code&gt;消费消息数 - 1&lt;/code&gt; 的值，比如有 5 条消息，这里就设置为 4&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ok, failed&lt;/code&gt;：ok 设置为消息数量，failed 设置为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case RECONSUME_LATER&lt;/code&gt;：消费失败&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ackIndex = -1&lt;/code&gt;：设置为 -1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (this.defaultMQPushConsumer.getMessageModel())&lt;/code&gt;：判断消费模式，默认是&lt;strong&gt;集群模式&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (int i = ackIndex + 1; i &amp;lt; msgs.size(); i++)&lt;/code&gt;：当消费失败时 ackIndex 为 -1，i 的起始值为 0，该消费任务内的&lt;strong&gt;全部消息&lt;/strong&gt;都会尝试回退给服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MessageExt msg&lt;/code&gt;：提取一条消息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean result = this.sendMessageBack(msg, context)&lt;/code&gt;：&lt;strong&gt;发送消息回退，同步发送&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!result)&lt;/code&gt;：回退失败的消息，将&lt;strong&gt;消息的重试属性加 1&lt;/strong&gt;，并加入到回退失败的集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!msgBackFailed.isEmpty())&lt;/code&gt;：回退失败集合不为空&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;consumeRequest.getMsgs().removeAll(msgBackFailed)&lt;/code&gt;：将回退失败的消息从当前消费任务的 msgs 集合内移除&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.submitConsumeRequestLater()&lt;/code&gt;：&lt;strong&gt;回退失败的消息会再次提交消费任务&lt;/strong&gt;，延迟 5 秒钟后再次尝试消费&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;long offset = ...removeMessage(msgs)&lt;/code&gt;：从 pq 中删除已经消费成功的消息，返回 offset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this...getOffsetStore().updateOffset()&lt;/code&gt;：更新消费者本地该 mq 的&lt;strong&gt;消费进度&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;消费请求&lt;/h5&gt;
&lt;p&gt;ConsumeRequest 是 ConsumeMessageConcurrentlyService 的内部类，是一个 Runnable 任务对象&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分配到该消费任务的消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final List&amp;lt;MessageExt&amp;gt; msgs;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消息队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ProcessQueue processQueue;	// 消息处理队列
private final MessageQueue messageQueue;	// 消息队列
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：执行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (this.processQueue.isDropped())&lt;/code&gt;：条件成立说明该 queue 经过 rbl 算法分配到其他的 consumer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageListenerConcurrently listener&lt;/code&gt;：获取消息监听器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConsumeConcurrentlyContext context&lt;/code&gt;：创建消费上下文对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultMQPushConsumerImpl.resetRetryAndNamespace()&lt;/code&gt;：重置重试标记
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final String groupTopic&lt;/code&gt;：获取当前消费者组的重试主题 &lt;code&gt;%RETRY%GroupName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (MessageExt msg : msgs)&lt;/code&gt;：遍历所有的消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String retryTopic = msg.getProperty(...)&lt;/code&gt;：原主题，一般消息没有该属性，只有被重复消费的消息才有&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (retryTopic != null &amp;amp;&amp;amp; groupTopic.equals(...))&lt;/code&gt;：条件成立说明该消息是被重复消费的消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msg.setTopic(retryTopic)&lt;/code&gt;：将被&lt;strong&gt;重复消费的消息主题修改回原主题&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (ConsumeMessageConcurrentlyService...hasHook())&lt;/code&gt;：前置处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean hasException = false&lt;/code&gt;：消费过程中，是否向外抛出异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageAccessor.setConsumeStartTimeStamp()&lt;/code&gt;：给每条消息设置消费开始时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status = listener.consumeMessage(Collections.unmodifiableList(msgs), context)&lt;/code&gt;：&lt;strong&gt;消费消息&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (ConsumeMessageConcurrentlyService...hasHook())&lt;/code&gt;：后置处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;...processConsumeResult(status, context, this)&lt;/code&gt;：&lt;strong&gt;处理消费结果&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;顺序消费&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ConsumeMessageOrderlyService 负责顺序消费服务&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;消息监听器：封装处理消息的逻辑，该监听器由开发者实现，并注册到 defaultMQPushConsumer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MessageListenerOrderly messageListener;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final BlockingQueue&amp;lt;Runnable&amp;gt; consumeRequestQueue;	// 消费任务队列
private final String consumerGroup;							// 消费者组
private volatile boolean stopped = false;					// 消费停止状态
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ThreadPoolExecutor consumeExecutor;				// 消费任务线程池
private final ScheduledExecutorService scheduledExecutorService;// 调度线程池，延迟提交消费任务
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;队列锁：消费者本地 MQ 锁，&lt;strong&gt;确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final MessageQueueLock messageQueueLock = new MessageQueueLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class MessageQueueLock {
    private ConcurrentMap&amp;lt;MessageQueue, Object&amp;gt; mqLockTable = new ConcurrentHashMap&amp;lt;MessageQueue, Object&amp;gt;();
    // 获取本地队列锁对象
    public Object fetchLockObject(final MessageQueue mq) {
        Object objLock = this.mqLockTable.get(mq);
        if (null == objLock) {
            objLock = new Object();
            Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock);
            if (prevLock != null) {
                objLock = prevLock;
            }
        }
        return objLock;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已经获取了 Broker 端该 Queue 的独占锁，为什么还要获取本地队列锁对象？（这里我也没太懂，先记录下来，本地多线程？）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broker queue 占用锁的角度是 Client 占用，Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后，将消息存储到消费者本地的 ProcessQueue 中，快照对象的 consuming 属性置为 true，表示本地的队列正在消费处理中&lt;/li&gt;
&lt;li&gt;ProcessQueue  调用 takeMessages 方法时会获取下一批待处理的消息，获取不到会修改 &lt;code&gt;consuming = false&lt;/code&gt;，本消费任务马上停止。&lt;/li&gt;
&lt;li&gt;如果此时 Pull 再次拉取一批当前 ProcessQueue  的 msg，会再次向顺序消费服务提交消费任务，此时需要本地队列锁对象同步本地线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;start()：启动消费服务，DefaultMQPushConsumerImpl 启动时会调用该方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void start()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.scheduledExecutorService.scheduleAtFixedRate()&lt;/code&gt;：提交锁续约任务，延迟 1 秒执行，周期为 20 秒钟&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConsumeMessageOrderlyService.this.lockMQPeriodically()&lt;/code&gt;：&lt;strong&gt;锁续约任务&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll()&lt;/code&gt;：对消费者的所有队列进行续约&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;submitConsumeRequest()：&lt;strong&gt;提交消费任务请求&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数：true 表示创建消费任务并提交，false不创建消费任务，说明消费者本地已经有消费任务在执行了
public void submitConsumeRequest(...., final boolean dispathToConsume) {
    if (dispathToConsume) {
        // 当前进程内不存在 顺序消费任务，创建新的消费任务，【提交到消费任务线程池】
        ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
        this.consumeExecutor.submit(consumeRequest);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processConsumeResult()：消费结果处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数1：msgs 本轮循环消费的消息集合    					参数2：status  消费状态
// 参数3：context 消费上下文 							参数4：消费任务
// 返回值：boolean 决定是否继续循环处理pq内的消息
public boolean processConsumeResult(final List&amp;lt;MessageExt&amp;gt; msgs, final ConsumeOrderlyStatus status, final ConsumeOrderlyContext context, final ConsumeRequest consumeRequest)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (context.isAutoCommit()) &lt;/code&gt;：默认自动提交&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;switch (status)&lt;/code&gt;：根据消费状态进行不同的处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case SUCCESS&lt;/code&gt;：消费成功&lt;/p&gt;
&lt;p&gt;&lt;code&gt;commitOffset = ...commit()&lt;/code&gt;：调用 pq 提交方法，会将本次循环处理的消息从顺序消费 map 删除，并且返回消息进度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;case SUSPEND_CURRENT_QUEUE_A_MOMENT&lt;/code&gt;：挂起当前队列&lt;/p&gt;
&lt;p&gt;&lt;code&gt;consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)&lt;/code&gt;：&lt;strong&gt;回滚消息&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (MessageExt msg : msgs)&lt;/code&gt;：遍历所有的消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())&lt;/code&gt;：从顺序消费临时容器中移除&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.msgTreeMap.put(msg.getQueueOffset(), msg)&lt;/code&gt;：添加到消息容器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.submitConsumeRequestLater()&lt;/code&gt;：再次提交消费任务，1 秒后执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;continueConsume = false&lt;/code&gt;：设置为 false，&lt;strong&gt;外层会退出本次的消费任务&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(...)&lt;/code&gt;：更新本地消费进度&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;消费请求&lt;/h5&gt;
&lt;p&gt;ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类，是一个 Runnable 任务对象&lt;/p&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run()：执行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final Object objLock&lt;/code&gt;：获取本地锁对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;synchronized (objLock)&lt;/code&gt;：本地队列锁，确保每个 MQ 的消费任务只有一个在执行，&lt;strong&gt;确保顺序消费&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(.. || (this.processQueue.isLocked() &amp;amp;&amp;amp; !this.processQueue.isLockExpired())))&lt;/code&gt;：当前队列持有分布式锁，并且锁未过期，持锁时间超过 30 秒算过期&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final long beginTime&lt;/code&gt;：消费开始时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (boolean continueConsume = true; continueConsume; )&lt;/code&gt;：根据是否继续消费的标记判断是否继续&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final int consumeBatchSize&lt;/code&gt;：获取每次循环处理的消息数量，一般是 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;MessageExt&amp;gt; msgs = this...takeMessages(consumeBatchSize)&lt;/code&gt;：到&lt;strong&gt;处理队列获取一批消息&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!msgs.isEmpty())&lt;/code&gt;：获取到了待消费的消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final ConsumeOrderlyContext context&lt;/code&gt;：创建消费上下文对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.processQueue.getLockConsume().lock()&lt;/code&gt;：&lt;strong&gt;获取 lockConsume 锁&lt;/strong&gt;，与 RBL 线程同步使用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status = messageListener.consumeMessage(...)&lt;/code&gt;：监听器处理消息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.processQueue.getLockConsume().unlock()&lt;/code&gt;：&lt;strong&gt;释放 lockConsume 锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (null == status)&lt;/code&gt;：处理消息状态返回 null，设置状态为挂起当前队列&lt;/p&gt;
&lt;p&gt;&lt;code&gt;continueConsume = ...processConsumeResult()&lt;/code&gt;：消费结果处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：获取到的消息是空&lt;/p&gt;
&lt;p&gt;&lt;code&gt;continueConsume = false&lt;/code&gt;：结束任务循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：当前队列未持有分布式锁，或者锁过期&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume()&lt;/code&gt;：重新提交任务，根据是否获取到队列锁，选择延迟 10 毫秒或者 300 毫秒&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;生产消费&lt;/h3&gt;
&lt;p&gt;生产流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先获取当前消息主题的发布信息，获取不到去 Namesrv 获取（默认有 TBW102），并将获取的到的路由数据转化为发布数据，&lt;strong&gt;创建 MQ 队列在多个 Broker 组&lt;/strong&gt;（一组代表一主多从的 Broker 架构），客户端实例同样更新订阅数据，创建 MQ 队列，放入负载均衡服务 topicSubscribeInfoTable 中&lt;/li&gt;
&lt;li&gt;然后从发布数据中选择一个 MQ 队列发送消息&lt;/li&gt;
&lt;li&gt;Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理，存储到 CommitLog。将重试次数过多的消息加入&lt;strong&gt;死信队列&lt;/strong&gt;，将延迟消息的主题和队列修改为调度主题和调度队列 ID&lt;/li&gt;
&lt;li&gt;Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务，让延迟消息得到有效的处理，将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog，消费者就可以进行消费了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息消费队列 ConsumerQueue 存储消息在 CommitLog 的索引，消费者通过该队列来读取消息实体内容，一个 MQ 就对应一个 CQ&lt;/li&gt;
&lt;li&gt;首先通过负载均衡服务，将分配到当前消费者实例的 MQ 创建 PullRequest，并放入 PullMessageService 的本地阻塞队列内&lt;/li&gt;
&lt;li&gt;PullMessageService 循环从阻塞队列获取请求对象，发起拉消息请求，并创建 PullCallback 回调对象，将正常拉取的消息&lt;strong&gt;提交到消费任务线程池&lt;/strong&gt;，并设置请求的下一次拉取位点，重新放入阻塞队列，形成闭环&lt;/li&gt;
&lt;li&gt;消费任务服务对消费失败的消息进行回退，通过内部生产者实例发送回退消息，回退失败的消息会再次提交消费任务重新消费&lt;/li&gt;
&lt;li&gt;Broker 端对拉取消息的请求进行处理（processRequestCommand），查询成功将消息放入响应体，通过 Netty 写回客户端，当 &lt;code&gt;pullRequest.offset == queue.maxOffset&lt;/code&gt; 说明该队列已经没有需要获取的消息，将请求放入长轮询集合等待有新消息&lt;/li&gt;
&lt;li&gt;PullRequestHoldService 负责长轮询，每 5 秒遍历一次长轮询集合，将满足条件的 PullRequest 再次提交到线程池内处理&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Zookeeper&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;h3&gt;框架特征&lt;/h3&gt;
&lt;p&gt;Zookeeper 是 Apache Hadoop 项目子项目，为分布式框架提供协调服务，是一个树形目录服务&lt;/p&gt;
&lt;p&gt;Zookeeper 是基于观察者模式设计的分布式服务管理框架，负责存储和管理共享数据，接受观察者的注册监控，一旦这些数据的状态发生变化，Zookeeper 会通知观察者&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zookeeper 是一个领导者（Leader），多个跟随者（Follower）组成的集群&lt;/li&gt;
&lt;li&gt;集群中只要有半数以上节点存活就能正常服务，所以 Zookeeper 适合部署奇数台服务器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局数据一致&lt;/strong&gt;，每个 Server 保存一份相同的数据副本，Client 无论连接到哪个 Server，数据都是一致&lt;/li&gt;
&lt;li&gt;更新的请求顺序执行，来自同一个 Client 的请求按其发送顺序依次执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据更新原子性&lt;/strong&gt;，一次数据更新要么成功，要么失败&lt;/li&gt;
&lt;li&gt;实时性，在一定的时间范围内，Client 能读到最新数据&lt;/li&gt;
&lt;li&gt;心跳检测，会定时向各个服务提供者发送一个请求（实际上建立的是一个 Socket 长连接）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-%E6%A1%86%E6%9E%B6%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1to4y1C7gw&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;应用场景&lt;/h3&gt;
&lt;p&gt;Zookeeper 提供的主要功能包括：统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡、分布式锁等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在分布式环境中，经常对应用/服务进行统一命名，便于识别，例如域名相对于 IP 地址更容易被接收&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/service/www.baidu.com 		# 节点路径
192.168.1.1  192.168.1.2	# 节点值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在节点中记录每台服务器的访问数，让访问数最少的服务器去处理最新的客户端请求，可以实现负载均衡&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.1.1  10	# 次数
192.168.1.1  15
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件同步可以通过 Zookeeper 实现，将配置信息写入某个 ZNode，其他客户端监视该节点，当节点数据被修改，通知各个客户端服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集群环境中，需要实时掌握每个集群节点的状态，可以将这些信息放入 ZNode，通过监控通知的机制实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现客户端实时观察服务器上下线的变化，通过心跳检测实现&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;h3&gt;安装搭建&lt;/h3&gt;
&lt;p&gt;安装步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;安装 JDK&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝 apache-zookeeper-3.5.7-bin.tar.gz 安装包到 Linux 系统下，并解压到指定目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;conf 目录下的配置文件重命名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv zoo_sample.cfg zoo.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim zoo.cfg
# 修改内容
dataDir=/home/seazean/SoftWare/zookeeper-3.5.7/zkData 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在对应目录创建 zkData 文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir zkData
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zookeeper 中的配置文件 zoo.cfg 中参数含义解读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tickTime = 2000：通信心跳时间，&lt;strong&gt;Zookeeper 服务器与客户端心跳&lt;/strong&gt;时间，单位毫秒&lt;/li&gt;
&lt;li&gt;initLimit = 10：Leader 与 Follower 初始通信时限，初始连接时能容忍的最多心跳次数&lt;/li&gt;
&lt;li&gt;syncLimit = 5：Leader 与 Follower 同步通信时限，LF 通信时间超过 &lt;code&gt;syncLimit * tickTime&lt;/code&gt;，Leader 认为 Follwer 下线&lt;/li&gt;
&lt;li&gt;dataDir：保存 Zookeeper 中的数据目录，默认是 tmp目录，容易被 Linux 系统定期删除，所以建议修改&lt;/li&gt;
&lt;li&gt;clientPort = 2181：客户端连接端口，通常不做修改&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;操作命令&lt;/h3&gt;
&lt;h4&gt;服务端&lt;/h4&gt;
&lt;p&gt;Linux 命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动 ZooKeeper 服务：&lt;code&gt;./zkServer.sh start&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看 ZooKeeper 服务：&lt;code&gt;./zkServer.sh status&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止 ZooKeeper 服务：&lt;code&gt;./zkServer.sh stop&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启 ZooKeeper 服务：&lt;code&gt;./zkServer.sh restart &lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看进程是否启动：&lt;code&gt;jps&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;客户端&lt;/h4&gt;
&lt;p&gt;Linux 命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;连接 ZooKeeper 服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./zkCli.sh					# 直接启动
./zkCli.sh –server ip:port	# 指定 host 启动
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客户端命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基础操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;quit						# 停止连接
help						# 查看命令帮助
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建命令：&lt;strong&gt;&lt;code&gt;/&lt;/code&gt; 代表根目录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create /path value			# 创建节点，value 可选
create -e /path value		# 创建临时节点
create -s /path value		# 创建顺序节点
create -es /path value  	# 创建临时顺序节点，比如node10000012 删除12后也会继续从13开始，只会增加
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls /path					# 显示指定目录下子节点
ls –s /path					# 查询节点详细信息
ls –w /path					# 监听子节点数量的变化
stat /path					# 查看节点状态
get –s /path				# 查询节点详细信息
get –w /path				# 监听节点数据的变化
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 属性，分为当前节点的属性和子节点属性
czxid: 节点被创建的事务ID, 是ZooKeeper中所有修改总的次序，每次修改都有唯一的 zxid，谁小谁先发生
ctime: 被创建的时间戳
mzxid: 最后一次被更新的事务ID 
mtime: 最后修改的时间戳
pzxid: 子节点列表最后一次被更新的事务ID
cversion: 子节点的变化号，修改次数
dataversion: 节点的数据变化号，数据的变化次数
aclversion: 节点的访问控制列表变化号
ephemeralOwner: 用于临时节点，代表节点拥有者的 session id，如果为持久节点则为0 
dataLength: 节点存储的数据的长度 
numChildren: 当前节点的子节点数量
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete /path				# 删除节点
deleteall /path				# 递归删除节点
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;数据结构&lt;/h3&gt;
&lt;p&gt;ZooKeeper 是一个树形目录服务，类似 Unix 的文件系统，每一个节点都被称为 ZNode，每个 ZNode 默认存储 1MB 的数据，节点上会保存数据和节点信息，每个 ZNode 都可以通过其路径唯一标识&lt;/p&gt;
&lt;p&gt;节点可以分为四大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PERSISTENT：持久化节点&lt;/li&gt;
&lt;li&gt;EPHEMERAL：临时节点，客户端和服务器端&lt;strong&gt;断开连接&lt;/strong&gt;后，创建的节点删除&lt;/li&gt;
&lt;li&gt;PERSISTENT_SEQUENTIAL：持久化顺序节点，创建 znode 时设置顺序标识，节点名称后会附加一个值，&lt;strong&gt;顺序号是一个单调递增的计数器&lt;/strong&gt;，由父节点维护&lt;/li&gt;
&lt;li&gt;EPHEMERAL_SEQUENTIAL：临时顺序节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：在分布式系统中，顺序号可以被用于为所有的事件进行全局排序，这样客户端可以通过顺序号推断事件的顺序&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-%E8%8A%82%E7%82%B9%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;p&gt;添加 Maven 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.zookeeper&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;zookeeper&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.5.7&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    // 参数一：连接地址
    // 参数二：会话超时时间
    // 参数三：监听器
    ZooKeeper zkClient = new ZooKeeper(&quot;192.168.3.128:2181&quot;, 20000, new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            System.out.println(&quot;监听处理函数&quot;);
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;集群介绍&lt;/h2&gt;
&lt;h3&gt;相关概念&lt;/h3&gt;
&lt;p&gt;Zookeepe 集群三个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Leader 领导者：处理客户端&lt;strong&gt;事务请求&lt;/strong&gt;，负责集群内部各服务器的调度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Follower 跟随者：处理客户端非事务请求，转发事务请求给 Leader 服务器，参与 Leader 选举投票&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Observer 观察者：观察集群的最新状态的变化，并将这些状态进行同步；处理非事务性请求，事务性请求会转发给 Leader 服务器进行处理；不会参与任何形式的投票。只提供非事务性的服务，通常用于在不影响集群事务处理能力的前提下，提升集群的非事务处理能力（提高集群读的能力，但是也降低了集群选主的复杂程度）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SID：服务器 ID，用来唯一标识一台集群中的机器，和 myid 一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ZXID：事务 ID，用来标识一次服务器状态的变更，在某一时刻集群中每台机器的 ZXID 值不一定完全一致，这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Epoch：每个 Leader 任期的代号，同一轮选举投票过程中的该值是相同的，投完一次票就增加&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选举机制：半数机制，超过半数的投票就通过&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一次启动选举规则：投票过半数时，服务器 ID 大的胜出&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二次启动选举规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EPOCH 大的直接胜出&lt;/li&gt;
&lt;li&gt;EPOCH 相同，事务 ID 大的胜出（事务 ID 越大，数据越新）&lt;/li&gt;
&lt;li&gt;事务 ID 相同，服务器 ID 大的胜出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;初次选举&lt;/h3&gt;
&lt;p&gt;选举过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器 1 启动，发起一次选举，服务器 1 投自己一票，票数不超过半数，选举无法完成，服务器 1 状态保持为 LOOKING&lt;/li&gt;
&lt;li&gt;服务器 2 启动，再发起一次选举，服务器 1 和 2 分别投自己一票并&lt;strong&gt;交换选票信息&lt;/strong&gt;，此时服务器 1 会发现服务器 2 的 SID 比自己投票推举的（服务器 1）大，更改选票为推举服务器 2。投票结果为服务器 1 票数 0 票，服务器 2 票数 2 票，票数不超过半数，选举无法完成，服务器 1、2 状态保持 LOOKING&lt;/li&gt;
&lt;li&gt;服务器 3 启动，发起一次选举，此时服务器 1 和 2 都会更改选票为服务器 3，投票结果为服务器 3 票数 3 票，此时服务器 3 的票数已经超过半数，服务器 3 当选 Leader，服务器 1、2 更改状态为 FOLLOWING，服务器 3 更改状态为 LEADING&lt;/li&gt;
&lt;li&gt;服务器 4 启动，发起一次选举，此时服务器 1、2、3 已经不是 LOOKING 状态，不会更改选票信息，交换选票信息结果后服务器 3 为 3 票，服务器 4 为 1 票，此时服务器 4 更改选票信息为服务器 3，并更改状态为 FOLLOWING&lt;/li&gt;
&lt;li&gt;服务器 5 启动，同 4 一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-%E5%88%9D%E6%AC%A1%E9%80%89%E4%B8%BE%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;再次选举&lt;/h3&gt;
&lt;p&gt;ZooKeeper 集群中的一台服务器出现以下情况之一时，就会开始进入 Leader 选举：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器初始化启动&lt;/li&gt;
&lt;li&gt;服务器运行期间无法和 Leader 保持连接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一台服务器进入 Leader 选举流程时，当前集群可能会处于以下两种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;集群中本来就已经存在一个 Leader，服务器试图去选举 Leader 时会被告知当前服务器的 Leader 信息，对于该服务器来说，只需要和 Leader 服务器建立连接，并进行状态同步即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集群中确实不存在 Leader，假设服务器 3 和 5 出现故障，开始进行 Leader 选举，SID 为 1、2、4 的机器投票情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(EPOCH，ZXID，SID): (1, 8, 1), (1, 8, 2), (1, 7, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据选举规则，服务器 2 胜出&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;数据写入&lt;/h3&gt;
&lt;p&gt;写操作就是事务请求，写入请求直接发送给 Leader 节点：Leader 会先将数据写入自身，同时通知其他 Follower 写入，&lt;strong&gt;当集群中有半数以上节点写入完成&lt;/strong&gt;，Leader 节点就会响应客户端数据写入完成&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-写入请求Leader.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;写入请求直接发送给 Follower 节点：Follower 没有写入权限，会将写请求转发给 Leader，Leader 将数据写入自身，通知其他 Follower 写入，当集群中有半数以上节点写入完成，Leader 会通知 Follower 写入完成，&lt;strong&gt;由 Follower 响应客户端数据写入完成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-写入请求Follower.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;底层协议&lt;/h2&gt;
&lt;h3&gt;Paxos&lt;/h3&gt;
&lt;p&gt;Paxos 算法：基于消息传递且具有高度容错特性的一致性算法&lt;/p&gt;
&lt;p&gt;优点：快速正确的在一个分布式系统中对某个数据值达成一致，并且保证不论发生任何异常，都不会破坏整个系统的一致性&lt;/p&gt;
&lt;p&gt;缺陷：在网络复杂的情况下，可能很久无法收敛，甚至陷入活锁的情况&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ZAB&lt;/h3&gt;
&lt;h4&gt;算法介绍&lt;/h4&gt;
&lt;p&gt;ZAB 协议借鉴了 Paxos 算法，是为 Zookeeper 设计的支持崩溃恢复的原子广播协议，基于该协议 Zookeeper 设计为只有一台客户端（Leader）负责处理外部的写事务请求，然后 Leader 将数据同步到其他 Follower 节点&lt;/p&gt;
&lt;p&gt;Zab 协议包括两种基本的模式：消息广播、崩溃恢复&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;消息广播&lt;/h4&gt;
&lt;p&gt;ZAB 协议针对事务请求的处理过程类似于一个&lt;strong&gt;两阶段提交&lt;/strong&gt;过程：广播事务阶段、广播提交操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端发起写操作请求，Leader 服务器将请求转化为事务 Proposal 提案，同时为 Proposal 分配一个全局的 ID，即 ZXID&lt;/li&gt;
&lt;li&gt;Leader 服务器为每个 Follower 分配一个单独的队列，将广播的 Proposal &lt;strong&gt;依次放到队列&lt;/strong&gt;中去，根据 FIFO 策略进行消息发送&lt;/li&gt;
&lt;li&gt;Follower 接收到 Proposal 后，将其以事务日志的方式写入本地磁盘中，写入成功后向 Leader 反馈一个 ACK 响应消息&lt;/li&gt;
&lt;li&gt;Leader 接收到超过半数以上 Follower 的 ACK 响应消息后，即认为消息发送成功，可以发送 Commit 消息&lt;/li&gt;
&lt;li&gt;Leader 向所有 Follower 广播 commit 消息，同时自身也会完成事务提交，Follower 接收到 Commit 后，将上一条事务提交&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-消息广播.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;两阶段提交模型可能因为 Leader 宕机带来数据不一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader 发起一个事务 Proposal 后就宕机，Follower 都没有 Proposal&lt;/li&gt;
&lt;li&gt;Leader 收到半数 ACK 宕机，没来得及向 Follower 发送 Commit&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;崩溃恢复&lt;/h4&gt;
&lt;p&gt;Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与&lt;strong&gt;过半 Follower的联系&lt;/strong&gt;，那么就会进入崩溃恢复模式，崩溃恢复主要包括两部分：Leader 选举和数据恢复&lt;/p&gt;
&lt;p&gt;Zab 协议崩溃恢复要求满足以下两个要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经被 Leader 提交的提案 Proposal，必须最终被所有的 Follower 服务器正确提交&lt;/li&gt;
&lt;li&gt;丢弃已经被 Leader 提出的，但是没有被提交的 Proposal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zab 协议需要保证选举出来的 Leader 需要满足以下条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新选举的 Leader 不能包含未提交的 Proposal，即新 Leader 必须都是已经提交了 Proposal 的 Follower 节点&lt;/li&gt;
&lt;li&gt;新选举的 Leader 节点含有&lt;strong&gt;最大的 ZXID&lt;/strong&gt;，可以避免 Leader 服务器检查 Proposal 的提交和丢弃工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-Leader选举.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;数据恢复阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完成 Leader 选举后，在正式开始工作之前（接收事务请求提出新的 Proposal），Leader 服务器会首先确认事务日志中的所有 Proposal 是否已经被集群中过半的服务器 Commit&lt;/li&gt;
&lt;li&gt;Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务的 Proposal，并且能将所有已经提交的事务 Proposal 应用到内存数据中，所以只有当 Follower 将所有尚未同步的事务 Proposal 都&lt;strong&gt;从 Leader 服务器上同步&lt;/strong&gt;，并且应用到内存数据后，Leader 才会把该 Follower 加入到真正可用的 Follower 列表中&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;异常处理&lt;/h4&gt;
&lt;p&gt;Zab 的事务编号 zxid 设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zxid 是一个 64 位的数字，低 32 位是一个简单的单增计数器，针对客户端每一个事务请求，Leader 在产生新的 Proposal 事务时，都会对该计数器加 1，而高 32 位则代表了 Leader 周期的 epoch 编号&lt;/li&gt;
&lt;li&gt;epoch 为当前集群所处的代或者周期，每次 Leader 变更后都会在 epoch 的基础上加 1，Follower 只服从 epoch 最高的 Leader 命令，所以旧的 Leader 崩溃恢复之后，其他 Follower 就不会继续追随&lt;/li&gt;
&lt;li&gt;每次选举产生一个新的 Leader，就会从新 Leader 服务器上取出本地事务日志中最大编号 Proposal 的 zxid，从 zxid 中解析得到对应的 epoch 编号，然后再对其加 1 后作为新的 epoch 值，并将低 32 位数字归零，由 0 开始重新生成 zxid&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zab 协议通过 epoch 编号来区分 Leader 变化周期，能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况&lt;/p&gt;
&lt;p&gt;Zab 数据同步过程：&lt;strong&gt;数据同步阶段要以 Leader 服务器为准&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个包含了上个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时，这台机器加入集群中会以 Follower 角色连上 Leader&lt;/li&gt;
&lt;li&gt;Leader 会根据自己服务器上最后提交的 Proposal 和 Follower 服务器的 Proposal 进行比对，让 Follower 进行一个&lt;strong&gt;回退或者前进操作&lt;/strong&gt;，到一个已经被集群中过半机器 Commit 的最新 Proposal（源码解析部分详解）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;CAP&lt;/h3&gt;
&lt;p&gt;CAP 理论指的是在一个分布式系统中，Consistency（一致性）、Availability（可用性）、Partition Tolerance（分区容错性）不能同时成立，ZooKeeper 保证的是 CP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ZooKeeper 不能保证每次服务请求的可用性，在极端环境下可能会丢弃一些请求，消费者程序需要重新请求才能获得结果&lt;/li&gt;
&lt;li&gt;进行 Leader 选举时&lt;strong&gt;集群都是不可用&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CAP 三个基本需求，因为 P 是必须的，因此分布式系统选择就在 CP 或者 AP 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一致性：指数据在多个副本之间是否能够保持数据一致的特性，当一个系统在数据一致的状态下执行更新操作后，也能保证系统的数据仍然处于一致的状态&lt;/li&gt;
&lt;li&gt;可用性：指系统提供的服务必须一直处于可用的状态，即使集群中一部分节点故障，对于用户的每一个操作请求总是能够在有限的时间内返回结果&lt;/li&gt;
&lt;li&gt;分区容错性：分布式系统在遇到任何网络分区故障时，仍然能够保证对外提供服务，不会宕机，除非是整个网络环境都发生了故障&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;监听机制&lt;/h2&gt;
&lt;h3&gt;实现原理&lt;/h3&gt;
&lt;p&gt;ZooKeeper 中引入了 Watcher 机制来实现了发布/订阅功能，客户端注册监听目录节点，在特定事件触发时，ZooKeeper 会通知所有关注该事件的客户端，保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听应用程序&lt;/p&gt;
&lt;p&gt;监听命令：&lt;strong&gt;只能生效一次&lt;/strong&gt;，接收一次通知，再次监听需要重新注册&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls –w /path					# 监听【子节点数量】的变化
get –w /path				# 监听【节点数据】的变化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在主线程中创建 Zookeeper 客户端，这时就会创建&lt;strong&gt;两个线程&lt;/strong&gt;，一个负责网络连接通信（connet），一个负责监听（listener）&lt;/li&gt;
&lt;li&gt;通过 connect 线程将注册的监听事件发送给 Zookeeper&lt;/li&gt;
&lt;li&gt;在 Zookeeper 的注册监听器列表中将注册的&lt;strong&gt;监听事件添加到列表&lt;/strong&gt;中&lt;/li&gt;
&lt;li&gt;Zookeeper 监听到有数据或路径变化，将消息发送给 listener 线程&lt;/li&gt;
&lt;li&gt;listener 线程内部调用 process() 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Curator 框架引入了 Cache 来实现对 ZooKeeper 服务端事件的监听，三种 Watcher：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NodeCache：只是监听某一个特定的节点&lt;/li&gt;
&lt;li&gt;PathChildrenCache：监控一个 ZNode 的子节点&lt;/li&gt;
&lt;li&gt;TreeCache：可以监控整个树上的所有节点，类似于 PathChildrenCache 和 NodeCache 的组合&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;监听案例&lt;/h3&gt;
&lt;h4&gt;整体架构&lt;/h4&gt;
&lt;p&gt;客户端实时监听服务器动态上下线&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-监听服务器状态.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;客户端：先启动客户端进行监听&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DistributeClient {
    private String connectString = &quot;192.168.3.128:2181&quot;;
    private int sessionTimeout = 20000;
    private ZooKeeper zk;

    public static void main(String[] args) throws Exception {
        DistributeClient client = new DistributeClient();
        
        // 1 获取zk连接
        client.getConnect();

        // 2 监听/servers下面子节点的增加和删除
        client.getServerList();

        // 3 业务逻辑
        client.business();
    }

    private void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    private void getServerList() throws KeeperException, InterruptedException {
        ArrayList&amp;lt;String&amp;gt; servers = new ArrayList&amp;lt;&amp;gt;();
        // 获取所有子节点，true 代表触发监听操作
        List&amp;lt;String&amp;gt; children = zk.getChildren(&quot;/servers&quot;, true);

        for (String child : children) {
            // 获取子节点的数据
            byte[] data = zk.getData(&quot;/servers/&quot; + child, false, null);
            servers.add(new String(data));
        }
        System.out.println(servers);
    }

    private void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                getServerList();
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端：启动时需要 Program arguments&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DistributeServer {
    private String connectString = &quot;192.168.3.128:2181&quot;;
    private int sessionTimeout = 20000;
    private ZooKeeper zk;

    public static void main(String[] args) throws Exception {
        DistributeServer server = new DistributeServer();

        // 1 获取 zookeeper 连接
        server.getConnect();

        // 2  注册服务器到 zk 集群，注意参数
        server.register(args[0]);

        // 3 启动业务逻辑
        server.business();
    }

    private void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    private void register(String hostname) throws KeeperException, InterruptedException {
        // OPEN_ACL_UNSAFE: ACL 开放
        // EPHEMERAL_SEQUENTIAL: 临时顺序节点
        String create = zk.create(&quot;/servers/&quot; + hostname, hostname.getBytes(),
                                  ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostname + &quot; is online&quot;);
    }

    private void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;分布式锁&lt;/h2&gt;
&lt;h3&gt;实现原理&lt;/h3&gt;
&lt;p&gt;分布式锁可以实现在分布式系统中多个进程有序的访问该临界资源，多个进程之间不会相互干扰&lt;/p&gt;
&lt;p&gt;核心思想：当客户端要获取锁，则创建节点，使用完锁，则删除该节点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;客户端获取锁时，在 /locks 节点下创建&lt;strong&gt;临时顺序&lt;/strong&gt;节点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用临时节点是为了防止当服务器或客户端宕机以后节点无法删除（持久节点），导致锁无法释放&lt;/li&gt;
&lt;li&gt;使用顺序节点是为了系统自动编号排序，找最小的节点，防止客户端饥饿现象，保证公平&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取 /locks 目录的所有子节点，判断自己的&lt;strong&gt;子节点序号是否最小&lt;/strong&gt;，成立则客户端获取到锁，使用完锁后将该节点删除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反之客户端需要找到比自己小的节点，&lt;strong&gt;对其注册事件监听器，监听删除事件&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端的 Watcher 收到删除事件通知，就会重新判断当前节点是否是子节点中序号最小，如果是则获取到了锁， 如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Curator&lt;/h3&gt;
&lt;p&gt;Curator 实现分布式锁 API，在 Curator 中有五种锁方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;InterProcessSemaphoreMutex：分布式排它锁（非可重入锁）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InterProcessMutex：分布式可重入排它锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InterProcessReadWriteLock：分布式读写锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InterProcessMultiLock：将多个锁作为单个实体管理的容器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InterProcessSemaphoreV2：共享信号量&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class CuratorLock {
    
    public static CuratorFramework getCuratorFramework() {
        // 重试策略对象
        ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
        // 构建客户端
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(&quot;192.168.3.128:2181&quot;)
                .connectionTimeoutMs(2000)	// 连接超时时间
                .sessionTimeoutMs(20000)	// 会话超时时间 单位ms
                .retryPolicy(policy)		// 重试策略
                .build();

        // 启动客户端
        client.start();
        System.out.println(&quot;zookeeper 启动成功&quot;);
        return client;
    }
    
    public static void main(String[] args) {
        // 创建分布式锁1
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), &quot;/locks&quot;);

        // 创建分布式锁2
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), &quot;/locks&quot;);

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock1.acquire();
                System.out.println(&quot;线程1 获取到锁&quot;);

                Thread.sleep(5 * 1000);

                lock1.release();
                System.out.println(&quot;线程1 释放锁&quot;);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock2.acquire();
                System.out.println(&quot;线程2 获取到锁&quot;);

                Thread.sleep(5 * 1000);

                lock2.release();
                System.out.println(&quot;线程2 释放锁&quot;);

            }
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.curator&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;curator-framework&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.curator&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;curator-recipes&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.curator&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;curator-client&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;源码解析&lt;/h2&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;p&gt;服务端程序的入口 QuorumPeerMain&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();
    main.initializeAndRun(args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;initializeAndRun 的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解析启动参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提交周期任务，定时删除过期的快照&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化通信模型，默认是 NIO 通信&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// QuorumPeerMain#runFromConfig
public void runFromConfig(QuorumPeerConfig config) {
    // 通信信组件初始化，默认是 NIO 通信
    ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
    // 初始化NIO 服务端socket，绑定2181 端口，可以接收客户端请求
    cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
    // 启动 zk
    quorumPeer.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 zookeeper&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// QuorumPeer#start
public synchronized void start() {
    if (!getView().containsKey(myid)) {
        throw new RuntimeException(&quot;My id &quot; + myid + &quot; not in the peer list&quot;);
    }
    // 冷启动数据恢复，将快照中数据恢复到 DataTree
    loadDataBase();
    // 启动通信工厂实例对象
    startServerCnxnFactory();
    try {
        adminServer.start();
    } catch (AdminServerException e) {
        LOG.warn(&quot;Problem starting AdminServer&quot;, e);
        System.out.println(e);
    }
    // 准备选举环境
    startLeaderElection();
    // 执行选举
    super.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;选举机制&lt;/h3&gt;
&lt;h4&gt;环境准备&lt;/h4&gt;
&lt;p&gt;QuorumPeer#startLeaderElection 初始化选举环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized public void startLeaderElection() {
    try {
        // Looking 状态，需要选举
        if (getPeerState() == ServerState.LOOKING) {
            // 选票组件: myid (serverid), zxid, epoch
            // 开始选票时，serverid 是自己，【先投自己】
            currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
        }
    }
    if (electionType == 0) {
        try {
            udpSocket = new DatagramSocket(getQuorumAddress().getPort());
            // 响应投票结果线程
            responder = new ResponderThread();
            responder.start();
        } catch (SocketException e) {
            throw new RuntimeException(e);
        }
    }
    // 创建选举算法实例
    this.electionAlg = createElectionAlgorithm(electionType);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// zk总的发送和接收队列准备好
protected Election createElectionAlgorithm(int electionAlgorithm){
    // 负责选举过程中的所有网络通信，创建各种队列和集合
    QuorumCnxManager qcm = createCnxnManager();
    QuorumCnxManager.Listener listener = qcm.listener;
    if(listener != null){
        // 启动监听线程, 调用 client = ss.accept()阻塞，等待处理请求
        listener.start();
        // 准备好发送和接收队列准备
        FastLeaderElection fle = new FastLeaderElection(this, qcm);
        // 启动选举线程，【WorkerSender 和 WorkerReceiver】
        fle.start();
        le = fle;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;选举源码&lt;/h4&gt;
&lt;p&gt;当 Zookeeper 启动后，首先都是 Looking 状态，通过选举让其中一台服务器成为 Leader&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;super.start()&lt;/code&gt; 相当于执行 &lt;code&gt;QuorumPeer#run()&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    case LOOKING:
        // 进行选举，选举结束返回最终成为 Leader 胜选的那张选票
        setCurrentVote(makeLEStrategy().lookForLeader());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FastLeaderElection 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;lookForLeader：选举&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Vote lookForLeader() {
    // 正常启动中其他服务器都会向我发送一个投票，保存每个服务器的最新合法有效的投票
    HashMap&amp;lt;Long, Vote&amp;gt; recvset = new HashMap&amp;lt;Long, Vote&amp;gt;();
	// 存储合法选举之外的投票结果
    HashMap&amp;lt;Long, Vote&amp;gt; outofelection = new HashMap&amp;lt;Long, Vote&amp;gt;();
	// 一次选举的最大等待时间，默认值是0.2s
    int notTimeout = finalizeWait;
	// 每发起一轮选举，logicalclock++,在没有合法的epoch 数据之前，都使用逻辑时钟代替
    synchronized(this){
        // 更新逻辑时钟，每进行一次选举，都需要更新逻辑时钟
        logicalclock.incrementAndGet();
        // 更新选票(serverid， zxid, epoch）
        updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
    }
    // 广播选票，把自己的选票发给其他服务器
    sendNotifications();
    // 一轮一轮的选举直到选举成功
    while ((self.getPeerState() == ServerState.LOOKING) &amp;amp;&amp;amp; (!stop)){ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendNotifications：广播选票&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void sendNotifications() {
    // 遍历投票参与者，给每台服务器发送选票
    for (long sid : self.getCurrentAndNextConfigVoters()) {
		// 创建发送选票
        ToSend notmsg = new ToSend(...);
        // 把发送选票放入发送队列
        sendqueue.offer(notmsg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FastLeaderElection 中有 WorkerSender 线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS)&lt;/code&gt;：&lt;strong&gt;阻塞获取要发送的选票&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;process(m)&lt;/code&gt;：处理要发送的选票&lt;/p&gt;
&lt;p&gt;&lt;code&gt;manager.toSend(m.sid, requestBuffer)&lt;/code&gt;：发送选票&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.mySid == sid)&lt;/code&gt;：如果&lt;strong&gt;消息的接收者 sid 是自己&lt;/strong&gt;，直接进入自己的 RecvQueue（自己投自己）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：如果接收者是其他服务器，创建对应的发送队列或者复用已经存在的发送队列，把消息放入该队列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;connectOne(sid)&lt;/code&gt;：建立连接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sock.connect(electionAddr, cnxTO)&lt;/code&gt;：建立与 sid 服务器的连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;initiateConnection(sock, sid)&lt;/code&gt;：初始化连接&lt;/p&gt;
&lt;p&gt;&lt;code&gt;startConnection(sock, sid)&lt;/code&gt;：创建并启动发送器线程和接收器线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dout = new DataOutputStream(buf)&lt;/code&gt;：&lt;strong&gt;获取 Socket 输出流&lt;/strong&gt;，向服务器发送数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;din = new DataInputStream(new BIS(sock.getInputStream())))&lt;/code&gt;：通过输入流读取对方发送过来的选票&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (sid &amp;gt; self.getId())&lt;/code&gt;：接收者 sid 比我的大，没有资格给对方发送连接请求的，直接关闭自己的客户端&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SendWorker sw&lt;/code&gt;：初始化发送器，并启动发送器线程，线程 run 方法
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;while (running &amp;amp;&amp;amp; !shutdown &amp;amp;&amp;amp; sock != null)&lt;/code&gt;：连接没有断开就一直运行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ByteBuffer b = pollSendQueue()&lt;/code&gt;：从发送队列 SendQueue 中获取发送消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lastMessageSent.put(sid, b)&lt;/code&gt;：更新对于 sid 这台服务器的最近一条消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send(b)&lt;/code&gt;：&lt;strong&gt;执行发送&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RecvWorker rw&lt;/code&gt;：初始化接收器，并启动接收器线程
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;din.readFully(msgArray, 0, length)&lt;/code&gt;：输入流接收消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addToRecvQueue(new Message(messagg, sid))&lt;/code&gt;：将消息放入接收消息 recvQueue 队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FastLeaderElection 中有 WorkerReceiver 线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;response = manager.pollRecvQueue()&lt;/code&gt;：从 RecvQueue 中&lt;strong&gt;阻塞获取出选举投票消息&lt;/strong&gt;（其他服务器发送过来的）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-选举源码.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;状态同步&lt;/h4&gt;
&lt;p&gt;选举结束后，每个节点都需要根据角色更新自己的状态，Leader 更新状态为 Leader，其他节点更新状态为 Follower，整体流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Follower 需要让 Leader 知道自己的状态 (sid, epoch, zxid)&lt;/li&gt;
&lt;li&gt;Leader 接收到信息，&lt;strong&gt;根据信息构建新的 epoch&lt;/strong&gt;，要返回对应的信息给 Follower，Follower 更新自己的 epoch&lt;/li&gt;
&lt;li&gt;Leader 需要根据 Follower 的状态，确定何种方式的数据同步 DIFF、TRUNC、SNAP，就是要&lt;strong&gt;以 Leader 服务器数据为准&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;DIFF：Leader 提交的 zxid 比 Follower 的 zxid 大，发送 Proposal 给 Follower 提交执行&lt;/li&gt;
&lt;li&gt;TRUNC：Follower 的 zxid 比leader 的 zxid 大，Follower 要进行回滚&lt;/li&gt;
&lt;li&gt;SNAP：Follower 没有任何数据，直接全量同步&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行数据同步，当 Leader 接收到超过半数 Follower 的 Ack 之后，进入正常工作状态，集群启动完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-同步源码.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;核心函数解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader 更新状态入口：&lt;code&gt;Leader.lead()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zk.loadData()&lt;/code&gt;：恢复数据到内存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnxAcceptor = new LearnerCnxAcceptor()&lt;/code&gt;：启动通信组件
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s = ss.accept()&lt;/code&gt;：等待其他 Follower 节点向 Leader 节点发送同步状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LearnerHandler fh &lt;/code&gt;：接收到 Follower 的请求，就创建 LearnerHandler 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fh.start()&lt;/code&gt;：启动线程，通过 switch-case 语法判断接收的命令，执行相应的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Follower 更新状态入口：&lt;code&gt;Follower.followerLeader()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;QuorumServer leaderServer = findLeader()&lt;/code&gt;：查找 Leader&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connectToLeader(addr, hostname) &lt;/code&gt;：与 Leader 建立连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO)&lt;/code&gt;：向 Leader 注册&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;主从工作&lt;/h4&gt;
&lt;p&gt;Leader：主服务的工作流程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-Leader%E5%90%AF%E5%8A%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Follower：从服务的工作流程，核心函数为 &lt;code&gt;Follower#followLeader()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;readPacket(qp)&lt;/code&gt;：读取信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processPacket(qp)&lt;/code&gt;：处理信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void processPacket(QuorumPacket qp) throws Exception{
    switch (qp.getType()) {
        case Leader.PING:
            break;
        case Leader.PROPOSAL:
            break;
        case Leader.COMMIT:
            break;
        case Leader.COMMITANDACTIVATE:
            break;
        case Leader.UPTODATE:
            break;
        case Leader.REVALIDATE:
            break;
        case Leader.SYNC:
            break;
        default:
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%88%9D%E5%A7%8B%E5%8C%96.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java笔记合集</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/java/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/java/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SE&lt;/h1&gt;
&lt;h2&gt;基础&lt;/h2&gt;
&lt;h3&gt;数据&lt;/h3&gt;
&lt;h4&gt;变量类型&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;成员变量&lt;/th&gt;
&lt;th&gt;局部变量&lt;/th&gt;
&lt;th&gt;静态变量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;定义位置&lt;/td&gt;
&lt;td&gt;在类中，方法外&lt;/td&gt;
&lt;td&gt;方法中或者方法的形参&lt;/td&gt;
&lt;td&gt;在类中，方法外&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;初始化值&lt;/td&gt;
&lt;td&gt;有默认初始化值&lt;/td&gt;
&lt;td&gt;无，赋值后才能使用&lt;/td&gt;
&lt;td&gt;有默认初始化值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调用方法&lt;/td&gt;
&lt;td&gt;对象调用&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;对象调用，类名调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储位置&lt;/td&gt;
&lt;td&gt;堆中&lt;/td&gt;
&lt;td&gt;栈中&lt;/td&gt;
&lt;td&gt;方法区（JDK8 以后移到堆中）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生命周期&lt;/td&gt;
&lt;td&gt;与对象共存亡&lt;/td&gt;
&lt;td&gt;与方法共存亡&lt;/td&gt;
&lt;td&gt;与类共存亡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;别名&lt;/td&gt;
&lt;td&gt;实例变量&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;类变量，静态成员变量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;静态变量只有一个，成员变量是类中的变量，局部变量是方法中的变量&lt;/p&gt;
&lt;p&gt;初学时笔记内容参考视频：https://www.bilibili.com/video/BV1TE41177mP，随着学习的深入又增加很多知识&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;数据类型&lt;/h4&gt;
&lt;h5&gt;基本类型&lt;/h5&gt;
&lt;p&gt;Java 语言提供了八种基本类型。六种数字类型（四个整数型，两个浮点型），一种字符类型，还有一种布尔型&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;byte：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;byte 数据类型是 8 位、有符号的，以二进制补码表示的整数，&lt;strong&gt;8 位一个字节&lt;/strong&gt;，首位是符号位&lt;/li&gt;
&lt;li&gt;最小值是 -128（-2^7）、最大值是 127（2^7-1）&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;byte 类型用在大型数组中节约空间，主要代替整数，byte 变量占用的空间只有 int 类型的四分之一&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;byte a = 100，byte b = -50&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;short：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;short 数据类型是 16 位、有符号的以二进制补码表示的整数&lt;/li&gt;
&lt;li&gt;最小值是 -32768（-2^15）、最大值是 32767（2^15 - 1）&lt;/li&gt;
&lt;li&gt;short 数据类型也可以像 byte 那样节省空间，一个 short 变量是 int 型变量所占空间的二分之一&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;short s = 1000，short r = -20000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;int：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数&lt;/li&gt;
&lt;li&gt;最小值是 -2,147,483,648（-2^31）、最大值是 2,147,483,647（2^31 - 1）&lt;/li&gt;
&lt;li&gt;一般地整型变量默认为 int 类型&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;int a = 100000, int b = -200000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;long：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;long 数据类型是 64 位 8 字节、有符号的以二进制补码表示的整数&lt;/li&gt;
&lt;li&gt;最小值是 -9,223,372,036,854,775,808（-2^63）、最大值是 9,223,372,036,854,775,807（2^63 -1）&lt;/li&gt;
&lt;li&gt;这种类型主要使用在需要比较大整数的系统上&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt; 0L&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;例子： &lt;code&gt;long a = 100000L，Long b = -200000L&lt;/code&gt;，L 理论上不分大小写，但是若写成 I 容易与数字 1 混淆，不容易分辩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;float：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;float 数据类型是单精度、32 位、符合 IEEE 754 标准的浮点数&lt;/li&gt;
&lt;li&gt;float 在储存大型浮点数组的时候可节省内存空间&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;0.0f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;浮点数不能用来表示精确的值，如货币&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;float f1 = 234.5F&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;double：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数&lt;/li&gt;
&lt;li&gt;浮点数的默认类型为 double 类型&lt;/li&gt;
&lt;li&gt;double 类型同样不能表示精确的值，如货币&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;0.0d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;double d1 = 123.4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;boolean：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;boolean 数据类型表示一位的信息&lt;/li&gt;
&lt;li&gt;只有两个取值：true 和 false&lt;/li&gt;
&lt;li&gt;JVM 规范指出 boolean 当做 int 处理，boolean 数组当做 byte 数组处理，这样可以得出 boolean 类型单独使用占了 4 个字节，在数组中是 1 个字节&lt;/li&gt;
&lt;li&gt;默认值是 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;boolean one = true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;char：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;char 类型是一个单一的 16 位&lt;strong&gt;两个字节&lt;/strong&gt;的 Unicode 字符&lt;/li&gt;
&lt;li&gt;最小值是 &lt;code&gt;\u0000&lt;/code&gt;（即为 0）&lt;/li&gt;
&lt;li&gt;最大值是 &lt;code&gt;\uffff&lt;/code&gt;（即为 65535）&lt;/li&gt;
&lt;li&gt;char 数据类型可以&lt;strong&gt;存储任何字符&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;例子：&lt;code&gt;char c = &apos;A&apos;&lt;/code&gt;，&lt;code&gt;char c = &apos;张&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;上下转型&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;float 与 double：&lt;/p&gt;
&lt;p&gt;Java 不能隐式执行&lt;strong&gt;向下转型&lt;/strong&gt;，因为这会使得精度降低，但是可以向上转型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1.1字面量属于double类型，不能直接将1.1直接赋值给 float 变量，因为这是向下转型
float f = 1.1;//报错
//1.1f 字面量才是 float 类型
float f = 1.1f;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;float f1 = 1.234f;
double d1 = f1;

double d2 = 1.23;
float f2 = (float) d2;//向下转型需要强转
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int i1 = 1245;
long l1 = i1;

long l2 = 1234;
int i2 = (int) l2;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐式类型转换：&lt;/p&gt;
&lt;p&gt;字面量 1 是 int 类型，比 short 类型精度要高，因此不能隐式地将 int 类型向下转型为 short 类型&lt;/p&gt;
&lt;p&gt;使用 += 或者 ++ 运算符会执行类型转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;short s1 = 1;
s1 += 1;	//s1++;
//上面的语句相当于将 s1 + 1 的计算结果进行了向下转型
s1 = (short) (s1 + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;引用类型&lt;/h5&gt;
&lt;p&gt;引用数据类型：类，接口，数组都是引用数据类型，又叫包装类&lt;/p&gt;
&lt;p&gt;包装类的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包装类作为类首先拥有了 Object 类的方法&lt;/li&gt;
&lt;li&gt;包装类作为引用类型的变量可以&lt;strong&gt;存储 null 值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;基本数据类型                包装类（引用数据类型）
byte                      Byte
short                     Short
int                       Integer
long                      Long

float                     Float
double                    Double
char                      Character
boolean                   Boolean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 为包装类做了一些特殊功能，具体来看特殊功能主要有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以把基本数据类型的值转换成字符串类型的值&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 toString() 方法&lt;/li&gt;
&lt;li&gt;调用 Integer.toString(基本数据类型的值) 得到字符串&lt;/li&gt;
&lt;li&gt;直接把基本数据类型 + 空字符串就得到了字符串（推荐使用）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把字符串类型的数值转换成对应的基本数据类型的值（&lt;strong&gt;重要&lt;/strong&gt;）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Xxx.parseXxx(&quot;字符串类型的数值&quot;) → &lt;code&gt;Integer.parseInt(numStr)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Xxx.valueOf(&quot;字符串类型的数值&quot;)   → &lt;code&gt;Integer.valueOf(numStr)&lt;/code&gt; （推荐使用）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class PackageClass02 {
    public static void main(String[] args) {
        // 1.把基本数据类型的值转成字符串
        Integer it = 100 ;
        // a.调用toString()方法。
        String itStr = it.toString();
        System.out.println(itStr+1);//1001
        // b.调用Integer.toString(基本数据类型的值)得到字符串。
        String itStr1 = Integer.toString(it);
        System.out.println(itStr1+1);//1001
        // c.直接把基本数据类型+空字符串就得到了字符串。
        String itStr2 = it + &quot;&quot;;
        System.out.println(itStr2+1);// 1001

        // 2.把字符串类型的数值转换成对应的基本数据类型的值
        String numStr = &quot;23&quot;;
        int numInt = Integer.valueOf(numStr);
        System.out.println(numInt+1);//24

        String doubleStr = &quot;99.9&quot;;
        double doubleDb = Double.valueOf(doubleStr);
        System.out.println(doubleDb+0.1);//100.0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;类型对比&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有了基本数据类型，为什么还要引用数据类型？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;引用数据类型封装了数据和处理该数据的方法，比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型&lt;/p&gt;
&lt;p&gt;Java 中大部分类和方法都是针对引用数据类型，包括泛型和集合&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用数据类型那么好，为什么还用基本数据类型？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;引用类型的对象要多储存对象头，对基本数据类型来说空间浪费率太高。逻辑上来讲，Java 只有包装类就够了，为了运行速度，需要用到基本数据类型；优先考虑运行效率的问题，所以二者同时存在是合乎情理的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 集合不能存放基本数据类型，只存放对象的引用？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不能放基本数据类型是因为不是 Object 的子类。泛型思想，如果不用泛型要写很多参数类型不同的但功能相同的函数（方法重载）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;==&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;== 比较基本数据类型：比较的是具体的值
== 比较引用数据类型：比较的是对象地址值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;装箱拆箱&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;自动装箱&lt;/strong&gt;：可以直接把基本数据类型的值或者变量赋值给包装类&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自动拆箱&lt;/strong&gt;：可以把包装类的变量直接赋值给基本数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class PackegeClass {
    public static void main(String[] args) {
        int a = 12 ;
        Integer a1 = 12 ;  // 自动装箱
        Integer a2 = a ;   // 自动装箱
        Integer a3 = null; // 引用数据类型的默认值可以为null

        Integer c = 100 ;
        int c1 = c ;      // 自动拆箱

        Integer it = Integer.valueOf(12);  	// 手工装箱！
        // Integer it1 = new Integer(12); 	// 手工装箱！
        Integer it2 = 12;

        Integer it3 = 111 ;
        int it33 = it3.intValue(); // 手工拆箱
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自动装箱&lt;/strong&gt;反编译后底层调用 &lt;code&gt;Integer.valueOf()&lt;/code&gt; 实现，源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static Integer valueOf(int i) {
    if (i &amp;gt;= IntegerCache.low &amp;amp;&amp;amp; i &amp;lt;= IntegerCache.high)
        // 【缓存池】，本质上是一个数组
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动拆箱调用 &lt;code&gt;java.lang.Integer#intValue&lt;/code&gt;，源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int intValue() {
    return value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存池&lt;/h4&gt;
&lt;p&gt;new Integer(123) 与 Integer.valueOf(123) 的区别在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;new Integer(123)：每次都会新建一个对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Integer.valueOf(123)：会使用缓存池中的对象，多次调用取得同一个对象的引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;valueOf() 方法的实现比较简单，就是先判断值是否在缓存池中，如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法，因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建，那么就会引用相同的对象&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本类型对应的缓存池如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boolean values true and false&lt;/li&gt;
&lt;li&gt;all byte values&lt;/li&gt;
&lt;li&gt;Short values between -128 and 127&lt;/li&gt;
&lt;li&gt;Long values between -128 and 127&lt;/li&gt;
&lt;li&gt;Integer values between -128 and 127&lt;/li&gt;
&lt;li&gt;Character in the range \u0000 to \u007F (0 and 127)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 jdk 1.8 所有的数值类缓冲池中，&lt;strong&gt;Integer 的缓存池 IntegerCache 很特殊，这个缓冲池的下界是 -128，上界默认是 127&lt;/strong&gt;，但是上界是可调的，在启动 JVM 时通过 &lt;code&gt;AutoBoxCacheMax=&amp;lt;size&amp;gt;&lt;/code&gt; 来指定这个缓冲池的大小，该选项在 JVM 初始化的时候会设定一个名为 java.lang.Integer.IntegerCache 系统属性，然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer x = 100;				// 自动装箱，底层调用 Integer.valueOf(1)
Integer y = 100;
System.out.println(x == y);   	// true

Integer x = 1000;
Integer y = 1000;
System.out.println(x == y);   	// false，因为缓存池最大127

int x = 1000;
Integer y = 1000;
System.out.println(x == y);		// true，因为 y 会调用 intValue 【自动拆箱】返回 int 原始值进行比较
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;输入数据&lt;/h4&gt;
&lt;p&gt;语法：&lt;code&gt;Scanner sc = new Scanner(System.in)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;next()：遇到了空格，就不再录入数据了，结束标记：空格、tab 键&lt;/li&gt;
&lt;li&gt;nextLine()：可以将数据完整的接收过来，结束标记：回车换行符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般使用 &lt;code&gt;sc.nextInt()&lt;/code&gt; 或者 &lt;code&gt;sc.nextLine()&lt;/code&gt; 接受整型和字符串，然后转成需要的数据类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scanner：&lt;code&gt;BufferedReader br = new BufferedReader(new InputStreamReader(System.in))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;print：&lt;code&gt;PrintStream.write()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;使用引用数据类型的API&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    while (sc.hasNextLine()) {
        String msg = sc.nextLine();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;p&gt;数组就是存储数据长度固定的容器，存储多个数据的数据类型要一致，&lt;strong&gt;数组也是一个对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;创建数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据类型[] 数组名：&lt;code&gt;int[] arr&lt;/code&gt;  （常用）&lt;/li&gt;
&lt;li&gt;数据类型 数组名[]：&lt;code&gt;int arr[]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...}：&lt;code&gt;int[] arr = new int[]{11,22,33}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;数据类型[] 数组名 = {元素1,元素2,...}：&lt;code&gt;int[] arr = {44,55,66}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;动态初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据类型[] 数组名 = new 数据类型[数组长度]：&lt;code&gt;int[] arr = new int[3]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;元素访问&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;索引&lt;/strong&gt;：每一个存储到数组的元素，都会自动的拥有一个编号，从 &lt;strong&gt;0&lt;/strong&gt; 开始。这个自动编号称为数组索引（index），可以通过数组的索引访问到数组中的元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问格式&lt;/strong&gt;：数组名[索引]，&lt;code&gt;arr[0]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;赋值：&lt;/strong&gt;&lt;code&gt;arr[0] = 10&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;内存分配&lt;/h4&gt;
&lt;p&gt;内存是计算机中的重要器件，临时存储区域，作用是运行程序。编写的程序是存放在硬盘中，在硬盘中的程序是不会运行的，必须放进内存中才能运行，运行完毕后会清空内存，Java 虚拟机要运行程序，必须要对内存进行空间的分配和管理&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;区域名称&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;寄存器&lt;/td&gt;
&lt;td&gt;给 CPU 使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本地方法栈&lt;/td&gt;
&lt;td&gt;JVM 在使用操作系统功能的时候使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法区&lt;/td&gt;
&lt;td&gt;存储可以运行的 class 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;堆内存&lt;/td&gt;
&lt;td&gt;存储对象或者数组，new 来创建的，都存储在堆内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方法栈&lt;/td&gt;
&lt;td&gt;方法运行时使用的内存，比如 main 方法运行，进入方法栈中执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;内存分配图：&lt;strong&gt;Java 数组分配在堆内存&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个数组内存图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D-%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两个数组内存图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D-%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个数组指向相同内存图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D-%E5%A4%9A%E4%B8%AA%E6%95%B0%E7%BB%84%E6%8C%87%E5%90%91%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84%E5%86%85%E5%AD%98%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数组异常&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;索引越界异常：ArrayIndexOutOfBoundsException&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空指针异常：NullPointerException&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArrayDemo {
    public static void main(String[] args) {
        int[] arr = new int[3];
        //把null赋值给数组
        arr = null;
        System.out.println(arr[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;arr = null，表示变量 arr 将不再保存数组的内存地址，也就不允许再操作数组，因此运行的时候会抛出空指针异常。在开发中，空指针异常是不能出现的，一旦出现了，就必须要修改编写的代码&lt;/p&gt;
&lt;p&gt;解决方案：给数组一个真正的堆内存空间引用即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;二维数组&lt;/h4&gt;
&lt;p&gt;二维数组也是一种容器，不同于一维数组，该容器存储的都是一维数组容器&lt;/p&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;动态初始化：数据类型[][] 变量名 = new 数据类型[m] [n]，&lt;code&gt;int[][] arr = new int[3][3]&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;m 表示这个二维数组，可以存放多少个一维数组，行&lt;/li&gt;
&lt;li&gt;n 表示每一个一维数组，可以存放多少个元素，列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据类型[][] 变量名 = new 数据类型 [][]{{元素1, 元素2...} , {元素1, 元素2...}&lt;/li&gt;
&lt;li&gt;数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...}&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int[][] arr = {{11,22,33}, {44,55,66}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test1 {
    /*
        步骤:
            1. 遍历二维数组，取出里面每一个一维数组
            2. 在遍历的过程中，对每一个一维数组继续完成遍历，获取内部存储的每一个元素
     */
    public static void main(String[] args) {
        int[][] arr = {{11, 22, 33}, {33, 44, 55}};
        // 1. 遍历二维数组，取出里面每一个一维数组
        for (int i = 0; i &amp;lt; arr.length; i++) {
            //System.out.println(arr[i]);
            // 2. 在遍历的过程中，对每一个一维数组继续完成遍历，获取内部存储的每一个元素
            //int[] temp = arr[i];
            for (int j = 0; j &amp;lt; arr[i].length; j++) {
                System.out.println(arr[i][j]);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;运算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;i++ 与 ++i 的区别？&lt;/p&gt;
&lt;p&gt;i++ 表示先将 i 放在表达式中运算，然后再加 1，++i 表示先将 i 加 1，然后再放在表达式中运算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;|| 和 |，&amp;amp;&amp;amp; 和&amp;amp; 的区别，逻辑运算符&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;amp; 和| 称为布尔运算符，位运算符；&amp;amp;&amp;amp; 和 || 称为条件布尔运算符，也叫短路运算符&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果 &amp;amp;&amp;amp; 运算符的第一个操作数是 false，就不需要考虑第二个操作数的值了，因为无论第二个操作数的值是什么，其结果都是 false；同样，如果第一个操作数是 true，|| 运算符就返回 true，无需考虑第二个操作数的值；但 &amp;amp; 和 | 却不是这样，它们总是要计算两个操作数。为了提高性能，&lt;strong&gt;尽可能使用 &amp;amp;&amp;amp; 和 || 运算符&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异或 ^：两位相异为 1，相同为 0，又叫不进位加法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同或：两位相同为 1，相异为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;switch：从 Java 7 开始，可以在 switch 条件判断语句中使用 String 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;a&quot;;
switch (s) {
    case &quot;a&quot;:
        System.out.println(&quot;aaa&quot;);
        break;
    case &quot;b&quot;:
        System.out.println(&quot;bbb&quot;);
        break;
    default:
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch 不支持 long、float、double，switch 的设计初衷是对那些只有少数几个值的类型进行等值判断，如果值过于复杂，那么用 if 比较合适&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;break：跳出一层循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移位运算：计算机里一般用&lt;strong&gt;补码表示数字&lt;/strong&gt;，正数、负数的表示区别就是最高位是 0 还是 1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正数的原码反码补码相同，最高位为 0&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100:	00000000  00000000  00000000  01100100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负数：
原码：最高位为 1，其余位置和正数相同
反码：保证符号位不变，其余位置取反
补码：保证符号位不变，其余位置取反后加 1，即反码 +1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-100 原码:	10000000  00000000  00000000  01100100	//32位
-100 反码:	11111111  11111111  11111111  10011011
-100 补码:	11111111  11111111  11111111  10011100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补码 → 原码：符号位不变，其余位置取反加 1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 运算符：将二进制位进行右移操作，相当于除 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 运算符：将二进制位进行左移操作，相当于乘 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; 运算符：无符号右移，忽略符号位，空位都以 0 补齐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运算规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正数的左移与右移，空位补 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负数原码的左移与右移，空位补 0&lt;/p&gt;
&lt;p&gt;负数反码的左移与右移，空位补 1&lt;/p&gt;
&lt;p&gt;负数补码，左移低位补 0（会导致负数变为正数的问题，因为移动了符号位），右移高位补 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无符号移位，空位补 0&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;参数&lt;/h3&gt;
&lt;h4&gt;形参实参&lt;/h4&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;形式参数，用于定义方法的时候使用的参数，只能是变量&lt;/li&gt;
&lt;li&gt;形参只有在方法被调用的时候，虚拟机才分配内存单元，方法调用结束之后便会释放所分配的内存单元&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实参：调用方法时传递的数据可以是常量，也可以是变量&lt;/p&gt;
&lt;h4&gt;可变参数&lt;/h4&gt;
&lt;p&gt;可变参数用在形参中可以接收多个数据，在方法内部&lt;strong&gt;本质上就是一个数组&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;格式：数据类型... 参数名称&lt;/p&gt;
&lt;p&gt;作用：传输参数非常灵活，可以不传输参数、传输一个参数、或者传输一个数组&lt;/p&gt;
&lt;p&gt;可变参数的注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个形参列表中可变参数只能有一个&lt;/li&gt;
&lt;li&gt;可变参数必须放在形参列表的&lt;strong&gt;最后面&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
	sum(); // 可以不传输参数。
	sum(10); // 可以传输一个参数。
	sum(10,20,30); // 可以传输多个参数。
	sum(new int[]{10,30,50,70,90}); // 可以传输一个数组。
}

public static void sum(int... nums){
	int sum = 0;
	for(int i : a) {
		sum += i;
	}
	return sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;方法&lt;/h3&gt;
&lt;h4&gt;方法概述&lt;/h4&gt;
&lt;p&gt;方法（method）是将具有独立功能的代码块组织成为一个整体，使其具有特殊功能的代码集&lt;/p&gt;
&lt;p&gt;注意：方法必须先创建才可以使用，该过程成为方法定义，方法创建后并不是直接可以运行的，需要手动使用后才执行，该过程成为方法调用&lt;/p&gt;
&lt;p&gt;在方法内部定义的叫局部变量，局部变量不能加 static，包括 protected、private、public 这些也不能加&lt;/p&gt;
&lt;p&gt;原因：局部变量是保存在栈中的，而静态变量保存于方法区（JDK8 在堆中），局部变量出了方法就被栈回收了，而静态变量不会，所以&lt;strong&gt;在局部变量前不能加 static 关键字&lt;/strong&gt;，静态变量是定义在类中，又叫类变量&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;定义调用&lt;/h4&gt;
&lt;p&gt;定义格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static 返回值类型 方法名(参数) {
	//方法体;
	return 数据 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据类型 变量名 = 方法名 (参数) ;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方法名：调用方法时候使用的标识&lt;/li&gt;
&lt;li&gt;参数：由数据类型和变量名组成，多个参数之间用逗号隔开&lt;/li&gt;
&lt;li&gt;方法体：完成功能的代码块&lt;/li&gt;
&lt;li&gt;return：如果方法操作完毕，有数据返回，用于把数据返回给调用者&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果方法操作完毕&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;void 类型的方法，直接调用即可，而且方法体中一般不写 return&lt;/li&gt;
&lt;li&gt;非 void 类型的方法，推荐用变量接收调用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：每个方法在被调用执行的时候，都会进入栈内存，并且拥有自己独立的内存空间，方法内部代码调用完毕之后，会从栈内存中弹栈消失&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;注意事项&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法不能嵌套定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo {
	public static void main(String[] args) {
	}
	public static void methodOne() {
		public static void methodTwo() {
			// 这里会引发编译错误!!!
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;void 表示无返回值，可以省略 return，也可以单独的书写 return，后面不加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void methodTwo() {
	//return 100; 编译错误，因为没有具体返回值类型
	return;
	//System.out.println(100); return语句后面不能跟数据或代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;方法重载&lt;/h4&gt;
&lt;h5&gt;重载介绍&lt;/h5&gt;
&lt;p&gt;方法重载指同一个类中定义的多个方法之间的关系，满足下列条件的多个方法相互构成重载：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;多个方法在&lt;strong&gt;同一个类&lt;/strong&gt;中&lt;/li&gt;
&lt;li&gt;多个方法具有&lt;strong&gt;相同的方法名&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;多个方法的&lt;strong&gt;参数不相同&lt;/strong&gt;，类型不同或者数量不同&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重载仅对应方法的定义，与方法的调用无关，调用方式参照标准格式&lt;/p&gt;
&lt;p&gt;重载仅针对同一个类中方法的名称与参数进行识别，与返回值无关，&lt;strong&gt;不能通过返回值来判定两个方法是否构成重载&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原理：JVM → 运行机制 → 方法调用 → 多态原理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo {
	public static void fn(int a) {
		//方法体
	}
    
	public static int fn(int a) { /*错误原因：重载与返回值无关*/
		//方法体
	}
    
    public static void fn(int a, int b) {/*正确格式*/
		//方法体
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;方法选取&lt;/h5&gt;
&lt;p&gt;重载的方法在编译过程中即可完成识别，方法调用时 Java 编译器会根据所传入参数的声明类型（注意与实际类型区分）来选取重载方法。选取的过程共分为三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一阶段：在不考虑对基本类型自动装拆箱 (auto-boxing，auto-unboxing)，以及可变长参数的情况下选取重载方法&lt;/li&gt;
&lt;li&gt;二阶段：如果第一阶段中没有找到适配的方法，那么在允许自动装拆箱，但不允许可变长参数的情况下选取重载方法&lt;/li&gt;
&lt;li&gt;三阶段：如果第二阶段中没有找到适配的方法，那么在允许自动装拆箱以及可变长参数的情况下选取重载方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 Java 编译器在同一个阶段中找到了多个适配的方法，那么会选择一个最为贴切的，而决定贴切程度的一个关键就是形式参数类型的继承关系，&lt;strong&gt;一般会选择形参为参数类型的子类的方法，因为子类时更具体的实现&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo {
    void invoke(Object obj, Object... args) { ... }
    void invoke(String s, Object obj, Object... args) { ... }

    invoke(null, 1); 	// 调用第二个invoke方法，选取的第二阶段
    invoke(null, 1, 2); // 调用第二个invoke方法，匹配第一个和第二个，但String是Object的子类
    
    invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖，才能调用第一个invoke方法
    							   // 可变参数底层是数组，JVM-&amp;gt;运行机制-&amp;gt;代码优化
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此不提倡可变长参数方法的重载&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;继承重载&lt;/h5&gt;
&lt;p&gt;除了同一个类中的方法，重载也可以作用于这个类所继承而来的方法。如果子类定义了与父类中&lt;strong&gt;非私有方法&lt;/strong&gt;同名的方法，而且这两个方法的参数类型不同，那么在子类中，这两个方法同样构成了重载&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这两个方法都是静态的，那么子类中的方法隐藏了父类中的方法&lt;/li&gt;
&lt;li&gt;如果这两个方法都不是静态的，且都不是私有的，那么子类的方法重写了父类中的方法，也就是&lt;strong&gt;多态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;参数传递&lt;/h4&gt;
&lt;p&gt;Java 的参数是以&lt;strong&gt;值传递&lt;/strong&gt;的形式传入方法中&lt;/p&gt;
&lt;p&gt;值传递和引用传递的区别在于传递后会不会影响实参的值：&lt;strong&gt;值传递会创建副本&lt;/strong&gt;，引用传递不会创建副本&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基本数据类型：形式参数的改变，不影响实际参数&lt;/p&gt;
&lt;p&gt;每个方法在栈内存中，都会有独立的栈空间，方法运行结束后就会弹栈消失&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArgsDemo01 {
	public static void main(String[] args) {
		int number = 100;
		System.out.println(&quot;调用change方法前：&quot; + number);//100
		change(number);
		System.out.println(&quot;调用change方法后：&quot; + number);//100
	}
	public static void change(int number) {
		number = 200;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用类型：形式参数的改变，影响实际参数的值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用数据类型的传参，本质上是将对象的地址以值的方式传递到形参中&lt;/strong&gt;，内存中会造成两个引用指向同一个内存的效果，所以即使方法弹栈，堆内存中的数据也已经是改变后的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog(&quot;A&quot;);
        func(dog);
        System.out.println(dog.getName());	// B
    }
    private static void func(Dog dog) {
        dog.setName(&quot;B&quot;);
    }
}
class Dog {
    String name;//.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;枚举&lt;/h3&gt;
&lt;p&gt;枚举是 Java 中的一种特殊类型，为了做信息的标志和信息的分类&lt;/p&gt;
&lt;p&gt;定义枚举的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 enum 枚举名称{
	第一行都是罗列枚举实例的名称。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;枚举的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;枚举类是用 final 修饰的，枚举类不能被继承&lt;/li&gt;
&lt;li&gt;枚举类默认继承了 java.lang.Enum 枚举类&lt;/li&gt;
&lt;li&gt;枚举类的第一行都是常量，必须是罗列枚举类的实例名称&lt;/li&gt;
&lt;li&gt;枚举类相当于是多例设计模式&lt;/li&gt;
&lt;li&gt;每个枚举项都是一个实例，是一个静态成员变量&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String name()&lt;/td&gt;
&lt;td&gt;获取枚举项的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int ordinal()&lt;/td&gt;
&lt;td&gt;返回枚举项在枚举类中的索引值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int compareTo(E  o)&lt;/td&gt;
&lt;td&gt;比较两个枚举项，返回的是索引值的差值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String toString()&lt;/td&gt;
&lt;td&gt;返回枚举常量的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static &amp;lt;T&amp;gt; T  valueOf(Class&amp;lt;T&amp;gt; type,String  name)&lt;/td&gt;
&lt;td&gt;获取指定枚举类中的指定名称的枚举值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;values()&lt;/td&gt;
&lt;td&gt;获得所有的枚举项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Season {
    SPRING , SUMMER , AUTUMN , WINTER;
}
// 枚举类的编译以后源代码：
public final class Season extends java.lang.Enum&amp;lt;Season&amp;gt; {
	public static final Season SPRING = new Season();
	public static final Season SUMMER = new Season();
	public static final Season AUTUMN = new Season();
	public static final Season WINTER = new Season();

	public static Season[] values();
	public static Season valueOf(java.lang.String);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API 使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EnumDemo {
    public static void main(String[] args){
        // 获取索引
        Season s = Season.SPRING;
        System.out.println(s);	//SPRING
        System.out.println(s.ordinal()); // 0，该值代表索引，summer 就是 1
        s.s.doSomething();
        // 获取全部枚举
        Season[] ss = Season.values();
        for(int i = 0; i &amp;lt; ss.length; i++){
            System.out.println(ss[i]);
        }
        
        int result = Season.SPRING.compareTo(Season.WINTER);
        System.out.println(result);//-3
    }
}
enum Season {
    SPRING , SUMMER , AUTUMN , WINTER;
    
    public void doSomething() {
        System.out.println(&quot;hello &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Debug&lt;/h3&gt;
&lt;p&gt;Debug 是供程序员使用的程序调试工具，它可以用于查看程序的执行流程，也可以用于追踪程序执行过程来调试程序。&lt;/p&gt;
&lt;p&gt;加断点 → Debug 运行 → 单步运行 → 看 Debugger 窗口 → 看 Console 窗口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Debug%E6%8C%89%E9%94%AE%E8%AF%B4%E6%98%8E.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Debug条件断点.png&quot; alt=&quot;Debug条件断点&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;对象&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;Java 是一种面向对象的高级编程语言&lt;/p&gt;
&lt;p&gt;面向对象三大特征：&lt;strong&gt;封装，继承，多态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两个概念：类和对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类：相同事物共同特征的描述，类只是学术上的一个概念并非真实存在的，只能描述一类事物&lt;/li&gt;
&lt;li&gt;对象：是真实存在的实例， 实例 == 对象，&lt;strong&gt;对象是类的实例化&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;结论：有了类和对象就可以描述万千世界所有的事物，必须先有类才能有对象&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;类&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;定义格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 class 类名{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;类名的首字母建议大写，满足驼峰模式，比如 StudentNameCode&lt;/li&gt;
&lt;li&gt;一个 Java 代码中可以定义多个类，按照规范一个 Java 文件一个类&lt;/li&gt;
&lt;li&gt;一个 Java 代码文件中，只能有一个类是 public 修饰，&lt;strong&gt;public 修饰的类名必须成为当前 Java 代码的文件名称&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;类中的成分:有且仅有五大成分
修饰符 class 类名{
		1.成员变量(Field):  	描述类或者对象的属性信息的。
        2.成员方法(Method):		描述类或者对象的行为信息的。
		3.构造器(Constructor):	 初始化一个对象返回。
		4.代码块
		5.内部类
	  }
类中有且仅有这五种成分，否则代码报错！
public class ClassDemo {
    System.out.println(1);//报错
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;构造器&lt;/h4&gt;
&lt;p&gt;构造器格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 类名(形参列表){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：初始化类的一个对象返回&lt;/p&gt;
&lt;p&gt;分类：无参数构造器，有参数构造器&lt;/p&gt;
&lt;p&gt;注意：&lt;strong&gt;一个类默认自带一个无参数构造器&lt;/strong&gt;，写了有参数构造器默认的无参数构造器就消失，还需要用无参数构造器就要重新写&lt;/p&gt;
&lt;p&gt;构造器初始化对象的格式：类名 对象名称 = new 构造器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无参数构造器的作用：初始化一个类的对象（使用对象的默认值初始化）返回&lt;/li&gt;
&lt;li&gt;有参数构造器的作用：初始化一个类的对象（可以在初始化对象的时候为对象赋值）返回&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;包&lt;/h3&gt;
&lt;p&gt;包：分门别类的管理各种不同的技术，便于管理技术，扩展技术，阅读技术&lt;/p&gt;
&lt;p&gt;定义包的格式：&lt;code&gt;package 包名&lt;/code&gt;，必须放在类名的最上面&lt;/p&gt;
&lt;p&gt;导包格式：&lt;code&gt;import 包名.类名&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;相同包下的类可以直接访问；不同包下的类必须导包才可以使用&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;封装&lt;/h3&gt;
&lt;p&gt;封装的哲学思维：合理隐藏，合理暴露&lt;/p&gt;
&lt;p&gt;封装最初的目的：提高代码的安全性和复用性，组件化&lt;/p&gt;
&lt;p&gt;封装的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;成员变量应该私有，用 private 修饰，只能在本类中直接访问&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供成套的 getter 和 setter 方法暴露成员变量的取值和赋值&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 private 修饰成员变量的原因：实现数据封装，不想让别人使用修改你的数据，比较安全&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;this&lt;/h3&gt;
&lt;p&gt;this 关键字的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this 关键字代表了当前对象的引用&lt;/li&gt;
&lt;li&gt;this 出现在方法中：&lt;strong&gt;哪个对象调用这个方法 this 就代表谁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;this 可以出现在构造器中：代表构造器正在初始化的那个对象&lt;/li&gt;
&lt;li&gt;this 可以区分变量是访问的成员变量还是局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;static&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的&lt;/p&gt;
&lt;p&gt;按照有无 static 修饰，成员变量和方法可以分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态成员变量（类变量）：static 修饰的成员变量，属于类本身，&lt;strong&gt;与类一起加载一次，只有一个&lt;/strong&gt;，直接用类名访问即可&lt;/li&gt;
&lt;li&gt;实例成员变量：无 static 修饰的成员变量，属于类的每个对象的，&lt;strong&gt;与类的对象一起加载&lt;/strong&gt;，对象有多少个，实例成员变量就加载多少个，必须用类的对象来访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态方法：有 static 修饰的成员方法称为静态方法也叫类方法，属于类本身的，直接用类名访问即可&lt;/li&gt;
&lt;li&gt;实例方法：无 static 修饰的成员方法称为实例方法，属于类的每个对象的，必须用类的对象来访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;static 用法&lt;/h4&gt;
&lt;p&gt;成员变量的访问语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态成员变量：只有一份可以被类和类的对象&lt;strong&gt;共享访问&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类名.静态成员变量（同一个类中访问静态成员变量可以省略类名不写）&lt;/li&gt;
&lt;li&gt;对象.静态成员变量（不推荐）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象.实例成员变量（先创建对象）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法的访问语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态方法：有 static 修饰，属于类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类名.静态方法（同一个类中访问静态成员可以省略类名不写）&lt;/li&gt;
&lt;li&gt;对象.静态方法（不推荐，参考 JVM → 运行机制 → 方法调用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例方法：无 static 修饰，属于对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象.实例方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    // 1.静态方法：有static修饰，属于类，直接用类名访问即可！
    public static void inAddr(){ }
    // 2.实例方法：无static修饰，属于对象，必须用对象访问！
    public void eat(){}
    
    public static void main(String[] args) {
        // a.类名.静态方法
        Student.inAddr();
        inAddr();
        // b.对象.实例方法
        // Student.eat(); // 报错了！
        Student sea = new Student();
        sea.eat();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;两个问题&lt;/h4&gt;
&lt;p&gt;内存问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈内存存放 main 方法和地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆内存存放对象和变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法区存放 class 和静态变量（jdk8 以后移入堆）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;访问问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例方法是否可以直接访问实例成员变量？可以，因为它们都属于对象&lt;/li&gt;
&lt;li&gt;实例方法是否可以直接访问静态成员变量？可以，静态成员变量可以被共享访问&lt;/li&gt;
&lt;li&gt;实例方法是否可以直接访问实例方法? 可以，实例方法和实例方法都属于对象&lt;/li&gt;
&lt;li&gt;实例方法是否可以直接访问静态方法？可以，静态方法可以被共享访问&lt;/li&gt;
&lt;li&gt;静态方法是否可以直接访问实例变量？ 不可以，实例变量&lt;strong&gt;必须用对象访问&lt;/strong&gt;！！&lt;/li&gt;
&lt;li&gt;静态方法是否可以直接访问静态变量？ 可以，静态成员变量可以被共享访问。&lt;/li&gt;
&lt;li&gt;静态方法是否可以直接访问实例方法? 不可以，实例方法必须用对象访问！！&lt;/li&gt;
&lt;li&gt;静态方法是否可以直接访问静态方法？可以，静态方法可以被共享访问！！&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;继承&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;继承是 Java 中一般到特殊的关系，是一种子类到父类的关系&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被继承的类称为：父类/超类&lt;/li&gt;
&lt;li&gt;继承父类的类称为：子类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继承的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提高代码的复用&lt;/strong&gt;，相同代码可以定义在父类中&lt;/li&gt;
&lt;li&gt;子类继承父类，可以直接使用父类这些代码（相同代码重复利用）&lt;/li&gt;
&lt;li&gt;子类得到父类的属性（成员变量）和行为（方法），还可以定义自己的功能，子类更强大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继承的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子类的全部构造器默认先访问父类的无参数构造器，再执行自己的构造器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单继承&lt;/strong&gt;：一个类只能继承一个直接父类&lt;/li&gt;
&lt;li&gt;多层继承：一个类可以间接继承多个父类（家谱）&lt;/li&gt;
&lt;li&gt;一个类可以有多个子类&lt;/li&gt;
&lt;li&gt;一个类要么默认继承了 Object 类，要么间接继承了 Object 类，&lt;strong&gt;Object 类是 Java 中的祖宗类&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;继承的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;子类 extends 父类{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子类不能继承父类的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子类不能继承父类的构造器，子类有自己的构造器&lt;/li&gt;
&lt;li&gt;子类是不能可以继承父类的私有成员的，可以反射暴力去访问继承自父类的私有成员&lt;/li&gt;
&lt;li&gt;子类是不能继承父类的静态成员，父类静态成员只有一份可以被子类共享访问，&lt;strong&gt;共享并非继承&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ExtendsDemo {
    public static void main(String[] args) {
        Cat c = new Cat();
        // c.run();
        Cat.test();
        System.out.println(Cat.schoolName);
    }
}

class Cat extends Animal{
}

class Animal{
    public static String schoolName =&quot;seazean&quot;;
    public static void test(){}
    private void run(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;变量访问&lt;/h4&gt;
&lt;p&gt;继承后成员变量的访问特点：&lt;strong&gt;就近原则&lt;/strong&gt;，子类有找子类，子类没有找父类，父类没有就报错&lt;/p&gt;
&lt;p&gt;如果要申明访问父类的成员变量可以使用：super.父类成员变量，super指父类引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ExtendsDemo {
    public static void wmain(String[] args) {
        Wolf w = new Wolf();w
        w.showName();
    }
}

class Wolf extends Animal{
    private String name = &quot;子类狼&quot;;
    public void showName(){
        String name = &quot;局部名称&quot;;
        System.out.println(name); // 局部name
        System.out.println(this.name); // 子类对象的name
        System.out.println(super.name); // 父类的
        System.out.println(name1); // 父类的
        //System.out.println(name2); // 报错。子类父类都没有
    }
}

class Animal{
    public String name = &quot;父类动物名称&quot;;
    public String name1 = &quot;父类&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;方法访问&lt;/h4&gt;
&lt;p&gt;子类继承了父类就得到了父类的方法，&lt;strong&gt;可以直接调用&lt;/strong&gt;，受权限修饰符的限制，也可以重写方法&lt;/p&gt;
&lt;p&gt;方法重写：子类重写一个与父类申明一样的方法来&lt;strong&gt;覆盖&lt;/strong&gt;父类的该方法&lt;/p&gt;
&lt;p&gt;方法重写的校验注解：@Override&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法加了这个注解，那就必须是成功重写父类的方法，否则报错&lt;/li&gt;
&lt;li&gt;@Override 优势：可读性好，安全，优雅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;子类可以扩展父类的功能，但不能改变父类原有的功能&lt;/strong&gt;，重写有以下三个限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子类方法的访问权限必须大于等于父类方法&lt;/li&gt;
&lt;li&gt;子类方法的返回类型必须是父类方法返回类型或为其子类型&lt;/li&gt;
&lt;li&gt;子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继承中的隐藏问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子类和父类方法都是静态的，那么子类中的方法会隐藏父类中的方法&lt;/li&gt;
&lt;li&gt;在子类中可以定义和父类成员变量同名的成员变量，此时子类的成员变量隐藏了父类的成员变量，在创建对象为对象分配内存的过程中，&lt;strong&gt;隐藏变量依然会被分配内存&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ExtendsDemo {
    public static void main(String[] args) {
        Wolf w = new Wolf();
        w.run();
    }
}
class Wolf extends Animal{
    @Override
    public void run(){}//
}
class Animal{
    public void run(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常见问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么子类构造器会先调用父类构造器？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子类的构造器的第一行默认 super() 调用父类的无参数构造器，写不写都存在&lt;/li&gt;
&lt;li&gt;子类继承父类，子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时，必须先调用父类构造器初始化继承自父类的属性和行为&lt;/li&gt;
&lt;li&gt;参考 JVM → 类加载 → 对象创建&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
    public Animal() {
        System.out.println(&quot;==父类Animal的无参数构造器==&quot;);
    }
}

class Tiger extends Animal {
    public Tiger() {
        super(); // 默认存在的，根据参数去匹配调用父类的构造器。
        System.out.println(&quot;==子类Tiger的无参数构造器==&quot;);
    }
    public Tiger(String name) {
        //super();  默认存在的，根据参数去匹配调用父类的构造器。
        System.out.println(&quot;==子类Tiger的有参数构造器==&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么 Java 是单继承的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答：反证法，假如 Java 可以多继承，请看如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A{
	public void test(){
		System.out.println(&quot;A&quot;);
	}
}
class B{
	public void test(){
		System.out.println(&quot;B&quot;);
	}
}
class C extends A , B {
	public static void main(String[] args){
		C c = new C();
        c.test(); 
        // 出现了类的二义性！所以Java不能多继承！！
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;super&lt;/h3&gt;
&lt;p&gt;继承后 super 调用父类构造器，父类构造器初始化继承自父类的数据。&lt;/p&gt;
&lt;p&gt;总结与拓展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this 代表了当前对象的引用（继承中指代子类对象）：this.子类成员变量、this.子类成员方法、&lt;strong&gt;this(...)&lt;/strong&gt; 可以根据参数匹配访问本类其他构造器&lt;/li&gt;
&lt;li&gt;super 代表了父类对象的引用（继承中指代了父类对象空间）：super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this(...) 借用本类其他构造器，super(...) 调用父类的构造器&lt;/li&gt;
&lt;li&gt;this(...) 或 super(...) 必须放在构造器的第一行，否则报错&lt;/li&gt;
&lt;li&gt;this(...) 和 super(...) &lt;strong&gt;不能同时出现&lt;/strong&gt;在构造器中，因为构造函数必须出现在第一行上，只能选择一个&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThisDemo {
    public static void main(String[] args) {
        // 需求：希望如果不写学校默认就是”张三“！
        Student s1 = new Student(&quot;天蓬元帅&quot;, 1000 );
        Student s2 = new Student(&quot;齐天大圣&quot;, 2000, &quot;清华大学&quot; );
    }
}
class Study extends Student {
   public Study(String name, int age, String schoolName) {
        super(name , age , schoolName) ; 
       // 根据参数匹配调用父类构造器
   }
}

class Student{
    private String name ;
    private int age ;
    private String schoolName ;

    public Student() {
    }
    public Student(String name , int age){
        // 借用兄弟构造器的功能！
        this(name , age , &quot;张三&quot;);
    }
	public Student(String name, int age, String schoolName) {
        this.name = name;
        this.age = age;
        this.schoolName = schoolName;
    }
	// .......get + set
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;final&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;final 用于修饰：类，方法，变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;final 修饰类，类不能被继承了，类中的方法和变量可以使用&lt;/li&gt;
&lt;li&gt;final 可以修饰方法，方法就不能被重写&lt;/li&gt;
&lt;li&gt;final 修饰变量总规则：变量有且仅能被赋值一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;final 和 abstract 的关系是&lt;strong&gt;互斥关系&lt;/strong&gt;，不能同时修饰类或者同时修饰方法&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;修饰变量&lt;/h4&gt;
&lt;h5&gt;静态变量&lt;/h5&gt;
&lt;p&gt;final 修饰静态成员变量，变量变成了常量&lt;/p&gt;
&lt;p&gt;常量：有 public static final 修饰，名称字母全部大写，多个单词用下划线连接&lt;/p&gt;
&lt;p&gt;final 修饰静态成员变量可以在哪些地方赋值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义的时候赋值一次&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在静态代码块中赋值一次&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class FinalDemo {
	//常量：public static final修饰，名称字母全部大写，下划线连接。
    public static final String SCHOOL_NAME = &quot;张三&quot; ;
    public static final String SCHOOL_NAME1;

    static{
        //SCHOOL_NAME = &quot;java&quot;;//报错
        SCHOOL_NAME1 = &quot;张三1&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实例变量&lt;/h5&gt;
&lt;p&gt;final 修饰变量的总规则：有且仅能被赋值一次&lt;/p&gt;
&lt;p&gt;final 修饰实例成员变量可以在哪些地方赋值 1 次：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义的时候赋值一次&lt;/li&gt;
&lt;li&gt;可以在实例代码块中赋值一次&lt;/li&gt;
&lt;li&gt;可以在每个构造器中赋值一次&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class FinalDemo {
    private final String name = &quot;张三&quot; ;
    private final String name1;
    private final String name2;
    {
        // 可以在实例代码块中赋值一次。
        name1 = &quot;张三1&quot;;
    }
	//构造器赋值一次
    public FinalDemo(){
        name2 = &quot;张三2&quot;;
    }
    public FinalDemo(String a){
        name2 = &quot;张三2&quot;;
    }

    public static void main(String[] args) {
        FinalDemo f1 = new FinalDemo();
        //f1.name = &quot;张三1&quot;; // 第二次赋值 报错！
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;抽象类&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;父类知道子类要完成某个功能，但是每个子类实现情况不一样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;抽象方法：没有方法体，只有方法签名，必须用 abstract 修饰的方法就是抽象方法&lt;/p&gt;
&lt;p&gt;抽象类：拥有抽象方法的类必须定义成抽象类，必须用 abstract 修饰，&lt;strong&gt;抽象类是为了被继承&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个类继承抽象类，&lt;strong&gt;必须重写抽象类的全部抽象方法&lt;/strong&gt;，否则这个类必须定义成抽象类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AbstractDemo {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.run();
    }
}

class Dog extends Animal{
    @Override
    public void run() { 
        System.out.println(&quot;🐕跑&quot;); 
    }
}

abstract class Animal{
    public abstract void run();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常见问题&lt;/h4&gt;
&lt;p&gt;一、抽象类是否有构造器，是否可以创建对象?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象类有构造器，但是抽象类不能创建对象，类的其他成分它都具备，构造器提供给子类继承后调用父类构造器使用&lt;/li&gt;
&lt;li&gt;抽象类中存在抽象方法，但不能执行，&lt;strong&gt;抽象类中也可没有抽象方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象在学术上本身意味着不能实例化&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class AbstractDemo {
    public static void main(String[] args) {
        //Animal a = new Animal(); 抽象类不能创建对象！
        //a.run(); // 抽象方法不能执行
    }
}
abstract class Animal{
    private String name;
    public static String schoolName = &quot;张三&quot;;
    public Animal(){ }

    public abstract void run();
    //普通方法
    public void go(){ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二、static 与 abstract 能同时使用吗？&lt;/p&gt;
&lt;p&gt;答：不能，被 static 修饰的方法属于类，是类自己的东西，不是给子类来继承的，而抽象方法本身没有实现，就是用来给子类继承&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;存在意义&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;被继承&lt;/strong&gt;，抽象类就是为了被子类继承，否则抽象类将毫无意义（核心）&lt;/p&gt;
&lt;p&gt;抽象类体现的是&quot;模板思想&quot;：&lt;strong&gt;部分实现，部分抽象&lt;/strong&gt;，可以使用抽象类设计一个模板模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//作文模板
public class ExtendsDemo {
    public static void main(String[] args) {
        Student xiaoMa = new Student();
        xiaoMa.write();
    }
}
class Student extends Template{
    @Override
    public String writeText() {return &quot;\t内容&quot;}
}
// 1.写一个模板类：代表了作文模板。
abstract class Template{
    private String title = &quot;\t\t\t\t\t标题&quot;;
    private String start = &quot;\t开头&quot;;
    private String last = &quot;\t结尾&quot;;
    public void write(){
        System.out.println(title+&quot;\n&quot;+start);
        System.out.println(writeText());
        System.out.println(last);
    }
    // 正文部分定义成抽象方法，交给子类重写！！
    public abstract String writeText();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;接口&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;接口是 Java 语言中一种引用类型，是方法的集合。&lt;/p&gt;
&lt;p&gt;接口是更加彻底的抽象，接口中只有抽象方法和常量，没有其他成分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 修饰符 interface 接口名称{
	// 抽象方法
	// 默认方法
	// 静态方法
	// 私有方法
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;抽象方法：接口中的抽象方法默认会加上 public abstract 修饰，所以可以省略不写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态方法：静态方法必须有方法体&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量：是 public static final 修饰的成员变量，仅能被赋值一次，值不能改变。常量的名称规范上要求全部大写，多个单词下划线连接，public static final 可以省略不写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface InterfaceDemo{
    //public static final String SCHOOL_NAME = &quot;张三&quot;;
	String SCHOOL_NAME = &quot;张三&quot;;
    
    //public abstract void run();
    void run();//默认补充
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;实现接口&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;接口是用来被类实现的。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类与类是继承关系：一个类只能直接继承一个父类，单继承&lt;/li&gt;
&lt;li&gt;类与接口是实现关系：一个类可以实现多个接口，多实现，接口不能继承类&lt;/li&gt;
&lt;li&gt;接口与接口继承关系：&lt;strong&gt;多继承&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{

}
修饰符 interface 接口名 extend 接口1,接口2,接口3,....{
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现多个接口的使用注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当一个类实现多个接口时，多个接口中存在同名的静态方法并不会冲突，只能通过各自接口名访问静态方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个类实现多个接口时，多个接口中存在同名的默认方法，实现类必须重写这个方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个类既继承一个父类，又实现若干个接口时，父类中成员方法与接口中默认方法重名，子类&lt;strong&gt;就近选择执行父类&lt;/strong&gt;的成员方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口中，没有构造器，&lt;strong&gt;不能创建对象&lt;/strong&gt;，接口是更彻底的抽象，连构造器都没有，自然不能创建对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InterfaceDemo {
    public static void main(String[] args) {
        Student s = new Student();
        s.run();
        s.rule();
    }
}
class Student implements Food, Person{
    @Override
    public void eat() {}
    
    @Override
    public void run() {}
}
interface Food{
    void eat();
}
interface Person{
    void run();
}
//可以直接 interface Person extend Food,
//然后 class Student implements Person 效果一样
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;新增功能&lt;/h4&gt;
&lt;p&gt;jdk1.8 以后新增的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认方法（就是普通实例方法）
&lt;ul&gt;
&lt;li&gt;必须用 default 修饰，默认会 public 修饰&lt;/li&gt;
&lt;li&gt;必须用接口的实现类的对象来调用&lt;/li&gt;
&lt;li&gt;必须有默认实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态方法
&lt;ul&gt;
&lt;li&gt;默认会 public 修饰&lt;/li&gt;
&lt;li&gt;接口的静态方法必须用接口的类名本身来调用&lt;/li&gt;
&lt;li&gt;调用格式：ClassName.method()&lt;/li&gt;
&lt;li&gt;必须有默认实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;私有方法：JDK 1.9 才开始有的，只能在&lt;strong&gt;本类中&lt;/strong&gt;被其他的默认方法或者私有方法访问&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class InterfaceDemo {
    public static void main(String[] args) {
        // 1.默认方法调用：必须用接口的实现类对象调用。
        Man m = new Man();
        m.run();
        m.work();

        // 2.接口的静态方法必须用接口的类名本身来调用。
        InterfaceJDK8.inAddr();
    }
}
class Man implements InterfaceJDK8 {
    @Override
    public void work() {
        System.out.println(&quot;工作中。。。&quot;);
    }
}

interface InterfaceJDK8 {
    //抽象方法！！
    void work();
    // a.默认方法（就是之前写的普通实例方法）
    // 必须用接口的实现类的对象来调用。
    default void run() {
        go();
        System.out.println(&quot;开始跑步🏃‍&quot;);
    }

    // b.静态方法
    // 注意：接口的静态方法必须用接口的类名本身来调用
    static void inAddr() {
        System.out.println(&quot;我们在武汉&quot;);
    }
    
    // c.私有方法（就是私有的实例方法）: JDK 1.9才开始有的。
    // 只能在本接口中被其他的默认方法或者私有方法访问。
    private void go() {
        System.out.println(&quot;开始。。&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;对比抽象类&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;参数&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;抽象类&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;接口&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;默认的方法实现&lt;/td&gt;
&lt;td&gt;可以有默认的方法实现&lt;/td&gt;
&lt;td&gt;接口完全是抽象的，jdk8 以后有默认的实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现&lt;/td&gt;
&lt;td&gt;子类使用 &lt;strong&gt;extends&lt;/strong&gt; 关键字来继承抽象类。如果子类不是抽象类的话，它需要提供抽象类中所有声明的方法的实现。&lt;/td&gt;
&lt;td&gt;子类使用关键字 &lt;strong&gt;implements&lt;/strong&gt; 来实现接口。它需要提供接口中所有声明的方法的实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构造器&lt;/td&gt;
&lt;td&gt;抽象类可以有构造器&lt;/td&gt;
&lt;td&gt;接口不能有构造器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;与正常Java类的区别&lt;/td&gt;
&lt;td&gt;除了不能实例化抽象类之外，和普通 Java 类没有任何区别&lt;/td&gt;
&lt;td&gt;接口是完全不同的类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问修饰符&lt;/td&gt;
&lt;td&gt;抽象方法有 &lt;strong&gt;public&lt;/strong&gt;、&lt;strong&gt;protected&lt;/strong&gt; 和 &lt;strong&gt;default&lt;/strong&gt; 这些修饰符&lt;/td&gt;
&lt;td&gt;接口默认修饰符是 &lt;strong&gt;public&lt;/strong&gt;，别的修饰符需要有方法体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;main方法&lt;/td&gt;
&lt;td&gt;抽象方法可以有 main 方法并且我们可以运行它&lt;/td&gt;
&lt;td&gt;jdk8 以前接口没有 main 方法，不能运行；jdk8 以后接口可以有 default 和 static 方法，可以运行 main 方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多继承&lt;/td&gt;
&lt;td&gt;抽象方法可以继承一个类和实现多个接口&lt;/td&gt;
&lt;td&gt;接口可以继承一个或多个其它接口，接口不可继承类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;比接口速度要快&lt;/td&gt;
&lt;td&gt;接口是稍微有点慢的，因为它需要时间去寻找在类中实现的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加新方法&lt;/td&gt;
&lt;td&gt;如果往抽象类中添加新的方法，可以给它提供默认的实现，因此不需要改变现在的代码&lt;/td&gt;
&lt;td&gt;如果往接口中添加方法，那么必须改变实现该接口的类&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;多态&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;多态的概念：同一个实体同时具有多种形式同一个类型的对象，执行同一个行为，在不同的状态下会表现出不同的行为特征&lt;/p&gt;
&lt;p&gt;多态的格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父类类型范围 &amp;gt; 子类类型范围&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;父类类型 对象名称 = new 子类构造器;
接口	  对象名称 = new 实现类构造器;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多态的执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于方法的调用：&lt;strong&gt;编译看左边，运行看右边&lt;/strong&gt;（分派机制）&lt;/li&gt;
&lt;li&gt;对于变量的调用：&lt;strong&gt;编译看左边，运行看左边&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多态的使用规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须存在继承或者实现关系&lt;/li&gt;
&lt;li&gt;必须存在父类类型的变量引用子类类型的对象&lt;/li&gt;
&lt;li&gt;存在方法重写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多态的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在多态形式下，右边对象可以实现组件化切换，便于扩展和维护，也可以实现类与类之间的&lt;strong&gt;解耦&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;父类类型作为方法形式参数，传递子类对象给方法，可以传入一切子类对象进行方法的调用，更能体现出多态的&lt;strong&gt;扩展性&lt;/strong&gt;与便利性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多态的劣势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多态形式下，不能直接调用子类特有的功能，因为编译看左边，父类中没有子类独有的功能，所以代码在编译阶段就直接报错了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class PolymorphicDemo {
    public static void main(String[] args) {
        Animal c = new Cat();
        c.run();
        //c.eat();//报错  编译看左边 需要强转
        go(c);
        go(new Dog);   
    }
    //用 Dog或者Cat 都没办法让所有动物参与进来，只能用Anima
    public static void go(Animal d){}
    
}
class Dog extends Animal{}

class Cat extends Animal{
    public void eat();
    @Override
    public void run(){}
}

class Animal{
    public void run(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;上下转型&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;基本数据类型的转换：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;小范围类型的变量或者值可以直接赋值给大范围类型的变量&lt;/li&gt;
&lt;li&gt;大范围类型的变量或者值必须强制类型转换给小范围类型的变量&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用数据类型的&lt;strong&gt;自动&lt;/strong&gt;类型转换语法：子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;父类引用指向子类对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向上转型 (upcasting)&lt;/strong&gt;：通过子类对象（小范围）实例化父类对象（大范围），这种属于自动转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向下转型 (downcasting)&lt;/strong&gt;：通过父类对象（大范围）实例化子类对象（小范围），这种属于强制转换&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class PolymorphicDemo {
    public static void main(String[] args){
        Animal a = new Cat();	// 向上转型
        Cat c = (Cat)a;			// 向下转型
    }
}
class Animal{}
class Cat extends Animal{}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;instanceof&lt;/h4&gt;
&lt;p&gt;instanceof：判断左边的对象是否是右边的类的实例，或者是其直接或间接子类，或者是其接口的实现类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用类型强制类型转换：父类类型的变量或者对象强制类型转换成子类类型的变量，否则报错&lt;/li&gt;
&lt;li&gt;强制类型转换的格式：&lt;strong&gt;类型 变量名称 = (类型)(对象或者变量)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;有继承/实现关系的两个类型就可以进行强制类型转换，编译阶段一定不报错，但是运行阶段可能出现类型转换异常 ClassCastException&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Demo{
    public static void main(String[] args){
		Aniaml a = new Dog();
		//Dog d = (Dog)a;
        //Cat c = (Cat)a; 编译不报错，运行报ClassCastException错误
        if(a instanceof Cat){
            Cat c = (Cat)a; 
        } else if(a instanceof Dog) {
            Dog d = (Dog)a;
        }
    }
}
class Dog extends Animal{}
class Cat extends Animal{}
class Animal{}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;内部类&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;内部类是类的五大成分之一：成员变量，方法，构造器，代码块，内部类&lt;/p&gt;
&lt;p&gt;概念：定义在一个类里面的类就是内部类&lt;/p&gt;
&lt;p&gt;作用：提供更好的封装性，体现出组件思想，&lt;strong&gt;间接解决类无法多继承引起的一系列问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分类：静态内部类、实例内部类（成员内部类）、局部内部类、&lt;strong&gt;匿名内部类&lt;/strong&gt;（重点）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;静态内部类&lt;/h4&gt;
&lt;p&gt;定义：有 static 修饰，属于外部类本身，会加载一次&lt;/p&gt;
&lt;p&gt;静态内部类中的成分研究：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类有的成分它都有，静态内部类属于外部类本身，只会加载一次&lt;/li&gt;
&lt;li&gt;特点与外部类是完全一样的，只是位置在别人里面&lt;/li&gt;
&lt;li&gt;可以定义静态成员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态内部类的访问格式：外部类名称.内部类名称&lt;/p&gt;
&lt;p&gt;静态内部类创建对象的格式：外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器&lt;/p&gt;
&lt;p&gt;静态内部类的访问拓展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态内部类中是否可以直接访问外部类的静态成员?	可以，外部类的静态成员只有一份，可以被共享&lt;/li&gt;
&lt;li&gt;静态内部类中是否可以直接访问外部类的实例成员?	不可以，外部类的成员必须用外部类对象访问&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Demo{
    public static void main(String[] args){
        Outter.Inner in = new Outter.Inner();
    }
}

static class Outter{
    public static int age;
    private double salary;
    public static class Inner{
         //拥有类的所有功能 构造器 方法 成员变量
         System.out.println(age);
         //System.out.println(salary);报错
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实例内部类&lt;/h4&gt;
&lt;p&gt;定义：无 static 修饰的内部类，属于外部类的每个对象，跟着外部类对象一起加载&lt;/p&gt;
&lt;p&gt;实例内部类的成分特点：实例内部类中不能定义静态成员，其他都可以定义&lt;/p&gt;
&lt;p&gt;实例内部类的访问格式：外部类名称.内部类名称&lt;/p&gt;
&lt;p&gt;创建对象的格式：外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Outter.Inner in = new Outter().new Inner()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实例内部类可以访问外部类的全部成员&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例内部类中可以直接访问外部类的静态成员，外部类的静态成员可以被共享访问&lt;/li&gt;
&lt;li&gt;实例内部类中可以访问外部类的实例成员，实例内部类属于外部类对象，可以直接访问外部类对象的实例成员&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;局部内部类&lt;/h4&gt;
&lt;p&gt;局部内部类：定义在方法中，在构造器中，代码块中，for 循环中定义的内部类&lt;/p&gt;
&lt;p&gt;局部内部类中的成分特点：只能定义实例成员，不能定义静态成员&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InnerClass{
	public static void main(String[] args){
        String name;
        class{}
    }
    public static void test(){
		class Animal{}
		class Cat extends Animal{}  
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;匿名内部类&lt;/h4&gt;
&lt;p&gt;匿名内部类：没有名字的局部内部类&lt;/p&gt;
&lt;p&gt;匿名内部类的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new 类名|抽象类|接口(形参){
	//方法重写。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;匿名内部类的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;匿名内部类不能定义静态成员&lt;/li&gt;
&lt;li&gt;匿名内部类一旦写出来，就会立即创建一个匿名内部类的对象返回&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;匿名内部类引用局部变量必须是&lt;strong&gt;常量&lt;/strong&gt;，底层创建为内部类的成员变量（原因：JVM → 运行机制 → 代码优化）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Anonymity {
    public static void main(String[] args) {
        Animal a = new Animal(){
            @Override
            public void run() {
                System.out.println(&quot;猫跑的贼溜~~&quot;);
                //System.out.println(n);
            }
        };
        a.run();
        a.go();
    }
}
abstract class Animal{
    public abstract void run();

    public void go(){
        System.out.println(&quot;开始go~~~&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;权限符&lt;/h3&gt;
&lt;p&gt;权限修饰符：有四种**（private -&amp;gt; 缺省 -&amp;gt; protected - &amp;gt; public ）**
可以修饰成员变量，修饰方法，修饰构造器，内部类，不同修饰符修饰的成员能够被访问的权限将受到限制&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;四种修饰符访问权限&lt;/th&gt;
&lt;th&gt;private&lt;/th&gt;
&lt;th&gt;缺省&lt;/th&gt;
&lt;th&gt;protected&lt;/th&gt;
&lt;th&gt;public&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;本类中&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本包下的子类中&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本包下其他类中&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其他包下的子类中&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其他包下的其他类中&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;protected 用于修饰成员，表示在继承体系中成员对于子类可见&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基类的 protected 成员是包内可见的，并且对子类可见&lt;/li&gt;
&lt;li&gt;若子类与基类不在同一包中，那么子类实例可以访问其从基类继承而来的 protected 方法（重写），而不能访问基类实例的 protected 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;h4&gt;静态代码块&lt;/h4&gt;
&lt;p&gt;静态代码块的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;静态代码块特点：
&lt;ul&gt;
&lt;li&gt;必须有 static 修饰，只能访问静态资源&lt;/li&gt;
&lt;li&gt;会与类一起优先加载，且自动触发执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态代码块作用：
&lt;ul&gt;
&lt;li&gt;可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先执行静态代码块，在执行 main 函数里的操作&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class CodeDemo {
    public static String schoolName ;
    public static ArrayList&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();

    // 静态代码块,属于类，与类一起加载一次!
    static {
        System.out.println(&quot;静态代码块被触发执行~~~~~~~&quot;);
        // 在静态代码块中进行静态资源的初始化操作
        schoolName = &quot;张三&quot;;
        lists.add(&quot;3&quot;);
        lists.add(&quot;4&quot;);
        lists.add(&quot;5&quot;);
    }
    public static void main(String[] args) {
        System.out.println(&quot;main方法被执行&quot;);
        System.out.println(schoolName);
        System.out.println(lists);
    }
}
/*静态代码块被触发执行~~~~~~~
main方法被执行
张三
[3, 4, 5] */
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实例代码块&lt;/h4&gt;
&lt;p&gt;实例代码块的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例代码块的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无 static 修饰，属于对象&lt;/li&gt;
&lt;li&gt;会与类的对象一起加载，每次创建类的对象的时候，实例代码块都会被加载且自动触发执行一次&lt;/li&gt;
&lt;li&gt;实例代码块的代码在底层实际上是提取到每个构造器中去执行的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例代码块的作用：实例代码块可以在创建对象之前进行实例资源的初始化操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class CodeDemo {
    private String name;
    private ArrayList&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();
    {
        name = &quot;代码块&quot;;
        lists.add(&quot;java&quot;);
        System.out.println(&quot;实例代码块被触发执行一次~~~~~~~~&quot;);
    }
    public CodeDemo02(){ }//构造方法
    public CodeDemo02(String name){}

    public static void main(String[] args) {
        CodeDemo c = new CodeDemo();//实例代码块被触发执行一次
        System.out.println(c.name);
        System.out.println(c.lists);
        new CodeDemo02();//实例代码块被触发执行一次
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;API&lt;/h2&gt;
&lt;h3&gt;Object&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Object 类是 Java 中的祖宗类，一个类或者默认继承 Object 类，或者间接继承 Object 类，Object 类的方法是一切子类都可以直接使用&lt;/p&gt;
&lt;p&gt;Object 类常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public String toString()&lt;/code&gt;：默认是返回当前对象在堆内存中的地址信息：类的全限名@内存地址，例：Student@735b478；
&lt;ul&gt;
&lt;li&gt;直接输出对象名称，默认会调用 toString() 方法，所以省略 toString() 不写；&lt;/li&gt;
&lt;li&gt;如果输出对象的内容，需要重写 toString() 方法，toString 方法存在的意义是为了被子类重写&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean equals(Object o)&lt;/code&gt;：默认是比较两个对象的引用是否相同&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected Object clone()&lt;/code&gt;：创建并返回此对象的副本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只要两个对象的内容一样，就认为是相等的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object o) {
	// 1.判断是否自己和自己比较，如果是同一个对象比较直接返回true
	if (this == o) return true;
	// 2.判断被比较者是否为null ,以及是否是学生类型。
	if (o == null || this.getClass() != o.getClass()) return false;
	// 3.o一定是学生类型，强制转换成学生，开始比较内容！
	Student student = (Student) o;
	return age == student.age &amp;amp;&amp;amp;
           sex == student.sex &amp;amp;&amp;amp;
           Objects.equals(name, student.name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;面试题&lt;/strong&gt;：== 和 equals 的区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;== 比较的是变量（栈）内存中存放的对象的（堆）内存地址，用来判断两个对象的&lt;strong&gt;地址&lt;/strong&gt;是否相同，即是否是指相同一个对象，比较的是真正意义上的指针操作&lt;/li&gt;
&lt;li&gt;Object 类中的方法，&lt;strong&gt;默认比较两个对象的引用&lt;/strong&gt;，重写 equals 方法比较的是两个对象的&lt;strong&gt;内容&lt;/strong&gt;是否相等，所有的类都是继承自 java.lang.Object 类，所以适用于所有对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;hashCode 的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hashCode 的存在主要是用于查找的快捷性，如 Hashtable，HashMap 等，可以在散列存储结构中确定对象的存储地址&lt;/li&gt;
&lt;li&gt;如果两个对象相同，就是适用于 equals(java.lang.Object) 方法，那么这两个对象的 hashCode 一定要相同&lt;/li&gt;
&lt;li&gt;哈希值相同的数据不一定内容相同，内容相同的数据哈希值一定相同&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;深浅克隆&lt;/h4&gt;
&lt;p&gt;Object 的 clone() 是 protected 方法，一个类不显式去重写 clone()，就不能直接去调用该类实例的 clone() 方法&lt;/p&gt;
&lt;p&gt;深浅拷贝（克隆）的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;浅拷贝 (shallowCopy)：&lt;strong&gt;对基本数据类型进行值传递，对引用数据类型只是复制了引用&lt;/strong&gt;，被复制对象属性的所有的引用仍然指向原来的对象，简而言之就是增加了一个指针指向原来对象的内存地址&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 中的复制方法基本都是浅拷贝&lt;/strong&gt;：Object.clone()、System.arraycopy()、Arrays.copyOf()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深拷贝 (deepCopy)：对基本数据类型进行值传递，对引用数据类型是一个整个独立的对象拷贝，会拷贝所有的属性并指向的动态分配的内存，简而言之就是把所有属性复制到一个新的内存，增加一个指针指向新内存。所以使用深拷贝的情况下，释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cloneable 接口是一个标识性接口，即该接口不包含任何方法（包括 clone），但是如果一个类想合法的进行克隆，那么就必须实现这个接口，在使用 clone() 方法时，若该类未实现 Cloneable 接口，则抛出异常&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Clone &amp;amp; Copy：&lt;code&gt;Student s = new Student&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Student s1 = s&lt;/code&gt;：只是 copy 了一下 reference，s 和 s1 指向内存中同一个 Object，对对象的修改会影响对方&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Student s2 = s.clone()&lt;/code&gt;：会生成一个新的 Student 对象，并且和 s 具有相同的属性值和方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shallow Clone &amp;amp; Deep Clone：&lt;/p&gt;
&lt;p&gt;浅克隆：Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是&lt;strong&gt;不可以被改变的对象&lt;/strong&gt;，所以在使用克隆时可以视为基本类型，只需浅克隆引用即可&lt;/li&gt;
&lt;li&gt;如果对一个引用类型进行克隆时只是克隆了它的引用，和原始对象共享对象成员变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Object%E6%B5%85%E5%85%8B%E9%9A%86.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;深克隆：在对整个对象浅克隆后，对其引用变量进行克隆，并将其更新到浅克隆对象中去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Student  implements Cloneable{
    private String name;
    private Integer age;
    private Date date;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student s = (Student) super.clone();
        s.date = (Date) date.clone();
        return s;
    }
    //.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SDP → 创建型 → 原型模式&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Objects&lt;/h3&gt;
&lt;p&gt;Objects 类与 Object 是继承关系&lt;/p&gt;
&lt;p&gt;Objects 的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static boolean equals(Object a, Object b)&lt;/code&gt;：比较两个对象是否相同&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static boolean equals(Object a, Object b) {
  // 进行非空判断，从而可以避免空指针异常
  return a == b || a != null &amp;amp;&amp;amp; a.equals(b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static boolean isNull(Object obj)&lt;/code&gt;：判断变量是否为 null ，为 null 返回 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static String toString(对象)&lt;/code&gt;：返回参数中对象的字符串表示形式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static String toString(对象, 默认字符串)&lt;/code&gt;：返回对象的字符串表示形式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ObjectsDemo {
    public static void main(String[] args) {
        Student s1 = null;
        Student s2 = new Student();
        System.out.println(Objects.equals(s1 , s2));//推荐使用
        // System.out.println(s1.equals(s2)); // 空指针异常
 
        System.out.println(Objects.isNull(s1));
        System.out.println(s1 == null);//直接判断比较好
    }
}

public class Student {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;String&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;String 被声明为 final，因此不可被继承 &lt;strong&gt;（Integer 等包装类也不能被继承）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class String implements java.io.Serializable, Comparable&amp;lt;String&amp;gt;, CharSequence {
 	/** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Java 9 之后，String 类的实现改用 byte 数组存储字符串，同时使用 &lt;code&gt;coder&lt;/code&gt; 来标识使用了哪种编码&lt;/p&gt;
&lt;p&gt;value 数组被声明为 final，这意味着 value 数组初始化之后就不能再引用其它数组，并且 String 内部没有改变 value 数组的方法，因此可以&lt;strong&gt;保证 String 不可变，也保证线程安全&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意：不能改变的意思是&lt;strong&gt;每次更改字符串都会产生新的对象&lt;/strong&gt;，并不是对原始对象进行改变&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;abc&quot;;
s = s + &quot;cd&quot;; //s = abccd 新对象
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常用方法&lt;/h4&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public boolean equals(String s)&lt;/code&gt;：比较两个字符串内容是否相同、区分大小写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public boolean equalsIgnoreCase(String anotherString)&lt;/code&gt;：比较字符串的内容，忽略大小写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public int length()&lt;/code&gt;：返回此字符串的长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String trim()&lt;/code&gt;：返回一个字符串，其值为此字符串，并删除任何前导和尾随空格&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String[] split(String regex)&lt;/code&gt;：将字符串按给定的正则表达式分割成字符串数组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public char charAt(int index)&lt;/code&gt;：取索引处的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public char[] toCharArray()&lt;/code&gt;：将字符串拆分为字符数组后返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public boolean startsWith(String prefix)&lt;/code&gt;：测试此字符串是否以指定的前缀开头&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public int indexOf(String str)&lt;/code&gt;：返回指定子字符串第一次出现的字符串内的索引，没有返回 -1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public int lastIndexOf(String str)&lt;/code&gt;：返回字符串最后一次出现 str 的索引，没有返回 -1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String substring(int beginIndex)&lt;/code&gt;：返回子字符串，以原字符串指定索引处到结尾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String substring(int i, int j)&lt;/code&gt;：指定索引处扩展到 j - 1 的位置，字符串长度为 j - i&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String toLowerCase()&lt;/code&gt;：将此 String 所有字符转换为小写，使用默认语言环境的规则&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String toUpperCase()&lt;/code&gt;：使用默认语言环境的规则将此 String 所有字符转换为大写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public String replace(CharSequence target, CharSequence replacement)&lt;/code&gt;：使用新值，将字符串中的旧值替换，得到新的字符串&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;String s = 123-78;
s.replace(&quot;-&quot;,&quot;&quot;);//12378
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;构造方式&lt;/h4&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public String()&lt;/code&gt;：创建一个空白字符串对象，不含有任何内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String(char[] chs)&lt;/code&gt;：根据字符数组的内容，来创建字符串对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String(String original)&lt;/code&gt;：根据传入的字符串内容，来创建字符串对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接赋值：&lt;code&gt;String s = &quot;abc&quot;&lt;/code&gt; 直接赋值的方式创建字符串对象，内容就是 abc&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过构造方法创建：通过 new 创建的字符串对象，每一次 new 都会申请一个内存空间，虽然内容相同，但是地址值不同，&lt;strong&gt;返回堆内存中对象的引用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;直接赋值方式创建：以 &lt;code&gt;&quot; &quot;&lt;/code&gt; 方式给出的字符串，只要字符序列相同（顺序和大小写），无论在程序代码中出现几次，JVM 都只会&lt;strong&gt;在 String Pool 中创建一个字符串对象&lt;/strong&gt;，并在字符串池中维护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;String str = new String(&quot;abc&quot;)&lt;/code&gt; 创建字符串对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个对象：字符串池中已经存在 abc 对象，那么直接在创建一个对象放入堆中，返回堆内引用&lt;/li&gt;
&lt;li&gt;创建两个对象：字符串池中未找到 abc 对象，那么分别在堆中和字符串池中创建一个对象，字符串池中的比较都是采用 equals()
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/String构造方法字节码.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;new String(&quot;a&quot;) + new String(&quot;b&quot;)&lt;/code&gt; 创建字符串对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象 1：new StringBuilder()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象 2：new String(&quot;a&quot;)、对象 3：常量池中的 a&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象 4：new String(&quot;b&quot;)、对象 5：常量池中的 b
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/String拼接方法字节码.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StringBuilder 的 toString()：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Override
public String toString() {
    return new String(value, 0, count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对象 6：new String(&quot;ab&quot;)&lt;/li&gt;
&lt;li&gt;StringBuilder 的 toString() 调用，&lt;strong&gt;在字符串常量池中没有生成 ab&lt;/strong&gt;，new String(&quot;ab&quot;) 会创建两个对象因为传参数的时候使用字面量创建了对象 ab，当使用数组构造 String 对象时，没有加入常量池的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;String Pool&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;字符串常量池（String Pool / StringTable / 串池）保存着所有字符串字面量（literal strings），这些字面量在编译时期就确定，常量池类似于 Java 系统级别提供的&lt;strong&gt;缓存&lt;/strong&gt;，存放对象和引用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;StringTable，类似 HashTable 结构，通过 &lt;code&gt;-XX:StringTableSize&lt;/code&gt; 设置大小，JDK 1.8 中默认 60013&lt;/li&gt;
&lt;li&gt;常量池中的字符串仅是符号，第一次使用时才变为对象，可以避免重复创建字符串对象&lt;/li&gt;
&lt;li&gt;字符串&lt;strong&gt;变量&lt;/strong&gt;的拼接的原理是 StringBuilder#append，append 方法比字符串拼接效率高（JDK 1.8）&lt;/li&gt;
&lt;li&gt;字符串&lt;strong&gt;常量&lt;/strong&gt;拼接的原理是编译期优化，拼接结果放入常量池&lt;/li&gt;
&lt;li&gt;可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;intern()&lt;/h5&gt;
&lt;p&gt;JDK 1.8：当一个字符串调用 intern() 方法时，如果 String Pool 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存在一个字符串和该字符串值相等，就会返回 String Pool 中字符串的引用（需要变量接收）&lt;/li&gt;
&lt;li&gt;不存在，会把对象的&lt;strong&gt;引用地址&lt;/strong&gt;复制一份放入串池，并返回串池中的引用地址，前提是堆内存有该对象，因为 Pool 在堆中，为了节省内存不再创建新对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK 1.6：将这个字符串对象尝试放入串池，如果有就不放入，返回已有的串池中的对象的引用；如果没有会把此对象复制一份，放入串池，把串池中的对象返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    // 常量池中的信息都加载到运行时常量池，这时a b ab是常量池中的符号，还不是java字符串对象，是懒惰的
    // ldc #2 会把 a 符号变为 &quot;a&quot; 字符串对象     ldc:反编译后的指令
    // ldc #3 会把 b 符号变为 &quot;b&quot; 字符串对象
    // ldc #4 会把 ab 符号变为 &quot;ab&quot; 字符串对象
    public static void main(String[] args) {
        String s1 = &quot;a&quot;; 	// 懒惰的
        String s2 = &quot;b&quot;;
        String s3 = &quot;ab&quot;;	// 串池
        
        String s4 = s1 + s2;	// 返回的是堆内地址
        // 原理：new StringBuilder().append(&quot;a&quot;).append(&quot;b&quot;).toString()  new String(&quot;ab&quot;)
        
        String s5 = &quot;a&quot; + &quot;b&quot;;  // javac 在编译期间的优化，结果已经在编译期确定为ab

        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true

        String x2 = new String(&quot;c&quot;) + new String(&quot;d&quot;); // new String(&quot;cd&quot;)
        // 虽然 new，但是在字符串常量池没有 cd 对象，因为 toString() 方法
        x2.intern();
        String x1 = &quot;cd&quot;;

        System.out.println(x1 == x2); //true
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;== 比较基本数据类型：比较的是具体的值&lt;/li&gt;
&lt;li&gt;== 比较引用数据类型：比较的是对象地址值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s1 = &quot;ab&quot;;								// 仅放入串池
String s2 = new String(&quot;a&quot;) + new String(&quot;b&quot;);	// 仅放入堆
// 上面两条指令的结果和下面的 效果 相同
String s = new String(&quot;ab&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;常见问题&lt;/h5&gt;
&lt;p&gt;问题一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    String s = new String(&quot;a&quot;) + new String(&quot;b&quot;);//new String(&quot;ab&quot;)
    //在上一行代码执行完以后，字符串常量池中并没有&quot;ab&quot;

    String s2 = s.intern();
    //jdk6：串池中创建一个字符串&quot;ab&quot;
    //jdk8：串池中没有创建字符串&quot;ab&quot;,而是创建一个引用指向 new String(&quot;ab&quot;)，将此引用返回

    System.out.println(s2 == &quot;ab&quot;);//jdk6:true  jdk8:true
    System.out.println(s == &quot;ab&quot;);//jdk6:false  jdk8:true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题二：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    String str1 = new StringBuilder(&quot;58&quot;).append(&quot;tongcheng&quot;).toString();
    System.out.println(str1 == str1.intern());//true，字符串池中不存在，把堆中的引用复制一份放入串池

    String str2 = new StringBuilder(&quot;ja&quot;).append(&quot;va&quot;).toString();
    System.out.println(str2 == str2.intern());//false，字符串池中存在，直接返回已经存在的引用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;System 类当调用 Version 的静态方法，导致 Version 初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void initializeSystemClass() {
    sun.misc.Version.init();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Version 类初始化时需要对静态常量字段初始化，被 launcher_name 静态常量字段所引用的 &lt;code&gt;&quot;java&quot;&lt;/code&gt; 字符串字面量就被放入的字符串常量池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.misc;

public class Version {
    private static final String launcher_name = &quot;java&quot;;
    private static final String java_version = &quot;1.8.0_221&quot;;
    private static final String java_runtime_name = &quot;Java(TM) SE Runtime Environment&quot;;
    private static final String java_profile_name = &quot;&quot;;
    private static final String java_runtime_version = &quot;1.8.0_221-b11&quot;;
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;内存位置&lt;/h5&gt;
&lt;p&gt;Java 7 之前，String Pool 被放在运行时常量池中，属于永久代；Java 7 以后，String Pool 被移到堆中，这是因为永久代的空间有限，在大量使用字符串的场景下会导致 OutOfMemoryError 错误&lt;/p&gt;
&lt;p&gt;演示 StringTable 位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-Xmx10m&lt;/code&gt; 设置堆内存 10m&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 JDK8 下设置： &lt;code&gt;-Xmx10m -XX:-UseGCOverheadLimit&lt;/code&gt;（运行参数在 Run Configurations VM options）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 JDK6 下设置： &lt;code&gt;-XX:MaxPermSize=10m&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();
    int i = 0;
    try {
        for (int j = 0; j &amp;lt; 260000; j++) {
            list.add(String.valueOf(j).intern());
            i++;
        }
    } catch (Throwable e) {
        e.printStackTrace();
    } finally {
        System.out.println(i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%86%85%E5%AD%98%E5%9B%BE%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;优化常量池&lt;/h4&gt;
&lt;p&gt;两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调整 -XX:StringTableSize=桶个数，数量越少，性能越差&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;intern 将字符串对象放入常量池，通过复用字符串的引用，减少内存占用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {
    public static void main(String[] args) throws IOException {
        List&amp;lt;String&amp;gt; address = new ArrayList&amp;lt;&amp;gt;();
        System.in.read();
        for (int i = 0; i &amp;lt; 10; i++) {
            //很多数据
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(&quot;linux.words&quot;), &quot;utf-8&quot;))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                }
                System.out.println(&quot;cost:&quot; +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;不可变好处&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;可以缓存 hash 值，例如 String 用做 HashMap 的 key，不可变的特性可以使得 hash 值也不可变，只要进行一次计算&lt;/li&gt;
&lt;li&gt;String Pool 的需要，如果一个 String 对象已经被创建过了，就会从 String Pool 中取得引用，只有 String 是不可变的，才可能使用 String Pool&lt;/li&gt;
&lt;li&gt;安全性，String 经常作为参数，String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的，那么在网络连接过程中，String 被改变，改变 String 的那一方以为现在连接的是其它主机，而实际情况却不一定是&lt;/li&gt;
&lt;li&gt;String 不可变性天生具备线程安全，可以在多个线程中安全地使用&lt;/li&gt;
&lt;li&gt;防止子类继承，破坏 String 的 API 的使用&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;StringBuilder&lt;/h3&gt;
&lt;p&gt;String StringBuffer 和 StringBuilder 区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String : &lt;strong&gt;不可变&lt;/strong&gt;的字符序列，线程安全&lt;/li&gt;
&lt;li&gt;StringBuffer : &lt;strong&gt;可变&lt;/strong&gt;的字符序列，线程安全，底层方法加 synchronized，效率低&lt;/li&gt;
&lt;li&gt;StringBuilder : &lt;strong&gt;可变&lt;/strong&gt;的字符序列，JDK5.0 新增；线程不安全，效率高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相同点：底层使用 char[] 存储&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public StringBuilder()&lt;/code&gt;：创建一个空白可变字符串对象，不含有任何内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public StringBuilder(String str)&lt;/code&gt;：根据字符串的内容，来创建可变字符串对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public StringBuilder append(任意类型)&lt;/code&gt;：添加数据，并返回对象本身&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public StringBuilder reverse()&lt;/code&gt;：返回相反的字符序列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String toString()&lt;/code&gt;：通过 toString() 就可以实现把 StringBuilder 转换为 String&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储原理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String str = &quot;abc&quot;;
char data[] = {&apos;a&apos;, &apos;b&apos;, &apos;c&apos;};
StringBuffer sb1 = new StringBuffer();//new byte[16] 
sb1.append(&apos;a&apos;); //value[0] = &apos;a&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;append 源码：扩容为二倍&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public AbstractStringBuilder append(String str) {
    if (str == null) return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
    // 创建超过数组长度就新的char数组，把数据拷贝过去
    if (minimumCapacity - value.length &amp;gt; 0) {
        //int newCapacity = (value.length &amp;lt;&amp;lt; 1) + 2;每次扩容2倍+2
        value = Arrays.copyOf(value, newCapacity(minimumCapacity));
    }
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    // 将字符串中的字符复制到目标字符数组中
	// 字符串调用该方法，此时value是字符串的值，dst是目标字符数组
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Arrays&lt;/h3&gt;
&lt;p&gt;Array 的工具类 Arrays&lt;/p&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static String toString(int[] a)&lt;/code&gt;：返回指定数组的内容的字符串表示形式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static void sort(int[] a)&lt;/code&gt;：按照数字顺序排列指定的数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static int binarySearch(int[] a, int key)&lt;/code&gt;：利用二分查找返回指定元素的索引&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; asList(T... a)&lt;/code&gt;：返回由指定数组支持的列表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MyArraysDemo {
      public static void main(String[] args) {
		//按照数字顺序排列指定的数组
        int [] arr = {3,2,4,6,7};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
		
        int [] arr = {1,2,3,4,5,6,7,8,9,10};
        int index = Arrays.binarySearch(arr, 0);
        System.out.println(index);
        //1,数组必须有序
        //2.如果要查找的元素存在,那么返回的是这个元素实际的索引
        //3.如果要查找的元素不存在,那么返回的是 (-插入点-1)
            //插入点:如果这个元素在数组中,他应该在哪个索引上.
      }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Random&lt;/h3&gt;
&lt;p&gt;用于生成伪随机数。&lt;/p&gt;
&lt;p&gt;使用步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;导入包：&lt;code&gt;import java.util.Random&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建对象：&lt;code&gt;Random r = new Random()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机整数：&lt;code&gt;int num = r.nextInt(10)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解释：10 代表的是一个范围，如果括号写 10，产生的随机数就是 0 - 9，括号写 20 的随机数则是 0 - 19&lt;/li&gt;
&lt;li&gt;获取 0 - 10：&lt;code&gt;int num = r.nextInt(10 + 1)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机小数：&lt;code&gt;public double nextDouble()&lt;/code&gt; 从范围 &lt;code&gt;0.0d&lt;/code&gt; 至 &lt;code&gt;1.0d&lt;/code&gt; （左闭右开），伪随机地生成并返回&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;System&lt;/h3&gt;
&lt;p&gt;System 代表当前系统&lt;/p&gt;
&lt;p&gt;静态方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static void exit(int status)&lt;/code&gt;：终止 JVM 虚拟机，&lt;strong&gt;非 0 是异常终止&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public static long currentTimeMillis()&lt;/code&gt;：获取当前系统此刻时间毫秒值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)&lt;/code&gt;：数组拷贝&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数一：原数组&lt;/li&gt;
&lt;li&gt;参数二：从原数组的哪个位置开始赋值&lt;/li&gt;
&lt;li&gt;参数三：目标数组&lt;/li&gt;
&lt;li&gt;参数四：从目标数组的哪个位置开始赋值&lt;/li&gt;
&lt;li&gt;参数五：赋值几个&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class SystemDemo {
    public static void main(String[] args) {
        //System.exit(0); // 0代表正常终止!!
        long startTime = System.currentTimeMillis();//定义sdf 按照格式输出
        for(int i = 0; i &amp;lt; 10000; i++){输出i}
		long endTime = new Date().getTime();
		System.out.println( (endTime - startTime)/1000.0 +&quot;s&quot;);//程序用时

        int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70};
        int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0]
        // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ]
        System.arraycopy(arr1, 2, arr2, 1, 3);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Date&lt;/h3&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Date()&lt;/code&gt;：创建当前系统的此刻日期时间对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Date(long time)&lt;/code&gt;：把时间毫秒值转换成日期对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public long getTime()&lt;/code&gt;：返回自 1970 年 1 月 1 日 00:00:00 GMT 以来总的毫秒数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时间记录的两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Date 日期对象&lt;/li&gt;
&lt;li&gt;时间毫秒值：从 &lt;code&gt;1970-01-01 00:00:00&lt;/code&gt; 开始走到此刻的总的毫秒值，1s = 1000ms&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class DateDemo {
    public static void main(String[] args) {
        Date d = new Date();
        System.out.println(d);//Fri Oct 16 21:58:44 CST 2020
        long time = d.getTime() + 121*1000;//过121s是什么时间
        System.out.println(time);//1602856875485
        
        Date d1 = new Date(time);
        System.out.println(d1);//Fri Oct 16 22:01:15 CST 2020
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    Date d = new Date();
    long startTime = d.getTime();
    for(int i = 0; i &amp;lt; 10000; i++){输出i}
    long endTime = new Date().getTime();
    System.out.println( (endTime - startTime) / 1000.0 +&quot;s&quot;);
    //运行一万次输出需要多长时间
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;DateFormat&lt;/h3&gt;
&lt;p&gt;DateFormat 作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以把“日期对象”或者“时间毫秒值”格式化成我们喜欢的时间形式（格式化时间）&lt;/li&gt;
&lt;li&gt;可以把字符串的时间形式解析成日期对象（解析字符串时间）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DateFormat 是一个抽象类，不能直接使用，使用它的子类：SimpleDateFormat&lt;/p&gt;
&lt;p&gt;SimpleDateFormat  简单日期格式化类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public SimpleDateFormat(String pattern)&lt;/code&gt;：指定时间的格式创建简单日期对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String format(Date date) &lt;/code&gt;：把日期对象格式化成我们喜欢的时间形式，返回字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String format(Object time)&lt;/code&gt;：把时间毫秒值格式化成设定的时间形式，返回字符串!&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Date parse(String date)&lt;/code&gt;：把字符串的时间解析成日期对象&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;yyyy年MM月dd日 HH:mm:ss EEE a&quot; 周几 上午下午&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
	Date date = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss);
    String time = sdf.format(date);
    System.out.println(time);//2020-10-18 19:58:34
    //过121s后是什么时间
    long time = date.getTime();
    time+=121;
    System.out.println(sdf.formate(time));
    String d = &quot;2020-10-18 20:20:20&quot;;//格式一致
    Date newDate = sdf.parse(d);
    System.out.println(sdf.format(newDate)); //按照前面的方法输出
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Calendar&lt;/h3&gt;
&lt;p&gt;Calendar 代表了系统此刻日期对应的日历对象，是一个抽象类，不能直接创建对象&lt;/p&gt;
&lt;p&gt;Calendar 日历类创建日历对象：&lt;code&gt;Calendar rightNow = Calendar.getInstance()&lt;/code&gt;（&lt;strong&gt;饿汉单例模式&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;Calendar 的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static Calendar getInstance()&lt;/code&gt;：返回一个日历类的对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int get(int field)&lt;/code&gt;：取日期中的某个字段信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void set(int field,int value)&lt;/code&gt;：修改日历的某个字段信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void add(int field,int amount)&lt;/code&gt;：为某个字段增加/减少指定的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public final Date getTime()&lt;/code&gt;：拿到此刻日期对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public long getTimeInMillis()&lt;/code&gt;：拿到此刻时间毫秒值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
	Calendar rightNow = Calendar.getInsance(); 
	int year = rightNow.get(Calendar.YEAR);//获取年
    int month = rightNow.get(Calendar.MONTH) + 1;//月要+1
    int days = rightNow.get(Calendar.DAY_OF_YEAR);
    rightNow.set(Calendar.YEAR , 2099);//修改某个字段
    rightNow.add(Calendar.HOUR , 15);//加15小时  -15就是减去15小时
    Date date = rightNow.getTime();//日历对象
    long time = rightNow.getTimeInMillis();//时间毫秒值
    //700天后是什么日子
    rightNow.add(Calendar.DAY_OF_YEAR , 701);
    Date date d = rightNow.getTime();
    SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
    System.out.println(sdf.format(d));//输出700天后的日期
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;LocalDateTime&lt;/h3&gt;
&lt;p&gt;JDK1.8 新增，线程安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LocalDate       表示日期（年月日）&lt;/li&gt;
&lt;li&gt;LocalTime       表示时间（时分秒）&lt;/li&gt;
&lt;li&gt;LocalDateTime    表示时间+ 日期 （年月日时分秒）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public static LocalDateTime now()：获取当前系统时间&lt;/li&gt;
&lt;li&gt;public static LocalDateTime of(年, 月 , 日, 时, 分, 秒)：使用指定年月日和时分秒初始化一个对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public int getYear()&lt;/td&gt;
&lt;td&gt;获取年&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getMonthValue()&lt;/td&gt;
&lt;td&gt;获取月份（1-12）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getDayOfMonth()&lt;/td&gt;
&lt;td&gt;获取月份中的第几天（1-31）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getDayOfYear()&lt;/td&gt;
&lt;td&gt;获取一年中的第几天（1-366）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public DayOfWeek getDayOfWeek()&lt;/td&gt;
&lt;td&gt;获取星期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getMinute()&lt;/td&gt;
&lt;td&gt;获取分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getHour()&lt;/td&gt;
&lt;td&gt;获取小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public LocalDate  toLocalDate()&lt;/td&gt;
&lt;td&gt;转换成为一个 LocalDate 对象（年月日）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public LocalTime toLocalTime()&lt;/td&gt;
&lt;td&gt;转换成为一个 LocalTime 对象（时分秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public String format(指定格式)&lt;/td&gt;
&lt;td&gt;把一个 LocalDateTime 格式化成为一个字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public LocalDateTime parse(准备解析的字符串, 解析格式)&lt;/td&gt;
&lt;td&gt;把一个日期字符串解析成为一个 LocalDateTime 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static DateTimeFormatter ofPattern(String pattern)&lt;/td&gt;
&lt;td&gt;使用指定的日期模板获取一个日期格式化器 DateTimeFormatter 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class JDK8DateDemo2 {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);

        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 11);
        System.out.println(localDateTime);
        DateTimeFormatter pattern = DateTimeFormatter.ofPattern(&quot;yyyy年MM月dd日 HH:mm:ss&quot;);
        String s = localDateTime.format(pattern);
		LocalDateTime parse = LocalDateTime.parse(s, pattern);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public LocalDateTime plusYears (long years)&lt;/td&gt;
&lt;td&gt;添加或者减去年&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public LocalDateTime withYear(int year)&lt;/td&gt;
&lt;td&gt;直接修改年&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;时间间隔&lt;/strong&gt; Duration 类API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public static Period between(开始时间,结束时间)&lt;/td&gt;
&lt;td&gt;计算两个“时间&quot;的间隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getYears()&lt;/td&gt;
&lt;td&gt;获得这段时间的年数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getMonths()&lt;/td&gt;
&lt;td&gt;获得此期间的总月数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int getDays()&lt;/td&gt;
&lt;td&gt;获得此期间的天数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public long toTotalMonths()&lt;/td&gt;
&lt;td&gt;获取此期间的总月数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static Durationbetween(开始时间,结束时间)&lt;/td&gt;
&lt;td&gt;计算两个“时间&quot;的间隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public long toSeconds()&lt;/td&gt;
&lt;td&gt;获得此时间间隔的秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public long toMillis()&lt;/td&gt;
&lt;td&gt;获得此时间间隔的毫秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public long toNanos()&lt;/td&gt;
&lt;td&gt;获得此时间间隔的纳秒&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class JDK8DateDemo9 {
    public static void main(String[] args) {
        LocalDate localDate1 = LocalDate.of(2020, 1, 1);
        LocalDate localDate2 = LocalDate.of(2048, 12, 12);
        Period period = Period.between(localDate1, localDate2);
        System.out.println(period);//P28Y11M11D
		Duration duration = Duration.between(localDateTime1, localDateTime2);
        System.out.println(duration);//PT21H57M58S
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Math&lt;/h3&gt;
&lt;p&gt;Math 用于做数学运算&lt;/p&gt;
&lt;p&gt;Math 类中的方法全部是静态方法，直接用类名调用即可：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public static int abs(int a)&lt;/td&gt;
&lt;td&gt;获取参数a的绝对值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static double ceil(double a)&lt;/td&gt;
&lt;td&gt;向上取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static double floor(double a)&lt;/td&gt;
&lt;td&gt;向下取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static double pow(double a, double b)&lt;/td&gt;
&lt;td&gt;获取 a 的 b 次幂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static long round(double a)&lt;/td&gt;
&lt;td&gt;四舍五入取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static int max(int a,int b)&lt;/td&gt;
&lt;td&gt;返回较大值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static int min(int a,int b)&lt;/td&gt;
&lt;td&gt;返回较小值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static double random()&lt;/td&gt;
&lt;td&gt;返回值为 double 的正值，[0.0,1.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class MathDemo {
    public static void main(String[] args) {
        // 1.取绝对值:返回正数。
        System.out.println(Math.abs(10));
        System.out.println(Math.abs(-10.3));
        // 2.向上取整: 5
        System.out.println(Math.ceil(4.00000001)); // 5.0
        System.out.println(Math.ceil(-4.00000001));//4.0
        // 3.向下取整：4
        System.out.println(Math.floor(4.99999999)); // 4.0
        System.out.println(Math.floor(-4.99999999)); // 5.0
        // 4.求指数次方
        System.out.println(Math.pow(2 , 3)); // 2^3 = 8.0
        // 5.四舍五入 10
        System.out.println(Math.round(4.49999)); // 4
        System.out.println(Math.round(4.500001)); // 5
        System.out.println(Math.round(5.5));//6
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;DecimalFormat&lt;/h3&gt;
&lt;p&gt;使任何形式的数字解析和格式化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[]args){
    double pi = 3.1415927;　//圆周率
    //取一位整数
    System.out.println(new DecimalFormat(&quot;0&quot;).format(pi));　　　//3
    //取一位整数和两位小数
    System.out.println(new DecimalFormat(&quot;0.00&quot;).format(pi));　//3.14
    //取两位整数和三位小数，整数不足部分以0填补。
    System.out.println(new DecimalFormat(&quot;00.000&quot;).format(pi));// 03.142
    //取所有整数部分
    System.out.println(new DecimalFormat(&quot;#&quot;).format(pi));　　　//3
    //以百分比方式计数，并取两位小数
    System.out.println(new DecimalFormat(&quot;#.##%&quot;).format(pi));　//314.16%

    long c =299792458;　　//光速
    //显示为科学计数法，并取五位小数
    System.out.println(new DecimalFormat(&quot;#.#####E0&quot;).format(c));//2.99792E8
    //显示为两位整数的科学计数法，并取四位小数
    System.out.println(new DecimalFormat(&quot;00.####E0&quot;).format(c));//29.9792E7
    //每三位以逗号进行分隔。
    System.out.println(new DecimalFormat(&quot;,###&quot;).format(c));//299,792,458
    //将格式嵌入文本
    System.out.println(new DecimalFormat(&quot;光速大小为每秒,###米。&quot;).format(c));

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;BigDecimal&lt;/h3&gt;
&lt;p&gt;Java 在 java.math 包中提供的 API 类，用来对超过16位有效位的数进行精确的运算&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static BigDecimal valueOf(double val)&lt;/code&gt;：包装浮点数成为大数据对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal(double val)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal(String val)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal add(BigDecimal value)&lt;/code&gt;：加法运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal subtract(BigDecimal value)&lt;/code&gt;：减法运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal multiply(BigDecimal value)&lt;/code&gt;：乘法运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal divide(BigDecimal value)&lt;/code&gt;：除法运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public double doubleValue()&lt;/code&gt;：把 BigDecimal 转换成 double 类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int intValue()&lt;/code&gt;：转为 int 其他类型相同&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public BigDecimal divide (BigDecimal value，精确几位，舍入模式)&lt;/code&gt;：除法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BigDecimalDemo {
    public static void main(String[] args) {
        // 浮点型运算的时候直接+ - * / 可能会出现数据失真（精度问题）。
        System.out.println(0.1 + 0.2);
        System.out.println(1.301 / 100);
        
        double a = 0.1 ;
        double b = 0.2 ;
        double c = a + b ;
        System.out.println(c);//0.30000000000000004
        
        // 1.把浮点数转换成大数据对象运算
        BigDecimal a1 = BigDecimal.valueOf(a);
        BigDecimal b1 = BigDecimal.valueOf(b);
        BigDecimal c1 = a1.add(b1);//a1.divide(b1);也可以
		System.out.println(c1);

        // BigDecimal只是解决精度问题的手段，double数据才是我们的目的！！
        double d = c1.doubleValue();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BigDecimal 是用来进行精确计算的&lt;/li&gt;
&lt;li&gt;创建 BigDecimal 的对象，构造方法使用参数类型为字符串的&lt;/li&gt;
&lt;li&gt;四则运算中的除法，如果除不尽请使用 divide 的三个参数的方法&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式);
//参数1：表示参与运算的BigDecimal 对象。
//参数2：表示小数点后面精确到多少位
//参数3：舍入模式  
// BigDecimal.ROUND_UP  进一法
// BigDecimal.ROUND_FLOOR 去尾法
// BigDecimal.ROUND_HALF_UP 四舍五入
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Regex&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;正则表达式的作用：是一些特殊字符组成的校验规则，可以校验信息的正确性，校验邮箱、电话号码、金额等。&lt;/p&gt;
&lt;p&gt;比如检验 qq 号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static boolean checkQQRegex(String qq){
    return qq!=null &amp;amp;&amp;amp; qq.matches(&quot;\\d{4,}&quot;);//即是数字 必须大于4位数
}// 用\\d  是因为\用来告诉它是一个校验类，不是普通的字符 比如 \t \n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;java.util.regex 包主要包括以下三个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Pattern 类：&lt;/p&gt;
&lt;p&gt;Pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法，要创建一个 Pattern 对象，必须首先调用其公共静态编译方法，返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Matcher 类：&lt;/p&gt;
&lt;p&gt;Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样，Matcher 也没有公共构造方法，需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PatternSyntaxException：&lt;/p&gt;
&lt;p&gt;PatternSyntaxException 是一个非强制异常类，它表示一个正则表达式模式中的语法错误。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;字符匹配&lt;/h4&gt;
&lt;h5&gt;普通字符&lt;/h5&gt;
&lt;p&gt;字母、数字、汉字、下划线、以及没有特殊定义的标点符号，都是“普通字符”。表达式中的普通字符，在匹配一个字符串的时候，匹配与之相同的一个字符。其他统称&lt;strong&gt;元字符&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;特殊字符&lt;/h5&gt;
&lt;p&gt;\r\n 是 Windows 中的文本行结束标签，在 Unix/Linux 则是 \n&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;元字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;\&lt;/td&gt;
&lt;td&gt;将下一个字符标记为一个特殊字符或原义字符，告诉它是一个校验类，不是普通字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\f&lt;/td&gt;
&lt;td&gt;换页符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\n&lt;/td&gt;
&lt;td&gt;换行符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\r&lt;/td&gt;
&lt;td&gt;回车符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\t&lt;/td&gt;
&lt;td&gt;制表符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\&lt;/td&gt;
&lt;td&gt;代表 \ 本身&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;()&lt;/td&gt;
&lt;td&gt;使用 () 定义一个子表达式。子表达式的内容可以当成一个独立元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;标准字符&lt;/h5&gt;
&lt;p&gt;能够与多种字符匹配的表达式，注意区分大小写，大写是相反的意思，只能校验&lt;strong&gt;单&lt;/strong&gt;个字符。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;元字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;匹配任意一个字符（除了换行符），如果要匹配包括 \n 在内的所有字符，一般用 [\s\S]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\d&lt;/td&gt;
&lt;td&gt;数字字符，0~9 中的任意一个，等价于 [0-9]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\D&lt;/td&gt;
&lt;td&gt;非数字字符，等价于  [ ^0-9]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\w&lt;/td&gt;
&lt;td&gt;大小写字母或数字或下划线，等价于[a-zA-Z_0-9_]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\W&lt;/td&gt;
&lt;td&gt;对\w取非，等价于[ ^\w]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\s&lt;/td&gt;
&lt;td&gt;空格、制表符、换行符等空白字符的其中任意一个，等价于[\f\n\r\t\v]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\S&lt;/td&gt;
&lt;td&gt;对 \s 取非&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;\x 匹配十六进制字符，\0 匹配八进制，例如 \xA 对应值为 10 的 ASCII 字符 ，即 \n&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;自定义符&lt;/h5&gt;
&lt;p&gt;自定义符号集合，[ ] 方括号匹配方式，能够匹配方括号中&lt;strong&gt;任意一个&lt;/strong&gt;字符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;元字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;[ab5@]&lt;/td&gt;
&lt;td&gt;匹配 &quot;a&quot; 或 &quot;b&quot; 或 &quot;5&quot; 或 &quot;@&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[^abc]&lt;/td&gt;
&lt;td&gt;匹配 &quot;a&quot;,&quot;b&quot;,&quot;c&quot; 之外的任意一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[f-k]&lt;/td&gt;
&lt;td&gt;匹配 &quot;f&quot;~&quot;k&quot; 之间的任意一个字母&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[^A-F0-3]&lt;/td&gt;
&lt;td&gt;匹配 &quot;A&quot;,&quot;F&quot;,&quot;0&quot;~&quot;3&quot; 之外的任意一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[a-d[m-p]]&lt;/td&gt;
&lt;td&gt;匹配 a 到 d 或者 m 到 p：[a-dm-p]（并集）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[a-z&amp;amp;&amp;amp;[m-p]]&lt;/td&gt;
&lt;td&gt;匹配 a 到 z 并且 m 到 p：[a-dm-p]（交集）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[^]&lt;/td&gt;
&lt;td&gt;取反&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正则表达式的特殊符号，被包含到中括号中，则失去特殊意义，除了 ^,- 之外，需要在前面加 \&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标准字符集合，除小数点外，如果被包含于中括号，自定义字符集合将包含该集合。
比如：[\d. \ -+] 将匹配：数字、小数点、+、-&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;量词字符&lt;/h5&gt;
&lt;p&gt;修饰匹配次数的特殊符号。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;匹配次数中的贪婪模式(匹配字符越多越好，默认 ！)，* 和 + 都是贪婪型元字符。&lt;/li&gt;
&lt;li&gt;匹配次数中的非贪婪模式（匹配字符越少越好，修饰匹配次数的特殊符号后再加上一个 ? 号）&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;元字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X?&lt;/td&gt;
&lt;td&gt;X 一次或一次也没，有相当于 {0,1}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X*&lt;/td&gt;
&lt;td&gt;X 不出现或出现任意次，相当于 {0,}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X+&lt;/td&gt;
&lt;td&gt;X 至少一次，相当于 {1,}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X{n}&lt;/td&gt;
&lt;td&gt;X 恰好 n 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{n,}&lt;/td&gt;
&lt;td&gt;X 至少 n 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{n,m}&lt;/td&gt;
&lt;td&gt;X 至少 n 次，但是不超过 m 次&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;位置匹配&lt;/h4&gt;
&lt;h5&gt;字符边界&lt;/h5&gt;
&lt;p&gt;本组标记匹配的不是字符而是位置，符合某种条件的位置&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;元字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;^&lt;/td&gt;
&lt;td&gt;与字符串开始的地方匹配（在字符集合中用来求非，在字符集合外用作匹配字符串的开头）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;与字符串结束的地方匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\b&lt;/td&gt;
&lt;td&gt;匹配一个单词边界&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;捕获组&lt;/h5&gt;
&lt;p&gt;捕获组是把多个字符当一个单独单元进行处理的方法，它通过对括号内的字符分组来创建。&lt;/p&gt;
&lt;p&gt;在表达式 &lt;code&gt;((A)(B(C)))&lt;/code&gt;，有四个这样的组：((A)(B(C)))、(A)、(B(C))、(C)（按照括号从左到右依次为 group(1)...）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 matcher 对象的 groupCount 方法返回一个 int 值，表示 matcher 对象当前有多个捕获组。&lt;/li&gt;
&lt;li&gt;特殊的组 group(0)、group()，代表整个表达式，该组不包括在 groupCount 的返回值中。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表达式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;|  (分支结构)&lt;/td&gt;
&lt;td&gt;左右两边表达式之间 &quot;或&quot; 关系，匹配左边或者右边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;()  (捕获组)&lt;/td&gt;
&lt;td&gt;(1) 在被修饰匹配次数的时候，括号中的表达式可以作为整体被修饰&amp;lt;br/&amp;gt;(2) 取匹配结果的时候，括号中的表达式匹配到的内容可以被单独得到&amp;lt;br/&amp;gt;(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(?:Expression)   非捕获组&lt;/td&gt;
&lt;td&gt;一些表达式中，不得不使用( )，但又不需要保存 () 中子表达式匹配的内容，这时可以用非捕获组来抵消使用( )带来的副作用。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;反向引用&lt;/h5&gt;
&lt;p&gt;反向引用（\number），又叫回溯引用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每一对()会分配一个编号，使用 () 的捕获根据左括号的顺序从1开始自动编号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过反向引用，可以对分组已捕获的字符串进行引用，继续匹配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;把匹配到的字符重复一遍在进行匹配&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用 1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String regex = &quot;((\d)3)\1[0-9](\w)\2{2}&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先匹配 ((\d)3)，其次 \1 匹配 ((\d)3) 已经匹配到的内容，\2 匹配 (\d)， {2} 指的是 \2 的值出现两次&lt;/li&gt;
&lt;li&gt;实例：23238n22（匹配到 2 未来就继续匹配 2）&lt;/li&gt;
&lt;li&gt;实例：43438n44&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用 2：爬虫&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String regex = &quot;&amp;lt;(h[1-6])&amp;gt;\w*?&amp;lt;\/\1&amp;gt;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;匹配结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;x&amp;lt;/h1&amp;gt;//匹配
&amp;lt;h2&amp;gt;x&amp;lt;/h2&amp;gt;//匹配
&amp;lt;h3&amp;gt;x&amp;lt;/h1&amp;gt;//不匹配
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;零宽断言&lt;/h5&gt;
&lt;p&gt;预搜索（零宽断言）（环视）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只进行子表达式的匹配，匹配内容不计入最终的匹配结果，是零宽度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断当前位置的前后字符，是否符合指定的条件，但不匹配前后的字符，&lt;strong&gt;是对位置的匹配&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正则表达式匹配过程中，如果子表达式匹配到的是字符内容，而非位置，并被保存到最终的匹配结果中，那么就认为这个子表达式是占有字符的；如果子表达式匹配的仅仅是位置，或者匹配的内容并不保存到最终的匹配结果中，那么就认为这个子表达式是&lt;strong&gt;零宽度&lt;/strong&gt;的。占有字符还是零宽度，是针对匹配的内容是否保存到最终的匹配结果中而言的&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表达式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(?=exp)&lt;/td&gt;
&lt;td&gt;断言自身出现的位置的后面能匹配表达式exp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(?&amp;lt;=exp)&lt;/td&gt;
&lt;td&gt;断言自身出现的位置的前面能匹配表达式exp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(?!exp)&lt;/td&gt;
&lt;td&gt;断言此位置的后面不能匹配表达式exp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(?&amp;lt;!exp)&lt;/td&gt;
&lt;td&gt;断言此位置的前面不能匹配表达式exp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;匹配模式&lt;/h4&gt;
&lt;p&gt;正则表达式的匹配模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IGNORECASE 忽略大小写模式
&lt;ul&gt;
&lt;li&gt;匹配时忽略大小写。&lt;/li&gt;
&lt;li&gt;默认情况下，正则表达式是要区分大小写的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SINGLELINE 单行模式
&lt;ul&gt;
&lt;li&gt;整个文本看作一个字符串，只有一个开头，一个结尾。&lt;/li&gt;
&lt;li&gt;使小数点 &quot;.&quot; 可以匹配包含换行符（\n）在内的任意字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MULTILINE 多行模式
&lt;ul&gt;
&lt;li&gt;每行都是一个字符串，都有开头和结尾。&lt;/li&gt;
&lt;li&gt;在指定了 MULTILINE 之后，如果需要仅匹配字符串开始和结束位置，可以使用 \A 和 \Z&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分组匹配&lt;/h4&gt;
&lt;p&gt;Pattern 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static Pattern compile(String regex)&lt;/code&gt;：将给定的正则表达式编译为模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Matcher matcher(CharSequence input)&lt;/code&gt;：创建一个匹配器，匹配给定的输入与此模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static boolean matches(String regex, CharSequence input)&lt;/code&gt;：编译正则表达式，并匹配输入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Matcher 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;boolean find()&lt;/code&gt;：扫描输入的序列，查找与该模式匹配的下一个子序列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String group()&lt;/code&gt;：返回与上一个匹配的输入子序列，同 group(0)，匹配整个表达式的子字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String group(int group)&lt;/code&gt;：返回在上一次匹配操作期间由给定组捕获的输入子序列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int groupCount()&lt;/code&gt;：返回此匹配器模式中捕获组的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Demo01{
	public static void main(String[] args) {
		//表达式对象
		Pattern p = Pattern.compile(&quot;\\w+&quot;);
		//创建Matcher对象
		Matcher m = p.matcher(&quot;asfsdf2&amp;amp;&amp;amp;3323&quot;);
		//boolean b = m.matches();//尝试将整个字符序列与该模式匹配
		//System.out.println(b);//false
		//boolean b2 = m.find();//该方法扫描输入的序列，查找与该模式匹配的下一个子序列
		//System.out.println(b2);//true
		
		//System.out.println(m.find());
		//System.out.println(m.group());//asfsdf2
		//System.out.println(m.find());
		//System.out.println(m.group());//3323
		
		while(m.find()){
			System.out.println(m.group());	//group(),group(0)匹配整个表达式的子字符串
			System.out.println(m.group(0));
		}
		
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Demo02 {
	public static void main(String[] args) {
		//在这个字符串：asfsdf23323，是否符合指定的正则表达式：\w+
		//表达式对象
        Pattern p = Pattern.compile(&quot;(([a-z]+)([0-9]+))&quot;);//不需要加多余的括号
		//创建Matcher对象
		Matcher m = p.matcher(&quot;aa232**ssd445&quot;);
	
		while(m.find()){
			System.out.println(m.group());//aa232  ssd445
			System.out.println(m.group(1));//aa232  ssd445
			System.out.println(m.group(2));//aa     ssd
            System.out.println(m.group(3));//232    445 
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;正则表达式改为 &lt;code&gt;&quot;(([a-z]+)(?:[0-9]+))&quot;&lt;/code&gt;   没有 group(3) 因为是非捕获组&lt;/li&gt;
&lt;li&gt;正则表达式改为 &lt;code&gt;&quot;([a-z]+)([0-9]+)&quot;&lt;/code&gt;  没有 group(3)    aa232  - aa  --232&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;h5&gt;基本验证&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
	System.out.println(&quot;a&quot;.matches(&quot;[abc]&quot;));//true判断a是否在abc
    System.out.println(&quot;a&quot;.matches(&quot;[^abc]&quot;));//false 判断a是否在abc之外的
    System.out.println(&quot;a&quot;.matches(&quot;\\d&quot;)); //false 是否a是整数
    System.out.println(&quot;a&quot;.matches(&quot;\\w&quot;)); //true 是否是字符
    System.out.println(&quot;你&quot;.matches(&quot;\\w&quot;)); // false
    System.out.println(&quot;aa&quot;.matches(&quot;\\w&quot;));//false 只能检验单个字符
    
    // 密码 必须是数字 字母 下划线 至少 6位
	System.out.println(&quot;ssds3c&quot;.matches(&quot;\\w{6,}&quot;)); // true
    // 验证。必须是数字和字符  必须是4位
    System.out.println(&quot;dsd22&quot;.matches(&quot;[a-zA-Z0-9]{4}&quot;)); // false
    System.out.println(&quot;A3dy&quot;.matches(&quot;[a-zA-Z0-9]{4}&quot;)); // true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;验证号码&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;//1开头 第二位是2-9的数字
public static void checkPhone(String phone){
    if(phone.matches(&quot;1[3-9]\\d{9}&quot;)){
        System.out.println(&quot;手机号码格式正确！&quot;);
    } else {.......}
}
//1111@qq.com  zhy@pic.com.cn
public static void checkEmail(String email){
    if(email.matches(&quot;\\w{1,}@\\w{1,}(\\.\\w{2,5}){1,2}&quot;)){
        System.out.println(&quot;邮箱格式正确！&quot;);
    }// .是任意字符 \\.就是点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;查找替换&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public String[] split(String regex)&lt;/code&gt;：按照正则表达式匹配的内容进行分割字符串，反回一个字符串数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String replaceAll(String regex,String newStr)&lt;/code&gt;：按照正则表达式匹配的内容进行替换&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//数组分割
public static void main(String[] args) {
	// 1.split的基础用法
	String names = &quot;风清扬,张无忌,周芷若&quot;;
	// 以“，”分割成字符串数组
    String[] nameArrs = names.split(&quot;,&quot;);

    // 2.split集合正则表达式做分割
    String names1 = &quot;风清扬lv434fda324张无忌87632fad2342423周芷若&quot;;
    // 以匹配正则表达式的内容为分割点分割成字符串数组
	String[] nameArrs1 = names1.split(&quot;\\w+&quot;);
    
	// 使用正则表达式定位出内容，替换成/
	System.out.println(names1.replaceAll(&quot;\\w+&quot;,&quot;/&quot;));//风清扬/张无忌/周芷若

	String names3 = &quot;风清扬,张无忌,周芷若&quot;;
	System.out.println(names3.replaceAll(&quot;,&quot;,&quot;-&quot;));//风清扬-张无忌-周芷若
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;搜索号码&lt;/h5&gt;
&lt;p&gt;找出所有 189 和 132 开头的手机号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RegexDemo {
    public static void main(String[] args) {
        String rs = &quot;189asjk65as1891898777745gkkkk189745612318936457894&quot;;
        String regex = &quot;(?=((189|132)\\d{8}))&quot;;
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(rs);
        while (matcher.find()) {
            System.out.println(matcher.group(1));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;集合&lt;/h2&gt;
&lt;h3&gt;集合概述&lt;/h3&gt;
&lt;p&gt;集合是一个大小可变的容器，容器中的每个数据称为一个元素&lt;/p&gt;
&lt;p&gt;集合特点：类型可以不确定，大小不固定；集合有很多，不同的集合特点和使用场景不同&lt;/p&gt;
&lt;p&gt;数组：类型和长度一旦定义出来就都固定&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在开发中，很多时候元素的个数是不确定的&lt;/li&gt;
&lt;li&gt;而且经常要进行元素的增删该查操作，集合都是非常合适的，开发中集合用的更多&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;存储结构&lt;/h3&gt;
&lt;p&gt;数据结构指的是数据以什么方式组织在一起，不同的数据结构，增删查的性能是不一样的&lt;/p&gt;
&lt;p&gt;数据存储的常用结构有：栈、队列、数组、链表和红黑树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;队列（queue）：先进先出，后进后出。(FIFO first in first out)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈（stack）：后进先出，先进后出 （LIFO）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组：数组是内存中的连续存储区域，分成若干等分的小区域（每个区域大小是一样的）元素存在索引&lt;/p&gt;
&lt;p&gt;特点：&lt;strong&gt;查询元素快&lt;/strong&gt;（根据索引快速计算出元素的地址，然后立即去定位），&lt;strong&gt;增删元素慢&lt;/strong&gt;（创建新数组，迁移元素）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链表：元素不是内存中的连续区域存储，元素是游离存储的，每个元素会记录下个元素的地址
特点：&lt;strong&gt;查询元素慢，增删元素快&lt;/strong&gt;（针对于首尾元素，速度极快，一般是双链表）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;树：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;二叉树：binary tree 永远只有一个根节点，是每个结点不超过2个节点的树（tree）&lt;/p&gt;
&lt;p&gt;特点：二叉排序树：小的左边，大的右边，但是可能树很高，性能变差，为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树，让树的高度差不大于1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;红黑树（基于红黑规则实现自平衡的排序二叉树）：树保证到了很矮小，但是又排好序，性能最高的&lt;/p&gt;
&lt;p&gt;特点：&lt;strong&gt;红黑树的增删查改性能都好&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各数据结构时间复杂度对比：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%9A%84%E5%A4%8D%E6%9D%82%E5%BA%A6%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图片来源：https://www.bigocheatsheet.com/&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Collection&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;Java 中集合的代表是 Collection，Collection 集合是 Java 中集合的祖宗类&lt;/p&gt;
&lt;p&gt;Collection 集合底层为数组：&lt;code&gt;[value1, value2, ....]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Collection集合的体系:
                      Collection&amp;lt;E&amp;gt;(接口)
                 /                         \
          Set&amp;lt;E&amp;gt;(接口)                    List&amp;lt;E&amp;gt;(接口)
      /               \                  /             \
 HashSet&amp;lt;E&amp;gt;(实现类) TreeSet&amp;lt;&amp;gt;(实现类)  ArrayList&amp;lt;E&amp;gt;(实现类)  LinekdList&amp;lt;&amp;gt;(实现类)
 /
LinkedHashSet&amp;lt;&amp;gt;(实现类)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;集合的特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set 系列集合：添加的元素是无序，不重复，无索引的
&lt;ul&gt;
&lt;li&gt;HashSet：添加的元素是无序，不重复，无索引的&lt;/li&gt;
&lt;li&gt;LinkedHashSet：添加的元素是有序，不重复，无索引的&lt;/li&gt;
&lt;li&gt;TreeSet：不重复，无索引，按照大小默认升序排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;List 系列集合：添加的元素是有序，可重复，有索引
&lt;ul&gt;
&lt;li&gt;ArrayList：添加的元素是有序，可重复，有索引&lt;/li&gt;
&lt;li&gt;LinekdList：添加的元素是有序，可重复，有索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;API&lt;/h4&gt;
&lt;p&gt;Collection 是集合的祖宗类，它的功能是全部集合都可以继承使用的，所以要学习它。&lt;/p&gt;
&lt;p&gt;Collection 子类的构造器都有可以包装其他子类的构造方法，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public ArrayList(Collection&amp;lt;? extends E&amp;gt; c)&lt;/code&gt;：构造新集合，元素按照由集合的迭代器返回的顺序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public HashSet(Collection&amp;lt;? extends E&amp;gt; c)&lt;/code&gt;：构造一个包含指定集合中的元素的新集合&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Collection API 如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public boolean add(E e)&lt;/code&gt;：把给定的对象添加到当前集合中 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void clear()&lt;/code&gt;：清空集合中所有的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean remove(E e)&lt;/code&gt;：把给定的对象在当前集合中删除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean contains(Object obj)&lt;/code&gt;：判断当前集合中是否包含给定的对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean isEmpty()&lt;/code&gt;：判断当前集合是否为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int size()&lt;/code&gt;：返回集合中元素的个数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Object[] toArray()&lt;/code&gt;：把集合中的元素，存储到数组中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean addAll(Collection&amp;lt;? extends E&amp;gt; c)&lt;/code&gt;：将指定集合中的所有元素添加到此集合&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class CollectionDemo {
    public static void main(String[] args) {
        Collection&amp;lt;String&amp;gt; sets = new HashSet&amp;lt;&amp;gt;();
        sets.add(&quot;MyBatis&quot;);
        System.out.println(sets.add(&quot;Java&quot;));//true
        System.out.println(sets.add(&quot;Java&quot;));//false
        sets.add(&quot;Spring&quot;);
        sets.add(&quot;MySQL&quot;);
        System.out.println(sets)//[]无序的;
        System.out.println(sets.contains(&quot;java&quot;));//true 存在
        Object[] arrs = sets.toArray();
        System.out.println(&quot;数组：&quot;+ Arrays.toString(arrs));
        
        Collection&amp;lt;String&amp;gt; c1 = new ArrayList&amp;lt;&amp;gt;();
        c1.add(&quot;java&quot;);
        Collection&amp;lt;String&amp;gt; c2 = new ArrayList&amp;lt;&amp;gt;();
        c2.add(&quot;ee&quot;);
        c1.addAll(c2);// c1:[java,ee]  c2:[ee];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;遍历&lt;/h4&gt;
&lt;p&gt;Collection 集合的遍历方式有三种:&lt;/p&gt;
&lt;p&gt;集合可以直接输出内容，因为底层重写了 toString() 方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;迭代器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Iterator iterator()&lt;/code&gt;：获取集合对应的迭代器，用来遍历集合中的元素的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E next()&lt;/code&gt;：获取下一个元素值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean hasNext()&lt;/code&gt;：判断是否有下一个元素，有返回 true ，反之返回 false&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default void remove()&lt;/code&gt;：从底层集合中删除此迭代器返回的最后一个元素，这种方法只能在每次调用 next() 时调用一次&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增强 for 循环：可以遍历集合或者数组，遍历集合实际上是迭代器遍历的简化写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺点：遍历无法知道遍历到了哪个元素了，因为没有索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 1.8 开始之后的新技术 Lambda 表达式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CollectionDemo {
    public static void main(String[] args) {
        Collection&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();
        lists.add(&quot;aa&quot;);
        lists.add(&quot;bb&quot;);
        lists.add(&quot;cc&quot;);
        System.out.println(lists); // lists = [aa, bb, cc]
		//迭代器流程
        // 1.得到集合的迭代器对象。
        Iterator&amp;lt;String&amp;gt; it = lists.iterator();
        // 2.使用while循环遍历。
        while(it.hasNext()){
            String ele = it.next();
            System.out.println(ele);
        }
        
		//增强for
        for (String ele : lists) {
            System.out.println(ele);
        }
        //lambda表达式
        lists.forEach(s -&amp;gt; {
            System.out.println(s);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;List&lt;/h4&gt;
&lt;h5&gt;概述&lt;/h5&gt;
&lt;p&gt;List 集合继承了 Collection 集合全部的功能。&lt;/p&gt;
&lt;p&gt;List 系列集合有索引，所以多了很多按照索引操作元素的功能：for 循环遍历（4 种遍历）&lt;/p&gt;
&lt;p&gt;List 系列集合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ArrayList：添加的元素是有序，可重复，有索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LinekdList：添加的元素是有序，可重复，有索引&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;ArrayList&lt;/h5&gt;
&lt;h6&gt;介绍&lt;/h6&gt;
&lt;p&gt;ArrayList 添加的元素，是有序，可重复，有索引的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public boolean add(E e)&lt;/code&gt;：将指定的元素追加到此集合的末尾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void add(int index, E element)&lt;/code&gt;：将指定的元素，添加到该集合中的指定位置上&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E get(int index)&lt;/code&gt;：返回集合中指定位置的元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E remove(int index)&lt;/code&gt;：移除列表中指定位置的元素，返回的是被移除的元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E set(int index, E element)&lt;/code&gt;：用指定元素替换集合中指定位置的元素，返回更新前的元素值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int indexOf(Object o)&lt;/code&gt;：返回列表中指定元素第一次出现的索引，如果不包含此元素，则返回 -1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    List&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();//多态
    lists.add(&quot;java1&quot;);
    lists.add(&quot;java1&quot;);//可以重复
    lists.add(&quot;java2&quot;);
    for(int i = 0 ; i &amp;lt; lists.size() ; i++ ) {
            String ele = lists.get(i);
            System.out.println(ele);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h6&gt;源码&lt;/h6&gt;
&lt;p&gt;ArrayList 实现类集合底层&lt;strong&gt;基于数组存储数据&lt;/strong&gt;的，查询快，增删慢，支持快速随机访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArrayList&amp;lt;E&amp;gt; extends AbstractList&amp;lt;E&amp;gt;
        implements List&amp;lt;E&amp;gt;, RandomAccess, Cloneable, java.io.Serializable{}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RandomAccess&lt;/code&gt; 是一个标志接口，表明实现这个这个接口的 List 集合是支持&lt;strong&gt;快速随机访问&lt;/strong&gt;的。在 &lt;code&gt;ArrayList&lt;/code&gt; 中，我们即可以通过元素的序号快速获取元素对象，这就是快速随机访问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrayList&lt;/code&gt; 实现了 &lt;code&gt;Cloneable&lt;/code&gt; 接口 ，即覆盖了函数 &lt;code&gt;clone()&lt;/code&gt;，能被克隆&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrayList&lt;/code&gt; 实现了 &lt;code&gt;Serializable &lt;/code&gt; 接口，这意味着 &lt;code&gt;ArrayList&lt;/code&gt; 支持序列化，能通过序列化去传输&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造函数：以无参数构造方法创建 ArrayList 时，实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时，才真正分配容量（惰性初始化），即向数组中添加第一个元素时，&lt;strong&gt;数组容量扩为 10&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// e 插入的元素  elementData底层数组   size 插入的位置
public boolean add(E e) {
    ensureCapacityInternal(size + 1);	// Increments modCount!!
    elementData[size++] = e;			// 插入size位置，然后加一
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 add 第 1 个元素到 ArrayList，size 是 0，进入 ensureCapacityInternal 方法，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 判断elementData是不是空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 返回默认值和最小需求容量最大的一个
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要的容量大于数组长度，进行扩容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 索引越界
    if (minCapacity - elementData.length &amp;gt; 0)
        // 调用grow方法进行扩容，调用此方法代表已经开始扩容了
        grow(minCapacity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定索引插入，&lt;strong&gt;在旧数组上操作&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将指定索引后的数据后移
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩容：新容量的大小为 &lt;code&gt;oldCapacity + (oldCapacity &amp;gt;&amp;gt; 1)&lt;/code&gt;，&lt;code&gt;oldCapacity &amp;gt;&amp;gt; 1&lt;/code&gt; 需要取整，所以新容量大约是旧容量的 1.5 倍左右，即 oldCapacity+oldCapacity/2&lt;/p&gt;
&lt;p&gt;扩容操作需要调用 &lt;code&gt;Arrays.copyOf()&lt;/code&gt;（底层 &lt;code&gt;System.arraycopy()&lt;/code&gt;）把原数组整个复制到&lt;strong&gt;新数组&lt;/strong&gt;中，这个操作代价很高，因此最好在创建 ArrayList 对象时就指定大概的容量大小，减少扩容操作的次数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity &amp;gt;&amp;gt; 1);
    //检查新容量是否大于最小需要容量，若小于最小需要容量，就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity &amp;lt; 0)
		newCapacity = minCapacity;//不需要扩容计算
    //检查新容量是否大于最大数组容量
    if (newCapacity - MAX_ARRAY_SIZE &amp;gt; 0)
        //如果minCapacity大于最大容量，则新容量则为`Integer.MAX_VALUE`
        //否则，新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MAX_ARRAY_SIZE：要分配的数组的最大大小，分配更大的&lt;strong&gt;可能&lt;/strong&gt;会导致&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OutOfMemoryError:Requested array size exceeds VM limit（请求的数组大小超出 VM 限制）&lt;/li&gt;
&lt;li&gt;OutOfMemoryError: Java heap space（堆区内存不足，可以通过设置 JVM 参数 -Xmx 来调节）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除元素：需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上，在旧数组上操作，该操作的时间复杂度为 O(N)，可以看到 ArrayList 删除元素的代价是非常高的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved &amp;gt; 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;序列化：ArrayList 基于数组并且具有动态扩容特性，因此保存元素的数组不一定都会被使用，就没必要全部进行序列化。保存元素的数组 elementData 使用 transient 修饰，该关键字声明数组默认不会被序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; transient Object[] elementData;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ensureCapacity：增加此实例的容量，以确保它至少可以容纳最小容量参数指定的元素数，减少增量重新分配的次数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public void ensureCapacity(int minCapacity) {
     if (minCapacity &amp;gt; elementData.length
         &amp;amp;&amp;amp; !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
              &amp;amp;&amp;amp; minCapacity &amp;lt;= DEFAULT_CAPACITY)) {
         modCount++;
         grow(minCapacity);
     }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fail-Fast&lt;/strong&gt;：快速失败，modCount 用来记录 ArrayList &lt;strong&gt;结构发生变化&lt;/strong&gt;的次数，结构发生变化是指添加或者删除至少一个元素的操作，或者是调整内部数组的大小，仅仅只是设置元素的值不算结构发生变化&lt;/p&gt;
&lt;p&gt;在进行序列化或者迭代等操作时，需要比较操作前后 modCount 是否改变，改变了抛出 ConcurrentModificationException 异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Iterator&amp;lt;E&amp;gt; iterator() {
    return new Itr();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private class Itr implements Iterator&amp;lt;E&amp;gt; {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

   	// 获取下一个元素时首先判断结构是否发生变化
    public E next() {
        checkForComodification();
       	// .....
    }
    // modCount 被其他线程改变抛出并发修改异常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
	// 【允许删除操作】
    public void remove() {
        // ...
        checkForComodification();
        // ...
        // 删除后重置 expectedModCount
        expectedModCount = modCount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Vector&lt;/h5&gt;
&lt;p&gt;同步：Vector 的实现与 ArrayList 类似，但是方法上使用了 synchronized 进行同步&lt;/p&gt;
&lt;p&gt;构造：默认长度为 10 的数组&lt;/p&gt;
&lt;p&gt;扩容：Vector 的构造函数可以传入 capacityIncrement 参数，作用是在扩容时使容量 capacity 增长 capacityIncrement，如果这个参数的值小于等于 0（默认0），扩容时每次都令 capacity 为原来的两倍&lt;/p&gt;
&lt;p&gt;对比 ArrayList&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Vector 是同步的，开销比 ArrayList 要大，访问速度更慢。最好使用 ArrayList 而不是 Vector，因为同步操作完全可以由程序来控制&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vector 每次扩容请求其大小的 2 倍（也可以通过构造函数设置增长的容量），而 ArrayList 是 1.5 倍&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;底层都是 &lt;code&gt;Object[]&lt;/code&gt; 数组存储&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;LinkedList&lt;/h5&gt;
&lt;h6&gt;介绍&lt;/h6&gt;
&lt;p&gt;LinkedList 也是 List 的实现类：基于&lt;strong&gt;双向链表&lt;/strong&gt;实现，使用 Node 存储链表节点信息，增删比较快，查询慢&lt;/p&gt;
&lt;p&gt;LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元素的特殊功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public boolean add(E e)&lt;/code&gt;：将指定元素添加到此列表的结尾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E poll()&lt;/code&gt;：检索并删除此列表的头（第一个元素）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void addFirst(E e)&lt;/code&gt;：将指定元素插入此列表的开头&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void addLast(E e)&lt;/code&gt;：将指定元素添加到此列表的结尾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E pop()&lt;/code&gt;：从此列表所表示的堆栈处弹出一个元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void push(E e)&lt;/code&gt;：将元素推入此列表所表示的堆栈&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int indexOf(Object o)&lt;/code&gt;：返回此列表中指定元素的第一次出现的索引，如果不包含返回 -1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int lastIndexOf(Object o)&lt;/code&gt;：从尾遍历找&lt;/li&gt;
&lt;li&gt;&lt;code&gt; public boolean remove(Object o)&lt;/code&gt;：一次只删除一个匹配的对象，如果删除了匹配对象返回 true&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E remove(int index)&lt;/code&gt;：删除指定位置的元素&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ListDemo {
    public static void main(String[] args) {
        // 1.用LinkedList做一个队列:先进先出，后进后出。
        LinkedList&amp;lt;String&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        // 入队
        queue.addLast(&quot;1号&quot;);
        queue.addLast(&quot;2号&quot;);
        queue.addLast(&quot;3号&quot;);
        System.out.println(queue); // [1号, 2号, 3号]
        // 出队
        System.out.println(queue.removeFirst());//1号
        System.out.println(queue.removeFirst());//2号
        System.out.println(queue);//[3号]

        // 做一个栈 先进后出
        LinkedList&amp;lt;String&amp;gt; stack = new LinkedList&amp;lt;&amp;gt;();
        // 压栈
        stack.push(&quot;第1颗子弹&quot;);//addFirst(e);
        stack.push(&quot;第2颗子弹&quot;);
        stack.push(&quot;第3颗子弹&quot;);
        System.out.println(stack); // [ 第3颗子弹, 第2颗子弹, 第1颗子弹]
        // 弹栈
        System.out.println(stack.pop());//removeFirst(); 第3颗子弹
        System.out.println(stack.pop());
        System.out.println(stack);// [第1颗子弹]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h6&gt;源码&lt;/h6&gt;
&lt;p&gt;LinkedList 是一个实现了 List 接口的&lt;strong&gt;双端链表&lt;/strong&gt;，支持高效的插入和删除操作，另外也实现了 Deque 接口，使得 LinkedList 类也具有队列的特性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/LinkedList%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使 LinkedList 变成线程安全的，可以调用静态类 Collections 类中的 synchronizedList 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List list = Collections.synchronizedList(new LinkedList(...));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;私有内部类 Node：这个类代表双端链表的节点 Node&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static class Node&amp;lt;E&amp;gt; {
    E item;
    Node&amp;lt;E&amp;gt; next;
    Node&amp;lt;E&amp;gt; prev;

    Node(Node&amp;lt;E&amp;gt; prev, E element, Node&amp;lt;E&amp;gt; next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：只有无参构造和用已有的集合创建链表的构造方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加元素：默认加到尾部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean add(E e) {
    linkLast(e);
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取元素：&lt;code&gt;get(int index)&lt;/code&gt; 根据指定索引返回数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取头节点 (index=0)：&lt;code&gt;getFirst()、element()、peek()、peekFirst()&lt;/code&gt; 这四个获取头结点方法的区别在于对链表为空时的处理方式，是抛出异常还是返回NULL，其中 &lt;code&gt;getFirst() element()&lt;/code&gt; 方法将会在链表为空时，抛出异常&lt;/li&gt;
&lt;li&gt;获取尾节点 (index=-1)：getLast() 方法在链表为空时，抛出 NoSuchElementException，而 peekLast() 不会，只会返回 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;remove()、removeFirst()、pop()：删除头节点&lt;/li&gt;
&lt;li&gt;removeLast()、pollLast()：删除尾节点，removeLast()在链表为空时抛出NoSuchElementException，而pollLast()方法返回null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 ArrayList&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是否保证线程安全：ArrayList 和 LinkedList 都是不同步的，也就是不保证线程安全&lt;/li&gt;
&lt;li&gt;底层数据结构：
&lt;ul&gt;
&lt;li&gt;Arraylist 底层使用的是 &lt;code&gt;Object&lt;/code&gt; 数组&lt;/li&gt;
&lt;li&gt;LinkedList 底层使用的是双向链表数据结构（JDK1.6 之前为循环链表，JDK1.7 取消了循环）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;插入和删除是否受元素位置的影响：
&lt;ul&gt;
&lt;li&gt;ArrayList 采用数组存储，所以插入和删除元素受元素位置的影响&lt;/li&gt;
&lt;li&gt;LinkedList采 用链表存储，所以对于&lt;code&gt;add(E e)&lt;/code&gt;方法的插入，删除元素不受元素位置的影响&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;是否支持快速随机访问：
&lt;ul&gt;
&lt;li&gt;LinkedList 不支持高效的随机元素访问，ArrayList 支持&lt;/li&gt;
&lt;li&gt;快速随机访问就是通过元素的序号快速获取元素对象(对应于 &lt;code&gt;get(int index)&lt;/code&gt; 方法)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内存空间占用：
&lt;ul&gt;
&lt;li&gt;ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间&lt;/li&gt;
&lt;li&gt;LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间（因为要存放直接后继和直接前驱以及数据）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;Set&lt;/h4&gt;
&lt;h5&gt;概述&lt;/h5&gt;
&lt;p&gt;Set 系列集合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HashSet：添加的元素是无序，不重复，无索引的&lt;/li&gt;
&lt;li&gt;LinkedHashSet：添加的元素是有序，不重复，无索引的&lt;/li&gt;
&lt;li&gt;TreeSet：不重复，无索引，按照大小默认升序排序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：没有索引，不能使用普通 for 循环遍历&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;HashSet&lt;/h5&gt;
&lt;p&gt;哈希值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;哈希值：JDK 根据对象的地址或者字符串或者数字计算出来的数值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取哈希值：Object 类中的 public int hashCode()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哈希值的特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个对象多次调用 hashCode() 方法返回的哈希值是相同的&lt;/li&gt;
&lt;li&gt;默认情况下，不同对象的哈希值是不同的，而重写 hashCode() 方法，可以实现让不同对象的哈希值相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;HashSet 底层就是基于 HashMap 实现，值是  PRESENT = new Object()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set 集合添加的元素是无序，不重复的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是如何去重复的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.对于有值特性的，Set集合可以直接判断进行去重复。
2.对于引用数据类型的类对象，Set集合是按照如下流程进行是否重复的判断。
    Set集合会让两两对象，先调用自己的hashCode()方法得到彼此的哈希值（所谓的内存地址）
    然后比较两个对象的哈希值是否相同，如果不相同则直接认为两个对象不重复。
    如果哈希值相同，会继续让两个对象进行equals比较内容是否相同，如果相同认为真的重复了
    如果不相同认为不重复。

            Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较
               /                     \
            false                    true
            /                          \
        不重复                        继续让两个对象进行equals比较
                                       /          \
                                     false        true
                                      /             \
                                    不重复          重复了
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set 系列集合元素无序的根本原因&lt;/p&gt;
&lt;p&gt;Set 系列集合添加元素无序的根本原因是因为&lt;strong&gt;底层采用了哈希表存储元素&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8 之前：哈希表 = 数组（初始容量16) + 链表  + （哈希算法）&lt;/li&gt;
&lt;li&gt;JDK 1.8 之后：哈希表 = 数组（初始容量16) + 链表 + 红黑树  + （哈希算法）
&lt;ul&gt;
&lt;li&gt;当链表长度超过阈值 8 且当前数组的长度 &amp;gt; 64时，将链表转换为红黑树，减少了查找时间&lt;/li&gt;
&lt;li&gt;当链表长度超过阈值 8 且当前数组的长度 &amp;lt; 64时，扩容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashSet%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84%E5%93%88%E5%B8%8C%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每个元素的 hashcode() 的值进行响应的算法运算，计算出的值相同的存入一个数组块中，以链表的形式存储，如果链表长度超过8就采取红黑树存储，所以输出的元素是无序的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何设置只要对象内容一样，就希望集合认为重复：&lt;strong&gt;重写 hashCode 和 equals 方法&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Linked&lt;/h5&gt;
&lt;p&gt;LinkedHashSet 为什么是有序的？&lt;/p&gt;
&lt;p&gt;LinkedHashSet 底层依然是使用哈希表存储元素的，但是每个元素都额外带一个链来维护添加顺序，不光增删查快，还有顺序，缺点是多了一个存储顺序的链会&lt;strong&gt;占内存空间&lt;/strong&gt;，而且不允许重复，无索引&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;TreeSet&lt;/h5&gt;
&lt;p&gt;TreeSet 集合自排序的方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有值特性的元素直接可以升序排序（浮点型，整型）&lt;/li&gt;
&lt;li&gt;字符串类型的元素会按照首字符的编号排序&lt;/li&gt;
&lt;li&gt;对于自定义的引用数据类型，TreeSet 默认无法排序，执行的时候报错，因为不知道排序规则&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;自定义的引用数据类型，TreeSet 默认无法排序，需要定制排序的规则，方案有 2 种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接为&lt;strong&gt;对象的类&lt;/strong&gt;实现比较器规则接口 Comparable，重写比较方法：&lt;/p&gt;
&lt;p&gt;方法：&lt;code&gt;public int compareTo(Employee o): this 是比较者, o 是被比较者&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  * 比较者大于被比较者，返回正数
  * 比较者小于被比较者，返回负数
  * 比较者等于被比较者，返回 0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接为&lt;strong&gt;集合&lt;/strong&gt;设置比较器 Comparator 对象，重写比较方法：&lt;/p&gt;
&lt;p&gt;方法：&lt;code&gt;public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较者大于被比较者，返回正数&lt;/li&gt;
&lt;li&gt;比较者小于被比较者，返回负数&lt;/li&gt;
&lt;li&gt;比较者等于被比较者，返回 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：如果类和集合都带有比较规则，优先使用集合自带的比较规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TreeSetDemo{
    public static void main(String[] args){
        Set&amp;lt;Student&amp;gt; students = new TreeSet&amp;lt;&amp;gt;();
		Collections.add(students,s1,s2,s3);
        System.out.println(students);//按照年龄比较 升序
        
        Set&amp;lt;Student&amp;gt; s = new TreeSet&amp;lt;&amp;gt;(new Comparator&amp;lt;Student&amp;gt;(){
            @Override
            public int compare(Student o1, Student o2) {
                // o1比较者   o2被比较者
                return o2.getAge() - o1.getAge();//降序
            }
        });
    }
}

public class Student implements Comparable&amp;lt;Student&amp;gt;{
    private String name;
    private int age;
    // 重写了比较方法。
    // e1.compareTo(o)
    // 比较者：this
    // 被比较者：o
    // 需求：按照年龄比较 升序，年龄相同按照姓名
    @Override
    public int compareTo(Student o) {
        int result = this.age - o.age;
        return result == 0 ? this.getName().compareTo(o.getName):result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较器原理：底层是以第一个元素为基准，加一个新元素，就会和第一个元素比，如果大于，就继续和大于的元素进行比较，直到遇到比新元素大的元素为止，放在该位置的左边（红黑树）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Queue&lt;/h4&gt;
&lt;p&gt;Queue：队列，先进先出的特性&lt;/p&gt;
&lt;p&gt;PriorityQueue 是优先级队列，底层存储结构为 Object[]，默认实现为小顶堆，每次出队最小的元素&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public PriorityQueue()&lt;/code&gt;：构造默认长度为 11 的队列（数组）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public PriorityQueue(Comparator&amp;lt;? super E&amp;gt; comparator)&lt;/code&gt;：利用比较器自定义堆排序的规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Queue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((v1, v2) -&amp;gt; v2 - v1);//实现大顶堆

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public boolean offer(E e)&lt;/code&gt;：将指定的元素插入到此优先级队列的&lt;strong&gt;尾部&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E poll() &lt;/code&gt;：检索并删除此队列的&lt;strong&gt;头元素&lt;/strong&gt;，如果此队列为空，则返回 null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public E peek()&lt;/code&gt;：检索但不删除此队列的头，如果此队列为空，则返回 null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean remove(Object o)&lt;/code&gt;：从该队列中删除指定元素（如果存在），删除元素 e 使用 o.equals(e) 比较，如果队列包含多个这样的元素，删除第一个&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Collections&lt;/h4&gt;
&lt;p&gt;java.utils.Collections：集合&lt;strong&gt;工具类&lt;/strong&gt;，Collections 并不属于集合，是用来操作集合的工具类&lt;/p&gt;
&lt;p&gt;Collections 有几个常用的API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; boolean addAll(Collection&amp;lt;? super T&amp;gt; c, T... e)&lt;/code&gt;：给集合对象批量添加元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static void shuffle(List&amp;lt;?&amp;gt; list)&lt;/code&gt;：打乱集合顺序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void sort(List&amp;lt;T&amp;gt; list)&lt;/code&gt;：将集合中元素按照默认规则排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void sort(List&amp;lt;T&amp;gt; list,Comparator&amp;lt;? super T&amp;gt; )&lt;/code&gt;：集合中元素按照指定规则排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; synchronizedList(List&amp;lt;T&amp;gt; list)&lt;/code&gt;：返回由指定 list 支持的线程安全 list&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; Set&amp;lt;T&amp;gt; singleton(T o)&lt;/code&gt;：返回一个只包含指定对象的不可变组&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class CollectionsDemo {
    public static void main(String[] args) {
        Collection&amp;lt;String&amp;gt; names = new ArrayList&amp;lt;&amp;gt;();
        Collections.addAll(names,&quot;张&quot;,&quot;王&quot;,&quot;李&quot;,&quot;赵&quot;);
        
        List&amp;lt;Double&amp;gt; scores = new ArrayList&amp;lt;&amp;gt;();
        Collections.addAll(scores, 98.5, 66.5 , 59.5 , 66.5 , 99.5 );
        Collections.shuffle(scores);
        Collections.sort(scores); // 默认升序排序！
        System.out.println(scores);
        
        List&amp;lt;Student&amp;gt; students = new ArrayList&amp;lt;&amp;gt;();
        Collections.addAll(students,s1,s2,s3,s4);
        Collections.sort(students,new Comparator&amp;lt;Student&amp;gt;(){
            
        })
    }
}

public class Student{
    private String name;
    private int age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Map&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;Collection 是单值集合体系，Map集合是一种双列集合，每个元素包含两个值。&lt;/p&gt;
&lt;p&gt;Map集合的每个元素的格式：key=value（键值对元素），Map集合也被称为键值对集合&lt;/p&gt;
&lt;p&gt;Map集合的完整格式：&lt;code&gt;{key1=value1, key2=value2, key3=value3, ...}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map集合的体系：
        Map&amp;lt;K , V&amp;gt;(接口,Map集合的祖宗类)
       /                      \
      TreeMap&amp;lt;K , V&amp;gt;           HashMap&amp;lt;K , V&amp;gt;(实现类,经典的，用的最多)
                                 \
                                  LinkedHashMap&amp;lt;K, V&amp;gt;(实现类)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Map 集合的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Map 集合的特点都是由键决定的&lt;/li&gt;
&lt;li&gt;Map 集合的键是无序，不重复的，无索引的（Set）&lt;/li&gt;
&lt;li&gt;Map 集合的值无要求（List）&lt;/li&gt;
&lt;li&gt;Map 集合的键值对都可以为 null&lt;/li&gt;
&lt;li&gt;Map 集合后面重复的键对应元素会覆盖前面的元素&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;HashMap：元素按照键是无序，不重复，无索引，值不做要求&lt;/p&gt;
&lt;p&gt;LinkedHashMap：元素按照键是有序，不重复，无索引，值不做要求&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;Map 集合的常用 API&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public V put(K key, V value)&lt;/code&gt;：把指定的键与值添加到 Map 集合中，&lt;strong&gt;重复的键会覆盖前面的值元素&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V remove(Object key)&lt;/code&gt;：把指定的键对应的键值对元素在集合中删除，返回被删除元素的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V get(Object key)&lt;/code&gt;：根据指定的键，在 Map 集合中获取对应的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Set&amp;lt;K&amp;gt; keySet()&lt;/code&gt;：获取 Map 集合中所有的键，存储到 &lt;strong&gt;Set 集合&lt;/strong&gt;中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Collection&amp;lt;V&amp;gt; values()&lt;/code&gt;：获取全部值的集合，存储到 &lt;strong&gt;Collection 集合&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Set&amp;lt;Map.Entry&amp;lt;K,V&amp;gt;&amp;gt; entrySet()&lt;/code&gt;：获取Map集合中所有的键值对对象的集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean containsKey(Object key)&lt;/code&gt;：判断该集合中是否有此键&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MapDemo {
    public static void main(String[] args) {
        Map&amp;lt;String , Integer&amp;gt; maps = new HashMap&amp;lt;&amp;gt;();
        maps.put(.....);
        System.out.println(maps.isEmpty());//false
        Integer value = maps.get(&quot;....&quot;);//返回键值对象
        Set&amp;lt;String&amp;gt; keys = maps.keySet();//获取Map集合中所有的键，
        //Map集合的键是无序不重复的，所以返回的是一个Set集合
        Collection&amp;lt;Integer&amp;gt; values = maps.values();
        //Map集合的值是不做要求的，可能重复，所以值要用Collection集合接收!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;遍历方式&lt;/h4&gt;
&lt;p&gt;Map集合的遍历方式有：3种。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;“键找值”的方式遍历：先获取 Map 集合全部的键，再根据遍历键找值。&lt;/li&gt;
&lt;li&gt;“键值对”的方式遍历：难度较大，采用增强 for 或者迭代器&lt;/li&gt;
&lt;li&gt;JDK 1.8 开始之后的新技术：foreach，采用 Lambda 表达式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;集合可以直接输出内容，因为底层重写了 toString() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    Map&amp;lt;String , Integer&amp;gt; maps = new HashMap&amp;lt;&amp;gt;();
	//(1)键找值
    Set&amp;lt;String&amp;gt; keys = maps.keySet();
    for(String key : keys) {
        System.out.println(key + &quot;=&quot; + maps.get(key));
    }
    //Iterator&amp;lt;String&amp;gt; iterator = hm.keySet().iterator();
    
    //(2)键值对
    //(2.1)普通方式
    Set&amp;lt;Map.Entry&amp;lt;String,Integer&amp;gt;&amp;gt; entries = maps.entrySet();
    for (Map.Entry&amp;lt;String, Integer&amp;gt; entry : entries) {
             System.out.println(entry.getKey() + &quot;=&amp;gt;&quot; + entry.getValue());
    }
    //(2.2)迭代器方式
    Iterator&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt; iterator = maps.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry&amp;lt;String, Integer&amp;gt; entry = iterator.next();
        System.out.println(entry.getKey() + &quot;=&quot; + entry.getValue());

    }
    //(3) Lamda
    maps.forEach((k,v) -&amp;gt; {
        System.out.println(k + &quot;==&amp;gt;&quot; + v);
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;HashMap&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;HashMap 基于哈希表的 Map 接口实现，是以 key-value 存储形式存在，主要用来存放键值对&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HashMap 的实现不是同步的，这意味着它不是线程安全的&lt;/li&gt;
&lt;li&gt;key 是唯一不重复的，底层的哈希表结构，依赖 hashCode 方法和 equals 方法保证键的唯一&lt;/li&gt;
&lt;li&gt;key、value 都可以为null，但是 key 位置只能是一个null&lt;/li&gt;
&lt;li&gt;HashMap 中的映射不是有序的，即存取是无序的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key 要存储的是自定义对象，需要重写 hashCode 和 equals 方法，防止出现地址不同内容相同的 key&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK7 对比 JDK8：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;7 = 数组 + 链表，8 = 数组 + 链表 + 红黑树&lt;/li&gt;
&lt;li&gt;7 中是头插法，多线程容易造成环，8 中是尾插法&lt;/li&gt;
&lt;li&gt;7 的扩容是全部数据重新定位，8 中是位置不变或者当前位置 + 旧 size 大小来实现&lt;/li&gt;
&lt;li&gt;7 是先判断是否要扩容再插入，8 中是先插入再看是否要扩容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;底层数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;哈希表（Hash table，也叫散列表），根据关键码值而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录，以加快查找的速度，这个映射函数叫做散列函数，存放记录的数组叫做散列表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK1.8 之前 HashMap 由数组+链表组成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组是 HashMap 的主体&lt;/li&gt;
&lt;li&gt;链表则是为了解决哈希冲突而存在的（&lt;strong&gt;拉链法解决冲突&lt;/strong&gt;），拉链法就是头插法，两个对象调用的 hashCode 方法计算的哈希码值（键的哈希）一致导致计算的数组索引值相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK1.8 以后 HashMap 由&lt;strong&gt;数组+链表 +红黑树&lt;/strong&gt;数据结构组成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决哈希冲突时有了较大的变化&lt;/li&gt;
&lt;li&gt;当链表长度&lt;strong&gt;超过（大于）阈值&lt;/strong&gt;（或者红黑树的边界值，默认为 8）并且当前数组的&lt;strong&gt;长度大于等于 64 时&lt;/strong&gt;，此索引位置上的所有数据改为红黑树存储&lt;/li&gt;
&lt;li&gt;即使哈希函数取得再好，也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时，就相当于一个长的单链表，假如单链表有 n 个元素，遍历的&lt;strong&gt;时间复杂度是 O(n)&lt;/strong&gt;，所以 JDK1.8 中引入了 红黑树（查找&lt;strong&gt;时间复杂度为 O(logn)&lt;/strong&gt;）来优化这个问题，使得查找效率更高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1nJ411J7AA&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;继承关系&lt;/h5&gt;
&lt;p&gt;HashMap 继承关系如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap%E7%BB%A7%E6%89%BF%E5%85%B3%E7%B3%BB.bmp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloneable 空接口，表示可以克隆， 创建并返回 HashMap 对象的一个副本。&lt;/li&gt;
&lt;li&gt;Serializable 序列化接口，属于标记性接口，HashMap 对象可以被序列化和反序列化。&lt;/li&gt;
&lt;li&gt;AbstractMap 父类提供了 Map 实现接口，以最大限度地减少实现此接口所需的工作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;序列化版本号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final long serialVersionUID = 362498820763181265L;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合的初始化容量（&lt;strong&gt;必须是二的 n 次幂&lt;/strong&gt; ）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 默认的初始容量是16 -- 1&amp;lt;&amp;lt;4相当于1*2的4次方---1*16
static final int DEFAULT_INITIAL_CAPACITY = 1 &amp;lt;&amp;lt; 4;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HashMap 构造方法指定集合的初始化容量大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么必须是 2 的 n 次幂？用位运算替代取余计算，减少 rehash 的代价（移动的节点少）&lt;/p&gt;
&lt;p&gt;HashMap 中添加元素时，需要根据 key 的 hash 值确定在数组中的具体位置。为了减少碰撞，把数据分配均匀，每个链表长度大致相同，实现该方法就是取模 &lt;code&gt;hash%length&lt;/code&gt;，计算机中直接求余效率不如位移运算， &lt;strong&gt;&lt;code&gt;hash % length == hash &amp;amp; (length-1)&lt;/code&gt; 的前提是 length 是 2 的 n 次幂&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;散列平均分布：2 的 n 次方是 1 后面 n 个 0，2 的 n 次方 -1 是 n 个 1，可以&lt;strong&gt;保证散列的均匀性&lt;/strong&gt;，减少碰撞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例如长度为8时候，3&amp;amp;(8-1)=3  2&amp;amp;(8-1)=2 ，不同位置上，不碰撞；
例如长度为9时候，3&amp;amp;(9-1)=0  2&amp;amp;(9-1)=0 ，都在0上，碰撞了；
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果输入值不是 2 的幂会怎么样？&lt;/p&gt;
&lt;p&gt;创建 HashMap 对象时，HashMap 通过位移运算和或运算得到的肯定是 2 的幂次数，并且是大于那个数的最近的数字，底层采用 tableSizeFor() 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认的负载因子，默认值是 0.75&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final float DEFAULT_LOAD_FACTOR = 0.75f;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合最大容量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 集合最大容量的上限是：2的30次幂
static final int MAXIMUM_CAPACITY = 1 &amp;lt;&amp;lt; 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当链表的值超过 8 则会转红黑树（JDK1.8 新增）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么 Map 桶中节点个数大于 8 才转为红黑树？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 HashMap 中有一段注释说明：&lt;strong&gt;空间和时间的权衡&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNodes占用空间大约是普通节点的两倍，所以我们只在箱子包含足够的节点时才使用树节点。当节点变少(由于删除或调整大小)时，就会被转换回普通的桶。在使用分布良好的用户hashcode时，很少使用树箱。理想情况下，在随机哈希码下，箱子中节点的频率服从&quot;泊松分布&quot;，默认调整阈值为0.75，平均参数约为0.5，尽管由于调整粒度的差异很大。忽略方差，列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k))
0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million
一个bin中链表长度达到8个元素的概率为0.00000006，几乎是不可能事件，所以我们选择8这个数字
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他说法
红黑树的平均查找长度是 log(n)，如果长度为 8，平均查找长度为 log(8)=3，链表的平均查找长度为 n/2，当长度为 8 时，平均查找长度为 8/2=4，这才有转换成树的必要；链表长度如果是小于等于 6，6/2=3，而 log(6)=2.6，虽然速度也很快的，但转化为树结构和生成树的时间并不短&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当链表的值小于 6 则会从红黑树转回链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 Map 里面的数量&lt;strong&gt;大于等于&lt;/strong&gt;这个阈值时，表中的桶才能进行树形化 ，否则桶内元素超过 8 时会扩容，而不是树形化。为了避免进行扩容、树形化选择的冲突，这个值不能小于 4 * TREEIFY_THRESHOLD (8)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 桶中结构转化为红黑树对应的数组长度最小的值 
static final int MIN_TREEIFY_CAPACITY = 64;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：数组比较小的情况下变为红黑树结构，反而会降低效率，红黑树需要进行左旋，右旋，变色这些操作来保持平衡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;table 用来初始化（必须是二的 n 次幂）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 存储元素的数组 
transient Node&amp;lt;K,V&amp;gt;[] table;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HashMap 中&lt;strong&gt;存放元素的个数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 存放元素的个数，HashMap中K-V的实时数量，不是table数组的长度
transient int size;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录 HashMap 的修改次数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 每次扩容和更改map结构的计数器
 transient int modCount;  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调整大小下一个容量的值计算方式为：容量 * 负载因子，容量是数组的长度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 临界值，当实际大小(容量*负载因子)超过临界值时，会进行扩容
int threshold;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;哈希表的加载因子&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; final float loadFactor;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加载因子的概述&lt;/p&gt;
&lt;p&gt;loadFactor 加载因子，是用来衡量 HashMap 满的程度，表示 HashMap 的疏密程度，影响 hash 操作到同一个数组位置的概率，计算 HashMap 的实时加载因子的方法为 &lt;strong&gt;size/capacity&lt;/strong&gt;，而不是占用桶的数量去除以 capacity，capacity 是桶的数量，也就是 table 的长度 length&lt;/p&gt;
&lt;p&gt;当 HashMap 容纳的元素已经达到数组长度的 75% 时，表示 HashMap 拥挤需要扩容，而扩容这个过程涉及到 rehash、复制数据等操作，非常消耗性能，所以开发中尽量减少扩容的次数，通过创建 HashMap 集合对象时指定初始容量来避免&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么加载因子设置为 0.75，初始化临界值是 12？&lt;/p&gt;
&lt;p&gt;loadFactor 太大导致查找元素效率低，存放的数据拥挤，太小导致数组的利用率低，存放的数据会很分散。loadFactor 的默认值为 &lt;strong&gt;0.75f 是官方给出的一个比较好的临界值&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;threshold 计算公式：capacity（数组长度默认16） * loadFactor（默认 0.75）。当 size &amp;gt;= threshold 的时候，那么就要考虑对数组的 resize（扩容），这就是衡量数组是否需要扩增的一个标准， 扩容后的 HashMap 容量是之前容量的&lt;strong&gt;两倍&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;构造方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造一个空的 HashMap ，&lt;strong&gt;默认初始容量（16）和默认负载因子（0.75）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
	// 将默认的加载因子0.75赋值给loadFactor，并没有创建数组
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造一个具有指定的初始容量和默认负载因子（0.75）HashMap&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造一个具有指定的初始容量和负载因子的 HashMap&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public HashMap(int initialCapacity, float loadFactor) {
    // 进行判断
    // 将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor
    this.loadFactor = loadFactor;
  	// 最后调用了tableSizeFor
    this.threshold = tableSizeFor(initialCapacity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于 &lt;code&gt;this.threshold = tableSizeFor(initialCapacity)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;JDK8 以后的构造方法中，并没有对 table 这个成员变量进行初始化，table 的初始化被推迟到了 put 方法中，在 put 方法中会对 threshold 重新计算&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;包含另一个 &lt;code&gt;Map&lt;/code&gt; 的构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 构造一个映射关系与指定 Map 相同的新 HashMap
public HashMap(Map&amp;lt;? extends K, ? extends V&amp;gt; m) {
    // 负载因子loadFactor变为默认的负载因子0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;putMapEntries 源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final void putMapEntries(Map&amp;lt;? extends K, ? extends V&amp;gt; m, boolean evict) {
    //获取参数集合的长度
    int s = m.size();
    if (s &amp;gt; 0) {
        //判断参数集合的长度是否大于0
        if (table == null) {  // 判断table是否已经初始化
            // pre-size
            // 未初始化，s为m的实际元素个数
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft &amp;lt; (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            // 计算得到的t大于阈值，则初始化阈值
            if (t &amp;gt; threshold)
                threshold = tableSizeFor(t);
        }
        // 已初始化，并且m元素个数大于阈值，进行扩容处理
        else if (s &amp;gt; threshold)
            resize();
        // 将m中的所有元素添加至HashMap中
        for (Map.Entry&amp;lt;? extends K, ? extends V&amp;gt; e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;float ft = ((float)s / loadFactor) + 1.0F&lt;/code&gt; 这一行代码中为什么要加 1.0F ？&lt;/p&gt;
&lt;p&gt;s / loadFactor 的结果是小数，加 1.0F 相当于是对小数做一个向上取整以尽可能的保证更大容量，更大的容量能够减少 resize 的调用次数，这样可以减少数组的扩容&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;hash()：HashMap 是支持 Key 为空的；HashTable 是直接用 Key 来获取 HashCode，key 为空会抛异常&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;amp;（按位与运算）：相同的二进制数位上，都是 1 的时候，结果为 1，否则为零&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;^（按位异或运算）：相同的二进制数位上，数字相同，结果为 0，不同为 1，&lt;strong&gt;不进位加法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;0 1 相互做 &amp;amp; | ^ 运算，结果出现 0 和 1 的数量分别是 3:1、1:3、1:1，所以异或是最平均的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static final int hash(Object key) {
    int h;
    // 1）如果key等于null：可以看到当key等于null的时候也是有哈希值的，返回的是0
    // 2）如果key不等于null：首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算 hash 的方法：将 hashCode 无符号右移 16 位，高 16bit 和低 16bit 做异或，扰动运算&lt;/p&gt;
&lt;p&gt;原因：当数组长度很小，假设是 16，那么 n-1 即为 1111 ，这样的值和 hashCode() 直接做按位与操作，实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大，低位变化很小，就很容易造成哈希冲突了，所以这里&lt;strong&gt;把高低位都利用起来，让高16 位也参与运算&lt;/strong&gt;，从而解决了这个问题&lt;/p&gt;
&lt;p&gt;哈希冲突的处理方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开放定址法：线性探查法（ThreadLocalMap 使用），平方探查法（i + 1^2、i - 1^2、i + 2^2……）、双重散列（多个哈希函数）&lt;/li&gt;
&lt;li&gt;链地址法：拉链法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;put()：jdk1.8 前是头插法 (链地址法)，多线程下扩容出现循环链表，jdk1.8 以后引入红黑树，插入方法变成尾插法&lt;/p&gt;
&lt;p&gt;第一次调用 put 方法时创建数组 Node[] table，因为散列表耗费内存，为了防止内存浪费，所以&lt;strong&gt;延迟初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;存储数据步骤（存储过程）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先通过 hash 值计算出 key 映射到哪个桶，哈希寻址&lt;/li&gt;
&lt;li&gt;如果桶上没有碰撞冲突，则直接插入&lt;/li&gt;
&lt;li&gt;如果出现碰撞冲突：如果该桶使用红黑树处理冲突，则调用红黑树的方法插入数据；否则采用传统的链式方法插入，如果链的长度达到临界值，则把链转变为红黑树&lt;/li&gt;
&lt;li&gt;如果数组位置相同，通过 equals 比较内容是否相同：相同则新的 value 覆盖旧 value，不相同则将新的键值对添加到哈希表中&lt;/li&gt;
&lt;li&gt;最后判断 size 是否大于阈值 threshold，则进行扩容&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;putVal() 方法中 key 在这里执行了一下 hash()，在 putVal 函数中使用到了上述 hash 函数计算的哈希值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  	//。。。。。。。。。。。。。。
  	if ((p = tab[i = (n - 1) &amp;amp; hash]) == null){//这里的n表示数组长度16
  		//.....
      } else {
          if (e != null) { // existing mapping for key
              V oldValue = e.value;
              //onlyIfAbsent默认为false，所以可以覆盖已经存在的数据，如果为true说明不能覆盖
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              afterNodeAccess(e);
              // 如果这里允许覆盖，就直接返回了
              return oldValue;
          }
      }
    // 如果是添加操作，modCount ++，如果不是替换，不会走这里的逻辑，modCount用来记录逻辑的变化
    ++modCount;
    // 数量大于扩容阈值
    if (++size &amp;gt; threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(n - 1) &amp;amp; hash&lt;/code&gt;：计算下标位置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap-putVal哈希运算.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;余数本质是不断做除法，把剩余的数减去，运算效率要比位运算低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;treeifyBin()&lt;/p&gt;
&lt;p&gt;节点添加完成之后判断此时节点个数是否大于 TREEIFY_THRESHOLD 临界值 8，如果大于则将链表转换为红黑树，转换红黑树的方法 treeifyBin，整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (binCount &amp;gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
   //转换为红黑树 tab表示数组名  hash表示哈希值
   treeifyBin(tab, hash);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果当前数组为空或者数组的长度小于进行树形化的阈 MIN_TREEIFY_CAPACITY = 64 就去扩容，而不是将节点变为红黑树&lt;/li&gt;
&lt;li&gt;如果是树形化遍历桶中的元素，创建相同个数的树形节点，复制内容，建立起联系，类似单向链表转换为双向链表&lt;/li&gt;
&lt;li&gt;让桶中的第一个元素即数组中的元素指向新建的红黑树的节点，以后这个桶里的元素就是红黑树而不是链表数据结构了&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tableSizeFor()：创建 HashMap 指定容量时，HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的 2 的 n 次幂&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int tableSizeFor(int cap) {//int cap = 10
    int n = cap - 1;
    n |= n &amp;gt;&amp;gt;&amp;gt; 1;
    n |= n &amp;gt;&amp;gt;&amp;gt; 2;
    n |= n &amp;gt;&amp;gt;&amp;gt; 4;
    n |= n &amp;gt;&amp;gt;&amp;gt; 8;
    n |= n &amp;gt;&amp;gt;&amp;gt; 16;
    return (n &amp;lt; 0) ? 1 : (n &amp;gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析算法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;int n = cap - 1&lt;/code&gt;：防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂， 不执行减 1 操作，则执行完后面的无符号右移操作之后，返回的 capacity 将是这个 cap 的 2 倍&lt;/li&gt;
&lt;li&gt;n=0 （cap-1 之后），则经过后面的几次无符号右移依然是 0，返回的 capacity 是 1，最后有 n+1&lt;/li&gt;
&lt;li&gt;|（按位或运算）：相同的二进制数位上，都是 0 的时候，结果为 0，否则为 1&lt;/li&gt;
&lt;li&gt;核心思想：&lt;strong&gt;把最高位是 1 的位以及右边的位全部置 1&lt;/strong&gt;，结果加 1 后就是大于指定容量的最小的 2 的 n 次幂&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如初始化的值为 10：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一次右移&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n = cap - 1;//cap=10  n=9
n |= n &amp;gt;&amp;gt;&amp;gt; 1;
00000000 00000000 00000000 00001001 //9
00000000 00000000 00000000 00000100 //9右移之后变为4
--------------------------------------------------
00000000 00000000 00000000 00001101 //按位或之后是13
//使得n的二进制表示中与最高位的1紧邻的右边一位为1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二次右移&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n |= n &amp;gt;&amp;gt;&amp;gt; 2;//n通过第一次右移变为了：n=13
00000000 00000000 00000000 00001101  // 13
00000000 00000000 00000000 00000011  // 13右移之后变为3
-------------------------------------------------
00000000 00000000 00000000 00001111	 //按位或之后是15
//无符号右移两位，会将最高位两个连续的1右移两位，然后再与原来的n做或操作，这样n的二进制表示的高位中会有4个连续的1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：容量最大是 32bit 的正数，因此最后 &lt;code&gt;n |= n &amp;gt;&amp;gt;&amp;gt; 16&lt;/code&gt;，最多是 32 个 1（但是这已经是负数了）。在执行 tableSizeFor 之前，对 initialCapacity 做了判断，如果大于 MAXIMUM_CAPACITY(2 ^ 30)，则取 MAXIMUM_CAPACITY；如果小于 MAXIMUM_CAPACITY(2 ^ 30)，会执行移位操作，所以移位操作之后，最大 30 个 1，加 1 之后得 2 ^ 30&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;得到的 capacity 被赋值给了 threshold&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 11&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int tableSizeFor(int cap) {
    //无符号右移，高位补0
	//-1补码: 11111111 11111111 11111111 11111111
    int n = -1 &amp;gt;&amp;gt;&amp;gt; Integer.numberOfLeadingZeros(cap - 1);
    return (n &amp;lt; 0) ? 1 : (n &amp;gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//返回最高位之前的0的位数
public static int numberOfLeadingZeros(int i) {
    if (i &amp;lt;= 0)
        return i == 0 ? 32 : 0;
    // 如果i&amp;gt;0，那么就表明在二进制表示中其至少有一位为1
    int n = 31;
    // i的最高位1在高16位，把i右移16位，让最高位1进入低16位继续递进判断
    if (i &amp;gt;= 1 &amp;lt;&amp;lt; 16) { n -= 16; i &amp;gt;&amp;gt;&amp;gt;= 16; }
    if (i &amp;gt;= 1 &amp;lt;&amp;lt;  8) { n -=  8; i &amp;gt;&amp;gt;&amp;gt;=  8; }
    if (i &amp;gt;= 1 &amp;lt;&amp;lt;  4) { n -=  4; i &amp;gt;&amp;gt;&amp;gt;=  4; }
    if (i &amp;gt;= 1 &amp;lt;&amp;lt;  2) { n -=  2; i &amp;gt;&amp;gt;&amp;gt;=  2; }
    return n - (i &amp;gt;&amp;gt;&amp;gt; 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resize()：&lt;/p&gt;
&lt;p&gt;当 HashMap 中的&lt;strong&gt;元素个数&lt;/strong&gt;超过 &lt;code&gt;(数组长度)*loadFactor(负载因子)&lt;/code&gt; 或者链表过长时（链表长度 &amp;gt; 8，数组长度 &amp;lt; 64），就会进行数组扩容，创建新的数组，伴随一次重新 hash 分配，并且遍历 hash 表中所有的元素非常耗时，所以要尽量避免 resize&lt;/p&gt;
&lt;p&gt;扩容机制为扩容为原来容量的 2 倍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (oldCap &amp;gt; 0) {
    if (oldCap &amp;gt;= MAXIMUM_CAPACITY) {
        // 以前的容量已经是最大容量了，这时调大 扩容阈值 threshold
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }
    else if ((newCap = oldCap &amp;lt;&amp;lt; 1) &amp;lt; MAXIMUM_CAPACITY &amp;amp;&amp;amp;
             oldCap &amp;gt;= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr &amp;lt;&amp;lt; 1; // double threshold
}
else if (oldThr &amp;gt; 0) // 初始化的threshold赋值给newCap
    newCap = oldThr;
else { 
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HashMap 在进行扩容后，节点&lt;strong&gt;要么就在原来的位置，要么就被分配到&quot;原位置+旧容量&quot;的位置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;判断：e.hash 与 oldCap 对应的有效高位上的值是 1，即当前数组长度 n 二进制为 1 的位为 x 位，如果 key 的哈希值 x 位也为 1，则扩容后的索引为 now + n&lt;/p&gt;
&lt;p&gt;注意：这里要求&lt;strong&gt;数组长度 2 的幂&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap-resize%E6%89%A9%E5%AE%B9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;普通节点：把所有节点分成高低位两个链表，转移到数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 遍历所有的节点
do {
    next = e.next;
    // oldCap 旧数组大小，2 的 n 次幂
    if ((e.hash &amp;amp; oldCap) == 0) {
        if (loTail == null)
            loHead = e;	//指向低位链表头节点
        else
            loTail.next = e;
        loTail = e;		//指向低位链表尾节点
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

if (loTail != null) {
    loTail.next = null;	// 低位链表的最后一个节点可能在原哈希表中指向其他节点，需要断开
    newTab[j] = loHead;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;红黑树节点：扩容时 split 方法会将树&lt;strong&gt;拆成高位和低位两个链表&lt;/strong&gt;，判断长度是否小于等于 6&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//如果低位链表首节点不为null，说明有这个链表存在
if (loHead != null) {
    //如果链表下的元素小于等于6
    if (lc &amp;lt;= UNTREEIFY_THRESHOLD)
        //那就从红黑树转链表了，低位链表，迁移到新数组中下标不变，还是等于原数组到下标
        tab[index] = loHead.untreeify(map);
    else {
        //低位链表，迁移到新数组中下标不变，把低位链表整个赋值到这个下标下
        tab[index] = loHead;
        //如果高位首节点不为空，说明原来的红黑树已经被拆分成两个链表了
        if (hiHead != null)
            //需要构建新的红黑树了
            loHead.treeify(tab);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;remove()：删除是首先先找到元素的位置，如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除，树小于 6 的时候退化为链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; final Node&amp;lt;K,V&amp;gt; removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
     Node&amp;lt;K,V&amp;gt;[] tab; Node&amp;lt;K,V&amp;gt; p; int n, index;
     // 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p，
     // 该节点为树的根节点或链表的首节点）不为空，从该节点p向下遍历，找到那个和key匹配的节点对象
     if ((tab = table) != null &amp;amp;&amp;amp; (n = tab.length) &amp;gt; 0 &amp;amp;&amp;amp;
         (p = tab[index = (n - 1) &amp;amp; hash]) != null) {
         Node&amp;lt;K,V&amp;gt; node = null, e; K k; V v;//临时变量，储存要返回的节点信息
         //key和value都相等，直接返回该节点
         if (p.hash == hash &amp;amp;&amp;amp;
             ((k = p.key) == key || (key != null &amp;amp;&amp;amp; key.equals(k))))
             node = p;
         
         else if ((e = p.next) != null) {
             //如果是树节点，调用getTreeNode方法从树结构中查找满足条件的节点
             if (p instanceof TreeNode)
                 node = ((TreeNode&amp;lt;K,V&amp;gt;)p).getTreeNode(hash, key);
             //遍历链表
             else {
                 do {
                     //e节点的键是否和key相等，e节点就是要删除的节点，赋值给node变量
                     if (e.hash == hash &amp;amp;&amp;amp;
                         ((k = e.key) == key ||
                          (key != null &amp;amp;&amp;amp; key.equals(k)))) {
                         node = e;
                         //跳出循环
                         break;
                     }
                     p = e;//把当前节点p指向e 继续遍历
                 } while ((e = e.next) != null);
             }
         }
         //如果node不为空，说明根据key匹配到了要删除的节点
         //如果不需要对比value值或者对比value值但是value值也相等，可以直接删除
         if (node != null &amp;amp;&amp;amp; (!matchValue || (v = node.value) == value ||
                              (value != null &amp;amp;&amp;amp; value.equals(v)))) {
             if (node instanceof TreeNode)
                 ((TreeNode&amp;lt;K,V&amp;gt;)node).removeTreeNode(this, tab, movable);
             else if (node == p)//node是首节点
                 tab[index] = node.next;
             else	//node不是首节点
                 p.next = node.next;
             ++modCount;
             --size;
             //LinkedHashMap
             afterNodeRemoval(node);
             return node;
         }
     }
     return null;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get()&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过 hash 值获取该 key 映射到的桶&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;桶上的 key 就是要查找的 key，则直接找到并返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;桶上的 key 不是要找的 key，则查看后续的节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果后续节点是红黑树节点，通过调用红黑树的方法根据 key 获取 value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果后续节点是链表节点，则通过循环遍历链表根据 key 获取 value&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;红黑树节点调用的是 getTreeNode 方法通过树形节点的 find 方法进行查&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找红黑树，之前添加时已经保证这个树是有序的，因此查找时就是折半查找，效率更高。&lt;/li&gt;
&lt;li&gt;这里和插入时一样，如果对比节点的哈希值相等并且通过 equals 判断值也相等，就会判断 key 相等，直接返回，不相等就从子树中递归查找&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度 O(1)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若为树，则在树中通过 key.equals(k) 查找，&lt;strong&gt;O(logn)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;若为链表，则在链表中通过 key.equals(k) 查找，&lt;strong&gt;O(n)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;并发异常&lt;/h5&gt;
&lt;p&gt;HashMap 和 ArrayList 一样，内部采用 modCount 用来记录集合结构发生变化的次数，结构发生变化是指添加或者删除至少一个元素的所有操作，或者是调整内部数组的大小，仅仅只是设置元素的值不算结构发生变化&lt;/p&gt;
&lt;p&gt;在进行序列化或者迭代等操作时，需要比较操作前后 modCount 是否改变，如果&lt;strong&gt;其他线程此时修改了集合内部的结构&lt;/strong&gt;，就会直接抛出 ConcurrentModificationException 异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HashMap map = new HashMap();
Iterator iterator = map.keySet().iterator();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;final class KeySet extends AbstractSet&amp;lt;K&amp;gt; {
    // 底层获取的是 KeyIterator
	public final Iterator&amp;lt;K&amp;gt; iterator()     { 
        return new KeyIterator(); 
    }
}
final class KeyIterator extends HashIterator implements Iterator&amp;lt;K&amp;gt; {
    // 回调 HashMap.HashIterator#nextNode
    public final K next() { 
        return nextNode().key; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;abstract class HashIterator {
    Node&amp;lt;K,V&amp;gt; next;        // next entry to return
    Node&amp;lt;K,V&amp;gt; current;     // current entry
    int expectedModCount;  // for 【fast-fail】，快速失败
    int index;             // current slot

    HashIterator() {
        // 把当前 map 的数量赋值给 expectedModCount，迭代时判断
        expectedModCount = modCount;
        Node&amp;lt;K,V&amp;gt;[] t = table;
        current = next = null;
        index = 0;
        if (t != null &amp;amp;&amp;amp; size &amp;gt; 0) { // advance to first entry
            do {} while (index &amp;lt; t.length &amp;amp;&amp;amp; (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }
	// iterator.next() 会调用这个函数
    final Node&amp;lt;K,V&amp;gt; nextNode() {
        Node&amp;lt;K,V&amp;gt;[] t;
        Node&amp;lt;K,V&amp;gt; e = next;
        // 这里会判断 集合的结构是否发生了变化，变化后 modCount 会改变，直接抛出并发异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null &amp;amp;&amp;amp; (t = table) != null) {
            do {} while (index &amp;lt; t.length &amp;amp;&amp;amp; (next = t[index++]) == null);
        }
        return e;
    }
	// 迭代器允许删除集合的元素，【删除后会重置 expectedModCount = modCount】
    public final void remove() {
        Node&amp;lt;K,V&amp;gt; p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        // 同步expectedModCount
        expectedModCount = modCount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;LinkedMap&lt;/h4&gt;
&lt;h5&gt;原理分析&lt;/h5&gt;
&lt;p&gt;LinkedHashMap 是 HashMap 的子类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：添加的元素按照键有序不重复的，有序的原因是底层维护了一个双向链表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：会占用一些内存空间&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 Set：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HashSet 集合相当于是 HashMap 集合的键，不带值&lt;/li&gt;
&lt;li&gt;LinkedHashSet 集合相当于是 LinkedHashMap 集合的键，不带值&lt;/li&gt;
&lt;li&gt;底层原理完全一样，都是基于哈希表按照键存储数据的，只是 Map 多了一个键的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源码解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内部维护了一个双向链表&lt;/strong&gt;，用来维护插入顺序或者 LRU 顺序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient LinkedHashMap.Entry&amp;lt;K,V&amp;gt; head;
transient LinkedHashMap.Entry&amp;lt;K,V&amp;gt; tail;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;accessOrder 决定了顺序，默认为 false 维护的是插入顺序（先进先出），true 为访问顺序（&lt;strong&gt;LRU 顺序&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final boolean accessOrder;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;维护顺序的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void afterNodeAccess(Node&amp;lt;K,V&amp;gt; p) {}
void afterNodeInsertion(boolean evict) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;put()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 调用父类HashMap的put方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
→ afterNodeInsertion(evict);// evict为true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;afterNodeInsertion方法，当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点，也就是链表首部节点 first&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry&amp;lt;K,V&amp;gt; first;
    // evict 只有在构建 Map 的时候才为 false，这里为 true
    if (evict &amp;amp;&amp;amp; (first = head) != null &amp;amp;&amp;amp; removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);//移除头节点
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;removeEldestEntry() 默认为 false，如果需要让它为 true，需要继承 LinkedHashMap 并且覆盖这个方法的实现，在实现 LRU 的缓存中特别有用，通过移除最近最久未使用的节点，从而保证缓存空间足够，并且缓存的数据都是热点数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected boolean removeEldestEntry(Map.Entry&amp;lt;K,V&amp;gt; eldest) {
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get()&lt;/p&gt;
&lt;p&gt;当一个节点被访问时，如果 accessOrder 为 true，则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后，在每次访问一个节点时会将这个节点移到链表尾部，那么链表首部就是最近最久未使用的节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V get(Object key) {
    Node&amp;lt;K,V&amp;gt; e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void afterNodeAccess(Node&amp;lt;K,V&amp;gt; e) {
    LinkedHashMap.Entry&amp;lt;K,V&amp;gt; last;
    if (accessOrder &amp;amp;&amp;amp; (last = tail) != e) {
        // 向下转型
        LinkedHashMap.Entry&amp;lt;K,V&amp;gt; p =
            (LinkedHashMap.Entry&amp;lt;K,V&amp;gt;)e, b = p.before, a = p.after;
        p.after = null;
        // 判断 p 是否是首节点
        if (b == null)
            //是头节点 让p后继节点成为头节点
            head = a;
        else
            //不是头节点 让p的前驱节点的next指向p的后继节点，维护链表的连接
            b.after = a;
        // 判断p是否是尾节点
        if (a != null)
            // 不是尾节点 让p后继节点指向p的前驱节点
            a.before = b;
        else
            // 是尾节点 让last指向p的前驱节点
            last = b;
        // 判断last是否是空
        if (last == null)
            // last为空说明p是尾节点或者只有p一个节点
            head = p;
        else {
            // last和p相互连接
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//调用HashMap的remove方法
final Node&amp;lt;K,V&amp;gt; removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)
→ afterNodeRemoval(node);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 HashMap 删除一个键值对时调用，会把在 HashMap 中删除的那个键值对一并从链表中删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void afterNodeRemoval(Node&amp;lt;K,V&amp;gt; e) {
    LinkedHashMap.Entry&amp;lt;K,V&amp;gt; p =
        (LinkedHashMap.Entry&amp;lt;K,V&amp;gt;)e, b = p.before, a = p.after;
    // 让p节点与前驱节点和后继节点断开链接
    p.before = p.after = null;
    // 判断p是否是头节点
    if (b == null)
        // p是头节点 让head指向p的后继节点
        head = a;
    else
        // p不是头节点 让p的前驱节点的next指向p的后继节点，维护链表的连接
        b.after = a;
    // 判断p是否是尾节点，是就让tail指向p的前驱节点，不是就让p.after指向前驱节点，双向
    if (a == null)
        tail = b;
    else
        a.before = b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;LRU&lt;/h5&gt;
&lt;p&gt;使用 LinkedHashMap 实现的一个 LRU 缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设定最大缓存空间 MAX_ENTRIES 为 3&lt;/li&gt;
&lt;li&gt;使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true，开启 LRU 顺序&lt;/li&gt;
&lt;li&gt;覆盖 removeEldestEntry() 方法实现，在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    LRUCache&amp;lt;Integer, String&amp;gt; cache = new LRUCache&amp;lt;&amp;gt;();
    cache.put(1, &quot;a&quot;);
    cache.put(2, &quot;b&quot;);
    cache.put(3, &quot;c&quot;);
    cache.get(1);//把1放入尾部
    cache.put(4, &quot;d&quot;);
    System.out.println(cache.keySet());//[3, 1, 4]只能存3个，移除2
}

class LRUCache&amp;lt;K, V&amp;gt; extends LinkedHashMap&amp;lt;K, V&amp;gt; {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() &amp;gt; MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;TreeMap&lt;/h4&gt;
&lt;p&gt;TreeMap 实现了 SotredMap 接口，是有序不可重复的键值对集合，基于红黑树（Red-Black tree）实现，每个 key-value 都作为一个红黑树的节点，如果构造 TreeMap 没有指定比较器，则根据 key 执行自然排序（默认升序），如果指定了比较器则按照比较器来进行排序&lt;/p&gt;
&lt;p&gt;TreeMap 集合指定大小规则有 2 种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接为对象的类实现比较器规则接口 Comparable，重写比较方法&lt;/li&gt;
&lt;li&gt;直接为集合设置比较器 Comparator 对象，重写比较方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：TreeSet 集合的底层是基于 TreeMap，只是键的附属值为空对象而已&lt;/p&gt;
&lt;p&gt;成员属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Entry 节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; static final class Entry&amp;lt;K,V&amp;gt; implements Map.Entry&amp;lt;K,V&amp;gt; {
     K key;
     V value;
     Entry&amp;lt;K,V&amp;gt; left;		//左孩子节点
     Entry&amp;lt;K,V&amp;gt; right;		//右孩子节点
     Entry&amp;lt;K,V&amp;gt; parent;		//父节点
     boolean color = BLACK;	//节点的颜色，在红黑树中只有两种颜色，红色和黑色
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;compare()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//如果comparator为null，采用comparable.compartTo进行比较，否则采用指定比较器比较大小
final int compare(Object k1, Object k2) {
    return comparator == null ? ((Comparable&amp;lt;? super K&amp;gt;)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/weixin_33991727/article/details/91518677&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;WeakMap&lt;/h4&gt;
&lt;p&gt;WeakHashMap 是基于弱引用的，内部的 Entry 继承 WeakReference，被弱引用关联的对象在&lt;strong&gt;下一次垃圾回收时会被回收&lt;/strong&gt;，并且构造方法传入引用队列，用来在清理对象完成以后清理引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static class Entry&amp;lt;K,V&amp;gt; extends WeakReference&amp;lt;Object&amp;gt; implements Map.Entry&amp;lt;K,V&amp;gt; {
    Entry(Object key, V value, ReferenceQueue&amp;lt;Object&amp;gt; queue, int hash, Entry&amp;lt;K,V&amp;gt; next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WeakHashMap 主要用来实现缓存，使用 WeakHashMap 来引用缓存对象，由 JVM 对这部分缓存进行回收&lt;/p&gt;
&lt;p&gt;Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能，ConcurrentCache 采取分代缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;经常使用的对象放入 eden 中，eden 使用 ConcurrentHashMap 实现，不用担心会被回收（伊甸园）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不常用的对象放入 longterm，longterm 使用 WeakHashMap 实现，这些老对象会被垃圾收集器回收&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用 get() 方法时，会先从 eden 区获取，如果没有找到的话再到 longterm 获取，当从 longterm 获取到就把对象放入 eden 中，从而保证经常被访问的节点不容易被回收&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用 put() 方法时，如果 eden 的大小超过了 size，那么就将 eden 中的所有对象都放入 longterm 中，利用虚拟机回收掉一部分不经常使用的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class ConcurrentCache&amp;lt;K, V&amp;gt; {
    private final int size;
    private final Map&amp;lt;K, V&amp;gt; eden;
    private final Map&amp;lt;K, V&amp;gt; longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap&amp;lt;&amp;gt;(size);
        this.longterm = new WeakHashMap&amp;lt;&amp;gt;(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() &amp;gt;= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}





&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;泛型&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;泛型（Generic）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;泛型就是一个标签：&amp;lt;数据类型&amp;gt;&lt;/li&gt;
&lt;li&gt;泛型可以在编译阶段约束只能操作某种数据类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.7 开始之后，泛型后面的申明可以省略不写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;泛型和集合都只能支持引用数据类型，不支持基本数据类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ArrayList&amp;lt;Object&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;(); 
lists.add(99.9);
lists.add(&apos;a&apos;);
lists.add(&quot;Java&quot;);
ArrayList&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
lists1.add(10);
lists1.add(20);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：泛型在编译阶段约束了操作的数据类型，从而不会出现类型转换异常，体现的是 Java 的严谨性和规范性&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;自定义&lt;/h4&gt;
&lt;h5&gt;泛型类&lt;/h5&gt;
&lt;p&gt;泛型类：使用了泛型定义的类就是泛型类&lt;/p&gt;
&lt;p&gt;泛型类格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 class 类名&amp;lt;泛型变量&amp;gt;{

}
泛型变量建议使用 E , T , K , V
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class GenericDemo {
    public static void main(String[] args) {
        MyArrayList&amp;lt;String&amp;gt; list = new MyArrayList&amp;lt;String&amp;gt;();
        MyArrayList&amp;lt;Integer&amp;gt; list1 = new MyArrayList&amp;lt;Integer&amp;gt;();
        list.add(&quot;自定义泛型类&quot;);
    }
}
class MyArrayList&amp;lt;E&amp;gt;{
	public void add(E e){}
    public void remove(E e){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;泛型方法&lt;/h5&gt;
&lt;p&gt;泛型方法：定义了泛型的方法就是泛型方法&lt;/p&gt;
&lt;p&gt;泛型方法的定义格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 &amp;lt;泛型变量&amp;gt; 返回值类型 方法名称(形参列表){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法定义了是什么泛型变量，后面就只能用什么泛型变量。&lt;/p&gt;
&lt;p&gt;泛型类的核心思想：把出现泛型变量的地方全部替换成传输的真实数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GenericDemo {
    public static void main(String[] args) {
        Integer[] num = {10 , 20 , 30 , 40 , 50};
        String s1 = arrToString(nums);
     
        String[] name = {&quot;张三&quot;,&quot;李四&quot;,&quot;王五&quot;};
        String s2 = arrToString(names);
    }

    public static &amp;lt;T&amp;gt; String arrToString(T[] arr){
        --------------
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义泛型接口&lt;/p&gt;
&lt;p&gt;泛型接口：使用了泛型定义的接口就是泛型接口。&lt;/p&gt;
&lt;p&gt;泛型接口的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 interface 接口名称&amp;lt;泛型变量&amp;gt;{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class GenericDemo {
    public static void main(String[] args) {
        Data d = new StudentData();
        d.add(new Student());
        ................
    }
}

public interface Data&amp;lt;E&amp;gt;{
    void add(E e);
    void delete(E e);
    void update(E e);
    E query(int index);
}
class Student{}
class StudentData implements Data&amp;lt;Student&amp;gt;{重写所有方法}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;通配符&lt;/h4&gt;
&lt;p&gt;通配符：？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? 可以用在使用泛型的时候代表一切类型&lt;/li&gt;
&lt;li&gt;E、T、K、V 是在定义泛型的时候使用代表一切类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;泛型的上下限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? extends Car：那么 ? 必须是 Car 或者其子类（泛型的上限）&lt;/li&gt;
&lt;li&gt;? super  Car：那么 ? 必须是 Car 或者其父类（泛型的下限，不是很常见）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//需求：开发一个极品飞车的游戏，所有的汽车都能一起参与比赛。
public class GenericDemo {
    public static void main(String[] args) {
        ArrayList&amp;lt;BMW&amp;gt; bmws = new ArrayList&amp;lt;&amp;gt;();
        ArrayList&amp;lt;AD&amp;gt; ads = new ArrayList&amp;lt;&amp;gt;();
        ArrayList&amp;lt;Dog&amp;gt; dogs = new ArrayList&amp;lt;&amp;gt;();
        run(bmws);
        //run(dogs);
    }
    //public static void run(ArrayList&amp;lt;?&amp;gt; car){}//这样 dou对象也能进入
    public static void run(ArrayList&amp;lt;? extends Car&amp;gt; car){}
}

class Car{}
class BMW extends Car{}
class AD extends Car{}
class Dog{}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;异常&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;异常：程序在编译或者执行的过程中可能出现的问题，Java 为常见的代码异常都设计一个类来代表&lt;/p&gt;
&lt;p&gt;错误：Error ，程序员无法处理的错误，只能重启系统，比如内存奔溃，JVM 本身的奔溃&lt;/p&gt;
&lt;p&gt;Java 中异常继承的根类是：Throwable&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;异常的体系:
         Throwable(根类，不是异常类)
      /              \
    Error           Exception（异常，需要研究和处理）
                    /            \
                   编译时异常     RuntimeException(运行时异常)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exception 异常的分类:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译时异常：继承自 Exception 的异常或者其子类，编译阶段就会报错&lt;/li&gt;
&lt;li&gt;运行时异常：继承自 RuntimeException 的异常或者其子类，编译阶段是不会出错的，在运行阶段出错&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;处理过程&lt;/h3&gt;
&lt;p&gt;异常的产生默认的处理过程解析：（自动处理的过程）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;默认会在出现异常的代码那里自动的创建一个异常对象：ArithmeticException（算术异常）&lt;/li&gt;
&lt;li&gt;异常会从方法中出现的点这里抛出给调用者，调用者最终抛出给 JVM 虚拟机&lt;/li&gt;
&lt;li&gt;虚拟机接收到异常对象后，先在控制台直接输出&lt;strong&gt;异常栈&lt;/strong&gt;信息数据&lt;/li&gt;
&lt;li&gt;直接从当前执行的异常点终止当前程序&lt;/li&gt;
&lt;li&gt;后续代码没有机会执行了，因为程序已经死亡&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class ExceptionDemo {
    public static void main(String[] args) {
        System.out.println(&quot;程序开始。。。。。。。。。。&quot;);
        chu( 10 ,0 );
        System.out.println(&quot;程序结束。。。。。。。。。。&quot;);//不执行
    }
    public static void chu(int a , int b){
        int c = a / b ;// 出现了运行时异常,自动创建异常对象：ArithmeticException
        System.out.println(&quot;结果是：&quot;+c);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;编译异常&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;编译时异常：继承自 Exception 的异常或者其子类，没有继承 RuntimeException，编译时异常是编译阶段就会报错&lt;/p&gt;
&lt;p&gt;编译时异常的作用是什么：在编译阶段就爆出一个错误，目的在于提醒，请检查并注意不要出 BUG&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws ParseException {
	String date = &quot;2015-01-12 10:23:21&quot;;
	SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
	Date d = sdf.parse(date);
	System.out.println(d);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;处理机制&lt;/h4&gt;
&lt;h5&gt;throws&lt;/h5&gt;
&lt;p&gt;在出现编译时异常的地方层层把异常抛出去给调用者，调用者最终抛出给 JVM 虚拟机，JVM 虚拟机输出异常信息，直接终止掉程序，这种方式与默认方式是一样的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exception 是异常最高类型可以抛出一切异常&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    System.out.println(&quot;程序开始。。。。&quot;);
    String s = &quot;2013-03-23 10:19:23&quot;;
    SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
    Date date = sdf.parse(s);
    System.out.println(&quot;程序结束。。。。。&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;try/catch&lt;/h5&gt;
&lt;p&gt;可以处理异常，并且出现异常后代码也不会死亡&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;捕获异常和处理异常的格式：&lt;strong&gt;捕获处理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try{
  // 监视可能出现异常的代码！
}catch(异常类型1 变量){
  // 处理异常
}catch(异常类型2 变量){
  // 处理异常
}...finall{
//资源释放
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监视捕获处理异常写法：Exception 可以捕获处理一切异常类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try{
    // 可能出现异常的代码！
}catch (Exception e){
    e.printStackTrace(); // **直接打印异常栈信息**
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Throwable成员方法:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public String getMessage()&lt;/code&gt;：返回此 throwable 的详细消息字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String toString()&lt;/code&gt;：返回此可抛出的简短描述&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void printStackTrace()&lt;/code&gt;：把异常的错误信息输出在控制台&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    System.out.println(&quot;程序开始。。。。&quot;);
    try {
        String s = &quot;2013-03-23 10:19:23&quot;;
        SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        Date date = sdf.parse(s);
        InputStream is = new FileInputStream(&quot;D:/meinv.png&quot;);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(&quot;程序结束。。。。。&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;规范做法&lt;/h5&gt;
&lt;p&gt;在出现异常的地方把异常一层一层的抛出给最外层调用者，最外层调用者集中捕获处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ExceptionDemo{
	public static void main(String[] args){
        System.out.println(&quot;程序开始。。。。&quot;);
        try {
            parseDate(&quot;2013-03-23 10:19:23&quot;);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(&quot;程序结束。。。。&quot;);
    }
    public static void parseDate(String time) throws Exception{...}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;运行异常&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;继承自 RuntimeException 的异常或者其子类，编译阶段是不会出错的，是在运行时阶段可能出现的错误，运行时异常编译阶段可以处理也可以不处理，代码编译都能通过&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常见的运行时异常&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数组索引越界异常：ArrayIndexOutOfBoundsException&lt;/li&gt;
&lt;li&gt;空指针异常：NullPointerException，直接输出没问题，调用空指针的变量的功能就会报错&lt;/li&gt;
&lt;li&gt;类型转换异常：ClassCastException&lt;/li&gt;
&lt;li&gt;迭代器遍历没有此元素异常：NoSuchElementException&lt;/li&gt;
&lt;li&gt;算术异常（数学操作异常）：ArithmeticException&lt;/li&gt;
&lt;li&gt;数字转换异常：NumberFormatException&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;处理机制&lt;/h4&gt;
&lt;p&gt;运行时异常在编译阶段是不会报错，在运行阶段才会出错，运行时出错了程序还是会停止，运行时异常也建议要处理，运行时异常是自动往外抛出的，不需要手工抛出&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运行时异常的处理规范&lt;/strong&gt;：直接在最外层捕获处理即可，底层会自动抛出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ExceptionDemo{
    public static void main(String[] args){
        System.out.println(&quot;程序开始。。。。&quot;);
        try{
            chu(10 / 0);//ArithmeticException: / by zero
            System.out.println(&quot;操作成功！&quot;);//没输出
        }catch (Exception e){
            e.printStackTrace();
            System.out.println(&quot;操作失败！&quot;);//输出了
        }
        System.out.println(&quot;程序结束。。。。&quot;);//输出了
    }
    
    public static void chu(int a , int b)  { System.out.println( a / b );}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Finally&lt;/h3&gt;
&lt;p&gt;用在捕获处理的异常格式中的，放在最后面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try{
    // 可能出现异常的代码！
}catch(Exception e){
    e.printStackTrace();
}finally{
    // 无论代码是出现异常还是正常执行，最终一定要执行这里的代码！！
}
try: 1次。
catch：0-N次  (如果有finally那么catch可以没有!!)
finally: 0-1次
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;finally 的作用&lt;/strong&gt;：可以在代码执行完毕以后进行资源的释放操作&lt;/p&gt;
&lt;p&gt;资源：资源都是实现了 Closeable 接口的，都自带 close() 关闭方法&lt;/p&gt;
&lt;p&gt;注意：如果在 finally 中出现了 return，会吞掉异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class FinallyDemo {
    public static void main(String[] args) {
        System.out.println(chu());//一定会输出 finally,优先级比return高
    }

    public static int chu(){
        try{
            int a = 10 / 2 ;
            return a ;
        }catch (Exception e){
            e.printStackTrace();
            return -1;
        }finally {
            System.out.println(&quot;=====finally被执行&quot;);
            //return 111; // 不建议在finally中写return，会覆盖前面所有的return值!
        }
    }
    public static void test(){
        InputStream is = null;
        try{
            is = new FileInputStream(&quot;D:/cang.png&quot;);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println(&quot;==finally被执行===&quot;);
            // 回收资源。用于在代码执行完毕以后进行资源的回收操作！
            try {
                if(is!=null)is.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;自定义&lt;/h3&gt;
&lt;p&gt;自定义异常:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义编译时异常：定义一个异常类继承 Exception，重写构造器，在出现异常的地方用 throw new 自定义对象抛出&lt;/li&gt;
&lt;li&gt;自定义运行时异常：定义一个异常类继承 RuntimeException，重写构造器，在出现异常的地方用 throw new 自定义对象抛出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;throws：用在方法上，用于抛出方法中的异常&lt;/p&gt;
&lt;p&gt;throw:  用在出现异常的地方，创建异常对象且立即从此处抛出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//需求：认为年龄小于0岁，大于200岁就是一个异常。
public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            checkAge(101);
        } catch (AgeIllegalException e) {
            e.printStackTrace();
        }
    }

    public static void checkAge(int age) throws ItheimaAgeIllegalException {
        if(age &amp;lt; 0 || age &amp;gt; 200){//年龄在0-200之间
            throw new AgeIllegalException(&quot;/ age is illegal!&quot;);
            //throw new AgeIllegalRuntimeException(&quot;/ age is illegal!&quot;);
        }else{
            System.out.println(&quot;年龄是：&quot; + age);
        }
    }
}

public class AgeIllegalException extends Exception{
    Alt + Insert-&amp;gt;Constructor 
}//编译时异常
public class AgeIllegalRuntimeException extends RuntimeException{
	public AgeIllegalRuntimeException() {
    }

    public AgeIllegalRuntimeException(String message) {
        super(message);
    }
}//运行时异常
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;处理规范&lt;/h3&gt;
&lt;p&gt;异常的语法注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行时异常被抛出可以不处理，可以自动抛出；&lt;strong&gt;编译时异常必须处理&lt;/strong&gt;；按照规范都应该处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重写方法申明抛出的异常，子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;方法默认都可以自动抛出运行时异常， throws RuntimeException 可以省略不写&lt;/li&gt;
&lt;li&gt;当多异常处理时，捕获处理，前面的异常类不能是后面异常类的父类&lt;/li&gt;
&lt;li&gt;在 try/catch 后可以追加 finally 代码块，其中的代码一定会被执行，通常用于资源回收操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;异常的作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可以处理代码问题，防止程序出现异常后的死亡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提高了程序的健壮性和安全性&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Demo{
    public static void main(String[] args){
        //请输入一个合法的年龄
        while(true){
            try{
                Scanner sc = new Scanner(System.in);
                System.out.println(&quot;请您输入您的年年龄：&quot;);
                int age = sc.nextInt();
                System.out.println(&quot;年龄：&quot;+age);
                break;
            }catch(Exception e){
                System.err.println(&quot;您的年龄是瞎输入的！&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;λ&lt;/h2&gt;
&lt;h3&gt;lambda&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Lambda 表达式是 JDK1.8 开始之后的新技术，是一种代码的新语法，一种特殊写法&lt;/p&gt;
&lt;p&gt;作用：为了简化匿名内部类的代码写法&lt;/p&gt;
&lt;p&gt;Lambda 表达式的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(匿名内部类被重写方法的形参列表) -&amp;gt; {
	//被重写方法的方法体代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lambda 表达式并不能简化所有匿名内部类的写法，只能简化&lt;strong&gt;函数式接口的匿名内部类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简化条件：首先必须是接口，接口中只能有一个抽象方法&lt;/p&gt;
&lt;p&gt;@FunctionalInterface 函数式接口注解：一旦某个接口加上了这个注解，这个接口只能有且仅有一个抽象方法&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;简化方法&lt;/h4&gt;
&lt;p&gt;Lambda 表达式的省略写法（进一步在 Lambda 表达式的基础上继续简化）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 Lambda 表达式的方法体代码只有一行代码，可以省略大括号不写，同时要省略分号；如果这行代码是 return 语句，必须省略 return 不写&lt;/li&gt;
&lt;li&gt;参数类型可以省略不写&lt;/li&gt;
&lt;li&gt;如果只有一个参数，参数类型可以省略，同时 &lt;code&gt;()&lt;/code&gt; 也可以省略&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; names = new ArrayList&amp;lt;&amp;gt;();
names.add(&quot;a&quot;);
names.add(&quot;b&quot;);
names.add(&quot;c&quot;);

names.forEach(new Consumer&amp;lt;String&amp;gt;() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

names.forEach((String s) -&amp;gt; {
        System.out.println(s);
});

names.forEach((s) -&amp;gt; {
    System.out.println(s);
});

names.forEach(s -&amp;gt; {
    System.out.println(s);
});

names.forEach(s -&amp;gt; System.out.println(s) );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常用简化&lt;/h4&gt;
&lt;p&gt;Comparator&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CollectionsDemo {
    public static void main(String[] args) {
        List&amp;lt;Student&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();//...s1 s2 s3
        Collections.addAll(lists , s1 , s2 , s3);
        Collections.sort(lists, new Comparator&amp;lt;Student&amp;gt;() {
            @Override
            public int compare(Student s1, Student s2) {
                return s1.getAge() - s2.getAge();
            }
        });
        
        // 简化写法
        Collections.sort(lists ,(Student t1, Student t2) -&amp;gt; {
                return t1.getAge() - t2.getAge();
        });
        // 参数类型可以省略,最简单的
        Collections.sort(lists ,(t1,t2) -&amp;gt; t1.getAge()-t2.getAge());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;方法引用&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;方法引用：方法引用是为了进一步简化 Lambda 表达式的写法&lt;/p&gt;
&lt;p&gt;方法引用的格式：类型或者对象::引用的方法&lt;/p&gt;
&lt;p&gt;关键语法是：&lt;code&gt;::&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lists.forEach( s -&amp;gt; System.out.println(s));
// 方法引用！
lists.forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;静态方法&lt;/h4&gt;
&lt;p&gt;引用格式：&lt;code&gt;类名::静态方法&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;简化步骤：定义一个静态方法，把需要简化的代码放到一个静态方法中去&lt;/p&gt;
&lt;p&gt;静态方法引用的注意事项：被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才能引用简化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//定义集合加入几个Student元素
// 使用静态方法进行简化！
Collections.sort(lists, (o1, o2) -&amp;gt; Student.compareByAge(o1 , o2));
// 如果前后参数是一样的，而且方法是静态方法，既可以使用静态方法引用
Collections.sort(lists, Student::compareByAge);

public class Student {
    private String name ;
    private int age ;

    public static int compareByAge(Student o1 , Student o2){
        return  o1.getAge() - o2.getAge();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实例方法&lt;/h4&gt;
&lt;p&gt;引用格式：&lt;code&gt;对象::实例方法&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;简化步骤：定义一个实例方法，把需要的代码放到实例方法中去&lt;/p&gt;
&lt;p&gt;实例方法引用的注意事项：被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();
        lists.add(&quot;java1&quot;);
        lists.add(&quot;java2&quot;);
        lists.add(&quot;java3&quot;);
        // 对象是 System.out = new PrintStream();
        // 实例方法：println()
        // 前后参数正好都是一个
        lists.forEach(s -&amp;gt; System.out.println(s));
        lists.forEach(System.out::println);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;特定类型&lt;/h4&gt;
&lt;p&gt;特定类型：String，任何类型&lt;/p&gt;
&lt;p&gt;引用格式：&lt;code&gt;特定类型::方法&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意事项：如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者，并且其余参数作为后面方法的形参，那么就可以用特定类型方法引用了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo{
    public static void main(String[] args) {
        String[] strs = new String[]{&quot;James&quot;, &quot;AA&quot;, &quot;John&quot;,
                &quot;Patricia&quot;,&quot;Dlei&quot; , &quot;Robert&quot;,&quot;Boom&quot;, &quot;Cao&quot; ,&quot;black&quot; ,
                &quot;Michael&quot;, &quot;Linda&quot;,&quot;cao&quot;,&quot;after&quot;,&quot;sa&quot;};

        // public static &amp;lt;T&amp;gt; void sort(T[] a, Comparator&amp;lt;? super T&amp;gt; c)
        // 需求：按照元素的首字符(忽略大小写)升序排序！！！
        Arrays.sort(strs, new Comparator&amp;lt;String&amp;gt;() {
            @Override
            public int compare(String s1, String s2) {
                return s1.compareToIgnoreCase(s2);//按照元素的首字符(忽略大小写)
            }
        });

        Arrays.sort(strs, ( s1,  s2 ) -&amp;gt;  s1.compareToIgnoreCase(s2));

        // 特定类型的方法引用：
        Arrays.sort(strs,  String::compareToIgnoreCase);
        System.out.println(Arrays.toString(strs));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;构造器&lt;/h4&gt;
&lt;p&gt;格式：&lt;code&gt;类名::new&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意事项：前后参数一致的情况下，又在创建对象，就可以使用构造器引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ConstructorDemo {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; lists = new ArrayList&amp;lt;&amp;gt;();
        lists.add(&quot;java1&quot;);
        lists.add(&quot;java2&quot;);
        lists.add(&quot;java3&quot;);

        // 集合默认只能转成Object类型的数组。
        Object[] objs = lists.toArray();

        // 我们想指定转换成字符串类型的数组！最新的写法可以结合构造器引用实现 
        String[] strs = lists.toArray(new IntFunction&amp;lt;String[]&amp;gt;() {
            @Override
            public String[] apply(int value) {
                return new String[value];
            }
        });
        String[] strs1 = lists.toArray(s -&amp;gt; new String[s]);
        String[] strs2 = lists.toArray(String[]::new);

        System.out.println(&quot;String类型的数组：&quot;+ Arrays.toString(strs2));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;I/O&lt;/h2&gt;
&lt;h3&gt;Stream&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;Stream 流其实就是一根传送带，元素在上面可以被 Stream 流操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以解决已有集合类库或者数组 API 的弊端&lt;/li&gt;
&lt;li&gt;Stream 流简化集合和数组的操作&lt;/li&gt;
&lt;li&gt;链式编程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;list.stream().filter(new Predicate&amp;lt;String&amp;gt;() {
            @Override
            public boolean test(String s) {
                return s.startsWith(&quot;张&quot;);
            }
        });

list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取流&lt;/h4&gt;
&lt;p&gt;集合获取 Stream 流用：&lt;code&gt;default Stream&amp;lt;E&amp;gt; stream()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数组：Arrays.stream(数组)   /  Stream.of(数组);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Collection集合获取Stream流。
Collection&amp;lt;String&amp;gt; c = new ArrayList&amp;lt;&amp;gt;();
Stream&amp;lt;String&amp;gt; listStream = c.stream();

// Map集合获取流
// 先获取键的Stream流。
Stream&amp;lt;String&amp;gt; keysStream = map.keySet().stream();
// 在获取值的Stream流
Stream&amp;lt;Integer&amp;gt; valuesStream = map.values().stream();
// 获取键值对的Stream流（key=value： Map.Entry&amp;lt;String,Integer&amp;gt;）
Stream&amp;lt;Map.Entry&amp;lt;String,Integer&amp;gt;&amp;gt; keyAndValues = map.entrySet().stream();

//数组获取流
String[] arr = new String[]{&quot;Java&quot;, &quot;JavaEE&quot; ,&quot;Spring Boot&quot;};
Stream&amp;lt;String&amp;gt; arrStream1 = Arrays.stream(arr);
Stream&amp;lt;String&amp;gt; arrStream2 = Stream.of(arr);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void forEach(Consumer&amp;lt;? super T&amp;gt; action)&lt;/td&gt;
&lt;td&gt;逐一处理（遍历）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long count&lt;/td&gt;
&lt;td&gt;返回流中的元素数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream&amp;lt;T&amp;gt; filter(Predicate&amp;lt;? super T&amp;gt; predicate)&lt;/td&gt;
&lt;td&gt;用于对流中的数据进行过滤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream&amp;lt;T&amp;gt; limit(long maxSize)&lt;/td&gt;
&lt;td&gt;返回此流中的元素组成的流，截取前指定参数个数的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream&amp;lt;T&amp;gt; skip(long n)&lt;/td&gt;
&lt;td&gt;跳过指定参数个数的数据，返回由该流的剩余元素组成的流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;R&amp;gt; Stream&amp;lt;R&amp;gt; map(Function&amp;lt;? super T,? extends R&amp;gt; mapper)&lt;/td&gt;
&lt;td&gt;加工方法，将当前流中的 T 类型数据转换为另一种 R 类型的流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static &amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; concat(Stream a, Stream b)&lt;/td&gt;
&lt;td&gt;合并 a 和 b 两个流为一个，调用 &lt;code&gt;Stream.concat(s1,s2)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream&amp;lt;T&amp;gt; distinct()&lt;/td&gt;
&lt;td&gt;返回由该流的不同元素组成的流&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class StreamDemo {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;张无忌&quot;); list.add(&quot;周芷若&quot;); list.add(&quot;赵敏&quot;);
        list.add(&quot;张三&quot;); list.add(&quot;张三丰&quot;); list.add(&quot;张&quot;);
        //取以张开头并且名字是三位数的
        list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;)
                .filter(s -&amp;gt; s.length == 3).forEach(System.out::println);
        //统计数量
		long count = list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;)
                .filter(s -&amp;gt; s.length == 3).count();
		//取前两个
		list.stream().filter(s -&amp;gt; s.length == 3).limit(2).forEach(...);
		//跳过前两个
		list.stream().filter(s -&amp;gt; s.length == 3).skip(2).forEach(...);

		// 需求：把名称都加上“张三的:+xxx”
		list.stream().map(s -&amp;gt; &quot;张三的&quot; + s).forEach(System.out::println);
		// 需求：把名称都加工厂学生对象放上去!!
		// list.stream().map(name -&amp;gt; new Student(name));
		list.stream.map(Student::new).forEach(System.out::println);
                                          	
		//数组流
		Stream&amp;lt;Integer&amp;gt; s1 = Stream.of(10,20,30,40,50);
		//集合流
		Stream&amp;lt;String&amp;gt; s2 = list.stream();
		//合并流
		Stream&amp;lt;Object&amp;gt; s3 = Stream.concat(s1,s2);
		s3.forEach(System.out::println);
    }
}
class Student{
    private String name;
    //......
}                                          
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;终结方法&lt;/h4&gt;
&lt;p&gt;终结方法：Stream 调用了终结方法，流的操作就全部终结，不能继续使用，如 foreach，count 方法等&lt;/p&gt;
&lt;p&gt;非终结方法：每次调用完成以后返回一个新的流对象，可以继续使用，支持&lt;strong&gt;链式编程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// foreach终结方法
list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;))
    .filter(s -&amp;gt; s.length() == 3).forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;收集流&lt;/h4&gt;
&lt;p&gt;收集 Stream：把 Stream 流的数据转回到集合中去&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stream 流：工具&lt;/li&gt;
&lt;li&gt;集合：目的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stream 收集方法：&lt;code&gt;R collect(Collector collector)&lt;/code&gt; 把结果收集到集合中&lt;/p&gt;
&lt;p&gt;Collectors 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; Collector toList()&lt;/code&gt;：把元素收集到 List 集合中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; Collector toSet()&lt;/code&gt;：把元素收集到 Set 集合中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static  Collector toMap(Function keyMapper,Function valueMapper)&lt;/code&gt;：把元素收集到 Map 集合中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object[] toArray()&lt;/code&gt;：把元素收集数组中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public static Collector groupingBy(Function&amp;lt;? super T, ? extends K&amp;gt; classifier)&lt;/code&gt;：分组&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
	List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
	Stream&amp;lt;String&amp;gt; stream = list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;));    
    //把stream流转换成Set集合。
    Set&amp;lt;String&amp;gt; set = stream.collect(Collectors.toSet());
    
    //把stream流转换成List集合。
    //重新定义，因为资源已经被关闭了
    Stream&amp;lt;String&amp;gt; stream1 = list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;));
    List&amp;lt;String&amp;gt; list = stream.collect(Collectors.toList());
    
    //把stream流转换成数组。
    Stream&amp;lt;String&amp;gt; stream2 = list.stream().filter(s -&amp;gt; s.startsWith(&quot;张&quot;));
    Object[] arr = stream2.toArray();
    // 可以借用构造器引用申明转换成的数组类型！！！
    String[] arr1 = stream2.toArray(String[]::new);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;File&lt;/h3&gt;
&lt;h4&gt;文件类&lt;/h4&gt;
&lt;p&gt;File 类：代表操作系统的文件对象，是用来操作操作系统的文件对象的，删除文件，获取文件信息，创建文件（文件夹），广义来说操作系统认为文件包含（文件和文件夹）&lt;/p&gt;
&lt;p&gt;File 类构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public File(String pathname)&lt;/code&gt;：根据路径获取文件对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public File(String parent , String child)&lt;/code&gt;：根据父路径和文件名称获取文件对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;File 类创建文件对象的格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File f = new File(&quot;绝对路径/相对路径&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;绝对路径：从磁盘的的盘符一路走到目的位置的路径
&lt;ul&gt;
&lt;li&gt;绝对路径依赖具体的环境，一旦脱离环境，代码可能出错&lt;/li&gt;
&lt;li&gt;一般是定位某个操作系统中的某个文件对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相对路径&lt;/strong&gt;：不带盘符的（重点）
&lt;ul&gt;
&lt;li&gt;默认是直接相对到工程目录下寻找文件的。&lt;/li&gt;
&lt;li&gt;相对路径只能用于寻找工程下的文件，可以跨平台&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File f = new File(&quot;文件对象/文件夹对象&quot;)&lt;/code&gt; 广义来说：文件是包含文件和文件夹的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class FileDemo{
    public static void main(String[] args) {
        // 1.创建文件对象：使用绝对路径
        // 文件路径分隔符：
        //      -- a.使用正斜杠： /
        //      -- b.使用反斜杠： \\
        //      -- c.使用分隔符API:File.separator
        //File f1 = new File(&quot;D:&quot;+File.separator+&quot;it&quot;+File.separator
		//+&quot;图片资源&quot;+File.separator+&quot;beautiful.jpg&quot;);
        File f1 = new File(&quot;D:\\seazean\\图片资源\\beautiful.jpg&quot;);
        System.out.println(f1.length()); // 获取文件的大小，字节大小

        // 2.创建文件对象：使用相对路径
        File f2 = new File(&quot;Day09Demo/src/dlei.txt&quot;);
        System.out.println(f2.length());

        // 3.创建文件对象：代表文件夹。
        File f3 = new File(&quot;D:\\it\\图片资源&quot;);
        System.out.println(f3.exists());// 判断路径是否存在！！
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;h5&gt;常用方法&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getAbsolutePath()&lt;/td&gt;
&lt;td&gt;返回此 File 的绝对路径名字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getPath()&lt;/td&gt;
&lt;td&gt;获取创建文件对象的时候用的路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getName()&lt;/td&gt;
&lt;td&gt;返回由此 File 表示的文件或目录的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long length()&lt;/td&gt;
&lt;td&gt;返回由此 File 表示的文件的长度（大小）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long length(FileFilter filter)&lt;/td&gt;
&lt;td&gt;文件过滤器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class FileDemo {
    public static void main(String[] args) {
        // 1.绝对路径创建一个文件对象
        File f1 = new File(&quot;E:/图片/test.jpg&quot;);
        // a.获取它的绝对路径。
        System.out.println(f1.getAbsolutePath());
        // b.获取文件定义的时候使用的路径。
        System.out.println(f1.getPath());
        // c.获取文件的名称：带后缀。
        System.out.println(f1.getName());
        // d.获取文件的大小：字节个数。
        System.out.println(f1.length());
        System.out.println(&quot;------------------------&quot;);

        // 2.相对路径
        File f2 = new File(&quot;Demo/src/test.txt&quot;);
        // a.获取它的绝对路径。
        System.out.println(f2.getAbsolutePath());
        // b.获取文件定义的时候使用的路径。
        System.out.println(f2.getPath());
        // c.获取文件的名称：带后缀。
        System.out.println(f2.getName());
        // d.获取文件的大小：字节个数。
        System.out.println(f2.length());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;判断方法&lt;/h5&gt;
&lt;p&gt;方法列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;boolean exists()&lt;/code&gt;：此 File 表示的文件或目录是否实际存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isDirectory()&lt;/code&gt;：此 File 表示的是否为目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isFile()&lt;/code&gt;：此 File 表示的是否为文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;File f = new File(&quot;Demo/src/test.txt&quot;);
// a.判断文件路径是否存在
System.out.println(f.exists()); // true
// b.判断文件对象是否是文件,是文件返回true ,反之
System.out.println(f.isFile()); // true
// c.判断文件对象是否是文件夹,是文件夹返回true ,反之
System.out.println(f.isDirectory()); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;创建删除&lt;/h5&gt;
&lt;p&gt;方法列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;boolean createNewFile()&lt;/code&gt;：当且仅当具有该名称的文件尚不存在时， 创建一个新的空文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean delete()&lt;/code&gt;：删除由此 File 表示的文件或目录（只能删除空目录）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean mkdir()&lt;/code&gt;：创建由此 File 表示的目录（只能创建一级目录）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean mkdirs()&lt;/code&gt;：可以创建多级目录（建议使用）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class FileDemo {
    public static void main(String[] args) throws IOException {
        File f = new File(&quot;Demo/src/test.txt&quot;);
        // a.创建新文件，创建成功返回true ,反之
        System.out.println(f.createNewFile());

        // b.删除文件或者空文件夹
        System.out.println(f.delete());
        // 不能删除非空文件夹，只能删除空文件夹
        File f1 = new File(&quot;E:/it/aaaaa&quot;);
        System.out.println(f1.delete());

        // c.创建一级目录
        File f2 = new File(&quot;E:/bbbb&quot;);
        System.out.println(f2.mkdir());

        // d.创建多级目录
        File f3 = new File(&quot;D:/it/e/a/d/ds/fas/fas/fas/fas/fas/fas&quot;);
        System.out.println(f3.mkdirs());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;遍历目录&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public String[] list()&lt;/code&gt;：获取当前目录下所有的一级文件名称到一个字符串数组中去返回&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public File[] listFiles()&lt;/code&gt;：获取当前目录下所有的一级文件对象到一个&lt;strong&gt;文件对象数组&lt;/strong&gt;中去返回（&lt;strong&gt;重点&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public long lastModified&lt;/code&gt;：返回此抽象路径名表示的文件上次修改的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class FileDemo {
    public static void main(String[] args) {
        File dir = new File(&quot;D:\\seazean&quot;);
        // a.获取当前目录对象下的全部一级文件名称到一个字符串数组返回。
        String[] names = dir.list();
        for (String name : names) {
            System.out.println(name);
        }
        // b.获取当前目录对象下的全部一级文件对象到一个File类型的数组返回。
        File[] files = dir.listFiles();
        for (File file : files) {
            System.out.println(file.getAbsolutePath());
        }

        // c
        File f1 = new File(&quot;D:\\图片资源\\beautiful.jpg&quot;);
        long time = f1.lastModified(); // 最后修改时间！
        SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        System.out.println(sdf.format(time));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;文件搜索&lt;/h4&gt;
&lt;p&gt;递归实现文件搜索（非规律递归）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义一个方法用于做搜索&lt;/li&gt;
&lt;li&gt;进入方法中进行业务搜索分析&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 去某个目录下搜索某个文件
 * @param dir 搜索文件的目录。
 * @param fileName 搜索文件的名称。
 */
public static void searchFiles(File dir , String fileName){
    // 1.判断是否存在该路径，是否是文件夹
    if(dir.exists() &amp;amp;&amp;amp; dir.isDirectory()){
        // 2.提取当前目录下的全部一级文件对象
        File files = dir.listFiles();// 可能是null/也可能是空集合[]
        // 3.判断是否存在一级文件对象,判断是否不为空目录
        if(files != null &amp;amp;&amp;amp; files.length &amp;gt; 0){
            // 4.判断一级文件对象
            for(File file : files){
                // 5.判断file是文件还是文件夹
                if(file.isFile()){
                    // 6.判断该文件是否为我要找的文件对象
                    if(f.getName().contains(fileName)){//模糊查找
                        sout(f.getAbsolutePath());
                        try {
                            // 启动它（拓展）
                            Runtime r = Runtime.getRuntime();
                            r.exec(f.getAbsolutePath());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                } else {
                    // 7.该文件是文件夹，文件夹要递归进入继续寻找
                    searchFiles(file,fileName)
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Character&lt;/h3&gt;
&lt;p&gt;字符集：为字符编制的一套编号规则&lt;/p&gt;
&lt;p&gt;计算机的底层是不能直接存储字符的，只能存储二进制 010101&lt;/p&gt;
&lt;p&gt;ASCII 编码：8 个开关一组就可以编码字符，1 个字节 2^8 = 256， 一个字节存储一个字符完全够用，英文和数字在底层存储都是采用 1 个字节存储的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a  97
b  98

A  65
B  66

0  48
1  49
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中国人：中国人有 9 万左右字符，2 个字节编码一个中文字符，1 个字节编码一个英文字符，这套编码叫：GBK 编码，兼容 ASCII 编码表&lt;/p&gt;
&lt;p&gt;美国人：收集全球所有的字符，统一编号，这套编码叫 Unicode 编码（万国码），一个英文等于两个字节，一个中文（含繁体）等于两个字节，中文标点占两个字节，英文标点占两个字节&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UTF-8 是变种形式，也必须兼容 ASCII 编码表&lt;/li&gt;
&lt;li&gt;UTF-8 一个中文一般占 3 个字节，中文标点占 3 个，英文字母和数字 1 个字节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编码前与编码后的编码集必须一致才不会乱码&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;IOStream&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;IO 输入输出流：输入/输出流&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input：输入&lt;/li&gt;
&lt;li&gt;Output：输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引入：File 类只能操作文件对象本身，不能读写文件对象的内容，读写数据内容，应该使用 IO 流&lt;/p&gt;
&lt;p&gt;IO 流是一个水流模型：IO 理解成水管，把数据理解成水流&lt;/p&gt;
&lt;p&gt;IO 流的分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按照流的方向分为：输入流，输出流。
&lt;ul&gt;
&lt;li&gt;输出流：以内存为基准，把内存中的数据&lt;strong&gt;写出到磁盘文件&lt;/strong&gt;或者网络介质中去的流称为输出流&lt;/li&gt;
&lt;li&gt;输入流：以内存为基准，把磁盘文件中的数据或者网络中的数据&lt;strong&gt;读入到内存&lt;/strong&gt;中的流称为输入流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按照流的内容分为：字节流，字符流
&lt;ul&gt;
&lt;li&gt;字节流：流中的数据的最小单位是一个一个的字节，这个流就是字节流&lt;/li&gt;
&lt;li&gt;字符流：流中的数据的最小单位是一个一个的字符，这个流就是字符流（&lt;strong&gt;针对于文本内容&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流大体分为四大类：字节输入流、字节输出流、字符输入流、字符输出流&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IO 流的体系：
        字节流                                   字符流
  字节输入流              字节输出流            字符输入流         字符输出流
InputStream           OutputStream          Reader            Writer   (抽象类)
FileInputStream       FileOutputStream      FileReader        FileWriter(实现类)
BufferedInputStream  BufferedOutputStream  BufferedReader   BufferedWriter(实现类缓冲流)
                                           InputStreamReader OutputStreamWriter
ObjectInputStream     ObjectOutputStream
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;字节流&lt;/h4&gt;
&lt;h5&gt;字节输入&lt;/h5&gt;
&lt;p&gt;FileInputStream 文件字节输入流：以内存为基准，把磁盘文件中的数据按照字节的形式读入到内存中的流&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public FileInputStream(File path)&lt;/code&gt;：创建一个字节输入流管道与源文件对象接通&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileInputStream(String pathName)&lt;/code&gt;：创建一个字节输入流管道与文件路径对接，底层实质上创建 File 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public int read()&lt;/code&gt;：每次读取一个字节返回，读取完毕会返回 -1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int read(byte[] buffer)&lt;/code&gt;：从字节输入流中读取字节到字节数组中去，返回读取的字节数量，没有字节可读返回 -1，&lt;strong&gt;byte 中新读取的数据默认是覆盖原数据&lt;/strong&gt;，构造 String 需要设定长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public String(byte[] bytes,int offset,int length)&lt;/code&gt;：构造新的 String&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public long transferTo(OutputStream out) &lt;/code&gt;：从输入流中读取所有字节，并按读取的顺序，将字节写入给定的输出流&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class FileInputStreamDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.创建文件对象定位dlei01.txt
        File file = new File(&quot;Demo/src/dlei01.txt&quot;);
        // 2.创建一个字节输入流管道与源文件接通
        InputStream is = new FileInputStream(file);
        // 3.读取一个字节的编号返回，读取完毕返回-1
		//int code1 = is.read(); // 读取一滴水，一个字节
		//System.out.println((char)code1);

        // 4.使用while读取字节数
        // 定义一个整数变量存储字节
        int ch = 0 ;
        while((ch = is.read())!= -1){
            System.out.print((char) ch);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个一个字节读取英文和数字没有问题，但是读取中文输出无法避免乱码，因为会截断中文的字节。一个一个字节的读取数据，性能也较差，所以&lt;strong&gt;禁止使用上面的方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;采取下面的方案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    //简化写法，底层实质上创建了File对象
    InputStream is = new FileInputStream(&quot;Demo/src/test.txt&quot;);
    byte[] buffer = new byte[3];//开发中使用byte[1024]
    int len;
    while((len = is.read(buffer)) !=-1){
        // 读取了多少就倒出多少！
        String rs = new String(buffer, 0, len);
        System.out.print(rs);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;File f = new File(&quot;Demo/src/test.txt&quot;);
InputStream is = new FileInputStream(f);
// 读取全部的
byte[] buffer = is.readAllBytes();
String rs = new String(buffer);
System.out.println(rs);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;字节输出&lt;/h5&gt;
&lt;p&gt;FileOutputStream 文件字节输出流：以内存为基准，把内存中的数据，按照字节的形式写出到磁盘文件中去&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public FileOutputStream(File file)&lt;/code&gt;：创建一个字节输出流管道通向目标文件对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileOutputStream(String file) &lt;/code&gt;：创建一个字节输出流管道通向目标文件路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileOutputStream(File file, boolean append)&lt;/code&gt; : 创建一个追加数据的字节输出流管道到目标文件对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileOutputStream(String file, boolean append)&lt;/code&gt; : 创建一个追加数据的字节输出流管道通向目标文件路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public void write(int a)&lt;/code&gt;：写一个字节出去&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public void write(byte[] buffer)&lt;/code&gt;：写一个字节数组出去&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public void write(byte[] buffer , int pos , int len)&lt;/code&gt;：写一个字节数组的一部分出去，从 pos 位置，写出 len 长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据，重新写入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OutputStream os = new FileOutputStream(&quot;Demo/out05&quot;)&lt;/code&gt;：覆盖数据管道&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OutputStream os = new FileOutputStream(&quot;Demo/out05&quot; , true)&lt;/code&gt;：追加数据的管道&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节输出流只能写字节出去，字节输出流默认是&lt;strong&gt;覆盖&lt;/strong&gt;数据管道&lt;/li&gt;
&lt;li&gt;换行用：&lt;strong&gt;os.write(&quot;\r\n&quot;.getBytes())&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;关闭和刷新：刷新流可以继续使用，关闭包含刷新数据但是流就不能使用了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;OutputStream os = new FileOutputStream(&quot;Demo/out05&quot;);
os.write(97);//a
os.write(&apos;b&apos;);
os.write(&quot;\r\n&quot;.getBytes());
os.write(&quot;我爱Java&quot;.getBytes());
os.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;文件复制&lt;/h5&gt;
&lt;p&gt;字节是计算机中一切文件的组成，所以字节流适合做一切文件的复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CopyDemo01 {
    public static void main(String[] args) {
        InputStream is = null ;
        OutputStream os = null ;
        try{
            //（1）创建一个字节输入流管道与源文件接通。
            is = new FileInputStream(&quot;D:\\seazean\\图片资源\\test.jpg&quot;);
            //（2）创建一个字节输出流与目标文件接通。
            os = new FileOutputStream(&quot;D:\\seazean\\test.jpg&quot;);
            //（3）创建一个字节数组作为桶
            byte buffer = new byte[1024];
            //（4）从字节输入流管道中读取数据，写出到字节输出流管道即可
            int len = 0;
            while((len = is.read(buffer)) != -1){
                os.write(buffer,0,len);
            }
            System.out.println(&quot;复制完成！&quot;);
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            /**（5）关闭资源！ */
            try{
                if(os!=null)os.close();
                if(is!=null)is.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;字符流&lt;/h4&gt;
&lt;h5&gt;字符输入&lt;/h5&gt;
&lt;p&gt;FileReader：文件字符输入流，以内存为基准，把磁盘文件的数据以字符的形式读入到内存，读取文本文件内容到内存中去&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public FileReader(File file)&lt;/code&gt;：创建一个字符输入流与源文件对象接通。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileReader(String filePath)&lt;/code&gt;：创建一个字符输入流与源文件路径接通。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public int read()&lt;/code&gt;：读取一个字符的编号返回，读取完毕返回 -1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int read(char[] buffer)&lt;/code&gt;：读取一个字符数组，读取多少个就返回多少个，读取完毕返回 -1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符流一个一个字符的读取文本内容输出，可以解决中文读取输出乱码的问题，适合操作文本文件，但是一个一个字符的读取文本内容性能较差&lt;/li&gt;
&lt;li&gt;字符流按照&lt;strong&gt;字符数组循环读取数据&lt;/strong&gt;，可以解决中文读取输出乱码的问题，而且性能也较好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;字符流不能复制图片，视频等类型的文件&lt;/strong&gt;。字符流在读取完了字节数据后并没有直接往目的地写，而是先查编码表，查到对应的数据就将该数据写入目的地。如果查不到，则码表会将一些未知区域中的数据去 map 这些字节数据，然后写到目的地，这样的话就造成了源数据和目的数据的不一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class FileReaderDemo01{//字符
    public static void main(String[] args) throws Exception {
        // 创建一个字符输入流管道与源文件路径接通
        Reader fr = new FileReader(&quot;Demo/src/test.txt&quot;);
        int ch;
        while((ch = fr.read()) != -1){
            System.out.print((char)ch);
        }
    }
}
public class FileReaderDemo02 {//字符数组
    public static void main(String[] args) throws Exception {
        Reader fr = new FileReader(&quot;Demo/src/test.txt&quot;);
        
        char[] buffer = new char[1024];
        int len;
        while((len = fr.read(buffer)) != -1) {
            System.out.print(new String(buffer, 0 , len));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;字符输出&lt;/h5&gt;
&lt;p&gt;FileWriter：文件字符输出流，以内存为基准，把内存中的数据按照字符的形式写出到磁盘文件中去&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public FileWriter(File file)&lt;/code&gt;：创建一个字符输出流管道通向目标文件对象（覆盖数据管道）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileWriter(String filePath)&lt;/code&gt;：创建一个字符输出流管道通向目标文件路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileWriter(File file, boolean append)&lt;/code&gt;：创建一个追加数据的字符输出流管道通向文件对象（追加数据管道）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public FileWriter(String filePath, boolean append)&lt;/code&gt;：创建一个追加数据的字符输出流管道通向目标文件路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void write(int c)&lt;/code&gt;：写一个字符出去&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void write(char[] buffer)&lt;/code&gt;：写一个字符数组出去&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void write(String c, int pos, int len)&lt;/code&gt;：写字符串的一部分出去&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void write(char[] buffer, int pos, int len)&lt;/code&gt;：写字符数组的一部分出去&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fw.write(&quot;\r\n&quot;)&lt;/code&gt;：换行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读写字符文件数据建议使用字符流&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Writer fw = new FileWriter(&quot;Demo/src/test.txt&quot;);
fw.write(97);   // 字符a
fw.write(&apos;b&apos;);  // 字符b
fw.write(&quot;Java是最优美的语言！&quot;);
fw.write(&quot;\r\n&quot;);
fw.close;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;缓冲流&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;缓冲流可以提高字节流和字符流的读写数据的性能&lt;/p&gt;
&lt;p&gt;缓冲流分为四类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BufferedInputStream：字节缓冲输入流，可以提高字节输入流读数据的性能&lt;/li&gt;
&lt;li&gt;BufferedOutStream：字节缓冲输出流，可以提高字节输出流写数据的性能&lt;/li&gt;
&lt;li&gt;BufferedReader：字符缓冲输入流，可以提高字符输入流读数据的性能&lt;/li&gt;
&lt;li&gt;BufferedWriter：字符缓冲输出流，可以提高字符输出流写数据的性能&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;字节缓冲输入&lt;/h5&gt;
&lt;p&gt;字节缓冲输入流：BufferedInputStream&lt;/p&gt;
&lt;p&gt;作用：可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道，提高字节输入流读数据的性能&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public BufferedInputStream(InputStream in)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;原理：缓冲字节输入流管道自带了一个 8KB 的缓冲池，每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去，以后我们直接从缓冲池读取数据，所以性能较好&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BufferedInputStreamDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.定义一个低级的字节输入流与源文件接通
        InputStream is = new FileInputStream(&quot;Demo/src/test.txt&quot;);
        // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。
        BufferInputStream bis = new BufferInputStream(is);
        // 3.定义一个字节数组按照循环读取。
        byte[] buffer = new byte[1024];
        int len;
        while((len = bis.read(buffer)) != -1){
            String rs = new String(buffer, 0 , len);
            System.out.print(rs);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;字节缓冲输出&lt;/h5&gt;
&lt;p&gt;字节缓冲输出流：BufferedOutputStream&lt;/p&gt;
&lt;p&gt;作用：可以把低级的字节输出流包装成一个高级的缓冲字节输出流，从而提高写数据的性能&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public BufferedOutputStream(OutputStream os)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;原理：缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去，性能提高了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BufferedOutputStreamDemo02 {
    public static void main(String[] args) throws Exception {
        // 1.写一个原始的字节输出流
        OutputStream os = new FileOutputStream(&quot;Demo/src/test.txt&quot;);
        // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流
        BufferedOutputStream bos =  new BufferedOutputStream(os);
        // 3.写数据出去
        bos.write(&apos;a&apos;);
        bos.write(100);
        bos.write(&quot;我爱中国&quot;.getBytes());
        bos.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;字节流性能&lt;/h5&gt;
&lt;p&gt;利用字节流的复制统计各种写法形式下缓冲流的性能执行情况&lt;/p&gt;
&lt;p&gt;复制流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用低级的字节流按照一个一个字节的形式复制文件&lt;/li&gt;
&lt;li&gt;使用低级的字节流按照一个一个字节数组的形式复制文件&lt;/li&gt;
&lt;li&gt;使用高级的缓冲字节流按照一个一个字节的形式复制文件&lt;/li&gt;
&lt;li&gt;使用高级的缓冲字节流按照一个一个字节数组的形式复制文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;高级的缓冲字节流按照一个一个字节数组的形式复制文件，性能最高，建议使用&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;字符缓冲输入&lt;/h5&gt;
&lt;p&gt;字符缓冲输入流：BufferedReader&lt;/p&gt;
&lt;p&gt;作用：字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流，可以提高字符输入流读数据的性能。&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public BufferedReader(Reader reader)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;原理：缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能&lt;/p&gt;
&lt;p&gt;按照行读取数据的功能：&lt;code&gt;public String readLine()&lt;/code&gt;  读取一行数据返回，读取完毕返回 null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    // 1.定义一个原始的字符输入流读取源文件
    Reader fr = new FileReader(&quot;Demo/src/test.txt&quot;);
    // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道
    BufferedReader br = new BufferedReader(fr);
    // 定义一个字符串变量存储每行数据
    String line;
    while((line = br.readLine()) != null){
        System.out.println(line);
    }
    br.close();
    //淘汰数组循环读取
    //char[] buffer = new char[1024];
    //int len;
    //while((len = br.read(buffer)) != -1){
    //System.out.println(new String(buffer , 0 , len));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;字符缓冲输出&lt;/h5&gt;
&lt;p&gt;符缓冲输出流：BufferedWriter&lt;/p&gt;
&lt;p&gt;作用：把低级的字符输出流包装成一个高级的缓冲字符输出流，提高写字符数据的性能。&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public BufferedWriter(Writer writer)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;原理：高级的字符缓冲输出流多了一个 8K 的字符缓冲池，写数据性能极大提高了&lt;/p&gt;
&lt;p&gt;字符缓冲输出流多了一个换行的特有功能：&lt;code&gt;public void newLine()&lt;/code&gt;  &lt;strong&gt;新建一行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    Writer fw = new FileWriter(&quot;Demo/src/test.txt&quot;,true);//追加
    BufferedWriter bw = new BufferedWriter(fw);
    
    bw.write(&quot;我爱学习Java&quot;);
    bw.newLine();//换行
    bw.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;高效原因&lt;/h5&gt;
&lt;p&gt;字符型缓冲流高效的原因：（空间换时间）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BufferedReader：每次调用 read 方法，只有第一次从磁盘中读取了 8192（&lt;strong&gt;8k&lt;/strong&gt;）个字符，存储到该类型对象的缓冲区数组中，将其中一个返回给调用者，再次调用 read 方法时，就不需要访问磁盘，直接从缓冲区中拿出一个数据即可，提升了效率&lt;/li&gt;
&lt;li&gt;BufferedWriter：每次调用 write 方法，不会直接将字符刷新到文件中，而是存储到字符数组中，等字符数组写满了，才一次性刷新到文件中，减少了和磁盘交互的次数，提升了效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字节型缓冲流高效的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BufferedInputStream：在该类型中准备了一个数组，存储字节信息，当外界调用 read() 方法想获取一个字节的时候，该对象从文件中一次性读取了 8192 个字节到数组中，只返回了第一个字节给调用者。将来调用者再次调用 read 方法时，当前对象就不需要再次访问磁盘，只需要从数组中取出一个字节返回给调用者即可，由于读取的是数组，所以速度非常快。当 8192 个字节全都读取完成之后，再需要读取一个字节，就得让该对象到文件中读取下一个 8192 个字节&lt;/li&gt;
&lt;li&gt;BufferedOutputStream：在该类型中准备了一个数组，存储字节信息，当外界调用 write 方法想写出一个字节的时候，该对象直接将这个字节存储到了自己的数组中，而不刷新到文件中。一直到该数组所有 8192 个位置全都占满，该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环没有将数组写满，最终在关闭流对象的时候，也会将该数组中的数据刷新到文件中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;strong&gt;字节流和字符流，都是装满时自动写出，或者没满时手动 flush 写出，或 close 时刷新写出&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;转换流&lt;/h4&gt;
&lt;h5&gt;乱码问题&lt;/h5&gt;
&lt;p&gt;字符流读取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;代码编码            文件编码         中文情况。
UTF-8              UTF-8           不乱码!
GBK                GBK             不乱码!
UTF-8              GBK             乱码!
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果代码编码和读取的文件编码一致，字符流读取的时候不会乱码&lt;/li&gt;
&lt;li&gt;如果代码编码和读取的文件编码不一致，字符流读取的时候会乱码&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;字符输入&lt;/h5&gt;
&lt;p&gt;字符输入转换流：InputStreamReader&lt;/p&gt;
&lt;p&gt;作用：解决字符流读取不同编码的乱码问题，把原始的&lt;strong&gt;字节流&lt;/strong&gt;按照默认的编码或指定的编码&lt;strong&gt;转换成字符输入流&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public InputStreamReader(InputStream is)&lt;/code&gt;：使用当前代码默认编码 UTF-8 转换成字符流&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public InputStreamReader(InputStream is, String charset)&lt;/code&gt;：指定编码把字节流转换成字符流&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class InputStreamReaderDemo{
    public static void main(String[] args) throws Exception {
        // 1.提取GBK文件的原始字节流
        InputStream is = new FileInputStream(&quot;D:\\seazean\\Netty.txt&quot;);
        // 2.把原始字节输入流通过转换流，转换成 字符输入转换流InputStreamReader
        InputStreamReader isr = new InputStreamReader(is, &quot;GBK&quot;); 
        // 3.包装成缓冲流
        BufferedReader br = new BufferedReader(isr);
        //循环读取
        String line;
        while((line = br.readLine()) != null){
            System.out.println(line);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;字符输出&lt;/h5&gt;
&lt;p&gt;字符输出转换流：OutputStreamWriter&lt;/p&gt;
&lt;p&gt;作用：可以指定编码&lt;strong&gt;把字节输出流转换成字符输出流&lt;/strong&gt;，可以指定写出去的字符的编码&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public OutputStreamWriter(OutputStream os)&lt;/code&gt;：用默认编码 UTF-8 把字节输出流转换成字符输出流&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public OutputStreamWriter(OutputStream os, String charset)&lt;/code&gt;：指定编码把字节输出流转换成&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;OutputStream os = new FileOutputStream(&quot;Demo/src/test.txt&quot;);
OutputStreamWriter osw = new OutputStreamWriter(os,&quot;GBK&quot;);
osw.write(&quot;我在学习Java&quot;);   
osw.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;序列化&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;对象序列化：把 Java 对象转换成字节序列的过程，将对象写入到 IO 流中，对象 =&amp;gt; 文件中&lt;/p&gt;
&lt;p&gt;对象反序列化：把字节序列恢复为 Java 对象的过程，从 IO 流中恢复对象，文件中 =&amp;gt; 对象&lt;/p&gt;
&lt;p&gt;transient 关键字修饰的成员变量，将不参与序列化&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;序列化&lt;/h5&gt;
&lt;p&gt;对象序列化流（对象字节输出流）：ObjectOutputStream&lt;/p&gt;
&lt;p&gt;作用：把内存中的 Java 对象数据保存到文件中去&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public ObjectOutputStream(OutputStream out)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;序列化方法：&lt;code&gt;public final void writeObject(Object obj)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意：对象如果想参与序列化，对象必须实现序列化接口 &lt;strong&gt;implements Serializable&lt;/strong&gt; ，否则序列化失败&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SerializeDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.创建User用户对象
        User user = new User(&quot;seazean&quot;,&quot;980823&quot;,&quot;七十一&quot;);
        // 2.创建低级的字节输出流通向目标文件
        OutputStream os = new FileOutputStream(&quot;Demo/src/obj.dat&quot;);
        // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream
        ObjectOutputStream oos = new ObjectOutputStream(os);
        // 4.通过对象字节输出流序列化对象：
        oos.writeObject(user);
        // 5.释放资源
        oos.close();
        System.out.println(&quot;序列化对象成功~~~~&quot;);
    }
}

class User implements Serializable {
    // 加入序列版本号
    private static final long serialVersionUID = 1L;

    private String loginName;
    private transient String passWord;
    private String userName;
    // get+set
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 序列化为二进制数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);	// 将该对象序列化为二进制数据
oos.flush();
byte[] bytes = bos.toByteArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;反序列&lt;/h5&gt;
&lt;p&gt;对象反序列化（对象字节输入流）：ObjectInputStream&lt;/p&gt;
&lt;p&gt;作用：读取序列化的对象文件恢复到 Java 对象中&lt;/p&gt;
&lt;p&gt;构造器：&lt;code&gt;public ObjectInputStream(InputStream is)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;方法：&lt;code&gt;public final Object readObject()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;序列化版本号：&lt;code&gt;private static final long serialVersionUID = 2L&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意：序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化，否则报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SerializeDemo02 {
    public static void main(String[] args) throws Exception {
        InputStream is = new FileInputStream(&quot;Demo/src/obj.dat&quot;);
        ObjectInputStream ois = new ObjectInputStream(is);
        User user = (User)ois.readObject();//反序列化
        System.out.println(user);
        System.out.println(&quot;反序列化完成！&quot;);
    }
}
class User implements Serializable {
    // 加入序列版本号
    private static final long serialVersionUID = 1L;
    //........
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;打印流&lt;/h4&gt;
&lt;p&gt;打印流 PrintStream / PrintWriter&lt;/p&gt;
&lt;p&gt;打印流的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以方便，快速的写数据出去，可以实现打印什么类型，就是什么类型&lt;/li&gt;
&lt;li&gt;PrintStream/PrintWriter 不光可以打印数据，还可以写字节数据和字符数据出去&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System.out.print() 底层基于打印流实现的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public PrintStream(OutputStream os)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public PrintStream(String filepath)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;System 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public static void setOut(PrintStream out)&lt;/code&gt;：让系统的输出流向打印流&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class PrintStreamDemo01 {
    public static void main(String[] args) throws Exception {
        PrintStream ps = new  PrintStream(&quot;Demo/src/test.txt&quot;);
        ps.println(任何类型的数据);
        ps.print(不换行);
        ps.write(&quot;我爱你&quot;.getBytes());
        ps.close();
    }
}
public class PrintStreamDemo02 {
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;==seazean0==&quot;);
        PrintStream ps = new PrintStream(&quot;Demo/src/log.txt&quot;);
        System.setOut(ps); // 让系统的输出流向打印流
		//不输出在控制台，输出到文件里
        System.out.println(&quot;==seazean1==&quot;);
        System.out.println(&quot;==seazean2==&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Close&lt;/h3&gt;
&lt;p&gt;try-with-resources：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try(
    // 这里只能放置资源对象，用完会自动调用close()关闭
){

}catch(Exception e){
 	e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;资源类一定是实现了 Closeable 接口，实现这个接口的类就是资源&lt;/p&gt;
&lt;p&gt;有 close() 方法，try-with-resources 会自动调用它的 close() 关闭资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try(
	/** （1）创建一个字节输入流管道与源文件接通。 */
	InputStream is  = new FileInputStream(&quot;D:\\seazean\\图片资源\\meinv.jpg&quot;);
	/** （2）创建一个字节输出流与目标文件接通。*/
	OutputStream os = new FileOutputStream(&quot;D:\\seazean\\meimei.jpg&quot;);
	/** （5）关闭资源！是自动进行的 */
){
	byte[] buffer = new byte[1024];
	int len = 0;
	while((len = is.read(buffer)) != -1){
		os.write(buffer, 0 , len);
	}
	System.out.println(&quot;复制完成！&quot;);
}catch (Exception e){
	e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Properties&lt;/h3&gt;
&lt;p&gt;Properties：属性集对象。就是一个 Map 集合，一个键值对集合&lt;/p&gt;
&lt;p&gt;核心作用：Properties 代表的是一个属性文件，可以把键值对数据存入到一个属性文件&lt;/p&gt;
&lt;p&gt;属性文件：后缀是 &lt;code&gt;.properties&lt;/code&gt; 结尾的文件，里面的内容都是 key=value&lt;/p&gt;
&lt;p&gt;Properties 方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Object setProperty(String key, String value)&lt;/td&gt;
&lt;td&gt;设置集合的键和值，底层调用 Hashtable 方法 put&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getProperty(String key)&lt;/td&gt;
&lt;td&gt;使用此属性列表中指定的键搜索属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set&amp;lt;String&amp;gt;   stringPropertyNames()&lt;/td&gt;
&lt;td&gt;所有键的名称的集合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;synchronized void load(Reader r)&lt;/td&gt;
&lt;td&gt;从输入字符流读取属性列表（键和元素对）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;synchronized void load(InputStream in)&lt;/td&gt;
&lt;td&gt;加载属性文件的数据到属性集对象中去&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void store(Writer w, String comments)&lt;/td&gt;
&lt;td&gt;将此属性列表(键和元素对)写入 Properties 表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void store(OutputStream os, String comments)&lt;/td&gt;
&lt;td&gt;保存数据到属性文件中去&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class PropertiesDemo01 {
    public static void main(String[] args) throws Exception {
        // a.创建一个属性集对象：Properties的对象。
        Properties properties = new Properties();//{}
        properties.setProperty(&quot;admin&quot; , &quot;123456&quot;);
        // b.把属性集对象的数据存入到属性文件中去（重点）
        OutputStream os = new FileOutputStream(&quot;Demo/src/users.properties&quot;);
        properties.store(os,&quot;i am very happy!!我保存了用户数据!&quot;);
        //参数一：被保存数据的输出管道
        //参数二：保存心得。就是对象保存的数据进行解释说明！
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class PropertiesDemo02 {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();//底层基于map集合
        properties.load(new FileInputStream(&quot;Demo/src/users.properties&quot;));
        System.out.println(properties);
        System.out.println(properties.getProperty(&quot;admin&quot;));
        
		Set&amp;lt;String&amp;gt; set = properties.stringPropertyNames();
        for (String s : set) {
            String value = properties.getProperty(s);
            System.out.println(s + value);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;RandomIO&lt;/h3&gt;
&lt;p&gt;RandomAccessFile 类：该类的实例支持读取和写入随机访问文件&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RandomAccessFile(File file, String mode)&lt;/code&gt;：创建随机访问文件流，从 File 参数指定的文件读取，可选择写入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RandomAccessFile(String name, String mode)&lt;/code&gt;：创建随机访问文件流，从指定名称文件读取，可选择写入文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void seek(long pos)&lt;/code&gt;：设置文件指针偏移，从该文件开头测量，发生下一次读取或写入(插入+覆盖)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void write(byte[] b)&lt;/code&gt;：从指定的字节数组写入 b.length 个字节到该文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int read(byte[] b)&lt;/code&gt;：从该文件读取最多 b.length 个字节的数据到字节数组&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    RandomAccessFile rf = new RandomAccessFile(new File(),&quot;rw&quot;);
    rf.write(&quot;hello world&quot;.getBytes());
    rf.seek(5);//helloxxxxld
    rf.write(&quot;xxxx&quot;.getBytes());
    rf.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Commons&lt;/h3&gt;
&lt;p&gt;commons-io 是 apache 提供的一组有关 IO 操作的类库，可以提高 IO 功能开发的效率&lt;/p&gt;
&lt;p&gt;commons-io 工具包提供了很多有关 IO 操作的类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;包&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;org.apache.commons.io&lt;/td&gt;
&lt;td&gt;有关 Streams、Readers、Writers、Files 的工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;org.apache.commons.io.input&lt;/td&gt;
&lt;td&gt;输入流相关的实现类，包含 Reader 和 InputStream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;org.apache.commons.io.output&lt;/td&gt;
&lt;td&gt;输出流相关的实现类，包含 Writer 和 OutputStream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;org.apache.commons.io.serialization&lt;/td&gt;
&lt;td&gt;序列化相关的类&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;IOUtils 和 FileUtils 可以方便的复制文件和文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CommonsIODemo01 {
    public static void main(String[] args) throws Exception {
        // 1.完成文件复制！
        IOUtils.copy(new FileInputStream(&quot;Demo/src/books.xml&quot;), 
                     new FileOutputStream(&quot;Demo/new.xml&quot;));
        // 2.完成文件复制到某个文件夹下！
        FileUtils.copyFileToDirectory(new File(&quot;Demo/src/books.xml&quot;),
                                      new File(&quot;D:/it&quot;));
        // 3.完成文件夹复制到某个文件夹下！
        FileUtils.copyDirectoryToDirectory(new File(&quot;D:\\it\\图片服务器&quot;) ,
                                           new File(&quot;D:\\&quot;));

        //  Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。
        Files.copy(Paths.get(&quot;Demo/src/books.xml&quot;)
                , new FileOutputStream(&quot;Demo/new11.txt&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;反射&lt;/h2&gt;
&lt;h3&gt;测试框架&lt;/h3&gt;
&lt;p&gt;单元测试的经典框架：Junit，是 Java 语言编写的第三方单元测试框架&lt;/p&gt;
&lt;p&gt;单元测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单元：在 Java 中，一个类就是一个单元&lt;/li&gt;
&lt;li&gt;单元测试：Junit 编写的一小段代码，用来对某个类中的某个方法进行功能测试或业务逻辑测试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Junit 单元测试框架的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用来对类中的方法功能进行有目的的测试，以保证程序的正确性和稳定性&lt;/li&gt;
&lt;li&gt;能够&lt;strong&gt;独立的&lt;/strong&gt;测试某个方法或者所有方法的预期正确性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试方法注意事项：&lt;strong&gt;必须是 public 修饰的，没有返回值，没有参数，使用注解@Test修饰&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Junit常用注解（Junit 4.xxxx 版本），@Test 测试方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Before：用来修饰实例方法，该方法会在每一个测试方法执行之前执行一次&lt;/li&gt;
&lt;li&gt;@After：用来修饰实例方法，该方法会在每一个测试方法执行之后执行一次&lt;/li&gt;
&lt;li&gt;@BeforeClass：用来静态修饰方法，该方法会在所有测试方法之前&lt;strong&gt;只&lt;/strong&gt;执行一次&lt;/li&gt;
&lt;li&gt;@AfterClass：用来静态修饰方法，该方法会在所有测试方法之后&lt;strong&gt;只&lt;/strong&gt;执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Junit 常用注解（Junit5.xxxx 版本），@Test 测试方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@BeforeEach：用来修饰实例方法，该方法会在每一个测试方法执行之前执行一次&lt;/li&gt;
&lt;li&gt;@AfterEach：用来修饰实例方法，该方法会在每一个测试方法执行之后执行一次&lt;/li&gt;
&lt;li&gt;@BeforeAll：用来静态修饰方法，该方法会在所有测试方法之前只执行一次&lt;/li&gt;
&lt;li&gt;@AfterAll：用来静态修饰方法，该方法会在所有测试方法之后只执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开始执行的方法：初始化资源&lt;/li&gt;
&lt;li&gt;执行完之后的方法：释放资源&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UserService {
    public String login(String loginName , String passWord){
        if(&quot;admin&quot;.equals(loginName)&amp;amp;&amp;amp;&quot;123456&quot;.equals(passWord)){
            return &quot;success&quot;;
        }
        return &quot;用户名或者密码错误！&quot;;
    }
    public void chu(int a , int b){
        System.out.println(a / b);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试方法的要求：1.必须public修饰 2.没有返回值没有参数 3. 必须使注解@Test修饰
public class UserServiceTest {
     // @Before：用来修饰实例方法，该方法会在每一个测试方法执行之前执行一次。
    @Before
    public void before(){
        System.out.println(&quot;===before===&quot;);
    }
    // @After：用来修饰实例方法，该方法会在每一个测试方法执行之后执行一次。
    @After
    public void after(){
        System.out.println(&quot;===after===&quot;);
    }
    // @BeforeClass：用来静态修饰方法，该方法会在所有测试方法之前只执行一次。
    @BeforeClass
    public static void beforeClass(){
        System.out.println(&quot;===beforeClass===&quot;);
    }
    // @AfterClass：用来静态修饰方法，该方法会在所有测试方法之后只执行一次。
    @AfterClass
    public static void afterClass(){
        System.out.println(&quot;===afterClass===&quot;);
    }
    @Test
    public void testLogin(){
        UserService userService = new UserService();
        String rs = userService.login(&quot;admin&quot;,&quot;123456&quot;);
        /**断言预期结果的正确性。
         * 参数一：测试失败的提示信息。
         * 参数二：期望值。
         * 参数三：实际值
         */
        Assert.assertEquals(&quot;登录业务功能方法有错误，请检查！&quot;,&quot;success&quot;,rs);
    }
    @Test
    public void testChu(){
        UserService userService = new UserService();
        userService.chu(10 , 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;介绍反射&lt;/h3&gt;
&lt;p&gt;反射是指对于任何一个类，在&quot;运行的时候&quot;都可以直接得到这个类全部成分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造器对象：Constructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员变量对象：Field&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法对象：Method&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心思想：在运行时获取类编译后的字节码文件对象，然后解析类中的全部成分&lt;/p&gt;
&lt;p&gt;反射提供了一个 Class 类型：HelloWorld.java → javac → HelloWorld.class&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Class c = HelloWorld.class&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：反射是工作在&lt;strong&gt;运行时&lt;/strong&gt;的技术，只有运行之后才会有 class 类对象&lt;/p&gt;
&lt;p&gt;作用：可以在运行时得到一个类的全部成分然后操作，破坏封装性，也可以破坏泛型的约束性。&lt;/p&gt;
&lt;p&gt;反射的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可扩展性：应用程序可以利用全限定名创建可扩展对象的实例，来使用来自外部的用户自定义类&lt;/li&gt;
&lt;li&gt;类浏览器和可视化开发环境：一个类浏览器需要可以枚举类的成员，可视化开发环境（如 IDE）可以从利用反射中可用的类型信息中受益，以帮助程序员编写正确的代码&lt;/li&gt;
&lt;li&gt;调试器和测试工具： 调试器需要能够检查一个类里的私有成员，测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义，以确保一组测试中有较高的代码覆盖率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反射的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能开销&lt;/strong&gt;：反射涉及了动态类型的解析，所以 JVM 无法对这些代码进行优化，反射操作的效率要比那些非射操作低得多，应该避免在经常被执行的代码或对性能要求很高的程序中使用反射&lt;/li&gt;
&lt;li&gt;安全限制：使用反射技术要求程序必须在一个没有安全限制的环境中运行，如果一个程序必须在有安全限制的环境中运行&lt;/li&gt;
&lt;li&gt;内部暴露：由于反射允许代码执行一些在正常情况下不被允许的操作（比如访问私有的属性和方法），所以使用反射可能会导致意料之外的副作用，这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性，因此当平台发生改变的时候，代码的行为就有可能也随着变化&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;获取元素&lt;/h3&gt;
&lt;h4&gt;获取类&lt;/h4&gt;
&lt;p&gt;反射技术的第一步是先得到 Class 类对象，有三种方式获取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类名.class&lt;/li&gt;
&lt;li&gt;类的对象.getClass()&lt;/li&gt;
&lt;li&gt;Class.forName(&quot;类的全限名&quot;)：&lt;code&gt;public static Class&amp;lt;?&amp;gt; forName(String className) &lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Class 类下的方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getSimpleName()&lt;/td&gt;
&lt;td&gt;获得类名字符串：类名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getName()&lt;/td&gt;
&lt;td&gt;获得类全名：包名+类名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T newInstance()&lt;/td&gt;
&lt;td&gt;创建 Class 对象关联类的对象，底层是调用无参数构造器，已经被淘汰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class ReflectDemo{
    public static void main(String[] args) throws Exception {
        // 反射的第一步永远是先得到类的Class文件对象: 字节码文件。
        // 1.类名.class
        Class c1 = Student.class;
        System.out.println(c1);//class _03反射_获取Class类对象.Student

        // 2.对象.getClass()
        Student swk = new Student();
        Class c2 = swk.getClass();
        System.out.println(c2);

        // 3.Class.forName(&quot;类的全限名&quot;)
        // 直接去加载该类的class文件。
        Class c3 = Class.forName(&quot;_03反射_获取Class类对象.Student&quot;);
        System.out.println(c3);

        System.out.println(c1.getSimpleName()); // 获取类名本身（简名）Student
        System.out.println(c1.getName()); //获取类的全限名_03反射_获取Class类对象.Student
    }
}
class Student{}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取构造&lt;/h4&gt;
&lt;p&gt;获取构造器的 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Constructor getConstructor(Class... parameterTypes)：根据参数匹配获取某个构造器，只能拿 public 修饰的构造器&lt;/li&gt;
&lt;li&gt;Constructor getDeclaredConstructor(Class... parameterTypes)：根据参数匹配获取某个构造器，只要申明就可以定位，不关心权限修饰符&lt;/li&gt;
&lt;li&gt;Constructor[] getConstructors()：获取所有的构造器，只能拿 public 修饰的构造器&lt;/li&gt;
&lt;li&gt;Constructor[] getDeclaredConstructors()：获取所有构造器，只要申明就可以定位，不关心权限修饰符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Constructor 的常用 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T newInstance(Object... initargs)&lt;/td&gt;
&lt;td&gt;创建对象，注入构造器需要的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setAccessible(true)&lt;/td&gt;
&lt;td&gt;修改访问权限，true 攻破权限（暴力反射）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getName()&lt;/td&gt;
&lt;td&gt;以字符串形式返回此构造函数的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getParameterCount()&lt;/td&gt;
&lt;td&gt;返回参数数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class&amp;lt;?&amp;gt;[] getParameterTypes&lt;/td&gt;
&lt;td&gt;返回参数类型数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class TestStudent01 {
    @Test
    public void getDeclaredConstructors(){
        // a.反射第一步先得到Class类对象
        Class c = Student.class ;
        // b.定位全部构造器，只要申明了就可以拿到
        Constructor[] cons = c.getDeclaredConstructors();
        // c.遍历这些构造器
        for (Constructor con : cons) {
            System.out.println(con.getName()+&quot;-&amp;gt;&quot;+con.getParameterCount());
        }
    }
    @Test
    public void getDeclaredConstructor() throws Exception {
        // a.反射第一步先得到Class类对象
        Class c = Student.class ;
        // b.定位某个构造器，根据参数匹配，只要申明了就可以获取
        //Constructor con = c.getDeclaredConstructor(); // 可以拿到！定位无参数构造器！
        Constructor con = c.getDeclaredConstructor(String.class, int.class); //有参数的！!
        // c.构造器名称和参数
        System.out.println(con.getName()+&quot;-&amp;gt;&quot;+con.getParameterCount());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private String name ;
    private int age ;
    private Student(){
        System.out.println(&quot;无参数构造器被执行~~~~&quot;);
    }
    public Student(String name, int age) {
        System.out.println(&quot;有参数构造器被执行~~~~&quot;);
        this.name = name;
        this.age = age;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试方法
public class TestStudent02 {
    // 1.调用无参数构造器得到一个类的对象返回。
    @Test
    public void createObj01() throws Exception {
        // a.反射第一步是先得到Class类对象
        Class c = Student.class ;
        // b.定位无参数构造器对象
        Constructor constructor = c.getDeclaredConstructor();
        // c.暴力打开私有构造器的访问权限
        constructor.setAccessible(true);
        // d.通过无参数构造器初始化对象返回
        Student swk = (Student) constructor.newInstance(); // 最终还是调用无参数构造器的！
        System.out.println(swk);//Student{name=&apos;null&apos;, age=0}
    }

    // 2.调用有参数构造器得到一个类的对象返回。
    @Test
    public void createObj02() throws Exception {
        // a.反射第一步是先得到Class类对象
        Class c = Student.class ;
        // b.定位有参数构造器对象
        Constructor constructor = c.getDeclaredConstructor(String.class , int.class);
        // c.通过无参数构造器初始化对象返回
        Student swk = (Student) constructor.newInstance(&quot;孙悟空&quot;,500); // 最终还是调用有参数构造器的！
        System.out.println(swk);//Student{name=&apos;孙悟空&apos;, age=500}
    }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取变量&lt;/h4&gt;
&lt;p&gt;获取 Field 成员变量 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Field getField(String name)：根据成员变量名获得对应 Field 对象，只能获得 public 修饰&lt;/li&gt;
&lt;li&gt;Field getDeclaredField(String name)：根据成员变量名获得对应 Field 对象，所有申明的变量&lt;/li&gt;
&lt;li&gt;Field[] getFields()：获得所有的成员变量对应的 Field 对象，只能获得 public 的&lt;/li&gt;
&lt;li&gt;Field[] getDeclaredFields()：获得所有的成员变量对应的 Field 对象，只要申明了就可以得到&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Field 的方法：给成员变量赋值和取值&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void set(Object obj, Object value)&lt;/td&gt;
&lt;td&gt;给对象注入某个成员变量数据，&lt;strong&gt;obj 是对象&lt;/strong&gt;，value 是值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object get(Object obj)&lt;/td&gt;
&lt;td&gt;获取指定对象的成员变量的值，&lt;strong&gt;obj 是对象&lt;/strong&gt;，没有对象为 null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setAccessible(true)&lt;/td&gt;
&lt;td&gt;暴力反射，设置为可以直接访问私有类型的属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class getType()&lt;/td&gt;
&lt;td&gt;获取属性的类型，返回 Class 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String getName()&lt;/td&gt;
&lt;td&gt;获取属性的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class FieldDemo {
    //获取全部成员变量
    @Test
    public void getDeclaredFields(){
        // a.先获取class类对象
        Class c = Dog.class;
        // b.获取全部申明的成员变量对象
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName()+&quot;-&amp;gt;&quot;+field.getType());
        }
    }
    //获取某个成员变量
    @Test
    public void getDeclaredField() throws Exception {
        // a.先获取class类对象
        Class c = Dog.class;
        // b.定位某个成员变量对象 :根据名称定位！！
        Field ageF = c.getDeclaredField(&quot;age&quot;);
        System.out.println(ageF.getName()+&quot;-&amp;gt;&quot;+ageF.getType());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Dog {
    private String name;
    private int age ;
    private String color ;
    public static String school;
    public static final String SCHOOL_1 = &quot;宠物学校&quot;;

    public Dog() {
    }

    public Dog(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试方法
public class FieldDemo02 {
    @Test
    public void setField() throws Exception {
        // a.反射的第一步获取Class类对象
        Class c = Dog.class ;
        // b.定位name成员变量
        Field name = c.getDeclaredField(&quot;name&quot;);
        // c.为这个成员变量赋值！
        Dog d = new Dog();
        name.setAccessible(true);
        name.set(d,&quot;泰迪&quot;);
        System.out.println(d);//Dog{name=&apos;泰迪&apos;, age=0, color=&apos;null&apos;}
        // d.获取成员变量的值
        String value = name.get(d)+&quot;&quot;;
        System.out.println(value);//泰迪
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取方法&lt;/h4&gt;
&lt;p&gt;获取 Method 方法 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Method getMethod(String name,Class...args)：根据方法名和参数类型获得方法对象，public 修饰&lt;/li&gt;
&lt;li&gt;Method getDeclaredMethod(String name,Class...args)：根据方法名和参数类型获得方法对象，包括 private&lt;/li&gt;
&lt;li&gt;Method[] getMethods()：获得类中的所有成员方法对象返回数组，只能获得 public 修饰且包含父类的&lt;/li&gt;
&lt;li&gt;Method[] getDeclaredMethods()：获得类中的所有成员方法对象，返回数组，只获得本类申明的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Method 常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public Object invoke(Object obj, Object... args)：使用指定的参数调用由此方法对象，obj 对象名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MethodDemo{
    //获得类中的所有成员方法对象
    @Test
    public void getDeclaredMethods(){
        // a.先获取class类对象
        Class c = Dog.class ;
        // b.获取全部申明的方法!
        Method[] methods = c.getDeclaredMethods();
        // c.遍历这些方法
        for (Method method : methods) {
            System.out.println(method.getName()+&quot;-&amp;gt;&quot;
                    + method.getParameterCount()+&quot;-&amp;gt;&quot; + method.getReturnType());
        }
    }
    @Test
    public void getDeclardMethod() throws Exception {
        Class c = Dog.class;
        Method run = c.getDeclaredMethod(&quot;run&quot;);
        // c.触发方法执行!
        Dog d = new Dog();
        Object o = run.invoke(d);
        System.out.println(o);// 如果方法没有返回值，结果是null
        
		//参数一：方法名称   参数二：方法的参数个数和类型(可变参数！)
        Method eat = c.getDeclaredMethod(&quot;eat&quot;,String.class);
        eat.setAccessible(true); // 暴力反射！
        
       	//参数一：被触发方法所在的对象  参数二：方法需要的入参值
        Object o1 = eat.invoke(d,&quot;肉&quot;);
        System.out.println(o1);// 如果方法没有返回值，结果是null
    }
}

public class Dog {
    private String name ;
    public Dog(){
    }
    public void run(){System.out.println(&quot;狗跑的贼快~~&quot;);}
	private void eat(){System.out.println(&quot;狗吃骨头&quot;);}
	private void eat(String name){System.out.println(&quot;狗吃&quot;+name);}
	public static void inAddr(){System.out.println(&quot;在吉山区有一只单身狗！&quot;);}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;暴力攻击&lt;/h3&gt;
&lt;p&gt;泛型只能工作在编译阶段，运行阶段泛型就消失了，反射工作在运行时阶段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;反射可以破坏面向对象的封装性（暴力反射）&lt;/li&gt;
&lt;li&gt;同时可以破坏泛型的约束性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        List&amp;lt;Double&amp;gt; scores = new ArrayList&amp;lt;&amp;gt;();
        scores.add(99.3);
        scores.add(199.3);
        scores.add(89.5);
        // 拓展：通过反射暴力的注入一个其他类型的数据进去。
        // a.先得到集合对象的Class文件对象
        Class c = scores.getClass();
        // b.从ArrayList的Class对象中定位add方法
        Method add = c.getDeclaredMethod(&quot;add&quot;, Object.class);
        // c.触发scores集合对象中的add执行（运行阶段，泛型不能约束了）
        add.invoke(scores, &quot;字符串&quot;);
        System.out.println(scores);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;注解&lt;/h2&gt;
&lt;h3&gt;概念&lt;/h3&gt;
&lt;p&gt;注解：类的组成部分，可以给类携带一些额外的信息，提供一种安全的类似注释标记的机制，用来将任何信息或元数据（metadata）与程序元素（类、方法、成员变量等）进行关联&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注解是给编译器或 JVM 看的，编译器或 JVM 可以根据注解来完成对应的功能&lt;/li&gt;
&lt;li&gt;注解类似修饰符，应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;父类中的注解是不能被子类继承的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注解作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标记&lt;/li&gt;
&lt;li&gt;框架技术多半都是在使用注解和反射，都是属于框架的底层基础技术&lt;/li&gt;
&lt;li&gt;在编译时进行格式检查，比如方法重写约束 @Override、函数式接口约束 @FunctionalInterface.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解格式&lt;/h3&gt;
&lt;p&gt;定义格式：自定义注解用 @interface 关键字，注解默认可以标记很多地方&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修饰符 @interface 注解名{
     // 注解属性
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用注解的格式：@注解名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Book
@MyTest
public class MyBook {
    //方法变量都可以注解
}

@interface Book{
}
@interface MyTest{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;注解属性&lt;/h3&gt;
&lt;h4&gt;普通属性&lt;/h4&gt;
&lt;p&gt;注解可以有属性，&lt;strong&gt;属性名必须带 ()&lt;/strong&gt;，在用注解的时候，属性必须赋值，除非属性有默认值&lt;/p&gt;
&lt;p&gt;属性的格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式 1：数据类型 属性名()&lt;/li&gt;
&lt;li&gt;格式 2：数据类型 属性名() default 默认值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;属性适用的数据类型:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;八种数据数据类型（int，short，long，double，byte，char，boolean，float）和 String、Class&lt;/li&gt;
&lt;li&gt;以上类型的数组形式都支持&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@MyBook(name=&quot;《精通Java基础》&quot;,authors = {&quot;播仔&quot;,&quot;Dlei&quot;,&quot;播妞&quot;} , price = 99.9 )
public class AnnotationDemo01 {
    @MyBook(name=&quot;《精通MySQL数据库入门到删库跑路》&quot;,authors = {&quot;小白&quot;,&quot;小黑&quot;} ,
     					price = 19.9 , address = &quot;北京&quot;)
    public static void main(String[] args) {
    }
}
// 自定义一个注解
@interface MyBook{
    String name();
    String[] authors(); // 数组
    double price();
    String address() default &quot;武汉&quot;;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;特殊属性&lt;/h4&gt;
&lt;p&gt;注解的特殊属性名称：value&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果只有一个 value 属性的情况下，使用 value 属性的时候可以省略 value 名称不写&lt;/li&gt;
&lt;li&gt;如果有多个属性，且多个属性没有默认值，那么 value 是不能省略的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//@Book(&quot;/deleteBook.action&quot;)
@Book(value = &quot;/deleteBook.action&quot; , age = 12)
public class AnnotationDemo01{
}

@interface Book{
    String value();
    int age() default 10;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;元注解&lt;/h3&gt;
&lt;p&gt;元注解是 sun 公司提供的，用来注解自定义注解&lt;/p&gt;
&lt;p&gt;元注解有四个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@Target：约束自定义注解可以标记的范围，默认值为任何元素，表示该注解用于什么地方，可用值定义在 ElementType 类中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ElementType.CONSTRUCTOR&lt;/code&gt;：用于描述构造器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.FIELD&lt;/code&gt;：成员变量、对象、属性（包括 enum 实例）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.LOCAL_VARIABLE&lt;/code&gt;：用于描述局部变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.METHOD&lt;/code&gt;：用于描述方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.PACKAGE&lt;/code&gt;：用于描述包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.PARAMETER&lt;/code&gt;：用于描述参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementType.TYPE&lt;/code&gt;：用于描述类、接口（包括注解类型）或 enum 声明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Retention：定义该注解的生命周期，申明注解的作用范围：编译时，运行时，可使用的值定义在 RetentionPolicy 枚举类中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RetentionPolicy.SOURCE&lt;/code&gt;：在编译阶段丢弃，这些注解在编译结束之后就不再有任何意义，只作用在源码阶段，生成的字节码文件中不存在，&lt;code&gt;@Override&lt;/code&gt;、&lt;code&gt;@SuppressWarnings&lt;/code&gt; 都属于这类注解&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RetentionPolicy.CLASS&lt;/code&gt;：在类加载时丢弃，在字节码文件的处理中有用，运行阶段不存在，默认值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RetentionPolicy.RUNTIME&lt;/code&gt; : 始终不会丢弃，运行期也保留该注解，因此可以使用反射机制读取该注解的信息，自定义的注解通常使用这种方式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Inherited：表示修饰的自定义注解可以被子类继承&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Documented：表示是否将自定义的注解信息添加在 Java 文档中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class AnnotationDemo01{
    // @MyTest // 只能注解方法
    private String name;

    @MyTest
    public static void main( String[] args) {
    }
}
@Target(ElementType.METHOD) // 申明只能注解方法
@Retention(RetentionPolicy.RUNTIME) // 申明注解从写代码一直到运行还在，永远存活！！
@interface MyTest{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;注解解析&lt;/h3&gt;
&lt;p&gt;开发中经常要知道一个类的成分上面到底有哪些注解，注解有哪些属性数据，这都需要进行注解的解析&lt;/p&gt;
&lt;p&gt;注解解析相关的接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Annotation：注解类型，该类是所有注解的父类，注解都是一个 Annotation 的对象&lt;/li&gt;
&lt;li&gt;AnnotatedElement：该接口定义了与注解解析相关的方法&lt;/li&gt;
&lt;li&gt;Class、Method、Field、Constructor 类成分：实现 AnnotatedElement 接口，拥有解析注解的能力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Class 类 API ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Annotation[] getDeclaredAnnotations()&lt;/code&gt;：获得当前对象上使用的所有注解，返回注解数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T getDeclaredAnnotation(Class&amp;lt;T&amp;gt; annotationClass)&lt;/code&gt;：根据注解类型获得对应注解对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T getAnnotation(Class&amp;lt;T&amp;gt; annotationClass)&lt;/code&gt;：根据注解类型获得对应注解对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isAnnotationPresent(Class&amp;lt;Annotation&amp;gt; class)&lt;/code&gt;：判断对象是否使用了指定的注解&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isAnnotation()&lt;/code&gt;：此 Class 对象是否表示注释类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注解原理：注解本质是&lt;strong&gt;特殊接口&lt;/strong&gt;，继承了 &lt;code&gt;Annotation&lt;/code&gt; ，其具体实现类是 Java 运行时生成的&lt;strong&gt;动态代理类&lt;/strong&gt;，通过反射获取注解时，返回的是运行时生成的动态代理对象 &lt;code&gt;$Proxy1&lt;/code&gt;，通过代理对象调用自定义注解（接口）的方法，回调 &lt;code&gt;AnnotationInvocationHandler&lt;/code&gt; 的 &lt;code&gt;invoke&lt;/code&gt; 方法，该方法会从 &lt;code&gt;memberValues&lt;/code&gt;  这个 Map 中找出对应的值，而 &lt;code&gt;memberValues&lt;/code&gt; 的来源是 Java 常量池&lt;/p&gt;
&lt;p&gt;解析注解数据的原理：注解在哪个成分上，就先拿哪个成分对象，比如注解作用在类上，则要该类的 Class 对象，再来拿上面的注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AnnotationDemo{
    @Test
    public void parseClass() {
        // 1.定位Class类对象
        Class c = BookStore.class;
        // 2.判断这个类上是否使用了某个注解
        if(c.isAnnotationPresent(Book.class)){
            // 3.获取这个注解对象
            Book b = (Book)c.getDeclarAnnotation(Book.class);
            System.out.println(book.value());
            System.out.println(book.price());
            System.out.println(Arrays.toString(book.authors()));
        }
    }
    @Test
    public void parseMethod() throws Exception {
        Class c = BookStore.class;
        Method run = c.getDeclaredMethod(&quot;run&quot;);
        if(run.isAnnotationPresent(Book.class)){
            Book b = (Book)run.getDeclaredAnnotation(Book.class);
           	sout(上面的三个);
        }
    }
}

@Book(value = &quot;《Java基础到精通》&quot;, price = 99.5, authors = {&quot;张三&quot;,&quot;李四&quot;})
class BookStore{
    @Book(value = &quot;《Mybatis持久层框架》&quot;, price = 199.5, authors = {&quot;王五&quot;,&quot;小六&quot;})
    public void run(){
    }
}
@Target({ElementType.TYPE,ElementType.METHOD}) // 类和成员方法上使用
@Retention(RetentionPolicy.RUNTIME) // 注解永久存活
@interface Book{
    String value();
    double price() default 100;
    String[] authors();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;XML&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;XML介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XML 指可扩展标记语言（EXtensible Markup Language）&lt;/li&gt;
&lt;li&gt;XML 是一种&lt;strong&gt;标记语言&lt;/strong&gt;，很类似 HTML，HTML文件也是XML文档&lt;/li&gt;
&lt;li&gt;XML 的设计宗旨是&lt;strong&gt;传输数据&lt;/strong&gt;，而非显示数据&lt;/li&gt;
&lt;li&gt;XML 标签没有被预定义，需要自行定义标签&lt;/li&gt;
&lt;li&gt;XML 被设计为具有自我描述性，易于阅读&lt;/li&gt;
&lt;li&gt;XML 是 W3C 的推荐标准&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;XML 与 HTML 的区别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XML 不是 HTML 的替代，XML 和 HTML 为不同的目的而设计&lt;/li&gt;
&lt;li&gt;XML 被设计为传输和存储数据，其焦点是数据的内容；XMl标签可自定义，便于阅读&lt;/li&gt;
&lt;li&gt;HTML 被设计用来显示数据，其焦点是数据的外观；HTML标签被预设好，便于浏览器识别&lt;/li&gt;
&lt;li&gt;HTML 旨在显示信息，而 XML 旨在传输信息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;创建&lt;/h3&gt;
&lt;p&gt;person.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;person id=&quot;110&quot;&amp;gt;
	&amp;lt;age&amp;gt;18&amp;lt;/age&amp;gt;		&amp;lt;!--年龄--&amp;gt;
	&amp;lt;name&amp;gt;张三&amp;lt;/name&amp;gt;	  &amp;lt;!--姓名--&amp;gt;
	&amp;lt;sex/&amp;gt;				&amp;lt;!--性别--&amp;gt;
&amp;lt;/person&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;组成&lt;/h3&gt;
&lt;p&gt;XML 文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为 xml&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文档声明&lt;/strong&gt;
&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=&quot;yes&quot; ?&amp;gt;&lt;/code&gt;，文档声明必须在第一行，以 &lt;code&gt;&amp;lt;?xml&lt;/code&gt; 开头，以 &lt;code&gt;?&amp;gt;&lt;/code&gt; 结束，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version：指定 XML 文档版本。必须属性，这里一般选择 1.0&lt;/li&gt;
&lt;li&gt;enconding：指定当前文档的编码，可选属性，默认值是 utf-8&lt;/li&gt;
&lt;li&gt;standalone：该属性不是必须的，描述 XML 文件是否依赖其他的 xml 文件，取值为 yes/no&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;元素&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式 1：&lt;code&gt;&amp;lt;person&amp;gt;&amp;lt;/person&amp;gt; &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;格式 2：&lt;code&gt;&amp;lt;person/&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;普通元素的结构由开始标签、元素体、结束标签组成&lt;/li&gt;
&lt;li&gt;标签由一对尖括号和合法标识符组成，标签必须成对出现。特殊的标签可以不成对，必须有结束标记 &amp;lt;/&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元素体：可以是元素，也可以是文本，例如：&lt;code&gt;&amp;lt;person&amp;gt;&amp;lt;name&amp;gt;张三&amp;lt;/name&amp;gt;&amp;lt;/person&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空元素：空元素只有标签，而没有结束标签，但&lt;strong&gt;元素必须自己闭合&lt;/strong&gt;，例如：&lt;code&gt;&amp;lt;sex/&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;元素命名：区分大小写、不能使用空格冒号、不建议用 XML、xml、Xml 等开头&lt;/li&gt;
&lt;li&gt;必须存在一个根标签，有且只能有一个&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;属性&lt;/strong&gt;：&lt;code&gt;&amp;lt;name id=&quot;1&quot; desc=&quot;高富帅&quot;&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性是元素的一部分，它必须出现在元素的开始标签中&lt;/li&gt;
&lt;li&gt;属性的定义格式：&lt;code&gt;属性名=“属性值”&lt;/code&gt;，其中属性值必须使用单引或双引号括起来&lt;/li&gt;
&lt;li&gt;一个元素可以有 0~N 个属性，但一个元素中不能出现同名属性&lt;/li&gt;
&lt;li&gt;属性名不能使用空格 , 不要使用冒号等特殊字符，且必须以字母开头&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注释&lt;/strong&gt;：&amp;lt;!--注释内容--&amp;gt;
XML的注释与HTML相同，既以 &lt;code&gt;&amp;lt;!--&lt;/code&gt; 开始，&lt;code&gt;--&amp;gt;&lt;/code&gt; 结束。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;转义字符&lt;/strong&gt;
XML 中的转义字符与 HTML 一样。因为很多符号已经被文档结构所使用，所以在元素体或属性值中想使用这些符号就必须使用转义字符（也叫实体字符），例如：&quot;&amp;gt;&quot;、&quot;&amp;lt;&quot;、&quot;&apos;&quot;、&quot;&quot;&quot;、&quot;&amp;amp;&quot;
XML 中仅有字符 &amp;lt; 和 &amp;amp; 是非法的。省略号、引号和大于号是合法的，把它们替换为实体引用&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字符&lt;/th&gt;
&lt;th&gt;预定义的转义字符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt; &amp;amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt; &amp;amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;双引号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&apos;&lt;/td&gt;
&lt;td&gt;&lt;code&gt; &amp;amp;apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单引号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&lt;/td&gt;
&lt;td&gt;&lt;code&gt; &amp;amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;和号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;字符区&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;![CDATA[
	文本数据
]]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;CDATA 指的是不应由 XML 解析器进行解析的文本数据（Unparsed Character Data）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CDATA 部分由 &quot;&amp;lt;![CDATA[&quot; 开始，由 &quot;]]&amp;gt;&quot; 结束；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;大量的转义字符在xml文档中时，会使XML文档的可读性大幅度降低。这时使用CDATA段就会好一些&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CDATA 部分不能包含字符串 ]]&amp;gt;，也不允许嵌套的 CDATA 部分&lt;/li&gt;
&lt;li&gt;标记 CDATA 部分结尾的 ]]&amp;gt; 不能包含空格或折行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;?xml-stylesheet type=&quot;text/css&quot; href=&quot;../css/xml.css&quot; ?&amp;gt;
&amp;lt;!-- 7.处理指令：导入外部的css样式控制xml的界面效果，没有啥用，xml不是为了展示好看的！--&amp;gt;
&amp;lt;!-- 1.申明 抬头 必须在第一行--&amp;gt;
&amp;lt;!-- 2.注释，本处就是注释，必须用前后尖括号围起来 --&amp;gt;
&amp;lt;!-- 3.标签（元素），注意一个XML文件只能有一个根标签--&amp;gt;
&amp;lt;student&amp;gt;
    &amp;lt;!-- 4.属性信息：id , desc--&amp;gt;
    &amp;lt;name id=&quot;1&quot; desc=&quot;高富帅&quot;&amp;gt;西门庆&amp;lt;/name&amp;gt;
    &amp;lt;age&amp;gt;32&amp;lt;/age&amp;gt;
    &amp;lt;!-- 5.实体字符：在xml文件中，我们不能直接写小于号，等一些特殊字符
        会与xml文件本身的内容冲突报错，此时必须用转义的实体字符。
    --&amp;gt;
    &amp;lt;sql&amp;gt;
       &amp;lt;!-- select * from student where age &amp;lt; 18 &amp;amp;&amp;amp; age &amp;gt; 10; --&amp;gt;
        select * from student where age &amp;amp;lt; 18 &amp;amp;amp;&amp;amp;amp; age &amp;amp;gt; 10;
    &amp;lt;/sql&amp;gt;
    &amp;lt;!-- 6.字符数据区：在xml文件中，我们不能直接写小于号，等一些特殊字符
        会与xml文件本身的内容冲突报错，此时必须用转义的实体字符
        或者也可以选择使用字符数据区，里面的内容可以随便了！
        --&amp;gt;
    &amp;lt;sql2&amp;gt;
        &amp;lt;![CDATA[
             select * from student where age &amp;lt; 18 &amp;amp;&amp;amp; age &amp;gt; 10;
        ]]&amp;gt;
    &amp;lt;/sql2&amp;gt;
&amp;lt;/student&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;约束&lt;/h3&gt;
&lt;h4&gt;DTD&lt;/h4&gt;
&lt;p&gt;DTD 是文档类型定义（Document Type Definition）。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。&lt;/p&gt;
&lt;p&gt;DTD 规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;约束元素的嵌套层级&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!ELEMENT 父标签 （子标签1，子标签2，…）&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;约束元素体里面的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!ELEMENT 标签名字 标签类型&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断元素
简单元素：没有子元素。
复杂元素：有子元素的元素；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标签类型&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签类型&lt;/th&gt;
&lt;th&gt;代码写法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PCDATA&lt;/td&gt;
&lt;td&gt;(#PCDATA)&lt;/td&gt;
&lt;td&gt;被解释的字符串数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMPTY&lt;/td&gt;
&lt;td&gt;EMPTY&lt;/td&gt;
&lt;td&gt;即空元素，例如&amp;lt;hr/&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ANY&lt;/td&gt;
&lt;td&gt;ANY&lt;/td&gt;
&lt;td&gt;即任意类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!ELEMENT persons (person+)&amp;gt;   	&amp;lt;!--约束人们至少一个人--&amp;gt;
&amp;lt;!ELEMENT person (name,age)&amp;gt;	&amp;lt;!--约束元素人的子元素必须为姓名、年龄，并且按顺序--&amp;gt;
&amp;lt;!ELEMENT name (#PCDATA)&amp;gt;		&amp;lt;!--&quot;姓名&quot;元素体为字符串数据--&amp;gt;
&amp;lt;!ELEMENT age ANY&amp;gt;       		&amp;lt;!--&quot;年龄&quot;元素体为任意类型--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
* 数量词

| 数量词符号 | 含义                         |
| ---------- | ---------------------------- |
| 空         | 表示元素出现一次             |
| *          | 表示元素可以出现0到多个      |
| +          | 表示元素可以出现至少1个      |
| ?          | 表示元素可以是0或1个         |
| ,          | 表示元素需要按照顺序显示     |
| \|         | 表示元素需要选择其中的某一个 |



&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性声明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!ATTLIST 标签名称 
		属性名称1 属性类型1 属性说明1
		属性名称2 属性类型2 属性说明2
		…
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性类型&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性类型&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CDATA&lt;/td&gt;
&lt;td&gt;代表属性是文本字符串， eg:&amp;lt;!ATTLIST 属性名 CDATA 属性说明&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;代码该属性值唯一，不能以数字开头， eg:&amp;lt;!ATTLIST 属性名 ID 属性说明&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENUMERATED&lt;/td&gt;
&lt;td&gt;代表属性值在指定范围内进行枚举 Eg:&amp;lt;!ATTLIST属性名 (社科类|工程类|教育类) &quot;社科类&quot;&amp;gt; &quot;社科类&quot;是默认值，属性如果不设置默认值就是&quot;社科类&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性说明&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性说明&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#REQUIRED&lt;/td&gt;
&lt;td&gt;代表属性是必须有的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#IMPLIED&lt;/td&gt;
&lt;td&gt;代表属性可有可无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#FIXED&lt;/td&gt;
&lt;td&gt;代表属性为固定值，实现方式：book_info CDATA #FIXED &quot;固定值&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!ATTLIST 书					   &amp;lt;!--设置&quot;书&quot;元素的的属性列表--&amp;gt;
	id ID #REQUIRED				&amp;lt;!--&quot;id&quot;属性值为必须有--&amp;gt;
	编号 CDATA #IMPLIED		   &amp;lt;!--&quot;编号&quot;属性可有可无--&amp;gt;
	出版社 (清华|北大) &quot;清华&quot; 	  &amp;lt;!--&quot;出版社&quot;属性值是枚举值，默认为“123”--&amp;gt;
	type CDATA #FIXED &quot;IT&quot;		&amp;lt;!--&quot;type&quot;属性为文本字符串并且固定值为&quot;IT&quot;--&amp;gt;
&amp;gt;
&amp;lt;!ATTLIST person id CDATA #REQUIRED&amp;gt;  &amp;lt;!--id是文本字符串必须有--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Schema&lt;/h4&gt;
&lt;p&gt;XSD 定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Schema 语言也可作为 XSD（XML Schema Definition）&lt;/li&gt;
&lt;li&gt;Schema 约束文件本身也是一个 XML 文件，符合 XML 的语法，这个文件的后缀名 .xsd&lt;/li&gt;
&lt;li&gt;一个 XML 中可以引用多个 Schema 约束文件，多个 Schema 使用名称空间区分（名称空间类似于 Java 包名）&lt;/li&gt;
&lt;li&gt;dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型，但是在 Schema 里面可以支持很多个数据类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema 文件约束 XML 文件的同时也被别的文件约束着&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;XSD 规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个文件，这个文件的后缀名为 .xsd&lt;/li&gt;
&lt;li&gt;定义文档声明&lt;/li&gt;
&lt;li&gt;schema 文件的根标签为： &amp;lt;schema&amp;gt;&lt;/li&gt;
&lt;li&gt;在 &amp;lt;schema&amp;gt; 中定义属性：
&lt;ul&gt;
&lt;li&gt;xmlns=http://www.w3.org/2001/XMLSchema&lt;/li&gt;
&lt;li&gt;代表当前文件时约束别人的，同时这个文件也对该 Schema 进行约束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在&amp;lt;schema&amp;gt;中定义属性 ：
&lt;ul&gt;
&lt;li&gt;targetNamespace = 唯一的 url 地址，指定当前这个 schema 文件的名称空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;名称空间&lt;/strong&gt;：当其他 xml 使用该 schema 文件，需要引入此空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在&amp;lt;schema&amp;gt;中定义属性 ：
&lt;ul&gt;
&lt;li&gt;elementFormDefault=&quot;qualified“，表示当前 schema 文件是一个质量良好的文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过 element 定义元素&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;判断当前元素是简单元素还是复杂元素&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;person.xsd&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;schema
    xmlns=&quot;http://www.w3.org/2001/XMLSchema&quot;     &amp;lt;!--本文件是约束别人的，也被约束--&amp;gt;
    targetNamespace=&quot;http://www.seazean.cn/javase&quot;&amp;lt;!--自己的名称空间--&amp;gt;
    elementFormDefault=&quot;qualified&quot;				  &amp;lt;!--本文件是质量好的--&amp;gt;
&amp;gt;

    &amp;lt;element name=&quot;persons&quot;&amp;gt;    		  &amp;lt;!--定义persons复杂元素--&amp;gt;
        &amp;lt;complexType&amp;gt;           		  &amp;lt;!--复杂的元素--&amp;gt;
            &amp;lt;sequence&amp;gt;					  &amp;lt;!--里面的元素必须按照顺序定义--&amp;gt;
                &amp;lt;element name = &quot;person&quot;&amp;gt; &amp;lt;!--定义person复杂元素--&amp;gt;
                    &amp;lt;complexType&amp;gt;
                        &amp;lt;sequence&amp;gt;
                            &amp;lt;!--定义name和age简单元素--&amp;gt;
                            &amp;lt;element name = &quot;name&quot; type = &quot;string&quot;&amp;gt;&amp;lt;/element&amp;gt;
                            &amp;lt;element name = &quot;age&quot; type = &quot;string&quot;&amp;gt;&amp;lt;/element&amp;gt;
                        &amp;lt;/sequence&amp;gt;
                    &amp;lt;/complexType&amp;gt;
                &amp;lt;/element&amp;gt;
            &amp;lt;/sequence&amp;gt;
        &amp;lt;/complexType&amp;gt;
    &amp;lt;/element&amp;gt;
    
&amp;lt;/schema&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Dom4J&lt;/h3&gt;
&lt;h4&gt;解析&lt;/h4&gt;
&lt;p&gt;XML 解析就是从 XML 中获取到数据，DOM 是解析思想&lt;/p&gt;
&lt;p&gt;DOM（Document Object Model）：文档对象模型，把文档的各个组成部分看做成对应的对象，把 XML 文件全部加载到内存，在内存中形成一个树形结构，再获取对应的值&lt;/p&gt;
&lt;p&gt;Dom4J 实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Dom4J 解析器构造方法：&lt;code&gt;SAXReader saxReader = new SAXReader()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SAXReader 常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Document read(File file)&lt;/code&gt;：Reads a Document from the given File&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Document read(InputStream in)&lt;/code&gt;：Reads a Document from the given stream using SAX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java Class 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public InputStream getResourceAsStream(String path)&lt;/code&gt;：加载文件成为一个字节输入流返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;根元素&lt;/h4&gt;
&lt;p&gt;Document 方法：&lt;code&gt;Element getRootElement()&lt;/code&gt; 获取根元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 需求：解析books.xml文件成为一个Document文档树对象，得到根元素对象。
public class Dom4JDemo {
    public static void main(String[] args) throws Exception {
        // 1.创建一个dom4j的解析器对象：代表整个dom4j框架。
        SAXReader saxReader = new SAXReader();
        // 2.第一种方式（简单）：通过解析器对象去加载xml文件数据，成为一个Document文档树对象。
        //Document document = saxReader.read(new File(&quot;Day13Demo/src/books.xml&quot;));
        
        // 3.第二种方式（代码多点）先把xml文件读成一个字节输入流
        // 这里的“/”是直接去src类路径下寻找文件。
        InputStream is = Dom4JDemo01.class.getResourceAsStream(&quot;/books.xml&quot;);
        Document document = saxReader.read(is);
        System.out.println(document);
		//org.dom4j.tree.DefaultDocument@27a5f880 [Document: name null]
		// 4.从document文档树对象中提取根元素对象
        Element root = document.getRootElement();
        System.out.println(root.getName());//books
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;books&amp;gt;
    &amp;lt;book id=&quot;0001&quot; desc=&quot;第一本书&quot;&amp;gt;
        &amp;lt;name&amp;gt;  JavaWeb开发教程&amp;lt;/name&amp;gt;
        &amp;lt;author&amp;gt;    张三&amp;lt;/author&amp;gt;
        &amp;lt;sale&amp;gt;100.00元   &amp;lt;/sale&amp;gt;
    &amp;lt;/book&amp;gt;
    &amp;lt;book id=&quot;0002&quot;&amp;gt;
        &amp;lt;name&amp;gt;三国演义&amp;lt;/name&amp;gt;
        &amp;lt;author&amp;gt;罗贯中&amp;lt;/author&amp;gt;
        &amp;lt;sale&amp;gt;100.00元&amp;lt;/sale&amp;gt;
    &amp;lt;/book&amp;gt;
    &amp;lt;user&amp;gt;
    &amp;lt;/user&amp;gt;
&amp;lt;/books&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;子元素&lt;/h4&gt;
&lt;p&gt;Element 元素的 API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String getName()：取元素的名称。&lt;/li&gt;
&lt;li&gt;List&amp;lt;Element&amp;gt; elements()：获取当前元素下的全部子元素（一级）&lt;/li&gt;
&lt;li&gt;List&amp;lt;Element&amp;gt; elements(String name)：获取当前元素下的指定名称的全部子元素（一级）&lt;/li&gt;
&lt;li&gt;Element element(String name)：获取当前元素下的指定名称的某个子元素，默认取第一个（一级）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Dom4JDemo {
    public static void main(String[] args) throws Exception {
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(new File(&quot;Day13Demo/src/books.xml&quot;));
        // 3.获取根元素对象
        Element root = document.getRootElement();
        System.out.println(root.getName());

        // 4.获取根元素下的全部子元素
        List&amp;lt;Element&amp;gt; sonElements = root.elements();
        for (Element sonElement : sonElements) {
            System.out.println(sonElement.getName());
        }
        // 5.获取根源下的全部book子元素
        List&amp;lt;Element&amp;gt; sonElements1 = root.elements(&quot;book&quot;);
        for (Element sonElement : sonElements1) {
            System.out.println(sonElement.getName());
        }
        
        // 6.获取根源下的指定的某个元素
        Element son = root.element(&quot;user&quot;);
        System.out.println(son.getName());
        // 默认会提取第一个名称一样的子元素对象返回！
        Element son1 = root.element(&quot;book&quot;);
        System.out.println(son1.attributeValue(&quot;id&quot;));
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;属性&lt;/h4&gt;
&lt;p&gt;Element 元素的 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List&amp;lt;Attribute&amp;gt; attributes()：获取元素的全部属性对象&lt;/li&gt;
&lt;li&gt;Attribute attribute(String name)：根据名称获取某个元素的属性对象&lt;/li&gt;
&lt;li&gt;String attributeValue(String var)：直接获取某个元素的某个属性名称的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Attribute 对象的 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String getName()：获取属性名称&lt;/li&gt;
&lt;li&gt;String getValue()：获取属性值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Dom4JDemo {
    public static void main(String[] args) throws Exception {
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(new File(&quot;Day13Demo/src/books.xml&quot;));
        Element root = document.getRootElement();
        // 4.获取book子元素
        Element bookEle = root.element(&quot;book&quot;);

        // 5.获取book元素的全部属性对象
        List&amp;lt;Attribute&amp;gt; attributes = bookEle.attributes();
        for (Attribute attribute : attributes) {
            System.out.println(attribute.getName()+&quot;-&amp;gt;&quot;+attribute.getValue());
        }

        // 6.获取Book元素的某个属性对象
        Attribute descAttr = bookEle.attribute(&quot;desc&quot;);
        System.out.println(descAttr.getName()+&quot;-&amp;gt;&quot;+descAttr.getValue());

        // 7.可以直接获取元素的属性值
        System.out.println(bookEle.attributeValue(&quot;id&quot;));
        System.out.println(bookEle.attributeValue(&quot;desc&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;文本&lt;/h4&gt;
&lt;p&gt;Element：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String elementText(String name)：可以直接获取当前元素的子元素的文本内容&lt;/li&gt;
&lt;li&gt;String elementTextTrim(String name)：去前后空格,直接获取当前元素的子元素的文本内容&lt;/li&gt;
&lt;li&gt;String getText()：直接获取当前元素的文本内容&lt;/li&gt;
&lt;li&gt;String getTextTrim()：去前后空格,直接获取当前元素的文本内容&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Dom4JDemo {
    public static void main(String[] args) throws Exception {
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(new File(&quot;Day13Demo/src/books.xml&quot;));
        Element root = document.getRootElement();
        // 4.得到第一个子元素book
        Element bookEle = root.element(&quot;book&quot;);

        // 5.直接拿到当前book元素下的子元素文本值
        System.out.println(bookEle.elementText(&quot;name&quot;));
        System.out.println(bookEle.elementTextTrim(&quot;name&quot;)); // 去前后空格
        System.out.println(bookEle.elementText(&quot;author&quot;));
        System.out.println(bookEle.elementTextTrim(&quot;author&quot;)); // 去前后空格

        // 6.先获取到子元素对象，再获取该文本值
        Element bookNameEle = bookEle.element(&quot;name&quot;);
        System.out.println(bookNameEle.getText());
        System.out.println(bookNameEle.getTextTrim());// 去前后空格
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;XPath&lt;/h3&gt;
&lt;p&gt;Dom4J 可以用于解析整个 XML 的数据，但是如果要检索 XML 中的某些信息，建议使用 XPath&lt;/p&gt;
&lt;p&gt;XPath 常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List&amp;lt;Node&amp;gt; selectNodes(String var1) : 检索出一批节点集合&lt;/li&gt;
&lt;li&gt;Node selectSingleNode(String var1) : 检索出一个节点返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XPath 提供的四种检索数据的写法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;绝对路径：/根元素/子元素/子元素&lt;/li&gt;
&lt;li&gt;相对路径：./子元素/子元素 (.代表了当前元素)&lt;/li&gt;
&lt;li&gt;全文搜索：
&lt;ul&gt;
&lt;li&gt;//元素：在全文找这个元素&lt;/li&gt;
&lt;li&gt;//元素1/元素2：在全文找元素1下面的一级元素 2&lt;/li&gt;
&lt;li&gt;//元素1//元素2：在全文找元素1下面的全部元素 2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;属性查找：
&lt;ul&gt;
&lt;li&gt;//@属性名称：在全文检索属性对象&lt;/li&gt;
&lt;li&gt;//元素[@属性名称]：在全文检索包含该属性的元素对象&lt;/li&gt;
&lt;li&gt;//元素[@属性名称=值]：在全文检索包含该属性的元素且属性值为该值的元素对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class XPathDemo {
    public static void main(String[] args) throws Exception {
        SAXReader saxReader = new SAXReader();
        InputStream is = XPathDemo.class.getResourceAsStream(&quot;/Contact.xml&quot;);
        Document document = saxReader.read(is);
        //1.使用绝对路径定位全部的name名称
        List&amp;lt;Node&amp;gt; nameNodes1 = document.selectNodes(&quot;/contactList/contact/name&quot;);
        for (Node nameNode : nameNodes) {
            System.out.println(nameNode.getText());
        }
        
        //2.相对路径。从根元素开始检索，.代表很根元素
        List&amp;lt;Node&amp;gt; nameNodes2 = root.selectNodes(&quot;./contact/name&quot;);
        
        //3.1 在全文中检索name节点
        List&amp;lt;Node&amp;gt; nameNodes3 = root.selectNodes(&quot;//name&quot;);//全部的
        //3.2 在全文中检索所有contact下的所有name节点  //包括sql，不外面的
        List&amp;lt;Node&amp;gt; nameNodes3 = root.selectNodes(&quot;//contact//name&quot;);
        //3.3 在全文中检索所有contact下的直接name节点
        List&amp;lt;Node&amp;gt; nameNodes3 = root.selectNodes(&quot;//contact/name&quot;);//不包括sql和外面
        
        //4.1 检索全部属性对象
        List&amp;lt;Node&amp;gt; attributes1 = root.selectNodes(&quot;//@id&quot;);//包括sql4
        //4.2 在全文检索包含该属性的元素对象
        List&amp;lt;Node&amp;gt; attributes1 = root.selectNodes(&quot;//contact[@id]&quot;);
        //4.3 在全文检索包含该属性的元素且属性值为该值的元素对象
        Node nodeEle = document.selectSingleNode(&quot;//contact[@id=2]&quot;);
        Element ele = (Element)nodeEle;
        System.out.println(ele.elementTextTrim(&quot;name&quot;));//xi
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;contactList&amp;gt;
&amp;lt;contact id=&quot;1&quot;&amp;gt;
    &amp;lt;name&amp;gt;小白&amp;lt;/name&amp;gt;
    &amp;lt;gender&amp;gt;女&amp;lt;/gender&amp;gt;
    &amp;lt;email&amp;gt;bai@seazean.cn&amp;lt;/email&amp;gt;
&amp;lt;/contact&amp;gt;
&amp;lt;contact id=&quot;2&quot;&amp;gt;
    &amp;lt;name&amp;gt;小黑&amp;lt;/name&amp;gt;
    &amp;lt;gender&amp;gt;男&amp;lt;/gender&amp;gt;
    &amp;lt;email&amp;gt;hei@seazean.cn&amp;lt;/email&amp;gt;
    &amp;lt;sql id=&quot;sql4&quot;&amp;gt;
        &amp;lt;name&amp;gt;sql语句&amp;lt;/name&amp;gt;
    &amp;lt;/sql&amp;gt;
&amp;lt;/contact&amp;gt;
&amp;lt;contact id=&quot;3&quot;&amp;gt;
    &amp;lt;name&amp;gt;小虎&amp;lt;/name&amp;gt;
    &amp;lt;gender&amp;gt;男&amp;lt;/gender&amp;gt;
    &amp;lt;email&amp;gt;hu@seazean.cn&amp;lt;/email&amp;gt;
&amp;lt;/contact&amp;gt;
&amp;lt;contact&amp;gt;&amp;lt;/contact&amp;gt;
&amp;lt;name&amp;gt;外面的名称&amp;lt;/name&amp;gt;
&amp;lt;/contactList&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SDP&lt;/h2&gt;
&lt;h3&gt;单例模式&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;单例模式（Singleton Pattern）是 Java 中最简单的设计模式之一，提供了一种创建对象的最佳方式&lt;/p&gt;
&lt;p&gt;单例设计模式分类两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;饿汉式：类加载就会导致该单实例对象被创建&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;懒汉式：类加载不会导致该单实例对象被创建，而是首次使用该对象时才会创建&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;饿汉式&lt;/h4&gt;
&lt;p&gt;饿汉式在类加载的过程导致该单实例对象被创建，&lt;strong&gt;虚拟机会保证类加载的线程安全&lt;/strong&gt;，但是如果只是为了加载该类不需要实例，则会造成内存的浪费&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态变量的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton {
    // 私有构造方法
    private Singleton() {}
    // 在成员位置创建该类的对象
    private static final Singleton instance = new Singleton();
    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
    
    // 解决序列化问题
    protected Object readResolve() {
    	return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加 final 修饰，所以不会被子类继承，防止子类中不适当的行为覆盖父类的方法，破坏了单例&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;防止反序列化破坏单例的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对单例声明 transient，然后实现 readObject(ObjectInputStream in) 方法，复用原来的单例&lt;/p&gt;
&lt;p&gt;条件：访问权限为 private/protected、返回值必须是 Object、异常可以不抛&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现 readResolve() 方法，当 JVM 从内存中反序列化地组装一个新对象，就会自动调用 readResolve 方法返回原来单例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法设置为私有，防止其他类无限创建对象，但是不能防止反射破坏&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态变量初始化在类加载时完成，&lt;strong&gt;由 JVM 保证线程安全&lt;/strong&gt;，能保证单例对象创建时的安全&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供静态方法而不是直接将 INSTANCE 设置为 public，体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态代码块的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    // 私有构造方法
    private Singleton() {}
    
    // 在成员位置创建该类的对象
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    
    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;枚举方式：枚举类型是所用单例实现中&lt;strong&gt;唯一一种不会被破坏&lt;/strong&gt;的单例实现模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println(&quot;doSomething&quot;);
    }
}
public static void main(String[] args) {
    Singleton.INSTANCE.doSomething();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;问题1：枚举单例是如何限制实例个数的？每个枚举项都是一个实例，是一个静态成员变量&lt;/li&gt;
&lt;li&gt;问题2：枚举单例在创建时是否有并发问题？否&lt;/li&gt;
&lt;li&gt;问题3：枚举单例能否被反射破坏单例？否，反射创建对象时判断是枚举类型就直接抛出异常&lt;/li&gt;
&lt;li&gt;问题4：枚举单例能否被反序列化破坏单例？否&lt;/li&gt;
&lt;li&gt;问题5：枚举单例属于懒汉式还是饿汉式？&lt;strong&gt;饿汉式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;问题6：枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做？添加构造方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反编译结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton extends java.lang.Enum&amp;lt;Singleton&amp;gt; { // Enum实现序列化接口
	public static final Singleton INSTANCE = new Singleton();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;懒汉式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程不安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    // 私有构造方法
    private Singleton() {}

    // 在成员位置创建该类的对象
    private static Singleton instance;

    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance == null) {
            // 多线程环境，会出现线程安全问题，可能多个线程同时进入这里
            instance = new Singleton();
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双端检锁机制&lt;/p&gt;
&lt;p&gt;在多线程的情况下，可能会出现空指针问题，出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作，所以需要使用 &lt;code&gt;volatile&lt;/code&gt; 关键字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton { 
    // 私有构造方法
    private Singleton() {}
    private static volatile Singleton instance;

    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        // 第一次判断，如果instance不为null，不进入抢锁阶段，直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                // 抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态内部类方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    // 私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内部类属于懒汉式，类加载本身就是懒惰的，首次调用时加载，然后对单例进行初始化&lt;/p&gt;
&lt;p&gt;类加载的时候方法不会被调用，所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载；加载的时候字节码常量池会被加入类的运行时常量池，解析工作是将常量池中的符号引用解析成直接引用，但是解析过程不一定非得在类加载时完成，可以延迟到运行时进行，所以静态内部类实现单例会&lt;strong&gt;延迟加载&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有线程安全问题，静态变量初始化在类加载时完成，由 JVM 保证线程安全&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;破坏单例&lt;/h4&gt;
&lt;h5&gt;反序列化&lt;/h5&gt;
&lt;p&gt;将单例对象序列化再反序列化，对象从内存反序列化到程序中会重新创建一个对象，通过反序列化得到的对象是不同的对象，而且得到的对象不是通过构造器得到的，&lt;strong&gt;反序列化得到的对象不执行构造器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Singleton&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton implements Serializable {	//实现序列化接口
    // 私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) throws Exception {
        //往文件中写对象
        //writeObject2File();
        //从文件中读取对象
        Singleton s1 = readObjectFromFile();
        Singleton s2 = readObjectFromFile();
        //判断两个反序列化后的对象是否是同一个对象
        System.out.println(s1 == s2);
    }

    private static Singleton readObjectFromFile() throws Exception {
        //创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(&quot;C://a.txt&quot;));
        //第一个读取Singleton对象
        Singleton instance = (Singleton) ois.readObject();
        return instance;
    }
    
    public static void writeObject2File() throws Exception {
        //获取Singleton类的对象
        Singleton instance = Singleton.getInstance();
        //创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(&quot;C://a.txt&quot;));
        //将instance对象写出到文件中
        oos.writeObject(instance);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;p&gt;在 Singleton 类中添加 &lt;code&gt;readResolve()&lt;/code&gt; 方法，在反序列化时被反射调用，如果定义了这个方法，就返回这个方法的值，如果没有定义，则返回新创建的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Object readResolve() {
    return SingletonHolder.INSTANCE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ObjectInputStream 类源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final Object readObject() throws IOException, ClassNotFoundException{
    //...
	Object obj = readObject0(false);//重点查看readObject0方法
}

private Object readObject0(boolean unshared) throws IOException {
    try {
		switch (tc) {
			case TC_OBJECT:
				return checkResolve(readOrdinaryObject(unshared));
        }
    } 
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
	// isInstantiable 返回true，执行 desc.newInstance()，通过反射创建新的单例类
    obj = desc.isInstantiable() ? desc.newInstance() : null; 
    // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
    if (obj != null &amp;amp;&amp;amp; handles.lookupException(passHandle) == null &amp;amp;&amp;amp; desc.hasReadResolveMethod()) {
    	// 通过反射调用 Singleton 类中的 readResolve 方法，将返回值赋值给rep变量
    	// 多次调用ObjectInputStream类中的readObject方法，本质调用定义的readResolve方法，返回的是同一个对象。
    	Object rep = desc.invokeReadResolve(obj);
    }
    return obj;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;反射破解&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;反射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) throws Exception {
        //获取Singleton类的字节码对象
        Class clazz = Singleton.class;
        //获取Singleton类的私有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);

        //创建Singleton类的对象s1
        Singleton s1 = (Singleton) constructor.newInstance();
        //创建Singleton类的对象s2
        Singleton s2 = (Singleton) constructor.newInstance();

        //判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(s1 == s2);	//false
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反射方式破解单例的解决方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static volatile Singleton instance;
    
    // 私有构造方法
    private Singleton() {
        // 反射破解单例模式需要添加的代码
        if(instance != null) {
            throw new RuntimeException();
        }
    }
    
    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance != null) {
            return instance;
        }
        synchronized (Singleton.class) {
            if(instance != null) {
                return instance;
            }
            instance = new Singleton();
            return instance;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Runtime&lt;/h4&gt;
&lt;p&gt;Runtime 类就是使用的单例设计模式中的饿汉式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Runtime {    
    private static Runtime currentRuntime = new Runtime();    
    public static Runtime getRuntime() {        
        return currentRuntime;    
    }   
    private Runtime() {}    
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 Runtime&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
        //获取Runtime类对象
        Runtime runtime = Runtime.getRuntime();

        //返回 Java 虚拟机中的内存总量。
        System.out.println(runtime.totalMemory());
        //返回 Java 虚拟机试图使用的最大内存量。
        System.out.println(runtime.maxMemory());

        //创建一个新的进程执行指定的字符串命令，返回进程对象
        Process process = runtime.exec(&quot;ipconfig&quot;);
        //获取命令执行后的结果，通过输入流获取
        InputStream inputStream = process.getInputStream();
        byte[] arr = new byte[1024 * 1024* 100];
        int b = inputStream.read(arr);
        System.out.println(new String(arr,0,b,&quot;gbk&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;代理模式&lt;/h3&gt;
&lt;h4&gt;静态代理&lt;/h4&gt;
&lt;p&gt;代理模式：由于某些原因需要给某对象提供一个代理以控制对该对象的访问，访问对象不适合或者不能直接引用为目标对象，代理对象作为访问对象和目标对象之间的中介&lt;/p&gt;
&lt;p&gt;Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理，静态代理代理类在编译期就生成，而动态代理代理类则是在 Java 运行时动态生成，动态代理又有 JDK 代理和 CGLib 代理两种&lt;/p&gt;
&lt;p&gt;代理（Proxy）模式分为三种角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象主题（Subject）类：通过接口或抽象类声明真实主题和代理对象实现的业务方法&lt;/li&gt;
&lt;li&gt;真实主题（Real Subject）类： 实现了抽象主题中的具体业务，是代理对象所代表的真实对象，是最终要引用的对象&lt;/li&gt;
&lt;li&gt;代理（Proxy）类：提供了与真实主题相同的接口，其内部含有对真实主题的引用，可以访问、控制或扩展真实主题的功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;买票案例，火车站是目标对象，代售点是代理对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;卖票接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface SellTickets {
    void sell();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;火车站，具有卖票功能，需要实现SellTickets接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TrainStation implements SellTickets {
    public void sell() {
        System.out.println(&quot;火车站卖票&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代售点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ProxyPoint implements SellTickets {
    private TrainStation station = new TrainStation();

    public void sell() {
        System.out.println(&quot;代理点收取一些服务费用&quot;);
        station.sell();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
        ProxyPoint pp = new ProxyPoint();
        pp.sell();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试类直接访问的是 ProxyPoint 类对象，也就是 ProxyPoint 作为访问对象和目标对象的中介&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;JDK&lt;/h4&gt;
&lt;h5&gt;使用方式&lt;/h5&gt;
&lt;p&gt;Java 中提供了一个动态代理类 Proxy，Proxy 并不是代理对象的类，而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) &lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数一：类加载器，负责加载代理类。传入类加载器，代理和被代理对象要用一个类加载器才是父子关系，不同类加载器加载相同的类在 JVM 中都不是同一个类对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数二：被代理业务对象的&lt;strong&gt;全部实现的接口&lt;/strong&gt;，代理对象与真实对象实现相同接口，知道为哪些方法做代理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数三：代理真正的执行方法，也就是代理的处理逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代理工厂：创建代理对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ProxyFactory {
    private TrainStation station = new TrainStation();
	//也可以在参数中提供 getProxyObject(TrainStation station)
    public SellTickets getProxyObject() {
        //使用 Proxy 获取代理对象
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(
            	station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) {
                        System.out.println(&quot;代理点(JDK动态代理方式)&quot;);
                        //执行真实对象
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();
        //必须时代理ji
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;p&gt;JDK 动态代理方式的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：可以为任意的接口实现类对象做代理，也可以为被代理对象的所有接口的所有方法做代理，动态代理可以在不改变方法源码的情况下，实现对方法功能的增强，提高了软件的可扩展性，Java 反射机制可以生成任意类型的动态代理类&lt;/li&gt;
&lt;li&gt;缺点：&lt;strong&gt;只能针对接口或者接口的实现类对象做代理对象&lt;/strong&gt;，普通类是不能做代理对象的&lt;/li&gt;
&lt;li&gt;原因：&lt;strong&gt;生成的代理类继承了 Proxy&lt;/strong&gt;，Java 是单继承的，所以 JDK 动态代理只能代理接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ProxyFactory 不是代理模式中的代理类，而代理类是程序在运行过程中动态的在内存中生成的类，可以通过 Arthas 工具查看代理类结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理类（$Proxy0）实现了 SellTickets 接口，真实类和代理类实现同样的接口&lt;/li&gt;
&lt;li&gt;代理类（$Proxy0）将提供了的匿名内部类对象传递给了父类&lt;/li&gt;
&lt;li&gt;代理类（$Proxy0）的修饰符是 public final&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);//InvocationHandler对象传递给父类
    }

    static {
        m3 = Class.forName(&quot;proxy.dynamic.jdk.SellTickets&quot;).getMethod(&quot;sell&quot;, new Class[0]);
    }

    public final void sell() {
        // 调用InvocationHandler的invoke方法
        this.h.invoke(this, m3, null);
    }
}

// Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
	protected InvocationHandler h;
	 
	protected Proxy(InvocationHandler h) {
        this.h = h;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在测试类中通过代理对象调用 sell() 方法&lt;/li&gt;
&lt;li&gt;根据多态的特性，执行的是代理类（$Proxy0）中的 sell() 方法&lt;/li&gt;
&lt;li&gt;代理类（$Proxy0）中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法&lt;/li&gt;
&lt;li&gt;invoke 方法通过反射执行了真实对象所属类（TrainStation）中的 sell() 方法&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;源码解析&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public static Object newProxyInstance(ClassLoader loader,
                                      Class&amp;lt;?&amp;gt;[] interfaces,
                                      InvocationHandler h){
    // InvocationHandler 为空则抛出异常
    Objects.requireNonNull(h);

    // 复制一份 interfaces
    final Class&amp;lt;?&amp;gt;[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    // 从缓存中查找 class 类型的代理对象，会调用 ProxyClassFactory#apply 方法
    Class&amp;lt;?&amp;gt; cl = getProxyClass0(loader, intfs);
	//proxyClassCache = new WeakCache&amp;lt;&amp;gt;(new KeyFactory(), new ProxyClassFactory())
 
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        // 获取代理类的构造方法，根据参数 InvocationHandler 匹配获取某个构造器
        final Constructor&amp;lt;?&amp;gt; cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        // 构造方法不是 pubic 的需要启用权限，暴力p
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction&amp;lt;Void&amp;gt;() {
                public Void run() {
                    // 设置可访问的权限
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
       	// cons 是构造方法，并且内部持有 InvocationHandler，在 InvocationHandler 中持有 target 目标对象
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Proxy 的静态内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final class ProxyClassFactory {
    // 代理类型的名称前缀
    private static final String proxyClassNamePrefix = &quot;$Proxy&quot;;

    // 生成唯一数字使用，结合上面的代理类型名称前缀一起生成
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

	//参数一：Proxy.newInstance 时传递的
    //参数二：Proxy.newInstance 时传递的接口集合
    @Override
    public Class&amp;lt;?&amp;gt; apply(ClassLoader loader, Class&amp;lt;?&amp;gt;[] interfaces) {
		
        Map&amp;lt;Class&amp;lt;?&amp;gt;, Boolean&amp;gt; interfaceSet = new IdentityHashMap&amp;lt;&amp;gt;(interfaces.length);
        // 遍历接口集合
        for (Class&amp;lt;?&amp;gt; intf : interfaces) {
            Class&amp;lt;?&amp;gt; interfaceClass = null;
            try {
                // 加载接口类到 JVM
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + &quot; is not visible from class loader&quot;);
            }
            // 如果 interfaceClass 不是接口 直接报错，保证集合内都是接口
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + &quot; is not an interface&quot;);
            }
            // 保证接口 interfaces 集合中没有重复的接口
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    &quot;repeated interface: &quot; + interfaceClass.getName());
            }
        }

        // 生成的代理类的包名
        String proxyPkg = null;   
        // 【生成的代理类访问修饰符 public final】 
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        // 检查接口集合内的接口，看看有没有某个接口的访问修饰符不是 public 的  如果不是 public 的接口，
        // 生成的代理类 class 就必须和它在一个包下，否则访问出现问题
        for (Class&amp;lt;?&amp;gt; intf : interfaces) {
            // 获取访问修饰符
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                // 获取当前接口的全限定名 包名.类名
                String name = intf.getName();
                int n = name.lastIndexOf(&apos;.&apos;);
                // 获取包名
                String pkg = ((n == -1) ? &quot;&quot; : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        &quot;non-public interfaces from different packages&quot;);
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + &quot;.&quot;;
        }

        // 获取唯一的编号
        long num = nextUniqueNumber.getAndIncrement();
        // 包名+ $proxy + 数字，比如 $proxy1
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 【生成二进制字节码，这个字节码写入到文件内】，就是编译好的 class 文件
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
        try {
            // 【使用加载器加载二进制到 jvm】，并且返回 class
            return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) { }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;CGLIB&lt;/h4&gt;
&lt;p&gt;CGLIB 是一个功能强大，高性能的代码生成包，为没有实现接口的类提供代理，为 JDK 动态代理提供了补充（$$Proxy）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CGLIB 是第三方提供的包，所以需要引入 jar 包的坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;cglib&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;cglib&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.2.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代理工厂类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ProxyFactory implements MethodInterceptor {
    private TrainStation target = new TrainStation();

    public TrainStation getProxyObject() {
        //创建Enhancer对象，类似于JDK动态代理的Proxy类，下一步就是设置几个参数
        Enhancer enhancer = new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(target.getClass());
        //设置回调函数
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
				System.out.println(&quot;代理点收取一些服务费用(CGLIB动态代理方式)&quot;);
        		Object o = methodProxy.invokeSuper(obj, args);
        		return null;//因为返回值为void
            }
        });
        //创建代理对象
        TrainStation obj = (TrainStation) enhancer.create();
        return obj;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CGLIB 的优缺点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;CGLIB 动态代理&lt;strong&gt;不限定&lt;/strong&gt;是否具有接口，可以对任意操作进行增强&lt;/li&gt;
&lt;li&gt;CGLIB 动态代理无需要原始被代理对象，动态创建出新的代理对象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JDKProxy 仅对接口方法做增强，CGLIB 对所有方法做增强&lt;/strong&gt;，包括 Object 类中的方法，toString、hashCode 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：CGLIB 不能对声明为 final 的类或者方法进行代理，因为 CGLIB 原理是&lt;strong&gt;动态生成被代理类的子类，继承被代理类&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;方式对比&lt;/h4&gt;
&lt;p&gt;三种方式对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;动态代理和静态代理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理（InvocationHandler.invoke），在接口方法数量比较多的时候，可以进行灵活处理，不需要像静态代理那样每一个方法进行中转&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态代理是程序&lt;strong&gt;在运行后通过反射创建字节码文件&lt;/strong&gt;交由 JVM 加载&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 代理和 CGLIB 代理：&lt;/p&gt;
&lt;p&gt;JDK 动态代理采用 &lt;code&gt;ProxyGenerator.generateProxyClass()&lt;/code&gt; 方法在运行时生成字节码；CGLIB 底层采用 ASM 字节码生成框架，使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高，到 JDK1.8 的时候，JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理，如果没有接口使用 CGLIB 代理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代理模式的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理对象可以增强目标对象的功能，被用来间接访问底层对象，与原始对象具有相同的 hashCode&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;代理模式能将客户端与目标对象分离，在一定程度上降低了系统的耦合度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：增加了系统的复杂度&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代理模式的使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;远程（Remote）代理：本地服务通过网络请求远程服务，需要实现网络通信，处理其中可能的异常。为了良好的代码设计和可维护性，将网络通信部分隐藏起来，只暴露给本地服务一个接口，通过该接口即可访问远程服务提供的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;防火墙（Firewall）代理：当你将浏览器配置成使用代理功能时，防火墙就将你的浏览器的请求转给互联网，当互联网返回响应时，代理服务器再把它转给你的浏览器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保护（Protect or Access）代理：控制对一个对象的访问，如果需要，可以给不同的用户提供不同级别的使用权限&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;JVM&lt;/h1&gt;
&lt;h2&gt;JVM概述&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;JVM：全称 Java Virtual Machine，即 Java 虚拟机，一种规范，本身是一个虚拟计算机，直接和操作系统进行交互，与硬件不直接交互，而操作系统可以帮我们完成和硬件进行交互的工作&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 虚拟机基于&lt;strong&gt;二进制字节码&lt;/strong&gt;执行，由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成&lt;/li&gt;
&lt;li&gt;JVM 屏蔽了与操作系统平台相关的信息，从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件，通过该机制实现的&lt;strong&gt;跨平台性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 代码执行流程：&lt;code&gt;Java 程序 --（编译）--&amp;gt; 字节码文件 --（解释执行）--&amp;gt; 操作系统（Win，Linux）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;JVM 结构：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-概述图.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;JVM、JRE、JDK 对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK(Java SE Development Kit)：Java 标准开发包，提供了编译、运行 Java 程序所需的各种工具和资源&lt;/li&gt;
&lt;li&gt;JRE( Java Runtime Environment)：Java 运行环境，用于解释执行 Java 的字节码文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-JRE关系.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/34907497/&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1PJ411n7xZ&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1yE411Z7AP&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;架构模型&lt;/h3&gt;
&lt;p&gt;Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计，Java 的指令都是根据栈来设计的，不同平台 CPU 架构不同，所以不能设计为基于寄存器架构&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于栈式架构的特点：
&lt;ul&gt;
&lt;li&gt;设计和实现简单，适用于资源受限的系统&lt;/li&gt;
&lt;li&gt;使用零地址指令方式分配，执行过程依赖操作栈，指令集更小，编译器容易实现
&lt;ul&gt;
&lt;li&gt;零地址指令：机器指令的一种，是指令系统中的一种不设地址字段的指令，只有操作码而没有地址码。这种指令有两种情况：一是无需操作数，另一种是操作数为默认的（隐含的），默认为操作数在寄存器（ACC）中，指令可直接访问寄存器&lt;/li&gt;
&lt;li&gt;一地址指令：一个操作码对应一个地址码，通过地址码寻找操作数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不需要硬件的支持，可移植性更好，更好实现跨平台&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于寄存器架构的特点：
&lt;ul&gt;
&lt;li&gt;需要硬件的支持，可移植性差&lt;/li&gt;
&lt;li&gt;性能更好，执行更高效，寄存器比内存快&lt;/li&gt;
&lt;li&gt;以一地址指令、二地址指令、三地址指令为主&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;生命周期&lt;/h3&gt;
&lt;p&gt;JVM 的生命周期分为三个阶段，分别为：启动、运行、死亡&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动&lt;/strong&gt;：当启动一个 Java 程序时，通过引导类加载器（bootstrap class loader）创建一个初始类（initial class），对于拥有 main 函数的类就是 JVM 实例运行的起点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;main() 方法是一个程序的初始起点，任何线程均可由在此处启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 JVM 内部有两种线程类型，分别为：用户线程和守护线程，&lt;strong&gt;JVM 使用的是守护线程，main() 和其他线程使用的是用户线程&lt;/strong&gt;，守护线程会随着用户线程的结束而结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行一个 Java 程序时，真真正正在执行的是一个 &lt;strong&gt;Java 虚拟机的进程&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM 有两种运行模式 Server 与 Client，两种模式的区别在于：Client 模式启动速度较快，Server 模式启动较慢；但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多&lt;/p&gt;
&lt;p&gt;Server 模式启动的 JVM 采用的是重量级的虚拟机，对程序采用了更多的优化；Client 模式启动的 JVM 采用的是轻量级的虚拟机&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;死亡&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当程序中的用户线程都中止，JVM 才会退出&lt;/li&gt;
&lt;li&gt;程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止&lt;/li&gt;
&lt;li&gt;线程调用 Runtime 类 halt 方法或 System 类 exit 方法，并且 Java 安全管理器允许这次 exit 或 halt 操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;内存结构&lt;/h2&gt;
&lt;h3&gt;内存概述&lt;/h3&gt;
&lt;p&gt;内存结构是 JVM 中非常重要的一部分，是非常重要的系统资源，是硬盘和 CPU 的桥梁，承载着操作系统和应用程序的实时运行，又叫运行时数据区&lt;/p&gt;
&lt;p&gt;JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略，保证了 JVM 的高效稳定运行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java1.8 以前的内存结构图：
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java7%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java1.8 之后的内存结果图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java8%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程运行诊断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定位：jps 定位进程 ID&lt;/li&gt;
&lt;li&gt;jstack 进程 ID：用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 OOM 错误：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;java.lang.StackOverflowError&lt;/li&gt;
&lt;li&gt;java.lang.OutOfMemoryError：java heap space&lt;/li&gt;
&lt;li&gt;java.lang.OutOfMemoryError：GC overhead limit exceeded&lt;/li&gt;
&lt;li&gt;java.lang.OutOfMemoryError：Direct buffer memory&lt;/li&gt;
&lt;li&gt;java.lang.OutOfMemoryError：unable to create new native thread&lt;/li&gt;
&lt;li&gt;java.lang.OutOfMemoryError：Metaspace&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;JVM内存&lt;/h3&gt;
&lt;h4&gt;虚拟机栈&lt;/h4&gt;
&lt;h5&gt;Java 栈&lt;/h5&gt;
&lt;p&gt;Java 虚拟机栈：Java Virtual Machine Stacks，&lt;strong&gt;每个线程&lt;/strong&gt;运行时所需要的内存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个方法被执行时，都会在虚拟机栈中创建一个栈帧 stack frame（&lt;strong&gt;一个方法一个栈帧&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 虚拟机规范允许 &lt;strong&gt;Java 栈的大小是动态的或者是固定不变的&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟机栈是&lt;strong&gt;每个线程私有的&lt;/strong&gt;，每个线程只能有一个活动栈帧，对应方法调用到执行完成的整个过程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个栈由多个栈帧（Frame）组成，对应着每次方法调用时所占用的内存，每个栈帧中存储着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量表：存储方法里的 Java 基本数据类型以及对象的引用&lt;/li&gt;
&lt;li&gt;动态链接：也叫指向运行时常量池的方法引用&lt;/li&gt;
&lt;li&gt;方法返回地址：方法正常退出或者异常退出的定义&lt;/li&gt;
&lt;li&gt;操作数栈或表达式栈和其他一些附加信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-虚拟机栈.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置栈内存大小：&lt;code&gt;-Xss size&lt;/code&gt;   &lt;code&gt;-Xss 1024k&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 JDK 1.4 中默认为 256K，而在 JDK 1.5+ 默认为 1M&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚拟机栈特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈内存&lt;strong&gt;不需要进行GC&lt;/strong&gt;，方法开始执行的时候会进栈，方法调用后自动弹栈，相当于清空了数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈内存分配越大越大，可用的线程数越少（内存越大，每个线程拥有的内存越大）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法内的局部变量是否&lt;strong&gt;线程安全&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果方法内局部变量没有逃离方法的作用访问，它是线程安全的（逃逸分析）&lt;/li&gt;
&lt;li&gt;如果是局部变量引用了对象，并逃离方法的作用范围，需要考虑线程安全&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异常：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈帧过多导致栈内存溢出 （超过了栈的容量），会抛出 OutOfMemoryError 异常&lt;/li&gt;
&lt;li&gt;当线程请求的栈深度超过最大值，会抛出 StackOverflowError 异常&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;局部变量&lt;/h5&gt;
&lt;p&gt;局部变量表也被称之为局部变量数组或本地变量表，本质上定义为一个数字数组，主要用于存储方法参数和定义在方法体内的局部变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表是建立在线程的栈上，是线程私有的数据，因此不存在数据安全问题&lt;/li&gt;
&lt;li&gt;表的容量大小是在编译期确定的，保存在方法的 Code 属性的 maximum local variables 数据项中&lt;/li&gt;
&lt;li&gt;表中的变量只在当前方法调用中有效，方法结束栈帧销毁，局部变量表也会随之销毁&lt;/li&gt;
&lt;li&gt;表中的变量也是重要的垃圾回收根节点，只要被表中数据直接或间接引用的对象都不会被回收&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部变量表最基本的存储单元是 &lt;strong&gt;slot（变量槽）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数值的存放总是在局部变量数组的 index0 开始，到数组长度 -1 的索引结束，JVM 为每一个 slot 都分配一个访问索引，通过索引即可访问到槽中的数据&lt;/li&gt;
&lt;li&gt;存放编译期可知的各种基本数据类型（8种），引用类型（reference），returnAddress 类型的变量&lt;/li&gt;
&lt;li&gt;32 位以内的类型只占一个 slot（包括 returnAddress 类型），64 位的类型（long 和 double）占两个 slot&lt;/li&gt;
&lt;li&gt;局部变量表中的槽位是可以&lt;strong&gt;重复利用&lt;/strong&gt;的，如果一个局部变量过了其作用域，那么之后申明的新的局部变量就可能会复用过期局部变量的槽位，从而达到节省资源的目的&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;操作数栈&lt;/h5&gt;
&lt;p&gt;栈：可以使用数组或者链表来实现&lt;/p&gt;
&lt;p&gt;操作数栈：在方法执行过程中，根据字节码指令，往栈中写入数据或提取数据，即入栈（push）或出栈（pop）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;保存计算过程的中间结果，同时作为计算过程中变量临时的存储空间，是执行引擎的一个工作区&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 虚拟机的解释引擎是基于栈的执行引擎，其中的栈指的就是操作数栈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果被调用的方法带有返回值的话，其&lt;strong&gt;返回值将会被压入当前栈帧的操作数栈中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;栈顶缓存技术 ToS（Top-of-Stack Cashing）：将栈顶元素全部缓存在 CPU 的寄存器中，以此降低对内存的读/写次数，提升执行的效率&lt;/p&gt;
&lt;p&gt;基于栈式架构的虚拟机使用的零地址指令更加紧凑，完成一项操作需要使用很多入栈和出栈指令，所以需要更多的指令分派（instruction dispatch）次数和内存读/写次数，由于操作数是存储在内存中的，因此频繁地执行内存读/写操作必然会影响执行速度，所以需要栈顶缓存技术&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;动态链接&lt;/h5&gt;
&lt;p&gt;动态链接是指向运行时常量池的方法引用，涉及到栈操作已经是类加载完成，这个阶段的解析是&lt;strong&gt;动态绑定&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为了支持当前方法的代码能够实现动态链接，每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E7%AC%A6%E5%8F%B7%E5%BC%95%E7%94%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 Java 源文件被编译成的字节码文件中，所有的变量和方法引用都作为符号引用保存在 class 的常量池中&lt;/p&gt;
&lt;p&gt;常量池的作用：提供一些符号和常量，便于指令的识别&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E8%BF%90%E8%A1%8C%E6%97%B6%E5%B8%B8%E9%87%8F%E6%B1%A0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;返回地址&lt;/h5&gt;
&lt;p&gt;Return Address：存放调用该方法的 PC 寄存器的值&lt;/p&gt;
&lt;p&gt;方法的结束有两种方式：正常执行完成、出现未处理的异常，在方法退出后都返回到该方法被调用的位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正常：调用者的 PC 计数器的值作为返回地址，即调用该方法的指令的&lt;strong&gt;下一条指令的地址&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;异常：返回地址是要通过异常表来确定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正常完成出口：执行引擎遇到任意一个方法返回的字节码指令（return），会有返回值传递给上层的方法调用者&lt;/p&gt;
&lt;p&gt;异常完成出口：方法执行的过程中遇到了异常（Exception），并且这个异常没有在方法内进行处理，本方法的异常表中没有搜素到匹配的异常处理器，导致方法退出&lt;/p&gt;
&lt;p&gt;两者区别：通过异常完成出口退出的不会给上层调用者产生任何的返回值&lt;/p&gt;
&lt;h5&gt;附加信息&lt;/h5&gt;
&lt;p&gt;栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息，例如对程序调试提供支持的信息&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;本地方法栈&lt;/h4&gt;
&lt;p&gt;本地方法栈是为虚拟机执行本地方法时提供服务的&lt;/p&gt;
&lt;p&gt;JNI：Java Native Interface，通过使用 Java 本地接口程序，可以确保代码在不同的平台上方便移植&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不需要进行 GC，与虚拟机栈类似，也是线程私有的，有 StackOverFlowError 和 OutOfMemoryError 异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟机栈执行的是 Java 方法，在 HotSpot JVM 中，直接将本地方法栈和虚拟机栈合二为一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地方法一般是由其他语言编写，并且被编译为基于本机硬件和操作系统的程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当某个线程调用一个本地方法时，就进入了不再受虚拟机限制的世界，和虚拟机拥有同样的权限&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地方法可以通过本地方法接口来&lt;strong&gt;访问虚拟机内部的运行时数据区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;直接从本地内存的堆中分配任意数量的内存&lt;/li&gt;
&lt;li&gt;可以直接使用本地处理器中的寄存器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：将本地的 C 函数（如 foo）编译到一个共享库（foo.so）中，当正在运行的 Java 程序调用 foo 时，Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dlopen 函数：Linux 系统加载和链接共享库&lt;/li&gt;
&lt;li&gt;dlclose 函数：卸载共享库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-本地方法栈.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;图片来源：https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;程序计数器&lt;/h4&gt;
&lt;p&gt;Program Counter Register 程序计数器（寄存器）&lt;/p&gt;
&lt;p&gt;作用：内部保存字节码的行号，用于记录正在执行的字节码指令地址（如果正在执行的是本地方法则为空）&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JVM 对于多线程是通过线程轮流切换并且分配线程执行时间，一个处理器只会处理执行一个线程&lt;/li&gt;
&lt;li&gt;切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是线程私有的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不会存在内存溢出&lt;/strong&gt;，是 JVM 规范中唯一一个不出现 OOM 的区域，所以这个空间不会进行 GC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 反编译指令：&lt;code&gt;javap -v Test.class&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;#20：代表去 Constant pool 查看该地址的指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: getstatic #20 		// PrintStream out = System.out;
3: astore_1 			// --
4: aload_1 				// out.println(1);
5: iconst_1 			// --
6: invokevirtual #26 	// --
9: aload_1 				// out.println(2);
10: iconst_2 			// --
11: invokevirtual #26 	// --
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;堆&lt;/h4&gt;
&lt;p&gt;Heap 堆：是 JVM 内存中最大的一块，由所有线程共享，由垃圾回收器管理的主要区域，堆中对象大部分都需要考虑线程安全的问题&lt;/p&gt;
&lt;p&gt;存放哪些资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象实例：类初始化生成的对象，&lt;strong&gt;基本数据类型的数组也是对象实例&lt;/strong&gt;，new 创建对象都使用堆内存&lt;/li&gt;
&lt;li&gt;字符串常量池：
&lt;ul&gt;
&lt;li&gt;字符串常量池原本存放于方法区，JDK7 开始放置于堆中&lt;/li&gt;
&lt;li&gt;字符串常量池&lt;strong&gt;存储的是 String 对象的直接引用或者对象&lt;/strong&gt;，是一张 string table&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态变量：静态变量是有 static 修饰的变量，JDK8 时从方法区迁移至堆中&lt;/li&gt;
&lt;li&gt;线程分配缓冲区 Thread Local Allocation Buffer：线程私有但不影响堆的共性，可以提升对象分配的效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置堆内存指令：&lt;code&gt;-Xmx Size&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;内存溢出：new 出对象，循环添加字符数据，当堆中没有内存空间可分配给实例，也无法再扩展时，就会抛出 OutOfMemoryError 异常&lt;/p&gt;
&lt;p&gt;堆内存诊断工具：（控制台命令）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;jps：查看当前系统中有哪些 Java 进程&lt;/li&gt;
&lt;li&gt;jmap：查看堆内存占用情况 &lt;code&gt;jhsdb jmap --heap --pid 进程id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;jconsole：图形界面的，多功能的监测工具，可以连续监测&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Java7 中堆内会存在&lt;strong&gt;年轻代、老年代和方法区（永久代）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Young 区被划分为三部分，Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的，另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候，GC 就会将存活的对象移到空闲的 Survivor 区间中，根据 JVM 的策略，在经过几次垃圾回收后，仍然存活于 Survivor 的对象将被移动到 Tenured 区间&lt;/li&gt;
&lt;li&gt;Tenured 区主要保存生命周期长的对象，一般是一些老的对象，当一些对象在 Young 复制转移一定的次数以后，对象就会被转移到 Tenured 区&lt;/li&gt;
&lt;li&gt;Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码，在 Java7 中堆内方法区会受到 GC 的管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分代原因：不同对象的生命周期不同，70%-99% 的对象都是临时对象，优化 GC 性能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    // 返回Java虚拟机中的堆内存总量
    long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
    // 返回Java虚拟机使用的最大堆内存量
    long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    
    System.out.println(&quot;-Xms : &quot; + initialMemory + &quot;M&quot;);//-Xms : 245M
    System.out.println(&quot;-Xmx : &quot; + maxMemory + &quot;M&quot;);//-Xmx : 3641M
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;方法区&lt;/h4&gt;
&lt;p&gt;方法区：是各个线程共享的内存区域，用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据，虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分，但是也叫 Non-Heap（非堆）&lt;/p&gt;
&lt;p&gt;方法区是一个 JVM 规范，&lt;strong&gt;永久代与元空间都是其一种实现方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方法区的大小不必是固定的，可以动态扩展，加载的类太多，可能导致永久代内存溢出 (OutOfMemoryError)&lt;/p&gt;
&lt;p&gt;方法区的 GC：针对常量池的回收及对类型的卸载，比较难实现&lt;/p&gt;
&lt;p&gt;为了&lt;strong&gt;避免方法区出现 OOM&lt;/strong&gt;，在 JDK8 中将堆内的方法区（永久代）移动到了本地内存上，重新开辟了一块空间，叫做元空间，元空间存储类的元信息，&lt;strong&gt;静态变量和字符串常量池等放入堆中&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;类元信息：在类编译期间放入方法区，存放了类的基本信息，包括类的方法、参数、接口以及常量池表&lt;/p&gt;
&lt;p&gt;常量池表（Constant Pool Table）是 Class 文件的一部分，存储了&lt;strong&gt;类在编译期间生成的字面量、符号引用&lt;/strong&gt;，JVM 为每个已加载的类维护一个常量池&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字面量：基本数据类型、字符串类型常量、声明为 final 的常量值等&lt;/li&gt;
&lt;li&gt;符号引用：类、字段、方法、接口等的符号引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行时常量池是方法区的一部分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常量池（编译器生成的字面量和符号引用）中的数据会在类加载的加载阶段放入运行时常量池&lt;/li&gt;
&lt;li&gt;类在解析阶段将这些符号引用替换成直接引用&lt;/li&gt;
&lt;li&gt;除了在编译期生成的常量，还允许动态生成，例如 String 类的 intern()&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;本地内存&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;虚拟机内存：Java 虚拟机在执行的时候会把管理的内存分配成不同的区域，受虚拟机内存大小的参数控制，当大小超过参数设置的大小时就会报 OOM&lt;/p&gt;
&lt;p&gt;本地内存：又叫做&lt;strong&gt;堆外内存&lt;/strong&gt;，线程共享的区域，本地内存这块区域是不会受到 JVM 的控制的，不会发生 GC；因此对于整个 Java 的执行效率是提升非常大，但是如果内存的占用超出物理内存的大小，同样也会报 OOM&lt;/p&gt;
&lt;p&gt;本地内存概述图：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-内存图对比.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;元空间&lt;/h4&gt;
&lt;p&gt;PermGen 被元空间代替，永久代的&lt;strong&gt;类信息、方法、常量池&lt;/strong&gt;等都移动到元空间区&lt;/p&gt;
&lt;p&gt;元空间与永久代区别：元空间不在虚拟机中，使用的本地内存，默认情况下，元空间的大小仅受本地内存限制&lt;/p&gt;
&lt;p&gt;方法区内存溢出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK1.8 以前会导致永久代内存溢出：java.lang.OutOfMemoryError: PerGen space&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; -XX:MaxPermSize=8m		#参数设置
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK1.8 以后会导致元空间内存溢出：java.lang.OutOfMemoryError: Metaspace&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-XX:MaxMetaspaceSize=8m	#参数设置	
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;元空间内存溢出演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i &amp;lt; 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号， public， 类名, 包名, 父类， 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, &quot;Class&quot; + i, null, &quot;java/lang/Object&quot;, null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass(&quot;Class&quot; + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;直接内存&lt;/h4&gt;
&lt;p&gt;直接内存是 Java 堆外、直接向系统申请的内存区间，不是虚拟机运行时数据区的一部分，也不是《Java 虚拟机规范》中定义的内存区域&lt;/p&gt;
&lt;p&gt;直接内存详解参考：NET → NIO → 直接内存&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;变量位置&lt;/h3&gt;
&lt;p&gt;变量的位置不取决于它是基本数据类型还是引用数据类型，取决于它的&lt;strong&gt;声明位置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;静态内部类和其他内部类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一个 class 文件只能对应一个 public 类型的类&lt;/strong&gt;，这个类可以有内部类，但不会生成新的 class 文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态内部类属于类本身，加载到方法区，其他内部类属于内部类的属性，加载到堆（待考证）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类变量是用 static 修饰符修饰，定义在方法外的变量，随着 Java 进程产生和销毁&lt;/li&gt;
&lt;li&gt;在 Java8 之前把静态变量存放于方法区，在 Java8 时存放在堆中的静态变量区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实例变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例（成员）变量是定义在类中，没有 static 修饰的变量，随着类的实例产生和销毁，是类实例的一部分&lt;/li&gt;
&lt;li&gt;在类初始化的时候，从运行时常量池取出直接引用或者值，&lt;strong&gt;与初始化的对象一起放入堆中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量是定义在类的方法中的变量&lt;/li&gt;
&lt;li&gt;在所在方法被调用时&lt;strong&gt;放入虚拟机栈的栈帧&lt;/strong&gt;中，方法执行结束后从虚拟机栈中弹出，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类常量池、运行时常量池、字符串常量池有什么关系？有什么区别？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类常量池与运行时常量池都存储在方法区，而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中&lt;/li&gt;
&lt;li&gt;在类编译过程中，会把类元信息放到方法区，类元信息的其中一部分便是类常量池，主要存放字面量和符号引用，而字面量的一部分便是文本字符&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;对于文本字符，会在解析时查找字符串常量池，查出这个文本字符对应的字符串对象的直接引用，将直接引用存储在运行时常量池&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;什么是字面量？什么是符号引用？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字面量：java 代码在编译过程中是无法构建引用的，字面量就是在编译时对于数据的一种表示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a = 1;				//这个1便是字面量
String b = &quot;iloveu&quot;;	//iloveu便是字面量
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;符号引用：在编译过程中并不知道每个类的地址，因为可能这个类还没有加载，如果在一个类中引用了另一个类，无法知道它的内存地址，只能用它的类名作为符号引用，在类加载完后用这个符号引用去获取内存地址&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;内存管理&lt;/h2&gt;
&lt;h3&gt;内存分配&lt;/h3&gt;
&lt;h4&gt;两种方式&lt;/h4&gt;
&lt;p&gt;不分配内存的对象无法进行其他操作，JVM 为对象分配内存的过程：首先计算对象占用空间大小，接着在堆中划分一块内存给新对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果内存规整，使用指针碰撞（Bump The Pointer）。所有用过的内存在一边，空闲的内存在另外一边，中间有一个指针作为分界点的指示器，分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离&lt;/li&gt;
&lt;li&gt;如果内存不规整，虚拟机需要维护一个空闲列表（Free List）分配。已使用的内存和未使用的内存相互交错，虚拟机维护了一个列表，记录上哪些内存块是可用的，再分配的时候从列表中找到一块足够大的空间划分给对象实例，并更新列表上的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;TLAB&lt;/h4&gt;
&lt;p&gt;TLAB：Thread Local Allocation Buffer，为每个线程在堆内单独分配了一个缓冲区，多线程分配内存时，使用 TLAB 可以避免线程安全问题，同时还能够提升内存分配的吞吐量，这种内存分配方式叫做&lt;strong&gt;快速分配策略&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈上分配使用的是栈来进行对象内存的分配&lt;/li&gt;
&lt;li&gt;TLAB 分配使用的是 Eden 区域进行内存分配，属于堆内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;堆区是线程共享区域，任何线程都可以访问到堆区中的共享数据，由于对象实例的创建在 JVM 中非常频繁，因此在并发环境下为避免多个线程操作同一地址，需要使用加锁等机制，进而影响分配速度&lt;/p&gt;
&lt;p&gt;问题：堆空间都是共享的么？ 不一定，因为还有 TLAB，在堆中划分出一块区域，为每个线程所独占&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JVM 是将 TLAB 作为内存分配的首选，但不是所有的对象实例都能够在 TLAB 中成功分配内存，一旦对象在 TLAB 空间分配内存失败时，JVM 就会通过&lt;strong&gt;使用加锁机制确保数据操作的原子性&lt;/strong&gt;，从而直接在堆中分配内存&lt;/p&gt;
&lt;p&gt;栈上分配优先于 TLAB 分配进行，逃逸分析中若可进行栈上分配优化，会优先进行对象栈上直接分配内存&lt;/p&gt;
&lt;p&gt;参数设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:UseTLAB&lt;/code&gt;：设置是否开启 TLAB 空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:TLABWasteTargetPercent&lt;/code&gt;：设置 TLAB 空间所占用 Eden 空间的百分比大小，默认情况下 TLAB 空间的内存非常小，仅占有整个 Eden 空间的1%&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:TLABRefillWasteFraction&lt;/code&gt;：指当 TLAB 空间不足，请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配，直接进行堆内存分配，否则还是会优先进行 TLAB 分配&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E8%BF%87%E7%A8%8B.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;逃逸分析&lt;/h4&gt;
&lt;p&gt;即时编译（Just-in-time Compilation，JIT）是一种通过在运行时将字节码翻译为机器码，从而改善性能的技术，在 HotSpot 实现中有多种选择：C1、C2 和 C1+C2，分别对应 Client、Server 和分层编译&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C1 编译速度快，优化方式比较保守；C2 编译速度慢，优化方式比较激进&lt;/li&gt;
&lt;li&gt;C1+C2 在开始阶段采用 C1 编译，当代码运行到一定热度之后采用 C2 重新编译&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;逃逸分析并不是直接的优化手段，而是一个代码分析方式，通过动态分析对象的作用域，为优化手段如栈上分配、标量替换和同步消除等提供依据，发生逃逸行为的情况有两种：方法逃逸和线程逃逸&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法逃逸：当一个对象在方法中定义之后，被外部方法引用
&lt;ul&gt;
&lt;li&gt;全局逃逸：一个对象的作用范围逃出了当前方法或者当前线程，比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值&lt;/li&gt;
&lt;li&gt;参数逃逸：一个对象被作为方法参数传递或者被参数引用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程逃逸：如类变量或实例变量，可能被其它线程访问到&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果不存在逃逸行为，则可以对该对象进行如下优化：同步消除、标量替换和栈上分配&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步消除&lt;/p&gt;
&lt;p&gt;线程同步本身比较耗时，如果确定一个对象不会逃逸出线程，不被其它线程访问到，那对象的读写就不会存在竞争，则可以消除对该对象的&lt;strong&gt;同步锁&lt;/strong&gt;，通过 &lt;code&gt;-XX:+EliminateLocks&lt;/code&gt; 可以开启同步消除 ( - 号关闭)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标量替换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标量替换：如果把一个对象拆散，将其成员变量恢复到基本类型来访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标量 (scalar) ：不可分割的量，如基本数据类型和 reference 类型&lt;/p&gt;
&lt;p&gt;聚合量 (Aggregate)：一个数据可以继续分解，对象一般是聚合量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果逃逸分析发现一个对象不会被外部访问，并且该对象可以被拆散，那么经过优化之后，并不直接生成该对象，而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-XX:+EliminateAllocations&lt;/code&gt;：开启标量替换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:+PrintEliminateAllocations&lt;/code&gt;：查看标量替换情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈上分配&lt;/p&gt;
&lt;p&gt;JIT 编译器在编译期间根据逃逸分析的结果，如果一个对象没有逃逸出方法的话，就可能被优化成栈上分配。分配完成后，继续在调用栈内执行，最后线程结束，栈空间被回收，局部变量对象也被回收，这样就无需 GC&lt;/p&gt;
&lt;p&gt;User 对象的作用域局限在方法 fn 中，可以使用标量替换的优化手段在栈上分配对象的成员变量，这样就不会生成 User 对象，大大减轻 GC 的压力&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JVM {
    public static void main(String[] args) throws Exception {
        int sum = 0;
        int count = 1000000;
        //warm up
        for (int i = 0; i &amp;lt; count ; i++) {
            sum += fn(i);
        }
        System.out.println(sum);
        System.in.read();
    }
    private static int fn(int age) {
        User user = new User(age);
        int i = user.getAge();
        return i;
    }
}

class User {
    private final int age;

    public User(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分代思想&lt;/h4&gt;
&lt;h5&gt;分代介绍&lt;/h5&gt;
&lt;p&gt;Java8 时，堆被分为了两份：新生代和老年代（1:2），在 Java7 时，还存在一个永久代&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新生代使用：复制算法&lt;/li&gt;
&lt;li&gt;老年代使用：标记 - 清除 或者 标记 - 整理 算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Minor GC 和 Full GC&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minor GC：回收新生代，新生代对象存活时间很短，所以 Minor GC 会频繁执行，执行的速度比较快&lt;/li&gt;
&lt;li&gt;Full GC：回收老年代和新生代，老年代对象其存活时间长，所以 Full GC 很少执行，执行速度会比 Minor GC 慢很多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eden 和 Survivor 大小比例默认为 8:1:1&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-分代收集算法.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;分代分配&lt;/h5&gt;
&lt;p&gt;工作机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对象优先在 Eden 分配&lt;/strong&gt;：当创建一个对象的时候，对象会被分配在新生代的 Eden 区，当 Eden 区要满了时候，触发 YoungGC&lt;/li&gt;
&lt;li&gt;当进行 YoungGC 后，此时在 Eden 区存活的对象被移动到 to 区，并且当前对象的年龄会加 1，清空 Eden 区&lt;/li&gt;
&lt;li&gt;当再一次触发 YoungGC 的时候，会把 Eden 区中存活下来的对象和 to 中的对象，移动到 from 区中，这些对象的年龄会加 1，清空 Eden 区和 to 区&lt;/li&gt;
&lt;li&gt;To 区永远是空 Survivor 区，From 区是有数据的，每次 MinorGC 后两个区域互换&lt;/li&gt;
&lt;li&gt;From 区和 To 区 也可以叫做 S0 区和 S1 区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;晋升到老年代：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;长期存活的对象进入老年代&lt;/strong&gt;：为对象定义年龄计数器，对象在 Eden 出生并经过 Minor GC 依然存活，将移动到 Survivor 中，年龄就增加 1 岁，增加到一定年龄则移动到老年代中&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:MaxTenuringThreshold&lt;/code&gt;：定义年龄的阈值，对象头中用 4 个 bit 存储，所以最大值是 15，默认也是 15&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;大对象直接进入老年代&lt;/strong&gt;：需要连续内存空间的对象，最典型的大对象是很长的字符串以及数组；避免在 Eden 和 Survivor 之间的大量复制；经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:PretenureSizeThreshold&lt;/code&gt;：大于此值的对象直接在老年代分配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动态对象年龄判定&lt;/strong&gt;：如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半，年龄大于等于该年龄的对象就可以直接进入老年代&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;空间分配担保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在发生 Minor GC 之前，虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间，如果条件成立的话，那么 Minor GC 可以确认是安全的&lt;/li&gt;
&lt;li&gt;如果不成立，虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败，如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于将尝试着进行一次 Minor GC；如果小于或者 HandlePromotionFailure 的值不允许冒险，那么就要进行一次 Full GC&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;回收策略&lt;/h3&gt;
&lt;h4&gt;触发条件&lt;/h4&gt;
&lt;p&gt;内存垃圾回收机制主要集中的区域就是线程共享区域：&lt;strong&gt;堆和方法区&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Minor GC 触发条件非常简单，当 Eden 空间满时，就将触发一次 Minor GC&lt;/p&gt;
&lt;p&gt;FullGC 同时回收新生代、老年代和方法区，只会存在一个 FullGC 的线程进行执行，其他的线程全部会被&lt;strong&gt;挂起&lt;/strong&gt;，有以下触发条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 System.gc()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在默认情况下，通过 System.gc() 或 Runtime.getRuntime().gc() 的调用，会显式触发 FullGC，同时对老年代和新生代进行回收，但是虚拟机不一定真正去执行，无法保证对垃圾收集器的调用&lt;/li&gt;
&lt;li&gt;不建议使用这种方式，应该让虚拟机管理内存。一般情况下，垃圾回收应该是自动进行的，无须手动触发；在一些特殊情况下，如正在编写一个性能基准，可以在运行之间调用 System.gc()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;老年代空间不足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了避免引起的 Full GC，应当尽量不要创建过大的对象以及数组&lt;/li&gt;
&lt;li&gt;通过 -Xmn 参数调整新生代的大小，让对象尽量在新生代被回收掉不进入老年代，可以通过 &lt;code&gt;-XX:MaxTenuringThreshold&lt;/code&gt; 调大对象进入老年代的年龄，让对象在新生代多存活一段时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间分配担保失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 1.7 及以前的永久代（方法区）空间不足&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Concurrent Mode Failure：执行 CMS GC 的过程中同时有对象要放入老年代，而此时老年代空间不足（可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足），便会报 Concurrent Mode Failure 错误，并触发 Full GC&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;手动 GC 测试，VM参数：&lt;code&gt;-XX:+PrintGcDetails&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void localvarGC1() {
    byte[] buffer = new byte[10 * 1024 * 1024];//10MB
    System.gc();	//输出: 不会被回收, FullGC时被放入老年代
}

public void localvarGC2() {
    byte[] buffer = new byte[10 * 1024 * 1024];
    buffer = null;
    System.gc();	//输出: 正常被回收
}
 public void localvarGC3() {
     {
         byte[] buffer = new byte[10 * 1024 * 1024];
     }
     System.gc();	//输出: 不会被回收, FullGC时被放入老年代
 }

public void localvarGC4() {
    {
        byte[] buffer = new byte[10 * 1024 * 1024];
    }
    int value = 10;
    System.gc();	//输出: 正常被回收，slot复用，局部变量过了其作用域 buffer置空
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;安全区域&lt;/h4&gt;
&lt;p&gt;安全点 (Safepoint)：程序执行时并非在所有地方都能停顿下来开始 GC，只有在安全点才能停下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Safe Point 的选择很重要，如果太少可能导致 GC 等待的时间太长，如果太多可能导致运行时的性能问题&lt;/li&gt;
&lt;li&gt;大部分指令的执行时间都非常短，通常会根据是否具有让程序长时间执行的特征为标准，选择些执行时间较长的指令作为 Safe Point， 如方法调用、循环跳转和异常跳转等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 GC 发生时，让所有线程都在最近的安全点停顿下来的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抢先式中断：没有虚拟机采用，首先中断所有线程，如果有线程不在安全点，就恢复线程让线程运行到安全点&lt;/li&gt;
&lt;li&gt;主动式中断：设置一个中断标志，各个线程运行到各个 Safe Point 时就轮询这个标志，如果中断标志为真，则将自己进行中断挂起&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题：Safepoint 保证程序执行时，在不太长的时间内就会遇到可进入 GC 的 Safepoint，但是当线程处于 Waiting 状态或 Blocked 状态，线程无法响应 JVM 的中断请求，运行到安全点去中断挂起，JVM 也不可能等待线程被唤醒，对于这种情况，需要安全区域来解决&lt;/p&gt;
&lt;p&gt;安全区域 (Safe Region)：指在一段代码片段中，&lt;strong&gt;对象的引用关系不会发生变化&lt;/strong&gt;，在这个区域中的任何位置开始 GC 都是安全的&lt;/p&gt;
&lt;p&gt;运行流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当线程运行到 Safe Region 的代码时，首先标识已经进入了 Safe Region，如果这段时间内发生 GC，JVM 会忽略标识为 Safe Region 状态的线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当线程即将离开 Safe Region 时，会检查 JVM 是否已经完成 GC，如果完成了则继续运行，否则线程必须等待 GC 完成，收到可以安全离开 SafeRegion 的信号&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;垃圾判断&lt;/h3&gt;
&lt;h4&gt;垃圾介绍&lt;/h4&gt;
&lt;p&gt;垃圾：&lt;strong&gt;如果一个或多个对象没有任何的引用指向它了，那么这个对象现在就是垃圾&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：释放没用的对象，清除内存里的记录碎片，碎片整理将所占用的堆内存移到堆的一端，以便 JVM 将整理出的内存分配给新的对象&lt;/p&gt;
&lt;p&gt;垃圾收集主要是针对堆和方法区进行，程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的，只存在于线程的生命周期内，线程结束之后就会消失，因此不需要对这三个区域进行垃圾回收&lt;/p&gt;
&lt;p&gt;在堆里存放着几乎所有的 Java 对象实例，在 GC 执行垃圾回收之前，首先需要区分出内存中哪些是存活对象，哪些是已经死亡的对象。只有被标记为己经死亡的对象，GC 才会在执行垃圾回收时，释放掉其所占用的内存空间，因此这个过程可以称为垃圾标记阶段，判断对象存活一般有两种方式：&lt;strong&gt;引用计数算法&lt;/strong&gt;和&lt;strong&gt;可达性分析算法&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;引用计数法&lt;/h4&gt;
&lt;p&gt;引用计数算法（Reference Counting）：对每个对象保存一个整型的引用计数器属性，用于记录对象被引用的情况。对于一个对象 A，只要有任何一个对象引用了 A，则 A 的引用计数器就加 1；当引用失效时，引用计数器就减 1；当对象 A 的引用计数器的值为 0，即表示对象A不可能再被使用，可进行回收（Java 没有采用）&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;回收没有延迟性，无需等到内存不够的时候才开始回收，运行时根据对象计数器是否为 0，可以直接回收&lt;/li&gt;
&lt;li&gt;在垃圾回收过程中，应用无需挂起；如果申请内存时，内存不足，则立刻报 OOM 错误&lt;/li&gt;
&lt;li&gt;区域性，更新对象的计数器时，只是影响到该对象，不会扫描全部对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每次对象被引用时，都需要去更新计数器，有一点时间开销&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浪费 CPU 资源，即使内存够用，仍然在运行时进行计数器的统计。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无法解决循环引用问题，会引发内存泄露&lt;/strong&gt;（最大的缺点）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public Object instance = null;
    public static void main(String[] args) {
        Test a = new Test();// a = 1
        Test b = new Test();// b = 1
        a.instance = b;		// b = 2
        b.instance = a;		// a = 2
        a = null;			// a = 1
        b = null;			// b = 1
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;可达性分析&lt;/h4&gt;
&lt;h5&gt;GC Roots&lt;/h5&gt;
&lt;p&gt;可达性分析算法：也可以称为根搜索算法、追踪性垃圾收集&lt;/p&gt;
&lt;p&gt;GC Roots 对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟机栈中局部变量表中引用的对象：各个线程被调用的方法中使用到的参数、局部变量等&lt;/li&gt;
&lt;li&gt;本地方法栈中引用的对象&lt;/li&gt;
&lt;li&gt;堆中类静态属性引用的对象&lt;/li&gt;
&lt;li&gt;方法区中的常量引用的对象&lt;/li&gt;
&lt;li&gt;字符串常量池（string Table）里的引用&lt;/li&gt;
&lt;li&gt;同步锁 synchronized 持有的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;GC Roots 是一组活跃的引用，不是对象&lt;/strong&gt;，放在 GC Roots Set 集合&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;工作原理&lt;/h5&gt;
&lt;p&gt;可达性分析算法以根对象集合（GCRoots）为起始点，从上至下的方式搜索被根对象集合所连接的目标对象&lt;/p&gt;
&lt;p&gt;分析工作必须在一个保障&lt;strong&gt;一致性的快照&lt;/strong&gt;中进行，否则结果的准确性无法保证，这也是导致 GC 进行时必须 Stop The World 的一个原因&lt;/p&gt;
&lt;p&gt;基本原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可达性分析算法后，内存中的存活对象都会被根对象集合直接或间接连接着，搜索走过的路径称为引用链&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果目标对象没有任何引用链相连，则是不可达的，就意味着该对象己经死亡，可以标记为垃圾对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在可达性分析算法中，只有能够被根对象集合直接或者间接连接的对象才是存活对象&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-可达性分析算法.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;三色标记&lt;/h5&gt;
&lt;h6&gt;标记算法&lt;/h6&gt;
&lt;p&gt;三色标记法把遍历对象图过程中遇到的对象，标记成以下三种颜色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白色：尚未访问过&lt;/li&gt;
&lt;li&gt;灰色：本对象已访问过，但是本对象引用到的其他对象尚未全部访问&lt;/li&gt;
&lt;li&gt;黑色：本对象已访问过，而且本对象引用到的其他对象也全部访问完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 Stop The World (STW) 时，对象间的引用是不会发生变化的，可以轻松完成标记，遍历访问过程为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始时，所有对象都在白色集合&lt;/li&gt;
&lt;li&gt;将 GC Roots 直接引用到的对象挪到灰色集合&lt;/li&gt;
&lt;li&gt;从灰色集合中获取对象：
&lt;ul&gt;
&lt;li&gt;将本对象引用到的其他对象全部挪到灰色集合中&lt;/li&gt;
&lt;li&gt;将本对象挪到黑色集合里面&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重复步骤 3，直至灰色集合为空时结束&lt;/li&gt;
&lt;li&gt;结束后，仍在白色集合的对象即为 GC Roots 不可达，可以进行回收&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-三色标记法过程.gif&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/12544c0ad5c1&lt;/p&gt;
&lt;hr /&gt;
&lt;h6&gt;并发标记&lt;/h6&gt;
&lt;p&gt;并发标记时，对象间的引用可能发生变化，多标和漏标的情况就有可能发生&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多标情况：&lt;strong&gt;当 E 变为灰色或黑色时，其他线程断开的 D 对 E 的引用，导致这部分对象仍会被标记为存活，本轮 GC 不会回收这部分内存，这部分本应该回收但是没有回收到的内存，被称之为&lt;/strong&gt;浮动垃圾&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;针对并发标记开始后的&lt;strong&gt;新对象&lt;/strong&gt;，通常的做法是直接全部当成黑色，也算浮动垃圾&lt;/li&gt;
&lt;li&gt;浮动垃圾并不会影响应用程序的正确性，只是需要等到下一轮垃圾回收中才被清除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-三色标记法多标情况.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;漏标情况：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件一：灰色对象断开了对一个白色对象的引用（直接或间接），即灰色对象原成员变量的引用发生了变化&lt;/li&gt;
&lt;li&gt;条件二：其他线程中修改了黑色对象，插入了一条或多条对该白色对象的新引用&lt;/li&gt;
&lt;li&gt;结果：导致该白色对象当作垃圾被 GC，影响到了程序的正确性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-三色标记法漏标情况.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;代码角度解释漏标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object G = objE.fieldG; // 读
objE.fieldG = null;  	// 写
objD.fieldG = G;     	// 写
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了解决问题，可以操作上面三步，&lt;strong&gt;将对象 G 记录起来，然后作为灰色对象再进行遍历&lt;/strong&gt;，比如放到一个特定的集合，等初始的 GC Roots 遍历完（并发标记），再遍历该集合（重新标记）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所以&lt;strong&gt;重新标记需要 STW&lt;/strong&gt;，应用程序一直在运行，该集合可能会一直增加新的对象，导致永远都运行不完&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决方法：添加读写屏障，读屏障拦截第一步，写屏障拦截第二三步，在读写前后进行一些后置处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写屏障 + 增量更新&lt;/strong&gt;：黑色对象新增引用，会将黑色对象变成灰色对象，最后对该节点重新扫描&lt;/p&gt;
&lt;p&gt;增量更新 (Incremental Update) 破坏了条件二，从而保证了不会漏标&lt;/p&gt;
&lt;p&gt;缺点：对黑色变灰的对象重新扫描所有引用，比较耗费时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写屏障 (Store Barrier) + SATB&lt;/strong&gt;：当原来成员变量的引用发生变化之前，记录下原来的引用对象&lt;/p&gt;
&lt;p&gt;保留 GC 开始时的对象图，即原始快照 SATB，当 GC Roots 确定后，对象图就已经确定，那后续的标记也应该是按照这个时刻的对象图走，如果期间对白色对象有了新的引用会记录下来，并且将白色对象变灰（说明可达了，并且原始快照中本来就应该是灰色对象），最后重新扫描该对象的引用关系&lt;/p&gt;
&lt;p&gt;SATB (Snapshot At The Beginning) 破坏了条件一，从而保证了不会漏标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;读屏障 (Load Barrier)&lt;/strong&gt;：破坏条件二，黑色对象引用白色对象的前提是获取到该对象，此时读屏障发挥作用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 Java HotSpot VM 为例，其并发标记时对漏标的处理方案如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS：写屏障 + 增量更新&lt;/li&gt;
&lt;li&gt;G1：写屏障 + SATB&lt;/li&gt;
&lt;li&gt;ZGC：读屏障&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;finalization&lt;/h4&gt;
&lt;p&gt;Java 语言提供了对象终止（finalization）机制来允许开发人员提供对象被销毁之前的自定义处理逻辑&lt;/p&gt;
&lt;p&gt;垃圾回收此对象之前，会先调用这个对象的 finalize() 方法，finalize() 方法允许在子类中被重写，用于在对象被回收时进行后置处理，通常在这个方法中进行一些资源释放和清理，比如关闭文件、套接字和数据库连接等&lt;/p&gt;
&lt;p&gt;生存 OR 死亡：如果从所有的根节点都无法访问到某个对象，说明对象己经不再使用，此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。&lt;strong&gt;一个无法触及的对象有可能在某个条件下复活自己&lt;/strong&gt;，所以虚拟机中的对象可能的三种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可触及的：从根节点开始，可以到达这个对象&lt;/li&gt;
&lt;li&gt;可复活的：对象的所有引用都被释放，但是对象有可能在 finalize() 中复活&lt;/li&gt;
&lt;li&gt;不可触及的：对象的 finalize() 被调用并且没有复活，那么就会进入不可触及状态，不可触及的对象不可能被复活，因为 &lt;strong&gt;finalize() 只会被调用一次&lt;/strong&gt;，等到这个对象再被标记为可回收时就必须回收&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;永远不要主动调用某个对象的 finalize() 方法，应该交给垃圾回收机制调用，原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;finalize() 时可能会导致对象复活&lt;/li&gt;
&lt;li&gt;finalize() 方法的执行时间是没有保障的，完全由 GC 线程决定，极端情况下，若不发生 GC，则 finalize() 方法将没有执行机会，因为优先级比较低，即使主动调用该方法，也不会因此就直接进行回收&lt;/li&gt;
&lt;li&gt;一个糟糕的 finalize() 会严重影响 GC 的性能&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;引用分析&lt;/h4&gt;
&lt;p&gt;无论是通过引用计数算法判断对象的引用数量，还是通过可达性分析算法判断对象是否可达，判定对象是否可被回收都与引用有关，Java 提供了四种强度不同的引用类型&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;强引用：被强引用关联的对象不会被回收，只有所有 GCRoots 都不通过强引用引用该对象，才能被垃圾回收&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;强引用可以直接访问目标对象&lt;/li&gt;
&lt;li&gt;虚拟机宁愿抛出 OOM 异常，也不会回收强引用所指向对象&lt;/li&gt;
&lt;li&gt;强引用可能导致&lt;strong&gt;内存泄漏&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Object obj = new Object();//使用 new 一个新对象的方式来创建强引用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软引用（SoftReference）：被软引用关联的对象只有在内存不够的情况下才会被回收&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**仅（可能有强引用，一个对象可以被多个引用）**有软引用引用该对象时，在垃圾回收后，内存仍不足时会再次出发垃圾回收，回收软引用对象&lt;/li&gt;
&lt;li&gt;配合&lt;strong&gt;引用队列来释放软引用自身&lt;/strong&gt;，在构造软引用时，可以指定一个引用队列，当软引用对象被回收时，就会加入指定的引用队列，通过这个队列可以跟踪对象的回收情况&lt;/li&gt;
&lt;li&gt;软引用通常用来实现内存敏感的缓存，比如高速缓存就有用到软引用；如果还有空闲内存，就可以暂时保留缓存，当内存不足时清理掉，这样就保证了使用缓存的同时不会耗尽内存&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Object obj = new Object();
SoftReference&amp;lt;Object&amp;gt; sf = new SoftReference&amp;lt;Object&amp;gt;(obj);
obj = null;  // 使对象只被软引用关联
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弱引用（WeakReference）：被弱引用关联的对象一定会被回收，只能存活到下一次垃圾回收发生之前&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅有弱引用引用该对象时，在垃圾回收时，无论内存是否充足，都会回收弱引用对象&lt;/li&gt;
&lt;li&gt;配合引用队列来释放弱引用自身&lt;/li&gt;
&lt;li&gt;WeakHashMap 用来存储图片信息，可以在内存不足的时候及时回收，避免了 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Object obj = new Object();
WeakReference&amp;lt;Object&amp;gt; wf = new WeakReference&amp;lt;Object&amp;gt;(obj);
obj = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚引用（PhantomReference）：也称为幽灵引用或者幻影引用，是所有引用类型中最弱的一个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个对象是否有虚引用的存在，不会对其生存时间造成影响，也无法通过虚引用得到一个对象&lt;/li&gt;
&lt;li&gt;为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程，能在这个对象被回收时收到一个系统通知&lt;/li&gt;
&lt;li&gt;必须配合引用队列使用，主要配合 ByteBuffer 使用，被引用对象回收时会将虚引用入队，由 Reference Handler 线程调用虚引用相关方法释放直接内存&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Object obj = new Object();
PhantomReference&amp;lt;Object&amp;gt; pf = new PhantomReference&amp;lt;Object&amp;gt;(obj, null);
obj = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终结器引用（finalization）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;无用属性&lt;/h4&gt;
&lt;h5&gt;无用类&lt;/h5&gt;
&lt;p&gt;方法区主要回收的是无用的类&lt;/p&gt;
&lt;p&gt;判定一个类是否是无用的类，需要同时满足下面 3 个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该类所有的实例都已经被回收，也就是 Java 堆中不存在该类的任何实例&lt;/li&gt;
&lt;li&gt;加载该类的 &lt;code&gt;ClassLoader&lt;/code&gt; 已经被回收&lt;/li&gt;
&lt;li&gt;该类对应的 &lt;code&gt;java.lang.Class&lt;/code&gt; 对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚拟机可以对满足上述 3 个条件的无用类进行回收，这里说的&lt;strong&gt;仅仅是可以&lt;/strong&gt;，而并不是和对象一样不使用了就会必然被回收&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;废弃常量&lt;/h5&gt;
&lt;p&gt;在常量池中存在字符串 &quot;abc&quot;，如果当前没有任何 String 对象引用该常量，说明常量 &quot;abc&quot; 是废弃常量，如果这时发生内存回收的话&lt;strong&gt;而且有必要的话&lt;/strong&gt;（内存不够用），&quot;abc&quot; 就会被系统清理出常量池&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;静态变量&lt;/h5&gt;
&lt;p&gt;类加载时（第一次访问），这个类中所有静态成员就会被加载到静态变量区，该区域的成员一旦创建，直到程序退出才会被回收&lt;/p&gt;
&lt;p&gt;如果是静态引用类型的变量，静态变量区只存储一份对象的引用地址，真正的对象在堆内，如果要回收该对象可以设置引用为 null&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/zhengzhb/article/details/7331354&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;回收算法&lt;/h3&gt;
&lt;h4&gt;复制算法&lt;/h4&gt;
&lt;p&gt;复制算法的核心就是，&lt;strong&gt;将原有的内存空间一分为二，每次只用其中的一块&lt;/strong&gt;，在垃圾回收时，将正在使用的对象复制到另一个内存空间中，然后将该内存空间清理，交换两个内存的角色，完成垃圾的回收&lt;/p&gt;
&lt;p&gt;应用场景：如果内存中的垃圾对象较多，需要复制的对象就较少，这种情况下适合使用该方式并且效率比较高，反之则不适合&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;算法优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有标记和清除过程，实现简单，运行速度快&lt;/li&gt;
&lt;li&gt;复制过去以后保证空间的连续性，不会出现碎片问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主要不足是&lt;strong&gt;只使用了内存的一半&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;对于 G1 这种分拆成为大量 region 的 GC，复制而不是移动，意味着 GC 需要维护 region 之间对象引用关系，不管是内存占用或者时间开销都不小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在的商业虚拟机都采用这种收集算法&lt;strong&gt;回收新生代&lt;/strong&gt;，因为新生代 GC 频繁并且对象的存活率不高，但是并不是划分为大小相等的两块，而是一块较大的 Eden 空间和两块较小的 Survivor 空间&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;标记清除&lt;/h4&gt;
&lt;p&gt;标记清除算法，是将垃圾回收分为两个阶段，分别是&lt;strong&gt;标记和清除&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标记&lt;/strong&gt;：Collector 从引用根节点开始遍历，标记所有被引用的对象，一般是在对象的 Header 中记录为可达对象，&lt;strong&gt;标记的是引用的对象，不是垃圾&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;清除&lt;/strong&gt;：Collector 对堆内存从头到尾进行线性的遍历，如果发现某个对象在其 Header 中没有标记为可达对象，则将其回收，把分块连接到&lt;strong&gt;空闲列表&lt;/strong&gt;的单向链表，判断回收后的分块与前一个空闲分块是否连续，若连续会合并这两个分块，之后进行分配时只需要遍历这个空闲列表，就可以找到分块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分配阶段&lt;/strong&gt;：程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block，如果找到的块等于 size，会直接返回这个分块；如果找到的块大于 size，会将块分割成大小为 size 与 block - size 的两部分，返回大小为 size 的分块，并把大小为 block - size 的块返回给空闲列表&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标记和清除过程效率都不高&lt;/li&gt;
&lt;li&gt;会产生大量不连续的内存碎片，导致无法给大对象分配内存，需要维护一个空闲链表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-标记清除算法.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;标记整理&lt;/h4&gt;
&lt;p&gt;标记整理（压缩）算法是在标记清除算法的基础之上，做了优化改进的算法&lt;/p&gt;
&lt;p&gt;标记阶段和标记清除算法一样，也是从根节点开始，对对象的引用进行标记，在清理阶段，并不是简单的直接清理可回收对象，而是&lt;strong&gt;将存活对象都向内存另一端移动&lt;/strong&gt;，然后清理边界以外的垃圾，从而&lt;strong&gt;解决了碎片化&lt;/strong&gt;的问题&lt;/p&gt;
&lt;p&gt;优点：不会产生内存碎片&lt;/p&gt;
&lt;p&gt;缺点：需要移动大量对象，处理效率比较低&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-标记整理算法.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mark-Sweep&lt;/th&gt;
&lt;th&gt;Mark-Compact&lt;/th&gt;
&lt;th&gt;Copying&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;最慢&lt;/td&gt;
&lt;td&gt;最快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间开销&lt;/td&gt;
&lt;td&gt;少（但会堆积碎片）&lt;/td&gt;
&lt;td&gt;少（不堆积碎片）&lt;/td&gt;
&lt;td&gt;通常需要活对象的 2 倍大小（不堆积碎片）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移动对象&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;垃圾回收器&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;垃圾收集器分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按线程数分（垃圾回收线程数），可以分为串行垃圾回收器和并行垃圾回收器
&lt;ul&gt;
&lt;li&gt;除了 CMS 和 G1 之外，其它垃圾收集器都是以串行的方式执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按照工作模式分，可以分为并发式垃圾回收器和独占式垃圾回收器
&lt;ul&gt;
&lt;li&gt;并发式垃圾回收器与应用程序线程交替工作，以尽可能减少应用程序的停顿时间&lt;/li&gt;
&lt;li&gt;独占式垃圾回收器（Stop the world）一旦运行，就停止应用程序中的所有用户线程，直到垃圾回收过程完全结束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按碎片处理方式分，可分为压缩式垃圾回收器和非压缩式垃圾回收器
&lt;ul&gt;
&lt;li&gt;压缩式垃圾回收器在回收完成后进行压缩整理，消除回收后的碎片，再分配对象空间使用指针碰撞&lt;/li&gt;
&lt;li&gt;非压缩式的垃圾回收器不进行这步操作，再分配对象空间使用空闲列表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按工作的内存区间分，又可分为年轻代垃圾回收器和老年代垃圾回收器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GC 性能指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;吞吐量&lt;/strong&gt;：程序的运行时间占总运行时间的比例（总运行时间 = 程序的运行时间 + 内存回收的时间）&lt;/li&gt;
&lt;li&gt;垃圾收集开销：吞吐量的补数，垃圾收集所用时间与总运行时间的比例&lt;/li&gt;
&lt;li&gt;暂停时间：执行垃圾收集时，程序的工作线程被暂停的时间&lt;/li&gt;
&lt;li&gt;收集频率：相对于应用程序的执行，收集操作发生的频率&lt;/li&gt;
&lt;li&gt;内存占用：Java 堆区所占的内存大小&lt;/li&gt;
&lt;li&gt;快速：一个对象从诞生到被回收所经历的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;垃圾收集器的组合关系&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E5%85%B3%E7%B3%BB%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;新生代收集器：Serial、ParNew、Parallel Scavenge&lt;/p&gt;
&lt;p&gt;老年代收集器：Serial old、Parallel old、CMS&lt;/p&gt;
&lt;p&gt;整堆收集器：G1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查看默认的垃圾收回收器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:+PrintcommandLineFlags&lt;/code&gt;：查看命令行相关参数（包含使用的垃圾收集器）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用命令行指令：jinfo -flag 相关垃圾回收器参数  进程 ID&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Serial&lt;/h4&gt;
&lt;p&gt;Serial：串行垃圾收集器，作用于新生代，是指使用单线程进行垃圾回收，采用&lt;strong&gt;复制算法&lt;/strong&gt;，新生代基本都是复制算法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;STW（Stop-The-World）&lt;/strong&gt;：垃圾回收时，只有一个线程在工作，并且 Java 应用中的所有线程都要暂停，等待垃圾回收的完成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Serial old&lt;/strong&gt;：执行老年代垃圾回收的串行收集器，内存回收算法使用的是&lt;strong&gt;标记-整理算法&lt;/strong&gt;，同样也采用了串行回收和 STW 机制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Serial old 是 Client 模式下默认的老年代的垃圾回收器&lt;/li&gt;
&lt;li&gt;Serial old 在 Server 模式下主要有两个用途：
&lt;ul&gt;
&lt;li&gt;在 JDK 1.5 以及之前版本（Parallel Old 诞生以前）中与 Parallel Scavenge 收集器搭配使用&lt;/li&gt;
&lt;li&gt;作为老年代 CMS 收集器的&lt;strong&gt;后备垃圾回收方案&lt;/strong&gt;，在并发收集发生 Concurrent Mode Failure 时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开启参数：&lt;code&gt;-XX:+UseSerialGC&lt;/code&gt; 等价于新生代用 Serial GC 且老年代用 Serial old GC&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Serial%E6%94%B6%E9%9B%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优点：简单而高效（与其他收集器的单线程比），对于限定单个 CPU 的环境来说，Serial 收集器由于没有线程交互的开销，可以获得最高的单线程收集效率&lt;/p&gt;
&lt;p&gt;缺点：对于交互性较强的应用而言，这种垃圾收集器是不能够接受的，比如 JavaWeb 应用&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;ParNew&lt;/h4&gt;
&lt;p&gt;Par 是 Parallel 并行的缩写，New 是只能处理的是新生代&lt;/p&gt;
&lt;p&gt;并行垃圾收集器在串行垃圾收集器的基础之上做了改进，&lt;strong&gt;采用复制算法&lt;/strong&gt;，将单线程改为了多线程进行垃圾回收，可以缩短垃圾回收的时间&lt;/p&gt;
&lt;p&gt;对于其他的行为（收集算法、stop the world、对象分配规则、回收策略等）同 Serial 收集器一样，应用在年轻代，除 Serial 外，只有&lt;strong&gt;ParNew GC 能与 CMS 收集器配合工作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;相关参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX：+UseParNewGC&lt;/code&gt;：表示年轻代使用并行收集器，不影响老年代&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:ParallelGCThreads&lt;/code&gt;：默认开启和 CPU 数量相同的线程数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew%E6%94%B6%E9%9B%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于新生代，回收次数频繁，使用并行方式高效&lt;/li&gt;
&lt;li&gt;对于老年代，回收次数少，使用串行方式节省资源（CPU 并行需要切换线程，串行可以省去切换线程的资源）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Parallel&lt;/h4&gt;
&lt;p&gt;Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器，&lt;strong&gt;采用复制算法&lt;/strong&gt;、并行回收和 Stop the World 机制&lt;/p&gt;
&lt;p&gt;Parallel Old 收集器：是一个应用于老年代的并行垃圾回收器，&lt;strong&gt;采用标记-整理算法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对比其他回收器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间&lt;/li&gt;
&lt;li&gt;Parallel 目标是达到一个可控制的吞吐量，被称为&lt;strong&gt;吞吐量优先&lt;/strong&gt;收集器&lt;/li&gt;
&lt;li&gt;Parallel Scavenge 对比 ParNew 拥有&lt;strong&gt;自适应调节策略&lt;/strong&gt;，可以通过一个开关参数打开 GC Ergonomics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停顿时间越短就越适合需要与用户交互的程序，良好的响应速度能提升用户体验&lt;/li&gt;
&lt;li&gt;高吞吐量可以高效率地利用 CPU 时间，尽快完成程序的运算任务，适合在后台运算而不需要太多交互&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;停顿时间和吞吐量的关系：新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降&lt;/p&gt;
&lt;p&gt;在注重吞吐量及 CPU 资源敏感的场合，都可以优先考虑 Parallel Scavenge + Parallel Old 收集器，在 Server 模式下的内存回收性能很好，&lt;strong&gt;Java8 默认是此垃圾收集器组合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParallelScavenge%E6%94%B6%E9%9B%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参数配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-XX：+UseParallelGC&lt;/code&gt;：手动指定年轻代使用 Paralle 并行收集器执行内存回收任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX：+UseParalleloldcc&lt;/code&gt;：手动指定老年代使用并行回收收集器执行内存回收任务
&lt;ul&gt;
&lt;li&gt;上面两个参数，默认开启一个，另一个也会被开启（互相激活），默认 JDK8 是开启的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:+UseAdaptivesizepplicy&lt;/code&gt;：设置 Parallel Scavenge 收集器具有&lt;strong&gt;自适应调节策略&lt;/strong&gt;，在这种模式下，年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:ParallelGcrhreads&lt;/code&gt;：设置年轻代并行收集器的线程数，一般与 CPU 数量相等，以避免过多的线程数影响垃圾收集性能
&lt;ul&gt;
&lt;li&gt;在默认情况下，当 CPU 数量小于 8 个，ParallelGcThreads 的值等于 CPU 数量&lt;/li&gt;
&lt;li&gt;当 CPU 数量大于 8 个，ParallelGCThreads 的值等于 3+[5*CPU Count]/8]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:MaxGCPauseMillis&lt;/code&gt;：设置垃圾收集器最大停顿时间（即 STW 的时间），单位是毫秒
&lt;ul&gt;
&lt;li&gt;对于用户来讲，停顿时间越短体验越好；在服务器端，注重高并发，整体的吞吐量&lt;/li&gt;
&lt;li&gt;为了把停顿时间控制在 MaxGCPauseMillis 以内，收集器在工作时会调整 Java 堆大小或其他一些参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:GCTimeRatio&lt;/code&gt;：垃圾收集时间占总时间的比例 =1/(N+1)，用于衡量吞吐量的大小
&lt;ul&gt;
&lt;li&gt;取值范围（0，100）。默认值 99，也就是垃圾回收时间不超过 1&lt;/li&gt;
&lt;li&gt;与 &lt;code&gt;-xx:MaxGCPauseMillis&lt;/code&gt; 参数有一定矛盾性，暂停时间越长，Radio 参数就容易超过设定的比例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;CMS&lt;/h4&gt;
&lt;p&gt;CMS 全称 Concurrent Mark Sweep，是一款&lt;strong&gt;并发的、使用标记-清除&lt;/strong&gt;算法、针对老年代的垃圾回收器，其最大特点是&lt;strong&gt;让垃圾收集线程与用户线程同时工作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间，停顿时间越短（&lt;strong&gt;低延迟&lt;/strong&gt;）越适合与用户交互的程序，良好的响应速度能提升用户体验&lt;/p&gt;
&lt;p&gt;分为以下四个流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始标记：使用 STW 出现短暂停顿，仅标记一下 GC Roots 能直接关联到的对象，速度很快&lt;/li&gt;
&lt;li&gt;并发标记：进行 GC Roots 开始遍历整个对象图，在整个回收过程中耗时最长，不需要 STW，可以与用户线程并发运行&lt;/li&gt;
&lt;li&gt;重新标记：修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象，比初始标记时间长但远比并发标记时间短，需要 STW（不停顿就会一直变化，采用写屏障 + 增量更新来避免漏标情况）&lt;/li&gt;
&lt;li&gt;并发清除：清除标记为可以回收对象，&lt;strong&gt;不需要移动存活对象&lt;/strong&gt;，所以这个阶段可以与用户线程同时并发的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mark Sweep 会造成内存碎片，不把算法换成 Mark Compact 的原因：Mark Compact 算法会整理内存，导致用户线程使用的&lt;strong&gt;对象的地址改变&lt;/strong&gt;，影响用户线程继续执行&lt;/p&gt;
&lt;p&gt;在整个过程中耗时最长的并发标记和并发清除过程中，收集器线程都可以与用户线程一起工作，不需要进行停顿&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-CMS%E6%94%B6%E9%9B%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优点：并发收集、低延迟&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;吞吐量降低：在并发阶段虽然不会导致用户停顿，但是会因为占用了一部分线程而导致应用程序变慢，CPU 利用率不够高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CMS 收集器&lt;strong&gt;无法处理浮动垃圾&lt;/strong&gt;，可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生&lt;/p&gt;
&lt;p&gt;浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾（产生了新对象），这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在，CMS 收集需要预留出一部分内存，不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾，就会出现 Concurrent Mode Failure，这时虚拟机将临时启用 Serial Old 来替代 CMS，导致很长的停顿时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标记 - 清除算法导致的空间碎片，往往出现老年代空间无法找到足够大连续空间来分配当前对象，不得不提前触发一次 Full GC；为新对象分配内存空间时，将无法使用指针碰撞（Bump the Pointer）技术，而只能够选择空闲列表（Free List）执行内存分配&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参数设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX：+UseConcMarkSweepGC&lt;/code&gt;：手动指定使用 CMS 收集器执行内存回收任务&lt;/p&gt;
&lt;p&gt;开启该参数后会自动将 &lt;code&gt;-XX:+UseParNewGC&lt;/code&gt; 打开，即：ParNew + CMS + Serial old的组合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:CMSInitiatingoccupanyFraction&lt;/code&gt;：设置堆内存使用率的阈值，一旦达到该阈值，便开始进行回收&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK5 及以前版本的默认值为 68，即当老年代的空间使用率达到 68% 时，会执行一次CMS回收&lt;/li&gt;
&lt;li&gt;JDK6 及以上版本默认值为 92%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:+UseCMSCompactAtFullCollection&lt;/code&gt;：用于指定在执行完 Full GC 后对内存空间进行压缩整理，以此避免内存碎片的产生，由于内存压缩整理过程无法并发执行，所带来的问题就是停顿时间变得更长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:CMSFullGCsBeforecompaction&lt;/code&gt;：&lt;strong&gt;设置在执行多少次 Full GC 后对内存空间进行压缩整理&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:ParallelCMSThreads&lt;/code&gt;：设置 CMS 的线程数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS 默认启动的线程数是 (ParallelGCThreads+3)/4，ParallelGCThreads 是年轻代并行收集器的线程数&lt;/li&gt;
&lt;li&gt;收集线程占用的 CPU 资源多于25%，对用户程序影响可能较大；当 CPU 资源比较紧张时，受到 CMS 收集器线程的影响，应用程序的性能在垃圾回收阶段可能会非常糟糕&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;G1&lt;/h4&gt;
&lt;h5&gt;G1 特点&lt;/h5&gt;
&lt;p&gt;G1（Garbage-First）是一款面向服务端应用的垃圾收集器，&lt;strong&gt;应用于新生代和老年代&lt;/strong&gt;、采用标记-整理算法、软实时、低延迟、可设定目标（最大 STW 停顿时间）的垃圾回收器，用于代替 CMS，适用于较大的堆（&amp;gt;4 ~ 6G），在 JDK9 之后默认使用 G1&lt;/p&gt;
&lt;p&gt;G1 对比其他处理器的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;并发与并行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并行性：G1 在回收期间，可以有多个 GC 线程同时工作，有效利用多核计算能力，此时用户线程 STW&lt;/li&gt;
&lt;li&gt;并发性：G1 拥有与应用程序交替执行的能力，部分工作可以和应用程序同时执行，因此不会在整个回收阶段发生完全阻塞应用程序的情况&lt;/li&gt;
&lt;li&gt;其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作，而 G1 GC 可以采用应用线程承担后台运行的 GC 工作，JVM 的 GC 线程处理速度慢时，系统会&lt;strong&gt;调用应用程序线程加速垃圾回收&lt;/strong&gt;过程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分区算法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从分代上看，G1  属于分代型垃圾回收器，区分年轻代和老年代，年轻代依然有 Eden 区和 Survivor 区。从堆结构上看，&lt;strong&gt;新生代和老年代不再物理隔离&lt;/strong&gt;，不用担心每个代内存是否足够，这种特性有利于程序长时间运行，分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将整个堆划分成约 2048 个大小相同的独立 Region 块，每个 Region 块大小根据堆空间的实际大小而定，整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂，所有 Region 大小相同，在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域，使得每个小空间可以单独进行垃圾回收&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;新的区域 Humongous&lt;/strong&gt;：本身属于老年代区，当出现了一个巨型对象超出了分区容量的一半，该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象，那么 G1 会寻找连续的 H 分区来存储，为了能找到连续的 H 区，有时候不得不启动 Full GC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;G1 不会对巨型对象进行拷贝，回收时被优先考虑，G1 会跟踪老年代所有 incoming 引用，这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Region 结构图：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1-Region%E5%8C%BA%E5%9F%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;空间整合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS：标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理&lt;/li&gt;
&lt;li&gt;G1：整体来看是&lt;strong&gt;基于标记 - 整理算法实现&lt;/strong&gt;的收集器，从局部（Region 之间）上来看是基于复制算法实现的，两种算法都可以避免内存碎片&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可预测的停顿时间模型（软实时 soft real-time）&lt;/strong&gt;：可以指定在 M 毫秒的时间片段内，消耗在 GC 上的时间不得超过 N 毫秒&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于分块的原因，G1 可以只选取部分区域进行内存回收，这样缩小了回收的范围，对于全局停顿情况也能得到较好的控制&lt;/li&gt;
&lt;li&gt;G1 跟踪各个 Region 里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间，通过过去回收的经验获得），在后台维护一个&lt;strong&gt;优先列表&lt;/strong&gt;，每次根据允许的收集时间优先回收价值最大的 Region，保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;相比于 CMS GC，G1 未必能做到 CMS 在最好情况下的延时停顿，但是最差情况要好很多&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;G1 垃圾收集器的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相较于 CMS，G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中，G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高&lt;/li&gt;
&lt;li&gt;从经验上来说，在小内存应用上 CMS 的表现大概率会优于 G1，而 G1 在大内存应用上则发挥其优势，平衡点在 6-8GB 之间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面向服务端应用，针对具有大内存、多处理器的机器&lt;/li&gt;
&lt;li&gt;需要低 GC 延迟，并具有大堆的应用程序提供解决方案&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;记忆集&lt;/h5&gt;
&lt;p&gt;记忆集 Remembered Set 在新生代中，每个 Region 都有一个 Remembered Set，用来被哪些其他 Region 里的对象引用（谁引用了我就记录谁）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-G1记忆集.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序对 Reference 类型数据写操作时，产生一个 Write Barrier 暂时中断操作，检查该对象和 Reference 类型数据是否在不同的 Region（跨代引用），不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中&lt;/li&gt;
&lt;li&gt;进行内存回收时，在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;垃圾收集器在新生代中建立了记忆集这样的数据结构，可以理解为它是一个抽象类，具体实现记忆集的三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字长精度&lt;/li&gt;
&lt;li&gt;对象精度&lt;/li&gt;
&lt;li&gt;卡精度(卡表)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;卡表（Card Table）在老年代中，是一种对记忆集的具体实现，主要定义了记忆集的记录精度、与堆内存的映射关系等，卡表中的每一个元素都对应着一块特定大小的内存块，这个内存块称之为卡页（card page），当存在跨代引用时，会将卡页标记为 dirty，JVM 对于卡页的维护也是通过写屏障的方式&lt;/p&gt;
&lt;p&gt;收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区，在任意一次收集暂停中，CSet 所有分区都会被释放，内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区，而混合收集会通过启发式算法，在老年代候选回收分区中，筛选出回收收益最高的分区添加到 CSet 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSet of Young Collection&lt;/li&gt;
&lt;li&gt;CSet of Mix Collection&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;工作原理&lt;/h5&gt;
&lt;p&gt;G1 中提供了三种垃圾回收模式：YoungGC、Mixed GC 和 Full GC，在不同的条件下被触发&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当堆内存使用达到一定值（默认 45%）时，开始老年代并发标记过程&lt;/li&gt;
&lt;li&gt;标记完成马上开始混合回收过程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1回收过程.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;顺时针：Young GC → Young GC + Concurrent Mark → Mixed GC 顺序，进行垃圾回收&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Young GC&lt;/strong&gt;：发生在年轻代的 GC 算法，一般对象（除了巨型对象）都是在 eden region 中分配内存，当所有 eden region 被耗尽无法申请内存时，就会触发一次 Young GC，G1 停止应用程序的执行 STW，把活跃对象放入老年代，垃圾对象回收&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回收过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;扫描根：根引用连同 RSet 记录的外部引用作为扫描存活对象的入口&lt;/li&gt;
&lt;li&gt;更新 RSet：处理 dirty card queue 更新 RS，此后 RSet 准确的反映对象的引用关系
&lt;ul&gt;
&lt;li&gt;dirty card queue：类似缓存，产生了引用先记录在这里，然后更新到 RSet&lt;/li&gt;
&lt;li&gt;作用：产生引用直接更新 RSet 需要线程同步开销很大，使用队列性能好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理 RSet：识别被老年代对象指向的 Eden 中的对象，这些被指向的对象被认为是存活的对象，把需要回收的分区放入 Young CSet 中进行回收&lt;/li&gt;
&lt;li&gt;复制对象：Eden 区内存段中存活的对象会被复制到 survivor 区，survivor 区内存段中存活的对象如果年龄未达阈值，年龄会加1，达到阀值会被会被复制到 old 区中空的内存分段，如果 survivor 空间不够，Eden 空间的部分数据会直接晋升到老年代空间&lt;/li&gt;
&lt;li&gt;处理引用：处理 Soft，Weak，Phantom，JNI Weak  等引用，最终 Eden 空间的数据为空，GC 停止工作&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**Concurrent Mark **：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始标记：标记从根节点直接可达的对象，这个阶段是 STW 的，并且会触发一次年轻代 GC&lt;/li&gt;
&lt;li&gt;并发标记 (Concurrent Marking)：在整个堆中进行并发标记（应用程序并发执行），可能被 YoungGC 中断。会计算每个区域的对象活性，即区域中存活对象的比例，若区域中的所有对象都是垃圾，则这个区域会被立即回收（&lt;strong&gt;实时回收&lt;/strong&gt;），给浮动垃圾准备出更多的空间，把需要收集的 Region 放入 CSet 当中&lt;/li&gt;
&lt;li&gt;最终标记：为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录，虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面，最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中，这阶段需要停顿线程，但是可并行执行（&lt;strong&gt;防止漏标&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;筛选回收：并发清理阶段，首先对 CSet 中各个 Region 中的回收价值和成本进行排序，根据用户所期望的 GC 停顿时间来制定回收计划，也需要 STW&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1%E6%94%B6%E9%9B%86%E5%99%A8.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mixed GC&lt;/strong&gt;：当很多对象晋升到老年代时，为了避免堆内存被耗尽，虚拟机会触发一个混合的垃圾收集器，即 Mixed GC，除了回收整个 young region，还会回收一部分的 old region，过程同 YGC&lt;/p&gt;
&lt;p&gt;注意：&lt;strong&gt;是一部分老年代，而不是全部老年代&lt;/strong&gt;，可以选择哪些老年代 region 收集，对垃圾回收的时间进行控制&lt;/p&gt;
&lt;p&gt;在 G1 中，Mixed GC 可以通过 &lt;code&gt;-XX:InitiatingHeapOccupancyPercent&lt;/code&gt; 设置阈值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Full GC&lt;/strong&gt;：对象内存分配速度过快，Mixed GC 来不及回收，导致老年代被填满，就会触发一次 Full GC，G1 的 Full GC 算法就是单线程执行的垃圾回收，会导致异常长时间的暂停时间，需要进行不断的调优，尽可能的避免 Full GC&lt;/p&gt;
&lt;p&gt;产生 Full GC 的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;晋升时没有足够的空间存放晋升的对象&lt;/li&gt;
&lt;li&gt;并发处理过程完成之前空间耗尽，浮动垃圾&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;相关参数&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-XX:+UseG1GC&lt;/code&gt;：手动指定使用 G1 垃圾收集器执行内存回收任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:G1HeapRegionSize&lt;/code&gt;：设置每个 Region 的大小。值是 2 的幂，范围是 1MB 到 32MB 之间，目标是根据最小的 Java 堆大小划分出约 2048 个区域，默认是堆内存的 1/2000&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:MaxGCPauseMillis&lt;/code&gt;：设置期望达到的最大 GC 停顿时间指标，JVM会尽力实现，但不保证达到，默认值是 200ms&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:+ParallelGcThread&lt;/code&gt;：设置 STW 时 GC 线程数的值，最多设置为 8&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:ConcGCThreads&lt;/code&gt;：设置并发标记线程数，设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:InitiatingHeapoccupancyPercent&lt;/code&gt;：设置触发并发 Mixed GC 周期的 Java 堆占用率阈值，超过此值，就触发 GC，默认值是 45&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:+ClassUnloadingWithConcurrentMark&lt;/code&gt;：并发标记类卸载，默认启用，所有对象都经过并发标记后，就可以知道哪些类不再被使用，当一个类加载器的所有类都不再使用，则卸载它所加载的所有类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:G1NewSizePercent&lt;/code&gt;：新生代占用整个堆内存的最小百分比（默认5％）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:G1MaxNewSizePercent&lt;/code&gt;：新生代占用整个堆内存的最大百分比（默认60％）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:G1ReservePercent=10&lt;/code&gt;：保留内存区域，防止 to space（Survivor中的 to 区）溢出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;调优&lt;/h5&gt;
&lt;p&gt;G1 的设计原则就是简化 JVM 性能调优，只需要简单的三步即可完成调优：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开启 G1 垃圾收集器&lt;/li&gt;
&lt;li&gt;设置堆的最大内存&lt;/li&gt;
&lt;li&gt;设置最大的停顿时间（STW）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不断调优暂停时间指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XX:MaxGCPauseMillis=x&lt;/code&gt; 可以设置启动应用程序暂停的时间，G1会根据这个参数选择 CSet 来满足响应时间的设置&lt;/li&gt;
&lt;li&gt;设置到 100ms 或者 200ms 都可以（不同情况下会不一样），但设置成50ms就不太合理&lt;/li&gt;
&lt;li&gt;暂停时间设置的太短，就会导致出现 G1 跟不上垃圾产生的速度，最终退化成 Full GC&lt;/li&gt;
&lt;li&gt;对这个参数的调优是一个持续的过程，逐步调整到最佳状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要设置新生代和老年代的大小：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小，G1 收集器在运行的时候会调整新生代和老年代的大小，从而达到我们为收集器设置的暂停时间目标&lt;/li&gt;
&lt;li&gt;设置了新生代大小相当于放弃了 G1 的自动调优，我们只需要设置整个堆内存的大小，剩下的交给 G1 自己去分配各个代的大小&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;ZGC&lt;/h4&gt;
&lt;p&gt;ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器，基于 Region 内存布局的，不设分代，使用了读屏障、染色指针和内存多重映射等技术来实现&lt;strong&gt;可并发的标记压缩算法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 CMS 和 G1 中都用到了写屏障，而 ZGC 用到了读屏障&lt;/li&gt;
&lt;li&gt;染色指针：直接&lt;strong&gt;将少量额外的信息存储在指针上的技术&lt;/strong&gt;，从 64 位的指针中拿高 4 位来标识对象此时的状态
&lt;ul&gt;
&lt;li&gt;染色指针可以使某个 Region 的存活对象被移走之后，这个 Region 立即就能够被释放和重用&lt;/li&gt;
&lt;li&gt;可以直接从指针中看到引用对象的三色标记状态（Marked0、Marked1）、是否进入了重分配集、是否被移动过（Remapped）、是否只能通过 finalize() 方法才能被访问到（Finalizable）&lt;/li&gt;
&lt;li&gt;可以大幅减少在垃圾收集过程中内存屏障的使用数量，写屏障的目的通常是为了记录对象引用的变动情况，如果将这些信息直接维护在指针中，显然就可以省去一些专门的记录操作&lt;/li&gt;
&lt;li&gt;可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内存多重映射：多个虚拟地址指向同一个物理地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可并发的标记压缩算法：染色指针标识对象是否被标记或移动，读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动，如果被移动就根据转发表访问新的移动对象，&lt;strong&gt;并更新引用&lt;/strong&gt;，不会像 G1 一样必须等待垃圾回收完成才能访问&lt;/p&gt;
&lt;p&gt;ZGC 目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停顿时间不会超过 10ms&lt;/li&gt;
&lt;li&gt;停顿时间不会随着堆的增大而增大（不管多大的堆都能保持在 10ms 以下）&lt;/li&gt;
&lt;li&gt;可支持几百 M，甚至几 T 的堆大小（最大支持4T）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ZGC 的工作过程可以分为 4 个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发标记（Concurrent Mark）： 遍历对象图做可达性分析的阶段，也要经过初始标记和最终标记，需要短暂停顿&lt;/li&gt;
&lt;li&gt;并发预备重分配（Concurrent Prepare for Relocate）：根据特定的查询条件统计得出本次收集过程要清理哪些 Region，将这些 Region 组成重分配集（Relocation Set）&lt;/li&gt;
&lt;li&gt;并发重分配（Concurrent Relocate）： 重分配是 ZGC 执行过程中的核心阶段，这个过程要把重分配集中的存活对象复制到新的 Region 上，并为重分配集中的&lt;strong&gt;每个 Region 维护一个转发表&lt;/strong&gt;（Forward Table），记录从旧地址到新地址的转向关系&lt;/li&gt;
&lt;li&gt;并发重映射（Concurrent Remap）：修正整个堆中指向重分配集中旧对象的所有引用，ZGC 的并发映射并不是一个必须要立即完成的任务，ZGC 很巧妙地把并发重映射阶段要做的工作，合并到下一次垃圾收集循环中的并发标记阶段里去完成，因为都是要遍历所有对象，这样合并节省了一次遍历的开销&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ZGC 几乎在所有地方并发执行的，除了初始标记的是 STW 的，但这部分的实际时间是非常少的，所以响应速度快，在尽可能对吞吐量影响不大的前提下，实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟&lt;/p&gt;
&lt;p&gt;优点：高吞吐量、低延迟&lt;/p&gt;
&lt;p&gt;缺点：浮动垃圾，当 ZGC 准备要对一个很大的堆做一次完整的并发收集，其全过程要持续十分钟以上，由于应用的对象分配速率很高，将创造大量的新对象产生浮动垃圾&lt;/p&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/jimoer/p/13170249.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC  不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最小化地使用内存和并行开销，选 Serial GC&lt;/li&gt;
&lt;li&gt;最大化应用程序的吞吐量，选 Parallel GC&lt;/li&gt;
&lt;li&gt;最小化 GC 的中断或停顿时间，选 CMS GC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E6%80%BB%E7%BB%93.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;内存泄漏&lt;/h3&gt;
&lt;h4&gt;泄露溢出&lt;/h4&gt;
&lt;p&gt;内存泄漏（Memory Leak）：是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放，造成系统内存的浪费，导致程序运行速度减慢甚至系统崩溃等严重后果&lt;/p&gt;
&lt;p&gt;可达性分析算法来判断对象是否是不再使用的对象，本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题，让 JVM 误以为此对象还在引用中，无法回收，造成内存泄漏&lt;/p&gt;
&lt;p&gt;内存溢出（out of memory）指的是申请内存时，没有足够的内存可以使用&lt;/p&gt;
&lt;p&gt;内存泄漏和内存溢出的关系：内存泄漏的越来越多，最终会导致内存溢出&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;几种情况&lt;/h4&gt;
&lt;h5&gt;静态集合&lt;/h5&gt;
&lt;p&gt;静态集合类的生命周期与 JVM 程序一致，则容器中的对象在程序结束之前将不能被释放，从而造成内存泄漏。原因是&lt;strong&gt;长生命周期的对象持有短生命周期对象的引用&lt;/strong&gt;，尽管短生命周期的对象不再使用，但是因为长生命周期对象持有它的引用而导致不能被回收&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MemoryLeak {
    static List list = new ArrayList();
    public void oomTest(){
        Object obj = new Object();//局部变量
        list.add(obj);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;单例模式&lt;/h5&gt;
&lt;p&gt;单例模式和静态集合导致内存泄露的原因类似，因为单例的静态特性，它的生命周期和 JVM 的生命周期一样长，所以如果单例对象持有外部对象的引用，那么这个外部对象也不会被回收，那么就会造成内存泄漏&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;内部类&lt;/h5&gt;
&lt;p&gt;内部类持有外部类的情况，如果一个外部类的实例对象调用方法返回了一个内部类的实例对象，即使那个外部类实例对象不再被使用，但由于内部类持有外部类的实例对象，这个外部类对象也不会被回收，造成内存泄漏&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;连接相关&lt;/h5&gt;
&lt;p&gt;数据库连接、网络连接和 IO 连接等，当不再使用时，需要显式调用 close 方法来释放与连接，垃圾回收器才会回收对应的对象，否则将会造成大量的对象无法被回收，从而引起内存泄漏&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;不合理域&lt;/h5&gt;
&lt;p&gt;变量不合理的作用域，一个变量的定义的作用范围大于其使用范围，很有可能会造成内存泄漏；如果没有及时地把对象设置为 null，也有可能导致内存泄漏的发生&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UsingRandom {
    private String msg;
    public void receiveMsg(){
        msg = readFromNet();// 从网络中接受数据保存到 msg 中
        saveDB(msg);		// 把 msg 保存到数据库中
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 readFromNet 方法把接收消息保存在 msg 中，然后调用 saveDB 方法把内容保存到数据库中，此时 msg 已经可以被回收，但是 msg 的生命周期与对象的生命周期相同，造成 msg 不能回收，产生内存泄漏&lt;/p&gt;
&lt;p&gt;解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;msg 变量可以放在 receiveMsg 方法内部，当方法使用完，msg 的生命周期也就结束，就可以被回收了&lt;/li&gt;
&lt;li&gt;在使用完 msg 后，把 msg 设置为 null，这样垃圾回收器也会回收 msg 的内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;改变哈希&lt;/h5&gt;
&lt;p&gt;当一个对象被存储进 HashSet 集合中以后，就&lt;strong&gt;不能修改这个对象中的那些参与计算哈希值的字段&lt;/strong&gt;，否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同，这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false，导致无法从 HashSet 集合中单独删除当前对象，造成内存泄漏&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存泄露&lt;/h5&gt;
&lt;p&gt;内存泄漏的一个常见来源是缓存，一旦把对象引用放入到缓存中，就会很容易被遗忘&lt;/p&gt;
&lt;p&gt;使用 WeakHashMap 代表缓存，当除了自身有对 key 的引用外没有其他引用，map 会自动丢弃此值&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;案例分析&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { //入栈
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() { //出栈
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序并没有明显错误，但 pop 函数存在内存泄漏问题，因为 pop 函数只是把栈顶索引下移一位，并没有把上一个出栈索引处的引用置空，导致&lt;strong&gt;栈数组一直强引用着已经出栈的对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;类加载&lt;/h2&gt;
&lt;h3&gt;对象访存&lt;/h3&gt;
&lt;h4&gt;存储结构&lt;/h4&gt;
&lt;p&gt;一个 Java 对象内存中存储为三部分：对象头（Header）、实例数据（Instance Data）和对齐填充 （Padding）&lt;/p&gt;
&lt;p&gt;对象头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;普通对象：分为两部分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mark Word&lt;/strong&gt;：用于存储对象自身的运行时数据， 如哈希码（HashCode）、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hash(25) + age(4) + lock(3) = 32bit					#32位系统
unused(25+1) + hash(31) + age(4) + lock(3) = 64bit	#64位系统
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Klass Word&lt;/strong&gt;：类型指针，&lt;strong&gt;指向该对象的 Class 类对象的指针&lt;/strong&gt;，虚拟机通过这个指针来确定这个对象是哪个类的实例；在 64 位系统中，开启指针压缩（-XX:+UseCompressedOops）或者 JVM 堆的最大值小于 32G，这个指针也是 4byte，否则是 8byte（就是 &lt;strong&gt;Java 中的一个引用的大小&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;|-----------------------------------------------------|
| 				  Object Header (64 bits) 			  |
|---------------------------|-------------------------|
| 	 Mark Word (32 bits)	|  Klass Word (32 bits)   |
|---------------------------|-------------------------|
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组对象：如果对象是一个数组，那在对象头中还有一块数据用于记录数组长度（12 字节）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|-------------------------------------------------------------------------------|
| 						  Object Header (96 bits) 							    |
|-----------------------|-----------------------------|-------------------------|
|  Mark Word(32bits)    | 	  Klass Word(32bits) 	  |   array length(32bits)  |
|-----------------------|-----------------------------|-------------------------|
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实例数据：实例数据部分是对象真正存储的有效信息，也是在程序代码中所定义的各种类型的字段内容，无论是从父类继承下来的，还是在子类中定义的，都需要记录起来&lt;/p&gt;
&lt;p&gt;对齐填充：Padding 起占位符的作用。64 位系统，由于 HotSpot VM 的自动内存管理系统要求&lt;strong&gt;对象起始地址必须是 8 字节的整数倍&lt;/strong&gt;，就是对象的大小必须是 8 字节的整数倍，而对象头部分正好是 8 字节的倍数（1 倍或者 2 倍），因此当对象实例数据部分没有对齐时，就需要通过对齐填充来补全&lt;/p&gt;
&lt;p&gt;32 位系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个 int 在 java 中占据 4byte，所以 Integer 的大小为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int value;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 需要补位4byte
4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int[] arr = new int[10]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 由于需要8位对齐，所以最终大小为56byte
4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;实际大小&lt;/h4&gt;
&lt;p&gt;浅堆（Shallow Heap）：&lt;strong&gt;对象本身占用的内存，不包括内部引用对象的大小&lt;/strong&gt;，32 位系统中一个对象引用占 4 个字节，每个对象头占用 8 个字节，根据堆快照格式不同，对象的大小会同 8 字节进行对齐&lt;/p&gt;
&lt;p&gt;JDK7 中的 String：2个 int 值共占 8 字节，value 对象引用占用 4 字节，对象头 8 字节，对齐后占 24 字节，为 String 对象的浅堆大小，与 value 实际取值无关，无论字符串长度如何，浅堆大小始终是 24 字节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final char value[];
private int hash;
private int hash32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保留集（Retained Set）：对象 A 的保留集指当对象 A 被垃圾回收后，可以被释放的所有的对象集合（包括 A 本身），所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合，就是仅被对象 A 所持有的对象的集合&lt;/p&gt;
&lt;p&gt;深堆（Retained Heap）：指对象的保留集中所有的对象的浅堆大小之和，一个对象的深堆指只能通过该对象访问到的（直接或间接）所有对象的浅堆之和，即对象被回收后，可以释放的真实空间&lt;/p&gt;
&lt;p&gt;对象的实际大小：一个对象所能触及的所有对象的浅堆大小之和，也就是通常意义上我们说的对象大小&lt;/p&gt;
&lt;p&gt;下图显示了一个简单的对象引用关系图，对象 A 引用了 C 和 D，对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身，&lt;strong&gt;A 的实际大小为 A、C、D 三者之和&lt;/strong&gt;，A 的深堆大小为 A 与 D 之和，由于对象 C 还可以通过对象 B 访问到 C，因此 C 不在对象 A 的深堆范围内&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-对象的实际大小.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;内存分析工具 MAT 提供了一种叫支配树的对象图，体现了对象实例间的支配关系&lt;/p&gt;
&lt;p&gt;基本性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象 A 的子树（所有被对象 A 支配的对象集合）表示对象 A 的保留集（retained set），即深堆&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果对象 A 支配对象 B，那么对象 A 的直接支配者也支配对象 B&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支配树的边与对象引用图的边不直接对应&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;左图表示对象引用图，右图表示左图所对应的支配树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E6%94%AF%E9%85%8D%E6%A0%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如：对象 F 与对象 D 相互引用，因为到对象 F 的所有路径必然经过对象 D，因此对象 D 是对象 F 的直接支配者&lt;/p&gt;
&lt;p&gt;参考文章：https://www.yuque.com/u21195183/jvm/nkq31c&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;节约内存&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;尽量使用基本数据类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;满足容量前提下，尽量用小字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量用数组，少用集合，数组中是可以使用基本类型的，但是集合中只能放包装类型，如果需要使用集合，推荐比较节约内存的集合工具：fastutil&lt;/p&gt;
&lt;p&gt;一个 ArrayList 集合，如果里面放了 10 个数字，占用多少内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient Object[] elementData;
private int size;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mark Word 占 4byte，Klass Word 占 4byte，一个 int 字段占 4byte，elementData 数组占 12byte，数组中 10 个 Integer 对象占 10×16，所以整个集合空间大小为 184byte（深堆）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间用 long/int 表示，不用 Date 或者 String&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;对象访问&lt;/h4&gt;
&lt;p&gt;JVM 是通过&lt;strong&gt;栈帧中的对象引用&lt;/strong&gt;访问到其内部的对象实例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;句柄访问：Java 堆中会划分出一块内存来作为句柄池，reference 中存储的就是对象的句柄地址，而句柄中包含了对象实例数据和类型数据各自的具体地址信息&lt;/p&gt;
&lt;p&gt;优点：reference 中存储的是稳定的句柄地址，在对象被移动（垃圾收集）时只会改变句柄中的实例数据指针，而 reference 本身不需要被修改&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AF%B9%E8%B1%A1%E8%AE%BF%E9%97%AE-%E5%8F%A5%E6%9F%84%E8%AE%BF%E9%97%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接指针（HotSpot 采用）：Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息，reference 中直接存储的对象地址&lt;/p&gt;
&lt;p&gt;优点：速度更快，&lt;strong&gt;节省了一次指针定位的时间开销&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缺点：对象被移动时（如进行 GC 后的内存重新排列），对象的 reference 也需要同步更新&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AF%B9%E8%B1%A1%E8%AE%BF%E9%97%AE-%E7%9B%B4%E6%8E%A5%E6%8C%87%E9%92%88.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/afraidToForget/p/12584866.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;对象创建&lt;/h3&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;p&gt;在 Java 中，对象的生命周期包括以下几个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 创建阶段 (Created)：
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 应用阶段 (In Use)：对象至少被一个强引用持有着
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 不可见阶段 (Invisible)：程序的执行已经超出了该对象的作用域，不再持有该对象的任何强引用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 不可达阶段 (Unreachable)：该对象不再被任何强引用所持有，包括 GC Root 的强引用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 收集阶段 (Collected)：垃圾回收器对该对象的内存空间重新分配做好准备，该对象如果重写了 finalize() 方法，则会去执行该方法
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 终结阶段 (Finalized)：等待垃圾回收器对该对象空间进行回收，当对象执行完 finalize() 方法后仍然处于不可达状态时进入该阶段
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 对象空间重分配阶段 (De-allocated)：垃圾回收器对该对象的所占用的内存空间进行回收或者再分配
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/sodino/article/details/38387049&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;创建时机&lt;/h4&gt;
&lt;p&gt;类在第一次实例化加载一次，后续实例化不再加载，引用第一次加载的类&lt;/p&gt;
&lt;p&gt;Java 对象创建时机：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用 new 关键字创建对象：由执行类实例创建表达式而引起的对象创建&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Class 类的 newInstance 方法（反射机制）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Constructor 类的 newInstance 方法（反射机制）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private int id;
    public Student(Integer id) {
        this.id = id;
    }
    public static void main(String[] args) throws Exception {
        Constructor&amp;lt;Student&amp;gt; c = Student.class.getConstructor(Integer.class);
        Student stu = c.newInstance(123);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制，事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Clone 方法创建对象：用 clone 方法创建对象的过程中并不会调用任何构造函数，要想使用 clone 方法，我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用（反）序列化机制创建对象：当反序列化一个对象时，JVM 会创建一个&lt;strong&gt;单独的对象&lt;/strong&gt;，在此过程中，JVM 并不会调用任何构造函数，为了反序列化一个对象，需要让类实现 Serializable 接口&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从 Java 虚拟机层面看，除了使用 new 关键字创建对象的方式外，其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;创建过程&lt;/h4&gt;
&lt;p&gt;创建对象的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;判断对象对应的类是否加载、链接、初始化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为对象分配内存：指针碰撞、空闲链表。当一个对象被创建时，虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量，即使从&lt;strong&gt;隐藏变量&lt;/strong&gt;也会被分配空间（继承部分解释了为什么会隐藏）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理并发安全问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采用 CAS 配上自旋保证更新的原子性&lt;/li&gt;
&lt;li&gt;每个线程预先分配一块 TLAB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化分配的空间：虚拟机将分配到的内存空间都初始化为零值（不包括对象头），保证对象实例字段在不赋值时可以直接使用，程序能访问到这些字段的数据类型所对应的零值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置对象的对象头：将对象的所属类（类的元数据信息）、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 init 方法进行实例化：实例变量初始化、实例代码块初始化 、构造函数初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例变量初始化与实例代码块初始化：&lt;/p&gt;
&lt;p&gt;对实例变量直接赋值或者使用实例代码块赋值，&lt;strong&gt;编译器会将其中的代码放到类的构造函数中去&lt;/strong&gt;，并且这些代码会被放在对超类构造函数的调用语句之后（Java 要求构造函数的第一条语句必须是超类构造函数的调用语句），构造函数本身的代码之前&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数初始化：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 要求在实例化类之前，必须先实例化其超类，以保证所创建实例的完整性&lt;/strong&gt;，在准备实例化一个类的对象前，首先准备实例化该类的父类，如果该类的父类还有父类，那么准备实例化该类的父类的父类，依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化，初始化父类中的变量和执行构造函数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;承上启下&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一个实例变量在对象初始化的过程中会被赋值几次？一个实例变量最多可以被初始化 4 次&lt;/p&gt;
&lt;p&gt;JVM 在为一个对象分配完内存之后，会给每一个实例变量赋予默认值，这个实例变量被第一次赋值；在声明实例变量的同时对其进行了赋值操作，那么这个实例变量就被第二次赋值；在实例代码块中又对变量做了初始化操作，那么这个实例变量就被第三次赋值；；在构造函数中也对变量做了初始化操作，那么这个实例变量就被第四次赋值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类的初始化过程与类的实例化过程的异同？&lt;/p&gt;
&lt;p&gt;类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程；类的实例化是指在类完全加载到内存中后创建对象的过程（类的实例化触发了类的初始化，先初始化才能实例化）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如一个类还未加载到内存中，那么在创建一个该类的实例时，具体过程是怎样的？（&lt;strong&gt;经典案例&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class StaticTest {
    public static void main(String[] args) {
        staticFunction();//调用静态方法，触发初始化
    }

    static StaticTest st = new StaticTest();

    static {   //静态代码块
        System.out.println(&quot;1&quot;);
    }

    {       // 实例代码块
        System.out.println(&quot;2&quot;);
    }

    StaticTest() {    // 实例构造器
        System.out.println(&quot;3&quot;);
        System.out.println(&quot;a=&quot; + a + &quot;,b=&quot; + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println(&quot;4&quot;);
    }

    int a = 110;    		// 实例变量
    static int b = 112;     // 静态变量
}/* Output: 
        2
        3
        a=110,b=0
        1
        4
 *///:~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;static StaticTest st = new StaticTest();&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例实例化不一定要在类初始化结束之后才开始&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在同一个类加载器下，一个类型只会被初始化一次。所以一旦开始初始化一个类，无论是否完成后续都不会再重新触发该类型的初始化阶段了（只考虑在同一个类加载器下的情形）。因此在实例化上述程序中的 st 变量时，&lt;strong&gt;实际上是把实例化嵌入到了静态初始化流程中，并且在上面的程序中，嵌入到了静态初始化的起始位置&lt;/strong&gt;，这就导致了实例初始化完全发生在静态初始化之前，这也是导致 a 为 110，b 为 0 的原因&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class StaticTest {
    &amp;lt;clinit&amp;gt;(){
        a = 110;    // 实例变量
        System.out.println(&quot;2&quot;);	// 实例代码块
        System.out.println(&quot;3&quot;);	// 实例构造器中代码的执行
        System.out.println(&quot;a=&quot; + a + &quot;,b=&quot; + b);  // 实例构造器中代码的执行
        类变量st被初始化
        System.out.println(&quot;1&quot;);	//静态代码块
        类变量b被初始化为112
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;加载过程&lt;/h3&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;p&gt;类是在运行期间&lt;strong&gt;第一次使用时动态加载&lt;/strong&gt;的（不使用不加载），而不是一次性加载所有类，因为一次性加载会占用很多的内存，加载的类信息存放于一块成为方法区的内存空间&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E7%B1%BB%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;包括 7 个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加载（Loading）&lt;/li&gt;
&lt;li&gt;链接：验证（Verification）、准备（Preparation）、解析（Resolution）&lt;/li&gt;
&lt;li&gt;初始化（Initialization）&lt;/li&gt;
&lt;li&gt;使用（Using）&lt;/li&gt;
&lt;li&gt;卸载（Unloading）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加载阶段&lt;/h4&gt;
&lt;p&gt;加载是类加载的其中一个阶段，注意不要混淆&lt;/p&gt;
&lt;p&gt;加载过程完成以下三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过类的完全限定名称获取定义该类的二进制字节流（二进制字节码）&lt;/li&gt;
&lt;li&gt;将该字节流表示的静态存储结构转换为方法区的运行时存储结构（Java 类模型）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;将字节码文件加载至方法区后，在堆中生成一个代表该类的 Class 对象，作为该类在方法区中的各种数据的访问入口&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中二进制字节流可以从以下方式中获取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 ZIP 包读取，成为 JAR、EAR、WAR 格式的基础&lt;/li&gt;
&lt;li&gt;从网络中获取，最典型的应用是 Applet&lt;/li&gt;
&lt;li&gt;由其他文件生成，例如由 JSP 文件生成对应的 Class 类&lt;/li&gt;
&lt;li&gt;运行时计算生成，例如动态代理技术，在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_java_mirror&lt;/code&gt; 即 Java 的类镜像，例如对 String 来说就是 String.class，作用是把 class 暴露给 Java 使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_super&lt;/code&gt; 即父类、&lt;code&gt;_fields&lt;/code&gt; 即成员变量、&lt;code&gt;_methods&lt;/code&gt; 即方法、&lt;code&gt;_constants&lt;/code&gt; 即常量池、&lt;code&gt;_class_loader&lt;/code&gt; 即类加载器、&lt;code&gt;_vtable&lt;/code&gt; &lt;strong&gt;虚方法表&lt;/strong&gt;、&lt;code&gt;_itable&lt;/code&gt; 接口方法表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加载过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个类还有父类没有加载，先加载父类&lt;/li&gt;
&lt;li&gt;加载和链接可能是交替运行的&lt;/li&gt;
&lt;li&gt;Class 对象和 _java_mirror 相互持有对方的地址，堆中对象通过 instanceKlass 和元空间进行交互&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-类的生命周期-加载.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;创建数组类有些特殊，因为数组类本身并不是由类加载器负责创建，而是由 JVM 在运行时根据需要而直接创建的，但数组的元素类型仍然需要依靠类加载器去创建，创建数组类的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果数组的元素类型是引用类型，那么遵循定义的加载过程递归加载和创建数组的元素类型&lt;/li&gt;
&lt;li&gt;JVM 使用指定的元素类型和数组维度来创建新的数组类&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本数据类型由启动类加载器加载&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;链接阶段&lt;/h4&gt;
&lt;h5&gt;验证&lt;/h5&gt;
&lt;p&gt;确保 Class 文件的字节流中包含的信息是否符合 JVM 规范，保证被加载类的正确性，不会危害虚拟机自身的安全&lt;/p&gt;
&lt;p&gt;主要包括&lt;strong&gt;四种验证&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文件格式验证&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语义检查，但凡在语义上不符合规范的，虚拟机不会给予验证通过&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否所有的类都有父类的存在（除了 Object 外，其他类都应该有父类）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否一些被定义为 final 的方法或者类被重写或继承了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非抽象类是否实现了所有抽象方法或者接口方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否存在不兼容的方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字节码验证，试图通过对字节码流的分析，判断字节码是否可以被正确地执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在字节码的执行过程中，是否会跳转到一条不存在的指令&lt;/li&gt;
&lt;li&gt;函数的调用是否传递了正确类型的参数&lt;/li&gt;
&lt;li&gt;变量的赋值是不是给了正确的数据类型&lt;/li&gt;
&lt;li&gt;栈映射帧（StackMapTable）在这个阶段用于检测在特定的字节码处，其局部变量表和操作数栈是否有着正确的数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;符号引用验证，Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;准备&lt;/h5&gt;
&lt;p&gt;准备阶段为&lt;strong&gt;静态变量（类变量）分配内存并设置初始值&lt;/strong&gt;，使用的是方法区的内存：&lt;/p&gt;
&lt;p&gt;说明：实例变量不会在这阶段分配内存，它会在对象实例化时随着对象一起被分配在堆中，类加载发生在所有实例化操作之前，并且类加载只进行一次，实例化可以进行多次&lt;/p&gt;
&lt;p&gt;类变量初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static 变量分配空间和赋值是两个步骤：&lt;strong&gt;分配空间在准备阶段完成，赋值在初始化阶段完成&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果 static 变量是 final 的基本类型以及字符串常量，那么编译阶段值（方法区）就确定了，准备阶段会显式初始化&lt;/li&gt;
&lt;li&gt;如果 static 变量是 final 的，但属于引用类型或者构造器方法的字符串，赋值在初始化阶段完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始值一般为 0 值，例如下面的类变量 value 被初始化为 0 而不是 123：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static int value = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量 value 被初始化为 123 而不是 0：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static final int value = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 并不支持 boolean 类型，对于 boolean 类型，内部实现是 int，由于 int 的默认值是 0，故 boolean 的默认值就是 false&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;解析&lt;/h5&gt;
&lt;p&gt;将常量池中类、接口、字段、方法的&lt;strong&gt;符号引用替换为直接引用&lt;/strong&gt;（内存地址）的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;符号引用：一组符号来描述目标，可以是任何字面量，属于编译原理方面的概念，如：包括类和接口的全限名、字段的名称和描述符、方法的名称和&lt;strong&gt;方法描述符&lt;/strong&gt;（因为类还没有加载完，很多方法是找不到的）&lt;/li&gt;
&lt;li&gt;直接引用：直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄，如果有了直接引用，那说明引用的目标必定已经存在于内存之中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：在 &lt;code&gt;com.demo.Solution&lt;/code&gt; 类中引用了 &lt;code&gt;com.test.Quest&lt;/code&gt;，把 &lt;code&gt;com.test.Quest&lt;/code&gt; 作为符号引用存进类常量池，在类加载完后，&lt;strong&gt;用这个符号引用去方法区找这个类的内存地址&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在类加载阶段解析的是非虚方法，静态绑定&lt;/li&gt;
&lt;li&gt;也可以在初始化阶段之后再开始解析，这是为了支持 Java 的&lt;strong&gt;动态绑定&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;通过解析操作，符号引用就可以转变为目标方法在类的虚方法表中的位置，从而使得方法被成功调用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Load2 {
    public static void main(String[] args) throws Exception{
    ClassLoader classloader = Load2.class.getClassLoader();
    // cloadClass 加载类方法不会导致类的解析和初始化，也不会加载D
    Class&amp;lt;?&amp;gt; c = classloader.loadClass(&quot;cn.jvm.t3.load.C&quot;);
        
    // new C();会导致类的解析和初始化，从而解析初始化D
    System.in.read();
    }
}
class C {
	D d = new D();
}
class D {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;h5&gt;介绍&lt;/h5&gt;
&lt;p&gt;初始化阶段才真正开始执行类中定义的 Java 程序代码，在准备阶段，类变量已经赋过一次系统要求的初始值；在初始化阶段，通过程序制定的计划去初始化类变量和其它资源，执行 &amp;lt;clinit&amp;gt;&lt;/p&gt;
&lt;p&gt;在编译生成 class 文件时，编译器会产生两个方法加于 class 文件中，一个是类的初始化方法 clinit，另一个是实例的初始化方法 init&lt;/p&gt;
&lt;p&gt;类构造器 &amp;lt;clinit&amp;gt;() 与实例构造器 &amp;lt;init&amp;gt;() 不同，它不需要程序员进行显式调用，在一个类的生命周期中，类构造器最多被虚拟机&lt;strong&gt;调用一次&lt;/strong&gt;，而实例构造器则会被虚拟机调用多次，只要程序员创建对象&lt;/p&gt;
&lt;p&gt;类在第一次实例化加载一次，把 class 读入内存，后续实例化不再加载，引用第一次加载的类&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;clinit&lt;/h5&gt;
&lt;p&gt;&amp;lt;clinit&amp;gt;()：类构造器，由编译器自动收集类中&lt;strong&gt;所有类变量的赋值动作和静态语句块&lt;/strong&gt;中的语句合并产生的&lt;/p&gt;
&lt;p&gt;作用：是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果类中没有静态变量或静态代码块，那么 clinit 方法将不会被生成&lt;/li&gt;
&lt;li&gt;clinit 方法只执行一次，在执行 clinit 方法时，必须先执行父类的clinit方法&lt;/li&gt;
&lt;li&gt;static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定&lt;/li&gt;
&lt;li&gt;static 不加 final 的变量都在初始化环节赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;线程安全&lt;/strong&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟机会保证一个类的 &amp;lt;clinit&amp;gt;() 方法在多线程环境下被正确的加锁和同步，如果多个线程同时初始化一个类，只会有一个线程执行这个类的 &amp;lt;clinit&amp;gt;() 方法，其它线程都阻塞等待，直到活动线程执行 &amp;lt;clinit&amp;gt;() 方法完毕&lt;/li&gt;
&lt;li&gt;如果在一个类的 &amp;lt;clinit&amp;gt;() 方法中有耗时的操作，就可能造成多个线程阻塞，在实际过程中此种阻塞很隐蔽&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特别注意：静态语句块只能访问到定义在它之前的类变量，定义在它之后的类变量只能赋值，不能访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    static {
        //i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  	// 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口中不可以使用静态语句块，但仍然有类变量初始化的赋值操作，因此接口与类一样都会生成 &amp;lt;clinit&amp;gt;() 方法，两者不同的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在初始化一个接口时，并不会先初始化它的父接口，所以执行接口的 &amp;lt;clinit&amp;gt;() 方法不需要先执行父接口的 &amp;lt;clinit&amp;gt;() 方法&lt;/li&gt;
&lt;li&gt;在初始化一个类时，不会先初始化所实现的接口，所以接口的实现类在初始化时不会执行接口的 &amp;lt;clinit&amp;gt;() 方法&lt;/li&gt;
&lt;li&gt;只有当父接口中定义的变量使用时，父接口才会初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;时机&lt;/h5&gt;
&lt;p&gt;类的初始化是懒惰的，只有在首次使用时才会被装载，JVM 不会无条件地装载 Class 类型，Java 虚拟机规定，一个类或接口在初次使用前，必须要进行初始化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主动引用&lt;/strong&gt;：虚拟机规范中并没有强制约束何时进行加载，但是规范严格规定了有且只有下列情况必须对类进行初始化（加载、验证、准备都会发生）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当创建一个类的实例时，使用 new 关键字，或者通过反射、克隆、反序列化（前文讲述的对象的创建时机）&lt;/li&gt;
&lt;li&gt;当调用类的静态方法或访问静态字段时，遇到 getstatic、putstatic、invokestatic 这三条字节码指令，如果类没有进行过初始化，则必须先触发其初始化
&lt;ul&gt;
&lt;li&gt;getstatic：程序访问类的静态变量（不是静态常量，常量会被加载到运行时常量池）&lt;/li&gt;
&lt;li&gt;putstatic：程序给类的静态变量赋值&lt;/li&gt;
&lt;li&gt;invokestatic ：调用一个类的静态方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;使用 java.lang.reflect 包的方法对类进行反射调用时，如果类没有进行初始化，则需要先触发其初始化&lt;/li&gt;
&lt;li&gt;当初始化一个类的时候，如果发现其父类还没有进行过初始化，则需要先触发其父类的初始化，但这条规则并&lt;strong&gt;不适用于接口&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当虚拟机启动时，需要指定一个要执行的主类（包含 main() 方法的那个类），虚拟机会先初始化这个主类&lt;/li&gt;
&lt;li&gt;MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制，而要想使用这两个调用， 就必须先使用 findStaticVarHandle 来初始化要调用的类&lt;/li&gt;
&lt;li&gt;补充：当一个接口中定义了 JDK8 新加入的默认方法（被 default 关键字修饰的接口方法）时，如果有这个接口的实现类发生了初始化，那该接口要在其之前被初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;被动引用&lt;/strong&gt;：所有引用类的方式都不会触发初始化，称为被动引用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过子类引用父类的静态字段，不会导致子类初始化，只会触发父类的初始化&lt;/li&gt;
&lt;li&gt;通过数组定义来引用类，不会触发此类的初始化。该过程会对数组类进行初始化，数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类，其中包含了数组的属性和方法&lt;/li&gt;
&lt;li&gt;常量（final 修饰）在编译阶段会存入调用类的常量池中，本质上没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化&lt;/li&gt;
&lt;li&gt;调用 ClassLoader 类的 loadClass() 方法加载一个类，并不是对类的主动使用，不会导致类的初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;init&lt;/h5&gt;
&lt;p&gt;init 指的是实例构造器，主要作用是在类实例化过程中执行，执行内容包括成员变量初始化和代码块的执行&lt;/p&gt;
&lt;p&gt;实例化即调用 &amp;lt;init&amp;gt;()V ，虚拟机会保证这个类的构造方法的线程安全，先为实例变量分配内存空间，再执行赋默认值，然后根据源码中的顺序执行赋初值或代码块，没有成员变量初始化和代码块则不会执行&lt;/p&gt;
&lt;p&gt;类实例化过程：&lt;strong&gt;父类的类构造器&amp;lt;clinit&amp;gt;() -&amp;gt; 子类的类构造器&amp;lt;clinit&amp;gt;() -&amp;gt; 父类的成员变量和实例代码块 -&amp;gt; 父类的构造函数 -&amp;gt; 子类的成员变量和实例代码块 -&amp;gt; 子类的构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;new 关键字会创建对象并复制 dup 一个对象引用，一个调用 &amp;lt;init&amp;gt; 方法，另一个用来赋值给接收者&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;卸载阶段&lt;/h4&gt;
&lt;p&gt;时机：执行了 System.exit() 方法，程序正常执行结束，程序在执行过程中遇到了异常或错误而异常终止，由于操作系统出现错误而导致Java 虚拟机进程终止&lt;/p&gt;
&lt;p&gt;卸载类即该类的 &lt;strong&gt;Class 对象被 GC&lt;/strong&gt;，卸载类需要满足3个要求:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;该类的所有的实例对象都已被 GC，也就是说堆不存在该类的实例对象&lt;/li&gt;
&lt;li&gt;该类没有在其他任何地方被引用&lt;/li&gt;
&lt;li&gt;该类的类加载器的实例已被 GC，一般是可替换类加载器的场景，如 OSGi、JSP 的重加载等，很难达成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 JVM 生命周期类，由 JVM 自带的类加载器加载的类是不会被卸载的，自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器，这些类加载器始终引用它们所加载的类，这些类始终是可及的&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;类加载器&lt;/h3&gt;
&lt;h4&gt;类加载&lt;/h4&gt;
&lt;p&gt;类加载方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐式加载：不直接在代码中调用 ClassLoader 的方法加载类对象
&lt;ul&gt;
&lt;li&gt;创建类对象、使用类的静态域、创建子类对象、使用子类的静态域&lt;/li&gt;
&lt;li&gt;在 JVM 启动时，通过三大类加载器加载 class&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;显式加载：
&lt;ul&gt;
&lt;li&gt;ClassLoader.loadClass(className)：只加载和连接，&lt;strong&gt;不会进行初始化&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Class.forName(String name, boolean initialize, ClassLoader loader)：使用 loader 进行加载和连接，根据参数 initialize 决定是否初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类的唯一性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件：
&lt;ul&gt;
&lt;li&gt;类的完整类名必须一致，包括包名&lt;/li&gt;
&lt;li&gt;加载这个类的 ClassLoader（指 ClassLoader 实例对象）必须相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这里的相等，包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true，也包括使用 instanceof 关键字做对象所属关系判定结果为 true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;命名空间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个类加载器都有自己的命名空间，命名空间由该加载器及所有的父加载器所加载的类组成&lt;/li&gt;
&lt;li&gt;在同一命名空间中，不会出现类的完整名字（包括类的包名）相同的两个类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可见性&lt;/strong&gt;，子类加载器可以访问父加载器加载的类型，但是反过来是不允许的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单一性&lt;/strong&gt;，由于父加载器的类型对于子加载器是可见的，所以父加载器中加载过的类型，不会在子加载器中重复加载&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加载器&lt;/h4&gt;
&lt;p&gt;类加载器是 Java 的核心组件，用于加载字节码到 JVM 内存，得到 Class 类的对象&lt;/p&gt;
&lt;p&gt;从 Java 虚拟机规范来讲，只存在以下两种不同的类加载器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动类加载器（Bootstrap ClassLoader）：使用 C++ 实现，是虚拟机自身的一部分&lt;/li&gt;
&lt;li&gt;自定义类加载器（User-Defined ClassLoader）：Java 虚拟机规范&lt;strong&gt;将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器&lt;/strong&gt;，使用 Java 语言实现，独立于虚拟机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从 Java 开发人员的角度看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动类加载器（Bootstrap ClassLoader）：
&lt;ul&gt;
&lt;li&gt;处于安全考虑，Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类&lt;/li&gt;
&lt;li&gt;类加载器负责加载在 &lt;code&gt;JAVA_HOME/jre/lib&lt;/code&gt; 或 &lt;code&gt;sun.boot.class.path&lt;/code&gt; 目录中的，或者被 -Xbootclasspath 参数所指定的路径中的类，并且是虚拟机识别的类库加载到虚拟机内存中&lt;/li&gt;
&lt;li&gt;仅按照文件名识别，如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载&lt;/li&gt;
&lt;li&gt;启动类加载器无法被 Java 程序直接引用，编写自定义类加载器时，如果要把加载请求委派给启动类加载器，直接使用 null 代替&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;扩展类加载器（Extension ClassLoader）：
&lt;ul&gt;
&lt;li&gt;由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader)  实现，上级为 Bootstrap，显示为 null&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;JAVA_HOME/jre/lib/ext&lt;/code&gt; 或者被 &lt;code&gt;java.ext.dir&lt;/code&gt; 系统变量所指定路径中的所有类库加载到内存中&lt;/li&gt;
&lt;li&gt;开发者可以使用扩展类加载器，创建的 JAR 放在此目录下，会由扩展类加载器自动加载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;应用程序类加载器（Application ClassLoader）：
&lt;ul&gt;
&lt;li&gt;由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现，上级为 Extension&lt;/li&gt;
&lt;li&gt;负责加载环境变量 classpath 或系统属性 &lt;code&gt;java.class.path&lt;/code&gt; 指定路径下的类库&lt;/li&gt;
&lt;li&gt;这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值，因此称为系统类加载器&lt;/li&gt;
&lt;li&gt;可以直接使用这个类加载器，如果应用程序中没有自定义类加载器，这个就是程序中默认的类加载器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自定义类加载器：由开发人员自定义的类加载器，上级是 Application&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    //获取系统类加载器
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

    //获取其上层  扩展类加载器
    ClassLoader extClassLoader = systemClassLoader.getParent();
    System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

    //获取其上层 获取不到引导类加载器
    ClassLoader bootStrapClassLoader = extClassLoader.getParent();
    System.out.println(bootStrapClassLoader);//null

    //对于用户自定义类来说：使用系统类加载器进行加载
    ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
    System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

    //String 类使用引导类加载器进行加载的 --&amp;gt; java核心类库都是使用启动类加载器加载的
    ClassLoader classLoader1 = String.class.getClassLoader();
    System.out.println(classLoader1);//null

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充两个类加载器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SecureClassLoader 扩展了 ClassLoader，新增了几个与使用相关的代码源和权限定义类验证（对 class 源码的访问权限）的方法，一般不会直接跟这个类打交道，更多是与它的子类 URLClassLoader 有所关联&lt;/li&gt;
&lt;li&gt;ClassLoader 是一个抽象类，很多方法是空的没有实现，而 URLClassLoader 这个实现类为这些方法提供了具体的实现，并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时，如果没有太过于复杂的需求，可以直接继承 URLClassLoader 类，这样就可以避免去编写 findClass() 方法及其获取字节码流的方式，使自定义类加载器编写更加简洁&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;ClassLoader 类，是一个抽象类，其后所有的类加载器都继承自 ClassLoader（不包括启动类加载器）&lt;/p&gt;
&lt;p&gt;获取 ClassLoader 的途径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取当前类的 ClassLoader：&lt;code&gt;clazz.getClassLoader()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取当前线程上下文的 ClassLoader：&lt;code&gt;Thread.currentThread.getContextClassLoader()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取系统的 ClassLoader：&lt;code&gt;ClassLoader.getSystemClassLoader()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取调用者的 ClassLoader：&lt;code&gt;DriverManager.getCallerClassLoader()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ClassLoader 类常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getParent()&lt;/code&gt;：返回该类加载器的超类加载器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loadclass(String name)&lt;/code&gt;：加载名为 name 的类，返回结果为 Class 类的实例，&lt;strong&gt;该方法就是双亲委派模式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findclass(String name)&lt;/code&gt;：查找二进制名称为 name 的类，返回结果为 Class 类的实例，该方法会在检查完父类加载器之后被 loadClass() 方法调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findLoadedClass(String name)&lt;/code&gt;：查找名称为 name 的已经被加载过的类，final 修饰无法重写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defineClass(String name, byte[] b, int off, int len)&lt;/code&gt;：将&lt;strong&gt;字节流&lt;/strong&gt;解析成 JVM 能够识别的类对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolveclass(Class&amp;lt;?&amp;gt; c)&lt;/code&gt;：链接指定的 Java 类，可以使类的 Class 对象创建完成的同时也被解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InputStream getResourceAsStream(String name)&lt;/code&gt;：指定资源名称获取输入流&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加载模型&lt;/h4&gt;
&lt;h5&gt;加载机制&lt;/h5&gt;
&lt;p&gt;在 JVM 中，对于类加载模型提供了三种，分别为全盘加载、双亲委派、缓存机制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**全盘加载：**当一个类加载器负责加载某个 Class 时，该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入，除非显示指定使用另外一个类加载器来载入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**双亲委派：**某个特定的类加载器在接到加载类的请求时，首先将加载任务委托给父加载器，&lt;strong&gt;依次递归&lt;/strong&gt;，如果父加载器可以完成类加载任务，就成功返回；只有当父加载器无法完成此加载任务时，才自己去加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**缓存机制：**会保证所有加载过的 Class 都会被缓存，当程序中需要使用某个 Class 时，类加载器先从缓存区中搜寻该 Class，只有当缓存区中不存在该 Class 对象时，系统才会读取该类对应的二进制数据，并将其转换成 Class 对象存入缓冲区（方法区）中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这就是修改了 Class 后，必须重新启动 JVM，程序所做的修改才会生效的原因&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;双亲委派&lt;/h5&gt;
&lt;p&gt;双亲委派模型（Parents Delegation Model）：该模型要求除了顶层的启动类加载器外，其它类加载器都要有父类加载器，这里的父子关系一般通过组合关系（Composition）来实现，而不是继承关系（Inheritance）&lt;/p&gt;
&lt;p&gt;工作过程：一个类加载器首先将类加载请求转发到父类加载器，只有当父类加载器无法完成时才尝试自己加载&lt;/p&gt;
&lt;p&gt;双亲委派机制的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以避免某一个类被重复加载，当父类已经加载后则无需重复加载，保证全局唯一性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 类随着它的类加载器一起具有一种带有优先级的层次关系，从而使得基础类得到统一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保护程序安全，防止类库的核心 API 被随意篡改&lt;/p&gt;
&lt;p&gt;例如：在工程中新建 java.lang 包，接着在该包下新建 String 类，并定义 main 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class String {
    public static void main(String[] args) {
        System.out.println(&quot;demo info&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时执行 main 函数会出现异常，在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制，java.lang.String 的在启动类加载器（Bootstrap）得到加载，启动类加载器优先级更高，在核心 jre 库中有其相同名字的类文件，但该类中并没有 main 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;双亲委派机制的缺点：检查类是否加载的委托过程是单向的，这个方式虽然从结构上看比较清晰，使各个 ClassLoader 的职责非常明确，但&lt;strong&gt;顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类&lt;/strong&gt;（可见性）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-双亲委派模型.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;源码分析&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;protected Class&amp;lt;?&amp;gt; loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
       // 调用当前类加载器的 findLoadedClass(name)，检查当前类加载器是否已加载过指定 name 的类
        Class c = findLoadedClass(name);
        
        // 当前类加载器如果没有加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判断当前类加载器是否有父类加载器
                if (parent != null) {
                    // 如果当前类加载器有父类加载器，则调用父类加载器的 loadClass(name,false)
         			// 父类加载器的 loadClass 方法，又会检查自己是否已经加载过
                    c = parent.loadClass(name, false);
                } else {
                    // 当前类加载器没有父类加载器，说明当前类加载器是 BootStrapClassLoader
          			// 则调用 BootStrap ClassLoader 的方法加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) { }

            if (c == null) {
                // 如果调用父类的类加载器无法对类进行加载，则用自己的 findClass() 方法进行加载
                // 可以自定义 findClass() 方法
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            // 链接指定的 Java 类，可以使类的 Class 对象创建完成的同时也被解析
            resolveClass(c);
        }
        return c;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;破坏委派&lt;/h5&gt;
&lt;p&gt;双亲委派模型并不是一个具有强制性约束的模型，而是 Java 设计者推荐给开发者的类加载器实现方式&lt;/p&gt;
&lt;p&gt;破坏双亲委派模型的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自定义 ClassLoader&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不想破坏双亲委派模型，只需要重写 findClass 方法&lt;/li&gt;
&lt;li&gt;如果想要去破坏双亲委派模型，需要去**重写 loadClass **方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入&lt;strong&gt;线程上下文类加载器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java 提供了很多服务提供者接口（Service Provider Interface，SPI），允许第三方为这些接口提供实现。常见的有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供，而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里，SPI 接口中的代码需要加载具体的实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPI 的接口是 Java 核心库的一部分，是由引导类加载器来加载的&lt;/li&gt;
&lt;li&gt;SPI 的实现类是由系统类加载器加载，引导类加载器是无法找到 SPI 的实现类，因为双亲委派模型中 BootstrapClassloader 无法委派 AppClassLoader 来加载类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK 开发人员引入了线程上下文类加载器（Thread Context ClassLoader），这种类加载器可以通过 Thread  类的 setContextClassLoader 方法进行设置线程上下文类加载器，在执行线程中抛弃双亲委派加载模式，使程序可以逆向使用类加载器，使 Bootstrap 加载器拿到了 Application 加载器加载的类，破坏了双亲委派模型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现程序的动态性，如代码热替换（Hot Swap）、模块热部署（Hot Deployment）&lt;/p&gt;
&lt;p&gt;IBM 公司主导的 JSR一291（OSGiR4.2）实现模块化热部署的关键是它自定义的类加载器机制的实现，每一个程序模块（OSGi 中称为 Bundle）都有一个自己的类加载器，当更换一个 Bundle 时，就把 Bundle 连同类加载器一起换掉以实现代码的热替换，在 OSGi 环境下，类加载器不再双亲委派模型推荐的树状结构，而是进一步发展为更加复杂的网状结构&lt;/p&gt;
&lt;p&gt;当收到类加载请求时，OSGi 将按照下面的顺序进行类搜索:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将以 java.* 开头的类，委派给父类加载器加载&lt;/li&gt;
&lt;li&gt;否则，将委派列表名单内的类，委派给父类加载器加载&lt;/li&gt;
&lt;li&gt;否则，将 Import 列表中的类，委派给 Export 这个类的 Bundle 的类加载器加载&lt;/li&gt;
&lt;li&gt;否则，查找当前 Bundle 的 ClassPath，使用自己的类加载器加载&lt;/li&gt;
&lt;li&gt;否则，查找类是否在自己的 Fragment Bundle 中，如果在就委派给 Fragment Bundle 类加载器加载&lt;/li&gt;
&lt;li&gt;否则，查找 Dynamic Import 列表的 Bundle，委派给对应 Bundle 的类加载器加载&lt;/li&gt;
&lt;li&gt;否则，类查找失败&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;热替换是指在程序的运行过程中，不停止服务，只通过替换程序文件来修改程序的行为，&lt;strong&gt;热替换的关键需求在于服务不能中断&lt;/strong&gt;，修改必须立即表现正在运行的系统之中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-热替换.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;沙箱机制&lt;/h4&gt;
&lt;p&gt;沙箱机制（Sandbox）：将 Java 代码限定在虚拟机特定的运行范围中，并且严格限制代码对本地系统资源访问，来保证对代码的有效隔离，防止对本地系统造成破坏&lt;/p&gt;
&lt;p&gt;沙箱&lt;strong&gt;限制系统资源访问&lt;/strong&gt;，包括 CPU、内存、文件系统、网络，不同级别的沙箱对资源访问的限制也不一样&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK1.0：Java 中将执行程序分成本地代码和远程代码两种，本地代码默认视为可信任的，而远程代码被看作是不受信的。对于授信的本地代码，可以访问一切本地资源，而对于非授信的远程代码不可以访问本地资源，其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍，比如当用户希望远程代码访问本地系统的文件时候，就无法实现&lt;/li&gt;
&lt;li&gt;JDK1.1：针对安全机制做了改进，增加了安全策略。允许用户指定代码对本地资源的访问权限&lt;/li&gt;
&lt;li&gt;JDK1.2：改进了安全机制，增加了代码签名，不论本地代码或是远程代码都会按照用户的安全策略设定，由类加载器加载到虚拟机中权限不同的运行空间，来实现差异化的代码执行权限控制&lt;/li&gt;
&lt;li&gt;JDK1.6：当前最新的安全机制，引入了域（Domain）的概念。虚拟机会把所有代码加载到不同的系统域和应用域，不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互，而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-沙箱机制.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;自定义&lt;/h4&gt;
&lt;p&gt;对于自定义类加载器的实现，只需要继承 ClassLoader 类，覆写 findClass 方法即可&lt;/p&gt;
&lt;p&gt;作用：隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//自定义类加载器，读取指定的类路径classPath下的class文件
public class MyClassLoader extends ClassLoader{
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
     public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.classPath = classPath;
    }

    @Override
    protected Class&amp;lt;?&amp;gt; findClass(String name) throws ClassNotFoundException {
       BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            // 获取字节码文件的完整路径
            String fileName = classPath + className + &quot;.class&quot;;
            // 获取一个输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            // 获取一个输出流
            baos = new ByteArrayOutputStream();
            // 具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            // 获取内存中的完整的字节数组的数据
            byte[] byteCodes = baos.toByteArray();
            // 调用 defineClass()，将字节数组的数据转换为 Class 的实例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    MyClassLoader loader = new MyClassLoader(&quot;D:\Workspace\Project\JVM_study\src\java1\&quot;);

    try {
        Class clazz = loader.loadClass(&quot;Demo1&quot;);
        System.out.println(&quot;加载此类的类的加载器为：&quot; + clazz.getClassLoader().getClass().getName());//MyClassLoader

        System.out.println(&quot;加载当前类的类的加载器的父类加载器为：&quot; + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;JDK9&lt;/h4&gt;
&lt;p&gt;为了保证兼容性，JDK9 没有改变三层类加载器架构和双亲委派模型，但为了模块化系统的顺利运行做了一些变动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;扩展机制被移除，扩展类加载器由于&lt;strong&gt;向后兼容性&lt;/strong&gt;的原因被保留，不过被重命名为平台类加载器（platform classloader），可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK9 基于模块化进行构建（原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件），其中 Java 类库就满足了可扩展的需求，那就无须再保留 &lt;code&gt;&amp;lt;JAVA_HOME&amp;gt;\lib\ext&lt;/code&gt; 目录，此前使用这个目录或者 &lt;code&gt;java.ext.dirs&lt;/code&gt; 系统变量来扩展 JDK 功能的机制就不需要再存在&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动类加载器、平台类加载器、应用程序类加载器全都继承于 &lt;code&gt;jdk.internal.loader.BuiltinClassLoader&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;运行机制&lt;/h2&gt;
&lt;h3&gt;执行过程&lt;/h3&gt;
&lt;p&gt;Java 文件编译执行的过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java%E6%96%87%E4%BB%B6%E7%BC%96%E8%AF%91%E6%89%A7%E8%A1%8C%E7%9A%84%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类加载器：用于装载字节码文件（.class文件）&lt;/li&gt;
&lt;li&gt;运行时数据区：用于分配存储空间&lt;/li&gt;
&lt;li&gt;执行引擎：执行字节码文件或本地方法&lt;/li&gt;
&lt;li&gt;垃圾回收器：用于对 JVM 中的垃圾内容进行回收&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;字节码&lt;/h3&gt;
&lt;h4&gt;跨平台性&lt;/h4&gt;
&lt;p&gt;Java 语言：跨平台的语言（write once ，run anywhere）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 Java 源代码成功编译成字节码后，在不同的平台上面运行&lt;strong&gt;无须再次编译&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;让一个 Java 程序正确地运行在 JVM 中，Java 源码就必须要被编译为符合 JVM 规范的字节码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编译过程中的编译器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前端编译器： Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ，&lt;strong&gt;把源代码编译为字节码文件 .class&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IntelliJ IDEA 使用 javac 编译器&lt;/li&gt;
&lt;li&gt;Eclipse 中，当开发人员编写完代码后保存时，ECJ 编译器就会把未编译部分的源码逐行进行编译，而非每次都全量编译，因此 ECJ 的编译效率会比 javac 更加迅速和高效&lt;/li&gt;
&lt;li&gt;前端编译器并不会直接涉及编译优化等方面的技术，具体优化细节移交给 HotSpot 的 JIT 编译器负责&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后端运行期编译器：HotSpot VM 的 C1、C2 编译器，也就是 JIT 编译器，Graal 编译器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JIT 编译器：执行引擎部分详解&lt;/li&gt;
&lt;li&gt;Graal 编译器：JDK10 HotSpot 加入的一个全新的即时编译器，编译效果短短几年时间就追平了 C2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态提前编译器：AOT  (Ahead Of Time Compiler）编译器，直接把源代码编译成本地机器代码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 9 引入，是与即时编译相对立的一个概念，即时编译指的是在程序的运行过程中将字节码转换为机器码，AOT 是程序运行之前便将字节码转换为机器码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：JVM 加载已经预编译成二进制库，可以直接执行，不必等待即时编译器的预热，减少 Java 应用第一次运行慢的现象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;破坏了 Java &lt;strong&gt;一次编译，到处运行&lt;/strong&gt;，必须为每个不同硬件编译对应的发行包&lt;/li&gt;
&lt;li&gt;降低了 Java 链接过程的动态性，加载的代码在编译期就必须全部已知&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;语言发展&lt;/h4&gt;
&lt;p&gt;机器码：各种用二进制编码方式表示的指令，与 CPU 紧密相关，所以不同种类的 CPU 对应的机器指令不同&lt;/p&gt;
&lt;p&gt;指令：指令就是把机器码中特定的 0 和 1 序列，简化成对应的指令，例如 mov，inc 等，可读性稍好，但是不同的硬件平台的同一种指令（比如 mov），对应的机器码也可能不同&lt;/p&gt;
&lt;p&gt;指令集：不同的硬件平台支持的指令是有区别的，每个平台所支持的指令，称之为对应平台的指令集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;x86 指令集，对应的是 x86 架构的平台&lt;/li&gt;
&lt;li&gt;ARM 指令集，对应的是 ARM 架构的平台&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;汇编语言：用助记符代替机器指令的操作码，用地址符号或标号代替指令或操作数的地址&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在不同的硬件平台，汇编语言对应着不同的机器语言指令集，通过汇编过程转换成机器指令&lt;/li&gt;
&lt;li&gt;计算机只认识指令码，汇编语言编写的程序也必须翻译成机器指令码，计算机才能识别和执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;高级语言：为了使计算机用户编程序更容易些，后来就出现了各种高级计算机语言&lt;/p&gt;
&lt;p&gt;字节码：是一种中间状态（中间码）的二进制代码，比机器码更抽象，需要直译器转译后才能成为机器码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节码为了实现特定软件运行和软件环境，与硬件环境无关&lt;/li&gt;
&lt;li&gt;通过编译器和虚拟机器实现，编译器将源码编译成字节码，虚拟机器将字节码转译为可以直接执行的指令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-高级语言执行过程.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;类结构&lt;/h4&gt;
&lt;h5&gt;文件结构&lt;/h5&gt;
&lt;p&gt;字节码是一种二进制的类文件，是编译之后供虚拟机解释执行的二进制字节码文件，&lt;strong&gt;一个 class 文件对应一个 public 类型的类或接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;字节码内容是 &lt;strong&gt;JVM 的字节码指令&lt;/strong&gt;，不是机器码，C、C++ 经由编译器直接生成机器码，所以执行效率比 Java 高&lt;/p&gt;
&lt;p&gt;JVM 官方文档：https://docs.oracle.com/javase/specs/jvms/se8/html/index.html&lt;/p&gt;
&lt;p&gt;根据 JVM 规范，类文件结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassFile {
	u4 				magic;						
    u2 				minor_version;						
    u2 				major_version;						
    u2 				constant_pool_count;
    cp_info			constant_pool[constant_pool_count-1];
    u2	 			access_flags;
    u2 				this_class;
    u2 				super_class;
    u2 				interfaces_count;
    u2 				interfaces[interfaces_count];
    u2 				fields_count;
    field_info 		fields[fields_count];
    u2 				methods_count;
    method_info 	methods[methods_count];
    u2 				attributes_count;
    attribute_info 	attributes[attributes_count];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;长度&lt;/th&gt;
&lt;th&gt;数量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u4&lt;/td&gt;
&lt;td&gt;magic&lt;/td&gt;
&lt;td&gt;魔数，识别类文件格式&lt;/td&gt;
&lt;td&gt;4个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;minor_version&lt;/td&gt;
&lt;td&gt;副版本号(小版本)&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;major_version&lt;/td&gt;
&lt;td&gt;主版本号(大版本)&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;constant_pool_count&lt;/td&gt;
&lt;td&gt;常量池计数器&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cp_info&lt;/td&gt;
&lt;td&gt;constant_pool&lt;/td&gt;
&lt;td&gt;常量池表&lt;/td&gt;
&lt;td&gt;n个字节&lt;/td&gt;
&lt;td&gt;constant_pool_count-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;access_flags&lt;/td&gt;
&lt;td&gt;访问标识&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;this_class&lt;/td&gt;
&lt;td&gt;类索引&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;super_class&lt;/td&gt;
&lt;td&gt;父类索引&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;interfaces_count&lt;/td&gt;
&lt;td&gt;接口计数&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;interfaces&lt;/td&gt;
&lt;td&gt;接口索引集合&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;interfaces_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;fields_count&lt;/td&gt;
&lt;td&gt;字段计数器&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;field_info&lt;/td&gt;
&lt;td&gt;fields&lt;/td&gt;
&lt;td&gt;字段表&lt;/td&gt;
&lt;td&gt;n个字节&lt;/td&gt;
&lt;td&gt;fields_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;methods_count&lt;/td&gt;
&lt;td&gt;方法计数器&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;method_info&lt;/td&gt;
&lt;td&gt;methods&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;n个字节&lt;/td&gt;
&lt;td&gt;methods_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;attributes_count&lt;/td&gt;
&lt;td&gt;属性计数器&lt;/td&gt;
&lt;td&gt;2个字节&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attribute_info&lt;/td&gt;
&lt;td&gt;attributes&lt;/td&gt;
&lt;td&gt;属性表&lt;/td&gt;
&lt;td&gt;n个字节&lt;/td&gt;
&lt;td&gt;attributes_count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储，这种结构中只有两种数据类型：无符号数和表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无符号数属于基本的数据类型，以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数，无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串&lt;/li&gt;
&lt;li&gt;表是由多个无符号数或者其他表作为数据项构成的复合数据类型，表都以 &lt;code&gt;_info&lt;/code&gt; 结尾，用于描述有层次关系的数据，整个 Class 文件本质上就是一张表，由于表没有固定长度，所以通常会在其前面加上个数说明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HelloWorld.java 执行 &lt;code&gt;javac -parameters -d . HellowWorld.java&lt;/code&gt;指令&lt;/li&gt;
&lt;li&gt;写入文件指令 &lt;code&gt;javap -v xxx.class &amp;gt;xxx.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IDEA 插件 jclasslib&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;魔数版本&lt;/h5&gt;
&lt;p&gt;魔数：每个 Class 文件开头的 4 个字节的无符号整数称为魔数（Magic Number），是 Class 文件的标识符，代表这是一个能被虚拟机接受的有效合法的 Class 文件，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;魔数值固定为 0xCAFEBABE，不符合则会抛出错误&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑，因为文件扩展名可以随意地改动&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;版本：4 个 字节，5 6两个字节代表的是编译的副版本号 minor_version，而 7 8 两个字节是编译的主版本号 major_version&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的，高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件，反之 JVM 会抛出异常 &lt;code&gt;java.lang.UnsupportedClassVersionError&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;主版本（十进制）&lt;/th&gt;
&lt;th&gt;副版本（十进制）&lt;/th&gt;
&lt;th&gt;编译器版本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E7%B1%BB%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图片来源：https://www.bilibili.com/video/BV1PJ411n7xZ&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;常量池&lt;/h5&gt;
&lt;p&gt;常量池中常量的数量是不固定的，所以在常量池的入口需要放置一项 u2 类型的无符号数，代表常量池计数器（constant_pool_count），这个容量计数是从 1 而不是 0 开始，是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目，这种情况可用索引值 0 来表示&lt;/p&gt;
&lt;p&gt;constant_pool 是一种表结构，以1 ~ constant_pool_count - 1为索引，表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用，这部分内容将在类加载后进入方法区的运行时常量池&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字面量（Literal） ：基本数据类型、字符串类型常量、声明为 final 的常量值等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;符号引用（Symbolic References）：类和接口的全限定名、字段的名称和描述符、方法的名称和描述符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全限定名：com/test/Demo 这个就是类的全限定名，仅仅是把包名的 &lt;code&gt;.&lt;/code&gt; 替换成 &lt;code&gt;/&lt;/code&gt;，为了使连续的多个全限定名之间不产生混淆，在使用时最后一般会加入一个 &lt;code&gt;;&lt;/code&gt; 表示全限定名结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简单名称：指没有类型和参数修饰的方法或者字段名称，比如字段 x 的简单名称就是 x&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;描述符：用来描述字段的数据类型、方法的参数列表（包括数量、类型以及顺序）和返回值&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志符&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;基本数据类型 byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;基本数据类型 char&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;基本数据类型 double&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;基本数据类型 float&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;基本数据类型 int&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;td&gt;基本数据类型 long&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;基本数据类型 short&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;基本数据类型 boolean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;代表 void 类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;td&gt;对象类型，比如：&lt;code&gt;Ljava/lang/Object;&lt;/code&gt;，不同方法间用&lt;code&gt;;&lt;/code&gt;隔开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[&lt;/td&gt;
&lt;td&gt;数组类型，代表一维数组。比如：&lt;code&gt;double[][][] is [[[D&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常量类型和结构：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;标志(或标识)&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_utf8_info&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;UTF-8编码的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Integer_info&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;整型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Float_info&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;浮点型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Long_info&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;长整型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Double_info&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;双精度浮点型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Class_info&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;类或接口的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_String_info&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;字符串类型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Fieldref_info&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;字段的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Methodref_info&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;类中方法的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_InterfaceMethodref_info&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;接口中方法的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_NameAndType_info&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;字段或方法的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_MethodHandle_info&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;表示方法句柄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_MethodType_info&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;标志方法类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_InvokeDynamic_info&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;表示一个动态方法调用点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;18 种常量没有出现 byte、short、char，boolean 的原因：编译之后都可以理解为 Integer&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;访问标识&lt;/h5&gt;
&lt;p&gt;访问标识（access_flag），又叫访问标志、访问标记，该标识用两个字节表示，用于识别一些类或者接口层次的访问信息，包括这个 Class 是类还是接口，是否定义为 public类型，是否定义为 abstract类型等&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类的访问权限通常为 ACC_ 开头的常量&lt;/li&gt;
&lt;li&gt;每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的，比如若是 public final 的类，则该标记为 &lt;code&gt;ACC_PUBLIC | ACC_FINAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;ACC_SUPER&lt;/code&gt; 可以让类更准确地定位到父类的方法，确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义，现代编译器都会设置并且使用这个标记&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志名称&lt;/th&gt;
&lt;th&gt;标志值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PUBLIC&lt;/td&gt;
&lt;td&gt;0x0001&lt;/td&gt;
&lt;td&gt;标志为 public 类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_FINAL&lt;/td&gt;
&lt;td&gt;0x0010&lt;/td&gt;
&lt;td&gt;标志被声明为 final，只有类可以设置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_SUPER&lt;/td&gt;
&lt;td&gt;0x0020&lt;/td&gt;
&lt;td&gt;标志允许使用 invokespecial 字节码指令的新语义，JDK1.0.2之后编译出来的类的这个标志默认为真，使用增强的方法调用父类方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_INTERFACE&lt;/td&gt;
&lt;td&gt;0x0200&lt;/td&gt;
&lt;td&gt;标志这是一个接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_ABSTRACT&lt;/td&gt;
&lt;td&gt;0x0400&lt;/td&gt;
&lt;td&gt;是否为 abstract 类型，对于接口或者抽象类来说，次标志值为真，其他类型为假&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_SYNTHETIC&lt;/td&gt;
&lt;td&gt;0x1000&lt;/td&gt;
&lt;td&gt;标志此类并非由用户代码产生（由编译器产生的类，没有源码对应）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_ANNOTATION&lt;/td&gt;
&lt;td&gt;0x2000&lt;/td&gt;
&lt;td&gt;标志这是一个注解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_ENUM&lt;/td&gt;
&lt;td&gt;0x4000&lt;/td&gt;
&lt;td&gt;标志这是一个枚举&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;索引集合&lt;/h5&gt;
&lt;p&gt;类索引、父类索引、接口索引集合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类索引用于确定这个类的全限定名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父类索引用于确定这个类的父类的全限定名，Java 语言不允许多重继承，所以父类索引只有一个，除了Object 之外，所有的 Java 类都有父类，因此除了 java.lang.Object 外，所有 Java 类的父类索引都不为0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口索引集合就用来描述这个类实现了哪些接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interfaces_count 项的值表示当前类或接口的直接超接口数量&lt;/li&gt;
&lt;li&gt;interfaces[] 接口索引集合，被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;长度&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;this_class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;super_class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;interfaces_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;interfaces[interfaces_count]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;字段表&lt;/h5&gt;
&lt;p&gt;字段 fields 用于描述接口或类中声明的变量，包括类变量以及实例变量，但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型，都是无法固定的，只能引用常量池中的常量来描述&lt;/p&gt;
&lt;p&gt;fields_count（字段计数器），表示当前 class 文件 fields 表的成员个数，用两个字节来表示&lt;/p&gt;
&lt;p&gt;fields[]（字段表）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表中的每个成员都是一个 fields_info 结构的数据项，用于表示当前类或接口中某个字段的完整描述&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字段访问标识：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志名称&lt;/th&gt;
&lt;th&gt;标志值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PUBLIC&lt;/td&gt;
&lt;td&gt;0x0001&lt;/td&gt;
&lt;td&gt;字段是否为public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PRIVATE&lt;/td&gt;
&lt;td&gt;0x0002&lt;/td&gt;
&lt;td&gt;字段是否为private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PROTECTED&lt;/td&gt;
&lt;td&gt;0x0004&lt;/td&gt;
&lt;td&gt;字段是否为protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_STATIC&lt;/td&gt;
&lt;td&gt;0x0008&lt;/td&gt;
&lt;td&gt;字段是否为static&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_FINAL&lt;/td&gt;
&lt;td&gt;0x0010&lt;/td&gt;
&lt;td&gt;字段是否为final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_VOLATILE&lt;/td&gt;
&lt;td&gt;0x0040&lt;/td&gt;
&lt;td&gt;字段是否为volatile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_TRANSTENT&lt;/td&gt;
&lt;td&gt;0x0080&lt;/td&gt;
&lt;td&gt;字段是否为transient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_SYNCHETIC&lt;/td&gt;
&lt;td&gt;0x1000&lt;/td&gt;
&lt;td&gt;字段是否为由编译器自动产生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_ENUM&lt;/td&gt;
&lt;td&gt;0x4000&lt;/td&gt;
&lt;td&gt;字段是否为enum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字段名索引：根据该值查询常量池中的指定索引项即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;描述符索引：用来描述字段的数据类型、方法的参数列表和返回值&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字符&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;byte&lt;/td&gt;
&lt;td&gt;有符号字节型树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;char&lt;/td&gt;
&lt;td&gt;Unicode字符，UTF-16编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;双精度浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;float&lt;/td&gt;
&lt;td&gt;单精度浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;整型数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;td&gt;long&lt;/td&gt;
&lt;td&gt;长整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;short&lt;/td&gt;
&lt;td&gt;有符号短整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;布尔值true/false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;void&lt;/td&gt;
&lt;td&gt;代表void类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L Classname&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;td&gt;一个名为Classname的实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;td&gt;一个一维数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性表集合：属性个数存放在 attribute_count 中，属性具体内容存放在 attribute 数组中，一个字段还可能拥有一些属性，用于存储更多的额外信息，比如初始化值、一些注释信息等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ConstantValue_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于常量属性而言，attribute_length 值恒为2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;方法表&lt;/h5&gt;
&lt;p&gt;方法表是 methods 指向常量池索引集合，其中每一个 method_info 项都对应着一个类或者接口中的方法信息，完整描述了每个方法的签名&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个方法不是抽象的或者不是 native 的，字节码中就会体现出来&lt;/li&gt;
&lt;li&gt;methods 表只描述当前类或接口中声明的方法，不包括从父类或父接口继承的方法&lt;/li&gt;
&lt;li&gt;methods 表可能会出现由编译器自动添加的方法，比如初始化方法 &amp;lt;cinit&amp;gt; 和实例化方法 &amp;lt;init&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**重载（Overload）**一个方法，除了要与原方法具有相同的简单名称之外，还要求必须拥有一个与原方法不同的特征签名，特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合，因为返回值不会包含在特征签名之中，因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中，特征签名的范围更大一些，只要描述符不是完全一致的两个方法就可以共存&lt;/p&gt;
&lt;p&gt;methods_count（方法计数器）：表示 class 文件 methods 表的成员个数，使用两个字节来表示&lt;/p&gt;
&lt;p&gt;methods[]（方法表）：每个表项都是一个 method_info 结构，表示当前类或接口中某个方法的完整描述&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法表结构如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;数量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;access_flags&lt;/td&gt;
&lt;td&gt;访问标志&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;name_index&lt;/td&gt;
&lt;td&gt;字段名索引&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;descriptor_index&lt;/td&gt;
&lt;td&gt;描述符索引&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;attrubutes_count&lt;/td&gt;
&lt;td&gt;属性计数器&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attribute_info&lt;/td&gt;
&lt;td&gt;attributes&lt;/td&gt;
&lt;td&gt;属性集合&lt;/td&gt;
&lt;td&gt;attributes_count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法表访问标志：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志名称&lt;/th&gt;
&lt;th&gt;标志值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PUBLIC&lt;/td&gt;
&lt;td&gt;0x0001&lt;/td&gt;
&lt;td&gt;字段是否为 public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PRIVATE&lt;/td&gt;
&lt;td&gt;0x0002&lt;/td&gt;
&lt;td&gt;字段是否为 private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_PROTECTED&lt;/td&gt;
&lt;td&gt;0x0004&lt;/td&gt;
&lt;td&gt;字段是否为 protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_STATIC&lt;/td&gt;
&lt;td&gt;0x0008&lt;/td&gt;
&lt;td&gt;字段是否为 static&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_FINAL&lt;/td&gt;
&lt;td&gt;0x0010&lt;/td&gt;
&lt;td&gt;字段是否为 final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_VOLATILE&lt;/td&gt;
&lt;td&gt;0x0040&lt;/td&gt;
&lt;td&gt;字段是否为 volatile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_TRANSTENT&lt;/td&gt;
&lt;td&gt;0x0080&lt;/td&gt;
&lt;td&gt;字段是否为 transient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_SYNCHETIC&lt;/td&gt;
&lt;td&gt;0x1000&lt;/td&gt;
&lt;td&gt;字段是否为由编译器自动产生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACC_ENUM&lt;/td&gt;
&lt;td&gt;0x4000&lt;/td&gt;
&lt;td&gt;字段是否为 enum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;属性表&lt;/h5&gt;
&lt;p&gt;属性表集合，指的是 Class 文件所携带的辅助信息，比如该 Class 文件的源文件的名称，以及任何带有 &lt;code&gt;RetentionPolicy.CLASS&lt;/code&gt; 或者 &lt;code&gt;RetentionPolicy.RUNTIME&lt;/code&gt; 的注解，这类信息通常被用于 Java 虚拟机的验证和运行，以及 Java 程序的调试。字段表、方法表都可以有自己的属性表，用于描述某些场景专有的信息&lt;/p&gt;
&lt;p&gt;attributes_ count（属性计数器）：表示当前文件属性表的成员个数&lt;/p&gt;
&lt;p&gt;attributes[]（属性表）：属性表的每个项的值必须是 attribute_info 结构&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属性的通用格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ConstantValue_attribute{
    u2 attribute_name_index;	//属性名索引
    u4 attribute_length;		//属性长度
    u2 attribute_info;			//属性表
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性类型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名称&lt;/th&gt;
&lt;th&gt;使用位置&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;Java 代码编译成的字节码指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ConstantValue&lt;/td&gt;
&lt;td&gt;字段表&lt;/td&gt;
&lt;td&gt;final 关键字定义的常量池&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated&lt;/td&gt;
&lt;td&gt;类、方法、字段表&lt;/td&gt;
&lt;td&gt;被声明为 deprecated 的方法和字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exceptions&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;方法抛出的异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EnclosingMethod&lt;/td&gt;
&lt;td&gt;类文件&lt;/td&gt;
&lt;td&gt;仅当一个类为局部类或者匿名类是才能拥有这个属性，这个属性用于标识这个类所在的外围方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InnerClass&lt;/td&gt;
&lt;td&gt;类文件&lt;/td&gt;
&lt;td&gt;内部类列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LineNumberTable&lt;/td&gt;
&lt;td&gt;Code 属性&lt;/td&gt;
&lt;td&gt;Java 源码的行号与字节码指令的对应关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocalVariableTable&lt;/td&gt;
&lt;td&gt;Code 属性&lt;/td&gt;
&lt;td&gt;方法的局部变量描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StackMapTable&lt;/td&gt;
&lt;td&gt;Code 属性&lt;/td&gt;
&lt;td&gt;JDK1.6 中新增的属性，供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;td&gt;类，方法表，字段表&lt;/td&gt;
&lt;td&gt;用于支持泛型情况下的方法签名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SourceFile&lt;/td&gt;
&lt;td&gt;类文件&lt;/td&gt;
&lt;td&gt;记录源文件名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SourceDebugExtension&lt;/td&gt;
&lt;td&gt;类文件&lt;/td&gt;
&lt;td&gt;用于存储额外的调试信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syothetic&lt;/td&gt;
&lt;td&gt;类，方法表，字段表&lt;/td&gt;
&lt;td&gt;标志方法或字段为编泽器自动生成的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocalVariableTypeTable&lt;/td&gt;
&lt;td&gt;类&lt;/td&gt;
&lt;td&gt;使用特征签名代替描述符，是为了引入泛型语法之后能描述泛型参数化类型而添加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RuntimeVisibleAnnotations&lt;/td&gt;
&lt;td&gt;类，方法表，字段表&lt;/td&gt;
&lt;td&gt;为动态注解提供支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RuntimelnvisibleAnnotations&lt;/td&gt;
&lt;td&gt;类，方法表，字段表&lt;/td&gt;
&lt;td&gt;用于指明哪些注解是运行时不可见的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RuntimeVisibleParameterAnnotation&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;作用与 RuntimeVisibleAnnotations 属性类似，只不过作用对象为方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RuntirmelnvisibleParameterAnniotation&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;作用与 RuntimelnvisibleAnnotations 属性类似，作用对象哪个为方法参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AnnotationDefauit&lt;/td&gt;
&lt;td&gt;方法表&lt;/td&gt;
&lt;td&gt;用于记录注解类元素的默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BootstrapMethods&lt;/td&gt;
&lt;td&gt;类文件&lt;/td&gt;
&lt;td&gt;用于保存 invokeddynanic 指令引用的引导方式限定符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;编译指令&lt;/h4&gt;
&lt;h5&gt;javac&lt;/h5&gt;
&lt;p&gt;javac：编译命令，将 java 源文件编译成 class 字节码文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;javac xx.java&lt;/code&gt; 不会在生成对应的局部变量表等信息，使用 &lt;code&gt;javac -g xx.java&lt;/code&gt; 可以生成所有相关信息&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;javap&lt;/h5&gt;
&lt;p&gt;javap 反编译生成的字节码文件，根据 class 字节码文件，反解析出当前类对应的 code 区 （字节码指令）、局部变量表、异常表和代码行偏移量映射表、常量池等信息&lt;/p&gt;
&lt;p&gt;用法：javap &amp;lt;options&amp;gt; &amp;lt;classes&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-help  --help  -?        输出此用法消息
-version                 版本信息
-public                  仅显示公共类和成员
-protected               显示受保护的/公共类和成员
-package                 显示程序包/受保护的/公共类和成员 (默认)
-p  -private             显示所有类和成员
						 #常用的以下三个
-v  -verbose             输出附加信息
-l                       输出行号和本地变量表
-c                       对代码进行反汇编	#反编译

-s                       输出内部类型签名
-sysinfo                 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)
-constants               显示最终常量
-classpath &amp;lt;path&amp;gt;        指定查找用户类文件的位置
-cp &amp;lt;path&amp;gt;               指定查找用户类文件的位置
-bootclasspath &amp;lt;path&amp;gt;    覆盖引导类文件的位置
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;指令集&lt;/h4&gt;
&lt;h5&gt;执行指令&lt;/h5&gt;
&lt;p&gt;Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码（opcode）以及零至多个代表此操作所需参数的操作数（operand）所构成，虚拟机中许多指令并不包含操作数，只有一个操作码（零地址指令）&lt;/p&gt;
&lt;p&gt;由于限制了 Java 虚拟机操作码的长度为一个字节（0~255），所以指令集的操作码总数不可能超过 256 条&lt;/p&gt;
&lt;p&gt;在 JVM 的指令集中，大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中，而 fload 指令加载的则是 float 类型的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;i 代表对 int 类型的数据操作&lt;/li&gt;
&lt;li&gt;l 代表 long&lt;/li&gt;
&lt;li&gt;s 代表 short&lt;/li&gt;
&lt;li&gt;b 代表 byte&lt;/li&gt;
&lt;li&gt;c 代表 char&lt;/li&gt;
&lt;li&gt;f 代表 float&lt;/li&gt;
&lt;li&gt;d 代表 double&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大部分的指令都没有支持 byte、char、short、boolean 类型，编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展（Sign-Extend-）为相应的 int 类型数据，将 boolean 和 char 类型数据零位扩展（Zero-Extend）为相应的 int 类型数据&lt;/p&gt;
&lt;p&gt;在做值相关操作时:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个指令，可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据，这些数据（可能是值，也可能是对象的引用）被压入操作数栈&lt;/li&gt;
&lt;li&gt;一个指令，也可以从操作数栈中取出一到多个值（pop 多次），完成赋值、加减乘除、方法传参、系统调用等等操作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;加载存储&lt;/h5&gt;
&lt;p&gt;加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递&lt;/p&gt;
&lt;p&gt;局部变量压栈指令：将给定的局部变量表中的数据压入操作数栈&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xload、xload_n，x 表示取值数据类型，为 i、l、f、d、a， n 为 0 到 3&lt;/li&gt;
&lt;li&gt;指令 xload_n 表示将第 n 个局部变量压入操作数栈，aload_n 表示将一个对象引用压栈&lt;/li&gt;
&lt;li&gt;指令 xload n 通过指定参数的形式，把局部变量压入操作数栈，局部变量数量超过 4 个时使用这个命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常量入栈指令：将常数压入操作数栈，根据数据类型和入栈内容的不同，又分为 const、push、ldc 指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;push：包括 bipush 和 sipush，区别在于接收数据类型的不同，bipush 接收 8 位整数作为参数，sipush 接收 16 位整数&lt;/li&gt;
&lt;li&gt;ldc：如果以上指令不能满足需求，可以使用 ldc 指令，接收一个 8 位的参数，该参数指向常量池中的 int、 float 或者 String 的索引，将指定的内容压入堆栈。ldc_w 接收两个 8 位参数，能支持的索引范围更大，如果要压入的元素是 long 或 double 类型的，则使用 ldc2_w 指令&lt;/li&gt;
&lt;li&gt;aconst_null 将 null 对象引用压入栈，iconst_m1 将 int 类型常量 -1 压入栈，iconst_0 将 int 类型常量 0 压入栈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;出栈装入局部变量表指令：将操作数栈中栈顶元素弹出后，装入局部变量表的指定位置，用于给局部变量赋值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xstore、xstore_n，x 表示取值类型为 i、l、f、d、a， n 为 0 到 3&lt;/li&gt;
&lt;li&gt;xastore 表示存入数组，x 取值为 i、l、f、d、a、b、c、s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩充局部变量表的访问索引的指令：wide&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;算术指令&lt;/h5&gt;
&lt;p&gt;算术指令用于对两个操作数栈上的值进行某种特定运算，并把计算结果重新压入操作数栈&lt;/p&gt;
&lt;p&gt;没有直接支持 byte、 short、 char 和 boolean 类型的算术指令，对于这些数据的运算，都使用 int 类型的指令来处理，数组类型也是转换成 int 数组&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加法指令：iadd、ladd、fadd、dadd&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;减法指令：isub、lsub、fsub、dsub&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;乘法指令：imu、lmu、fmul、dmul&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除法指令：idiv、ldiv、fdiv、ddiv&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;求余指令：irem、lrem、frem、drem（remainder 余数）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取反指令：ineg、lneg、fneg、dneg （negation 取反）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自增指令：iinc（直接&lt;strong&gt;在局部变量 slot 上进行运算&lt;/strong&gt;，不用放入操作数栈）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位运算指令，又可分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;位移指令：ishl、ishr、 iushr、lshl、lshr、 lushr&lt;/li&gt;
&lt;li&gt;按位或指令：ior、lor&lt;/li&gt;
&lt;li&gt;按位与指令：iand、land&lt;/li&gt;
&lt;li&gt;按位异或指令：ixor、lxor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较指令：dcmpg、dcmpl、 fcmpg、fcmpl、lcmp&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运算模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向最接近数舍入模式，JVM 在进行浮点数计算时，所有的运算结果都必须舍入到适当的精度，非精确结果必须舍入为可被表示的最接近的精确值，如果有两种可表示形式与该值一样接近，将优先选择最低有效位为零的&lt;/li&gt;
&lt;li&gt;向零舍入模式：将浮点数转换为整数时，该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NaN 值：当一个操作产生溢出时，将会使用有符号的无穷大表示，如果某个操作结果没有明确的数学定义，将使用 NaN 值来表示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double j = i / 0.0;
System.out.println(j);//无穷大，NaN: not a number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;分析 i++&lt;/strong&gt;：从字节码角度分析：a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 4 iload_1		//存入操作数栈
 5 iinc 1 by 1	//自增i++
 8 istore_3		//把操作数栈没有自增的数据的存入局部变量表
 9 iinc 2 by 1	//++i
12 iload_2		//加载到操作数栈
13 istore 4		//存入局部变量表，这个存入没有 _ 符号，_只能到3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);	//11
        System.out.println(b);	//34
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i &amp;lt; 10) {
            x = x++;
            i++;
        }
        System.out.println(x); // 结果是 0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;类型转换&lt;/h5&gt;
&lt;p&gt;类型转换指令可以将两种不同的数值类型进行相互转换，除了 boolean 之外的七种类型&lt;/p&gt;
&lt;p&gt;宽化类型转换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JVM 支持以下数值的宽化类型转换（widening numeric conversion），小范围类型到大范围类型的安全转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 int 类型到 long、float 或者 double 类型，对应的指令为 i2l、i2f、i2d&lt;/li&gt;
&lt;li&gt;从 long 类型到 float、 double 类型，对应的指令为 l2f、l2d&lt;/li&gt;
&lt;li&gt;从 float 类型到 double 类型，对应的指令为 f2d&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;精度损失问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宽化类型转换是不会因为超过目标类型最大值而丢失信息&lt;/li&gt;
&lt;li&gt;从 int 转换到 float 或者 long 类型转换到 double 时，将可能发生精度丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的，JVM 把它们当作 int 处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;窄化类型转换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java 虚拟机直接支持以下窄化类型转换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 int 类型至 byte、 short 或者 char 类型，对应的指令有 i2b、i2c、i2s&lt;/li&gt;
&lt;li&gt;从 long 类型到 int 类型，对应的指令有 l2i&lt;/li&gt;
&lt;li&gt;从 float 类型到 int 或者 long 类型，对应的指令有:f2i、f2l&lt;/li&gt;
&lt;li&gt;从 double 类型到 int、long 或 float 者类型，对应的指令有 d2i、d2、d2f&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;精度损失问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级，转换过程可能会导致数值丢失精度&lt;/li&gt;
&lt;li&gt;将一个浮点值窄化转换为整数类型 T（T 限于 int 或 long 类型之一）时，将遵循以下转换规则：
&lt;ul&gt;
&lt;li&gt;如果浮点值是 NaN，那转换结果就是 int 或 long 类型的 0&lt;/li&gt;
&lt;li&gt;如果浮点值不是无穷大的话，浮点值使用 IEEE 754 的向零舍入模式取整，获得整数值 v，如果 v 在目标类型 T 的表示范围之内，那转换结果就是 v，否则将根据 v 的符号，转换为 T 所能表示的最大或者最小正数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;创建访问&lt;/h5&gt;
&lt;p&gt;创建指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建类实例指令：new，接收一个操作数指向常量池的索引，表示要创建的类型，执行完成后将对象的引用压入栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0:  new             #2 // class com/jvm/bytecode/Demo
3:  dup
4:  invokespecial   #3 // Method &quot;&amp;lt;init&amp;gt;&quot;:()V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;dup 是复制操作数栈栈顶的内容&lt;/strong&gt;，需要两份引用原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个要配合 invokespecial 调用该对象的构造方法 &amp;lt;init&amp;gt;:()V （会消耗掉栈顶一个引用）&lt;/li&gt;
&lt;li&gt;一个要配合 astore_1 赋值给局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数组的指令：newarray、anewarray、multianewarray&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;newarray：创建基本类型数组&lt;/li&gt;
&lt;li&gt;anewarray：创建引用类型数组&lt;/li&gt;
&lt;li&gt;multianewarray：创建多维数组&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字段访问指令：对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问类字段（static字段，或者称为类变量）的指令：getstatic、putstatic&lt;/li&gt;
&lt;li&gt;访问类实例字段（非static字段，或者称为实例变量）的指令：getfield、 putfield&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类型检查指令：检查类实例或数组类型的指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;checkcast：用于检查类型强制转换是否可以进行，如果可以进行 checkcast 指令不会改变操作数栈，否则它会抛出 ClassCastException 异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;instanceof：判断给定对象是否是某一个类的实例，会将判断结果压入操作数栈&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;方法指令&lt;/h5&gt;
&lt;p&gt;方法调用指令：invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法调用章节详解&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;操作数栈&lt;/h5&gt;
&lt;p&gt;JVM 提供的操作数栈管理指令，可以用于直接操作操作数栈的指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pop、pop2：将一个或两个元素从栈顶弹出，并且直接废弃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dup、dup2，dup_x1、dup2_x1，dup_x2、dup2_x2：复制栈顶一个或两个数值并重新压入栈顶&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;swap：将栈最顶端的两个 slot 数值位置交换，JVM 没有提供交换两个 64 位数据类型数值的指令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nop：一个非常特殊的指令，字节码为 0x00，和汇编语言中的 nop 一样，表示什么都不做，一般可用于调试、占位等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;控制转移&lt;/h5&gt;
&lt;p&gt;比较指令：比较栈顶两个元素的大小，并将比较结果入栈&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lcmp：比较两个 long 类型值&lt;/li&gt;
&lt;li&gt;fcmpl：比较两个 float 类型值（当遇到NaN时，返回-1）&lt;/li&gt;
&lt;li&gt;fcmpg：比较两个 float 类型值（当遇到NaN时，返回1）&lt;/li&gt;
&lt;li&gt;dcmpl：比较两个 double 类型值（当遇到NaN时，返回-1）&lt;/li&gt;
&lt;li&gt;dcmpg：比较两个 double 类型值（当遇到NaN时，返回1）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;条件跳转指令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ifeq&lt;/td&gt;
&lt;td&gt;equals，当栈顶int类型数值等于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifne&lt;/td&gt;
&lt;td&gt;not equals，当栈顶in类型数值不等于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iflt&lt;/td&gt;
&lt;td&gt;lower than，当栈顶in类型数值小于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifle&lt;/td&gt;
&lt;td&gt;lower or equals，当栈顶in类型数值小于等于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifgt&lt;/td&gt;
&lt;td&gt;greater than，当栈顶int类型数组大于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifge&lt;/td&gt;
&lt;td&gt;greater or equals，当栈顶in类型数值大于等于0时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifnull&lt;/td&gt;
&lt;td&gt;为 null 时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ifnonnull&lt;/td&gt;
&lt;td&gt;不为 null 时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;比较条件跳转指令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;if_icmpeq&lt;/td&gt;
&lt;td&gt;比较栈顶两 int 类型数值大小（下同），当前者等于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_icmpne&lt;/td&gt;
&lt;td&gt;当前者不等于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_icmplt&lt;/td&gt;
&lt;td&gt;当前者小于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_icmple&lt;/td&gt;
&lt;td&gt;当前者小于等于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_icmpgt&lt;/td&gt;
&lt;td&gt;当前者大于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_icmpge&lt;/td&gt;
&lt;td&gt;当前者大于等于后者时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_acmpeq&lt;/td&gt;
&lt;td&gt;当结果相等时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;if_acmpne&lt;/td&gt;
&lt;td&gt;当结果不相等时跳转&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;多条件分支跳转指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tableswitch：用于 switch 条件跳转，case 值连续&lt;/li&gt;
&lt;li&gt;lookupswitch：用于 switch 条件跳转，case 值不连续&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无条件跳转指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;goto：用来进行跳转到指定行号的字节码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;goto_w：无条件跳转（宽索引）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;异常处理&lt;/h5&gt;
&lt;h6&gt;处理机制&lt;/h6&gt;
&lt;p&gt;抛出异常指令：athrow 指令&lt;/p&gt;
&lt;p&gt;JVM 处理异常（catch 语句）不是由字节码指令来实现的，而是&lt;strong&gt;采用异常表来完成&lt;/strong&gt;的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {    
    int i = 0;    
    try {    	
        i = 10;    
    } catch (Exception e) {   
        i = 20;   
    } finally {
        i = 30;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字节码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多出一个 &lt;strong&gt;Exception table&lt;/strong&gt; 的结构，&lt;strong&gt;[from, to) 是前闭后开的检测范围&lt;/strong&gt;，一旦这个范围内的字节码执行出现异常，则通过 type 匹配异常类型，如果一致，进入 target 所指示行号&lt;/li&gt;
&lt;li&gt;11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置，因为异常出现时，只能进入 Exception table 中一个分支，所以局部变量表 slot 2 位置被共用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    0: 	iconst_0
    1: 	istore_1 	// 0 -&amp;gt; i	-&amp;gt;赋值
    2: 	bipush 10 	// try 10 放入操作数栈顶
    4: 	istore_1 	// 10 -&amp;gt; i 将操作数栈顶数据弹出，存入局部变量表的 slot1
    5: 	bipush 30 	// 【finally】 
    7: 	istore_1 	// 30 -&amp;gt; i 
    8: 	goto 27 	// return -----------------------------------
    11: astore_2 	// catch Exceptin -&amp;gt; e ----------------------
    12: bipush 20 	// 
    14: istore_1 	// 20 -&amp;gt; i 
    15: bipush 30 	// 【finally】 
    17: istore_1 	// 30 -&amp;gt; i 
    18: goto 27 	// return -----------------------------------
    21: astore_3 	// catch any -&amp;gt; slot 3 ----------------------
    22: bipush 30 	// 【finally】
    24: istore_1 	// 30 -&amp;gt; i 
    25: aload_3 	// 将局部变量表的slot 3数据弹出，放入操作数栈栈顶
    26: athrow 		// throw 抛出异常
    27: return
Exception table:
	// 任何阶段出现任务异常都会执行 finally
	from   to 	target 	type
		2	5 		11 	Class java/lang/Exception
		2 	5 		21 	any // 剩余的异常类型，比如 Error
		11 15 		21 	any // 剩余的异常类型，比如 Error
LineNumberTable: ...
LocalVariableTable:
	Start Length Slot Name Signature
	12 		3 		2 	e 	Ljava/lang/Exception;
	0 		28 		0 args 	[Ljava/lang/String;
	2 		26 		1 	i 	I
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;finally&lt;/h6&gt;
&lt;p&gt;finally 中的代码被&lt;strong&gt;复制了 3 份&lt;/strong&gt;，分别放入 try 流程，catch 流程以及 catch 剩余的异常类型流程（上节案例）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static int test() {
    try {
    	return 10;
    } finally {
    	return 20;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字节码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    0: bipush 10 	// 10 放入栈顶
    2: istore_0 	// 10 -&amp;gt; slot 0 【从栈顶移除了】
    3: bipush 20 	// 20 放入栈顶
    5: ireturn 		// 返回栈顶 int(20)
    6: astore_1 	// catch any 存入局部变量表的 slot1
    7: bipush 20 	// 20 放入栈顶
    9: ireturn 		// 返回栈顶 int(20)
Exception table:
	from   to 	target 	type
		0	3		6 	any      
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;return&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;吞异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static int test() {
    try {
    	return 10;
    } finally {
    	return 20;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    0: bipush 10 	// 10 放入栈顶
    2: istore_0 	// 10 -&amp;gt; slot 0 【从栈顶移除了】
    3: bipush 20 	// 20 放入栈顶
    5: ireturn 		// 返回栈顶 int(20)
    6: astore_1 	// catch any  存入局部变量表的 slot1
    7: bipush 20 	// 20 放入栈顶
    9: ireturn 		// 返回栈顶 int(20)
Exception table:
	from   to 	target 	type
		0	3		6 	any      
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于 finally 中的 ireturn 被插入了所有可能的流程，因此返回结果以 finally 的为准&lt;/li&gt;
&lt;li&gt;字节码中没有 &lt;strong&gt;athrow&lt;/strong&gt; ，表明如果在 finally 中出现了 return，会&lt;strong&gt;吞掉异常&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不吞异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    public static void main(String[] args) {
    	int result = test();
    	System.out.println(result);//10
	}
	public static int test() {
        int i = 10;
        try {
            return i;//返回10
        } finally {
            i = 20;
        }
   	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    0: 	bipush 10 	// 10 放入栈顶
    2: 	istore_0 	// 10 赋值给i，放入slot 0
    3: 	iload_0 	// i(10)加载至操作数栈
    4: 	istore_1 	// 10 -&amp;gt; slot 1，【暂存至 slot 1，目的是为了固定返回值】
    5: 	bipush 20 	// 20 放入栈顶
    7: 	istore_0 	// 20 slot 0
    8: 	iload_1 	// slot 1(10) 载入 slot 1 暂存的值
    9: 	ireturn 	// 返回栈顶的 int(10)
    10: astore_2	// catch any -&amp;gt; slot 2 存入局部变量表的 slot2
    11: bipush 20
    13: istore_0
    14: aload_2
    15: athrow		// 不会吞掉异常
Exception table:
	from   to 	target 	type
	  3	   5		10 	any  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;同步控制&lt;/h5&gt;
&lt;p&gt;方法级的同步：是隐式的，无须通过字节码指令来控制，它实现在方法调用和返回操作之中，虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法&lt;/p&gt;
&lt;p&gt;方法内指定指令序列的同步：有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;montiorenter：进入并获取对象监视器，即为栈顶对象加锁&lt;/li&gt;
&lt;li&gt;monitorexit：释放并退出对象监视器，即为栈顶对象解锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码指令同步控制.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;执行流程&lt;/h4&gt;
&lt;p&gt;原始 Java 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {	
    public static void main(String[] args) {        
        int a = 10;        
        int b = Short.MAX_VALUE + 1;        
        int c = a + b;        
        System.out.println(c);	
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;javap -v Demo.class：省略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常量池载入运行时常量池&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法区字节码载入方法区&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;main 线程开始运行，分配栈帧内存：（操作数栈stack=2，局部变量表locals=4）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行引擎&lt;/strong&gt;开始执行字节码&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bipush 10&lt;/code&gt;：将一个 byte 压入操作数栈（其长度会补齐 4 个字节），类似的指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sipush 将一个 short 压入操作数栈（其长度会补齐 4 个字节）&lt;/li&gt;
&lt;li&gt;ldc 将一个 int 压入操作数栈&lt;/li&gt;
&lt;li&gt;ldc2_w 将一个 long 压入操作数栈（分两次压入，因为 long 是 8 个字节）&lt;/li&gt;
&lt;li&gt;这里小的数字都是和字节码指令存在一起，超过 short 范围的数字存入了常量池&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;istore_1&lt;/code&gt;：将操作数栈顶数据弹出，存入局部变量表的 slot 1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ldc #3&lt;/code&gt;：从常量池加载 #3 数据到操作数栈
Short.MAX_VALUE 是 32767，所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;istore_2&lt;/code&gt;：将操作数栈顶数据弹出，存入局部变量表的 slot 2&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iload_1&lt;/code&gt;：将局部变量表的 slot 1 数据弹出，放入操作数栈栈顶&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iload_2&lt;/code&gt;：将局部变量表的 slot 2 数据弹出，放入操作数栈栈顶&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iadd&lt;/code&gt;：执行相加操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;istore_3&lt;/code&gt;：将操作数栈顶数据弹出，存入局部变量表的 slot 3&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getstatic #4&lt;/code&gt;：获取静态字段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iload_3&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;invokevirtual #5&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到常量池 #5 项&lt;/li&gt;
&lt;li&gt;定位到方法区 java/io/PrintStream.println:(I)V 方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成新的栈帧&lt;/strong&gt;（分配 locals、stack等）&lt;/li&gt;
&lt;li&gt;传递参数，执行新栈帧中的字节码&lt;/li&gt;
&lt;li&gt;执行完毕，弹出栈帧&lt;/li&gt;
&lt;li&gt;清除 main 操作数栈内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;return：完成 main 方法调用，弹出 main 栈帧，程序结束&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;执行引擎&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;执行引擎：Java 虚拟机的核心组成部分之一，类加载主要任务是负责装载字节码到其内部，但字节码并不能够直接运行在操作系统之上，需要执行引擎将&lt;strong&gt;字节码指令解释/编译为对应平台上的本地机器指令&lt;/strong&gt;，进行执行&lt;/p&gt;
&lt;p&gt;虚拟机是一个相对于物理机的概念，这两种机器都有代码执行能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上&lt;/li&gt;
&lt;li&gt;虚拟机的执行引擎是由软件自行实现的，可以不受物理条件制约地定制指令集与执行引擎的结构体系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 是&lt;strong&gt;半编译半解释型语言&lt;/strong&gt;，将解释执行与编译执行二者结合起来进行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解释器：根据预定义的规范对字节码采用逐行解释的方式执行，将每条字节码文件中的内容翻译为对应平台的本地机器指令执行&lt;/li&gt;
&lt;li&gt;即时编译器（JIT : Just In Time Compiler）：虚拟机运行时将源代码直接编译成&lt;strong&gt;和本地机器平台相关的机器码&lt;/strong&gt;后再执行，并存入 Code Cache，下次遇到相同的代码直接执行，效率高&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;执行方式&lt;/h4&gt;
&lt;p&gt;HotSpot VM 采用&lt;strong&gt;解释器与即时编译器并存的架构&lt;/strong&gt;，解释器和即时编译器能够相互协作，去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间&lt;/p&gt;
&lt;p&gt;HostSpot JVM 的默认执行方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当程序启动后，解释器可以马上发挥作用立即执行，省去编译器编译的时间（解释器存在的&lt;strong&gt;必要性&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;随着程序运行时间的推移，即时编译器逐渐发挥作用，根据热点探测功能，将有价值的字节码编译为本地机器指令，以换取更高的程序执行效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HotSpot VM 可以通过 VM 参数设置程序执行方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-Xint：完全采用解释器模式执行程序&lt;/li&gt;
&lt;li&gt;-Xcomp：完全采用即时编译器模式执行程序。如果即时编译出现问题，解释器会介入执行&lt;/li&gt;
&lt;li&gt;-Xmixed：采用解释器 + 即时编译器的混合模式共同执行程序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;热点探测&lt;/h4&gt;
&lt;p&gt;热点代码：被 JIT 编译器编译的字节码，根据代码被调用执行的频率而定，一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码&lt;/p&gt;
&lt;p&gt;热点探测：JIT 编译器在运行时会针热点代码做出深度优化，将其直接编译为对应平台的本地机器指令进行缓存，以提升程序执行性能&lt;/p&gt;
&lt;p&gt;JIT 编译在默认情况是异步进行的，当触发某方法或某代码块的优化时，先将其放入编译队列，然后由编译线程进行编译，编译之后的代码放在 CodeCache 中，通过 &lt;code&gt;-XX:-BackgroundCompilation&lt;/code&gt; 参数可以关闭异步编译&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CodeCache&lt;/strong&gt; 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI&lt;/li&gt;
&lt;li&gt;如果 CodeCache 区域被占满，编译器被停用，字节码将不会编译为机器码，应用程序继续运行，但运行性能会降低很多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HotSpot VM 采用的热点探测方式是基于计数器的热点探测，为每一个方法都建立 2 个不同类型的计数器：方法调用计数器（Invocation Counter）和回边计数器（BackEdge Counter）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法调用计数器：用于统计方法被调用的次数，默认阈值在 Client 模式 下是 1500 次，在 Server 模式下是 10000 次（需要进行激进的优化），超过这个阈值，就会触发 JIT 编译，阈值可以通过虚拟机参数 &lt;code&gt;-XX:CompileThreshold&lt;/code&gt; 设置&lt;/p&gt;
&lt;p&gt;工作流程：当一个方法被调用时， 会先检查该方法是否存在被 JIT 编译过的版本，存在则使用编译后的本地代码来执行；如果不存在则将此方法的调用计数器值加 1，然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值，如果超过阈值会向即时编译器&lt;strong&gt;提交一个该方法的代码编译请求&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回边计数器：统计一个方法中循环体代码执行的次数，在字节码中控制流向后跳转的指令称为回边&lt;/p&gt;
&lt;p&gt;如果一个方法中的循环体需要执行多次，可以优化为为栈上替换，简称 OSR (On StackReplacement) 编译，&lt;strong&gt;OSR 替换循环代码体的入口，C1、C2 替换的是方法调用的入口&lt;/strong&gt;，OSR 编译后会出现方法的整段代码被编译了，但是只有循环体部分才执行编译后的机器码，其他部分仍是解释执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分层编译&lt;/h4&gt;
&lt;p&gt;HotSpot VM 内嵌两个 JIT 编译器，分别为 Client Compiler 和 Server Compiler，简称 C1 编译器和 C2 编译器&lt;/p&gt;
&lt;p&gt;C1 编译器会对字节码进行简单可靠的优化，耗时短，以达到更快的编译速度，C1 编译器的优化方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法内联：&lt;strong&gt;将调用的函数代码编译到调用点处&lt;/strong&gt;，这样可以减少栈帧的生成，减少参数传递以及跳转过程&lt;/p&gt;
&lt;p&gt;方法内联能够消除方法调用的固定开销，任何方法除非被内联，否则调用都会有固定开销，来源于保存程序在该方法中的执行位置，以及新建、压入和弹出新方法所使用的栈帧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int square(final int i) {
	return i * i;
}
System.out.println(square(9));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;square 是热点方法，会进行内联，把方法内代码拷贝粘贴到调用者的位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System.out.println(9 * 9);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还能够进行常量折叠（constant folding）的优化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System.out.println(81);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;冗余消除：根据运行时状况进行代码折叠或削除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内联缓存：是一种加快动态绑定的优化技术（方法调用部分详解）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C2 编译器进行耗时较长的优化以及激进优化，优化的代码执行效率更高，当激进优化的假设不成立时，再退回使用 C1 编译，这也是使用分层编译的原因&lt;/p&gt;
&lt;p&gt;C2 的优化主要是在全局层面，逃逸分析是优化的基础：标量替换、栈上分配、同步消除&lt;/p&gt;
&lt;p&gt;VM 参数设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-client：指定 Java 虚拟机运行在 Client 模式下，并使用 C1 编译器&lt;/li&gt;
&lt;li&gt;-server：指定 Java 虚拟机运行在 Server 模式下，并使用 C2 编译器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-server -XX:+TieredCompilation&lt;/code&gt;：在 1.8 之前，分层编译默认是关闭的，可以添加该参数开启&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分层编译策略 (Tiered Compilation)：程序解释执行可以触发 C1 编译，将字节码编译成机器码，加上性能监控，C2 编译会根据性能监控信息进行激进优化，JVM 将执行状态分成了 5 个层次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;0 层，解释执行（Interpreter）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1 层，使用 C1 即时编译器编译执行（不带 profiling）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 层，使用 C1 即时编译器编译执行（带基本的 profiling）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3 层，使用 C1 即时编译器编译执行（带完全的 profiling）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4 层，使用 C2 即时编译器编译执行（C1 和 C2 协作运行）&lt;/p&gt;
&lt;p&gt;说明：profiling 是指在运行过程中收集一些程序执行状态的数据，例如方法的调用次数，循环的回边次数等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/20bd2e9b1f03&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;方法调用&lt;/h3&gt;
&lt;h4&gt;方法识别&lt;/h4&gt;
&lt;p&gt;Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符（method descriptor）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方法描述符是由方法的参数类型以及返回类型所构成&lt;/strong&gt;，Java 层面叫方法特征签名&lt;/li&gt;
&lt;li&gt;在同一个类中，如果同时出现多个名字相同且描述符也相同的方法，那么 Java 虚拟机会在类的验证阶段报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JVM 根据名字和描述符来判断的，只要返回值不一样（方法描述符不一样），其它完全一样，在 JVM 中是允许的，但 Java 语言不允许&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 返回值类型不同，编译阶段直接报错
public static Integer invoke(Object... args) {
    return 1;
}
public static int invoke(Object... args) {
    return 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;调用机制&lt;/h4&gt;
&lt;p&gt;方法调用并不等于方法执行，方法调用阶段唯一的任务就是&lt;strong&gt;确定被调用方法的版本&lt;/strong&gt;，不是方法的具体运行过程&lt;/p&gt;
&lt;p&gt;在 JVM 中，将符号引用转换为直接引用有两种机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态链接：当一个字节码文件被装载进 JVM 内部时，如果被调用的目标方法在编译期可知，且运行期保持不变，将调用方法的符号引用转换为直接引用的过程称之为静态链接（类加载的解析阶段）&lt;/li&gt;
&lt;li&gt;动态链接：被调用的方法在编译期无法被确定下来，只能在程序运行期将调用方法的符号引用转换为直接引用，由于这种引用转换过程具备动态性，因此被称为动态链接（初始化后的解析阶段）&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;对应方法的绑定（分配）机制：静态绑定和动态绑定，编译器已经区分了重载的方法（静态绑定和动态绑定），因此可以认为虚拟机中不存在重载&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;非虚方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非虚方法在编译期就确定了具体的调用版本，这个版本在运行时是不可变的&lt;/li&gt;
&lt;li&gt;静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法&lt;/li&gt;
&lt;li&gt;所有普通成员方法、实例方法、被重写的方法都是虚方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;动态类型语言和静态类型语言：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在于对类型的检查是在编译期还是在运行期，满足前者就是静态类型语言，反之则是动态类型语言&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态语言是判断变量自身的类型信息；动态类型语言是判断变量值的类型信息，变量没有类型信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Java 是静态类型语言&lt;/strong&gt;（尽管 Lambda 表达式为其增加了动态特性），JS，Python 是动态类型语言&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;abc&quot;;   //Java
info = &quot;abc&quot;;       //Python
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;调用指令&lt;/h4&gt;
&lt;h5&gt;五种指令&lt;/h5&gt;
&lt;p&gt;普通调用指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;invokestatic：调用静态方法&lt;/li&gt;
&lt;li&gt;invokespecial：调用私有方法、构造器，和父类的实例方法或构造器，以及所实现接口的默认方法&lt;/li&gt;
&lt;li&gt;invokevirtual：调用所有虚方法（虚方法分派）&lt;/li&gt;
&lt;li&gt;invokeinterface：调用接口方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;动态调用指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;invokedynamic：动态解析出需要调用的方法
&lt;ul&gt;
&lt;li&gt;Java7 为了实现动态类型语言支持而引入了该指令，但是并没有提供直接生成 invokedynamic 指令的方法，需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令&lt;/li&gt;
&lt;li&gt;Java8 的 lambda 表达式的出现，invokedynamic 指令在 Java 中才有了直接生成方式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指令对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通调用指令固化在虚拟机内部，方法的调用执行不可干预，根据方法的符号引用链接到具体的目标方法&lt;/li&gt;
&lt;li&gt;动态调用指令支持用户确定方法&lt;/li&gt;
&lt;li&gt;invokestatic 和 invokespecial 指令调用的方法称为非虚方法，虚拟机能够直接识别具体的目标方法&lt;/li&gt;
&lt;li&gt;invokevirtual 和 invokeinterface 指令调用的方法称为虚方法，虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指令说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果虚拟机能够确定目标方法有且仅有一个，比如说目标方法被标记为 final，那么可以不通过动态绑定，直接确定目标方法&lt;/li&gt;
&lt;li&gt;普通成员方法是由 invokevirtual 调用，属于&lt;strong&gt;动态绑定&lt;/strong&gt;，即支持多态&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;符号引用&lt;/h5&gt;
&lt;p&gt;在编译过程中，虚拟机并不知道目标方法的具体内存地址，Java 编译器会暂时用符号引用来表示该目标方法，这一符号引用包括目标方法所在的类或接口的名字，以及目标方法的方法名和方法描述符&lt;/p&gt;
&lt;p&gt;符号引用存储在方法区常量池中，根据目标方法是否为接口方法，分为接口符号引用和非接口符号引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constant pool:
...
  #16 = InterfaceMethodref #27.#29	// 接口
...
  #22 = Methodref          #1.#33	// 非接口
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于非接口符号引用，假定该符号引用所指向的类为 C，则 Java 虚拟机会按照如下步骤进行查找：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 C 中查找符合名字及描述符的方法&lt;/li&gt;
&lt;li&gt;如果没有找到，在 C 的父类中继续搜索，直至 Object 类&lt;/li&gt;
&lt;li&gt;如果没有找到，在 C 所直接实现或间接实现的接口中搜索，这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法，则任意返回其中一个&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于接口符号引用，假定该符号引用所指向的接口为 I，则 Java 虚拟机会按照如下步骤进行查找：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 I 中查找符合名字及描述符的方法&lt;/li&gt;
&lt;li&gt;如果没有找到，在 Object 类中的公有实例方法中搜索&lt;/li&gt;
&lt;li&gt;如果没有找到，则在 I 的超接口中搜索，这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;执行流程&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    public Demo() { }
    private void test1() { }
    private final void test2() { }

    public void test3() { }
    public static void test4() { }

    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        Demo.test4();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几种不同的方法调用对应的字节码指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0:  new             #2 // class cn/jvm/t3/bytecode/Demo
3:  dup
4:  invokespecial   #3 // Method &quot;&amp;lt;init&amp;gt;&quot;:()V
7:  astore_1
8:  aload_1
9:  invokespecial   #4 // Method test1:()V
12: aload_1
13: invokespecial   #5 // Method test2:()V
16: aload_1
17: invokevirtual   #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic    #7 // Method test4:()V
25: invokestatic    #7 // Method test4:()V
28: return
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;invokespecial 调用该对象的构造方法 &amp;lt;init&amp;gt;:()V&lt;/li&gt;
&lt;li&gt;invokevirtual 调用对象的成员方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d.test4()&lt;/code&gt; 是通过&lt;strong&gt;对象引用&lt;/strong&gt;调用一个静态方法，在调用 invokestatic 之前执行了 pop 指令，把对象引用从操作数栈弹掉
&lt;ul&gt;
&lt;li&gt;不建议使用 &lt;code&gt;对象.静态方法()&lt;/code&gt; 的方式调用静态方法，多了 aload 和 pop 指令&lt;/li&gt;
&lt;li&gt;成员方法与静态方法调用的区别是：执行方法前是否需要对象引用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;多态原理&lt;/h4&gt;
&lt;h5&gt;执行原理&lt;/h5&gt;
&lt;p&gt;Java 虚拟机中关于方法重写的判定基于方法描述符，如果子类定义了与父类中非私有、非静态方法同名的方法，只有当这两个方法的参数类型以及返回类型一致，Java 虚拟机才会判定为重写&lt;/p&gt;
&lt;p&gt;理解多态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多态有编译时多态和运行时多态，即静态绑定和动态绑定&lt;/li&gt;
&lt;li&gt;前者是通过方法重载实现，后者是通过重写实现（子类覆盖父类方法，虚方法表）&lt;/li&gt;
&lt;li&gt;虚方法：运行时动态绑定的方法，对比静态绑定的非虚方法调用来说，虚方法调用更加耗时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法重写的本质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找到操作数栈的第一个元素&lt;strong&gt;所执行的对象的实际类型&lt;/strong&gt;，记作 C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果在类型 C 中找到与描述符和名称都相符的方法，则进行访问&lt;strong&gt;权限校验&lt;/strong&gt;（私有的），如果通过则返回这个方法的直接引用，查找过程结束；如果不通过，则返回 java.lang.IllegalAccessError 异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找不到，就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果始终没有找到合适的方法，则抛出 java.lang.AbstractMethodError 异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;虚方法表&lt;/h5&gt;
&lt;p&gt;在虚拟机工作过程中会频繁使用到动态绑定，每次动态绑定的过程中都要重新在类的元数据中搜索合适目标，影响到执行效率。为了提高性能，JVM 采取了一种用&lt;strong&gt;空间换取时间&lt;/strong&gt;的策略来实现动态绑定，在每个&lt;strong&gt;类的方法区&lt;/strong&gt;建立一个虚方法表（virtual method table），实现使用索引表来代替查找，可以快速定位目标方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;invokevirtual 所使用的虚方法表（virtual method table，vtable），执行流程
&lt;ol&gt;
&lt;li&gt;先通过栈帧中的对象引用找到对象，分析对象头，找到对象的实际 Class&lt;/li&gt;
&lt;li&gt;Class 结构中有 vtable，查表得到方法的具体地址，执行方法的字节码&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;invokeinterface 所使用的接口方法表（interface method table，itable）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚方法表会在类加载的链接阶段被创建并开始初始化，类的变量初始值准备完成之后，JVM 会把该类的方法表也初始化完毕&lt;/p&gt;
&lt;p&gt;虚方法表的执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于静态绑定的方法调用而言，实际引用是一个指向方法的指针&lt;/li&gt;
&lt;li&gt;对于动态绑定的方法调用而言，实际引用是方法表的索引值，也就是方法的间接地址。Java 虚拟机获取调用者的实际类型，并在该实际类型的虚方法表中，根据索引值获得目标方法内存偏移量（指针）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了优化对象调用方法的速度，方法区的类型信息会增加一个指针，该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表，本质上是一个数组，每个数组元素指向一个当前类及其祖先类中非私有的实例方法&lt;/p&gt;
&lt;p&gt;方法表满足以下的特质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其一，子类方法表中包含父类方法表中的&lt;strong&gt;所有方法&lt;/strong&gt;，并且在方法表中的索引值与父类方法表种的索引值相同&lt;/li&gt;
&lt;li&gt;其二，&lt;strong&gt;非重写的方法指向父类的方法表项，与父类共享一个方法表项，重写的方法指向本身自己的实现&lt;/strong&gt;，这就是为什么多态情况下可以访问父类的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-虚方法表.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Passenger 类的方法表包括两个方法，分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置，是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致，为了保持简洁，这里不考虑 Object 类中的其他方法。&lt;/p&gt;
&lt;p&gt;虚方法表对性能的影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用了方法表的动态绑定与静态绑定相比，仅仅多出几个内存解引用操作：访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法，但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计&lt;/li&gt;
&lt;li&gt;上述优化的效果看上去不错，但实际上&lt;strong&gt;仅存在于解释执行&lt;/strong&gt;中，或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段：内联缓存（inlining cache）和方法内联（method inlining）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Person {
    public String toString() {
        return &quot;I&apos;m a person.&quot;;
    }
    public void eat() {}
    public void speak() {}
}

class Boy extends Person {
    public String toString() {
        return &quot;I&apos;m a boy&quot;;
    }
    public void speak() {}
    public void fight() {}
}

class Girl extends Person {
    public String toString() {
        return &quot;I&apos;m a girl&quot;;
    }
    public void speak() {}
    public void sing() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-%E8%99%9A%E6%96%B9%E6%B3%95%E8%A1%A8%E6%8C%87%E5%90%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考文档：https://www.cnblogs.com/kaleidoscope/p/9790766.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;内联缓存&lt;/h5&gt;
&lt;p&gt;内联缓存：是一种加快动态绑定的优化技术，能够缓存虚方法调用中&lt;strong&gt;调用者的动态类型以及该类型所对应的目标方法&lt;/strong&gt;。在之后的执行过程中，如果碰到已缓存的类型，便会直接调用该类型所对应的目标方法；反之内联缓存则会退化至使用基于方法表的动态绑定&lt;/p&gt;
&lt;p&gt;多态的三个术语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单态 (monomorphic)：指的是仅有一种状态的情况&lt;/li&gt;
&lt;li&gt;多态 (polymorphic)：指的是有限数量种状态的情况，二态（bimorphic）是多态的其中一种&lt;/li&gt;
&lt;li&gt;超多态 (megamorphic)：指的是更多种状态的情况，通常用一个具体数值来区分多态和超多态，在这个数值之下，称之为多态，否则称之为超多态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于内联缓存来说，有对应的单态内联缓存、多态内联缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单态内联缓存：只缓存了一种动态类型以及所对应的目标方法，实现简单，比较所缓存的动态类型，如果命中，则直接调用对应的目标方法。&lt;/li&gt;
&lt;li&gt;多态内联缓存：缓存了多个动态类型及其目标方法，需要逐个将所缓存的动态类型与当前动态类型进行比较，如果命中，则调用对应的目标方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了节省内存空间，&lt;strong&gt;Java 虚拟机只采用单态内联缓存&lt;/strong&gt;，没有命中的处理方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;替换单态内联缓存中的纪录，类似于 CPU 中的数据缓存，对数据的局部性有要求，即在替换内联缓存之后的一段时间内，方法调用的调用者的动态类型应当保持一致，从而能够有效地利用内联缓存&lt;/li&gt;
&lt;li&gt;劣化为超多态状态，这也是 Java 虚拟机的具体实现方式，这种状态实际上放弃了优化的机会，将直接访问方法表来动态绑定目标方法，但是与替换内联缓存纪录相比节省了写缓存的额外开销&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然内联缓存附带内联二字，但是并没有内联目标方法&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/intro/100010301&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;代码优化&lt;/h3&gt;
&lt;h4&gt;语法糖&lt;/h4&gt;
&lt;p&gt;语法糖：指 Java 编译器把 *.java 源码编译为 *.class 字节码的过程中，自动生成和转换的一些代码，主要是为了减轻程序员的负担&lt;/p&gt;
&lt;h4&gt;构造器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Candy1 {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Candy1 {
    // 这个无参构造是编译器帮助我们加上的
    public Candy1() {
        super(); // 即调用父类 Object 的无参构造方法，即调用 java/lang/Object.&quot;
        &amp;lt;init&amp;gt;&quot;:()V
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;拆装箱&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Integer x = 1;
int y = x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码在 JDK 5 之前是无法编译通过的，必须改写为代码片段2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer x = Integer.valueOf(1);
int y = x.intValue();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDK5 以后编译阶段自动转换成上述片段&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;泛型擦除&lt;/h4&gt;
&lt;p&gt;泛型也是在 JDK 5 开始加入的特性，但 Java 在编译泛型代码后会执行&lt;strong&gt;泛型擦除&lt;/strong&gt;的动作，即泛型信息在编译为字节码之后就丢失了，实际的类型都&lt;strong&gt;当做了 Object 类型&lt;/strong&gt;来处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器真正生成的字节码中，还要额外做一个类型转换的操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;可变参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Candy4 {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
    	foo(&quot;hello&quot;, &quot;world&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可变参数 &lt;code&gt;String... args&lt;/code&gt; 其实是 &lt;code&gt;String[] args&lt;/code&gt; ， Java 编译器会在编译期间将上述代码变换为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
	foo(new String[]{&quot;hello&quot;, &quot;world&quot;});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如果调用了 &lt;code&gt;foo()&lt;/code&gt; 则等价代码为 &lt;code&gt;foo(new String[]{})&lt;/code&gt; ，创建了一个空的数组，而不会传递 null 进去&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;foreach&lt;/h4&gt;
&lt;p&gt;数组的循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖
for (int e : array) {
	System.out.println(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后为循环取数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(int i = 0; i &amp;lt; array.length; ++i) {
	int e = array[i];
	System.out.println(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集合的循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
	System.out.println(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后转换为对迭代器的调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
    Integer e = (Integer)iter.next();
    System.out.println(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：foreach 循环写法，能够配合数组以及所有实现了 Iterable 接口的集合类一起使用，其中 Iterable 用来获取集合的迭代器&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;switch&lt;/h4&gt;
&lt;h5&gt;字符串&lt;/h5&gt;
&lt;p&gt;switch 可以作用于字符串和枚举类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch (str) {
    case &quot;hello&quot;: {
        System.out.println(&quot;h&quot;);
        break;
    }
    case &quot;world&quot;: {
        System.out.println(&quot;w&quot;);
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;strong&gt;switch 配合 String 和枚举使用时，变量不能为 null&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;会被编译器转换为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte x = -1;
switch(str.hashCode()) {
    case 99162322: // hello 的 hashCode
        if (str.equals(&quot;hello&quot;)) {
        	x = 0;
        }
    	break;
    case 113318802: // world 的 hashCode
        if (str.equals(&quot;world&quot;)) {
        	x = 1;
        }
}
switch(x) {
    case 0:
    	System.out.println(&quot;h&quot;);
    	break;
    case 1:
    	System.out.println(&quot;w&quot;);
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行了两遍 switch，第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型，第二遍才是利用 byte 执行进行比较&lt;/li&gt;
&lt;li&gt;hashCode 是为了提高效率，减少可能的比较；而 equals 是为了防止 hashCode 冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;枚举&lt;/h5&gt;
&lt;p&gt;switch 枚举的例子，原始代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Sex {
	MALE, FEMALE
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println(&quot;男&quot;); 
                break;
            case FEMALE:
                System.out.println(&quot;女&quot;); 
                break;
        }
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译转换后的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 定义一个合成类（仅 jvm 使用，对我们不可见）
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号，从 0 开始
* 即 MALE 的 ordinal()=0，FEMALE 的 ordinal()=1
*/
static class $MAP {
    // 数组大小即为枚举元素个数，里面存储 case 用来对比的数字
    static int[] map = new int[2];
    static {
    	map[Sex.MALE.ordinal()] = 1;
    	map[Sex.FEMALE.ordinal()] = 2;
	}
}
public static void foo(Sex sex) {
    int x = $MAP.map[sex.ordinal()];
    switch (x) {
        case 1:
        	System.out.println(&quot;男&quot;);
        	break;
        case 2:
        	System.out.println(&quot;女&quot;);
        	break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;枚举类&lt;/h4&gt;
&lt;p&gt;JDK 7 新增了枚举类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Sex {
	MALE, FEMALE
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译转换后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Sex extends Enum&amp;lt;Sex&amp;gt; {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    static {
        MALE = new Sex(&quot;MALE&quot;, 0);
        FEMALE = new Sex(&quot;FEMALE&quot;, 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }
    private Sex(String name, int ordinal) {
    	super(name, ordinal);
    }
    public static Sex[] values() {
    	return $VALUES.clone();
    }
    public static Sex valueOf(String name) {
    	return Enum.valueOf(Sex.class, name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;try-w-r&lt;/h4&gt;
&lt;p&gt;JDK 7 开始新增了对需要关闭的资源处理的特殊语法 &lt;code&gt;try-with-resources&lt;/code&gt;，格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try(资源变量 = 创建资源对象){
} catch( ) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中资源对象需要实现 &lt;strong&gt;AutoCloseable&lt;/strong&gt; 接口，例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ，使用 try-withresources可以不用写 finally 语句块，编译器会帮助生成关闭资源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try(InputStream is = new FileInputStream(&quot;d:\\1.txt&quot;)) {
	System.out.println(is);
} catch (IOException e) {
	e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换成：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;addSuppressed(Throwable e)&lt;/code&gt;：添加被压制异常，是为了防止异常信息的丢失（&lt;strong&gt;fianlly 中如果抛出了异常&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
    InputStream is = new FileInputStream(&quot;d:\\1.txt&quot;);
    Throwable t = null;
    try {
    	System.out.println(is);
    } catch (Throwable e1) {
    	// t 是我们代码出现的异常
    	t = e1;
    	throw e1;
    } finally {
        // 判断了资源不为空
        if (is != null) {
            // 如果我们代码有异常
            if (t != null) {
                try {
                	is.close();
                } catch (Throwable e2) {
                    // 如果 close 出现异常，作为被压制异常添加
                    t.addSuppressed(e2);
                }
            } else {
                // 如果我们代码没有异常，close 出现的异常就是最后 catch 块中的 e
                is.close();
            }
		}
	}
} catch (IOException e) {
    e.printStackTrace();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;方法重写&lt;/h4&gt;
&lt;p&gt;方法重写时对返回值分两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父子类的返回值完全一致&lt;/li&gt;
&lt;li&gt;子类返回值可以是父类返回值的子类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A {
    public Number m() {
		return 1;
    }
}
class B extends A {
    @Override
    // 子类m方法的返回值是Integer是父类m方法返回值Number的子类
    public Integer m() {
    	return 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于子类，Java 编译器会做如下处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class B extends A {
    public Integer m() {
    	return 2;
    }
	// 此方法才是真正重写了父类 public Number m() 方法
	public synthetic bridge Number m() {
    	// 调用 public Integer m()
    	return m();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中桥接方法比较特殊，仅对 Java 虚拟机可见，并且与原来的 public Integer m() 没有命名冲突&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;匿名内部类&lt;/h4&gt;
&lt;h5&gt;无参优化&lt;/h5&gt;
&lt;p&gt;源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            	System.out.println(&quot;ok&quot;);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转化后代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 额外生成的类
final class Candy11$1 implements Runnable {
    Candy11$1() {
    }
    public void run() {
    	System.out.println(&quot;ok&quot;);
    }
}
public class Candy11 {
    public static void main(String[] args) {
    	Runnable runnable = new Candy11$1();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;带参优化&lt;/h5&gt;
&lt;p&gt;引用局部变量的匿名内部类，源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            	System.out.println(&quot;ok:&quot; + x);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换后代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final class Candy11$1 implements Runnable {
    int val$x;
    Candy11$1(int x) {
    	this.val$x = x;
    }
    public void run() {
    	System.out.println(&quot;ok:&quot; + this.val$x);
    }
}
public class Candy11 {
    public static void test(final int x) {
    	Runnable runnable = new Candy11$1(x);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;局部变量在底层创建为内部类的成员变量，必须是 final 的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 Java 中方法调用是值传递的，在匿名内部类中对变量的操作都是基于原变量的副本，不会影响到原变量的值，所以&lt;strong&gt;原变量的值的改变也无法同步到副本中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作，也是&lt;strong&gt;防止外部操作修改了变量而内部类无法随之变化&lt;/strong&gt;出现的影响&lt;/p&gt;
&lt;p&gt;在创建 &lt;code&gt;Candy11$1 &lt;/code&gt; 对象时，将 x 的值赋值给了 &lt;code&gt;Candy11$1&lt;/code&gt; 对象的 val 属性，x 不应该再发生变化了，因为发生变化，this.val$x 属性没有机会再跟着变化&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;反射优化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Reflect1 {
    public static void foo() {
    	System.out.println(&quot;foo...&quot;);
    }
    public static void main(String[] args) throws Exception {
        Method foo = Reflect1.class.getMethod(&quot;foo&quot;);
        for (int i = 0; i &amp;lt;= 16; i++) {
            System.out.printf(&quot;%d\t&quot;, i);
            foo.invoke(null);
        }
        System.in.read();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;foo.invoke 0 ~ 15 次调用的是 MethodAccessor 的实现类 &lt;code&gt;NativeMethodAccessorImpl.invoke0()&lt;/code&gt;，本地方法执行速度慢；当调用到第 16 次时，会采用运行时生成的类 &lt;code&gt;sun.reflect.GeneratedMethodAccessor1&lt;/code&gt; 代替&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(Object obj, Object[] args)throws Exception {
    // inflationThreshold 膨胀阈值，默认 15
    if (++numInvocations &amp;gt; ReflectionFactory.inflationThreshold()
        &amp;amp;&amp;amp; !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
        MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                           method.getName(),
                           method.getParameterTypes(),
                           method.getReturnType(),
                           method.getExceptionTypes(),
                           method.getModifiers());
        parent.setDelegate(acc);
    }
    // 【调用本地方法实现】
    return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    // 如果有参数，那么抛非法参数异常
    block4 : {
        if (arrobject == null || arrobject.length == 0) break block4;
            throw new IllegalArgumentException();
    }
    try {
        // 【可以看到，已经是直接调用方法】
        Reflect1.foo();
        // 因为没有返回值
        return null;
    }
   //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过查看 ReflectionFactory 源码可知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sun.reflect.noInflation 可以用来禁用膨胀，直接生成 GeneratedMethodAccessor1，但首次生成比较耗时，如果仅反射调用一次，不划算&lt;/li&gt;
&lt;li&gt;sun.reflect.inflationThreshold 可以修改膨胀阈值&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;系统优化&lt;/h2&gt;
&lt;h3&gt;性能调优&lt;/h3&gt;
&lt;h4&gt;性能指标&lt;/h4&gt;
&lt;p&gt;性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等，而这些性能指标又依赖于系统服务器的资源，如 CPU、内存、磁盘 IO、网络 IO 等，对于这些指标数据的收集，通常可以根据Java本身的工具或指令进行查询&lt;/p&gt;
&lt;p&gt;几个重要的指标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;停顿时间（响应时间）：提交请求和返回该请求的响应之间使用的时间，比如垃圾回收中 STW 的时间&lt;/li&gt;
&lt;li&gt;吞吐量：对单位时间内完成的工作量（请求）的量度（可以对比 GC 的性能指标）&lt;/li&gt;
&lt;li&gt;并发数：同一时刻，对服务器有实际交互的请求数&lt;/li&gt;
&lt;li&gt;QPS：Queries Per Second，每秒处理的查询量&lt;/li&gt;
&lt;li&gt;TPS：Transactions Per Second，每秒产生的事务数&lt;/li&gt;
&lt;li&gt;内存占用：Java 堆区所占的内存大小&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;优化步骤&lt;/h4&gt;
&lt;p&gt;对于一个系统要部署上线时，则一定会对 JVM 进行调整，不经过任何调整直接上线，容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;性能监控：通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打印 GC 日志，通过 GCviewer 或者 http://gceasy.io 来分析异常信息&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;运用命令行工具、jstack、jmap、jinfo 等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dump 出堆文件，使用内存分析工具分析文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用阿里 Arthas、jconsole、JVisualVM 来&lt;strong&gt;实时查看 JVM 状态&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jstack 查看堆栈信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能调优：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适当增加内存，根据业务背景选择垃圾回收器&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优化代码，控制内存使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;增加机器，分散节点压力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合理设置线程池线程数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用中间件提高程序效率，比如缓存、消息队列等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;参数调优&lt;/h4&gt;
&lt;p&gt;对于 JVM 调优，主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置堆的初始大小和最大大小，为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间，通常把最大、初始大小设置为相同的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Xms：设置堆的初始化大小
-Xmx：设置堆的最大大小
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置，则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小，来减少 YGC 发生的次数，虽然次数减少了，但 Eden 区满的时候，由于占用的空间较大，导致释放缓慢，此时 STW 的时间较长，因此需要按照程序情况去调优&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-XX:SurvivorRatio
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;年轻代和老年代默认比例为 1:2，可以通过调整二者空间大小比率来设置两者的大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-XX:newSize   设置年轻代的初始大小
-XX:MaxNewSize   设置年轻代的最大大小，  初始大小和最大大小两个值通常相同
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程堆栈的设置：&lt;strong&gt;每个线程默认会开启 1M 的堆栈&lt;/strong&gt;，用于存放栈帧、调用参数、局部变量等，但一般 256K 就够用，通常减少每个线程的堆栈，可以产生更多的线程，但这实际上还受限于操作系统&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Xss   对每个线程stack大小的调整,-Xss128k
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般一天超过一次 FullGC 就是有问题，首先通过工具查看是否出现内存泄露，如果出现内存泄露则调整代码，没有的话则调整 JVM 参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统 CPU 持续飙高的话，首先先排查代码问题，如果代码没问题，则咨询运维或者云服务器供应商，通常服务器重启或者服务器迁移即可解决&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果数据查询性能很低下的话，如果系统并发量并没有多少，则应更加关注数据库的相关问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果服务器配置还不错，JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;命令行篇&lt;/h3&gt;
&lt;h4&gt;jps&lt;/h4&gt;
&lt;p&gt;jps（Java Process Statu）：显示指定系统内所有的 HotSpot 虚拟机进程（查看虚拟机进程信息），可用于查询正在运行的虚拟机进程，进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的，是唯一的&lt;/p&gt;
&lt;p&gt;使用语法：&lt;code&gt;jps [options] [hostid]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;options 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-q：仅仅显示 LVMID（local virtual machine id），即本地虚拟机唯一 id，不显示主类的名称等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-l：输出应用程序主类的全类名或如果进程执行的是 jar 包，则输出 jar 完整路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-m：输出虚拟机进程启动时传递给主类 main()的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-v：列出虚拟机进程启动时的JVM参数，比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ostid 参数：RMI注册表中注册的主机名，如果想要远程监控主机上的 java 程序，需要安装 jstatd&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;jstat&lt;/h4&gt;
&lt;p&gt;jstat（JVM Statistics Monitoring Tool）：用于监视 JVM 各种运行状态信息的命令行工具，可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据，在没有 GUI 的图形界面，只提供了纯文本控制台环境的服务器上，它是运行期定位虚拟机性能问题的首选工具，常用于检测垃圾回收问题以及内存泄漏问题&lt;/p&gt;
&lt;p&gt;使用语法：&lt;code&gt;jstat -&amp;lt;option&amp;gt; [-t] [-h&amp;lt;lines&amp;gt;] &amp;lt;vmid&amp;gt; [&amp;lt;interval&amp;gt; [&amp;lt;count&amp;gt;]]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;查看命令相关参数：jstat-h 或 jstat-help&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;vmid 是进程 id 号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;option 参数：&lt;/p&gt;
&lt;p&gt;类装载相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-class：显示 ClassLoader 的相关信息，类的装载、卸载数量、总空间、类装载所消耗的时间等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;垃圾回收相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-gc：显示与GC相关的堆信息，年轻代、老年代、永久代等的容量、已用空间、GC时间合计等信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gccapacity：显示内容与 -gc 基本相同，但输出主要关注 Java 堆各个区域使用到的最大、最小空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gcutil：显示内容与 -gc 基本相同，但输出主要关注已使用空间占总空间的百分比&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gccause：与 -gcutil 功能一样，但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gcnew：显示新生代 GC 状况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gcnewcapacity：显示内容与 -gcnew 基本相同，输出主要关注使用到的最大、最小空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-geold：显示老年代 GC 状况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gcoldcapacity：显示内容与 -gcold 基本相同，输出主要关注使用到的最大、最小空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-gcpermcapacity：显示永久代使用到的最大、最小空间&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JIT 相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-compiler：显示 JIT 编译器编译过的方法、耗时等信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-printcompilation：输出已经被 JIT 编译的方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;jinfo&lt;/h4&gt;
&lt;p&gt;jinfo（Configuration Info for Java）：查看虚拟机配置参数信息，也可用于调整虚拟机的配置参数，开发人员可以很方便地找到 Java 虚拟机参数的当前值&lt;/p&gt;
&lt;p&gt;使用语法：&lt;code&gt;jinfo [options] pid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;options 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no option：输出全部的参数和系统属性&lt;/li&gt;
&lt;li&gt;-flag name：输出对应名称的参数&lt;/li&gt;
&lt;li&gt;-flag [+-]name：开启或者关闭对应名称的参数 只有被标记为manageable的参数才可以被动态修改&lt;/li&gt;
&lt;li&gt;-flag name=value：设定对应名称的参数&lt;/li&gt;
&lt;li&gt;-flags：输出全部的参数&lt;/li&gt;
&lt;li&gt;-sysprops：输出系统属性&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;jmap&lt;/h4&gt;
&lt;p&gt;jmap（JVM Memory Map）：获取 dump 文件，还可以获取目标 Java 进程的内存相关信息，包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等&lt;/p&gt;
&lt;p&gt;使用语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jmap [options] &amp;lt;pid&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jmap [options] &amp;lt;executable &amp;lt;core&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jmap [options] [server_id@] &amp;lt;remote server IP or hostname&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;option 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-dump：生成 dump 文件（Java堆转储快照，二进制文件），-dump:live 只保存堆中的存活对象&lt;/li&gt;
&lt;li&gt;-heap：输出整个堆空间的详细信息，包括 GC 的使用、堆配置信息，以及内存的使用信息等&lt;/li&gt;
&lt;li&gt;-histo：输出堆空间中对象的统计信息，包括类、实例数量和合计容量，-histo:live 只统计堆中的存活对象&lt;/li&gt;
&lt;li&gt;-J &amp;lt;flag&amp;gt;：传递参数给 jmap 启动的 jvm&lt;/li&gt;
&lt;li&gt;-finalizerinfo：显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象，仅 linux/solaris 平台有效&lt;/li&gt;
&lt;li&gt;-permstat：以 ClassLoader 为统计口径输出永久代的内存状态信息，仅 linux/solaris 平台有效&lt;/li&gt;
&lt;li&gt;-F：当虚拟机进程对 -dump 选项没有任何响应时，强制执行生成 dump 文件，仅 linux/solaris 平台有效&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;jhat&lt;/h4&gt;
&lt;p&gt;jhat（JVM Heap Analysis Tool）：Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用，用于&lt;strong&gt;分析 jmap 生成的 heap dump 文件&lt;/strong&gt;（堆转储快照），jhat 内置了一个微型的 HTTP/HTML 服务器，生成 dump 文件的分析结果后，用户可以在浏览器中查看分析结果&lt;/p&gt;
&lt;p&gt;使用语法：&lt;code&gt;jhat &amp;lt;options&amp;gt; &amp;lt;dumpfile&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;options 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-stack false｜true：关闭｜打开对象分配调用栈跟踪&lt;/li&gt;
&lt;li&gt;-refs false｜true：关闭｜打开对象引用跟踪&lt;/li&gt;
&lt;li&gt;-port port-number：设置 jhat HTTP Server 的端口号，默认 7000&lt;/li&gt;
&lt;li&gt;-exclude exclude-file：执行对象查询时需要排除的数据成员&lt;/li&gt;
&lt;li&gt;-baseline exclude-file：指定一个基准堆转储&lt;/li&gt;
&lt;li&gt;-debug int：设置 debug 级别&lt;/li&gt;
&lt;li&gt;-version：启动后显示版本信息就退出&lt;/li&gt;
&lt;li&gt;-J &amp;lt;flag&amp;gt;：传入启动参数，比如 -J-Xmx512m&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：jhat 命令在 JDK9、JDK10 中已经被删除，官方建议用 VisualVM 代替&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;jstack&lt;/h4&gt;
&lt;p&gt;jstack（JVM Stack Trace）：用于生成虚拟机指定进程当前时刻的线程快照（虚拟机堆栈跟踪），线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合&lt;/p&gt;
&lt;p&gt;线程快照的作用：可用于定位线程出现长时间停顿的原因，如线程间死锁、死循环、请求外部资源导致的长时间等待等问题，用 jstack 显示各个线程调用的堆栈情况&lt;/p&gt;
&lt;p&gt;使用语法：&lt;code&gt;jstack [options] pid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;options 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-F：当正常输出的请求不被响应时，强制输出线程堆栈&lt;/li&gt;
&lt;li&gt;-l：除堆栈外，显示关于锁的附加信息&lt;/li&gt;
&lt;li&gt;-m：如果调用本地方法的话，可以显示 C/C++ 的堆栈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 thread dump 中的几种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;死锁：Deadlock&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等待资源：Waiting on condition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等待获取监视器：Waiting on monitor entry&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阻塞：Blocked&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行中：Runnable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;暂停：Suspended&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象等待中：Object.wait() 或 TIMED＿WAITING&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止：Parked&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;jcmd&lt;/h4&gt;
&lt;p&gt;jcmd 是一个多功能命令行工具，可以用来实现前面除了 jstat 之外所有命令的功能，比如 dump、内存使用、查看 Java 进程、导出线程信息、执行 GC、JVM 运行时间等&lt;/p&gt;
&lt;p&gt;jcmd -l：列出所有的JVM进程&lt;/p&gt;
&lt;p&gt;jcmd 进程号 help：针对指定的进程，列出支持的所有具体命令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thread.print：可以替换 jstack 指令&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GC.class_histogram：可以替换 jmap 中的 -histo 操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GC.heap_dump：可以替换 jmap 中的 -dump 操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GC.run：可以查看GC的执行情况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VM.uptime：可以查看程序的总执行时间，可以替换 jstat 指令中的 -t  操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VM.system_properties：可以替换 jinfo -sysprops 进程 id&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VM.flags：可以获取 JVM 的配置参数信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;jstatd&lt;/h4&gt;
&lt;p&gt;jstatd 是一个 RMI 服务端程序，相当于代理服务器，建立本地计算机与远程监控工具的通信，jstatd 服务器将本机的 Java 应用程序信息传递到远程计算机&lt;/p&gt;
&lt;p&gt;远程主机信息收集，前面的指令只涉及到监控本机的 Java 应用程序，而在这些工具中，一些监控工具也支持对远程计算机的监控（如 jps、jstat），为了启用远程监控，则需要配合使用 jstatd 工具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-jstatd%E5%9B%BE%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;GUI工具&lt;/h3&gt;
&lt;p&gt;工具的使用此处不再多言，推荐一个写的非常好的文章，JVM 调优部分的笔记全部参考此文章编写&lt;/p&gt;
&lt;p&gt;视频链接：https://www.bilibili.com/video/BV1PJ411n7xZ?p=304&lt;/p&gt;
&lt;p&gt;文章链接：https://www.yuque.com/u21195183/jvm/lv1zot&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;运行参数&lt;/h3&gt;
&lt;h4&gt;参数选项&lt;/h4&gt;
&lt;p&gt;添加 JVM 参数选项：进入 Run/Debug Configurations → VM options 设置参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标准参数选项：&lt;code&gt;java [-options] class [args...]&lt;/code&gt; 或 &lt;code&gt;java [-options] -jar jarfile [args...]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;命令：&lt;code&gt;-? -help&lt;/code&gt; 可以输出此命令的相关选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\Seazean&amp;gt;java -version
java version &quot;1.8.0_221&quot;
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
# mixed mode 字样，代表当前系统使用的是混合模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hotspot JVM 有两种模式，分别是 Server 和 Client，分别通过 -server 和- client 设置模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;32 位系统上，默认使用 Client 类型的 JVM，要使用 Server 模式，机器配置至少有 2 个以上的内核和 2G 以上的物理内存，Client 模式适用于对内存要求较小的桌面应用程序，默认使用 Serial 串行垃圾收集器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;64 位系统上，只支持 Server 模式的 JVM，适用于需要大内存的应用程序，默认使用并行垃圾收集器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-X 参数选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Xmixed           混合模式执行 (默认)
-Xint             仅解释模式执行
-Xbootclasspath:&amp;lt;用;分隔的目录和zip/jar文件&amp;gt;设置搜索路径以引导类和资源
-Xbootclasspath/a:&amp;lt;用;分隔的目录和zip/jar文件&amp;gt;附加在引导类路径末尾
-Xbootclasspath/p:&amp;lt;用;分隔的目录和zip/jar文件&amp;gt;置于引导类路径之前
-Xdiag            显示附加诊断消息
-Xnoclassgc       禁用类垃圾收集
-Xincgc           启用增量垃圾收集
-Xloggc:&amp;lt;file&amp;gt;    将 GC 状态记录在文件中 (带时间戳)
-Xbatch           禁用后台编译
-Xprof            输出 cpu 配置文件数据
-Xfuture          启用最严格的检查, 预期将来的默认值
-Xrs              减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni       对 JNI 函数执行其他检查
-Xshare:off       不尝试使用共享类数据
-Xshare:auto      在可能的情况下使用共享类数据 (默认)
-Xshare:on        要求使用共享类数据, 否则将失败。
-XshowSettings    显示所有设置并继续
-XshowSettings:all			显示所有设置并继续
-XshowSettings:vm 			显示所有与 vm 相关的设置并继续
-XshowSettings:properties	显示所有属性设置并继续
-XshowSettings:locale		显示所有与区域设置相关的设置并继续
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-XX 参数选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#Boolean类型格式
-XX:+&amp;lt;option&amp;gt;  			启用option属性
-XX:-&amp;lt;option&amp;gt;  			禁用option属性
#非Boolean类型格式
-XX:&amp;lt;option&amp;gt;=&amp;lt;number&amp;gt;  	设置option数值，可以带单位如k/K/m/M/g/G
-XX:&amp;lt;option&amp;gt;=&amp;lt;string&amp;gt;  	设置option字符值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序运行中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置Boolean类型参数
jinfo -flag [+|-]&amp;lt;name&amp;gt; &amp;lt;pid&amp;gt;
# 设置非Boolean类型参数
jinfo -flag &amp;lt;name&amp;gt;=&amp;lt;value&amp;gt; &amp;lt;pid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;打印参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-XX:+PrintCommandLineFlags 	程序运行时JVM默认设置或用户手动设置的XX选项
-XX:+PrintFlagsInitial 		打印所有XX选项的默认值
-XX:+PrintFlagsFinal 		打印所有XX选项的实际值
-XX:+PrintVMOptions 		打印JVM的参数
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;内存参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 栈
-Xss128k &amp;lt;==&amp;gt; -XX:ThreadStackSize=128k 		设置线程栈的大小为128K

# 堆
-Xms2048m &amp;lt;==&amp;gt; -XX:InitialHeapSize=2048m 	设置JVM初始堆内存为2048M（默认为物理内存的1/64）
-Xmx2048m &amp;lt;==&amp;gt; -XX:MaxHeapSize=2048m 		设置JVM最大堆内存为2048M（默认为物理内存的1/4）
-Xmn2g &amp;lt;==&amp;gt; -XX:NewSize=2g 					设置年轻代大小为2G
-XX:SurvivorRatio=8 						设置Eden区与Survivor区的比值，默认为8
-XX:NewRatio=2 								设置老年代与年轻代的比例，默认为2
-XX:+UseAdaptiveSizePolicy 					设置大小比例自适应，默认开启
-XX:PretenureSizeThreadshold=1024 			设置让大于此阈值的对象直接分配在老年代，只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15 				设置新生代晋升老年代的年龄限制，默认为15
-XX:TargetSurvivorRatio 					设置MinorGC结束后Survivor区占用空间的期望比例

# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 		设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 						使用压缩对象
-XX:+UseCompressedClassPointers 			使用压缩类指针
-XX:CompressedClassSpaceSize 				设置Klass Metaspace的大小，默认1G

# 直接内存
-XX:MaxDirectMemorySize 					指定DirectMemory容量，默认等于Java堆最大值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：参数前面是&lt;code&gt;+&lt;/code&gt;号说明是开启，如果是&lt;code&gt;- &lt;/code&gt;号说明是关闭&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;OOM参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-XX:+HeapDumpOnOutMemoryError 	内存出现OOM时生成Heap转储文件，两者互斥
-XX:+HeapDumpBeforeFullGC 		出现FullGC时生成Heap转储文件，两者互斥
-XX:HeapDumpPath=&amp;lt;path&amp;gt; 		指定heap转储文件的存储路径，默认当前目录
-XX:OnOutOfMemoryError=&amp;lt;path&amp;gt; 	指定可行性程序或脚本的路径，当发生OOM时执行脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;日志参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-XX:+PrintGC &amp;lt;==&amp;gt; -verbose:gc  	打印简要日志信息
-XX:+PrintGCDetails            	打印详细日志信息
-XX:+PrintGCTimeStamps  		打印程序启动到GC发生的时间，搭配-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps  		打印GC发生时的时间戳，搭配-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC 			 	打印GC前后的堆信息，如下图
-Xloggc:&amp;lt;file&amp;gt; 					输出GC导指定路径下的文件中
-XX:+TraceClassLoading  		监控类的加载
-XX:+PrintTenuringDistribution	打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布
-XX:+PrintGCApplicationStoppedTime  	打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime  	打印垃圾收集之前应用未中断的执行时间
-XX:+PrintReferenceGC 					打印回收了多少种不同引用类型的引用
-XX:+UseGCLogFileRotation 				启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=1  				设置GC日志文件的循环数目
-XX:GCLogFileSize=1M  					设置GC日志文件的大小
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;其他参数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-XX:+DisableExplicitGC  	禁用hotspot执行System.gc()，默认禁用
-XX:+DoEscapeAnalysis  		开启逃逸分析
-XX:+UseBiasedLocking  		开启偏向锁
-XX:+UseLargePages  		开启使用大页面
-XX:+PrintTLAB  			打印TLAB的使用情况
-XX:TLABSize  				设置TLAB大小
-XX:ReservedCodeCacheSize=&amp;lt;n&amp;gt;[g|m|k]、-XX:InitialCodeCacheSize=&amp;lt;n&amp;gt;[g|m|k] 指定代码缓存大小
-XX:+UseCodeCacheFlushing  	放弃一些被编译的代码，避免代码缓存被占满时JVM切换到interpreted-only的情况
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;代码获取&lt;/h4&gt;
&lt;p&gt;Java 提供了 java.lang.management 包用于监视和管理 Java 虚拟机和 Java 运行时中的其他组件，允许本地或远程监控和管理运行的 Java 虚拟机。ManagementFactory 类较为常用，Runtime 类可获取内存、CPU 核数等相关的数据，通过使用这些方法，可以监控应用服务器的堆内存使用情况，设置一些阈值进行报警等处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MemoryMonitor {
    public static void main(String[] args) {
        MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memorymbean.getHeapMemoryUsage();
        System.out.println(&quot;INIT HEAP: &quot; + usage.getInit() / 1024 / 1024 + &quot;m&quot;);
        System.out.println(&quot;MAX HEAP: &quot; + usage.getMax() / 1024 / 1024 + &quot;m&quot;);
        System.out.println(&quot;USE HEAP: &quot; + usage.getUsed() / 1024 / 1024 + &quot;m&quot;);
        System.out.println(&quot;\nFull Information:&quot;);
        System.out.println(&quot;Heap Memory Usage: &quot; + memorymbean.getHeapMemoryUsage());
        System.out.println(&quot;Non-Heap Memory Usage: &quot; + memorymbean.getNonHeapMemoryUsage());

        System.out.println(&quot;====通过java来获取相关系统状态====&quot;);
        System.out.println(&quot;当前堆内存大小totalMemory &quot; + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + &quot;m&quot;);// 当前堆内存大小
        System.out.println(&quot;空闲堆内存大小freeMemory &quot; + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + &quot;m&quot;);// 空闲堆内存大小
        System.out.println(&quot;最大可用总堆内存maxMemory &quot; + Runtime.getRuntime().maxMemory() / 1024 / 1024 + &quot;m&quot;);// 最大可用总堆内存大小

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;日志分析&lt;/h3&gt;
&lt;h4&gt;日志分类&lt;/h4&gt;
&lt;p&gt;HotSpot VM 的 GC 按照回收区域分为两类：一种是部分收集（Partial GC），一种是整堆收集（Full GC）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;部分收集（Partial GC）：不是完整收集整个 Java 堆的垃圾收集。其中又分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新生代收集（Minor GC/Young GC）：只是新生代（Eden/S0、S1）的垃圾收集&lt;/li&gt;
&lt;li&gt;老年代收集（Major GC/Old GC）：只是老年代的垃圾收集，只有 CMS GC 会有单独收集老年代的行为&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;混合收集（Mixed GC）：收集整个新生代以及部分老年代的垃圾收集，只有 G1 GC 会有这种行为&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整堆收集（Full GC）：收集整个 Java 堆和方法区的垃圾收集。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Minor GC/Young GC 日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[GC (Allocation Failure) [PSYoungGen: 31744K-&amp;gt;2192K (36864K) ] 31744K-&amp;gt;2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full GC 日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K-&amp;gt;0K (132096K) ] [Par01dGen: 416K-&amp;gt;5453K (50176K) ]5520K-&amp;gt;5453K (182272K), [Metaspace: 20637K-&amp;gt;20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;日志解析&lt;/h4&gt;
&lt;p&gt;通过日志看垃圾收集器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Serial 收集器：新生代显示 &lt;code&gt;[DefNew&lt;/code&gt;，即 &lt;code&gt;Default New Generation&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ParNew 收集器：新生代显示 &lt;code&gt;[ParNew&lt;/code&gt;，即 &lt;code&gt;Parallel New Generation&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Parallel Scavenge 收集器：新生代显示 &lt;code&gt;[PSYoungGen&lt;/code&gt;，JDK1.7 使用的 PSYoungGen&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Parallel Old 收集器：老年代显示 &lt;code&gt;[ParoldGen&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;G1 收集器：显示 &lt;code&gt;garbage-first heap&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过日志看 GC 原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Allocation Failure：表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Metadata GCThreshold：Metaspace 区不足&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FErgonomics：JVM 自适应调整导致的 GC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;System：调用了 System.gc() 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过日志看 GC 前后情况：GC 前内存占用 → GC 后内存占用（该区域内存总大小）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PSYoungGen: 5986K-&amp;gt;696K (8704K)] 5986K-&amp;gt;704K (9216K)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;中括号内：GC 回收前年轻代堆大小 → 回收后大小（年轻代堆总大小）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;括号外：GC 回收前年轻代和老年代大小 → 回收后大小（年轻代和老年代总大小）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代，Survivor 区只计算 from 部分，而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系：Eden:S0:S1=8:1:1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过日志看 GC 时间：GC 日志中有三个时间 user、sys、real&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;user：进程执行用户态代码（核心之外）所使用的时间，这是执行此进程所使用的实际 CPU 时间，其他进程和此进程阻塞的时间并不包括在内，在垃圾收集的情况下，表示 GC 线程执行所使用的 CPU 总时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sys：进程在内核态消耗的 CPU 时间，即在内核执行系统调用或等待系统事件所使用的 CPU 时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;real：程序从开始到结束所用的时钟时间，这个时间包括其他进程使用的时间片和进程阻塞的时间（比如等待 I/O 完成），对于并行 GC，这个数字应该接近（用户时间＋系统时间）除以垃圾收集器使用的线程数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于多核的原因，一般的 GC 事件中，real time 小于 sys time＋user time，因为是多个线程并发的去做 GC。如果 real＞sys＋user 的话，则说明 IO 负载非常重或 CPU 不够用&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;分析工具&lt;/h4&gt;
&lt;p&gt;GCEasy 是一款在线的 GC 日志分析器，可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能，大多数功能是免费的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官网地址：https://gceasy.io/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GCViewer 是一款离线的 GC 日志分析器，用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:&amp;lt;file&amp;gt;，还可以计算与垃圾回收相关的性能指标（吞吐量、累积的暂停、最长的暂停等），当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时，此功能非常有用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;源码下载：https://github.com/chewiebug/GCViewer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行版本下载：https://github.com/chewiebug/GCViewer/wiki/Changelog&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.yuque.com/u21195183/jvm/ukmb3k&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;ALG&lt;/h1&gt;
&lt;h2&gt;递归&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;算法：解题方案的准确而完整的描述，是一系列解决问题的清晰指令，代表着用系统的方法解决问题的策略机制&lt;/p&gt;
&lt;p&gt;递归：程序调用自身的编程技巧&lt;/p&gt;
&lt;p&gt;递归：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接递归：自己的方法调用自己&lt;/li&gt;
&lt;li&gt;间接递归：自己的方法调用别的方法，别的方法又调用自己&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;递归如果控制的不恰当，会形成递归的死循环，从而导致栈内存溢出错误&lt;/p&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/35263893/&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;算法&lt;/h3&gt;
&lt;h4&gt;核心思想&lt;/h4&gt;
&lt;p&gt;递归的三要素（理论）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归的终结点&lt;/li&gt;
&lt;li&gt;递归的公式&lt;/li&gt;
&lt;li&gt;递归的方向：必须走向终结点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// f(x)=f(x-1)+1;   f(1)=1;    f(10)=?
// 1.递归的终结点： f(1)  = 1
// 2.递归的公式：f(x) = f(x - 1) + 1
// 3.递归的方向：必须走向终结点
public static int f(int x){
    if(x == 1){
        return 1;
    }else{
        return f(x-1) + 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;公式转换&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 已知： f(x) = f(x + 1) + 2,  f(1) = 1。求：f(10) = ?
// 公式转换
// f(x-1)=f(x-1+1)+2 =&amp;gt; f(x)=f(x-1)+2
//（1）递归的公式：   f(n) = f(n-1)- 2 ;
//（2）递归的终结点：  f(1) = 1
//（3）递归的方向：必须走向终结点。
public static int f(int n){
    if(n == 1){
        return 1;
    }else{
        return f(n-1) - 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;注意事项&lt;/h4&gt;
&lt;p&gt;以上理论只能针对于&lt;strong&gt;规律化递归&lt;/strong&gt;，如果是非规律化是不能套用以上公式的！
非规律化递归的问题：文件搜索，啤酒问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;h4&gt;猴子吃桃&lt;/h4&gt;
&lt;p&gt;猴子第一天摘了若干个桃子，当即吃了一半，觉得好不过瘾，然后又多吃了一个。第二天又吃了前一天剩下的一半，觉得好不过瘾，然后又多吃了一个。以后每天都是如此。等到第十天再吃的时候发现只有1个桃子，问猴子第一天总共摘了多少个桃子？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
（1）公式： f(x+1)=f(x)-f(x)/2-1; ==&amp;gt; 2f(x+1) = f(x) - 2 ==&amp;gt; f(x)=2f(x+1)+2
（2）终结点：f(10) = 1
（3）递归的方向：走向了终结点
*/

public static int f(int x){
    if(x == 10){
        return 1;
    } else {
        return 2*f(x+1)+2
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;递归求和&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;//（1）递归的终点接：f(1) = 1
//（2）递归的公式： f(n) = f(n-1) + n
//（3）递归的方向必须走向终结点：
public static int f(int n){
        if(n == 1 ) return 1;
        return f(n-1) + n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;汉诺塔&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Hanoi {
    public static void main(String[] args) {
        hanoi(&apos;X&apos;, &apos;Y&apos;, &apos;Z&apos;, 3);
    }

    // 将n个块分治的从x移动到z，y为辅助柱
    private static void hanoi(char x, char y, char z, int n) {
        if (n == 1) {
            System.out.println(x + &quot;→&quot; + z);    // 直接将x的块移动到z
        } else {
            hanoi(x, z, y, n - 1);           	// 分治处理n-1个块，先将n-1个块借助z移到y
            System.out.println(x + &quot;→&quot; + z);    // 然后将x最下面的块（最大的）移动到z
            hanoi(y, x, z, n - 1);           	// 最后将n-1个块从y移动到z，x为辅助柱
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度 O(2^n)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;啤酒问题&lt;/h4&gt;
&lt;p&gt;非规律化递归问题，啤酒 2 元 1 瓶，4 个盖子可以换 1 瓶，2 个空瓶可以换 1 瓶&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BeerDemo{
    // 定义一个静态变量存储可以喝酒的总数
    public static int totalNum;
    public static int lastBottleNum;
    public static int lastCoverNum;
    public static void main(String[] args) {
        buyBeer(10);
        System.out.println(&quot;总数：&quot;+totalNum);
        System.out.println(&quot;剩余盖子：&quot;+ lastCoverNum);
        System.out.println(&quot;剩余瓶子：&quot;+ lastBottleNum);
    }
    public static void buyBeer(int money){
        int number = money / 2;
        totalNum += number;
        // 算出当前剩余的全部盖子和瓶子数，换算成金额继续购买。
        int currentBottleNum = lastBottleNum + number ;
        int currentCoverNum = lastCoverNum + number ;
        // 把他们换算成金额
        int totalMoney = 0 ;
        totalMoney += (currentBottleNum/2)*2; // 除2代表可以换几个瓶子，乘2代表换算成钱，秒！
        lastBottleNum = currentBottleNum % 2 ;// 取余//算出剩余的瓶子
     
        totalMoney += (currentCoverNum / 4) * 2;
        lastCoverNum = currentCoverNum % 4 ;

        // 继续拿钱买酒
        if(totalMoney &amp;gt;= 2){
            buyBeer(totalMoney);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;排序&lt;/h2&gt;
&lt;h3&gt;冒泡排序&lt;/h3&gt;
&lt;p&gt;冒泡排序（Bubble Sort）：两个数比较大小，较大的数下沉，较小的数冒起来&lt;/p&gt;
&lt;p&gt;算法描述：每次从数组的第一个位置开始两两比较，把较大的元素与较小的元素进行层层交换，最终把当前最大的一个元素存入到数组当前的末尾&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定总共需要冒几轮：数组的长度-1&lt;/li&gt;
&lt;li&gt;每轮两两比较几次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-冒泡排序.gif&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 0 1位置比较，大的放后面，然后1 2位置比较，大的继续放后面，一轮循环最后一位是最大值
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        int flag;//标记本趟排序是否发生了交换
        //比较i和i+1，不需要再比最后一个位置
        for (int i = 0; i &amp;lt; arr.length - 1; i++) {
            flag = 0;
            //最后i位不需要比，已经排序好
            for (int j = 0; j &amp;lt; arr.length - 1 - i; j++) {
                if (arr[j] &amp;gt; arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = 1;//发生了交换
                }
            }
            //没有发生交换，证明已经有序，不需要继续排序，节省时间
            if(flag == 0) {
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;冒泡排序时间复杂度：最坏情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元素比较的次数为：&lt;code&gt;(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;元素交换的次数为：&lt;code&gt;(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;总执行次数为：&lt;code&gt;(N^2/2-N/2)+(N^2/2-N/2)=N^2-N&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按照大 O 推导法则，保留函数中的最高阶项那么最终冒泡排序的时间复杂度为 O(N^2)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;选择排序&lt;/h3&gt;
&lt;h4&gt;简单选择&lt;/h4&gt;
&lt;p&gt;选择排序（Selection-sort）：一种简单直观的排序算法&lt;/p&gt;
&lt;p&gt;算法描述：首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置，然后再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;控制选择几轮：数组的长度 - 1&lt;/li&gt;
&lt;li&gt;控制每轮从当前位置开始比较几次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-选择排序.gif&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SelectSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        for (int i = 0; i &amp;lt; arr.length - 1; i++) {
            //获取最小索引位置
            int minIndex = i;
            for (int j = i + 1; j &amp;lt; arr.length; j++) {
                if (arr[minIndex] &amp;gt; arr[j]) {
                    minIndex = j;
                }
            }
            //交换元素
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
        System.out.println(Arrays.toString(arr));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择排序时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据比较次数：&lt;code&gt;(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;数据交换次数：&lt;code&gt;N-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;时间复杂度：&lt;code&gt;N^2/2-N/2+（N-1）=N^2/2+N/2-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据大 O 推导法则，保留最高阶项，去除常数因子，时间复杂度为 O(N^2)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;堆排序&lt;/h4&gt;
&lt;p&gt;堆排序（Heapsort）是指利用堆这种数据结构所设计的一种排序算法，堆结构是一个近似完全二叉树的结构，并同时满足子结点的键值或索引总是小于（或者大于）父节点&lt;/p&gt;
&lt;p&gt;优先队列：堆排序每次上浮过程都会将最大或者最小值放在堆顶，应用于优先队列可以将优先级最高的元素浮到堆顶&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将初始待排序关键字序列（R1,R2….Rn）构建成大顶堆，并通过上浮对堆进行调整，此堆为初始的无序区，&lt;strong&gt;堆顶为最大数&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将堆顶元素 R[1] 与最后一个元素 R[n] 交换，此时得到新的无序区（R1,R2,……Rn-1）和新的有序区 Rn，且满足 R[1,2…n-1]&amp;lt;=R[n]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;交换后新的堆顶 R[1] 可能违反堆的性质，因此需要对当前无序区（R1,R2,……Rn-1）调整为新堆，然后再次将 R[1] 与无序区最后一个元素交换，得到新的无序区（R1,R2….Rn-2）和新的有序区（Rn-1,Rn），不断重复此过程直到有序区的元素个数为 n-1，则整个排序过程完成&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-堆排序.jpg&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;floor：向下取整&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        heapSort(arr, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    //high为数组最大索引
    private static void heapSort(int[] arr, int high) {
        //建堆，逆排序，因为堆排序定义的交换顺序是从当前结点往下交换，逆序排可以避免多余的交换
        //i初始值是最后一个节点的父节点，如果参数是数组长度len，则 i = len / 2 -1
        for (int i = (high - 1) / 2; i &amp;gt;= 0; i--) {
            //调整函数
            sift(arr, i, high);
        }
        //从尾索引开始排序
        for (int i = high; i &amp;gt; 0; i--) {
            //将最大的节点放入末尾
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            //继续寻找最大的节点
            sift(arr, 0, i - 1);
        }
    }

    //调整函数，调整arr[low]的元素，从索引low到high的范围调整
    private static void sift(int[] arr, int low, int high) {
        //暂存调整元素
        int temp = arr[low];
        int i = low, j = low * 2 + 1;//j是左节点
        while (j &amp;lt;= high) {
            //判断是否有右孩子，并且比较左右孩子中较大的节点
            if (j &amp;lt; high &amp;amp;&amp;amp; arr[j] &amp;lt; arr[j + 1]) {
                j++;    //指向右孩子
            }
            if (temp &amp;lt; arr[j]) {
                arr[i] = arr[j];
                i = j;  //继续向下调整
                j = 2 * i + 1;
            } else {
                //temp &amp;gt; arr[j]，说明也大于j的孩子，探测结束
                break;
            }
        }
        //将被调整的节点放入最终的位置
        arr[i] = temp;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;堆排序的时间复杂度是 O(nlogn)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;插入排序&lt;/h3&gt;
&lt;h4&gt;直接插入&lt;/h4&gt;
&lt;p&gt;插入排序（Insertion Sort）：在要排序的一组数中，假定前 n-1 个数已经排好序，现在将第 n 个数插到这个有序数列中，使得这 n 个数也是排好顺序的，如此反复循环，直到全部排好顺序&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-插入排序.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InsertSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        for (int i = 1; i &amp;lt; arr.length; i++) {
            for (int j = i; j &amp;gt; 0; j--) {
                // 比较索引j处的值和索引j-1处的值，
                // 如果索引j-1处的值比索引j处的值大，则交换数据，
                // 如果不大，那么就找到合适的位置了，退出循环即可；
                if (arr[j - 1] &amp;gt; arr[j]) {
                    int temp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(arr));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;插入排序时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较的次数为：&lt;code&gt;(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;交换的次数为：&lt;code&gt;(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;总执行次数为：&lt;code&gt;(N^2/2-N/2)+(N^2/2-N/2)=N^2-N&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按照大 O 推导法则，保留函数中的最高阶项那么最终插入排序的时间复杂度为 O(N^2)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;希尔排序&lt;/h4&gt;
&lt;p&gt;希尔排序（Shell Sort）：也是一种插入排序，也称为缩小增量排序&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选定一个增长量 h，按照增长量 h 作为数据分组的依据，对数据进行分组&lt;/li&gt;
&lt;li&gt;对分好组的每一组数据完成插入排序&lt;/li&gt;
&lt;li&gt;减小增长量，最小减为 1，重复第二步操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-希尔排序.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;希尔排序的核心在于间隔序列的设定，既可以提前设定好间隔序列，也可以动态的定义间隔序列，希尔排序就是插入排序增加了间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        // 确定增长量h的初始值
        int h = 1;
        while (h &amp;lt; arr.length / 2) {
            h = 2 * h + 1;
        }
        // 希尔排序
        while (h &amp;gt;= 1) {
            // 找到待插入的元素
            for (int i = h; i &amp;lt; arr.length; i++) {
                // 把待插入的元素插到有序数列中
                for (int j = i; j &amp;gt;= h; j -= h) {
                    // 待插入的元素是arr[j]，比较arr[j]和arr[j-h]
                    if (arr[j] &amp;lt; arr[j - h]) {
                        int temp = arr[j];
                        arr[j] = arr[j - h];
                        arr[j - h] = temp;
                    }
                }
            }
            // 减小h的值，减小规则为：
            h = h / 2;
        }
        System.out.println(Arrays.toString(arr));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在希尔排序中，增长量 h 并没有固定的规则，有很多论文研究了各种不同的递增序列，但都无法证明某个序列是最好的，所以对于希尔排序的时间复杂度分析就认为 O(nlogn)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;归并排序&lt;/h3&gt;
&lt;h4&gt;实现方式&lt;/h4&gt;
&lt;p&gt;归并排序（Merge Sort）：建立在归并操作上的一种有效的排序算法，该算法是采用分治法的典型的应用。将已有序的子序列合并，得到完全有序的序列；即先使每个子序列有序，再使子序列段间有序。若将两个有序表合并成一个有序表，称为二路归并。&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一组数据拆分成两个元素相等的子组，并对每一个子组继续拆分，直到拆分后的每个子组的元素个数是1为止&lt;/li&gt;
&lt;li&gt;将相邻的两个子组进行合并成一个有序的大组&lt;/li&gt;
&lt;li&gt;不断的重复步骤2，直到最终只有一个组为止&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-归并排序.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;归并步骤：每次比较两端最小的值，把最小的值放在辅助数组的左边&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-%E5%BD%92%E5%B9%B6%E6%AD%A5%E9%AA%A41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-%E5%BD%92%E5%B9%B6%E6%AD%A5%E9%AA%A42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-%E5%BD%92%E5%B9%B6%E6%AD%A5%E9%AA%A43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class MergeSort {
    public static void main(String[] args) {
        int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        mergeSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
	// low为arr最小索引，high为最大索引
    public static void mergeSort(int[] arr, int low, int high) {
        // low == high 时说明只有一个元素了，直接返回
        if (low &amp;lt; high) {
            int mid = (low + high) / 2;
            mergeSort(arr, low, mid);		// 归并排序前半段
            mergeSort(arr, mid + 1, high);	// 归并排序后半段
            merge(arr, low, mid, high);		// 将两段有序段合成一段有序段
        }
    }

    private static void merge(int[] arr, int low, int mid, int high) {
        int index = 0;
        // 定义左右指针
        int left = low, right = mid + 1;
        int[] assist = new int[high - low + 1];
        
        while (left &amp;lt;= mid &amp;amp;&amp;amp; right &amp;lt;= high) {
            assist[index++] = arr[left] &amp;lt; arr[right] ? arr[left++] : arr[right++];
        }
        while (left &amp;lt;= mid) {
            assist[index++] = arr[left++];
        }
        while (right &amp;lt;= high) {
            assist[index++] = arr[right++];
        }

        for (int k = 0; k &amp;lt; assist.length; k++) {
            arr[low++] = assist[k];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-归并排序时间复杂度.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;用树状图来描述归并，假设元素的个数为 n，那么使用归并排序拆分的次数为 &lt;code&gt;log2(n)&lt;/code&gt;，即层数，每次归并需要做 n 次对比，最终得出的归并排序的时间复杂度为 &lt;code&gt;log2(n)*n&lt;/code&gt;，根据大O推导法则，忽略底数，最终归并排序的时间复杂度为 O(nlogn)&lt;/p&gt;
&lt;p&gt;归并排序的缺点：需要申请额外的数组空间，导致空间复杂度提升，是典型的&lt;strong&gt;以空间换时间&lt;/strong&gt;的操作&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;快速排序&lt;/h3&gt;
&lt;p&gt;快速排序（Quick Sort）：通过&lt;strong&gt;分治思想&lt;/strong&gt;对冒泡排序的改进，基本过程是通过一趟排序将要排序的数据分割成独立的两部分，其中一部分的所有数据都比另外一部分的所有数据都要小，然后再按此方法对这两部分数据分别进行快速排序，以此达到整个数据变成有序序列&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从数列中挑出一个元素，称为基准（pivot）&lt;/li&gt;
&lt;li&gt;重新排序数列，所有比基准值小的摆放在基准前面，所有比基准值大的摆在基准的后面（相同的数可以到任一边），在这个分区退出之后，该基准就处于数列的中间位置，这个称为分区（partition）操作；&lt;/li&gt;
&lt;li&gt;递归地（recursive）把小于基准值元素的子数列和大于基准值元素的子数列排序&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-快速排序.gif&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr, int low, int high) {
        // 递归结束的条件
        if (low &amp;gt;= high) {
            return;
        }
        
        int left = low;
        int right = high;
        // 基准数
        int temp = arr[left];
        while (left &amp;lt; right) {
            // 用 &amp;gt;= 可以防止多余的交换
            while (arr[right] &amp;gt;= temp &amp;amp;&amp;amp; right &amp;gt; left) {
                right--;
            }
            // 做判断防止相等
            if (right &amp;gt; left) {
                // 到这里说明 arr[right] &amp;lt; temp 
                arr[left] = arr[right];// 此时把arr[right]元素视为空
                left++;
            }
            while (arr[left] &amp;lt;= temp &amp;amp;&amp;amp; left &amp;lt; right) {
                left++;
            }
            if (right &amp;gt; left) {
                arr[right] = arr[left];
                right--;
            }
        }
        // left == right
        arr[left] = temp;
        quickSort(arr, low, left-1);
        quickSort(arr, right + 1, high);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;快速排序和归并排序的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速排序是另外一种分治的排序算法，将一个数组分成两个子数组，将两部分独立的排序&lt;/li&gt;
&lt;li&gt;归并排序的处理过程是由下到上的，先处理子问题，然后再合并。而快排正好相反，它的处理过程是由上到下的，先分区，然后再处理子问题&lt;/li&gt;
&lt;li&gt;快速排序和归并排序是互补的：归并排序将数组分成两个子数组分别排序，并将有序的子数组归并从而将整个数组排序，而快速排序的方式则是当两个数组都有序时，整个数组自然就有序了&lt;/li&gt;
&lt;li&gt;在归并排序中，一个数组被等分为两半，归并调用发生在处理整个数组之前，在快速排序中，切分数组的位置取决于数组的内容，递归调用发生在处理整个数组之后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最优情况：每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树，共切分了 logn 次，所以，最优情况下快速排序的时间复杂度为 O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最坏情况：每一次切分选择的基准数字是当前序列中最大数或者最小数，这使得每次切分都会有一个子组，那么总共就得切分n次，所以最坏情况下，快速排序的时间复杂度为 O(n^2)&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-快排时间复杂度.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;平均情况：每一次切分选择的基准数字不是最大值和最小值，也不是中值，这种情况用数学归纳法证明，快速排序的时间复杂度为 O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐视频：https://www.bilibili.com/video/BV1b7411N798?t=1001&amp;amp;p=81&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/nrsc272420199/article/details/82587933&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;基数排序&lt;/h3&gt;
&lt;p&gt;基数排序（Radix Sort）：又叫桶排序和箱排序，借助多关键字排序的思想对单逻辑关键字进行排序的方法&lt;/p&gt;
&lt;p&gt;计数排序其实是桶排序的一种特殊情况，当要排序的 n 个数据，所处的范围并不大的时候，比如最大值是 k，我们就可以把数据划分成 k 个桶，每个桶内的数据值都是相同的，省掉了桶内排序的时间&lt;/p&gt;
&lt;p&gt;按照低位先排序，然后收集；再按照高位排序，然后再收集；依次类推，直到最高位。有时候有些属性是有优先级顺序的，先按低优先级排序，再按高优先级排序。最后的次序就是高优先级高的在前，高优先级相同的低优先级高的在前&lt;/p&gt;
&lt;p&gt;解释：先排低位再排高位，可以说明在高位相等的情况下低位是递增的，如果高位也是递增，则数据有序&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-基数排序.gif&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;实现思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获得最大数的位数，可以通过将最大数变为 String 类型，再求长度&lt;/li&gt;
&lt;li&gt;将所有待比较数值（正整数）统一为同样的数位长度，&lt;strong&gt;位数较短的数前面补零&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;从最低位开始，依次进行一次排序&lt;/li&gt;
&lt;li&gt;从最低位排序一直到最高位（个位 → 十位 → 百位 → … →最高位）排序完成以后，数列就变成一个有序序列&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BucketSort {
    public static void main(String[] args) {
        int[] arr = new int[]{576, 22, 26, 548, 1, 3, 843, 536, 735, 43, 3, 912, 88};
        bucketSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static void bucketSort(int[] arr) {
        // 桶的个数固定为10个（个位是0~9），数组长度为了防止所有的数在同一行
        int[][] bucket = new int[10][arr.length];
        //记录每个桶中的有多少个元素
        int[] elementCounts = new int[10];

        //获取数组的最大元素
        int max = arr[0];
        for (int i = 1; i &amp;lt; arr.length; i++) {
            max = max &amp;gt; arr[i] ? max : arr[i];
        }
        String maxEle = Integer.toString(max);
        //将数组中的元素放入桶中，最大数的位数相当于需要几次放入桶中
        for (int i = 0, step = 1; i &amp;lt; maxEle.length(); i++, step *= 10) {
            for (int j = 0; j &amp;lt; arr.length; j++) {
                //获取最后一位的数据，也就是索引
                int index = (arr[j] / step) % 10;
                //放入具体位置
                bucket[index][elementCounts[index]] = arr[j];
                //存储每个桶的数量
                elementCounts[index]++;
            }
            //收集回数组
            for (int j = 0, index = 0; j &amp;lt; 10; j++) {
                //先进先出
                int position = 0;
                //桶中有元素就取出
                while (elementCounts[j] &amp;gt; 0) {
                    arr[index] = bucket[j][position];
                    elementCounts[j]--;
                    position++;
                    index++;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空间换时间&lt;/p&gt;
&lt;p&gt;推荐视频：https://www.bilibili.com/video/BV1b7411N798?p=86&lt;/p&gt;
&lt;p&gt;参考文章：https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;算法总结&lt;/h3&gt;
&lt;h4&gt;稳定性&lt;/h4&gt;
&lt;p&gt;稳定性：在待排序的记录序列中，存在多个具有相同的关键字的记录，若经过排序，这些记录的相对次序保持不变，即在原序列中 &lt;code&gt;r[i]=r[j]&lt;/code&gt;，且 r[i] 在 r[j] 之前，而在排序后的序列中，r[i] 仍在 r[j] 之前，则称这种排序算法是稳定的，否则称为不稳定的&lt;/p&gt;
&lt;p&gt;如果一组数据只需要一次排序，则稳定性一般是没有意义的，如果一组数据需要多次排序，稳定性是有意义的。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-稳定性.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;冒泡排序：只有当 &lt;code&gt;arr[i]&amp;gt;arr[i+1]&lt;/code&gt; 的时候，才会交换元素的位置，而相等的时候并不交换位置，所以冒泡排序是一种稳定排序算法&lt;/li&gt;
&lt;li&gt;选择排序：是给每个位置选择当前元素最小的，例如有数据{5(1)，8 ，5(2)， 3， 9 }，第一遍选择到的最小元素为3，所以5(1)会和3进行交换位置，此时5(1)到了5(2)后面，破坏了稳定性，所以是不稳定的排序算法&lt;/li&gt;
&lt;li&gt;插入排序：比较是从有序序列的末尾开始，也就是想要插入的元素和已经有序的最大者开始比起，如果比它大则直接插入在其后面，否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的，那么把要插入的元素放在相等元素的后面。相等元素的前后顺序没有改变，从原无序序列出去的顺序就是排好序后的顺序，所以插入排序是稳定的&lt;/li&gt;
&lt;li&gt;希尔排序：按照不同步长对元素进行插入排序，虽然一次插入排序是稳定的，但在不同的插入排序过程中，相同的元素可能在各自的插入排序中移动，最后其稳定性就会被打乱，所以希尔排序是不稳定的&lt;/li&gt;
&lt;li&gt;归并排序在归并的过程中，只有 &lt;code&gt;arr[i]&amp;lt;arr[i+1]&lt;/code&gt; 的时候才会交换位置，如果两个元素相等则不会交换位置，所以它并不会破坏稳定性，归并排序是稳定的&lt;/li&gt;
&lt;li&gt;快速排序：快排需要一个基准值，在基准值的右侧找一个比基准值小的元素，在基准值的左侧找一个比基准值大的元素，然后交换这两个元素，此时会破坏稳定性，所以快速排序是一种不稳定的算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记忆口诀：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;情绪不稳定，快些选一堆好友来聊天&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快：快速排序、些：希尔排序、选：选择排序、堆：堆排序&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;算法对比&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;补充问题&lt;/h4&gt;
&lt;p&gt;海量数据问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;海量数据排序：
&lt;ul&gt;
&lt;li&gt;外部排序：归并 + 败者树&lt;/li&gt;
&lt;li&gt;基数排序：https://time.geekbang.org/column/article/42038&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;海量数据查询：
&lt;ul&gt;
&lt;li&gt;布隆过滤器判断是否存在&lt;/li&gt;
&lt;li&gt;构建索引：B+ 树、跳表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;查找&lt;/h2&gt;
&lt;p&gt;正常查找：从第一个元素开始遍历，一个一个的往后找，综合查找比较耗时&lt;/p&gt;
&lt;p&gt;二分查找也称折半查找（Binary Search）是一种效率较高的查找方法，数组必须是有序数组&lt;/p&gt;
&lt;p&gt;过程：每次先与中间的元素进行比较，如果大于往右边找，如果小于往左边找，如果等于就返回该元素索引位置，如果没有该元素，返回 -1&lt;/p&gt;
&lt;p&gt;时间复杂度：O(logn)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*定义一个方法，记录开始的索引位置和结束的索引位置。
取出中间索引位置的值，拿元素与中间位置的值进行比较，如果小于中间值，结束位置=中间索引-1.
取出中间索引位置的值，拿元素与中间位置的值进行比较，如果大于中间值，开始位置=中间索引+1.
循环正常执行的条件：开始位置索引&amp;lt;=结束位置索引。否则说明寻找完毕但是没有该元素值返回-1.*/
public class binarySearch {
    public static void main(String[] args) {
        int[] arr = {10, 14, 21, 38, 45, 47, 53, 81, 87, 99};
        System.out.println(&quot;81的索引是：&quot; + binarySearch(arr,81));

    }

    public static int binarySearch(int[] arr, int des) {
        int start = 0;
        int end = arr.length - 1;

        // 确保不会出现重复查找，越界
        while (start &amp;lt;= end) {
            // 计算出中间索引值
            int mid = (start + end) / 2;
            if (des == arr[mid]) {
                return mid;
            } else if (des &amp;gt; arr[mid]) {
                start = mid + 1;
            } else if (des &amp;lt; arr[mid]) {
                end = mid - 1;
            }
        }
        // 如果上述循环执行完毕还没有返回索引，说明根本不存在该元素值，直接返回-1
        return -1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查找第一个匹配的元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static int binarySearch(int[] arr, int des) {
        int start = 0;
        int end = arr.length - 1;

        while (start &amp;lt;= end) {
            int mid = (start + end) / 2;
            if (des == arr[mid]) {
                //如果 mid 等于 0，那这个元素已经是数组的第一个元素，那肯定是我要找的
                if (mid == 0 || a[mid - 1] != des) {
                    return mid;
                } else {
                    //a[mid]前面的一个元素 a[mid-1]也等于 value，
                    //要找的元素肯定出现在[low, mid-1]之间
                    high = mid - 1
                }
            } else if (des &amp;gt; arr[mid]) {
                start = mid + 1;
            } else if (des &amp;lt; arr[mid]) {
                end = mid - 1;
            }
        }
        return -1;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;匹配&lt;/h2&gt;
&lt;h3&gt;BF&lt;/h3&gt;
&lt;p&gt;Brute Force 暴力匹配算法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    String s = &quot;seazean&quot;;
    String t = &quot;az&quot;;
    System.out.println(match(s,t));//2
}

public static int match(String s,String t) {
    int k = 0;
    int i = k, j = 0;
    //防止越界
    while (i &amp;lt; s.length() &amp;amp;&amp;amp; j &amp;lt; t.length()) {
        if (s.charAt(i) == t.charAt(j)) {
            ++i;
            ++j;
        } else {
            k++;
            i = k;
            j = 0;
        }
    }
    //说明是匹配成功
    if (j &amp;gt;= t.length()) {
        return k;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平均时间复杂度：O(m+n)，最坏时间复杂度：O(m*n)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;RK&lt;/h3&gt;
&lt;p&gt;把主串得长度记为 n，模式串得长度记为 m，通过哈希算法对主串中的 n-m+1 个子串分别求哈希值，然后逐个与模式串的哈希值比较大小，如果某个子串的哈希值与模式串相等，再去对比值是否相等（防止哈希冲突），那就说明对应的子串和模式串匹配了&lt;/p&gt;
&lt;p&gt;因为哈希值是一个数字，数字之间比较是否相等是非常快速的&lt;/p&gt;
&lt;p&gt;第一部分计算哈希值的时间复杂度为 O(n)，第二部分对比的时间复杂度为 O(1)，整体平均时间复杂度为 O(n)，最坏为 O(n*m)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;KMP&lt;/h3&gt;
&lt;p&gt;KMP 匹配：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;next 数组的核心就是自己匹配自己，主串代表后缀，模式串代表前缀&lt;/li&gt;
&lt;li&gt;nextVal 数组的核心就是回退失配&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Kmp {
    public static void main(String[] args) {
        String s = &quot;acababaabc&quot;;
        String t = &quot;abaabc&quot;;
        //[-1, 0, 0, 1, 1, 2]
        System.out.println(Arrays.toString(getNext(t)));
        //[-1, 0, -1, 1, 0, 2]
        System.out.println(Arrays.toString(getNextVal(t)));
        //5
        System.out.println(kmp(s, t));
    }

    private static int kmp(String s, String t) {
        int[] next = getNext(t);
        int i = 0, j = 0;
        while (i &amp;lt; s.length() &amp;amp;&amp;amp; j &amp;lt; t.length()) {
            //j==-1时说明第一个位置匹配失败，所以将s的下一个和t的首字符比较
            if (j == -1 || s.charAt(i) == t.charAt(j)) {
                i++;
                j++;
            } else {
                //模式串右移，比较s的当前位置与t的next[j]位置
                j = next[j];
            }
        }
        if (j &amp;gt;= t.length()) {
            return i - j + 1;
        }
        return -1;
    }
	//next数组
    private static int[] getNext(String t) {
        int[] next = new int[t.length()];
        next[0] = -1;
        int j = -1;
        int i = 0;
        while (i &amp;lt; t.length() - 1) {
            // 根据已知的前j位推测第j+1位
            // j=-1说明首位就没有匹配，即t[0]!=t[i]，说明next[i+1]没有最大前缀，为0
            if (j == -1 || t.charAt(i) == t.charAt(j)) {
                // 因为模式串已经匹配到了索引j处，说明之前的位都是相等的
                // 因为是自己匹配自己，所以模式串就是前缀，主串就是后缀，j就是最长公共前缀
                // 当i+1位置不匹配时（i位之前匹配），可以跳转到j+1位置对比，next[i+1]=j+1
                i++;
                j++;
                next[i] = j;
            } else {
                //i位置的数据和j位置的不相等，所以回退对比i和next[j]位置的数据
                j = next[j];
            }

        }
        return next;
    }
	//nextVal
    private static int[] getNextVal(String t) {
        int[] nextVal = new int[t.length()];
        nextVal[0] = -1;
        int j = -1;
        int i = 0;
        while (i &amp;lt; t.length() - 1) {
            if (j == -1 || t.charAt(i) == t.charAt(j)) {
                i++;
                j++;
                // 如果t[i+1] == t[next(i+1)]=next[j+1]，回退后仍然失配，所以要继续回退
                if (t.charAt(i) == t.charAt(j)) {
                    nextVal[i] = nextVal[j];
                } else {
                    nextVal[i] = j;
                }
            } else {
                j = nextVal[j];
            }
        }
        return nextVal;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平均和最坏时间复杂度都是 O(m+n)&lt;/p&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/tangzhengyue/p/4315393.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;树&lt;/h2&gt;
&lt;h3&gt;二叉树&lt;/h3&gt;
&lt;p&gt;二叉树中，任意一个节点的度要小于等于 2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点：在树结构中,每一个元素称之为节点&lt;/li&gt;
&lt;li&gt;度：每一个节点的子节点数量称之为度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/二叉树结构图.png&quot; alt=&quot;二叉树结构图&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;排序树&lt;/h3&gt;
&lt;h4&gt;存储结构&lt;/h4&gt;
&lt;p&gt;二叉排序树（BST），又称二叉查找树或者二叉搜索树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个节点上最多有两个子节点&lt;/li&gt;
&lt;li&gt;左子树上所有节点的值都小于根节点的值&lt;/li&gt;
&lt;li&gt;右子树上所有节点的值都大于根节点的值&lt;/li&gt;
&lt;li&gt;不存在重复的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/二叉查找树结构图.png&quot; alt=&quot;二叉查找树&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;节点类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static class TreeNode {
    int key;
    TreeNode left;  //左节点
    TreeNode right; //右节点

    private TreeNode(int key) {
        this.key = key;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; // 递归查找
private static TreeNode search(TreeNode root, int key) {
    //递归结束的条件
    if (root == null) {
        return null;
    }
    if (key == root.key) {
        return root;
    } else if (key &amp;gt; root.key) {
        return search(root.right, key);
    } else {
        return search(root.left, key);
    }
}

// 非递归
private static TreeNode search1(TreeNode root, int key) {
    while (root != null) {
        if (key == root.key) {
            return root;
        } else if (key &amp;gt; root.key) {
            root = root.right;
        } else {
            root = root.left;
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int insert(TreeNode root, int key) {
    if (root == null) {
        root = new TreeNode(key);
        root.left = null;
        root.right = null;
        return 1;
    } else {
        if (key == root.key) {
            return 0;
        } else if (key &amp;gt; root.key) {
            return insert(root.right, key);
        } else {
            return insert(root.left, key);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 构造函数，返回根节点
private static TreeNode createBST(int[] arr) {
    if (arr.length &amp;gt; 0) {
        TreeNode root = new TreeNode(arr[0]);
        for (int i = 1; i &amp;lt; arr.length; i++) {
            insert(root, arr[i]);
        }
        return root;
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除节点：要删除节点12，先找到节点19，然后移动并替换节点12&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-二叉查找树删除节点.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;代码链接：https://leetcode-cn.com/submissions/detail/190232548/&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1iJ411E7xW?t=756&amp;amp;p=86&lt;/p&gt;
&lt;p&gt;图片来源：https://leetcode-cn.com/problems/delete-node-in-a-bst/solution/tu-jie-yi-dong-jie-dian-er-bu-shi-xiu-ga-edtn/&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;平衡树&lt;/h3&gt;
&lt;p&gt;平衡二叉树（AVL）的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二叉树左右两个子树的高度差不超过 1&lt;/li&gt;
&lt;li&gt;任意节点的左右两个子树都是一颗平衡二叉树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;平衡二叉树旋转：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;旋转触发时机：当添加一个节点之后，该树不再是一颗平衡二叉树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;平衡二叉树和二叉查找树对比结构图&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E5%92%8C%E4%BA%8C%E5%8F%89%E6%9F%A5%E6%89%BE%E6%A0%91%E5%AF%B9%E6%AF%94%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;平衡二叉树和二叉查找树对比&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左旋：将根节点的右侧往左拉，原先的右子节点变成新的父节点，并把多余的左子节点出让，给已经降级的根节点当右子节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E5%B7%A6%E6%97%8B01.png&quot; alt=&quot;平衡二叉树左旋&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;右旋：将根节点的左侧往右拉，左子节点变成了新的父节点，并把多余的右子节点出让，给已经降级根节点当左子节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E5%8F%B3%E6%97%8B01.png&quot; alt=&quot;平衡二叉树右旋&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐文章：https://pdai.tech/md/algorithm/alg-basic-tree-balance.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;红黑树&lt;/h3&gt;
&lt;p&gt;红黑树的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个节点可以是红或者黑&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;红黑树不是高度平衡的，它的平衡是通过自己的红黑规则进行实现的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;红黑树的红黑规则有哪些：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每一个节点或是红色的，或者是黑色的&lt;/li&gt;
&lt;li&gt;根节点必须是黑色&lt;/li&gt;
&lt;li&gt;如果一个节点没有子节点或者父节点，则该节点相应的指针属性值为 Nil，这些 Nil 视为叶节点，每个叶节点 (Nil) 是黑色的&lt;/li&gt;
&lt;li&gt;如果某一个节点是红色，那么它的子节点必须是黑色（不能出现两个红色节点相连的情况）&lt;/li&gt;
&lt;li&gt;对每一个节点，从该节点到其所有后代叶节点的简单路径上，均包含相同数目的黑色节点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;红黑树与 AVL 树的比较：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AVL 树是更加严格的平衡，可以提供更快的查找速度，适用于读取&lt;strong&gt;查找密集型任务&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;红黑树只是做到近似平衡，并不是严格的平衡，红黑树的插入删除比 AVL 树更便于控制，红黑树更适合于&lt;strong&gt;插入修改密集型任务&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;红黑树整体性能略优于 AVL 树，AVL 树的旋转比红黑树的旋转多，更加难以平衡和调试，插入和删除的效率比红黑树慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E7%BA%A2%E9%BB%91%E6%A0%91%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;红黑树&quot; /&gt;&lt;/p&gt;
&lt;p&gt;红黑树添加节点的默认颜色为红色，效率高
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/%E7%BA%A2%E9%BB%91%E6%A0%91%E6%B7%BB%E5%8A%A0%E8%8A%82%E7%82%B9%E9%A2%9C%E8%89%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;红黑树添加节点后如何保持红黑规则：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根节点位置
&lt;ul&gt;
&lt;li&gt;直接变为黑色&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非根节点位置
&lt;ul&gt;
&lt;li&gt;父节点为黑色
&lt;ul&gt;
&lt;li&gt;不需要任何操作,默认红色即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;父节点为红色
&lt;ul&gt;
&lt;li&gt;叔叔节点为红色
&lt;ol&gt;
&lt;li&gt;将&quot;父节点&quot;设为黑色,将&quot;叔叔节点&quot;设为黑色&lt;/li&gt;
&lt;li&gt;将&quot;祖父节点&quot;设为红色&lt;/li&gt;
&lt;li&gt;如果&quot;祖父节点&quot;为根节点,则将根节点再次变成黑色&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;叔叔节点为黑色
&lt;ol&gt;
&lt;li&gt;将&quot;父节点&quot;设为黑色&lt;/li&gt;
&lt;li&gt;将&quot;祖父节点&quot;设为红色&lt;/li&gt;
&lt;li&gt;以&quot;祖父节点&quot;为支点进行旋转&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;并查集&lt;/h3&gt;
&lt;h4&gt;基本实现&lt;/h4&gt;
&lt;p&gt;并查集是一种树型的数据结构，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个元素都唯一的对应一个结点&lt;/li&gt;
&lt;li&gt;每一组数据中的多个元素都在同一颗树中&lt;/li&gt;
&lt;li&gt;一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系&lt;/li&gt;
&lt;li&gt;元素在树中并没有子父级关系的硬性要求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-并查集.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以高效地进行如下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询元素 p 和元素 q 是否属于同一组&lt;/li&gt;
&lt;li&gt;合并元素 p 和元素 q 所在的组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储结构：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-并查集存储结构.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;合并方式：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-并查集合并.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UF {
    //记录节点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    //记录分组的个数
    private int count;

    //初始化并查集
    public UF(int N) {
        //初始化分组数量
        this.count = N;
        //初始化eleAndGroup数量
        this.eleAndGroup = new int[N];
        //初始化eleAndGroup中的元素及其所在分组的标识符，eleAndGroup索引作为每个节点的元素
        //每个索引处的值就是该组的索引，就是该元素所在的组的标识符
        for (int i = 0; i &amp;lt; eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
    }

    //查询p所在的分组的标识符
    public int find(int p) {
        return eleAndGroup[p];
    }

    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connect(int p, int q) {
        return find(p) == find(q);
    }

    //把p元素所在分组和q元素所在分组合并
    public void union(int p, int q) {
        //判断元素q和p是否已经在同一个分组中，如果已经在同一个分组中，则结束方法就可以了
        if (connect(p, q)) {
            return;
        }
        int pGroup = find(p);//找到p所在分组的标识符
        int qGroup = find(q);//找到q所在分组的标识符

        //合并组，让p所在组的 所有元素 的组标识符变为q所在分组的标识符
        for (int i = 0; i &amp;lt; eleAndGroup.length; i++) {
            if (eleAndGroup[i] == pGroup) {
                eleAndGroup[i] = qGroup;
            }
        }
        //分组个数-1
        this.count--;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    //创建并查集对象
    UF uf = new UF(5);
    System.out.println(uf);

    //从控制台录入两个合并的元素，调用union方法合并，观察合并后并查集的分组
    Scanner sc = new Scanner(System.in);

    while (true) {
        System.out.println(&quot;输入第一个要合并的元素&quot;);
        int p = sc.nextInt();
        System.out.println(&quot;输入第二个要合并的元素&quot;);
        int q = sc.nextInt();
        if (uf.connect(p, q)) {
            System.out.println(p + &quot;元素已经和&quot; + q + &quot;元素已经在同一个组&quot;);
            continue;
        }
        uf.union(p, q);
        System.out.println(&quot;当前并查集中还有：&quot; + uf.count() + &quot;个分组&quot;);
        System.out.println(uf);
        System.out.println(&quot;********************&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最坏情况下 union 算法的时间复杂度也是 O(N^2)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;优化实现&lt;/h4&gt;
&lt;p&gt;让每个索引处的节点都指向它的父节点，当 eleGroup[i] = i 时，说明 i 是根节点&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-并查集优化.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//查询p所在的分组的标识符，递归寻找父标识符，直到找到根节点
public int findRoot(int p) {
    while (p != eleAndGroup[p]) {
        p = eleAndGroup[p];
    }
    //p == eleGroup[p]，说明p是根节点
    return p;
}

//判断并查集中元素p和元素q是否在同一分组中
public boolean connect(int p, int q) {
    return findRoot(p) == findRoot(q);
}

//把p元素所在分组和q元素所在分组合并
public void union(int p, int q) {
    //找到p q对应的根节点
    int pRoot = findRoot(p);
    int qRoot = findRoot(q);
    if (pRoot == qRoot) {
        return;
    }
    //让p所在树的节点根节点为q的所在的根节点，只需要把根节点改一下，时间复杂度 O(1)
    eleAndGroup[pRoot] = qRoot;
    this.count-
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平均时间复杂度为 O(N)，最坏时间复杂度是 O(N^2)&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-并查集时间复杂度.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;继续优化：路径压缩，保证每次把小树合并到大树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UF_Tree_Weighted {
    private int[] eleAndGroup;
    private int count;
    private int[] size;//存储每一个根结点对应的树中的保存的节点的个数

    //初始化并查集
    public UF_Tree_Weighted(int N) {
        this.count = N;
        this.eleAndGroup = new int[N];
        for (int i = 0; i &amp;lt; eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
        this.size = new int[N];
        //默认情况下，size中每个索引处的值都是1
        for (int i = 0; i &amp;lt; size.length; i++) {
            size[i] = 1;
        }
    }
	//查询p所在的分组的标识符，父标识符
    public int findRoot(int p) {
        while (p != eleAndGroup[p]) {
            p = eleAndGroup[p];
        }
        return p;
    }

    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connect(int p, int q) {
        return findRoot(p) == findRoot(q);
    }

    //把p元素所在分组和q元素所在分组合并
    public void union(int p, int q) {
        //找到p q对应的根节点
        int pRoot = findRoot(p);
        int qRoot = findRoot(q);
        if (pRoot == qRoot) {
            return;
        }
        //判断pRoot对应的树大还是qRoot对应的树大，最终需要把较小的树合并到较大的树中
        if (size[pRoot] &amp;lt; size[qRoot]) {
            eleAndGroup[pRoot] = qRoot;
            size[qRoot] += size[pRoot];
        } else {
            eleAndGroup[qRoot] = pRoot;
            size[pRoot] += size[qRoot];
        }
        //组的数量-1、
        this.count--;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;应用场景&lt;/h4&gt;
&lt;p&gt;并查集存储的每一个整数表示的是一个大型计算机网络中的计算机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通&lt;/li&gt;
&lt;li&gt;可以调用 union(int p,int q) 使得 p 和 q 之间连通，这样两台计算机之间就可以通信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;畅通工程：某省调查城镇交通状况，得到现有城镇道路统计表，表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通，但不一定有直接的道路相连，只要互相间接通过道路可达即可，问最少还需要建设多少条道路？&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-应用场景.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;解题思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个并查集 UF_Tree_Weighted(20)&lt;/li&gt;
&lt;li&gt;分别调用 union(0,1)、union(6,9)、union(3,8)、union(5,11)、union(2,12)、union(6,10)、union(4,8)，表示已经修建好的道路把对应的城市连接起来&lt;/li&gt;
&lt;li&gt;如果城市全部连接起来，那么并查集中剩余的分组数目为 1，所有的城市都在一个树中，只需要获取当前并查集中剩余的数目减去 1，就是还需要修建的道路数目&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args)throws Exception {
    Scanner sc = new Scanner(System.in);
    //读取城市数目，初始化并查集
    int number = sc.nextInt();
    //读取已经修建好的道路数目
    int roadNumber = sc.nextInt();
    UF_Tree_Weighted uf = new UF_Tree_Weighted(number);
    //循环读取已经修建好的道路，并调用union方法
    for (int i = 0; i &amp;lt; roadNumber; i++) {
        int p = sc.nextInt();
        int q = sc.nextInt();
        uf.union(p,q);
    }
    //获取剩余的分组数量
    int groupNumber = uf.count();
    //计算出还需要修建的道路
    System.out.println(&quot;还需要修建&quot;+(groupNumber-1)+&quot;道路，城市才能相通&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1iJ411E7xW?p=142&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;字典树&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Trie 树，也叫字典树，是一种专门处理字符串匹配的树形结构，用来解决在一组字符串集合中快速查找某个字符串的问题，Trie 树的本质就是利用字符串之间的公共前缀，将重复的前缀合并在一起&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根节点不包含任何信息&lt;/li&gt;
&lt;li&gt;每个节点表示一个字符串中的字符，从&lt;strong&gt;根节点到红色节点的一条路径表示一个字符串&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;红色节点并不都是叶子节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-字典树构造过程1.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-字典树构造过程2.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;注意：要查找的是字符串“he”，从根节点开始，沿着某条路径来匹配，可以匹配成功。但是路径的最后一个节点“e”并不是红色的，也就是说，“he”是某个字符串的前缀子串，但并不能完全匹配任何字符串&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现Trie&lt;/h4&gt;
&lt;p&gt;通过一个下标与字符一一映射的数组，来存储子节点的指针&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-字典树存储结构.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;时间复杂度是 O(n)（n 表示要查找字符串的长度）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Trie {
    private TrieNode root = new TrieNode(&apos;/&apos;);

    //插入一个字符
    public void insert(char[] chars) {
        TrieNode p = root;
        for (int i = 0; i &amp;lt; chars.length; i++) {
            //获取字符的索引位置
            int index = chars[i] - &apos;a&apos;;
            if (p.children[index] == null) {
                TrieNode node = new TrieNode(chars[i]);
                p.children[index] = node;
            }
            p = p.children[index];
        }
        p.isEndChar = true;
    }

    //查找一个字符串
    public boolean find(char[] chars) {
        TrieNode p = root;
        for (int i = 0; i &amp;lt; chars.length; i++) {
            int index = chars[i] - &apos;a&apos;;
            if (p.children[index] == null) {
                return false;
            }
            p = p.children[index];
        }
        if (p.isEndChar) {
            //完全匹配
            return true;
        } else {
            // 不能完全匹配，只是前缀
            return false;
        }
    }


    private class TrieNode {
        char data;
        TrieNode[] children = new TrieNode[26];//26个英文字母
        boolean isEndChar = false;//结尾字符为true
        public TrieNode(char data) {
            this.data = data;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;优化Trie&lt;/h4&gt;
&lt;p&gt;Trie 树是非常耗内存，采取空间换时间的思路。Trie 树的变体有很多，可以在一定程度上解决内存消耗的问题。比如缩点优化，对只有一个子节点的节点，而且此节点不是一个串的结束节点，可以将此节点与子节点合并&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-%E5%AD%97%E5%85%B8%E6%A0%91%E7%BC%A9%E7%82%B9%E4%BC%98%E5%8C%96.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/72414&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;图&lt;/h2&gt;
&lt;p&gt;图的邻接表形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AGraph {
    private VertexNode[] adjList;   //邻接数组
    private int vLen, eLen;         //顶点数和边数

    public AGraph(int vLen, int eLen) {
        this.vLen = vLen;
        this.eLen = eLen;
        adjList = new VertexNode[vLen];
    }
    //弧节点
    private class ArcNode {
        int adjVex;         //该边所指向的顶点的位置
        ArcNode nextArc;    //下一条边（弧）
        //int info  		//添加权值

        public ArcNode(int adjVex) {
            this.adjVex = adjVex;
            nextArc = null;
        }
    }

    //表顶点
    private class VertexNode {
        char data;      	//顶点信息
        ArcNode firstArc;  	//指向第一条边的指针

        public VertexNode(char data) {
            this.data = data;
            firstArc = null;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图的邻接矩阵形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MGraph {
    private int[][] edges;      //邻接矩阵定义，有权图将int改为float
    private int vLen;           //顶点数
    private int eLen;           //边数
    private VertexNode[] vex;   //存放节点信息

    public MGraph(int vLen, int eLen) {
        this.vLen = vLen;
        this.eLen = eLen;
        this.edges = new int[vLen][vLen];
        this.vex = new VertexNode[vLen];
    }

    private class VertexNode {
        int num;    //顶点编号
        String info;  //顶点信息

        public VertexNode(int num) {
            this.num = num;
            this.info = null;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图相关的算法需要很多的流程图，此处不再一一列举，推荐参考书籍《数据结构高分笔记》&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;位图&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;布隆过滤器：一种数据结构，是一个很长的二进制向量（位数组）和一系列随机映射函数（哈希函数），既然是二进制，每个空间存放的不是 0 就是 1，但是初始默认值都是 0，所以布隆过滤器不存数据只存状态&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-Bitmaps数据结构.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这种数据结构是高效且性能很好的，但缺点是具有一定的错误识别率和删除难度。并且理论情况下，添加到集合中的元素越多，误报的可能性就越大&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;p&gt;向布隆过滤器中添加一个元素 key 时，会通过多个 hash 函数得到多个哈希值，在位数组中把对应下标的值置为 1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8%E6%B7%BB%E5%8A%A0%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;布隆过滤器查询一个数据，是否在二进制的集合中，查询过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 K 个哈希函数计算该数据，对应计算出的 K 个 hash 值&lt;/li&gt;
&lt;li&gt;通过 hash 值找到对应的二进制的数组下标&lt;/li&gt;
&lt;li&gt;判断方法：如果存在一处位置的二进制数据是 0，那么该数据一定不存在。如果都是 1，则认为数据存在集合中（会误判）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;布隆过滤器优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二进制组成的数组，占用内存极少，并且插入和查询速度都足够快&lt;/li&gt;
&lt;li&gt;去重方便：当字符串第一次存储时对应的位数组下标设置为 1，当第二次存储相同字符串时，因为对应位置已设置为 1，所以很容易知道此值已经存在&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着数据的增加，误判率会增加：添加数据是通过计算数据的 hash 值，不同的字符串可能哈希出来的位置相同，导致无法确定到底是哪个数据存在，&lt;strong&gt;这种情况可以适当增加位数组大小或者调整哈希函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;无法删除数据：可能存在几个数据占据相同的位置，所以删除一位会导致很多数据失效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总结：&lt;strong&gt;布隆过滤器判断某个元素存在，小概率会误判。如果判断某个元素不在，那这个元素一定不在&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/ysocean/p/12594982.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Guava&lt;/h3&gt;
&lt;p&gt;引入 Guava 的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.google.guava&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;guava&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;28.0-jre&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定误判率为（0.01）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    // 创建布隆过滤器对象
    BloomFilter&amp;lt;Integer&amp;gt; filter = BloomFilter.create(
        Funnels.integerFunnel(),
        1500,
        0.01);
    // 判断指定元素是否存在
    System.out.println(filter.mightContain(1));
    System.out.println(filter.mightContain(2));
    // 将元素添加进布隆过滤器
    filter.put(1);
    filter.put(2);
    System.out.println(filter.mightContain(1));
    System.out.println(filter.mightContain(2));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;实现布隆&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MyBloomFilter {
    //布隆过滤器容量
    private static final int DEFAULT_SIZE = 2 &amp;lt;&amp;lt; 28;
    //bit数组，用来存放key
    private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
    //后面hash函数会用到，用来生成不同的hash值，随意设置
    private static final int[] ints = {1, 6, 16, 38, 58, 68};

    //add方法，计算出key的hash值，并将对应下标置为true
    public void add(Object key) {
        Arrays.stream(ints).forEach(i -&amp;gt; bitSet.set(hash(key, i)));
    }

    //判断key是否存在，true不一定说明key存在，但是false一定说明不存在
    public boolean isContain(Object key) {
        boolean result = true;
        for (int i : ints) {
            //短路与，只要有一个bit位为false，则返回false
            result = result &amp;amp;&amp;amp; bitSet.get(hash(key, i));
        }
        return result;
    }

    //hash函数，借鉴了hashmap的扰动算法
    private int hash(Object key, int i) {
        int h;
        return key == null ? 0 : (i * (DEFAULT_SIZE - 1) &amp;amp; ((h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java JUC + NIO</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/prog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/prog/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JUC&lt;/h1&gt;
&lt;h2&gt;进程&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;进程：程序是静止的，进程实体的运行过程就是进程，是系统进行&lt;strong&gt;资源分配的基本单位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进程的特征：并发性、异步性、动态性、独立性、结构性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程&lt;/strong&gt;：线程是属于进程的，是一个基本的 CPU 执行单元，是程序执行流的最小单元。线程是进程中的一个实体，是系统&lt;strong&gt;独立调度的基本单位&lt;/strong&gt;，线程本身不拥有系统资源，只拥有一点在运行中必不可少的资源，与同属一个进程的其他线程共享进程所拥有的全部资源&lt;/p&gt;
&lt;p&gt;关系：一个进程可以包含多个线程，这就是多线程，比如看视频是进程，图画、声音、广告等就是多个线程&lt;/p&gt;
&lt;p&gt;线程的作用：使多道程序更好的并发执行，提高资源利用率和系统吞吐量，增强操作系统的并发性能&lt;/p&gt;
&lt;p&gt;并发并行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并行：在同一时刻，有多个指令在多个 CPU 上同时执行&lt;/li&gt;
&lt;li&gt;并发：在同一时刻，有多个指令在单个 CPU 上交替执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步异步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要等待结果返回，才能继续运行就是同步&lt;/li&gt;
&lt;li&gt;不需要等待结果返回，就能继续运行就是异步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV16J411h7Rd&lt;/p&gt;
&lt;p&gt;笔记的整体结构依据视频编写，并随着学习的深入补充了很多知识&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;p&gt;线程进程对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进程基本上相互独立的，而线程存在于进程内，是进程的一个子集&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程拥有共享的资源，如内存空间等，供其&lt;strong&gt;内部的线程共享&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程间通信较为复杂&lt;/p&gt;
&lt;p&gt;同一台计算机的进程通信称为 IPC（Inter-process communication）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量：信号量是一个计数器，用于多进程对共享数据的访问，解决同步相关的问题并避免竞争条件&lt;/li&gt;
&lt;li&gt;共享存储：多个进程可以访问同一块内存空间，需要使用信号量用来同步对共享存储的访问&lt;/li&gt;
&lt;li&gt;管道通信：管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件，该文件同一时间只允许一个进程访问，所以只支持&lt;strong&gt;半双工通信&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;匿名管道（Pipes）：用于具有亲缘关系的父子进程间或者兄弟进程之间的通信&lt;/li&gt;
&lt;li&gt;命名管道（Names Pipes）：以磁盘文件的方式存在，可以实现本机任意两个进程通信，遵循 FIFO&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消息队列：内核中存储消息的链表，由消息队列标识符标识，能在不同进程之间提供&lt;strong&gt;全双工通信&lt;/strong&gt;，对比管道：
&lt;ul&gt;
&lt;li&gt;匿名管道存在于内存中的文件；命名管道存在于实际的磁盘介质或者文件系统；消息队列存放在内核中，只有在内核重启（操作系统重启）或者显示地删除一个消息队列时，该消息队列才被真正删除&lt;/li&gt;
&lt;li&gt;读进程可以根据消息类型有选择地接收消息，而不像 FIFO 那样只能默认地接收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同计算机之间的&lt;strong&gt;进程通信&lt;/strong&gt;，需要通过网络，并遵守共同的协议，例如 HTTP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;套接字：与其它通信机制不同的是，可用于不同机器间的互相通信&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程通信相对简单，因为线程之间共享进程内的内存，一个例子是多个线程可以访问同一个共享变量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 中的通信机制&lt;/strong&gt;：volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程更轻量，线程上下文切换成本一般上要比进程上下文切换低&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;线程&lt;/h2&gt;
&lt;h3&gt;创建线程&lt;/h3&gt;
&lt;h4&gt;Thread&lt;/h4&gt;
&lt;p&gt;Thread 创建线程方式：创建线程类，匿名内部类方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;start() 方法底层其实是给 CPU 注册当前线程，并且触发 run() 方法执行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程的启动必须调用 start() 方法，如果线程直接调用 run() 方法，相当于变成了普通类的执行，此时主线程将只有执行该线程&lt;/li&gt;
&lt;li&gt;建议线程先创建子线程，主线程的任务放在之后，否则主线程（main）永远是先执行完&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thread 构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Thread()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Thread(String name)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
       	for(int i = 0 ; i &amp;lt; 100 ; i++ ){
            System.out.println(&quot;main线程&quot; + i)
        }
        // main线程输出放在上面 就变成有先后顺序了，因为是 main 线程驱动的子线程运行
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0 ; i &amp;lt; 100 ; i++ ) {
            System.out.println(&quot;子线程输出：&quot;+i)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承 Thread 类的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：编码简单&lt;/li&gt;
&lt;li&gt;缺点：线程类已经继承了 Thread 类无法继承其他类了，功能不能通过继承拓展（单继承的局限性）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Runnable&lt;/h4&gt;
&lt;p&gt;Runnable 创建线程方式：创建线程类，匿名内部类方式&lt;/p&gt;
&lt;p&gt;Thread 的构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Thread(Runnable target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Thread(Runnable target, String name)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Runnable target = new MyRunnable();
        Thread t1 = new Thread(target,&quot;1号线程&quot;);
		t1.start();
        Thread t2 = new Thread(target);//Thread-0
    }
}

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i &amp;lt; 10 ; i++ ){
            System.out.println(Thread.currentThread().getName() + &quot;-&amp;gt;&quot; + i);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Thread 类本身也是实现了 Runnable 接口&lt;/strong&gt;，Thread 类中持有 Runnable 的属性，执行线程 run 方法底层是调用 Runnable#run：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Thread implements Runnable {
    private Runnable target;
    
    public void run() {
        if (target != null) {
          	// 底层调用的是 Runnable 的 run 方法
            target.run();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runnable 方式的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;缺点：代码复杂一点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;线程任务类只是实现了 Runnable 接口，可以继续继承其他类，避免了单继承的局限性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同一个线程任务对象可以被包装成多个线程对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适合多个多个线程去共享同一个资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现解耦操作，线程任务代码可以被多个线程共享，线程任务代码和线程独立&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池可以放入实现 Runnable 或 Callable 线程任务对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Callable&lt;/h4&gt;
&lt;p&gt;实现 Callable 接口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义一个线程任务类实现 Callable 接口，申明线程执行的结果类型&lt;/li&gt;
&lt;li&gt;重写线程任务类的 call 方法，这个方法可以直接返回执行的结果&lt;/li&gt;
&lt;li&gt;创建一个 Callable 的线程任务对象&lt;/li&gt;
&lt;li&gt;把 Callable 的线程任务对象&lt;strong&gt;包装成一个未来任务对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;把未来任务对象包装成线程对象&lt;/li&gt;
&lt;li&gt;调用线程的 start() 方法启动线程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;public FutureTask(Callable&amp;lt;V&amp;gt; callable)&lt;/code&gt;：未来任务对象，在线程执行完后得到线程的执行结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FutureTask 就是 Runnable 对象，因为 &lt;strong&gt;Thread 类只能执行 Runnable 实例的任务对象&lt;/strong&gt;，所以把 Callable 包装成未来任务对象&lt;/li&gt;
&lt;li&gt;线程池部分详解了 FutureTask 的源码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;public V get()&lt;/code&gt;：同步等待 task 执行完毕的结果，如果在线程中获取另一个线程执行结果，会阻塞等待，用于线程同步&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get() 线程会阻塞等待任务执行完成&lt;/li&gt;
&lt;li&gt;run() 执行完后会把结果设置到 FutureTask  的一个成员变量，get() 线程可以获取到该变量的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：同 Runnable，并且能得到线程执行的结果&lt;/li&gt;
&lt;li&gt;缺点：编码复杂&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Callable call = new MyCallable();
        FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(call);
        Thread t = new Thread(task);
        t.start();
        try {
            String s = task.get(); // 获取call方法返回的结果（正常/异常结果）
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }
    }

public class MyCallable implements Callable&amp;lt;String&amp;gt; {
    @Override//重写线程任务类方法
    public String call() throws Exception {
        return Thread.currentThread().getName() + &quot;-&amp;gt;&quot; + &quot;Hello World&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;线程方法&lt;/h3&gt;
&lt;h4&gt;API&lt;/h4&gt;
&lt;p&gt;Thread 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public void start()&lt;/td&gt;
&lt;td&gt;启动一个新线程，Java虚拟机调用此线程的 run 方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void run()&lt;/td&gt;
&lt;td&gt;线程启动后调用该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void setName(String name)&lt;/td&gt;
&lt;td&gt;给当前线程取名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void getName()&lt;/td&gt;
&lt;td&gt;获取当前线程的名字&amp;lt;br /&amp;gt;线程存在默认名称：子线程是 Thread-索引，主线程是 main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static Thread currentThread()&lt;/td&gt;
&lt;td&gt;获取当前线程对象，代码在哪个线程中执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static void sleep(long time)&lt;/td&gt;
&lt;td&gt;让当前线程休眠多少毫秒再继续执行&amp;lt;br /&amp;gt;&lt;strong&gt;Thread.sleep(0)&lt;/strong&gt; : 让操作系统立刻重新进行一次 CPU 竞争&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static native void yield()&lt;/td&gt;
&lt;td&gt;提示线程调度器让出当前线程对 CPU 的使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getPriority()&lt;/td&gt;
&lt;td&gt;返回此线程的优先级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void setPriority(int priority)&lt;/td&gt;
&lt;td&gt;更改此线程的优先级，常用 1 5 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void interrupt()&lt;/td&gt;
&lt;td&gt;中断这个线程，异常处理机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static boolean interrupted()&lt;/td&gt;
&lt;td&gt;判断当前线程是否被打断，清除打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public boolean isInterrupted()&lt;/td&gt;
&lt;td&gt;判断当前线程是否被打断，不清除打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void join()&lt;/td&gt;
&lt;td&gt;等待这个线程结束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void join(long millis)&lt;/td&gt;
&lt;td&gt;等待这个线程死亡 millis 毫秒，0 意味着永远等待&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final native boolean isAlive()&lt;/td&gt;
&lt;td&gt;线程是否存活（还没有运行完毕）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void setDaemon(boolean on)&lt;/td&gt;
&lt;td&gt;将此线程标记为守护线程或用户线程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;run start&lt;/h4&gt;
&lt;p&gt;run：称为线程体，包含了要执行的这个线程的内容，方法运行结束，此线程随即终止。直接调用 run 是在主线程中执行了 run，没有启动新的线程，需要顺序执行&lt;/p&gt;
&lt;p&gt;start：使用 start 是启动新的线程，此线程处于就绪（可运行）状态，通过新的线程间接执行 run 中的代码&lt;/p&gt;
&lt;p&gt;说明：&lt;strong&gt;线程控制资源类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;run() 方法中的异常不能抛出，只能 try/catch&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为父类中没有抛出任何异常，子类不能比父类抛出更多的异常&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常不能跨线程传播回 main() 中&lt;/strong&gt;，因此必须在本地进行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;sleep yield&lt;/h4&gt;
&lt;p&gt;sleep：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 sleep 会让当前线程从 &lt;code&gt;Running&lt;/code&gt; 进入 &lt;code&gt;Timed Waiting&lt;/code&gt; 状态（阻塞）&lt;/li&gt;
&lt;li&gt;sleep() 方法的过程中，&lt;strong&gt;线程不会释放对象锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;其它线程可以使用 interrupt 方法打断正在睡眠的线程，这时 sleep 方法会抛出 InterruptedException&lt;/li&gt;
&lt;li&gt;睡眠结束后的线程未必会立刻得到执行，需要抢占 CPU&lt;/li&gt;
&lt;li&gt;建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;yield：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用&lt;/li&gt;
&lt;li&gt;具体的实现依赖于操作系统的任务调度器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会放弃 CPU 资源，锁资源不会释放&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;join&lt;/h4&gt;
&lt;p&gt;public final void join()：等待这个线程结束&lt;/p&gt;
&lt;p&gt;原理：调用者轮询检查线程 alive 状态，t1.join() 等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final synchronized void join(long millis) throws InterruptedException {
    // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
    while (isAlive()) {
        wait(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;join 方法是被 synchronized 修饰的，本质上是一个对象锁，其内部的 wait 方法调用也是释放锁的，但是&lt;strong&gt;释放的是当前的线程对象锁，而不是外面的锁&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用某个线程（t1）的 join 方法后，该线程（t1）抢占到 CPU 资源，就不再释放，直到线程执行完毕&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程同步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;join 实现线程同步，因为会阻塞等待另一个线程的结束，才能继续向下运行
&lt;ul&gt;
&lt;li&gt;需要外部共享变量，不符合面向对象封装的思想&lt;/li&gt;
&lt;li&gt;必须等待线程结束，不能配合线程池使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Future 实现（同步）：get() 方法阻塞等待执行结果
&lt;ul&gt;
&lt;li&gt;main 线程接收结果&lt;/li&gt;
&lt;li&gt;get 方法是让调用线程同步等待&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
        });
        t1.start();
        t1.join();//不等待线程执行结束，输出的10
        System.out.println(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;interrupt&lt;/h4&gt;
&lt;h5&gt;打断线程&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;public void interrupt()&lt;/code&gt;：打断这个线程，异常处理机制&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public static boolean interrupted()&lt;/code&gt;：判断当前线程是否被打断，打断返回 true，&lt;strong&gt;清除打断标记&lt;/strong&gt;，连续调用两次一定返回 false&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public boolean isInterrupted()&lt;/code&gt;：判断当前线程是否被打断，不清除打断标记&lt;/p&gt;
&lt;p&gt;打断的线程会发生上下文切换，操作系统会保存线程信息，抢占到 CPU 后会从中断的地方接着运行（打断不是停止）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sleep、wait、join 方法都会让线程进入阻塞状态，打断线程&lt;strong&gt;会清空打断状态&lt;/strong&gt;（false）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()-&amp;gt;{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, &quot;t1&quot;);
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
    System.out.println(&quot; 打断状态: {}&quot; + t1.isInterrupted());// 打断状态: {}false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打断正常运行的线程：不会清空打断状态（true）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    Thread t2 = new Thread(()-&amp;gt;{
        while(true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if(interrupted) {
                System.out.println(&quot; 打断状态: {}&quot; + interrupted);//打断状态: {}true
                break;
            }
        }
    }, &quot;t2&quot;);
    t2.start();
    Thread.sleep(500);
    t2.interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;打断 park&lt;/h5&gt;
&lt;p&gt;park 作用类似 sleep，打断 park 线程，不会清空打断状态（true）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -&amp;gt; {
        System.out.println(&quot;park...&quot;);
        LockSupport.park();
        System.out.println(&quot;unpark...&quot;);
        System.out.println(&quot;打断状态：&quot; + Thread.currentThread().isInterrupted());//打断状态：true
    }, &quot;t1&quot;);
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果打断标记已经是 true, 则 park 会失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LockSupport.park();
System.out.println(&quot;unpark...&quot;);
LockSupport.park();//失效，不会阻塞
System.out.println(&quot;unpark...&quot;);//和上一个unpark同时执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以修改获取打断状态方法，使用 &lt;code&gt;Thread.interrupted()&lt;/code&gt;，清除打断标记&lt;/p&gt;
&lt;p&gt;LockSupport 类在 同步 → park-un 详解&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;终止模式&lt;/h5&gt;
&lt;p&gt;终止模式之两阶段终止模式：Two Phase Termination&lt;/p&gt;
&lt;p&gt;目标：在一个线程 T1 中如何优雅终止线程 T2？优雅指的是给 T2 一个后置处理器&lt;/p&gt;
&lt;p&gt;错误思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用线程对象的 stop() 方法停止线程：stop 方法会真正杀死线程，如果这时线程锁住了共享资源，当它被杀死后就再也没有机会释放锁，其它线程将永远无法获取锁&lt;/li&gt;
&lt;li&gt;使用 System.exit(int) 方法停止线程：目的仅是停止一个线程，但这种做法会让整个程序都停止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两阶段终止模式图示：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-两阶段终止模式.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;打断线程可能在任何时间，所以需要考虑在任何时刻被打断的处理方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    private Thread monitor;
    // 启动监控线程
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
                        System.out.println(&quot;后置处理&quot;);
                        break;
                    }
                    try {
                        Thread.sleep(1000);					// 睡眠
                        System.out.println(&quot;执行监控记录&quot;);	// 在此被打断不会异常
                    } catch (InterruptedException e) {		// 在睡眠期间被打断，进入异常处理的逻辑
                        e.printStackTrace();
                        // 重新设置打断标记，打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;daemon&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;public final void setDaemon(boolean on)&lt;/code&gt;：如果是 true ，将此线程标记为守护线程&lt;/p&gt;
&lt;p&gt;线程&lt;strong&gt;启动前&lt;/strong&gt;调用此方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println(&quot;running&quot;);
    }
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户线程：平常创建的普通线程&lt;/p&gt;
&lt;p&gt;守护线程：服务于用户线程，只要其它非守护线程运行结束了，即使守护线程代码没有执行完，也会强制结束。守护进程是&lt;strong&gt;脱离于终端并且在后台运行的进程&lt;/strong&gt;，脱离终端是为了避免在执行的过程中的信息在终端上显示&lt;/p&gt;
&lt;p&gt;说明：当运行的线程都是守护线程，Java 虚拟机将退出，因为普通线程执行完后，JVM 是守护线程，不会继续运行下去&lt;/p&gt;
&lt;p&gt;常见的守护线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垃圾回收器线程就是一种守护线程&lt;/li&gt;
&lt;li&gt;Tomcat 中的 Acceptor 和 Poller 线程都是守护线程，所以 Tomcat 接收到 shutdown 命令后，不会等待它们处理完当前请求&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;不推荐&lt;/h4&gt;
&lt;p&gt;不推荐使用的方法，这些方法已过时，容易破坏同步代码块，造成线程死锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void stop()&lt;/code&gt;：停止线程运行&lt;/p&gt;
&lt;p&gt;废弃原因：方法粗暴，除非可能执行 finally 代码块以及释放 synchronized 外，线程将直接被终止，如果线程持有 JUC 的互斥锁可能导致锁来不及释放，造成其他线程永远等待的局面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void suspend()&lt;/code&gt;：&lt;strong&gt;挂起（暂停）线程运行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;废弃原因：如果目标线程在暂停时对系统资源持有锁，则在目标线程恢复之前没有线程可以访问该资源，如果&lt;strong&gt;恢复目标线程的线程&lt;/strong&gt;在调用 resume 之前会尝试访问此共享资源，则会导致死锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void resume()&lt;/code&gt;：恢复线程运行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;线程原理&lt;/h3&gt;
&lt;h4&gt;运行机制&lt;/h4&gt;
&lt;p&gt;Java Virtual Machine Stacks（Java 虚拟机栈）：每个线程启动后，虚拟机就会为其分配一块栈内存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个栈由多个栈帧（Frame）组成，对应着每次方法调用时所占用的内存&lt;/li&gt;
&lt;li&gt;每个线程只能有一个活动栈帧，对应着当前正在执行的那个方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程上下文切换（Thread Context Switch）：一些原因导致 CPU 不再执行当前线程，转而执行另一个线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程的 CPU 时间片用完&lt;/li&gt;
&lt;li&gt;垃圾回收&lt;/li&gt;
&lt;li&gt;有更高优先级的线程需要运行&lt;/li&gt;
&lt;li&gt;线程自己调用了 sleep、yield、wait、join、park 等方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序计数器（Program Counter Register）：记住下一条 JVM 指令的执行地址，是线程私有的&lt;/p&gt;
&lt;p&gt;当 Context Switch 发生时，需要由操作系统保存当前线程的状态（PCB 中），并恢复另一个线程的状态，包括程序计数器、虚拟机栈中每个栈帧的信息，如局部变量、操作数栈、返回地址等&lt;/p&gt;
&lt;p&gt;JVM 规范并没有限定线程模型，以 HotSopot 为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 的线程是内核级线程（1:1 线程模型），每个 Java 线程都映射到一个操作系统原生线程，需要消耗一定的内核资源（堆栈）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程的调度是在内核态运行的，而线程中的代码是在用户态运行&lt;/strong&gt;，所以线程切换（状态改变）会导致用户与内核态转换进行系统调用，这是非常消耗性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 中 main 方法启动的是一个进程也是一个主线程，main 方法里面的其他线程均为子线程，main 线程是这些线程的父线程&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;线程调度&lt;/h4&gt;
&lt;p&gt;线程调度指系统为线程分配处理器使用权的过程，方式有两种：协同式线程调度、抢占式线程调度（Java 选择）&lt;/p&gt;
&lt;p&gt;协同式线程调度：线程的执行时间由线程本身控制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：线程做完任务才通知系统切换到其他线程，相当于所有线程串行执行，不会出现线程同步问题&lt;/li&gt;
&lt;li&gt;缺点：线程执行时间不可控，如果代码编写出现问题，可能导致程序一直阻塞，引起系统的奔溃&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抢占式线程调度：线程的执行时间由系统分配&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：线程执行时间可控，不会因为一个线程的问题而导致整体系统不可用&lt;/li&gt;
&lt;li&gt;缺点：无法主动为某个线程多分配时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 提供了线程优先级的机制，优先级会提示（hint）调度器优先调度该线程，但这仅仅是一个提示，调度器可以忽略它。在线程的就绪状态时，如果 CPU 比较忙，那么优先级高的线程会获得更多的时间片，但 CPU 闲时，优先级几乎没作用&lt;/p&gt;
&lt;p&gt;说明：并不能通过优先级来判断线程执行的先后顺序&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;未来优化&lt;/h4&gt;
&lt;p&gt;内核级线程调度的成本较大，所以引入了更轻量级的协程。用户线程的调度由用户自己实现（多对一的线程模型，多&lt;strong&gt;个用户线程映射到一个内核级线程&lt;/strong&gt;），被设计为协同式调度，所以叫协程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有栈协程：协程会完整的做调用栈的保护、恢复工作，所以叫有栈协程&lt;/li&gt;
&lt;li&gt;无栈协程：本质上是一种有限状态机，状态保存在闭包里，比有栈协程更轻量，但是功能有限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有栈协程中有一种特例叫纤程，在新并发模型中，一段纤程的代码被分为两部分，执行过程和调度器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行过程：用于维护执行现场，保护、恢复上下文状态&lt;/li&gt;
&lt;li&gt;调度器：负责编排所有要执行的代码顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;线程状态&lt;/h3&gt;
&lt;p&gt;进程的状态参考操作系统：创建态、就绪态、运行态、阻塞态、终止态&lt;/p&gt;
&lt;p&gt;线程由生到死的完整过程（生命周期）：当线程被创建并启动以后，既不是一启动就进入了执行状态，也不是一直处于执行状态，在 API 中 &lt;code&gt;java.lang.Thread.State&lt;/code&gt; 这个枚举中给出了六种线程状态：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;线程状态&lt;/th&gt;
&lt;th&gt;导致状态发生条件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NEW（新建）&lt;/td&gt;
&lt;td&gt;线程刚被创建，但是并未启动，还没调用 start 方法，只有线程对象，没有线程特征&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runnable（可运行）&lt;/td&gt;
&lt;td&gt;线程可以在 Java 虚拟机中运行的状态，可能正在运行自己代码，也可能没有，这取决于操作系统处理器，调用了 t.start() 方法：就绪（经典叫法）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked（阻塞）&lt;/td&gt;
&lt;td&gt;当一个线程试图获取一个对象锁，而该对象锁被其他的线程持有，则该线程进入 Blocked 状态；当该线程持有锁时，该线程将变成 Runnable 状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting（无限等待）&lt;/td&gt;
&lt;td&gt;一个线程在等待另一个线程执行一个（唤醒）动作时，该线程进入 Waiting 状态，进入这个状态后不能自动唤醒，必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timed Waiting （限期等待）&lt;/td&gt;
&lt;td&gt;有几个方法有超时参数，调用将进入 Timed Waiting 状态，这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teminated（结束）&lt;/td&gt;
&lt;td&gt;run 方法正常退出而死亡，或者因为没有捕获的异常终止了 run 方法而死亡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B6%E7%A7%8D%E7%8A%B6%E6%80%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NEW → RUNNABLE：当调用 t.start() 方法时，由 NEW → RUNNABLE&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; WAITING：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 obj.wait() 方法时&lt;/p&gt;
&lt;p&gt;调用 obj.notify()、obj.notifyAll()、t.interrupt()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;竞争锁成功，t 线程从 WAITING → RUNNABLE&lt;/li&gt;
&lt;li&gt;竞争锁失败，t 线程从 WAITING → BLOCKED&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前线程调用 t.join() 方法，注意是当前线程在 t 线程对象的监视器上等待&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前线程调用 LockSupport.park() 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING：调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; BLOCKED：t 线程用 synchronized(obj) 获取了对象锁时竞争失败&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;查看线程&lt;/h3&gt;
&lt;p&gt;Windows：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务管理器可以查看进程和线程数，也可以用来杀死进程&lt;/li&gt;
&lt;li&gt;tasklist 查看进程&lt;/li&gt;
&lt;li&gt;taskkill 杀死进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ps -ef 查看所有进程&lt;/li&gt;
&lt;li&gt;ps -fT -p &amp;lt;PID&amp;gt; 查看某个进程（PID）的所有线程&lt;/li&gt;
&lt;li&gt;kill 杀死进程&lt;/li&gt;
&lt;li&gt;top 按大写 H 切换是否显示线程&lt;/li&gt;
&lt;li&gt;top -H -p &amp;lt;PID&amp;gt; 查看某个进程（PID）的所有线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jps 命令查看所有 Java 进程&lt;/li&gt;
&lt;li&gt;jstack &amp;lt;PID&amp;gt; 查看某个 Java 进程（PID）的所有线程状态&lt;/li&gt;
&lt;li&gt;jconsole 来查看某个 Java 进程中线程的运行情况（图形界面）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;同步&lt;/h2&gt;
&lt;h3&gt;临界区&lt;/h3&gt;
&lt;p&gt;临界资源：一次仅允许一个进程使用的资源成为临界资源&lt;/p&gt;
&lt;p&gt;临界区：访问临界资源的代码块&lt;/p&gt;
&lt;p&gt;竞态条件：多个线程在临界区内执行，由于代码的执行序列不同而导致结果无法预测，称之为发生了竞态条件&lt;/p&gt;
&lt;p&gt;一个程序运行多个线程是没有问题，多个线程读共享资源也没有问题，在多个线程对共享资源读写操作时发生指令交错，就会出现问题&lt;/p&gt;
&lt;p&gt;为了避免临界区的竞态条件发生（解决线程安全问题）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞式的解决方案：synchronized，lock&lt;/li&gt;
&lt;li&gt;非阻塞式的解决方案：原子变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;管程（monitor）：由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块，保证同一时刻只有一个进程在管程内活动，即管程内定义的操作在同一时刻只被一个进程调用（由编译器实现）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;synchronized：对象锁，保证了临界区内代码的原子性&lt;/strong&gt;，采用互斥的方式让同一时刻至多只有一个线程能持有对象锁，其它线程获取这个对象锁时会阻塞，保证拥有锁的线程可以安全的执行临界区内的代码，不用担心线程上下文切换&lt;/p&gt;
&lt;p&gt;互斥和同步都可以采用 synchronized 关键字来完成，区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥是保证临界区的竞态条件发生，同一时刻只能有一个线程执行临界区代码&lt;/li&gt;
&lt;li&gt;同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程安全，性能差&lt;/li&gt;
&lt;li&gt;线程不安全性能好，假如开发中不会存在多线程安全问题，建议使用线程不安全的设计类&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;syn-ed&lt;/h3&gt;
&lt;h4&gt;使用锁&lt;/h4&gt;
&lt;h5&gt;同步块&lt;/h5&gt;
&lt;p&gt;锁对象：理论上可以是&lt;strong&gt;任意的唯一对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized 是可重入、不公平的重量级锁&lt;/p&gt;
&lt;p&gt;原则上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁对象建议使用共享资源&lt;/li&gt;
&lt;li&gt;在实例方法中使用 this 作为锁对象，锁住的 this 正好是共享资源&lt;/li&gt;
&lt;li&gt;在静态方法中使用类名 .class 字节码作为锁对象，因为静态成员属于类，被所有实例对象共享，所以需要锁住类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步代码块格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized(锁对象){
	// 访问共享资源的核心代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class demo {
    static int counter = 0;
    //static修饰，则元素是属于类本身的，不属于对象  ，与类一起加载一次，只有一个
    static final Object room = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (room) {
                    counter++;
                }
            }
        }, &quot;t1&quot;);
        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (room) {
                    counter--;
                }
            }
        }, &quot;t2&quot;);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;同步方法&lt;/h5&gt;
&lt;p&gt;把出现线程安全问题的核心方法锁起来，每次只能一个线程进入访问&lt;/p&gt;
&lt;p&gt;synchronized 修饰的方法的不具备继承性，所以子类是线程不安全的，如果子类的方法也被 synchronized 修饰，两个锁对象其实是一把锁，而且是&lt;strong&gt;子类对象作为锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用法：直接给方法加上一个修饰符 synchronized&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体；
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) { 
	方法体；
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步方法底层也是有锁对象的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果方法是实例方法：同步方法默认用 this 作为的锁对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public synchronized void test() {} //等价于
public void test() {
    synchronized(this) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果方法是静态方法：同步方法默认用类名 .class 作为的锁对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test{
	public synchronized static void test() {}
}
//等价于
class Test{
    public void test() {
        synchronized(Test.class) {}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;线程八锁&lt;/h5&gt;
&lt;p&gt;线程八锁就是考察 synchronized 锁住的是哪个对象，直接百度搜索相关的实例&lt;/p&gt;
&lt;p&gt;说明：主要关注锁住的对象是不是同一个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁住类对象，所有类的实例的方法都是安全的，类的所有实例都相当于同一把锁&lt;/li&gt;
&lt;li&gt;锁住 this 对象，只有在当前实例对象的线程内是安全的，如果有多个实例就不安全&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程不安全：因为锁住的不是同一个对象，线程 1 调用 a 方法锁住的类对象，线程 2 调用 b 方法锁住的 n2 对象，不是同一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Number{
    public static synchronized void a(){
		Thread.sleep(1000);
        System.out.println(&quot;1&quot;);
    }
    public synchronized void b() {
        System.out.println(&quot;2&quot;);
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()-&amp;gt;{ n1.a(); }).start();
    new Thread(()-&amp;gt;{ n2.b(); }).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程安全：因为 n1 调用 a() 方法，锁住的是类对象，n2 调用 b() 方法，锁住的也是类对象，所以线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Number{
    public static synchronized void a(){
		Thread.sleep(1000);
        System.out.println(&quot;1&quot;);
    }
    public static synchronized void b() {
        System.out.println(&quot;2&quot;);
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()-&amp;gt;{ n1.a(); }).start();
    new Thread(()-&amp;gt;{ n2.b(); }).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;锁原理&lt;/h4&gt;
&lt;h5&gt;Monitor&lt;/h5&gt;
&lt;p&gt;Monitor 被翻译为监视器或管程&lt;/p&gt;
&lt;p&gt;每个 Java 对象都可以关联一个 Monitor 对象，Monitor 也是 class，其&lt;strong&gt;实例存储在堆中&lt;/strong&gt;，如果使用 synchronized 给对象上锁（重量级）之后，该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针，这就是重量级锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mark Word 结构：最后两位是&lt;strong&gt;锁标志位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord%E7%BB%93%E6%9E%8432%E4%BD%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;64 位虚拟机 Mark Word：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord%E7%BB%93%E6%9E%8464%E4%BD%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开始时 Monitor 中 Owner 为 null&lt;/li&gt;
&lt;li&gt;当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2，Monitor 中只能有一个 Owner，&lt;strong&gt;obj 对象的 Mark Word 指向 Monitor&lt;/strong&gt;，把&lt;strong&gt;对象原有的 MarkWord 存入线程栈中的锁记录&lt;/strong&gt;中（轻量级锁部分详解）
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理1.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/li&gt;
&lt;li&gt;在 Thread-2 上锁的过程，Thread-3、Thread-4、Thread-5 也执行 synchronized(obj)，就会进入 EntryList BLOCKED（双向链表）&lt;/li&gt;
&lt;li&gt;Thread-2 执行完同步代码块的内容，根据 obj 对象头中 Monitor 地址寻找，设置 Owner 为空，把线程栈的锁记录中的对象头的值设置回 MarkWord&lt;/li&gt;
&lt;li&gt;唤醒 EntryList 中等待的线程来竞争锁，竞争是&lt;strong&gt;非公平的&lt;/strong&gt;，如果这时有新的线程想要获取锁，可能直接就抢占到了，阻塞队列的线程就会继续阻塞&lt;/li&gt;
&lt;li&gt;WaitSet 中的 Thread-0，是以前获得过锁，但条件不满足进入 WAITING 状态的线程（wait-notify 机制）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 必须是进入同一个对象的 Monitor 才有上述的效果&lt;/li&gt;
&lt;li&gt;不加 synchronized 的对象不会关联监视器，不遵从以上规则&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;字节码&lt;/h5&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println(&quot;ok&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0: 	new				#2		// new Object
3: 	dup
4: 	invokespecial 	#1 		// invokespecial &amp;lt;init&amp;gt;:()V，非虚方法
7: 	astore_1 				// lock引用 -&amp;gt; lock
8: 	aload_1					// lock （synchronized开始）
9: 	dup						// 一份用来初始化，一份用来引用
10: astore_2 				// lock引用 -&amp;gt; slot 2
11: monitorenter 			// 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic 		#3		// System.out
15: ldc 			#4		// &quot;ok&quot;
17: invokevirtual 	#5 		// invokevirtual println:(Ljava/lang/String;)V
20: aload_2 				// slot 2(lock引用)
21: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 				// any -&amp;gt; slot 3
26: aload_2 				// slot 2(lock引用)
27: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
    from to target type
      12 22 25 		any
      25 28 25 		any
LineNumberTable: ...
LocalVariableTable:
    Start Length Slot Name Signature
    	0 	31 		0 args [Ljava/lang/String;
    	8 	23 		1 lock Ljava/lang/Object;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过异常 &lt;strong&gt;try-catch 机制&lt;/strong&gt;，确保一定会被解锁&lt;/li&gt;
&lt;li&gt;方法级别的 synchronized 不会在字节码指令中有所体现&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁升级&lt;/h4&gt;
&lt;h5&gt;升级过程&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;synchronized 是可重入、不公平的重量级锁&lt;/strong&gt;，所以可以对其进行优化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无锁 -&amp;gt; 偏向锁 -&amp;gt; 轻量级锁 -&amp;gt; 重量级锁	// 随着竞争的增加，只能锁升级，不能降级
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%94%81%E5%8D%87%E7%BA%A7%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;偏向锁&lt;/h5&gt;
&lt;p&gt;偏向锁的思想是偏向于让第一个获取锁对象的线程，这个线程之后重新获取该锁不再需要同步操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当锁对象第一次被线程获得的时候进入偏向状态，标记为 101，同时&lt;strong&gt;使用 CAS 操作将线程 ID 记录到 Mark Word&lt;/strong&gt;。如果 CAS 操作成功，这个线程以后进入这个锁相关的同步块，查看这个线程 ID 是自己的就表示没有竞争，就不需要再进行任何同步操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当有另外一个线程去尝试获取这个锁对象时，偏向状态就宣告结束，此时撤销偏向（Revoke Bias）后恢复到未锁定或轻量级锁状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构64位.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;一个对象创建时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果开启了偏向锁（默认开启），那么对象创建后，MarkWord 值为 0x05 即最后 3 位为 101，thread、epoch、age 都为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;偏向锁是默认是延迟的，不会在程序启动时立即生效，如果想避免延迟，可以加 VM 参数 &lt;code&gt;-XX:BiasedLockingStartupDelay=0&lt;/code&gt; 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因：在刚开始执行代码时，会有好多线程来抢锁，如果开偏向锁效率反而降低&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个对象已经计算过 hashCode，就再也无法进入偏向状态了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 VM 参数 &lt;code&gt;-XX:-UseBiasedLocking&lt;/code&gt; 禁用偏向锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;撤销偏向锁的状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用对象的 hashCode：偏向锁的对象 MarkWord 中存储的是线程 id，调用 hashCode 导致偏向锁被撤销&lt;/li&gt;
&lt;li&gt;当有其它线程使用偏向锁对象时，会将偏向锁升级为轻量级锁&lt;/li&gt;
&lt;li&gt;调用 wait/notify，需要申请 Monitor，进入 WaitSet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;批量撤销&lt;/strong&gt;：如果对象被多个线程访问，但没有竞争，这时偏向了线程 T1 的对象仍有机会重新偏向 T2，重偏向会重置对象的 Thread ID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;批量重偏向：当撤销偏向锁阈值超过 20 次后，JVM 会觉得是不是偏向错了，于是在给这些对象加锁时重新偏向至加锁线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;批量撤销：当撤销偏向锁阈值超过 40 次后，JVM 会觉得自己确实偏向错了，根本就不该偏向，于是整个类的所有对象都会变为不可偏向的，新建的对象也是不可偏向的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;轻量级锁&lt;/h5&gt;
&lt;p&gt;一个对象有多个线程要加锁，但加锁的时间是错开的（没有竞争），可以使用轻量级锁来优化，轻量级锁对使用者是透明的（不可见）&lt;/p&gt;
&lt;p&gt;可重入锁：线程可以进入任何一个它已经拥有的锁所同步着的代码块，可重入锁最大的作用是&lt;strong&gt;避免死锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;轻量级锁在没有竞争时（锁重入时），每次重入仍然需要执行 CAS 操作，Java 6 才引入的偏向锁来优化&lt;/p&gt;
&lt;p&gt;锁重入实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
    	// 同步块 B
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建锁记录（Lock Record）对象，每个线程的&lt;strong&gt;栈帧&lt;/strong&gt;都会包含一个锁记录的结构，存储锁定对象的 Mark Word&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让锁记录中 Object reference 指向锁住的对象，并尝试用 CAS 替换 Object 的 Mark Word，将 Mark Word 的值存入锁记录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 替换成功，对象头中存储了锁记录地址和状态 00（轻量级锁） ，表示由该线程给对象加锁
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 失败，有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是其它线程已经持有了该 Object 的轻量级锁，这时表明有竞争，进入锁膨胀过程&lt;/li&gt;
&lt;li&gt;如果是线程自己执行了 synchronized 锁重入，就添加一条 Lock Record 作为重入的计数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%863.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当退出 synchronized 代码块（解锁时）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有取值为 null 的锁记录，表示有重入，这时重置锁记录，表示重入计数减 1&lt;/li&gt;
&lt;li&gt;如果锁记录的值不为 null，这时使用 CAS &lt;strong&gt;将 Mark Word 的值恢复给对象头&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;成功，则解锁成功&lt;/li&gt;
&lt;li&gt;失败，说明轻量级锁进行了锁膨胀或已经升级为重量级锁，进入重量级锁解锁流程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;锁膨胀&lt;/h5&gt;
&lt;p&gt;在尝试加轻量级锁的过程中，CAS 操作无法成功，可能是其它线程为此对象加上了轻量级锁（有竞争），这时需要进行锁膨胀，将轻量级锁变为&lt;strong&gt;重量级锁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当 Thread-1 进行轻量级加锁时，Thread-0 已经对该对象加了轻量级锁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Thread-1 加轻量级锁失败，进入锁膨胀流程：为 Object 对象申请 Monitor 锁，&lt;strong&gt;通过 Object 对象头获取到持锁线程&lt;/strong&gt;，将 Monitor 的 Owner 置为 Thread-0，将 Object 的对象头指向重量级锁地址，然后自己进入 Monitor 的 EntryList BLOCKED&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 Thread-0 退出同步块解锁时，使用 CAS 将 Mark Word 的值恢复给对象头失败，这时进入重量级解锁流程，即按照 Monitor 地址找到 Monitor 对象，设置 Owner 为 null，唤醒 EntryList 中 BLOCKED 线程&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁优化&lt;/h4&gt;
&lt;h5&gt;自旋锁&lt;/h5&gt;
&lt;p&gt;重量级锁竞争时，尝试获取锁的线程不会立即阻塞，可以使用&lt;strong&gt;自旋&lt;/strong&gt;（默认 10 次）来进行优化，采用循环的方式去尝试获取锁&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自旋占用 CPU 时间，单核 CPU 自旋就是浪费时间，因为同一时刻只能运行一个线程，多核 CPU 自旋才能发挥优势&lt;/li&gt;
&lt;li&gt;自旋失败的线程会进入阻塞状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：不会进入阻塞状态，&lt;strong&gt;减少线程上下文切换的消耗&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缺点：当自旋的线程越来越多时，会不断的消耗 CPU 资源&lt;/p&gt;
&lt;p&gt;自旋锁情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自旋成功的情况：
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-自旋成功.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自旋失败的情况：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-自旋失败.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自旋锁说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Java 6 之后自旋锁是自适应的，比如对象刚刚的一次自旋操作成功过，那么认为这次自旋成功的可能性会高，就多自旋几次；反之，就少自旋甚至不自旋，比较智能&lt;/li&gt;
&lt;li&gt;Java 7 之后不能控制是否开启自旋功能，由 JVM 控制&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//手写自旋锁
public class SpinLock {
    // 泛型装的是Thread，原子引用线程
    AtomicReference&amp;lt;Thread&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + &quot; come in&quot;);

        //开始自旋，期望值为null，更新值是当前线程
        while (!atomicReference.compareAndSet(null, thread)) {
            Thread.sleep(1000);
            System.out.println(thread.getName() + &quot; 正在自旋&quot;);
        }
        System.out.println(thread.getName() + &quot; 自旋成功&quot;);
    }

    public void unlock() {
        Thread thread = Thread.currentThread();

        //线程使用完锁把引用变为null
		atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + &quot; invoke unlock&quot;);
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        new Thread(() -&amp;gt; {
            //占有锁
            lock.lock();
            Thread.sleep(10000); 

            //释放锁
            lock.unlock();
        },&quot;t1&quot;).start();

        // 让main线程暂停1秒，使得t1线程，先执行
        Thread.sleep(1000);

        new Thread(() -&amp;gt; {
            lock.lock();
            lock.unlock();
        },&quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;锁消除&lt;/h5&gt;
&lt;p&gt;锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除，这是 JVM &lt;strong&gt;即时编译器的优化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;锁消除主要是通过&lt;strong&gt;逃逸分析&lt;/strong&gt;来支持，如果堆上的共享数据不可能逃逸出去被其它线程访问到，那么就可以把它们当成私有数据对待，也就可以将它们的锁进行消除（同步消除：JVM 逃逸分析）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;锁粗化&lt;/h5&gt;
&lt;p&gt;对相同对象多次加锁，导致线程发生多次重入，频繁的加锁操作就会导致性能损耗，可以使用锁粗化方式优化&lt;/p&gt;
&lt;p&gt;如果虚拟机探测到一串的操作都对同一个对象加锁，将会把加锁的范围扩展（粗化）到整个操作序列的外部&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一些看起来没有加锁的代码，其实隐式的加了很多锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;String 是一个不可变的类，编译器会对 String 的拼接自动优化。在 JDK 1.5 之前，转化为 StringBuffer 对象的连续 append() 操作，每个 append() 方法中都有一个同步块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩展到第一个 append() 操作之前直至最后一个 append() 操作之后，只需要加锁一次就可以&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;多把锁&lt;/h4&gt;
&lt;p&gt;多把不相干的锁：一间大屋子有两个功能睡觉、学习，互不相干。现在一人要学习，一人要睡觉，如果只用一间屋子（一个对象锁）的话，那么并发度很低&lt;/p&gt;
&lt;p&gt;将锁的粒度细分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处，是可以增强并发度&lt;/li&gt;
&lt;li&gt;坏处，如果一个线程需要同时获得多把锁，就容易发生死锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：准备多个对象锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    BigRoom bigRoom = new BigRoom();
    new Thread(() -&amp;gt; { bigRoom.study(); }).start();
    new Thread(() -&amp;gt; { bigRoom.sleep(); }).start();
}
class BigRoom {
    private final Object studyRoom = new Object();
    private final Object sleepRoom = new Object();

    public void sleep() throws InterruptedException {
        synchronized (sleepRoom) {
            System.out.println(&quot;sleeping 2 小时&quot;);
            Thread.sleep(2000);
        }
    }

    public void study() throws InterruptedException {
        synchronized (studyRoom) {
            System.out.println(&quot;study 1 小时&quot;);
            Thread.sleep(1000);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;活跃性&lt;/h4&gt;
&lt;h5&gt;死锁&lt;/h5&gt;
&lt;h6&gt;形成&lt;/h6&gt;
&lt;p&gt;死锁：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放，由于线程被无限期地阻塞，因此程序不可能正常终止&lt;/p&gt;
&lt;p&gt;Java 死锁产生的四个必要条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;互斥条件，即当资源被一个线程使用（占有）时，别的线程不能使用&lt;/li&gt;
&lt;li&gt;不可剥夺条件，资源请求者不能强制从资源占有者手中夺取资源，资源只能由资源占有者主动释放&lt;/li&gt;
&lt;li&gt;请求和保持条件，即当资源请求者在请求其他的资源的同时保持对原有资源的占有&lt;/li&gt;
&lt;li&gt;循环等待条件，即存在一个等待循环队列：p1 要 p2 的资源，p2 要 p1 的资源，形成了一个等待环路&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;四个条件都成立的时候，便形成死锁。死锁情况下打破上述任何一个条件，便可让死锁消失&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Dead {
    public static Object resources1 = new Object();
    public static Object resources2 = new Object();
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            // 线程1：占用资源1 ，请求资源2
            synchronized(resources1){
                System.out.println(&quot;线程1已经占用了资源1，开始请求资源2&quot;);
                Thread.sleep(2000);//休息两秒，防止线程1直接运行完成。
                //2秒内线程2肯定可以锁住资源2
                synchronized (resources2){
                    System.out.println(&quot;线程1已经占用了资源2&quot;);
                }
        }).start();
        new Thread(() -&amp;gt; {
            // 线程2：占用资源2 ，请求资源1
            synchronized(resources2){
                System.out.println(&quot;线程2已经占用了资源2，开始请求资源1&quot;);
                Thread.sleep(2000);
                synchronized (resources1){
                    System.out.println(&quot;线程2已经占用了资源1&quot;);
                }
            }}
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h6&gt;定位&lt;/h6&gt;
&lt;p&gt;定位死锁的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 jps 定位进程 id，再用 &lt;code&gt;jstack id&lt;/code&gt; 定位死锁，找到死锁的线程去查看源码，解决优化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Thread-1&quot; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000]
	java.lang.Thread.State: BLOCKED (on object monitor)
#省略    
&quot;Thread-1&quot; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000]
	java.lang.Thread.State: BLOCKED (on object monitor)
#省略

Found one Java-level deadlock:
===================================================
&quot;Thread-1&quot;:
    waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
    which is held by &quot;Thread-0&quot;
&quot;Thread-0&quot;:
    waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
    which is held by &quot;Thread-1&quot;
    
Java stack information for the threads listed above:
===================================================
&quot;Thread-1&quot;:
    at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
    - waiting to lock &amp;lt;0x000000076b5bf1c0&amp;gt; (a java.lang.Object)
    - locked &amp;lt;0x000000076b5bf1d0&amp;gt; (a java.lang.Object)
    at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)
&quot;Thread-0&quot;:
    at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
    - waiting to lock &amp;lt;0x000000076b5bf1d0&amp;gt; (a java.lang.Object)
    - locked &amp;lt;0x000000076b5bf1c0&amp;gt; (a java.lang.Object)
    at thread.TestDeadLock$$Lambda$1/495053715
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程，再利用 &lt;code&gt;top -Hp 进程id&lt;/code&gt; 来定位是哪个线程，最后再用 jstack &amp;lt;pid&amp;gt;的输出来看各个线程栈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免死锁：避免死锁要注意加锁顺序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用 jconsole 工具，在 &lt;code&gt;jdk\bin&lt;/code&gt; 目录下&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;活锁&lt;/h5&gt;
&lt;p&gt;活锁：指的是任务或者执行者没有被阻塞，由于某些条件没有满足，导致一直重复尝试—失败—尝试—失败的过程&lt;/p&gt;
&lt;p&gt;两个线程互相改变对方的结束条件，最后谁也无法结束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            // 期望减到 0 退出循环
            while (count &amp;gt; 0) {
                Thread.sleep(200);
                count--;
                System.out.println(&quot;线程一count:&quot; + count);
            }
        }, &quot;t1&quot;).start();
        new Thread(() -&amp;gt; {
            // 期望超过 20 退出循环
            while (count &amp;lt; 20) {
                Thread.sleep(200);
                count++;
                System.out.println(&quot;线程二count:&quot;+ count);
            }
        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;饥饿&lt;/h5&gt;
&lt;p&gt;饥饿：一个线程由于优先级太低，始终得不到 CPU 调度执行，也不能够结束&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;wait-ify&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;需要获取对象锁后才可以调用 &lt;code&gt;锁对象.wait()&lt;/code&gt;，notify 随机唤醒一个线程，notifyAll 唤醒所有线程去竞争 CPU&lt;/p&gt;
&lt;p&gt;Object 类 API：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待，直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待，或是被唤醒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;strong&gt;wait 是挂起线程，需要唤醒的都是挂起操作&lt;/strong&gt;，阻塞线程可以自己去争抢锁，挂起的线程需要唤醒后去争抢锁&lt;/p&gt;
&lt;p&gt;对比 sleep()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理不同：sleep() 方法是属于 Thread 类，是线程用来控制自身流程的，使此线程暂停执行一段时间而把执行机会让给其他线程；wait() 方法属于 Object 类，用于线程间通信&lt;/li&gt;
&lt;li&gt;对&lt;strong&gt;锁的处理机制&lt;/strong&gt;不同：调用 sleep() 方法的过程中，线程不会释放对象锁，当调用 wait() 方法的时候，线程会放弃对象锁，进入等待此对象的等待锁定池（不释放锁其他线程怎么抢占到锁执行唤醒操作），但是都会释放 CPU&lt;/li&gt;
&lt;li&gt;使用区域不同：wait() 方法必须放在**同步控制方法和同步代码块（先获取锁）**中使用，sleep() 方法则可以放在任何地方使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;底层原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owner 线程发现条件不满足，调用 wait 方法，即可进入 WaitSet 变为 WAITING 状态&lt;/li&gt;
&lt;li&gt;BLOCKED 和 WAITING 的线程都处于阻塞状态，不占用 CPU 时间片&lt;/li&gt;
&lt;li&gt;BLOCKED 线程会在 Owner 线程释放锁时唤醒&lt;/li&gt;
&lt;li&gt;WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒，唤醒后并不意味者立刻获得锁，&lt;strong&gt;需要进入 EntryList 重新竞争&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;代码优化&lt;/h4&gt;
&lt;p&gt;虚假唤醒：notify 只能随机唤醒一个 WaitSet 中的线程，这时如果有其它线程也在等待，那么就可能唤醒不了正确的线程&lt;/p&gt;
&lt;p&gt;解决方法：采用 notifyAll&lt;/p&gt;
&lt;p&gt;notifyAll 仅解决某个线程的唤醒问题，使用 if + wait 判断仅有一次机会，一旦条件不成立，无法重新判断&lt;/p&gt;
&lt;p&gt;解决方法：用 while + wait，当条件不成立，再次 wait&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.demo&quot;)
public class demo {
    static final Object room = new Object();
    static boolean hasCigarette = false;    //有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -&amp;gt; {
            synchronized (room) {
                log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
                while (!hasCigarette) {//while防止虚假唤醒
                    log.debug(&quot;没烟，先歇会！&quot;);
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
                if (hasCigarette) {
                    log.debug(&quot;可以开始干活了&quot;);
                } else {
                    log.debug(&quot;没干成活...&quot;);
                }
            }
        }, &quot;小南&quot;).start();

        new Thread(() -&amp;gt; {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
                if (!hasTakeout) {
                    log.debug(&quot;没外卖，先歇会！&quot;);
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
                if (hasTakeout) {
                    log.debug(&quot;可以开始干活了&quot;);
                } else {
                    log.debug(&quot;没干成活...&quot;);
                }
            }
        }, &quot;小女&quot;).start();


        Thread.sleep(1000);
        new Thread(() -&amp;gt; {
        // 这里能不能加 synchronized (room)？
            synchronized (room) {
                hasTakeout = true;
				//log.debug(&quot;烟到了噢！&quot;);
                log.debug(&quot;外卖到了噢！&quot;);
                room.notifyAll();
            }
        }, &quot;送外卖的&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;park-un&lt;/h3&gt;
&lt;p&gt;LockSupport 是用来创建锁和其他同步类的&lt;strong&gt;线程原语&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LockSupport 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LockSupport.park()&lt;/code&gt;：暂停当前线程，挂起原语&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LockSupport.unpark(暂停的线程对象)&lt;/code&gt;：恢复某个线程的运行&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Thread t1 = new Thread(() -&amp;gt; {
        System.out.println(&quot;start...&quot;);	//1
		Thread.sleep(1000);// Thread.sleep(3000)
        // 先 park 再 unpark 和先 unpark 再 park 效果一样，都会直接恢复线程的运行
        System.out.println(&quot;park...&quot;);	//2
        LockSupport.park();
        System.out.println(&quot;resume...&quot;);//4
    },&quot;t1&quot;);
    t1.start();
   	Thread.sleep(2000);
    System.out.println(&quot;unpark...&quot;);	//3
    LockSupport.unpark(t1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LockSupport 出现就是为了增强 wait &amp;amp; notify 的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wait，notify 和 notifyAll 必须配合 Object Monitor 一起使用，而 park、unpark 不需要&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark &lt;strong&gt;以线程为单位&lt;/strong&gt;来阻塞和唤醒线程，而 notify 只能随机唤醒一个等待线程，notifyAll 是唤醒所有等待线程&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark 可以先 unpark，而 wait &amp;amp; notify 不能先 notify。类比生产消费，先消费发现有产品就消费，没有就等待；先生产就直接产生商品，然后线程直接消费&lt;/li&gt;
&lt;li&gt;wait 会释放锁资源进入等待队列，&lt;strong&gt;park 不会释放锁资源&lt;/strong&gt;，只负责阻塞当前线程，会释放 CPU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：类似生产者消费者&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先 park：
&lt;ol&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 0，这时获得 _mutex 互斥锁&lt;/li&gt;
&lt;li&gt;线程进入 _cond 条件变量挂起&lt;/li&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1&lt;/li&gt;
&lt;li&gt;唤醒 _cond 条件变量中的 Thread_0，Thread_0 恢复运行，设置 _counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;先 unpark：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1&lt;/li&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 1，这时线程无需挂起，继续运行，设置 _counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;安全分析&lt;/h3&gt;
&lt;p&gt;成员变量和静态变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果它们没有共享，则线程安全&lt;/li&gt;
&lt;li&gt;如果它们被共享了，根据它们的状态是否能够改变，分两种情况：
&lt;ul&gt;
&lt;li&gt;如果只有读操作，则线程安全&lt;/li&gt;
&lt;li&gt;如果有读写操作，则这段代码是临界区，需要考虑线程安全问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量是线程安全的&lt;/li&gt;
&lt;li&gt;局部变量引用的对象不一定线程安全（逃逸分析）：
&lt;ul&gt;
&lt;li&gt;如果该对象没有逃离方法的作用访问，它是线程安全的（每一个方法有一个栈帧）&lt;/li&gt;
&lt;li&gt;如果该对象逃离方法的作用范围，需要考虑线程安全问题（暴露引用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见线程安全类：String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程安全的是指，多个线程调用它们同一个实例的某个方法时，是线程安全的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;每个方法是原子的，但多个方法的组合不是原子的&lt;/strong&gt;，只能保证调用的方法内部安全：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hashtable table = new Hashtable();
// 线程1，线程2
if(table.get(&quot;key&quot;) == null) {
	table.put(&quot;key&quot;, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无状态类线程安全，就是没有成员变量的类&lt;/p&gt;
&lt;p&gt;不可变类线程安全：String、Integer 等都是不可变类，&lt;strong&gt;内部的状态不可以改变&lt;/strong&gt;，所以方法是线程安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;replace 等方法底层是新建一个对象，复制过去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();	// 线程不安全
String S1 = &quot;...&quot;;							// 线程安全
final String S2 = &quot;...&quot;;					// 线程安全
Date D1 = new Date();						// 线程不安全
final Date D2 = new Date();					// 线程不安全，final让D2引用的对象不能变，但对象的内容可以变
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抽象方法如果有参数，被重写后行为不确定可能造成线程不安全，被称之为外星方法：&lt;code&gt;public abstract foo(Student s);&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;同步模式&lt;/h3&gt;
&lt;h4&gt;保护性暂停&lt;/h4&gt;
&lt;h5&gt;单任务版&lt;/h5&gt;
&lt;p&gt;Guarded Suspension，用在一个线程等待另一个线程的执行结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一个结果需要从一个线程传递到另一个线程，让它们关联同一个 GuardedObject&lt;/li&gt;
&lt;li&gt;如果有结果不断从一个线程到另一个线程那么可以使用消息队列（见生产者/消费者）&lt;/li&gt;
&lt;li&gt;JDK 中，join 的实现、Future 的实现，采用的就是此模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BF%9D%E6%8A%A4%E6%80%A7%E6%9A%82%E5%81%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    GuardedObject object = new GuardedObjectV2();
    new Thread(() -&amp;gt; {
        sleep(1);
        object.complete(Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;));
    }).start();
    
    Object response = object.get(2500);
    if (response != null) {
        log.debug(&quot;get response: [{}] lines&quot;, ((List&amp;lt;String&amp;gt;) response).size());
    } else {
        log.debug(&quot;can&apos;t get response&quot;);
    }
}

class GuardedObject {
    private Object response;
    private final Object lock = new Object();

    //获取结果
    //timeout :最大等待时间
    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4) 假设 millis 是 1000，结果在 400 时唤醒了，那么还有 600 要等
                long waitTime = millis - timePassed;
                log.debug(&quot;waitTime: {}&quot;, waitTime);
                //经历时间超过最大等待时间退出循环
                if (waitTime &amp;lt;= 0) {
                    log.debug(&quot;break...&quot;);
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒，这时已经经历的时间假设为 400
                timePassed = System.currentTimeMillis() - begin;
                log.debug(&quot;timePassed: {}, object is null {}&quot;,
                        timePassed, response == null);
            }
            return response;
        }
    }

    //产生结果
    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足，通知等待线程
            this.response = response;
            log.debug(&quot;notify...&quot;);
            lock.notifyAll();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;多任务版&lt;/h5&gt;
&lt;p&gt;多任务版保护性暂停：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BF%9D%E6%8A%A4%E6%80%A7%E6%9A%82%E5%81%9C%E5%A4%9A%E4%BB%BB%E5%8A%A1%E7%89%88.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i &amp;lt; 3; i++) {
        new People().start();
    }
    Thread.sleep(1000);
    for (Integer id : Mailboxes.getIds()) {
        new Postman(id, id + &quot;号快递到了&quot;).start();
    }
}

@Slf4j(topic = &quot;c.People&quot;)
class People extends Thread{
    @Override
    public void run() {
        // 收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug(&quot;开始收信i d:{}&quot;, guardedObject.getId());
        Object mail = guardedObject.get(5000);
        log.debug(&quot;收到信id:{}，内容:{}&quot;, guardedObject.getId(),mail);
    }
}

class Postman extends Thread{
    private int id;
    private String mail;
    //构造方法
    @Override
    public void run() {
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.debug(&quot;开始送信i d:{}，内容:{}&quot;, guardedObject.getId(),mail);
        guardedObject.complete(mail);
    }
}

class  Mailboxes {
    private static Map&amp;lt;Integer, GuardedObject&amp;gt; boxes = new Hashtable&amp;lt;&amp;gt;();
    private static int id = 1;

    //产生唯一的id
    private static synchronized int generateId() {
        return id++;
    }

    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    public static Set&amp;lt;Integer&amp;gt; getIds() {
        return boxes.keySet();
    }
}
class GuardedObject {
    //标识，Guarded Object
    private int id;//添加get set方法
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;顺序输出&lt;/h4&gt;
&lt;p&gt;顺序输出 2  1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -&amp;gt; {
        while (true) {
            //try { Thread.sleep(1000); } catch (InterruptedException e) { }
            // 当没有许可时，当前线程暂停运行；有许可时，用掉这个许可，当前线程恢复运行
            LockSupport.park();
            System.out.println(&quot;1&quot;);
        }
    });
    Thread t2 = new Thread(() -&amp;gt; {
        while (true) {
            System.out.println(&quot;2&quot;);
            // 给线程 t1 发放『许可』（多次连续调用 unpark 只会发放一个『许可』）
            LockSupport.unpark(t1);
            try { Thread.sleep(500); } catch (InterruptedException e) { }
        }
    });
    t1.start();
    t2.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;交替输出&lt;/h4&gt;
&lt;p&gt;连续输出 5 次 abc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class day2_14 {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;a&quot;, a, b);
        }).start();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;b&quot;, b, c);
        }).start();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;c&quot;, c, a);
        }).start();

        Thread.sleep(1000);
        awaitSignal.lock();
        try {
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    //参数1：打印内容  参数二：条件变量  参数二：唤醒下一个
    public void print(String str, Condition condition, Condition next) {
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            lock();
            try {
                condition.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;异步模式&lt;/h3&gt;
&lt;h4&gt;传统版&lt;/h4&gt;
&lt;p&gt;异步模式之生产者/消费者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ShareData {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception{
        // 同步代码块，加锁
        lock.lock();
        try {
            // 判断  防止虚假唤醒
            while(number != 0) {
                // 等待不能生产
                condition.await();
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + &quot;\t &quot; + number);
            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws Exception{
        // 同步代码块，加锁
        lock.lock();
        try {
            // 判断 防止虚假唤醒
            while(number == 0) {
                // 等待不能消费
                condition.await();
            }
            // 干活
            number--;
            System.out.println(Thread.currentThread().getName() + &quot;\t &quot; + number);
            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class TraditionalProducerConsumer {
	public static void main(String[] args) {
        ShareData shareData = new ShareData();
        // t1线程，生产
        new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5; i++) {
            	shareData.increment();
            }
        }, &quot;t1&quot;).start();

        // t2线程，消费
        new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5; i++) {
				shareData.decrement();
            }
        }, &quot;t2&quot;).start(); 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;改进版&lt;/h4&gt;
&lt;p&gt;异步模式之生产者/消费者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费队列可以用来平衡生产和消费的线程资源，不需要产生结果和消费结果的线程一一对应&lt;/li&gt;
&lt;li&gt;生产者仅负责产生结果数据，不关心数据该如何处理，而消费者专心处理结果数据&lt;/li&gt;
&lt;li&gt;消息队列是有容量限制的，满时不会再加入数据，空时不会再消耗数据&lt;/li&gt;
&lt;li&gt;JDK 中各种阻塞队列，采用的就是这种模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class demo {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        for (int i = 0; i &amp;lt; 3; i++) {
            int id = i;
            new Thread(() -&amp;gt; {
                queue.put(new Message(id,&quot;值&quot;+id));
            }, &quot;生产者&quot; + i).start();
        }
        
        new Thread(() -&amp;gt; {
            while (true) {
                try {
                    Thread.sleep(1000);
                    Message message = queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },&quot;消费者&quot;).start();
    }
}

//消息队列类，Java间线程之间通信
class MessageQueue {
    private LinkedList&amp;lt;Message&amp;gt; list = new LinkedList&amp;lt;&amp;gt;();//消息的队列集合
    private int capacity;//队列容量
    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    //获取消息
    public Message take() {
        //检查队列是否为空
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    sout(Thread.currentThread().getName() + &quot;:队列为空，消费者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列的头部获取消息返回
            Message message = list.removeFirst();
            sout(Thread.currentThread().getName() + &quot;：已消费消息--&quot; + message);
            list.notifyAll();
            return message;
        }
    }

    //存入消息
    public void put(Message message) {
        synchronized (list) {
            //检查队列是否满
            while (list.size() == capacity) {
                try {
                    sout(Thread.currentThread().getName()+&quot;:队列为已满，生产者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将消息加入队列尾部
            list.addLast(message);
            sout(Thread.currentThread().getName() + &quot;:已生产消息--&quot; + message);
            list.notifyAll();
        }
    }
}

final class Message {
    private int id;
    private Object value;
	//get set
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;阻塞队列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ExecutorService consumer = Executors.newFixedThreadPool(1);
    ExecutorService producer = Executors.newFixedThreadPool(1);
    BlockingQueue&amp;lt;Integer&amp;gt; queue = new SynchronousQueue&amp;lt;&amp;gt;();
    producer.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;生产...&quot;);
            Thread.sleep(1000);
            queue.put(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    consumer.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;等待消费...&quot;);
            Integer result = queue.take();
            System.out.println(&quot;结果为:&quot; + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;内存&lt;/h2&gt;
&lt;h3&gt;JMM&lt;/h3&gt;
&lt;h4&gt;内存模型&lt;/h4&gt;
&lt;p&gt;Java 内存模型是 Java Memory Model（JMM），本身是一种&lt;strong&gt;抽象的概念&lt;/strong&gt;，实际上并不存在，描述的是一组规则或规范，通过这组规范定义了程序中各个变量（包括实例字段，静态字段和构成数组对象的元素）的访问方式&lt;/p&gt;
&lt;p&gt;JMM 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;屏蔽各种硬件和操作系统的内存访问差异，实现让 Java 程序在各种平台下都能达到一致的内存访问效果&lt;/li&gt;
&lt;li&gt;规定了线程和内存之间的一些关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据 JMM 的设计，系统存在一个主内存（Main Memory），Java 中所有变量都存储在主存中，对于所有线程都是共享的；每条线程都有自己的工作内存（Working Memory），工作内存中保存的是主存中某些&lt;strong&gt;变量的拷贝&lt;/strong&gt;，线程对所有变量的操作都是先对变量进行拷贝，然后在工作内存中进行，不能直接操作主内存中的变量；线程之间无法相互直接访问，线程间的通信（传递）必须通过主内存来完成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主内存和工作内存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主内存：计算机的内存，也就是经常提到的 8G 内存，16G 内存，存储所有共享变量的值&lt;/li&gt;
&lt;li&gt;工作内存：存储该线程使用到的共享变量在主内存的的值的副本拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;JVM 和 JMM 之间的关系&lt;/strong&gt;：JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分，这两者基本上是没有关系的，如果两者一定要勉强对应起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主内存主要对应于 Java 堆中的对象实例数据部分，而工作内存则对应于虚拟机栈中的部分区域&lt;/li&gt;
&lt;li&gt;从更低层次上说，主内存直接对应于物理硬件的内存，工作内存对应寄存器和高速缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;内存交互&lt;/h4&gt;
&lt;p&gt;Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作，每个操作都是&lt;strong&gt;原子&lt;/strong&gt;的&lt;/p&gt;
&lt;p&gt;非原子协定：没有被 volatile 修饰的 long、double 外，默认按照两次 32 位的操作&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-内存交互.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lock：作用于主内存，将一个变量标识为被一个线程独占状态（对应 monitorenter）&lt;/li&gt;
&lt;li&gt;unclock：作用于主内存，将一个变量从独占状态释放出来，释放后的变量才可以被其他线程锁定（对应 monitorexit）&lt;/li&gt;
&lt;li&gt;read：作用于主内存，把一个变量的值从主内存传输到工作内存中&lt;/li&gt;
&lt;li&gt;load：作用于工作内存，在 read 之后执行，把 read 得到的值放入工作内存的变量副本中&lt;/li&gt;
&lt;li&gt;use：作用于工作内存，把工作内存中一个变量的值传递给&lt;strong&gt;执行引擎&lt;/strong&gt;，每当遇到一个使用到变量的操作时都要使用该指令&lt;/li&gt;
&lt;li&gt;assign：作用于工作内存，把从执行引擎接收到的一个值赋给工作内存的变量&lt;/li&gt;
&lt;li&gt;store：作用于工作内存，把工作内存的一个变量的值传送到主内存中&lt;/li&gt;
&lt;li&gt;write：作用于主内存，在 store 之后执行，把 store 得到的值放入主内存的变量中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;三大特性&lt;/h4&gt;
&lt;h5&gt;可见性&lt;/h5&gt;
&lt;p&gt;可见性：是指当多个线程访问同一个变量时，一个线程修改了这个变量的值，其他线程能够立即看得到修改的值&lt;/p&gt;
&lt;p&gt;存在不可见问题的根本原因是由于缓存的存在，线程持有的是共享变量的副本，无法感知其他线程对于共享变量的更改，导致读取的值不是最新的。但是 final 修饰的变量是&lt;strong&gt;不可变&lt;/strong&gt;的，就算有缓存，也不会存在不可见的问题&lt;/p&gt;
&lt;p&gt;main 线程对 run 变量的修改对于 t 线程不可见，导致了 t 线程无法停止：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static boolean run = true;	//添加volatile
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()-&amp;gt;{
        while(run){
        // ....
        }
	});
    t.start();
    sleep(1);
    run = false; // 线程t不会如预想的停下来
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始状态， t 线程刚开始从主内存读取了 run 的值到工作内存&lt;/li&gt;
&lt;li&gt;因为 t 线程要频繁从主内存中读取 run 的值，JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中，减少对主存中 run 的访问，提高效率&lt;/li&gt;
&lt;li&gt;1 秒之后，main 线程修改了 run 的值，并同步至主存，而 t 是从自己工作内存中的高速缓存中读取这个变量的值，结果永远是旧值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-%E5%8F%AF%E8%A7%81%E6%80%A7%E4%BE%8B%E5%AD%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;原子性&lt;/h5&gt;
&lt;p&gt;原子性：不可分割，完整性，也就是说某个线程正在做某个具体业务时，中间不可以被分割，需要具体完成，要么同时成功，要么同时失败，保证指令不会受到线程上下文切换的影响&lt;/p&gt;
&lt;p&gt;定义原子操作的使用规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不允许 read 和 load、store 和 write 操作之一单独出现，必须顺序执行，但是不要求连续&lt;/li&gt;
&lt;li&gt;不允许一个线程丢弃 assign 操作，必须同步回主存&lt;/li&gt;
&lt;li&gt;不允许一个线程无原因地（没有发生过任何 assign 操作）把数据从工作内存同步会主内存中&lt;/li&gt;
&lt;li&gt;一个新的变量只能在主内存中诞生，不允许在工作内存中直接使用一个未被初始化（assign 或者 load）的变量，即对一个变量实施 use 和 store 操作之前，必须先自行 assign 和 load 操作&lt;/li&gt;
&lt;li&gt;一个变量在同一时刻只允许一条线程对其进行 lock 操作，但 lock 操作可以被同一线程重复执行多次，多次执行 lock 后，只有&lt;strong&gt;执行相同次数的 unlock&lt;/strong&gt; 操作，变量才会被解锁，&lt;strong&gt;lock 和 unlock 必须成对出现&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果对一个变量执行 lock 操作，将会&lt;strong&gt;清空工作内存中此变量的值&lt;/strong&gt;，在执行引擎使用这个变量之前需要重新从主存加载&lt;/li&gt;
&lt;li&gt;如果一个变量事先没有被 lock 操作锁定，则不允许执行 unlock 操作，也不允许去 unlock 一个被其他线程锁定的变量&lt;/li&gt;
&lt;li&gt;对一个变量执行 unlock 操作之前，必须&lt;strong&gt;先把此变量同步到主内存&lt;/strong&gt;中（执行 store 和 write 操作）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;有序性&lt;/h5&gt;
&lt;p&gt;有序性：在本线程内观察，所有操作都是有序的；在一个线程观察另一个线程，所有操作都是无序的，无序是因为发生了指令重排序&lt;/p&gt;
&lt;p&gt;CPU 的基本工作是执行存储的指令序列，即程序，程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程，为了提高性能，编译器和处理器会对指令重排，一般分为以下三种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;源代码 -&amp;gt; 编译器优化的重排 -&amp;gt; 指令并行的重排 -&amp;gt; 内存系统的重排 -&amp;gt; 最终执行指令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现代 CPU 支持多级指令流水线，几乎所有的冯•诺伊曼型计算机的 CPU，其工作都可以分为 5 个阶段：取指令、指令译码、执行指令、访存取数和结果写回，可以称之为&lt;strong&gt;五级指令流水线&lt;/strong&gt;。CPU 可以在一个时钟周期内，同时运行五条指令的&lt;strong&gt;不同阶段&lt;/strong&gt;（每个线程不同的阶段），本质上流水线技术并不能缩短单条指令的执行时间，但变相地提高了指令地吞吐率&lt;/p&gt;
&lt;p&gt;处理器在进行重排序时，必须要考虑&lt;strong&gt;指令之间的数据依赖性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单线程环境也存在指令重排，由于存在依赖性，最终执行结果和代码顺序的结果一致&lt;/li&gt;
&lt;li&gt;多线程环境中线程交替执行，由于编译器优化重排，会获取其他线程处在不同阶段的指令同时执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指令周期是取出一条指令并执行这条指令的时间，一般由若干个机器周期组成&lt;/li&gt;
&lt;li&gt;机器周期也称为 CPU 周期，一条指令的执行过程划分为若干个阶段（如取指、译码、执行等），每一阶段完成一个基本操作，完成一个基本操作所需要的时间称为机器周期&lt;/li&gt;
&lt;li&gt;振荡周期指周期性信号作周期性重复变化的时间间隔&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;cache&lt;/h3&gt;
&lt;h4&gt;缓存机制&lt;/h4&gt;
&lt;h5&gt;缓存结构&lt;/h5&gt;
&lt;p&gt;在计算机系统中，CPU 高速缓存（CPU Cache，简称缓存）是用于减少处理器访问内存所需平均时间的部件；在存储体系中位于自顶向下的第二层，仅次于 CPU 寄存器；其容量远小于内存，但速度却可以接近处理器的频率&lt;/p&gt;
&lt;p&gt;CPU 处理器速度远远大于在主内存中的，为了解决速度差异，在它们之间架设了多级缓存，如 L1、L2、L3 级别的缓存，这些缓存离 CPU 越近就越快，将频繁操作的数据缓存到这里，加快访问速度&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-CPU缓存结构.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;从 CPU 到&lt;/th&gt;
&lt;th&gt;大约需要的时钟周期&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;寄存器&lt;/td&gt;
&lt;td&gt;1 cycle (4GHz 的 CPU 约为 0.25ns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;3~4 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;10~20 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;40~45 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;120~240 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;缓存使用&lt;/h5&gt;
&lt;p&gt;当处理器发出内存访问请求时，会先查看缓存内是否有请求数据，如果存在（命中），则不用访问内存直接返回该数据；如果不存在（失效），则要先把内存中的相应数据载入缓存，再将其返回处理器&lt;/p&gt;
&lt;p&gt;缓存之所以有效，主要因为程序运行时对内存的访问呈现局部性（Locality）特征。既包括空间局部性（Spatial Locality），也包括时间局部性（Temporal Locality），有效利用这种局部性，缓存可以达到极高的命中率&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;伪共享&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;缓存以缓存行 cache line 为单位&lt;/strong&gt;，每个缓存行对应着一块内存，一般是 64 byte（8 个 long），在 CPU 从主存获取数据时，以 cache line 为单位加载，于是相邻的数据会一并加载到缓存中&lt;/p&gt;
&lt;p&gt;缓存会造成数据副本的产生，即同一份数据会缓存在不同核心的缓存行中，CPU 要保证数据的一致性，需要做到某个 CPU 核心更改了数据，其它 CPU 核心对应的&lt;strong&gt;整个缓存行必须失效&lt;/strong&gt;，这就是伪共享&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-内存伪共享.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;padding：通过填充，让数据落在不同的 cache line 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Contended：原理参考 无锁 → Adder → 优化机制 → 伪共享&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux 查看 CPU 缓存行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令：&lt;code&gt;cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内存地址格式：[高位组标记] [低位索引] [偏移量]&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存一致&lt;/h4&gt;
&lt;p&gt;缓存一致性：当多个处理器运算任务都涉及到同一块主内存区域的时候，将可能导致各自的缓存数据不一样&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-缓存一致性.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;MESI（Modified Exclusive Shared Or Invalid）是一种广泛使用的&lt;strong&gt;支持写回策略的缓存一致性协议&lt;/strong&gt;，CPU 中每个缓存行（caceh line）使用 4 种状态进行标记（使用额外的两位 bit 表示)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;M：被修改（Modified）&lt;/p&gt;
&lt;p&gt;该缓存行只被缓存在该 CPU 的缓存中，并且是被修改过的，与主存中的数据不一致 (dirty)，该缓存行中的内存需要写回 (write back) 主存。该状态的数据再次被修改不会发送广播，因为其他核心的数据已经在第一次修改时失效一次&lt;/p&gt;
&lt;p&gt;当被写回主存之后，该缓存行的状态会变成独享 (exclusive) 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;E：独享的（Exclusive）&lt;/p&gt;
&lt;p&gt;该缓存行只被缓存在该 CPU 的缓存中，是未被修改过的 (clear)，与主存中数据一致，修改数据不需要通知其他 CPU 核心，该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared)&lt;/p&gt;
&lt;p&gt;当 CPU 修改该缓存行中内容时，该状态可以变成 Modified 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;S：共享的（Shared）&lt;/p&gt;
&lt;p&gt;该状态意味着该缓存行可能被多个 CPU 缓存，并且各个缓存中的数据与主存数据一致，当 CPU 修改该缓存行中，会向其它 CPU 核心广播一个请求，使该缓存行变成无效状态 (Invalid)，然后再更新当前 Cache 里的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I：无效的（Invalid）&lt;/p&gt;
&lt;p&gt;该缓存是无效的，可能有其它 CPU 修改了该缓存行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：各个处理器访问缓存时都遵循一些协议，在读写时要根据协议进行操作，协议主要有 MSI、MESI 等&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;处理机制&lt;/h4&gt;
&lt;p&gt;单核 CPU 处理器会自动保证基本内存操作的原子性&lt;/p&gt;
&lt;p&gt;多核 CPU 处理器，每个 CPU 处理器内维护了一块内存，每个内核内部维护着一块缓存，当多线程并发读写时，就会出现缓存数据不一致的情况。处理器提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总线锁定：当处理器要操作共享变量时，在 BUS 总线上发出一个 LOCK 信号，其他处理器就无法操作这个共享变量，该操作会导致大量阻塞，从而增加系统的性能开销（&lt;strong&gt;平台级别的加锁&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;缓存锁定：当处理器对缓存中的共享变量进行了操作，其他处理器有嗅探机制，将各自缓存中的该共享变量的失效，读取时会重新从主内存中读取最新的数据，基于 MESI 缓存一致性协议来实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有如下两种情况处理器不会使用缓存锁定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当操作的数据跨多个缓存行，或没被缓存在处理器内部，则处理器会使用总线锁定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有些处理器不支持缓存锁定，比如：Intel 486 和 Pentium 处理器也会调用总线锁定&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总线机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;总线嗅探：每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了，当处理器发现自己的缓存对应的内存地址的数据被修改，就&lt;strong&gt;将当前处理器的缓存行设置为无效状态&lt;/strong&gt;，当处理器对这个数据进行操作时，会重新从内存中把数据读取到处理器缓存中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总线风暴：当某个 CPU 核心更新了 Cache 中的数据，要把该事件广播通知到其他核心（&lt;strong&gt;写传播&lt;/strong&gt;），CPU 需要每时每刻监听总线上的一切活动，但是不管别的核心的 Cache 是否缓存相同的数据，都需要发出一个广播事件，不断的从主内存嗅探和 CAS 循环，无效的交互会导致总线带宽达到峰值；因此不要大量使用 volatile 关键字，使用 volatile、syschonized 都需要根据实际场景&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;volatile&lt;/h3&gt;
&lt;h4&gt;同步机制&lt;/h4&gt;
&lt;p&gt;volatile 是 Java 虚拟机提供的&lt;strong&gt;轻量级&lt;/strong&gt;的同步机制（三大特性）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证可见性&lt;/li&gt;
&lt;li&gt;不保证原子性&lt;/li&gt;
&lt;li&gt;保证有序性（禁止指令重排）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：volatile 修饰的变量进行读操作与普通变量几乎没什么差别，但是写操作相对慢一些，因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行，但是开销比锁要小&lt;/p&gt;
&lt;p&gt;synchronized 无法禁止指令重排和处理器优化，为什么可以保证有序性可见性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加了锁之后，只能有一个线程获得到了锁，获得不到锁的线程就要阻塞，所以同一时间只有一个线程执行，相当于单线程，由于数据依赖性的存在，单线程的指令重排是没有问题的&lt;/li&gt;
&lt;li&gt;线程加锁前，将&lt;strong&gt;清空工作内存&lt;/strong&gt;中共享变量的值，使用共享变量时需要从主内存中重新读取最新的值；线程解锁前，必须把共享变量的最新值&lt;strong&gt;刷新到主内存&lt;/strong&gt;中（JMM 内存交互章节有讲）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;指令重排&lt;/h4&gt;
&lt;p&gt;volatile 修饰的变量，可以禁用指令重排&lt;/p&gt;
&lt;p&gt;指令重排实例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;example 1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void mySort() {
	int x = 11;	//语句1
	int y = 12;	//语句2  谁先执行效果一样
	x = x + 5;	//语句3
	y = x * x;	//语句4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行顺序是：1 2 3 4、2 1 3 4、1 3 2 4&lt;/p&gt;
&lt;p&gt;指令重排也有限制不会出现：4321，语句 4 需要依赖于 y 以及 x 的申明，因为存在数据依赖，无法首先执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;example 2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
    	r.r1 = num + num;
    } else {
    	r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
	num = 2;
	ready = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情况一：线程 1 先执行，ready = false，结果为 r.r1 = 1&lt;/p&gt;
&lt;p&gt;情况二：线程 2 先执行 num = 2，但还没执行 ready = true，线程 1 执行，结果为 r.r1 = 1&lt;/p&gt;
&lt;p&gt;情况三：线程 2 先执行 ready = true，线程 1 执行，进入 if 分支结果为 r.r1 = 4&lt;/p&gt;
&lt;p&gt;情况四：线程 2 执行 ready = true，切换到线程 1，进入 if 分支为 r.r1 = 0，再切回线程 2 执行 num = 2，发生指令重排&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;底层原理&lt;/h4&gt;
&lt;h5&gt;缓存一致&lt;/h5&gt;
&lt;p&gt;使用 volatile 修饰的共享变量，底层通过汇编 lock 前缀指令进行缓存锁定，在线程修改完共享变量后写回主存，其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态，读取时会重新从主内存中读取最新的数据&lt;/p&gt;
&lt;p&gt;lock 前缀指令就相当于内存屏障，Memory Barrier（Memory Fence）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 volatile 变量的写指令后会加入写屏障&lt;/li&gt;
&lt;li&gt;对 volatile 变量的读指令前会加入读屏障&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内存屏障有三个作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保对内存的读-改-写操作原子执行&lt;/li&gt;
&lt;li&gt;阻止屏障两侧的指令重排序&lt;/li&gt;
&lt;li&gt;强制把缓存中的脏数据写回主内存，让缓存行中相应的数据失效&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;内存屏障&lt;/h5&gt;
&lt;p&gt;保证&lt;strong&gt;可见性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写屏障（sfence，Store Barrier）保证在该屏障之前的，对共享变量的改动，都同步到主存当中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读屏障（lfence，Load Barrier）保证在该屏障之后的，对共享变量的读取，从主存刷新变量值，加载的是主存中最新数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
    	r.r1 = num + num;
    } else {
    	r.r1 = 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-volatile保证可见性.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全能屏障：mfence（modify/mix Barrier），兼具 sfence 和 lfence 的功能&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保证&lt;strong&gt;有序性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后&lt;/li&gt;
&lt;li&gt;读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不能解决指令交错：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写屏障仅仅是保证之后的读能够读到最新的结果，但不能保证其他线程的读跑到写屏障之前&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有序性的保证也只是保证了本线程内相关代码不被重排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile i = 0;
new Thread(() -&amp;gt; {i++});
new Thread(() -&amp;gt; {i--});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i++ 反编译后的指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: iconst_1			// 当int取值 -1~5 时，JVM采用iconst指令将常量压入栈中
1: istore_1			// 将操作数栈顶数据弹出，存入局部变量表的 slot 1
2: iinc		1, 1	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-volatile不能保证原子性.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;交互规则&lt;/h5&gt;
&lt;p&gt;对于 volatile 修饰的变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程对变量的 use 与 load、read 操作是相关联的，所以变量使用前必须先从主存加载&lt;/li&gt;
&lt;li&gt;线程对变量的 assign 与 store、write 操作是相关联的，所以变量使用后必须同步至主存&lt;/li&gt;
&lt;li&gt;线程 1 和线程 2 谁先对变量执行 read 操作，就会先进行 write 操作，防止指令重排&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;双端检锁&lt;/h4&gt;
&lt;h5&gt;检锁机制&lt;/h5&gt;
&lt;p&gt;Double-Checked Locking：双端检锁机制&lt;/p&gt;
&lt;p&gt;DCL（双端检锁）机制不一定是线程安全的，原因是有指令重排的存在，加入 volatile 可以禁止指令重排&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2，这里的判断不是线程安全的
            // 首次访问会同步，而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                // 这里是线程安全的判断，防止其他线程在当前线程等待锁的期间完成了初始化
                if (INSTANCE == null) { 
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不锁 INSTANCE 的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;INSTANCE 要重新赋值&lt;/li&gt;
&lt;li&gt;INSTANCE 是 null，线程加锁之前需要获取对象的引用，设置对象头，null 没有引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;懒惰初始化&lt;/li&gt;
&lt;li&gt;首次使用 getInstance() 才使用 synchronized 加锁，后续使用时无需加锁&lt;/li&gt;
&lt;li&gt;第一个 if 使用了 INSTANCE 变量，是在同步块之外，但在多线程环境下会产生问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;DCL问题&lt;/h5&gt;
&lt;p&gt;getInstance 方法对应的字节码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: 	getstatic 		#2 		// Field INSTANCE:Ltest/Singleton;
3: 	ifnonnull 		37
6: 	ldc 			#3 		// class test/Singleton
8: 	dup
9: 	astore_0
10: monitorenter
11: getstatic 		#2 		// Field INSTANCE:Ltest/Singleton;
14: ifnonnull 27
17: new 			#3 		// class test/Singleton
20: dup
21: invokespecial 	#4 		// Method &quot;&amp;lt;init&amp;gt;&quot;:()V
24: putstatic 		#2 		// Field INSTANCE:Ltest/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic 		#2 		// Field INSTANCE:Ltest/Singleton;
40: areturn
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;17 表示创建对象，将对象引用入栈&lt;/li&gt;
&lt;li&gt;20 表示复制一份对象引用，引用地址&lt;/li&gt;
&lt;li&gt;21 表示利用一个对象引用，调用构造方法初始化对象&lt;/li&gt;
&lt;li&gt;24 表示利用一个对象引用，赋值给 static INSTANCE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步骤 21 和 24 之间不存在数据依赖关系&lt;/strong&gt;，而且无论重排前后，程序的执行结果在单线程中并没有改变，因此这种重排优化是允许的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键在于 0:getstatic 这行代码在 monitor 控制之外，可以越过 monitor 读取 INSTANCE 变量的值&lt;/li&gt;
&lt;li&gt;当其他线程访问 INSTANCE 不为 null 时，由于 INSTANCE 实例未必已初始化，那么 t2 拿到的是将是一个未初始化完毕的单例返回，这就造成了线程安全的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL%E5%87%BA%E7%8E%B0%E7%9A%84%E9%97%AE%E9%A2%98.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;解决方法&lt;/h5&gt;
&lt;p&gt;指令重排只会保证串行语义的执行一致性（单线程），但并不会关系多线程间的语义一致性&lt;/p&gt;
&lt;p&gt;引入 volatile，来保证出现指令重排的问题，从而保证单例模式的线程安全性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static volatile SingletonDemo INSTANCE = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;ha-be&lt;/h3&gt;
&lt;p&gt;happens-before 先行发生&lt;/p&gt;
&lt;p&gt;Java 内存模型具备一些先天的“有序性”，即不需要通过任何同步手段（volatile、synchronized 等）就能够得到保证的安全，这个通常也称为 happens-before 原则，它是可见性与有序性的一套规则总结&lt;/p&gt;
&lt;p&gt;不符合 happens-before 规则，JMM 并不能保证一个线程的可见性和有序性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;程序次序规则 (Program Order Rule)：一个线程内，逻辑上书写在前面的操作先行发生于书写在后面的操作 ，因为多个操作之间有先后依赖关系，则不允许对这些操作进行重排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁定规则 (Monitor Lock Rule)：一个 unlock 操作先行发生于后面（时间的先后）对同一个锁的 lock 操作，所以线程解锁 m 之前对变量的写（解锁前会刷新到主内存中），对于接下来对 m 加锁的其它线程对该变量的读可见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;volatile 变量规则&lt;/strong&gt;  (Volatile Variable Rule)：对 volatile 变量的写操作先行发生于后面对这个变量的读&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递规则 (Transitivity)：具有传递性，如果操作 A 先行发生于操作 B，而操作 B 又先行发生于操作 C，则可以得出操作 A 先行发生于操作 C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程启动规则 (Thread Start Rule)：Thread 对象的 start()方 法先行发生于此线程中的每一个操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int x = 10;//线程 start 前对变量的写，对该线程开始后对该变量的读可见
new Thread(()-&amp;gt;{	System.out.println(x);	},&quot;t1&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程中断规则 (Thread Interruption Rule)：对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程终止规则 (Thread Termination Rule)：线程中所有的操作都先行发生于线程的终止检测，可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象终结规则（Finaizer Rule）：一个对象的初始化完成（构造函数执行结束）先行发生于它的 finalize() 方法的开始&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;设计模式&lt;/h3&gt;
&lt;h4&gt;终止模式&lt;/h4&gt;
&lt;p&gt;终止模式之两阶段终止模式：停止标记用 volatile 是为了保证该变量在多个线程之间的可见性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TwoPhaseTermination {
    // 监控线程
    private Thread monitor;
    // 停止标记
    private volatile boolean stop = false;;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -&amp;gt; {
            while (true) {
                Thread thread = Thread.currentThread();
                if (stop) {
                    System.out.println(&quot;后置处理&quot;);
                    break;
                }
                try {
                    Thread.sleep(1000);// 睡眠
                    System.out.println(thread.getName() + &quot;执行监控记录&quot;);
                } catch (InterruptedException e) {
                   	System.out.println(&quot;被打断，退出睡眠&quot;);
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        monitor.interrupt();// 让线程尽快退出Timed Waiting
    }
}
// 测试
public static void main(String[] args) throws InterruptedException {
    TwoPhaseTermination tpt = new TwoPhaseTermination();
    tpt.start();
    Thread.sleep(3500);
    System.out.println(&quot;停止监控&quot;);
    tpt.stop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Balking&lt;/h4&gt;
&lt;p&gt;Balking （犹豫）模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事，那么本线程就无需再做了，直接结束返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MonitorService {
    // 用来表示是否已经有线程已经在执行启动了
    private volatile boolean starting = false;
    public void start() {
        System.out.println(&quot;尝试启动监控线程...&quot;);
        synchronized (this) {
            if (starting) {
            	return;
            }
            starting = true;
        }
        // 真正启动监控线程...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比保护性暂停模式：保护性暂停模式用在一个线程等待另一个线程的执行结果，当条件不满足时线程等待&lt;/p&gt;
&lt;p&gt;例子：希望 doInit() 方法仅被调用一次，下面的实现出现的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 t1 线程进入 init() 准备 doInit()，t2 线程进来，initialized 还为f alse，则 t2 就又初始化一次&lt;/li&gt;
&lt;li&gt;volatile 适合一个线程写，其他线程读的情况，这个代码需要加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class TestVolatile {
    volatile boolean initialized = false;
    
    void init() {
        if (initialized) {
            return;
        }
    	doInit();
    	initialized = true;
    }
    private void doInit() {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;无锁&lt;/h2&gt;
&lt;h3&gt;CAS&lt;/h3&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;p&gt;无锁编程：Lock Free&lt;/p&gt;
&lt;p&gt;CAS 的全称是 Compare-And-Swap，是 &lt;strong&gt;CPU 并发原语&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法，调用 UnSafe 类中的 CAS 方法，JVM 会实现出 CAS 汇编指令，这是一种完全依赖于硬件的功能，实现了原子操作&lt;/li&gt;
&lt;li&gt;CAS 是一种系统原语，原语属于操作系统范畴，是由若干条指令组成 ，用于完成某个功能的一个过程，并且原语的执行必须是连续的，执行过程中不允许被中断，所以 CAS 是一条 CPU 的原子指令，不会造成数据不一致的问题，是线程安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;底层原理：CAS 的底层是 &lt;code&gt;lock cmpxchg&lt;/code&gt; 指令（X86 架构），在单核和多核 CPU 下都能够保证比较交换的原子性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;程序是在单核处理器上运行，会省略 lock 前缀，单处理器自身会维护处理器内的顺序一致性，不需要 lock 前缀的内存屏障效果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序是在多核处理器上运行，会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时，CPU 会执行&lt;strong&gt;总线锁定或缓存锁定&lt;/strong&gt;，将修改的变量写入到主存，这个过程不会被线程的调度机制所打断，保证了多个线程对内存操作的原子性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用：比较当前工作内存中的值和主物理内存中的值，如果相同则执行规定操作，否则继续比较直到主内存和工作内存的值一致为止&lt;/p&gt;
&lt;p&gt;CAS 特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS 体现的是&lt;strong&gt;无锁并发、无阻塞并发&lt;/strong&gt;，线程不会陷入阻塞，线程不需要频繁切换状态（上下文切换，系统调用）&lt;/li&gt;
&lt;li&gt;CAS 是基于乐观锁的思想&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CAS 缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行的是循环操作，如果比较不成功一直在循环，最差的情况某个线程一直取到的值和预期值都不一样，就会无限循环导致饥饿，&lt;strong&gt;使用 CAS 线程数不要超过 CPU 的核心数&lt;/strong&gt;，采用分段 CAS 和自动迁移机制&lt;/li&gt;
&lt;li&gt;只能保证一个共享变量的原子操作
&lt;ul&gt;
&lt;li&gt;对于一个共享变量执行操作时，可以通过循环 CAS 的方式来保证原子操作&lt;/li&gt;
&lt;li&gt;对于多个共享变量操作时，循环 CAS 就无法保证操作的原子性，这个时候&lt;strong&gt;只能用锁来保证原子性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;引出来 ABA 问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;乐观锁&lt;/h4&gt;
&lt;p&gt;CAS 与 synchronized 总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 是从悲观的角度出发：总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会阻塞（共享资源每次只给一个线程使用，其它线程阻塞，用完后再把资源转让给其它线程），因此 synchronized 也称之为悲观锁，ReentrantLock 也是一种悲观锁，性能较差&lt;/li&gt;
&lt;li&gt;CAS 是从乐观的角度出发：总是假设最好的情况，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。&lt;strong&gt;如果别人修改过，则获取现在最新的值，如果别人没修改过，直接修改共享数据的值&lt;/strong&gt;，CAS 这种机制也称之为乐观锁，综合性能较好&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Atomic&lt;/h3&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;常见原子类：AtomicInteger、AtomicBoolean、AtomicLong&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public AtomicInteger()&lt;/code&gt;：初始化一个默认值为 0 的原子型 Integer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public AtomicInteger(int initialValue)&lt;/code&gt;：初始化一个指定值的原子型 Integer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public final int get()&lt;/td&gt;
&lt;td&gt;获取 AtomicInteger 的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getAndIncrement()&lt;/td&gt;
&lt;td&gt;以原子方式将当前值加 1，返回的是自增前的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int incrementAndGet()&lt;/td&gt;
&lt;td&gt;以原子方式将当前值加 1，返回的是自增后的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getAndSet(int value)&lt;/td&gt;
&lt;td&gt;以原子方式设置为 newValue 的值，返回旧值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int addAndGet(int data)&lt;/td&gt;
&lt;td&gt;以原子方式将输入的数值与实例中的值相加并返回&amp;lt;br /&amp;gt;实例：AtomicInteger 里的 value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;原理分析&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;AtomicInteger 原理&lt;/strong&gt;：自旋锁  + CAS 算法&lt;/p&gt;
&lt;p&gt;CAS 算法：有 3 个操作数（内存值 V， 旧的预期值 A，要修改的值 B）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当旧的预期值 A == 内存值 V   此时可以修改，将 V 改为 B&lt;/li&gt;
&lt;li&gt;当旧的预期值 A !=  内存值 V   此时不能修改，并重新获取现在的最新值，重新获取的动作就是自旋&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析 getAndSet 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AtomicInteger：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final int getAndSet(int newValue) {
    /**
    * this: 		当前对象
    * valueOffset:	内存偏移量，内存地址
    */
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;valueOffset：偏移量表示该变量值相对于当前对象地址的偏移，Unsafe 就是根据内存偏移地址获取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField(&quot;value&quot;));
//调用本地方法   --&amp;gt;
public native long objectFieldOffset(Field var1);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unsafe 类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// val1: AtomicInteger对象本身，var2: 该对象值得引用地址，var4: 需要变动的数
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // var5: 用 var1 和 var2 找到的内存中的真实值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;var5：从主内存中拷贝到工作内存中的值（每次都要从主内存拿到最新的值到本地内存），然后执行 &lt;code&gt;compareAndSwapInt()&lt;/code&gt; 再和主内存的值进行比较，假设方法返回 false，那么就一直执行 while 方法，直到期望的值和真实值一样，修改数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变量 value 用 volatile 修饰，保证了多线程之间的内存可见性，避免线程从工作缓存中获取失效的变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析 getAndUpdate 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;getAndUpdate：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final int getAndUpdate(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();	//当前值，cas的期望值
        next = updateFunction.applyAsInt(prev);//期望值更新到该值
    } while (!compareAndSet(prev, next));//自旋
    return prev;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数式接口：可以自定义操作逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AtomicInteger a = new AtomicInteger();
a.getAndUpdate(i -&amp;gt; i + 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;compareAndSet：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final boolean compareAndSet(int expect, int update) {
    /**
    * this: 		当前对象
    * valueOffset:	内存偏移量，内存地址
    * expect:		期望的值
    * update: 		更新的值
    */
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;原子引用&lt;/h4&gt;
&lt;p&gt;原子引用：对 Object 进行原子操作，提供一种读和写都是原子性的对象引用变量&lt;/p&gt;
&lt;p&gt;原子引用类：AtomicReference、AtomicStampedReference、AtomicMarkableReference&lt;/p&gt;
&lt;p&gt;AtomicReference 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;code&gt;AtomicReference&amp;lt;T&amp;gt; atomicReference = new AtomicReference&amp;lt;T&amp;gt;()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public final boolean compareAndSet(V expectedValue, V newValue)&lt;/code&gt;：CAS 操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public final void set(V newValue)&lt;/code&gt;：将值设置为 newValue&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public final V get()&lt;/code&gt;：返回当前值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Student s1 = new Student(33, &quot;z3&quot;);
        
        // 创建原子引用包装类
        AtomicReference&amp;lt;Student&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();
        // 设置主内存共享变量为s1
        atomicReference.set(s1);

        // 比较并交换，如果现在主物理内存的值为 z3，那么交换成 l4
        while (true) {
            Student s2 = new Student(44, &quot;l4&quot;);
            if (atomicReference.compareAndSet(s1, s2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

class Student {
    private int id;
    private String name;
    //。。。。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子数组&lt;/h4&gt;
&lt;p&gt;原子数组类：AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray&lt;/p&gt;
&lt;p&gt;AtomicIntegerArray 类方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
*   i		the index
* expect 	the expected value
* update 	the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子更新器&lt;/h4&gt;
&lt;p&gt;原子更新器类：AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater&lt;/p&gt;
&lt;p&gt;利用字段更新器，可以针对对象的某个域（Field）进行原子操作，只能配合 volatile 修饰的字段使用，否则会出现异常 &lt;code&gt;IllegalArgumentException: Must be volatile type&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static &amp;lt;U&amp;gt; AtomicIntegerFieldUpdater&amp;lt;U&amp;gt; newUpdater(Class&amp;lt;U&amp;gt; c, String fieldName)&lt;/code&gt;：构造方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abstract boolean compareAndSet(T obj, int expect, int update)&lt;/code&gt;：CAS&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UpdateDemo {
    private volatile int field;
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
            		.newUpdater(UpdateDemo.class, &quot;field&quot;);
        UpdateDemo updateDemo = new UpdateDemo();
        fieldUpdater.compareAndSet(updateDemo, 0, 10);
        System.out.println(updateDemo.field);//10
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子累加器&lt;/h4&gt;
&lt;p&gt;原子累加器类：LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator&lt;/p&gt;
&lt;p&gt;LongAdder 和 LongAccumulator 区别：&lt;/p&gt;
&lt;p&gt;相同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的&lt;/li&gt;
&lt;li&gt;LongAddr 类是 LongAccumulator 类的一个特例，只是 LongAccumulator 提供了更强大的功能，可以自定义累加规则，当accumulatorFunction 为 null 时就等价于 LongAddr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 casBase 时，LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算，LongAddr 使用 casBase(b = base, b + x)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LongAccumulator 类功能更加强大，构造方法参数中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;accumulatorFunction 是一个双目运算器接口，可以指定累加规则，比如累加或者相乘，其根据输入的两个参数返回一个计算值，LongAdder 内置累加规则&lt;/li&gt;
&lt;li&gt;identity 则是 LongAccumulator 累加器的初始值，LongAccumulator 可以为累加器提供非0的初始值，而 LongAdder 只能提供默认的 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Adder&lt;/h3&gt;
&lt;h4&gt;优化机制&lt;/h4&gt;
&lt;p&gt;LongAdder 是 Java8 提供的类，跟 AtomicLong 有相同的效果，但对 CAS 机制进行了优化，尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能&lt;/p&gt;
&lt;p&gt;CAS 底层实现是在一个循环中不断地尝试修改目标值，直到修改成功。如果竞争不激烈修改成功率很高，否则失败率很高，失败后这些重复的原子性操作会耗费性能（导致大量线程&lt;strong&gt;空循环，自旋转&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;优化核心思想：数据分离，将 AtomicLong 的&lt;strong&gt;单点的更新压力分担到各个节点，空间换时间&lt;/strong&gt;，在低并发的时候直接更新，可以保障和 AtomicLong 的性能基本一致，而在高并发的时候通过分散减少竞争，提高了性能&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分段 CAS 机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在发生竞争时，创建 Cell 数组用于将不同线程的操作离散（通过 hash 等算法映射）到不同的节点上&lt;/li&gt;
&lt;li&gt;设置多个累加单元（会根据需要扩容，最大为 CPU 核数），Therad-0 累加 Cell[0]，而 Thread-1 累加 Cell[1] 等，最后将结果汇总&lt;/li&gt;
&lt;li&gt;在累加时操作的不同的 Cell 变量，因此减少了 CAS 重试失败，从而提高性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;自动分段迁移机制&lt;/strong&gt;：某个 Cell 的 value 执行 CAS 失败，就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;伪共享&lt;/h4&gt;
&lt;p&gt;Cell 为累加单元：数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的，这个域是 ThreadLocalRandom 更新的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Striped64.Cell
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 用 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
    	return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cell 是数组形式，&lt;strong&gt;在内存中是连续存储的&lt;/strong&gt;，64 位系统中，一个 Cell 为 24 字节（16 字节的对象头和 8 字节的 value），每一个 cache line 为 64 字节，因此缓存行可以存下 2 个的 Cell 对象，当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1]，无论谁修改成功都会导致当前缓存行失效，从而导致对方的数据失效，需要重新去主存获取，影响效率&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BC%AA%E5%85%B1%E4%BA%AB1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;@sun.misc.Contended：防止缓存行伪共享，在使用此注解的对象或字段的前后各增加 128 字节大小的 padding，使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时&lt;strong&gt;占用不同的缓存行&lt;/strong&gt;，这样就不会造成对方缓存行的失效&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BC%AA%E5%85%B1%E4%BA%AB2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;源码解析&lt;/h4&gt;
&lt;p&gt;Striped64 类成员属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示当前计算机CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors()
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域，当 cells 扩容时，也会将数据写到 base 中
transient volatile long base;
// 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁
transient volatile int cellsBusy;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cells 占用内存是相对比较大的，是惰性加载的，在无竞争或者其他线程正在初始化 cells 数组的情况下，直接更新 base 域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在第一次发生竞争时（casBase 失败）会创建一个大小为 2 的 cells 数组，将当前累加的值包装为 Cell 对象，放入映射的槽位上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分段累加的过程中，如果当前线程对应的 cells 槽位为空，就会新建 Cell 填充，如果出现竞争，就会重新计算线程对应的槽位，继续自旋尝试修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍，然后 rehash，&lt;strong&gt;数组长度总是 2 的 n 次幂&lt;/strong&gt;，默认最大为 CPU 核数，但是可以超过，如果核数是 6 核，数组最长是 8&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;LongAdder#add：累加方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void add(long x) {
    // as 为累加单元数组的引用，b 为基础值，v 表示期望值
    // m 表示 cells 数组的长度 - 1，a 表示当前线程命中的 cell 单元格
    Cell[] as; long b, v; int m; Cell a;
    
    // cells 不为空说明 cells 已经被初始化，线程发生了竞争，去更新对应的 cell 槽位
    // 进入 || 后的逻辑去更新 base 域，更新失败表示发生竞争进入条件
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // uncontended 为 true 表示 cell 没有竞争
        boolean uncontended = true;
        
        // 条件一: true 说明 cells 未初始化，多线程写 base 发生竞争需要进行初始化 cells 数组
        //		  fasle 说明 cells 已经初始化，进行下一个条件寻找自己的 cell 去累加
        // 条件二: getProbe() 获取 hash 值，&amp;amp; m 的逻辑和 HashMap 的逻辑相同，保证散列的均匀性
        // 		  true 说明当前线程对应下标的 cell 为空，需要创建 cell
        //        false 说明当前线程对应的 cell 不为空，进行下一个条件【将 x 值累加到对应的 cell 中】
        // 条件三: 有取反符号，false 说明 cas 成功，直接返回，true 说明失败，当前线程对应的 cell 有竞争
        if (as == null || (m = as.length - 1) &amp;lt; 0 ||
            (a = as[getProbe() &amp;amp; m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
        	// 【uncontended 在对应的 cell 上累加失败的时候才为 false，其余情况均为 true】
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Striped64#longAccumulate：cell 数组创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;							// x  			null 			false | true
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell
    if ((h = getProbe()) == 0) {
        // 初始化 probe，获取 hash 值
        ThreadLocalRandom.current(); 
        h = getProbe();	
        // 默认情况下 当前线程肯定是写入到了 cells[0] 位置，不把它当做一次真正的竞争
        wasUncontended = true;
    }
    // 表示【扩容意向】，false 一定不会扩容，true 可能会扩容
    boolean collide = false; 
    //自旋
    for (;;) {
        // as 表示cells引用，a 表示当前线程命中的 cell，n 表示 cells 数组长度，v 表示 期望值
        Cell[] as; Cell a; int n; long v;
        // 【CASE1】: 表示 cells 已经初始化了，当前线程应该将数据写入到对应的 cell 中
        if ((as = cells) != null &amp;amp;&amp;amp; (n = as.length) &amp;gt; 0) {
            // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null，需要创建 new Cell
            if ((a = as[(n - 1) &amp;amp; h]) == null) {
                // 判断 cellsBusy 是否被锁
                if (cellsBusy == 0) {   
                    // 创建 cell, 初始累加值为 x
                    Cell r = new Cell(x);  
                    // 加锁
                    if (cellsBusy == 0 &amp;amp;&amp;amp; casCellsBusy()) {
                        // 创建成功标记，进入【创建 cell 逻辑】
                        boolean created = false;	
                        try {
                            Cell[] rs; int m, j;
                            // 把当前 cells 数组赋值给 rs，并且不为 null
                            if ((rs = cells) != null &amp;amp;&amp;amp;
                                (m = rs.length) &amp;gt; 0 &amp;amp;&amp;amp;
                                // 再次判断防止其它线程初始化过该位置，当前线程再次初始化该位置会造成数据丢失
                                // 因为这里是线程安全的判断，进行的逻辑不会被其他线程影响
                                rs[j = (m - 1) &amp;amp; h] == null) {
                                // 把新创建的 cell 填充至当前位置
                                rs[j] = r;
                                created = true;	// 表示创建完成
                            }
                        } finally {
                            cellsBusy = 0;		// 解锁
                        }
                        if (created)			// true 表示创建完成，可以推出循环了
                            break;
                        continue;
                    }
                }
                collide = false;
            }
            // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas
            else if (!wasUncontended)
                wasUncontended = true;
            // CASE 1.3: 当前线程 rehash 过，如果新命中的 cell 不为空，就尝试累加，false 说明新命中也有竞争
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
            // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容
            else if (n &amp;gt;= NCPU || cells != as)
                collide = false; 		// 扩容意向改为false，【表示不能扩容了】
            // CASE 1.5: 更改扩容意向，如果 n &amp;gt;= NCPU，这里就永远不会执行到，case1.4 永远先于 1.5 执行
            else if (!collide)
                collide = true;
            // CASE 1.6: 【扩容逻辑】，进行加锁
            else if (cellsBusy == 0 &amp;amp;&amp;amp; casCellsBusy()) {
                try {
                    // 线程安全的检查，防止期间被其他线程扩容了
                    if (cells == as) {     
                        // 扩容为以前的 2 倍
                        Cell[] rs = new Cell[n &amp;lt;&amp;lt; 1];
                        // 遍历移动值
                        for (int i = 0; i &amp;lt; n; ++i)
                            rs[i] = as[i];
                        // 把扩容后的引用给 cells
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;	// 解锁
                }
                collide = false;	// 扩容意向改为 false，表示不扩容了
                continue;
            }
            // 重置当前线程 Hash 值，这就是【分段迁移机制】
            h = advanceProbe(h);
        }

        // 【CASE2】: 运行到这说明 cells 还未初始化，as 为null
        // 判断是否没有加锁，没有加锁就用 CAS 加锁
        // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells，这里不是线程安全的判断
        else if (cellsBusy == 0 &amp;amp;&amp;amp; cells == as &amp;amp;&amp;amp; casCellsBusy()) {
            // 初始化标志，开始 【初始化 cells 数组】
            boolean init = false;
            try { 
               	// 再次判断 cells == as 防止其它线程已经提前初始化了，当前线程再次初始化导致丢失数据
                // 因为这里是【线程安全的，重新检查，经典 DCL】
                if (cells == as) {
                    Cell[] rs = new Cell[2];	// 初始化数组大小为2
                    rs[h &amp;amp; 1] = new Cell(x);	// 填充线程对应的cell
                    cells = rs;
                    init = true;				// 初始化成功，标记置为 true
                }
            } finally {
                cellsBusy = 0;					// 解锁啊
            }
            if (init)
                break;							// 初始化成功直接跳出自旋
        }
        // 【CASE3】: 运行到这说明其他线程在初始化 cells，当前线程将值累加到 base，累加成功直接结束自旋
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sum：获取最终结果通过 sum 整合，&lt;strong&gt;保证最终一致性，不保证强一致性&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        // 遍历 累加
        for (int i = 0; i &amp;lt; as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ABA&lt;/h3&gt;
&lt;p&gt;ABA 问题：当进行获取主内存值时，该内存值在写入主内存时已经被修改了 N 次，但是最终又改成原来的值&lt;/p&gt;
&lt;p&gt;其他线程先把 A 改成 B 又改回 A，主线程&lt;strong&gt;仅能判断出共享变量的值与最初值 A 是否相同&lt;/strong&gt;，不能感知到这种从 A 改为 B 又 改回 A 的情况，这时 CAS 虽然成功，但是过程存在问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public AtomicStampedReference(V initialRef, int initialStamp)&lt;/code&gt;：初始值和初始版本号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt; public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)&lt;/code&gt;：&lt;strong&gt;期望引用和期望版本号都一致&lt;/strong&gt;才进行 CAS 修改数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void set(V newReference, int newStamp)&lt;/code&gt;：设置值和版本号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V getReference()&lt;/code&gt;：返回引用的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int getStamp()&lt;/code&gt;：返回当前版本号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    AtomicStampedReference&amp;lt;Integer&amp;gt; atomicReference = new AtomicStampedReference&amp;lt;&amp;gt;(100,1);
    int startStamp = atomicReference.getStamp();
    new Thread(() -&amp;gt;{
        int stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(100, 101, stamp, stamp + 1);
        stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(101, 100, stamp, stamp + 1);
    },&quot;t1&quot;).start();

    new Thread(() -&amp;gt;{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {
            System.out.println(atomicReference.getReference());//100
            System.out.println(Thread.currentThread().getName() + &quot;线程修改失败&quot;);
        }
    },&quot;t2&quot;).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Unsafe&lt;/h3&gt;
&lt;p&gt;Unsafe 是 CAS 的核心类，由于 Java 无法直接访问底层系统，需要通过本地（Native）方法来访问&lt;/p&gt;
&lt;p&gt;Unsafe 类存在 sun.misc 包，其中所有方法都是 native 修饰的，都是直接调用&lt;strong&gt;操作系统底层资源&lt;/strong&gt;执行相应的任务，基于该类可以直接操作特定的内存数据，其内部方法操作类似 C 的指针&lt;/p&gt;
&lt;p&gt;模拟实现原子整数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
    if (atomicInteger.compareAndSwap(20)) {
        System.out.println(atomicInteger.getValue());
    }
}

class MyAtomicInteger {
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
    private volatile int value;

    static {
        try {
            //Unsafe unsafe = Unsafe.getUnsafe()这样会报错，需要反射获取
            Field theUnsafe = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
            // 获取 value 属性的内存地址，value 属性指向该地址，直接设置该地址的值可以修改 value 的值
            VALUE_OFFSET = UNSAFE.objectFieldOffset(
                		   MyAtomicInteger.class.getDeclaredField(&quot;value&quot;));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }

    public boolean compareAndSwap(int update) {
        while (true) {
            int prev = this.value;
            int next = update;
            //							当前对象  内存偏移量    期望值 更新值
            if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
                System.out.println(&quot;CAS成功&quot;);
                return true;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;final&lt;/h3&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class TestFinal {
	final int a = 20;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字节码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: aload_0
1: invokespecial #1 // Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
4: aload_0
5: bipush 20		// 将值直接放入栈中
7: putfield #2 		// Field a:I
&amp;lt;-- 写屏障
10: return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;final 变量的赋值通过 putfield 指令来完成，在这条指令之后也会加入写屏障，保证在其它线程读到它的值时不会出现为 0 的情况&lt;/p&gt;
&lt;p&gt;其他线程访问 final 修饰的变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复制一份放入栈中&lt;/strong&gt;直接访问，效率高&lt;/li&gt;
&lt;li&gt;大于 short 最大值会将其复制到类的常量池，访问时从常量池获取&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;不可变&lt;/h4&gt;
&lt;p&gt;不可变：如果一个对象不能够修改其内部状态（属性），那么就是不可变对象&lt;/p&gt;
&lt;p&gt;不可变对象线程安全的，不存在并发修改和可见性问题，是另一种避免竞争的方式&lt;/p&gt;
&lt;p&gt;String 类也是不可变的，该类和类中所有属性都是 final 的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类用 final 修饰保证了该类中的方法不能被覆盖，防止子类无意间破坏不可变性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无写入方法（set）确保外部不能对内部属性进行修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性用 final 修饰保证了该属性是只读的，不能修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class String
    implements java.io.Serializable, Comparable&amp;lt;String&amp;gt;, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更改 String 类数据时，会构造新字符串对象，生成新的 char[] value，通过&lt;strong&gt;创建副本对象来避免共享的方式称之为保护性拷贝&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;State&lt;/h3&gt;
&lt;p&gt;无状态：成员变量保存的数据也可以称为状态信息，无状态就是没有成员变量&lt;/p&gt;
&lt;p&gt;Servlet 为了保证其线程安全，一般不为 Servlet 设置成员变量，这种没有任何成员变量的类是线程安全的&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Local&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;ThreadLocal 类用来提供线程内部的局部变量，这种变量在多线程环境下访问（通过 get 和 set 方法访问）时能保证各个线程的变量相对独立于其他线程内的变量，分配在堆内的 &lt;strong&gt;TLAB&lt;/strong&gt; 中&lt;/p&gt;
&lt;p&gt;ThreadLocal 实例通常来说都是 &lt;code&gt;private static&lt;/code&gt; 类型的，属于一个线程的本地变量，用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据，所以是线程安全的&lt;/p&gt;
&lt;p&gt;ThreadLocal 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程并发：应用在多线程并发的场景下&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递数据：通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量，减少传递复杂度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程隔离：每个线程的变量都是独立的，不会互相影响&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 synchronized：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;synchronized&lt;/th&gt;
&lt;th&gt;ThreadLocal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原理&lt;/td&gt;
&lt;td&gt;同步机制采用&lt;strong&gt;以时间换空间&lt;/strong&gt;的方式，只提供了一份变量，让不同的线程排队访问&lt;/td&gt;
&lt;td&gt;ThreadLocal 采用&lt;strong&gt;以空间换时间&lt;/strong&gt;的方式，为每个线程都提供了一份变量的副本，从而实现同时访问而相不干扰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;侧重点&lt;/td&gt;
&lt;td&gt;多个线程之间访问资源的同步&lt;/td&gt;
&lt;td&gt;多线程中让每个线程之间的数据相互隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;h5&gt;常用方法&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ThreadLocal&amp;lt;&amp;gt;()&lt;/td&gt;
&lt;td&gt;创建 ThreadLocal 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;protected T initialValue()&lt;/td&gt;
&lt;td&gt;返回当前线程局部变量的初始值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void set( T value)&lt;/td&gt;
&lt;td&gt;设置当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public T get()&lt;/td&gt;
&lt;td&gt;获取当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void remove()&lt;/td&gt;
&lt;td&gt;移除当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class MyDemo {

    private static ThreadLocal&amp;lt;String&amp;gt; tl = new ThreadLocal&amp;lt;&amp;gt;();

    private String content;

    private String getContent() {
        // 获取当前线程绑定的变量
        return tl.get();
    }

    private void setContent(String content) {
        // 变量content绑定到当前线程
        tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i &amp;lt; 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 设置数据
                    demo.setContent(Thread.currentThread().getName() + &quot;的数据&quot;);
                    System.out.println(&quot;-----------------------&quot;);
                    System.out.println(Thread.currentThread().getName() + &quot;---&amp;gt;&quot; + demo.getContent());
                }
            });
            thread.setName(&quot;线程&quot; + i);
            thread.start();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;应用场景&lt;/h5&gt;
&lt;p&gt;ThreadLocal 适用于下面两种场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程需要有自己单独的实例&lt;/li&gt;
&lt;li&gt;实例需要在多个方法中共享，但不希望被多线程共享&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ThreadLocal 方案有两个突出的优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;传递数据：保存每个线程绑定的数据，在需要的地方可以直接获取，避免参数直接传递带来的代码耦合问题&lt;/li&gt;
&lt;li&gt;线程隔离：各线程之间的数据相互隔离却又具备并发性，避免同步方式带来的性能损失&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ThreadLocal 用于数据连接的事务管理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JdbcUtils {
    // ThreadLocal对象，将connection绑定在当前线程中
    private static final ThreadLocal&amp;lt;Connection&amp;gt; tl = new ThreadLocal();
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
            //如果没有，则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中，非常重要的操作
            tl.set(conn);
        }
        return conn;
    }
	// ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadLocalDateUtil {
    private static ThreadLocal&amp;lt;DateFormat&amp;gt; threadLocal = new ThreadLocal&amp;lt;DateFormat&amp;gt;() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;h5&gt;底层结构&lt;/h5&gt;
&lt;p&gt;JDK8 以前：每个 ThreadLocal 都创建一个 Map，然后用线程作为 Map 的 key，要存储的局部变量作为 Map 的 value，达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露，因为 Thread 停止后无法通过 key 删除对应的数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84JDK8%E5%89%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK8 以后：每个 Thread 维护一个 ThreadLocalMap，这个 Map 的 key 是 ThreadLocal 实例本身，value 是真正要存储的值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个 Thread 线程内部都有一个 Map (ThreadLocalMap)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Map 里面存储 ThreadLocal 对象（key）和线程的私有变量（value）&lt;/li&gt;
&lt;li&gt;Thread 内部的 Map 是由 ThreadLocal 维护的，由 ThreadLocal 负责向 map 获取和设置线程的变量值&lt;/li&gt;
&lt;li&gt;对于不同的线程，每次获取副本值时，别的线程并不能获取到当前线程的副本值，形成副本的隔离，互不干扰&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84JDK8%E5%90%8E.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK8 前后对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 Map 存储的 Entry 数量会变少，因为之前的存储数量由 Thread 的数量决定，现在由 ThreadLocal 的数量决定，在实际编程当中，往往 ThreadLocal 的数量要少于 Thread 的数量&lt;/li&gt;
&lt;li&gt;当 Thread 销毁之后，对应的 ThreadLocalMap 也会随之销毁，能减少内存的使用，&lt;strong&gt;防止内存泄露&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员变量&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Thread 类的相关属性：&lt;strong&gt;每一个线程持有一个 ThreadLocalMap 对象&lt;/strong&gt;，存放由 ThreadLocal 和数据组成的 Entry 键值对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadLocal.ThreadLocalMap threadLocals = null
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 ThreadLocal 对象的哈希值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int threadLocalHashCode = nextHashCode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;threadLocalHashCode &amp;amp; (table.length - 1)&lt;/code&gt; 计算当前 entry 需要存放的位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static AtomicInteger nextHashCode = new AtomicInteger()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;斐波那契数也叫黄金分割数，hash 的&lt;strong&gt;增量&lt;/strong&gt;就是这个数字，带来的好处是 hash 分布非常均匀：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int HASH_INCREMENT = 0x61c88647
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;方法都是线程安全的，因为 ThreadLocal 属于一个线程的，ThreadLocal 中的方法，逻辑都是获取当前线程维护的 ThreadLocalMap 对象，然后进行数据的增删改查，没有指定初始值的 threadlcoal 对象默认赋值为 null&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;initialValue()：返回该线程局部变量的初始值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;延迟调用的方法，在执行 get 方法时才执行&lt;/li&gt;
&lt;li&gt;该方法缺省（默认）实现直接返回一个 null&lt;/li&gt;
&lt;li&gt;如果想要一个初始值，可以重写此方法， 该方法是一个 &lt;code&gt;protected&lt;/code&gt; 的方法，为了让子类覆盖而设计的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected T initialValue() {
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nextHashCode()：计算哈希值，ThreadLocal 的散列方式称之为&lt;strong&gt;斐波那契散列&lt;/strong&gt;，每次获取哈希值都会加上 HASH_INCREMENT，这样做可以尽量避免 hash 冲突，让哈希值能均匀的分布在 2 的 n 次方的数组中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int nextHashCode() {
    // 哈希值自增一个 HASH_INCREMENT 数值
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;set()：修改当前线程与当前 threadlocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否存在
    if (map != null)
        // 调用 threadLocalMap.set 方法进行重写或者添加
        map.set(this, value);
    else
        // map 为空，调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程，参数2是局部变量
        createMap(t, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 获取当前线程 Thread 对应维护的 ThreadLocalMap 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 创建当前线程Thread对应维护的ThreadLocalMap 
void createMap(Thread t, T firstValue) {
    // 【这里的 this 是调用此方法的 threadLocal】，创建一个新的 Map 并设置第一个数据
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get()：获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
        // 以当前的 ThreadLocal 为 key，调用 getEntry 获取对应的存储实体 e
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 对 e 进行判空 
        if (e != null) {
            // 获取存储实体 e 对应的 value值
            T result = (T)e.value;
            return result;
        }
    }
    /*有两种情况有执行当前代码
      第一种情况: map 不存在，表示此线程没有维护的 ThreadLocalMap 对象
      第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】，就会设置为默认值 */
    // 初始化当前线程与当前 threadLocal 对象相关联的 value
    return setInitialValue();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private T setInitialValue() {
    // 调用initialValue获取初始化的值，此方法可以被子类重写, 如果不重写默认返回 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否初始化过
    if (map != null)
        // 存在则调用 map.set 设置此实体 entry，value 是默认的值
        map.set(this, value);
    else
        // 调用 createMap 进行 ThreadLocalMap 对象的初始化中
        createMap(t, value);
    // 返回线程与当前 threadLocal 关联的局部变量
    return value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：移除当前线程与当前 threadLocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void remove() {
    // 获取当前线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // map 存在则调用 map.remove，this时当前ThreadLocal，以this为key删除对应的实体
        m.remove(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;LocalMap&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ThreadLocalMap 是 ThreadLocal 的内部类，没有实现 Map 接口，用独立的方式实现了 Map 的功能，其内部 Entry 也是独立实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;

// 存放数据的table，数组长度必须是2的整次幂。
private Entry[] table;

// 数组里面 entrys 的个数，可以用于判断 table 当前使用量是否超过阈值
private int size = 0;

// 进行扩容的阈值，表使用量大于它的时候进行扩容。
private int threshold;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存储结构 Entry：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Entry 继承 WeakReference，key 是弱引用，目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑&lt;/li&gt;
&lt;li&gt;Entry 限制只能用 ThreadLocal 作为 key，key 为 null (entry.get() == null) 意味着 key 不再被引用，entry 也可以从 table 中清除&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static class Entry extends WeakReference&amp;lt;ThreadLocal&amp;lt;?&amp;gt;&amp;gt; {
    Object value;
    Entry(ThreadLocal&amp;lt;?&amp;gt; k, Object v) {
        // this.referent = referent = key;
        super(k);
        value = v;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：延迟初始化的，线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadLocalMap(ThreadLocal&amp;lt;?&amp;gt; firstKey, Object firstValue) {
    // 初始化table，创建一个长度为16的Entry数组
    table = new Entry[INITIAL_CAPACITY];
    // 【寻址算法】计算索引
    int i = firstKey.threadLocalHashCode &amp;amp; (INITIAL_CAPACITY - 1);
    // 创建 entry 对象，存放到指定位置的 slot 中
    table[i] = new Entry(firstKey, firstValue);
    // 数据总量是 1
    size = 1;
    // 将阈值设置为 （当前数组长度 * 2）/ 3。
    setThreshold(INITIAL_CAPACITY);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;set()：添加数据，ThreadLocalMap 使用&lt;strong&gt;线性探测法来解决哈希冲突&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该方法会一直探测下一个地址，直到有空的地址后插入，若插入后 Map 数量超过阈值，数组会扩容为原来的 2 倍&lt;/p&gt;
&lt;p&gt;假设当前 table 长度为16，计算出来 key 的 hash 值为 14，如果 table[14] 上已经有值，并且其 key 与当前 key 不一致，那么就发生了 hash 冲突，这个时候将 14 加 1 得到 15，取 table[15] 进行判断，如果还是冲突会回到 0，取 table[0]，以此类推，直到可以插入，可以把 Entry[]  table 看成一个&lt;strong&gt;环形数组&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线性探测法会出现&lt;strong&gt;堆积问题&lt;/strong&gt;，可以采取平方探测法解决&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象，并进行垃圾清理，防止出现内存泄漏&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private void set(ThreadLocal&amp;lt;?&amp;gt; key, Object value) {
    // 获取散列表
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (len-1);
    // 使用线性探测法向后查找元素，碰到 entry 为空时停止探测
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取当前元素 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // ThreadLocal 对应的 key 存在，【直接覆盖之前的值】
        if (k == key) {
            e.value = value;
            return;
        }
        // 【这两个条件谁先成立不一定，所以 replaceStaleEntry 中还需要判断 k == key 的情况】
        
        // key 为 null，但是值不为 null，说明之前的 ThreadLocal 对象已经被回收了，当前是【过期数据】
        if (k == null) {
            // 【碰到一个过期的 slot，当前数据复用该槽位，替换过期数据】
            // 这个方法还进行了垃圾清理动作，防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 逻辑到这说明碰到 slot == null 的位置，则在空元素的位置创建一个新的 Entry
    tab[i] = new Entry(key, value);
    // 数量 + 1
    int sz = ++size;
    
    // 【做一次启发式清理】，如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义，那么进行 rehash
    if (!cleanSomeSlots(i, sz) &amp;amp;&amp;amp; sz &amp;gt;= threshold)
        // 扩容
        rehash();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 获取【环形数组】的下一个索引
private static int nextIndex(int i, int len) {
    // 索引越界后从 0 开始继续获取
    return ((i + 1 &amp;lt; len) ? i + 1 : 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 在指定位置插入指定的数据
private void replaceStaleEntry(ThreadLocal&amp;lt;?&amp;gt; key, Object value, int staleSlot) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
	// 探测式清理的开始下标，默认从当前 staleSlot 开始
    int slotToExpunge = staleSlot;
    // 以当前 staleSlot 开始【向前迭代查找】，找到索引靠前过期数据，找到以后替换 slotToExpunge 值
    // 【保证在一个区间段内，从最前面的过期数据开始清理】
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

	// 以 staleSlot 【向后去查找】，直到碰到 null 为止，还是线性探测
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 获取当前节点的 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
		// 条件成立说明是【替换逻辑】
        if (k == key) {
            e.value = value;
            // 因为本来要在 staleSlot 索引处插入该数据，现在找到了i索引处的key与数据一致
            // 但是 i 位置距离正确的位置更远，因为是向后查找，所以还是要在 staleSlot 位置插入当前 entry
            // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置，
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
			
            // 条件成立说明向前查找过期数据并未找到过期的 entry，但 staleSlot 位置已经不是过期数据了，i 位置才是
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            
            // 【清理过期数据，expungeStaleEntry 探测式清理，cleanSomeSlots 启发式清理】
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
		// 条件成立说明当前遍历的 entry 是一个过期数据，并且该位置前面也没有过期数据
        if (k == null &amp;amp;&amp;amp; slotToExpunge == staleSlot)
            // 探测式清理过期数据的开始下标修改为当前循环的 index，因为 staleSlot 会放入要添加的数据
            slotToExpunge = i;
    }
	// 向后查找过程中并未发现 k == key 的 entry，说明当前是一个【取代过期数据逻辑】
    // 删除原有的数据引用，防止内存泄露
    tab[staleSlot].value = null;
    // staleSlot 位置添加数据，【上面的所有逻辑都不会更改 staleSlot 的值】
    tab[staleSlot] = new Entry(key, value);

    // 条件成立说明除了 staleSlot 以外，还发现其它的过期 slot，所以要【开启清理数据的逻辑】
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int prevIndex(int i, int len) {
    // 形成一个环绕式的访问，头索引越界后置为尾索引
    return ((i - 1 &amp;gt;= 0) ? i - 1 : len - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getEntry()：ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key，调用 getEntry 获取对应的存储实体 e&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Entry getEntry(ThreadLocal&amp;lt;?&amp;gt; key) {
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (table.length - 1);
    // 访问散列表中指定指定位置的 slot 
    Entry e = table[i];
    // 条件成立，说明 slot 有值并且 key 就是要寻找的 key，直接返回
    if (e != null &amp;amp;&amp;amp; e.get() == key)
        return e;
    else
        // 进行线性探测
        return getEntryAfterMiss(key, i, e);
}
// 线性探测寻址
private Entry getEntryAfterMiss(ThreadLocal&amp;lt;?&amp;gt; key, int i, Entry e) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;

    // 开始遍历，碰到 slot == null 的情况，搜索结束
    while (e != null) {
		// 获取当前 slot 中 entry 对象的 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // 条件成立说明找到了，直接返回
        if (k == key)
            return e;
        if (k == null)
             // 过期数据，【探测式过期数据回收】
            expungeStaleEntry(i);
        else
            // 更新 index 继续向后走
            i = nextIndex(i, len);
        // 获取下一个槽位中的 entry
        e = tab[i];
    }
    // 说明当前区段没有找到相应数据
    // 【因为存放数据是线性的向后寻找槽位，都是紧挨着的，不可能越过一个 空槽位 在后面放】，可以减少遍历的次数
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rehash()：触发一次全量清理，如果数组长度大于等于长度的 &lt;code&gt;2/3 * 3/4 = 1/2&lt;/code&gt;，则进行 resize&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void rehash() {
    // 清楚当前散列表内的【所有】过期的数据
    expungeStaleEntries();
    
    // threshold = len * 2 / 3，就是 2/3 * (1 - 1/4)
    if (size &amp;gt;= threshold - threshold / 4)
        resize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    // 【遍历所有的槽位，清理过期数据】
    for (int j = 0; j &amp;lt; len; j++) {
        Entry e = tab[j];
        if (e != null &amp;amp;&amp;amp; e.get() == null)
            expungeStaleEntry(j);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entry &lt;strong&gt;数组为扩容为原来的 2 倍&lt;/strong&gt; ，重新计算 key 的散列值，如果遇到 key 为 null 的情况，会将其 value 也置为 null，帮助 GC&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新数组的长度是老数组的二倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    // 统计新table中的entry数量
    int count = 0;
	// 遍历老表，进行【数据迁移】
    for (int j = 0; j &amp;lt; oldLen; ++j) {
        // 访问老表的指定位置的 entry
        Entry e = oldTab[j];
        // 条件成立说明老表中该位置有数据，可能是过期数据也可能不是
        if (e != null) {
            ThreadLocal&amp;lt;?&amp;gt; k = e.get();
            // 过期数据
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 非过期数据，在新表中进行哈希寻址
                int h = k.threadLocalHashCode &amp;amp; (newLen - 1);
                // 【线程探测】
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 将数据存放到新表合适的 slot 中
                newTab[h] = e;
                count++;
            }
        }
    }
	// 设置下一次触发扩容的指标：threshold = len * 2 / 3;
    setThreshold(newLen);
    size = count;
    // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用
    table = newTab;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：删除 Entry&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void remove(ThreadLocal&amp;lt;?&amp;gt; key) {
    Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 找到了对应的 key
        if (e.get() == key) {
            // 设置 key 为 null
            e.clear();
            // 探测式清理
            expungeStaleEntry(i);
            return;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;清理方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;探测式清理：沿着开始位置向后探测清理过期数据，沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位，重定位后的元素理论上更接近 &lt;code&gt;i = entry.key &amp;amp; (table.length - 1)&lt;/code&gt;，让&lt;strong&gt;数据的排列更紧凑&lt;/strong&gt;，会优化整个散列表查询性能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// table[staleSlot] 是一个过期数据，以这个位置开始继续向后查找过期数据
private int expungeStaleEntry(int staleSlot) {
    // 获取散列表和数组长度
    Entry[] tab = table;
    int len = tab.length;

    // help gc，先把当前过期的 entry 置空，在取消对 entry 的引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 数量-1
    size--;

    Entry e;
    int i;
    // 从 staleSlot 开始向后遍历，直到碰到 slot == null 结束，【区间内清理过期数据】
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // 当前 entry 是过期数据
        if (k == null) {
            // help gc
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 当前 entry 不是过期数据的逻辑，【rehash】
            // 重新计算当前 entry 对应的 index
            int h = k.threadLocalHashCode &amp;amp; (len - 1);
            // 条件成立说明当前 entry 存储时发生过 hash 冲突，向后偏移过了
            if (h != i) {
                // 当前位置置空
                tab[i] = null;
                // 以正确位置 h 开始，向后查找第一个可以存放 entry 的位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将当前元素放入到【距离正确位置更近的位置，有可能就是正确位置】
                tab[h] = e;
            }
        }
    }
    // 返回 slot = null 的槽位索引，图例是 7，这个索引代表【索引前面的区间已经清理完成垃圾了】
    return i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal探测式清理1.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal探测式清理2.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启发式清理：向后循环扫描过期数据，发现过期数据调用探测式清理方法，如果连续几次的循环都没有发现过期数据，就停止扫描&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//  i 表示启发式清理工作开始位置，一般是空 slot，n 一般传递的是 table.length 
private boolean cleanSomeSlots(int i, int n) {
    // 表示启发式清理工作是否清除了过期数据
    boolean removed = false;
    // 获取当前 map 的散列表引用
    Entry[] tab = table;
    int len = tab.length;
    do {
        // 获取下一个索引，因为探测式返回的 slot 为 null
        i = nextIndex(i, len);
        Entry e = tab[i];
        // 条件成立说明是过期的数据，key 被 gc 了
        if (e != null &amp;amp;&amp;amp; e.get() == null) {
            // 【发现过期数据重置 n 为数组的长度】
            n = len;
            // 表示清理过过期数据
            removed = true;
            // 以当前过期的 slot 为开始节点 做一次探测式清理工作
            i = expungeStaleEntry(i);
        }
        // 假设 table 长度为 16
        // 16 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 8，8 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 4，4 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 2，2 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 1，1 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 0
        // 连续经过这么多次循环【没有扫描到过期数据】，就停止循环，扫描到空 slot 不算，因为不是过期数据
    } while ((n &amp;gt;&amp;gt;&amp;gt;= 1) != 0);
    
    // 返回清除标记
    return removed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/457326371/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存泄漏&lt;/h4&gt;
&lt;p&gt;Memory leak：内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放，造成系统内存的浪费，导致程序运行速度减慢甚至系统崩溃等严重后果，内存泄漏的堆积终将导致内存溢出&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 key 使用强引用：使用完 ThreadLocal ，threadLocal Ref 被回收，但是 threadLocalMap 的 Entry 强引用了 threadLocal，造成 threadLocal 无法被回收，无法完全避免内存泄漏&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal内存泄漏强引用.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 key 使用弱引用：使用完 ThreadLocal ，threadLocal Ref 被回收，ThreadLocalMap 只持有 ThreadLocal 的弱引用，所以threadlocal 也可以被回收，此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行，依然存在强引用链，value 不会被回收，而这块 value 永远不会被访问到，也会导致 value 内存泄漏&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal内存泄漏弱引用.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两个主要原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有手动删除这个 Entry&lt;/li&gt;
&lt;li&gt;CurrentThread 依然运行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根本原因：ThreadLocalMap 是 Thread的一个属性，&lt;strong&gt;生命周期跟 Thread 一样长&lt;/strong&gt;，如果没有手动删除对应 Entry 就会导致内存泄漏&lt;/p&gt;
&lt;p&gt;解决方法：使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以&lt;/p&gt;
&lt;p&gt;ThreadLocal 内部解决方法：在 ThreadLocalMap 中的 set/getEntry 方法中，通过线性探测法对 key 进行判断，如果 key 为 null（ThreadLocal 为 null）会对 Entry 进行垃圾回收。所以&lt;strong&gt;使用弱引用比强引用多一层保障&lt;/strong&gt;，就算不调用 remove，也有机会进行 GC&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;变量传递&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;父子线程：创建子线程的线程是父线程，比如实例中的 main 线程就是父线程&lt;/p&gt;
&lt;p&gt;ThreadLocal 中存储的是线程的局部变量，如果想&lt;strong&gt;实现线程间局部变量传递&lt;/strong&gt;可以使用 InheritableThreadLocal 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ThreadLocal&amp;lt;String&amp;gt; threadLocal = new InheritableThreadLocal&amp;lt;&amp;gt;();
    threadLocal.set(&quot;父线程设置的值&quot;);

    new Thread(() -&amp;gt; System.out.println(&quot;子线程输出：&quot; + threadLocal.get())).start();
}
// 子线程输出：父线程设置的值
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;p&gt;InheritableThreadLocal 源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InheritableThreadLocal&amp;lt;T&amp;gt; extends ThreadLocal&amp;lt;T&amp;gt; {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
                  // 该参数默认是 true
                  boolean inheritThreadLocals) {
  	// ...
    Thread parent = currentThread();

    // 判断父线程（创建子线程的线程）的 inheritableThreadLocals 属性不为 null
    if (inheritThreadLocals &amp;amp;&amp;amp; parent.inheritableThreadLocals != null) {
        // 复制父线程的 inheritableThreadLocals 属性，实现父子线程局部变量共享
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
    }
    // ..
}
// 【本质上还是创建 ThreadLocalMap，只是把父类中的可继承数据设置进去了】
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 获取父线程的哈希表
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
	// 【逐个复制父线程 ThreadLocalMap 中的数据】
    for (int j = 0; j &amp;lt; len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            ThreadLocal&amp;lt;Object&amp;gt; key = (ThreadLocal&amp;lt;Object&amp;gt;) e.get();
            if (key != null) {
                // 调用的是 InheritableThreadLocal#childValue(T parentValue)
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode &amp;amp; (len - 1);
                // 线性探测
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/feichitianxia/article/details/110495764&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;线程池&lt;/h2&gt;
&lt;h3&gt;基本概述&lt;/h3&gt;
&lt;p&gt;线程池：一个容纳多个线程的容器，容器中的线程可以重复使用，省去了频繁创建和销毁线程对象的操作&lt;/p&gt;
&lt;p&gt;线程池作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;降低资源消耗，减少了创建和销毁线程的次数，每个工作线程都可以被重复利用，可执行多个任务&lt;/li&gt;
&lt;li&gt;提高响应速度，当任务到达时，如果有线程可以直接用，不会出现系统僵死&lt;/li&gt;
&lt;li&gt;提高线程的可管理性，如果无限制的创建线程，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;线程池的核心思想：&lt;strong&gt;线程复用&lt;/strong&gt;，同一个线程可以被重复使用，来处理多个任务&lt;/p&gt;
&lt;p&gt;池化技术 (Pool) ：一种编程技巧，核心思想是资源复用，在请求量大时能优化应用性能，降低系统频繁建连的资源开销&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;阻塞队列&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;有界队列和无界队列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有界队列：有固定大小的队列，比如设定了固定大小的 LinkedBlockingQueue，又或者大小为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无界队列：没有设置固定大小的队列，这些队列可以直接入队，直到溢出（超过 Integer.MAX_VALUE），所以相当于无界&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现：&lt;strong&gt;FIFO 队列&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ArrayBlockQueue：由数组结构组成的有界阻塞队列&lt;/li&gt;
&lt;li&gt;LinkedBlockingQueue：由链表结构组成的无界（默认大小 Integer.MAX_VALUE）的阻塞队列&lt;/li&gt;
&lt;li&gt;PriorityBlockQueue：支持优先级排序的无界阻塞队列&lt;/li&gt;
&lt;li&gt;DelayedWorkQueue：使用优先级队列实现的延迟无界阻塞队列&lt;/li&gt;
&lt;li&gt;SynchronousQueue：不存储元素的阻塞队列，每一个生产线程会阻塞到有一个 put 的线程放入元素为止&lt;/li&gt;
&lt;li&gt;LinkedTransferQueue：由链表结构组成的无界阻塞队列&lt;/li&gt;
&lt;li&gt;LinkedBlockingDeque：由链表结构组成的&lt;strong&gt;双向&lt;/strong&gt;阻塞队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与普通队列（LinkedList、ArrayList等）的不同点在于阻塞队列中阻塞添加和阻塞删除方法，以及线程安全：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞添加 put()：当阻塞队列元素已满时，添加队列元素的线程会被阻塞，直到队列元素不满时才重新唤醒线程执行&lt;/li&gt;
&lt;li&gt;阻塞删除 take()：在队列元素为空时，删除队列元素的线程将被阻塞，直到队列不为空再执行删除操作（一般会返回被删除的元素)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;核心方法&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法类型&lt;/th&gt;
&lt;th&gt;抛出异常&lt;/th&gt;
&lt;th&gt;特殊值&lt;/th&gt;
&lt;th&gt;阻塞&lt;/th&gt;
&lt;th&gt;超时&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;插入（尾）&lt;/td&gt;
&lt;td&gt;add(e)&lt;/td&gt;
&lt;td&gt;offer(e)&lt;/td&gt;
&lt;td&gt;put(e)&lt;/td&gt;
&lt;td&gt;offer(e,time,unit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移除（头）&lt;/td&gt;
&lt;td&gt;remove()&lt;/td&gt;
&lt;td&gt;poll()&lt;/td&gt;
&lt;td&gt;take()&lt;/td&gt;
&lt;td&gt;poll(time,unit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;检查（队首元素）&lt;/td&gt;
&lt;td&gt;element()&lt;/td&gt;
&lt;td&gt;peek()&lt;/td&gt;
&lt;td&gt;不可用&lt;/td&gt;
&lt;td&gt;不可用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;抛出异常组：
&lt;ul&gt;
&lt;li&gt;当阻塞队列满时：在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full&lt;/li&gt;
&lt;li&gt;当阻塞队列空时：再往队列中 remove 移除元素，会抛出 NoSuchException&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊值组：
&lt;ul&gt;
&lt;li&gt;插入方法：成功 true，失败 false&lt;/li&gt;
&lt;li&gt;移除方法：成功返回出队列元素，队列没有就返回 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阻塞组：
&lt;ul&gt;
&lt;li&gt;当阻塞队列满时，生产者继续往队列里 put 元素，队列会一直阻塞生产线程直到队列有空间 put 数据或响应中断退出&lt;/li&gt;
&lt;li&gt;当阻塞队列空时，消费者线程试图从队列里 take 元素，队列会一直阻塞消费者线程直到队列中有可用元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;超时退出：当阻塞队列满时，队里会阻塞生产者线程一定时间，超过限时后生产者线程会退出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;链表队列&lt;/h4&gt;
&lt;h5&gt;入队出队&lt;/h5&gt;
&lt;p&gt;LinkedBlockingQueue 源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LinkedBlockingQueue&amp;lt;E&amp;gt; extends AbstractQueue&amp;lt;E&amp;gt;
			implements BlockingQueue&amp;lt;E&amp;gt;, java.io.Serializable {
	static class Node&amp;lt;E&amp;gt; {
        E item;
        /**
        * 下列三种情况之一
        * - 真正的后继节点
        * - 自己, 发生在出队时
        * - null, 表示是没有后继节点, 是尾节点了
        */
        Node&amp;lt;E&amp;gt; next;

        Node(E x) { item = x; }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;入队：&lt;strong&gt;尾插法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始化链表 &lt;code&gt;last = head = new Node&amp;lt;E&amp;gt;(null)&lt;/code&gt;，&lt;strong&gt;Dummy 节点用来占位&lt;/strong&gt;，item 为 null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public LinkedBlockingQueue(int capacity) {
    // 默认是 Integer.MAX_VALUE
    if (capacity &amp;lt;= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node&amp;lt;E&amp;gt;(null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个节点入队：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void enqueue(Node&amp;lt;E&amp;gt; node) {
    // 从右向左计算
    last = last.next = node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%85%A5%E9%98%9F%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再来一个节点入队 &lt;code&gt;last = last.next = node&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;出队：&lt;strong&gt;出队头节点&lt;/strong&gt;，FIFO&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;出队源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private E dequeue() {
    Node&amp;lt;E&amp;gt; h = head;
    // 获取临头节点
    Node&amp;lt;E&amp;gt; first = h.next;
    // 自己指向自己，help GC
    h.next = h;
    head = first;
    // 出队的元素
    E x = first.item;
    // 【当前节点置为 Dummy 节点】
    first.item = null;
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;h = head&lt;/code&gt; → &lt;code&gt;first = h.next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%87%BA%E9%98%9F%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;h.next = h&lt;/code&gt; → &lt;code&gt;head = first&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%87%BA%E9%98%9F%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;first.item = null&lt;/code&gt;：当前节点置为 Dummy 节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;加锁分析&lt;/h5&gt;
&lt;p&gt;用了两把锁和 dummy 节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用一把锁，同一时刻，最多只允许有一个线程（生产者或消费者，二选一）执行&lt;/li&gt;
&lt;li&gt;用两把锁，同一时刻，可以允许两个线程同时（一个生产者与一个消费者）执行
&lt;ul&gt;
&lt;li&gt;消费者与消费者线程仍然串行&lt;/li&gt;
&lt;li&gt;生产者与生产者线程仍然串行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程安全分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当节点总数大于 2 时（包括 dummy 节点），&lt;strong&gt;putLock 保证的是 last 节点的线程安全，takeLock 保证的是 head 节点的线程安全&lt;/strong&gt;，两把锁保证了入队和出队没有竞争&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当节点总数等于 2 时（即一个 dummy 节点，一个正常节点）这时候，仍然是两把锁锁两个对象，不会竞争&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当节点总数等于 1 时（就一个 dummy 节点）这时 take 线程会被 notEmpty 条件阻塞，有竞争，会阻塞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();	// 阻塞等待不满，说明已经满了

// 用于 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();	// 阻塞等待不空，说明已经是空的
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;入队出队：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;put 操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void put(E e) throws InterruptedException {
    // 空指针异常
    if (e == null) throw new NullPointerException();
    int c = -1;
    // 把待添加的元素封装为 node 节点
    Node&amp;lt;E&amp;gt; node = new Node&amp;lt;E&amp;gt;(e);
    // 获取全局生产锁
    final ReentrantLock putLock = this.putLock;
    // count 用来维护元素计数
    final AtomicInteger count = this.count;
    // 获取可打断锁，会抛出异常
    putLock.lockInterruptibly();
    try {
    	// 队列满了等待
        while (count.get() == capacity) {
            // 【等待队列不满时，就可以生产数据】，线程处于 Waiting
            notFull.await();
        }
        // 有空位, 入队且计数加一，尾插法
        enqueue(node);
        // 返回自增前的数字
        c = count.getAndIncrement();
        // put 完队列还有空位, 唤醒其他生产 put 线程，唤醒一个减少竞争
        if (c + 1 &amp;lt; capacity)
            notFull.signal();
    } finally {
        // 解锁
        putLock.unlock();
    }
    // c自增前是0，说明生产了一个元素，唤醒一个 take 线程
    if (c == 0)
        signalNotEmpty();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 调用 notEmpty.signal()，而不是 notEmpty.signalAll() 是为了减少竞争，因为只剩下一个元素
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;take 操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public E take() throws InterruptedException {
    E x;
    int c = -1;
    // 元素个数
    final AtomicInteger count = this.count;
    // 获取全局消费锁
    final ReentrantLock takeLock = this.takeLock;
    // 可打断锁
    takeLock.lockInterruptibly();
    try {
        // 没有元素可以出队
        while (count.get() == 0) {
            // 【阻塞等待队列不空，就可以消费数据】，线程处于 Waiting
            notEmpty.await();
        }
        // 出队，计数减一，FIFO，出队头节点
        x = dequeue();
        // 返回自减前的数字
        c = count.getAndDecrement();
        // 队列还有元素
        if (c &amp;gt; 1)
            // 唤醒一个消费take线程
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    // c 是消费前的数据，消费前满了，消费一个后还剩一个空位，唤醒生产线程
    if (c == capacity)
        // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
        signalNotFull();
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;性能比较&lt;/h5&gt;
&lt;p&gt;主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linked 支持有界，Array 强制有界&lt;/li&gt;
&lt;li&gt;Linked 实现是链表，Array 实现是数组&lt;/li&gt;
&lt;li&gt;Linked 是懒惰的，而 Array 需要提前初始化 Node 数组&lt;/li&gt;
&lt;li&gt;Linked 每次入队会生成新 Node，而 Array 的 Node 是提前创建好的&lt;/li&gt;
&lt;li&gt;Linked 两把锁，Array 一把锁&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;同步队列&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;SynchronousQueue 是一个不存储元素的 BlockingQueue，&lt;strong&gt;每一个生产者必须阻塞匹配到一个消费者&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;运行当前程序的平台拥有 CPU 的数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int NCPUS = Runtime.getRuntime().availableProcessors()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定超时时间后，当前线程最大自旋次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只有一个 CPU 时自旋次数为 0，所有程序都是串行执行，多核 CPU 时自旋 32 次是一个经验值
static final int maxTimedSpins = (NCPUS &amp;lt; 2) ? 0 : 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自旋的原因：线程挂起唤醒需要进行上下文切换，涉及到用户态和内核态的转变，是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到，如果自旋期间被匹配到，那么直接就返回了，如果自旋次数达到某个指标后，还是会将当前线程挂起&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未指定超时时间，当前线程最大自旋次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int maxUntimedSpins = maxTimedSpins * 16;	// maxTimedSpins 的 16 倍
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定超时限制的阈值，小于该值的线程不会被挂起：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final long spinForTimeoutThreshold = 1000L;	// 纳秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超时时间设置的小于该值，就会被禁止挂起，阻塞再唤醒的成本太高，不如选择自旋空转&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile Transferer&amp;lt;E&amp;gt; transferer;
abstract static class Transferer&amp;lt;E&amp;gt; {
    /**
    * 参数一：可以为 null，null 时表示这个请求是一个 REQUEST 类型的请求，反之是一个 DATA 类型的请求
    * 参数二：如果为 true 表示指定了超时时间，如果为 false 表示不支持超时，会一直阻塞到匹配或者被打断
    * 参数三：超时时间限制，单位是纳秒
    
    * 返回值：返回值如果不为 null 表示匹配成功，DATA 类型的请求返回当前线程 put 的数据
    * 	     如果返回 null，表示请求超时或被中断
    */
    abstract E transfer(E e, boolean timed, long nanos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SynchronousQueue(boolean fair) {
    // fair 默认 false
    // 非公平模式实现的数据结构是栈，公平模式的数据结构是队列
    transferer = fair ? new TransferQueue&amp;lt;E&amp;gt;() : new TransferStack&amp;lt;E&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    return transferer.transfer(e, true, 0) != null;
}
public E poll() {
    return transferer.transfer(null, true, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;非公实现&lt;/h5&gt;
&lt;p&gt;TransferStack 是非公平的同步队列，因为所有的请求都被压入栈中，栈顶的元素会最先得到匹配，造成栈底的等待线程饥饿&lt;/p&gt;
&lt;p&gt;TransferStack 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示 Node 类型为请求类型
static final int REQUEST    = 0;
// 表示 Node类 型为数据类型
static final int DATA       = 1;
// 表示 Node 类型为匹配中类型
// 假设栈顶元素为 REQUEST-NODE，当前请求类型为 DATA，入栈会修改类型为 FULFILLING 【栈顶 &amp;amp; 栈顶之下的一个node】
// 假设栈顶元素为 DATA-NODE，当前请求类型为 REQUEST，入栈会修改类型为 FULFILLING 【栈顶 &amp;amp; 栈顶之下的一个node】
static final int FULFILLING = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈顶元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile SNode head;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内部类 SNode：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class SNode {
    // 指向下一个栈帧
    volatile SNode next; 
    // 与当前 node 匹配的节点
    volatile SNode match;
    // 假设当前node对应的线程自旋期间未被匹配成功，那么node对应的线程需要挂起，
    // 挂起前 waiter 保存对应的线程引用，方便匹配成功后，被唤醒。
    volatile Thread waiter;
    
    // 数据域，不为空表示当前 Node 对应的请求类型为 DATA 类型，反之则表示 Node 为 REQUEST 类型
    Object item; 
    // 表示当前Node的模式 【DATA/REQUEST/FULFILLING】
    int mode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SNode(Object item) {
    this.item = item;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置方法：设置 Node 对象的 next 字段，此处&lt;strong&gt;对 CAS 进行了优化&lt;/strong&gt;，提升了 CAS 的效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean casNext(SNode cmp, SNode val) {
    //【优化：cmp == next】，可以提升一部分性能。 cmp == next 不相等，就没必要走 cas指令。
    return cmp == next &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匹配方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean tryMatch(SNode s) {
    // 当前 node 尚未与任何节点发生过匹配，CAS 设置 match 字段为 s 节点，表示当前 node 已经被匹配
    if (match == null &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
        // 当前 node 如果自旋结束，会 park 阻塞，阻塞前将 node 对应的 Thread 保留到 waiter 字段
        // 获取当前 node 对应的阻塞线程
        Thread w = waiter;
        // 条件成立说明 node 对应的 Thread 正在阻塞
        if (w != null) {
            waiter = null;
            // 使用 unpark 方式唤醒线程
            LockSupport.unpark(w);
        }
        return true;
    }
    // 匹配成功返回 true
    return match == s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 取消节点的方法
void tryCancel() {
    // match 字段指向自己，表示这个 node 是取消状态，取消状态的 node，最终会被强制移除出栈
    UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}

boolean isCancelled() {
    return match == this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferStack 类成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;snode()：填充节点方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static SNode snode(SNode s, Object e, SNode next, int mode) {
    // 引用指向空时，snode 方法会创建一个 SNode 对象 
    if (s == null) s = new SNode(e);
    // 填充数据
    s.mode = mode;
    s.next = next;
    return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;transfer()：核心方法，请求匹配出栈，不匹配阻塞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E transfer(E e, boolean timed, long nanos) {
	// 包装当前线程的 node
    SNode s = null;
    // 根据元素判断当前的请求类型
    int mode = (e == null) ? REQUEST : DATA;
	// 自旋
    for (;;) {
        // 获取栈顶指针
        SNode h = head;
       // 【CASE1】：当前栈为空或者栈顶 node 模式与当前请求模式一致无法匹配，做入栈操作
        if (h == null || h.mode == mode) {
            // 当前请求是支持超时的，但是 nanos &amp;lt;= 0 说明这个请求不支持 “阻塞等待”
            if (timed &amp;amp;&amp;amp; nanos &amp;lt;= 0) { 
                // 栈顶元素是取消状态
                if (h != null &amp;amp;&amp;amp; h.isCancelled())
                    // 栈顶出栈，设置新的栈顶
                    casHead(h, h.next);
                else
                    // 表示【匹配失败】
                    return null;
            // 入栈
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                // 等待被匹配的逻辑，正常情况返回匹配的节点；取消情况返回当前节点，就是 s
                SNode m = awaitFulfill(s, timed, nanos);
                // 说明当前 node 是【取消状态】
                if (m == s) { 
                    // 将取消节点出栈
                    clean(s);
                    return null;
                }
                // 执行到这说明【匹配成功】了
                // 栈顶有节点并且 匹配节点还未出栈，需要协助出栈
                if ((h = head) != null &amp;amp;&amp;amp; h.next == s)
                    casHead(h, s.next);
                // 当前 node 模式为 REQUEST 类型，返回匹配节点的 m.item 数据域
                // 当前 node 模式为 DATA 类型：返回 node.item 数据域，当前请求提交的数据 e
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        // 【CASE2】：逻辑到这说明请求模式不一致，如果栈顶不是 FULFILLING 说明没被其他节点匹配，【当前可以匹配】
        } else if (!isFulfilling(h.mode)) {
            // 头节点是取消节点，match 指向自己，协助出栈
            if (h.isCancelled())
                casHead(h, h.next);
            // 入栈当前请求的节点
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { 
                    // m 是 s 的匹配的节点
                    SNode m = s.next;
                    // m 节点在 awaitFulfill 方法中被中断，clean 了自己
                    if (m == null) {
                        // 清空栈
                        casHead(s, null);
                        s = null;
                        // 返回到外层自旋中
                        break;
                    }
                    // 获取匹配节点的下一个节点
                    SNode mn = m.next;
                    // 尝试匹配，【匹配成功】，则将 fulfilling 和 m 一起出栈，并且唤醒被匹配的节点的线程
                    if (m.tryMatch(s)) {
                        casHead(s, mn);
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else
                        // 匹配失败，出栈 m
                        s.casNext(m, mn);
                }
            }
        // 【CASE3】：栈顶模式为 FULFILLING 模式，表示【栈顶和栈顶下面的节点正在发生匹配】，当前请求需要做协助工作
        } else {
            // h 表示的是 fulfilling 节点，m 表示 fulfilling 匹配的节点
            SNode m = h.next;
            if (m == null)
                // 清空栈
                casHead(h, null);
            else {
                SNode mn = m.next;
                // m 和 h 匹配，唤醒 m 中的线程
                if (m.tryMatch(h))
                    casHead(h, mn);
                else
                    h.casNext(m, mn);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;awaitFulfill()：阻塞当前线程等待被匹配，返回匹配的节点，或者被取消的节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    // 等待的截止时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 当前线程
    Thread w = Thread.currentThread();
    // 表示当前请求线程在下面的 for(;;) 自旋检查的次数
    int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    // 自旋检查逻辑：是否匹配、是否超时、是否被中断
    for (;;) {
        // 当前线程收到中断信号，需要设置 node 状态为取消状态
        if (w.isInterrupted())
            s.tryCancel();
        // 获取与当前 s 匹配的节点
        SNode m = s.match;
        if (m != null)
            // 可能是正常的匹配的，也可能是取消的
            return m;
        // 执行了超时限制就判断是否超时
        if (timed) {
            nanos = deadline - System.nanoTime();
            // 【超时了，取消节点】
            if (nanos &amp;lt;= 0L) {
                s.tryCancel();
                continue;
            }
        }
        // 说明当前线程还可以进行自旋检查
        if (spins &amp;gt; 0)
            // 自旋一次 递减 1
            spins = shouldSpin(s) ? (spins - 1) : 0;
        // 说明没有自旋次数了
        else if (s.waiter == null)
            //【把当前 node 对应的 Thread 保存到 node.waiter 字段中，要阻塞了】
            s.waiter = w;
        // 没有超时限制直接阻塞
        else if (!timed)
            LockSupport.park(this);
        // nanos &amp;gt; 1000 纳秒的情况下，才允许挂起当前线程
        else if (nanos &amp;gt; spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;boolean shouldSpin(SNode s) {
    // 获取栈顶
    SNode h = head;
    // 条件一成立说明当前 s 就是栈顶，允许自旋检查
    // 条件二成立说明当前 s 节点自旋检查期间，又来了一个与当前 s 节点匹配的请求，双双出栈后条件会成立
    // 条件三成立前提当前 s 不是栈顶元素，并且当前栈顶正在匹配中，这种状态栈顶下面的元素，都允许自旋检查
    return (h == s || h == null || isFulfilling(h.mode));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;clear()：指定节点出栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void clean(SNode s) {
    // 清空数据域和关联线程
    s.item = null;
    s.waiter = null;
    
	// 获取取消节点的下一个节点
    SNode past = s.next;
    // 判断后继节点是不是取消节点，是就更新 past
    if (past != null &amp;amp;&amp;amp; past.isCancelled())
        past = past.next;

    SNode p;
    // 从栈顶开始向下检查，【将栈顶开始向下的 取消状态 的节点全部清理出去】，直到碰到 past 或者不是取消状态为止
    while ((p = head) != null &amp;amp;&amp;amp; p != past &amp;amp;&amp;amp; p.isCancelled())
        // 修改的是内存地址对应的值，p 指向该内存地址所以数据一直在变化
        casHead(p, p.next);
	// 说明中间遇到了不是取消状态的节点，继续迭代下去
    while (p != null &amp;amp;&amp;amp; p != past) {
        SNode n = p.next;
        if (n != null &amp;amp;&amp;amp; n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;公平实现&lt;/h5&gt;
&lt;p&gt;TransferQueue 是公平的同步队列，采用 FIFO 的队列实现，请求节点与队尾模式不同，需要与队头发生匹配&lt;/p&gt;
&lt;p&gt;TransferQueue 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;指向队列的 dummy 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode head;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指向队列的尾节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode tail;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被清理节点的前驱节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode cleanMe;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;入队操作是两步完成的，第一步是 t.next = newNode，第二步是 tail = newNode，所以队尾节点出队，是一种非常特殊的情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferQueue 内部类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QNode：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class QNode {
    // 指向当前节点的下一个节点
    volatile QNode next;
    // 数据域，Node 代表的是 DATA 类型 item 表示数据，否则 Node 代表的 REQUEST 类型，item == null
    volatile Object item;
    // 假设当前 node 对应的线程自旋期间未被匹配成功，那么 node 对应的线程需要挂起，
    // 挂起前 waiter 保存对应的线程引用，方便匹配成功后被唤醒。
    volatile Thread waiter;
    // true 当前 Node 是一个 DATA 类型，false 表示当前 Node 是一个 REQUEST 类型
    final boolean isData;

	// 构建方法
    QNode(Object item, boolean isData) {
        this.item = item;
        this.isData = isData;
    }

    // 尝试取消当前 node，取消状态的 node 的 item 域指向自己
    void tryCancel(Object cmp) {
        UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
    }

    // 判断当前 node 是否为取消状态
    boolean isCancelled() {
        return item == this;
    }

    // 判断当前节点是否 “不在” 队列内，当 next 指向自己时，说明节点已经出队。
    boolean isOffList() {
        return next == this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferQueue 类成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置头尾节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void advanceHead(QNode h, QNode nh) {
    // 设置头指针指向新的节点，
    if (h == head &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
        // 老的头节点出队
        h.next = h;
}
void advanceTail(QNode t, QNode nt) {
    if (tail == t)
        // 更新队尾节点为新的队尾
        UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;transfer()：核心方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E transfer(E e, boolean timed, long nanos) {
    // s 指向当前请求对应的 node
    QNode s = null;
    // 是否是 DATA 类型的请求
    boolean isData = (e != null);
	// 自旋
    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)
            continue;
		// head 和 tail 同时指向 dummy 节点，说明是空队列
        // 队尾节点与当前请求类型是一致的情况，说明阻塞队列中都无法匹配，
        if (h == t || t.isData == isData) {
            // 获取队尾 t 的 next 节点
            QNode tn = t.next;
            // 多线程环境中其他线程可能修改尾节点
            if (t != tail)
                continue;
            // 已经有线程入队了，更新 tail
            if (tn != null) {
                advanceTail(t, tn);
                continue;
            }
            // 允许超时，超时时间小于 0，这种方法不支持阻塞等待
            if (timed &amp;amp;&amp;amp; nanos &amp;lt;= 0)
                return null;
            // 创建 node 的逻辑
            if (s == null)
                s = new QNode(e, isData);
            // 将 node 添加到队尾
            if (!t.casNext(null, s))
                continue;
			// 更新队尾指针
            advanceTail(t, s);
            
            // 当前节点 等待匹配....
            Object x = awaitFulfill(s, e, timed, nanos);
            
            // 说明【当前 node 状态为 取消状态】，需要做出队逻辑
            if (x == s) {
                clean(t, s);
                return null;
            }
			// 说明当前 node 仍然在队列内，匹配成功，需要做出队逻辑
            if (!s.isOffList()) {
                // t 是当前 s 节点的前驱节点，判断 t 是不是头节点，是就更新 dummy 节点为 s 节点
                advanceHead(t, s);
                // s 节点已经出队，所以需要把它的 item 域设置为它自己，表示它是个取消状态
                if (x != null)
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
		// 队尾节点与当前请求节点【互补匹配】
        } else {
            // h.next 节点，【请求节点与队尾模式不同，需要与队头发生匹配】，TransferQueue 是一个【公平模式】
            QNode m = h.next;
            // 并发导致其他线程修改了队尾节点，或者已经把 head.next 匹配走了
            if (t != tail || m == null || h != head)
                continue;
			// 获取匹配节点的数据域保存到 x
            Object x = m.item;
            // 判断是否匹配成功
            if (isData == (x != null) ||
                x == m ||
                !m.casItem(x, e)) {
                advanceHead(h, m);
                continue;
            }
			// 【匹配完成】，将头节点出队，让这个新的头结点成为 dummy 节点
            advanceHead(h, m);
            // 唤醒该匹配节点的线程
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;awaitFulfill()：阻塞当前线程等待被匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    // 表示等待截止时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    // 自选检查的次数
    int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        // 被打断就取消节点
        if (w.isInterrupted())
            s.tryCancel(e);
        // 获取当前 Node 数据域
        Object x = s.item;
        
        // 当前请求为 DATA 模式时：e 请求带来的数据
        // s.item 修改为 this，说明当前 QNode 对应的线程 取消状态
        // s.item 修改为 null 表示已经有匹配节点了，并且匹配节点拿走了 item 数据

        // 当前请求为 REQUEST 模式时：e == null
        // s.item 修改为 this，说明当前 QNode 对应的线程 取消状态
        // s.item != null 且 item != this  表示当前 REQUEST 类型的 Node 已经匹配到 DATA 了 
        if (x != e)
            return x;
        // 超时检查
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos &amp;lt;= 0L) {
                s.tryCancel(e);
                continue;
            }
        }
        // 自旋次数减一
        if (spins &amp;gt; 0)
            --spins;
        // 没有自旋次数了，把当前线程封装进去 waiter
        else if (s.waiter == null)
            s.waiter = w;
        // 阻塞
        else if (!timed)
            LockSupport.park(this);
        else if (nanos &amp;gt; spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;操作Pool&lt;/h3&gt;
&lt;h4&gt;创建方式&lt;/h4&gt;
&lt;h5&gt;Executor&lt;/h5&gt;
&lt;p&gt;存放线程的容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final HashSet&amp;lt;Worker&amp;gt; workers = new HashSet&amp;lt;Worker&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&amp;lt;Runnable&amp;gt; workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;corePoolSize：核心线程数，定义了最小可以同时运行的线程数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;maximumPoolSize：最大线程数，当队列中存放的任务达到队列容量时，当前可以同时运行的数量变为最大线程数，创建线程并立即执行最新的任务，与核心线程数之间的差值又叫救急线程数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;keepAliveTime：救急线程最大存活时间，当线程池中的线程数量大于 &lt;code&gt;corePoolSize&lt;/code&gt; 的时候，如果这时没有新的任务提交，核心线程外的线程不会立即销毁，而是会等到 &lt;code&gt;keepAliveTime&lt;/code&gt; 时间超过销毁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unit：&lt;code&gt;keepAliveTime&lt;/code&gt; 参数的时间单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;workQueue：阻塞队列，存放被提交但尚未被执行的任务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;threadFactory：线程工厂，创建新线程时用到，可以为线程创建时起名字&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;handler：拒绝策略，线程到达最大线程数仍有新任务时会执行拒绝策略&lt;/p&gt;
&lt;p&gt;RejectedExecutionHandler 下有 4 个实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbortPolicy：让调用者抛出 RejectedExecutionException 异常，&lt;strong&gt;默认策略&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;CallerRunsPolicy：让调用者运行的调节机制，将某些任务回退到调用者，从而降低新任务的流量&lt;/li&gt;
&lt;li&gt;DiscardPolicy：直接丢弃任务，不予任何处理也不抛出异常&lt;/li&gt;
&lt;li&gt;DiscardOldestPolicy：放弃队列中最早的任务，把当前任务加入队列中尝试再次提交当前任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：其他框架拒绝策略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dubbo：在抛出 RejectedExecutionException 异常前记录日志，并 dump 线程栈信息，方便定位问题&lt;/li&gt;
&lt;li&gt;Netty：创建一个新线程来执行任务&lt;/li&gt;
&lt;li&gt;ActiveMQ：带超时等待（60s）尝试放入队列&lt;/li&gt;
&lt;li&gt;PinPoint：它使用了一个拒绝策略链，会逐一尝试策略链中每种拒绝策略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建线程池，这时没有创建线程（&lt;strong&gt;懒惰&lt;/strong&gt;），等待提交过来的任务请求，调用 execute 方法才会创建线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用 execute() 方法添加一个请求任务时，线程池会做如下判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果正在运行的线程数量小于 corePoolSize，那么马上创建线程运行这个任务&lt;/li&gt;
&lt;li&gt;如果正在运行的线程数量大于或等于 corePoolSize，那么将这个任务放入队列&lt;/li&gt;
&lt;li&gt;如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize，那么会创建非核心线程&lt;strong&gt;立刻运行这个任务&lt;/strong&gt;，对于阻塞队列中的任务不公平。这是因为创建每个 Worker（线程）对象会绑定一个初始任务，启动 Worker 时会优先执行&lt;/li&gt;
&lt;li&gt;如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize，那么线程池会启动饱和&lt;strong&gt;拒绝策略&lt;/strong&gt;来执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个线程完成任务时，会从队列中取下一个任务来执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个线程空闲超过一定的时间（keepAliveTime）时，线程池会判断：如果当前运行的线程数大于 corePoolSize，那么这个线程就被停掉，所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;图片来源：https://space.bilibili.com/457326371/&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;Executors&lt;/h5&gt;
&lt;p&gt;Executors 提供了四种线程池的创建：newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;newFixedThreadPool：创建一个拥有 n 个线程的线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;核心线程数 == 最大线程数（没有救急线程被创建），因此也无需超时时间&lt;/li&gt;
&lt;li&gt;LinkedBlockingQueue 是一个单向链表实现的阻塞队列，默认大小为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;，也就是无界队列，可以放任意数量的任务，在任务比较多的时候会导致 OOM（内存溢出）&lt;/li&gt;
&lt;li&gt;适用于任务量已知，相对耗时的长期任务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;newCachedThreadPool：创建一个可扩容的线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                  new SynchronousQueue&amp;lt;Runnable&amp;gt;());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心线程数是 0， 最大线程数是 29 个 1，全部都是救急线程（60s 后可以回收），可能会创建大量线程，从而导致 &lt;strong&gt;OOM&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SynchronousQueue 作为阻塞队列，没有容量，对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止（类似一手交钱、一手交货）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适合任务数比较密集，但每个任务执行时间较短的情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;newSingleThreadExecutor：创建一个只有 1 个线程的单线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;保证所有任务按照&lt;strong&gt;指定顺序执行&lt;/strong&gt;，线程数固定为 1，任务数多于 1 时会放入无界队列排队，任务执行完毕，这唯一的线程也不会被释放&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个单线程串行执行任务，如果任务执行失败而终止那么没有任何补救措施，线程池会新建一个线程，保证池的正常工作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Executors.newSingleThreadExecutor() 线程个数始终为 1，不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式，只对外暴露了 ExecutorService 接口，因此不能调用 ThreadPoolExecutor 中特有的方法&lt;/p&gt;
&lt;p&gt;原因：父类不能直接调用子类中的方法，需要反射或者创建对象的方式，可以调用子类静态方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Executors.newFixedThreadPool(1) 初始时为 1，可以修改。对外暴露的是 ThreadPoolExecutor 对象，可以强转后调用 setCorePoolSize 等方法进行修改&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;开发要求&lt;/h5&gt;
&lt;p&gt;阿里巴巴 Java 开发手册要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程资源必须通过线程池提供，不允许在应用中自行显式创建线程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销，解决资源不足的问题&lt;/li&gt;
&lt;li&gt;如果不使用线程池，有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池不允许使用 Executors 去创建，而是通过 ThreadPoolExecutor 的方式，这样的处理方式更加明确线程池的运行规则，规避资源耗尽的风险&lt;/p&gt;
&lt;p&gt;Executors 返回的线程池对象弊端如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FixedThreadPool 和 SingleThreadPool：请求队列长度为 Integer.MAX_VALUE，可能会堆积大量的请求，从而导致 OOM&lt;/li&gt;
&lt;li&gt;CacheThreadPool 和 ScheduledThreadPool：允许创建线程数量为 Integer.MAX_VALUE，可能会创建大量的线程，导致 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建多大容量的线程池合适？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一般来说池中&lt;strong&gt;总线程数是核心池线程数量两倍&lt;/strong&gt;，确保当核心池有线程停止时，核心池外有线程进入核心池&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过小会导致程序不能充分地利用系统资源、容易导致饥饿&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过大会导致更多的线程上下文切换，占用更多内存&lt;/p&gt;
&lt;p&gt;上下文切换：当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态，以便下次再切换回这个任务时，可以再加载这个任务的状态，任务从保存到再加载的过程就是一次上下文切换&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心线程数常用公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CPU 密集型任务 (N+1)：&lt;/strong&gt; 这种任务消耗的是 CPU 资源，可以将核心线程数设置为 N (CPU 核心数) + 1，比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断，或者其它原因导致的任务暂停而带来的影响。一旦任务暂停，CPU 某个核心就会处于空闲状态，而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间&lt;/p&gt;
&lt;p&gt;CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;I/O 密集型任务：&lt;/strong&gt; 这种系统 CPU 处于阻塞状态，用大部分的时间来处理 I/O 交互，而线程在处理 I/O 的时间段内不会占用 CPU 来处理，这时就可以将 CPU 交出给其它线程使用，因此在 I/O 密集型任务的应用中，我们可以多配置一些线程，具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数)，阻塞系数在 0.8~0.9 之间&lt;/p&gt;
&lt;p&gt;IO 密集型就是涉及到网络读取，文件读取此类任务 ，特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少，大部分时间都花在了等待 IO 操作完成上&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;提交方法&lt;/h4&gt;
&lt;p&gt;ExecutorService 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void execute(Runnable command)&lt;/td&gt;
&lt;td&gt;执行任务（Executor 类 API）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future&amp;lt;?&amp;gt; submit(Runnable task)&lt;/td&gt;
&lt;td&gt;提交任务 task()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future submit(Callable&amp;lt;T&amp;gt; task)&lt;/td&gt;
&lt;td&gt;提交任务 task，用返回值 Future 获得任务执行结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks, long timeout, TimeUnit unit)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务，超时时间针对所有task，超时会取消没有执行完的任务，并抛出超时异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T invokeAny(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务，哪个任务先成功执行完毕，返回此任务执行结果，其它任务取消&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;execute 和 submit 都属于线程池的方法，对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;execute 只能执行 Runnable 类型的任务，没有返回值； submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务，底层是&lt;strong&gt;封装成 FutureTask，然后调用 execute 执行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;execute 会直接抛出任务执行时的异常，submit 会吞掉异常，可通过 Future 的 get 方法将任务执行时的异常重新抛出&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;关闭方法&lt;/h4&gt;
&lt;p&gt;ExecutorService 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void shutdown()&lt;/td&gt;
&lt;td&gt;线程池状态变为 SHUTDOWN，等待任务执行完后关闭线程池，不会接收新任务，但已提交任务会执行完，而且也可以添加线程（不绑定任务）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Runnable&amp;gt; shutdownNow()&lt;/td&gt;
&lt;td&gt;线程池状态变为 STOP，用 interrupt 中断正在执行的任务，直接关闭线程池，不会接收新任务，会将队列中的任务返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isShutdown()&lt;/td&gt;
&lt;td&gt;不在 RUNNING 状态的线程池，此执行者已被关闭，方法返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isTerminated()&lt;/td&gt;
&lt;td&gt;线程池状态是否是 TERMINATED，如果所有任务在关闭后完成，返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean awaitTermination(long timeout, TimeUnit unit)&lt;/td&gt;
&lt;td&gt;调用 shutdown 后，由于调用线程不会等待所有任务运行结束，如果它想在线程池 TERMINATED 后做些事情，可以利用此方法等待&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;处理异常&lt;/h4&gt;
&lt;p&gt;execute 会直接抛出任务执行时的异常，submit 会吞掉异常，有两种处理方法&lt;/p&gt;
&lt;p&gt;方法 1：主动捉异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(1);
pool.submit(() -&amp;gt; {
    try {
        System.out.println(&quot;task1&quot;);
        int i = 1 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法 2：使用 Future 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(1);
Future&amp;lt;?&amp;gt; future = pool.submit(() -&amp;gt; {
    System.out.println(&quot;task1&quot;);
    int i = 1 / 0;
    return true;
});
System.out.println(future.get());
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;工作原理&lt;/h3&gt;
&lt;h4&gt;状态信息&lt;/h4&gt;
&lt;p&gt;ThreadPoolExecutor 使用 int 的&lt;strong&gt;高 3 位来表示线程池状态，低 29 位表示线程数量&lt;/strong&gt;。这些信息存储在一个原子变量 ctl 中，目的是将线程池状态与线程个数合二为一，这样就可以用一次 CAS 原子操作进行赋值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;状态表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 高3位：表示当前线程池运行状态，除去高3位之后的低位：表示当前线程池中所拥有的线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 表示在 ctl 中，低 COUNT_BITS 位，是用于存放当前线程数量的位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 低 COUNT_BITS 位所能表达的最大数值，000 11111111111111111111 =&amp;gt; 5亿多
private static final int CAPACITY   = (1 &amp;lt;&amp;lt; COUNT_BITS) - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;四种状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 111 000000000000000000，转换成整数后其实就是一个【负数】
private static final int RUNNING    = -1 &amp;lt;&amp;lt; COUNT_BITS;
// 000 000000000000000000
private static final int SHUTDOWN   =  0 &amp;lt;&amp;lt; COUNT_BITS;
// 001 000000000000000000
private static final int STOP       =  1 &amp;lt;&amp;lt; COUNT_BITS;
// 010 000000000000000000
private static final int TIDYING    =  2 &amp;lt;&amp;lt; COUNT_BITS;
// 011 000000000000000000
private static final int TERMINATED =  3 &amp;lt;&amp;lt; COUNT_BITS;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;高3位&lt;/th&gt;
&lt;th&gt;接收新任务&lt;/th&gt;
&lt;th&gt;处理阻塞任务队列&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RUNNING&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHUTDOWN&lt;/td&gt;
&lt;td&gt;000&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;不接收新任务，但处理阻塞队列剩余任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STOP&lt;/td&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;中断正在执行的任务，并抛弃阻塞队列任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIDYING&lt;/td&gt;
&lt;td&gt;010&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;任务全执行完毕，活动线程为 0 即将进入终结&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TERMINATED&lt;/td&gt;
&lt;td&gt;011&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;终止状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取当前线程池运行状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~CAPACITY = ~000 11111111111111111111 = 111 000000000000000000000（取反）
// c == ctl = 111 000000000000000000111
// 111 000000000000000000111
// 111 000000000000000000000
// 111 000000000000000000000	获取到了运行状态
private static int runStateOf(int c)     { return c &amp;amp; ~CAPACITY; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取当前线程池线程数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//        c = 111 000000000000000000111
// CAPACITY = 000 111111111111111111111
//            000 000000000000000000111 =&amp;gt; 7
private static int workerCountOf(int c)  { return c &amp;amp; CAPACITY; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重置当前线程池状态 ctl：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rs 表示线程池状态，wc 表示当前线程池中 worker（线程）数量，相与以后就是合并后的状态
private static int ctlOf(int rs, int wc) { return rs | wc; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较当前线程池 ctl 所表示的状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 比较当前线程池 ctl 所表示的状态，是否小于某个状态 s
// 状态对比：RUNNING &amp;lt; SHUTDOWN &amp;lt; STOP &amp;lt; TIDYING &amp;lt; TERMINATED
private static boolean runStateLessThan(int c, int s) { return c &amp;lt; s; }
// 比较当前线程池 ctl 所表示的状态，是否大于等于某个状态s
private static boolean runStateAtLeast(int c, int s) { return c &amp;gt;= s; }
// 小于 SHUTDOWN 的一定是 RUNNING，SHUTDOWN == 0
private static boolean isRunning(int c) { return c &amp;lt; SHUTDOWN; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置线程池 ctl：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 CAS 方式 让 ctl 值 +1 ，成功返回 true, 失败返回 false
private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}
// 使用 CAS 方式 让 ctl 值 -1 ，成功返回 true, 失败返回 false
private boolean compareAndDecrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect - 1);
}
// 将 ctl 值减一，do while 循环会一直重试，直到成功为止
private void decrementWorkerCount() {
    do {} while (!compareAndDecrementWorkerCount(ctl.get()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员属性&lt;/h4&gt;
&lt;p&gt;成员变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程池中存放 Worker 的容器&lt;/strong&gt;：线程池没有初始化，直接往池中加线程即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final HashSet&amp;lt;Worker&amp;gt; workers = new HashSet&amp;lt;Worker&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程全局锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 增加减少 worker 或者时修改线程池运行状态需要持有 mainLock
private final ReentrantLock mainLock = new ReentrantLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可重入锁的条件变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当外部线程调用 awaitTermination() 方法时，会等待当前线程池状态为 Termination 为止
private final Condition termination = mainLock.newCondition()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池相关参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int corePoolSize;				// 核心线程数量
private volatile int maximumPoolSize;			// 线程池最大线程数量
private volatile long keepAliveTime;			// 空闲线程存活时间
private volatile ThreadFactory threadFactory;	// 创建线程时使用的线程工厂，默认是 DefaultThreadFactory
private final BlockingQueue&amp;lt;Runnable&amp;gt; workQueue;// 【超过核心线程提交任务就放入 阻塞队列】
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private volatile RejectedExecutionHandler handler;	// 拒绝策略，juc包提供了4中方式
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认策略
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录线程池相关属性的数值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int largestPoolSize;		// 记录线程池生命周期内线程数最大值
private long completedTaskCount;	// 记录线程池所完成任务总数，当某个 worker 退出时将完成的任务累加到该属性
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制&lt;strong&gt;核心线程数量内的线程是否可以被回收&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// false（默认）代表不可以，为 true 时核心线程空闲超过 keepAliveTime 也会被回收
// allowCoreThreadTimeOut(boolean value) 方法可以设置该值
private volatile boolean allowCoreThreadTimeOut;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内部类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Worker 类：&lt;strong&gt;每个 Worker 对象会绑定一个初始任务&lt;/strong&gt;，启动 Worker 时优先执行，这也是造成线程池不公平的原因。Worker 继承自 AQS，本身具有锁的特性，采用独占锁模式，state = 0 表示未被占用，&amp;gt; 0 表示被占用，&amp;lt; 0 表示初始状态不能被抢锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
	final Thread thread;			// worker 内部封装的工作线程
    Runnable firstTask;				// worker 第一个执行的任务，普通的 Runnable 实现类或者是 FutureTask
    volatile long completedTasks;	// 记录当前 worker 所完成任务数量
    
    // 构造方法
    Worker(Runnable firstTask) {
        // 设置AQS独占模式为初始化中状态，这个状态不能被抢占锁
       	setState(-1);
        // firstTask不为空时，当worker启动后，内部线程会优先执行firstTask，执行完后会到queue中去获取下个任务
        this.firstTask = firstTask;
        // 使用线程工厂创建一个线程，并且【将当前worker指定为Runnable】，所以thread启动时会调用 worker.run()
        this.thread = getThreadFactory().newThread(this);
    }
    // 【不可重入锁】
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public Thread newThread(Runnable r) {
    // 将当前 worker 指定为 thread 的执行方法，线程调用 start 会调用 r.run()
    Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拒绝策略相关的内部类&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;h5&gt;提交方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AbstractExecutorService#submit()：提交任务，&lt;strong&gt;把 Runnable 或 Callable 任务封装成 FutureTask 执行&lt;/strong&gt;，可以通过方法返回的任务对象，调用 get 阻塞获取任务执行的结果或者异常，源码分析在笔记的 Future 部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Future&amp;lt;?&amp;gt; submit(Runnable task) {
    // 空指针异常
    if (task == null) throw new NullPointerException();
    // 把 Runnable 封装成未来任务对象，执行结果就是 null，也可以通过参数指定 FutureTask#get 返回数据
    RunnableFuture&amp;lt;Void&amp;gt; ftask = newTaskFor(task, null);
    // 执行方法
    execute(ftask);
    return ftask;
}
public &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Callable&amp;lt;T&amp;gt; task) {
    if (task == null) throw new NullPointerException();
    // 把 Callable 封装成未来任务对象
    RunnableFuture&amp;lt;T&amp;gt; ftask = newTaskFor(task);
    // 执行方法
    execute(ftask);	
    // 返回未来任务对象，用来获取返回值
    return ftask;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;protected &amp;lt;T&amp;gt; RunnableFuture&amp;lt;T&amp;gt; newTaskFor(Runnable runnable, T value) {
    // Runnable 封装成 FutureTask，【指定返回值】
    return new FutureTask&amp;lt;T&amp;gt;(runnable, value);
}
protected &amp;lt;T&amp;gt; RunnableFuture&amp;lt;T&amp;gt; newTaskFor(Callable&amp;lt;T&amp;gt; callable) {
    // Callable 直接封装成 FutureTask
    return new FutureTask&amp;lt;T&amp;gt;(callable);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;execute()：执行任务，&lt;strong&gt;但是没有返回值，没办法获取任务执行结果&lt;/strong&gt;，出现异常会直接抛出任务执行时的异常。根据线程池中的线程数，选择添加任务时的处理方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// command 可以是普通的 Runnable 实现类，也可以是 FutureTask，不能是 Callable
public void execute(Runnable command) {
    // 非空判断
    if (command == null)
        throw new NullPointerException();
  	// 获取 ctl 最新值赋值给 c，ctl 高 3 位表示线程池状态，低位表示当前线程池线程数量。
    int c = ctl.get();
    // 【1】当前线程数量小于核心线程数，此次提交任务直接创建一个新的 worker，线程池中多了一个新的线程
    if (workerCountOf(c) &amp;lt; corePoolSize) {
        // addWorker 为创建线程的过程，会创建 worker 对象并且将 command 作为 firstTask，优先执行
        if (addWorker(command, true))
            return;
        
        // 执行到这条语句，说明 addWorker 一定是失败的，存在并发现象或者线程池状态被改变，重新获取状态
        // SHUTDOWN 状态下也有可能创建成功，前提 firstTask == null 而且当前 queue 不为空（特殊情况）
        c = ctl.get();
    }
    // 【2】执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败
    // 	判断当前线程池是否处于running状态，成立就尝试将 task 放入到 workQueue 中
    if (isRunning(c) &amp;amp;&amp;amp; workQueue.offer(command)) {
        int recheck = ctl.get();
        // 条件一成立说明线程池状态被外部线程给修改了，可能是执行了 shutdown() 方法，该状态不能接收新提交的任务
        // 所以要把刚提交的任务删除，删除成功说明提交之后线程池中的线程还未消费（处理）该任务
        if (!isRunning(recheck) &amp;amp;&amp;amp; remove(command))
            // 任务出队成功，走拒绝策略
            reject(command);
        // 执行到这说明线程池是 running 状态，获取线程池中的线程数量，判断是否是 0
        // 【担保机制】，保证线程池在 running 状态下，最起码得有一个线程在工作
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 【3】offer失败说明queue满了
    // 如果线程数量尚未达到 maximumPoolSize，会创建非核心 worker 线程直接执行 command，【这也是不公平的原因】
    // 如果当前线程数量达到 maximumPoolSiz，这里 addWorker 也会失败，走拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;添加线程&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;prestartAllCoreThreads()：&lt;strong&gt;提前预热&lt;/strong&gt;，创建所有的核心线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(null, true))
        ++n;
    return n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addWorker()：&lt;strong&gt;添加线程到线程池&lt;/strong&gt;，返回 true 表示创建 Worker 成功，且线程启动。首先判断线程池是否允许添加线程，允许就让线程数量 + 1，然后去创建 Worker 加入线程池&lt;/p&gt;
&lt;p&gt;注意：SHUTDOWN 状态也能添加线程，但是要求新加的 Woker 没有 firstTask，而且当前 queue 不为空，所以创建一个线程来帮助线程池执行队列中的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// core == true 表示采用核心线程数量限制，false 表示采用 maximumPoolSize
private boolean addWorker(Runnable firstTask, boolean core) {
    // 自旋【判断当前线程池状态是否允许创建线程】，允许就设置线程数量 + 1
    retry:
    for (;;) {
        // 获取 ctl 的值
        int c = ctl.get();
        // 获取当前线程池运行状态
        int rs = runStateOf(c);	
        
        // 判断当前线程池状态【是否允许添加线程】
        
        // 当前线程池是 SHUTDOWN 状态，但是队列里面还有任务尚未处理完，需要处理完 queue 中的任务
        // 【不允许再提交新的 task，所以 firstTask 为空，但是可以继续添加 worker】
        if (rs &amp;gt;= SHUTDOWN &amp;amp;&amp;amp; !(rs == SHUTDOWN &amp;amp;&amp;amp; firstTask == null &amp;amp;&amp;amp; !workQueue.isEmpty()))
            return false;
        for (;;) {
            // 获取线程池中线程数量
            int wc = workerCountOf(c);
            // 条件一一般不成立，CAPACITY是5亿多，根据 core 判断使用哪个大小限制线程数量，超过了返回 false
            if (wc &amp;gt;= CAPACITY || wc &amp;gt;= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 记录线程数量已经加 1，类比于申请到了一块令牌，条件失败说明其他线程修改了数量
            if (compareAndIncrementWorkerCount(c))
                // 申请成功，跳出了 retry 这个 for 自旋
                break retry;
            // CAS 失败，没有成功的申请到令牌
            c = ctl.get();
            // 判断当前线程池状态是否发生过变化，被其他线程修改了，可能其他线程调用了 shutdown() 方法
            if (runStateOf(c) != rs)
                // 返回外层循环检查是否能创建线程，在 if 语句中返回 false
                continue retry;
           
        }
    }
    
    //【令牌申请成功，开始创建线程】
    
	// 运行标记，表示创建的 worker 是否已经启动，false未启动  true启动
    boolean workerStarted = false;
    // 添加标记，表示创建的 worker 是否添加到池子中了，默认false未添加，true是添加。
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 【创建 Worker，底层通过线程工厂 newThread 方法创建执行线程，指定了首先执行的任务】
        w = new Worker(firstTask);
        // 将新创建的 worker 节点中的线程赋值给 t
        final Thread t = w.thread;
        // 这里的判断为了防止 程序员自定义的 ThreadFactory 实现类有 bug，创造不出线程
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            // 加互斥锁，要添加 worker 了
            mainLock.lock();
            try {
                // 获取最新线程池运行状态保存到 rs
                int rs = runStateOf(ctl.get());
				// 判断线程池是否为RUNNING状态，不是再【判断当前是否为SHUTDOWN状态且firstTask为空，特殊情况】
                if (rs &amp;lt; SHUTDOWN || (rs == SHUTDOWN &amp;amp;&amp;amp; firstTask == null)) {
                    // 当线程start后，线程isAlive会返回true，这里还没开始启动线程，如果被启动了就需要报错
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    
                    //【将新建的 Worker 添加到线程池中】
                    workers.add(w);
                    int s = workers.size();
					// 当前池中的线程数量是一个新高，更新 largestPoolSize
                    if (s &amp;gt; largestPoolSize)
                        largestPoolSize = s;
                    // 添加标记置为 true
                    workerAdded = true;
                }
            } finally {
                // 解锁啊
                mainLock.unlock();
            }
            // 添加成功就【启动线程执行任务】
            if (workerAdded) {
                // Thread 类中持有 Runnable 任务对象，调用的是 Runnable 的 run ，也就是 FutureTask
                t.start();
                // 运行标记置为 true
                workerStarted = true;
            }
        }
    } finally {
        // 如果启动线程失败，做清理工作
        if (! workerStarted)
            addWorkerFailed(w);
    }
    // 返回新创建的线程是否启动
    return workerStarted;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addWorkerFailed()：清理任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    // 持有线程池全局锁，因为操作的是线程池相关的东西
    mainLock.lock();
    try {
        //条件成立需要将 worker 在 workers 中清理出去。
        if (w != null)
            workers.remove(w);
        // 将线程池计数 -1，相当于归还令牌。
        decrementWorkerCount();
        // 尝试停止线程池
        tryTerminate();
    } finally {
        //释放线程池全局锁。
        mainLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;运行方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Worker#run：Worker 实现了 Runnable 接口，当线程启动时，会调用 Worker 的 run() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    // ThreadPoolExecutor#runWorker()
    runWorker(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;runWorker()：线程启动就要&lt;strong&gt;执行任务&lt;/strong&gt;，会一直 while 循环获取任务并执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();	
    // 获取 worker 的 firstTask
    Runnable task = w.firstTask;
    // 引用置空，【防止复用该线程时重复执行该任务】
    w.firstTask = null;
    // 初始化 worker 时设置 state = -1，表示不允许抢占锁
    // 这里需要设置 state = 0 和 exclusiveOwnerThread = null，开始独占模式抢锁
    w.unlock();
    // true 表示发生异常退出，false 表示正常退出。
    boolean completedAbruptly = true;
    try {
        // firstTask 不是 null 就直接运行，否则去 queue 中获取任务
        // 【getTask 如果是阻塞获取任务，会一直阻塞在take方法，直到获取任务，不会走返回null的逻辑】
        while (task != null || (task = getTask()) != null) {
            // worker 加锁，shutdown 时会判断当前 worker 状态，【根据独占锁状态判断是否空闲】
            w.lock();
            
			// 说明线程池状态大于 STOP，目前处于 STOP/TIDYING/TERMINATION，此时给线程一个中断信号
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 // 说明线程处于 RUNNING 或者 SHUTDOWN 状态，清除打断标记
                 (Thread.interrupted() &amp;amp;&amp;amp; runStateAtLeast(ctl.get(), STOP))) &amp;amp;&amp;amp; !wt.isInterrupted())
                // 中断线程，设置线程的中断标志位为 true
                wt.interrupt();
            try {
                // 钩子方法，【任务执行的前置处理】
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 【执行任务】
                    task.run();
                } catch (Exception x) {
                 	//.....
                } finally {
                    // 钩子方法，【任务执行的后置处理】
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;		// 将局部变量task置为null，代表任务执行完成
                w.completedTasks++;	// 更新worker完成任务数量
                w.unlock();			// 解锁
            }
        }
        // getTask()方法返回null时会走到这里，表示queue为空并且线程空闲超过保活时间，【当前线程执行退出逻辑】
        completedAbruptly = false;	
    } finally {
        // 正常退出 completedAbruptly = false
       	// 异常退出 completedAbruptly = true，【从 task.run() 内部抛出异常】时，跳到这一行
        processWorkerExit(w, completedAbruptly);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unlock()：重置锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unlock() { release(1); }
// 外部不会直接调用这个方法 这个方法是 AQS 内调用的，外部调用 unlock 时触发此方法
protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);		// 设置持有者为 null
    setState(0);						// 设置 state = 0
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getTask()：获取任务，线程空闲时间超过 keepAliveTime 就会被回收，判断的依据是&lt;strong&gt;当前线程阻塞获取任务超过保活时间&lt;/strong&gt;，方法返回 null 就代表当前线程要被回收了，返回到 runWorker 执行线程退出逻辑。线程池具有担保机制，对于 RUNNING 状态下的超时回收，要保证线程池中最少有一个线程运行，或者任务阻塞队列已经是空&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Runnable getTask() {
    // 超时标记，表示当前线程获取任务是否超时，true 表示已超时
    boolean timedOut = false; 
    for (;;) {
        int c = ctl.get();
        // 获取线程池当前运行状态
        int rs = runStateOf(c);
		
        // 【tryTerminate】打断线程后执行到这，此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空
        // 所以下面的 if 条件一定是成立的，可以直接返回 null，线程就应该退出了
        if (rs &amp;gt;= SHUTDOWN &amp;amp;&amp;amp; (rs &amp;gt;= STOP || workQueue.isEmpty())) {
            // 使用 CAS 自旋的方式让 ctl 值 -1
            decrementWorkerCount();
            return null;
        }
        
		// 获取线程池中的线程数量
        int wc = workerCountOf(c);

        // 线程没有明确的区分谁是核心或者非核心线程，是根据当前池中的线程数量判断
        
        // timed = false 表示当前这个线程 获取task时不支持超时机制的，当前线程会使用 queue.take() 阻塞获取
        // timed = true 表示当前这个线程 获取task时支持超时机制，使用 queue.poll(xxx,xxx) 超时获取
        // 条件一代表允许回收核心线程，那就无所谓了，全部线程都执行超时回收
        // 条件二成立说明线程数量大于核心线程数，当前线程认为是非核心线程，有保活时间，去超时获取任务
        boolean timed = allowCoreThreadTimeOut || wc &amp;gt; corePoolSize;
        
		// 如果线程数量是否超过最大线程数，直接回收
        // 如果当前线程【允许超时回收并且已经超时了】，就应该被回收了，由于【担保机制】还要做判断：
        // 	  wc &amp;gt; 1 说明线程池还用其他线程，当前线程可以直接回收
        //    workQueue.isEmpty() 前置条件是 wc = 1，【如果当前任务队列也是空了，最后一个线程就可以退出】
        if ((wc &amp;gt; maximumPoolSize || (timed &amp;amp;&amp;amp; timedOut)) &amp;amp;&amp;amp; (wc &amp;gt; 1 || workQueue.isEmpty())) {
            // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程，返回 null，代表可以退出
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 根据当前线程是否需要超时回收，【选择从队列获取任务的方法】是超时获取或者阻塞获取
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
            // 获取到任务返回任务，【阻塞获取会阻塞到获取任务为止】，不会返回 null
            if (r != null)
                return r;
            // 获取任务为 null 说明超时了，将超时标记设置为 true，下次自旋时返 null
            timedOut = true;
        } catch (InterruptedException retry) {
            // 阻塞线程被打断后超时标记置为 false，【说明被打断不算超时】，要继续获取，直到超时或者获取到任务
            // 如果线程池 SHUTDOWN 状态下的打断，会在循环获取任务前判断，返回 null
            timedOut = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processWorkerExit()：&lt;strong&gt;线程退出线程池&lt;/strong&gt;，也有担保机制，保证队列中的任务被执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常退出 completedAbruptly = false，异常退出为 true
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 条件成立代表当前 worker 是发生异常退出的，task 任务执行过程中向上抛出异常了
    if (completedAbruptly) 
        // 从异常时到这里 ctl 一直没有 -1，需要在这里 -1
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    // 加锁
    mainLock.lock();
    try {
        // 将当前 worker 完成的 task 数量，汇总到线程池的 completedTaskCount
        completedTaskCount += w.completedTasks;
		// 将 worker 从线程池中移除
        workers.remove(w);
    } finally {
        mainLock.unlock();	// 解锁
    }
	// 尝试停止线程池，唤醒下一个线程
    tryTerminate();

    int c = ctl.get();
    // 线程池不是停止状态就应该有线程运行【担保机制】
    if (runStateLessThan(c, STOP)) {
        // 正常退出的逻辑，是对空闲线程回收，不是执行出错
        if (!completedAbruptly) {
            // 根据是否回收核心线程确定【线程池中的线程数量最小值】
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 最小值为 0，但是线程队列不为空，需要一个线程来完成任务担保机制
            if (min == 0 &amp;amp;&amp;amp; !workQueue.isEmpty())
                min = 1;
            // 线程池中的线程数量大于最小值可以直接返回
            if (workerCountOf(c) &amp;gt;= min)
                return;
        }
        // 执行 task 时发生异常，有个线程因为异常终止了，需要添加
        // 或者线程池中的数量小于最小值，这里要创建一个新 worker 加进线程池
        addWorker(null, false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;停止方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：停止线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池全局锁
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 设置线程池状态为 SHUTDOWN，如果线程池状态大于 SHUTDOWN，就不会设置直接返回
        advanceRunState(SHUTDOWN);
        // 中断空闲线程
        interruptIdleWorkers();
        // 空方法，子类可以扩展
        onShutdown(); 
    } finally {
        // 释放线程池全局锁
        mainLock.unlock();
    }
    tryTerminate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;interruptIdleWorkers()：shutdown 方法会&lt;strong&gt;中断所有空闲线程&lt;/strong&gt;，根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务，不会中断正在运行的线程，所以 shutdown 方法会让所有的任务执行完毕&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// onlyOne == true 说明只中断一个线程 ，false 则中断所有线程
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    / /持有全局锁
    mainLock.lock();
    try {
        // 遍历所有 worker
        for (Worker w : workers) {
            // 获取当前 worker 的线程
            Thread t = w.thread;
            // 条件一成立：说明当前迭代的这个线程尚未中断
            // 条件二成立：说明【当前worker处于空闲状态】，阻塞在poll或者take，因为worker执行task时是要加锁的
            //           每个worker有一个独占锁，w.tryLock()尝试加锁，加锁成功返回 true
            if (!t.isInterrupted() &amp;amp;&amp;amp; w.tryLock()) {
                try {
                    // 中断线程，处于 queue 阻塞的线程会被唤醒，进入下一次自旋，返回 null，执行退出相逻辑
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    // 释放worker的独占锁
                    w.unlock();
                }
            }
            // false，代表中断所有的线程
            if (onlyOne)
                break;
        }

    } finally {
        // 释放全局锁
        mainLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdownNow()：直接关闭线程池，不会等待任务执行完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public List&amp;lt;Runnable&amp;gt; shutdownNow() {
    // 返回值引用
    List&amp;lt;Runnable&amp;gt; tasks;
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池全局锁
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 设置线程池状态为STOP
        advanceRunState(STOP);
        // 中断线程池中【所有线程】
        interruptWorkers();
        // 从阻塞队列中导出未处理的task
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }

    tryTerminate();
    // 返回当前任务队列中 未处理的任务。
    return tasks;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tryTerminate()：设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final void tryTerminate() {
    for (;;) {
        // 获取 ctl 的值
        int c = ctl.get();
        // 线程池正常，或者有其他线程执行了状态转换的方法，当前线程直接返回
        if (isRunning(c) || runStateAtLeast(c, TIDYING) ||
            // 线程池是 SHUTDOWN 并且任务队列不是空，需要去处理队列中的任务
            (runStateOf(c) == SHUTDOWN &amp;amp;&amp;amp; ! workQueue.isEmpty()))
            return;
        
        // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空
        // 判断线程池中线程的数量
        if (workerCountOf(c) != 0) {
            // 【中断一个空闲线程】，在 queue.take() | queue.poll() 阻塞空闲
            // 唤醒后的线程会在getTask()方法返回null，
            // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
		// 池中的线程数量为 0 来到这里
        final ReentrantLock mainLock = this.mainLock;
        // 加全局锁
        mainLock.lock();
        try {
            // 设置线程池状态为 TIDYING 状态，线程数量为 0
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 结束线程池
                    terminated();
                } finally {
                    // 设置线程池状态为TERMINATED状态。
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 【唤醒所有调用 awaitTermination() 方法的线程】
                    termination.signalAll();
                }
                return;
            }
        } finally {
			// 释放线程池全局锁
            mainLock.unlock();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Future&lt;/h4&gt;
&lt;h5&gt;线程使用&lt;/h5&gt;
&lt;p&gt;FutureTask 未来任务对象，继承 Runnable、Future 接口，用于包装 Callable 对象，实现任务的提交&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(new Callable&amp;lt;String&amp;gt;() {
        @Override
        public String call() throws Exception {
            return &quot;Hello World&quot;;
        }
    });
    new Thread(task).start();	//启动线程
    String msg = task.get();	//获取返回任务数据
    System.out.println(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public FutureTask(Callable&amp;lt;V&amp;gt; callable){
	this.callable = callable;	// 属性注入
    this.state = NEW; 			// 任务状态设置为 new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public FutureTask(Runnable runnable, V result) {
    // 适配器模式
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       
}
public static &amp;lt;T&amp;gt; Callable&amp;lt;T&amp;gt; callable(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    // 使用装饰者模式将 runnable 转换成 callable 接口，外部线程通过 get 获取
    // 当前任务执行结果时，结果可能为 null 也可能为传进来的值，【传进来什么返回什么】
    return new RunnableAdapter&amp;lt;T&amp;gt;(task, result);
}
static final class RunnableAdapter&amp;lt;T&amp;gt; implements Callable&amp;lt;T&amp;gt; {
    final Runnable task;
    final T result;
    // 构造方法
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        // 实则调用 Runnable#run 方法
        task.run();
        // 返回值为构造 FutureTask 对象时传入的返回值或者是 null
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;FutureTask 类的成员属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;任务状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示当前task状态
private volatile int state;
// 当前任务尚未执行
private static final int NEW          = 0;
// 当前任务正在结束，尚未完全结束，一种临界状态
private static final int COMPLETING   = 1;
// 当前任务正常结束
private static final int NORMAL       = 2;
// 当前任务执行过程中发生了异常，内部封装的 callable.run() 向上抛出异常了
private static final int EXCEPTIONAL  = 3;
// 当前任务被取消
private static final int CANCELLED    = 4;
// 当前任务中断中
private static final int INTERRUPTING = 5;
// 当前任务已中断
private static final int INTERRUPTED  = 6;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Callable&amp;lt;V&amp;gt; callable;	// Runnable 使用装饰者模式伪装成 Callable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存储任务执行的结果&lt;/strong&gt;，这是 run 方法返回值是 void 也可以获取到执行结果的原因：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常情况下：任务正常执行结束，outcome 保存执行结果，callable 返回值
// 非正常情况：callable 向上抛出异常，outcome 保存异常
private Object outcome; 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行当前任务的线程对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile Thread runner;	// 当前任务被线程执行期间，保存当前执行任务的线程对象引用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程阻塞队列的头节点&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 会有很多线程去 get 当前任务的结果，这里使用了一种数据结构头插头取（类似栈）的一个队列来保存所有的 get 线程
private volatile WaitNode waiters;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class WaitNode {
    // 单向链表
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;FutureTask 类的成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FutureTask#run&lt;/strong&gt;：任务执行入口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    //条件一：成立说明当前 task 已经被执行过了或者被 cancel 了，非 NEW 状态的任务，线程就不需要处理了
    //条件二：线程是 NEW 状态，尝试设置当前任务对象的线程是当前线程，设置失败说明其他线程抢占了该任务，直接返回
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        // 执行到这里，当前 task 一定是 NEW 状态，而且【当前线程也抢占 task 成功】
        Callable&amp;lt;V&amp;gt; c = callable;
        // 判断任务是否为空，防止空指针异常；判断 state 状态，防止外部线程在此期间 cancel 掉当前任务
        // 【因为 task 的执行者已经设置为当前线程，所以这里是线程安全的】
        if (c != null &amp;amp;&amp;amp; state == NEW) {
            V result;
            // true 表示 callable.run 代码块执行成功 未抛出异常
            // false 表示 callable.run 代码块执行失败 抛出异常
            boolean ran;
            try {
				// 【调用自定义的方法，执行结果赋值给 result】
                result = c.call();
                // 没有出现异常
                ran = true;
            } catch (Throwable ex) {
                // 出现异常，返回值置空，ran 置为 false
                result = null;
                ran = false;
                // 设置返回的异常
                setException(ex);
            }
            // 代码块执行正常
            if (ran)
                // 设置返回的结果
                set(result);
        }
    } finally {
        // 任务执行完成，取消线程的引用，help GC
        runner = null;
        int s = state;
        // 判断任务是不是被中断
        if (s &amp;gt;= INTERRUPTING)
            // 执行中断处理方法
            handlePossibleCancellationInterrupt(s);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#set：设置正常返回值，首先将任务状态设置为 COMPLETING 状态代表完成中，逻辑执行完设置为 NORMAL 状态代表任务正常执行完成，最后唤醒 get() 阻塞线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void set(V v) {
    // CAS 方式设置当前任务状态为完成中，设置失败说明其他线程取消了该任务
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【将结果赋值给 outcome】
        outcome = v;
        // 将当前任务状态修改为 NORMAL 正常结束状态。
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#setException：设置异常返回值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 赋值给返回结果，用来向上层抛出来的异常
        outcome = t;
        // 将当前任务的状态 修改为 EXCEPTIONAL
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#finishCompletion：&lt;strong&gt;唤醒 get() 阻塞线程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void finishCompletion() {
    // 遍历所有的等待的节点，q 指向头节点
    for (WaitNode q; (q = waiters) != null;) {
        // 使用cas设置 waiters 为 null，防止外部线程使用cancel取消当前任务，触发finishCompletion方法重复执行
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            // 自旋
            for (;;) {
                // 获取当前 WaitNode 节点封装的 thread
                Thread t = q.thread;
                // 当前线程不为 null，唤醒当前 get() 等待获取数据的线程
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                // 获取当前节点的下一个节点
                WaitNode next = q.next;
                // 当前节点是最后一个节点了
                if (next == null)
                    break;
                // 断开链表
                q.next = null; // help gc
                q = next;
            }
            break;
        }
    }
    done();
    callable = null;	// help GC
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#handlePossibleCancellationInterrupt：任务中断处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void handlePossibleCancellationInterrupt(int s) {
    if (s == INTERRUPTING)
        // 中断状态中
        while (state == INTERRUPTING)
            // 等待中断完成
            Thread.yield();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FutureTask#get&lt;/strong&gt;：获取任务执行的返回值，执行 run 和 get 的不是同一个线程，一般有多个线程 get，只有一个线程 run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V get() throws InterruptedException, ExecutionException {
    // 获取当前任务状态
    int s = state;
    // 条件成立说明任务还没执行完成
    if (s &amp;lt;= COMPLETING)
        // 返回 task 当前状态，可能当前线程在里面已经睡了一会
        s = awaitDone(false, 0L);
    return report(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#awaitDone：&lt;strong&gt;get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    // 0 不带超时
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 引用当前线程，封装成 WaitNode 对象
    WaitNode q = null;
    // 表示当前线程 waitNode 对象，是否进入阻塞队列
    boolean queued = false;
    // 【三次自旋开始休眠】
    for (;;) {
        // 判断当前 get() 线程是否被打断，打断返回 true，清除打断标记
        if (Thread.interrupted()) {
            // 当前线程对应的等待 node 出队，
            removeWaiter(q);
            throw new InterruptedException();
        }
		// 获取任务状态
        int s = state;
        // 条件成立说明当前任务执行完成已经有结果了
        if (s &amp;gt; COMPLETING) {
            // 条件成立说明已经为当前线程创建了 WaitNode，置空 help GC
            if (q != null)
                q.thread = null;
            // 返回当前的状态
            return s;
        }
        // 条件成立说明当前任务接近完成状态，这里让当前线程释放一下 cpu ，等待进行下一次抢占 cpu
        else if (s == COMPLETING) 
            Thread.yield();
        // 【第一次自旋】，当前线程还未创建 WaitNode 对象，此时为当前线程创建 WaitNode对象
        else if (q == null)
            q = new WaitNode();
        // 【第二次自旋】，当前线程已经创建 WaitNode 对象了，但是node对象还未入队
        else if (!queued)
            // waiters 指向队首，让当前 WaitNode 成为新的队首，【头插法】，失败说明其他线程修改了新的队首
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
        // 【第三次自旋】，会到这里，或者 else 内
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos &amp;lt;= 0L) {
                removeWaiter(q);
                return state;
            }
            // 阻塞指定的时间
            LockSupport.parkNanos(this, nanos);
        }
        // 条件成立：说明需要阻塞
        else
            // 【当前 get 操作的线程被 park 阻塞】，除非有其它线程将唤醒或者将当前线程中断
            LockSupport.park(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#report：封装运行结果，可以获取 run() 方法中设置的成员变量 outcome，&lt;strong&gt;这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private V report(int s) throws ExecutionException {
    // 获取执行结果，是在一个 futuretask 对象中的属性，可以直接获取
    Object x = outcome;
    // 当前任务状态正常结束
    if (s == NORMAL)
        return (V)x;	// 直接返回 callable 的逻辑结果
    // 当前任务被取消或者中断
    if (s &amp;gt;= CANCELLED)
        throw new CancellationException();		// 抛出异常
    // 执行到这里说明自定义的 callable 中的方法有异常，使用 outcome 上层抛出异常
    throw new ExecutionException((Throwable)x);	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FutureTask#cancel：任务取消，打断正在执行该任务的线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean cancel(boolean mayInterruptIfRunning) {
    // 条件一：表示当前任务处于运行中或者处于线程池任务队列中
    // 条件二：表示修改状态，成功可以去执行下面逻辑，否则返回 false 表示 cancel 失败
    if (!(state == NEW &amp;amp;&amp;amp;
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                                   mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {
        // 如果任务已经被执行，是否允许打断
        if (mayInterruptIfRunning) {
            try {
                // 获取执行当前 FutureTask 的线程
                Thread t = runner;
                if (t != null)
                    // 打断执行的线程
                    t.interrupt();
            } finally {
                // 设置任务状态为【中断完成】
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        // 唤醒所有 get() 阻塞的线程
        finishCompletion();
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;任务调度&lt;/h3&gt;
&lt;h4&gt;Timer&lt;/h4&gt;
&lt;p&gt;Timer 实现定时功能，Timer 的优点在于简单易用，但由于所有任务都是由同一个线程来调度，因此所有任务都是串行执行的，同一时间只能有一个任务在执行，前一个任务的延迟或异常都将会影响到之后的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void method1() {
    Timer timer = new Timer();
    TimerTask task1 = new TimerTask() {
        @Override
        public void run() {
            System.out.println(&quot;task 1&quot;);
            //int i = 1 / 0;//任务一的出错会导致任务二无法执行
            Thread.sleep(2000);
        }
    };
    TimerTask task2 = new TimerTask() {
        @Override
        public void run() {
            System.out.println(&quot;task 2&quot;);
        }
    };
    // 使用 timer 添加两个任务，希望它们都在 1s 后执行
	// 但由于 timer 内只有一个线程来顺序执行队列中的任务，因此任务1的延时，影响了任务2的执行
    timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1
    timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Scheduled&lt;/h4&gt;
&lt;p&gt;任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用内部类 ScheduledFutureTask 封装任务&lt;/li&gt;
&lt;li&gt;使用内部类 DelayedWorkQueue 作为线程池队列&lt;/li&gt;
&lt;li&gt;重写 onShutdown 方法去处理 shutdown 后的任务&lt;/li&gt;
&lt;li&gt;提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法，以便开发者进行扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;code&gt;Executors.newScheduledThreadPool(int corePoolSize)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 最大线程数固定为 Integer.MAX_VALUE，保活时间 keepAliveTime 固定为 0
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          // 阻塞队列是 DelayedWorkQueue
          new DelayedWorkQueue());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; schedule(Runnable/Callable&amp;lt;V&amp;gt;, long delay, TimeUnit u)&lt;/code&gt;：延迟执行任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; scheduleAtFixedRate(Runnable/Callable&amp;lt;V&amp;gt;, long initialDelay, long period, TimeUnit unit)&lt;/code&gt;：定时执行周期任务，不考虑执行的耗时，参数为初始延迟时间、间隔时间、单位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; scheduleWithFixedDelay(Runnable/Callable&amp;lt;V&amp;gt;, long initialDelay, long delay, TimeUnit unit)&lt;/code&gt;：定时执行周期任务，考虑执行的耗时，参数为初始延迟时间、间隔时间、单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;延迟任务，但是出现异常并不会在控制台打印，也不会影响其他线程的执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    // 线程池大小为1时也是串行执行
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    // 添加两个任务，都在 1s 后同时执行
    executor.schedule(() -&amp;gt; {
    	System.out.println(&quot;任务1，执行时间：&quot; + new Date());
        //int i = 1 / 0;
    	try { Thread.sleep(2000); } catch (InterruptedException e) { }
    }, 1000, TimeUnit.MILLISECONDS);
    
    executor.schedule(() -&amp;gt; {
    	System.out.println(&quot;任务2，执行时间：&quot; + new Date());
    }, 1000, TimeUnit.MILLISECONDS);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 scheduleAtFixedRate：&lt;strong&gt;一次任务的启动到下一次任务的启动&lt;/strong&gt;之间只要大于等于间隔时间，抢占到 CPU 就会立即执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    System.out.println(&quot;start...&quot; + new Date());
    
    pool.scheduleAtFixedRate(() -&amp;gt; {
        System.out.println(&quot;running...&quot; + new Date());
        Thread.sleep(2000);
    }, 1, 1, TimeUnit.SECONDS);
}

/*start...Sat Apr 24 18:08:12 CST 2021
running...Sat Apr 24 18:08:13 CST 2021
running...Sat Apr 24 18:08:15 CST 2021
running...Sat Apr 24 18:08:17 CST 2021
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 scheduleWithFixedDelay：&lt;strong&gt;一次任务的结束到下一次任务的启动之间&lt;/strong&gt;等于间隔时间，抢占到 CPU 就会立即执行，这个方法才是真正的设置两个任务之间的间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
    System.out.println(&quot;start...&quot; + new Date());
    
    pool.scheduleWithFixedDelay(() -&amp;gt; {
        System.out.println(&quot;running...&quot; + new Date());
        Thread.sleep(2000);
    }, 1, 1, TimeUnit.SECONDS);
}
/*start...Sat Apr 24 18:11:41 CST 2021
running...Sat Apr 24 18:11:42 CST 2021
running...Sat Apr 24 18:11:45 CST 2021
running...Sat Apr 24 18:11:48 CST 2021
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员属性&lt;/h4&gt;
&lt;h5&gt;成员变量&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;shutdown 后是否继续执行周期任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile boolean continueExistingPeriodicTasksAfterShutdown;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown 后是否继续执行延迟任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消方法是否将该任务从队列中移除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 默认 false，不移除，等到线程拿到任务之后抛弃
private volatile boolean removeOnCancel = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务的序列号，可以用来比较优先级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final AtomicLong sequencer = new AtomicLong();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;延迟任务&lt;/h5&gt;
&lt;p&gt;ScheduledFutureTask 继承 FutureTask，实现 RunnableScheduledFuture 接口，具有延迟执行的特点，覆盖 FutureTask 的 run 方法来实现对&lt;strong&gt;延时执行、周期执行&lt;/strong&gt;的支持。对于延时任务调用 FutureTask#run，而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列&lt;/p&gt;
&lt;p&gt;在调度线程池中无论是 runnable 还是 callable，无论是否需要延迟和定时，所有的任务都会被封装成 ScheduledFutureTask&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;任务序列号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final long sequenceNumber;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long time;			// 任务可以被执行的时间，交付时间，以纳秒表示
private final long period;	// 0 表示非周期任务，正数表示 fixed-rate 模式的周期，负数表示 fixed-delay 模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fixed-rate：两次开始启动的间隔，fixed-delay：一次执行结束到下一次开始启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实际的任务对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RunnableScheduledFuture&amp;lt;V&amp;gt; outerTask = this;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务在队列数组中的索引下标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DelayedWorkQueue 底层使用的数据结构是最小堆，记录当前任务在堆中的索引，-1 代表删除
int heapIndex;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    // 任务的触发时间
    this.time = ns;
    // 任务的周期，多长时间执行一次
    this.period = period;
    // 任务的序号
    this.sequenceNumber = sequencer.getAndIncrement();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;compareTo()：ScheduledFutureTask 根据执行时间 time 正序排列，如果执行时间相同，在按照序列号 sequenceNumber 正序排列，任务需要放入 DelayedWorkQueue，延迟队列中使用该方法按照从小到大进行排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        // 类型强转
        ScheduledFutureTask&amp;lt;?&amp;gt; x = (ScheduledFutureTask&amp;lt;?&amp;gt;)other;
        // 比较者 - 被比较者的执行时间
        long diff = time - x.time;
        // 比较者先执行
        if (diff &amp;lt; 0)
            return -1;
        // 被比较者先执行
        else if (diff &amp;gt; 0)
            return 1;
        // 比较者的序列号小
        else if (sequenceNumber &amp;lt; x.sequenceNumber)
            return -1;
        else
            return 1;
    }
    // 不是 ScheduledFutureTask 类型时，根据延迟时间排序
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff &amp;lt; 0) ? -1 : (diff &amp;gt; 0) ? 1 : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;run()：执行任务，非周期任务直接完成直接结束，&lt;strong&gt;周期任务执行完后会设置下一次的执行时间，重新放入线程池的阻塞队列&lt;/strong&gt;，如果线程池中的线程数量少于核心线程，就会添加 Worker 开启新线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    // 是否周期性，就是判断 period 是否为 0
    boolean periodic = isPeriodic();
    // 根据是否是周期任务检查当前状态能否执行任务，不能执行就取消任务
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 非周期任务，直接调用 FutureTask#run 执行
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 周期任务的执行，返回 true 表示执行成功
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 设置周期任务的下一次执行时间
        setNextRunTime();
        // 任务的下一次执行安排，如果当前线程池状态可以执行周期任务，加入队列，并开启新线程
        reExecutePeriodic(outerTask);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;周期任务正常完成后&lt;strong&gt;任务的状态不会变化&lt;/strong&gt;，依旧是 NEW，不会设置 outcome 属性。但是如果本次任务执行出现异常，会进入 setException 方法将任务状态置为异常，把异常保存在 outcome 中，方法返回 false，后续的该任务将不会再周期的执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected boolean runAndReset() {
    // 任务不是新建的状态了，或者被别的线程执行了，直接返回 false
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        Callable&amp;lt;V&amp;gt; c = callable;
        if (c != null &amp;amp;&amp;amp; s == NEW) {
            try {
                // 执行方法，没有返回值
                c.call();
                ran = true;
            } catch (Throwable ex) {
                // 出现异常，把任务设置为异常状态，唤醒所有的 get 阻塞线程
                setException(ex);
            }
        }
    } finally {
		// 执行完成把执行线程引用置为 null
        runner = null;
        s = state;
        // 如果线程被中断进行中断处理
        if (s &amp;gt;= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    // 如果正常执行，返回 true，并且任务状态没有被取消
    return ran &amp;amp;&amp;amp; s == NEW;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 任务下一次的触发时间
private void setNextRunTime() {
    long p = period;
    if (p &amp;gt; 0)
        // fixed-rate 模式，【时间设置为上一次执行任务的时间 + p】，两次任务执行的时间差
        time += p;
    else
        // fixed-delay 模式，下一次执行时间是【当前这次任务结束的时间（就是现在） + delay 值】
        time = triggerTime(-p);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reExecutePeriodic()&lt;strong&gt;：准备任务的下一次执行，重新放入阻塞任务队列&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ScheduledThreadPoolExecutor#reExecutePeriodic
void reExecutePeriodic(RunnableScheduledFuture&amp;lt;?&amp;gt; task) {
    if (canRunInCurrentRunState(true)) {
        // 【放入任务队列】
        super.getQueue().add(task);
        // 如果提交完任务之后，线程池状态变为了 shutdown 状态，需要再次检查是否可以执行，
        // 如果不能执行且任务还在队列中未被取走，则取消任务
        if (!canRunInCurrentRunState(true) &amp;amp;&amp;amp; remove(task))
            task.cancel(false);
        else
            // 当前线程池状态可以执行周期任务，加入队列，并【根据线程数量是否大于核心线程数确定是否开启新线程】
            ensurePrestart();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cancel()：取消任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean cancel(boolean mayInterruptIfRunning) {
    // 调用父类 FutureTask#cancel 来取消任务
    boolean cancelled = super.cancel(mayInterruptIfRunning);
    // removeOnCancel 用于控制任务取消后是否应该从阻塞队列中移除
    if (cancelled &amp;amp;&amp;amp; removeOnCancel &amp;amp;&amp;amp; heapIndex &amp;gt;= 0)
        // 从等待队列中删除该任务，并调用 tryTerminate() 判断是否需要停止线程池
        remove(this);
    return cancelled;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;延迟队列&lt;/h5&gt;
&lt;p&gt;DelayedWorkQueue 是支持延时获取元素的阻塞队列，内部采用优先队列 PriorityQueue（小根堆、满二叉树）存储元素&lt;/p&gt;
&lt;p&gt;其他阻塞队列存储节点的数据结构大都是链表，&lt;strong&gt;延迟队列是数组&lt;/strong&gt;，所以延迟队列出队头元素后需要&lt;strong&gt;让其他元素（尾）替换到头节点&lt;/strong&gt;，防止空指针异常&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int INITIAL_CAPACITY = 16;			// 初始容量
private int size = 0;									// 节点数量
private RunnableScheduledFuture&amp;lt;?&amp;gt;[] queue = 
    new RunnableScheduledFuture&amp;lt;?&amp;gt;[INITIAL_CAPACITY];	// 存放节点
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ReentrantLock lock = new ReentrantLock();	// 控制并发
private final Condition available = lock.newCondition();// 条件队列
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阻塞等待头节点的线程：线程池内的某个线程去 take() 获取任务时，如果延迟队列顶层节点不为 null（队列内有任务），但是节点任务还不到触发时间，线程就去检查&lt;strong&gt;队列的 leader字段&lt;/strong&gt;是否被占用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果未被占用，则当前线程占用该字段，然后当前线程到 available 条件队列指定超时时间 &lt;code&gt;堆顶任务.time - now()&lt;/code&gt; 挂起&lt;/li&gt;
&lt;li&gt;如果被占用，当前线程直接到 available 条件队列不指定超时时间的挂起&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// leader 在 available 条件队列内是首元素，它超时之后会醒过来，然后再次将堆顶元素获取走，获取走之后，take()结束之前，会调用是 available.signal() 唤醒下一个条件队列内的等待者，然后释放 lock，下一个等待者被唤醒后去到 AQS 队列，做 acquireQueue(node) 逻辑
private Thread leader = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;offer()：插入节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean offer(Runnable x) {
    // 判空
    if (x == null)
        throw new NullPointerException();
    RunnableScheduledFuture&amp;lt;?&amp;gt; e = (RunnableScheduledFuture&amp;lt;?&amp;gt;)x;
    // 队列锁，增加删除数据时都要加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = size;
        // 队列数量大于存放节点的数组长度，需要扩容
        if (i &amp;gt;= queue.length)
            // 扩容为原来长度的 1.5 倍
            grow();
        size = i + 1;
        // 当前是第一个要插入的节点
        if (i == 0) {
            queue[0] = e;
            // 修改 ScheduledFutureTask 的 heapIndex 属性，表示该对象在队列里的下标
            setIndex(e, 0);
        } else {
            // 向上调整元素的位置，并更新 heapIndex 
            siftUp(i, e);
        }
        // 情况1：当前任务是第一个加入到 queue 内的任务，所以在当前任务加入到 queue 之前，take() 线程会直接
        //		到 available 队列不设置超时的挂起，并不会去占用 leader 字段，这时需会唤醒一个线程 让它去消费
       	// 情况2：当前任务【优先级最高】，原堆顶任务可能还未到触发时间，leader 线程设置超时的在 available 挂起
        //		原先的 leader 等待的是原先的头节点，所以 leader 已经无效，需要将 leader 线程唤醒，
        //		唤醒之后它会检查堆顶，如果堆顶任务可以被消费，则直接获取走，否则继续成为 leader 等待新堆顶任务
        if (queue[0] == e) {
            // 将 leader 设置为 null
            leader = null;
            // 直接随便唤醒等待头结点的阻塞线程
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 插入新节点后对堆进行调整，进行节点上移，保持其特性【节点的值小于子节点的值】，小顶堆
private void siftUp(int k, RunnableScheduledFuture&amp;lt;?&amp;gt; key) {
    while (k &amp;gt; 0) {
        // 父节点，就是堆排序
        int parent = (k - 1) &amp;gt;&amp;gt;&amp;gt; 1;
        RunnableScheduledFuture&amp;lt;?&amp;gt; e = queue[parent];
        // key 和父节点比，如果大于父节点可以直接返回，否则就继续上浮
        if (key.compareTo(e) &amp;gt;= 0)
            break;
        queue[k] = e;
        setIndex(e, k);
        k = parent;
    }
    queue[k] = key;
    setIndex(key, k);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;poll()：非阻塞获取头结点，&lt;strong&gt;获取执行时间最近并且可以执行的&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 非阻塞获取
public RunnableScheduledFuture&amp;lt;?&amp;gt; poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取队头节点，因为是小顶堆
        RunnableScheduledFuture&amp;lt;?&amp;gt; first = queue[0];
        // 头结点为空或者的延迟时间没到返回 null
        if (first == null || first.getDelay(NANOSECONDS) &amp;gt; 0)
            return null;
        else
            // 头结点达到延迟时间，【尾节点成为替代节点下移调整堆结构】，返回头结点
            return finishPoll(first);
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private RunnableScheduledFuture&amp;lt;?&amp;gt; finishPoll(RunnableScheduledFuture&amp;lt;?&amp;gt; f) {
    // 获取尾索引
    int s = --size;
    // 获取尾节点
    RunnableScheduledFuture&amp;lt;?&amp;gt; x = queue[s];
    // 将堆结构最后一个节点占用的 slot 设置为 null，因为该节点要尝试升级成堆顶，会根据特性下调
    queue[s] = null;
    // s == 0 说明 当前堆结构只有堆顶一个节点，此时不需要做任何的事情
    if (s != 0)
        // 从索引处 0 开始向下调整
        siftDown(0, x);
    // 出队的元素索引设置为 -1
    setIndex(f, -1);
    return f;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;take()：阻塞获取头节点，读取当前堆中最小的也就是触发时间最近的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public RunnableScheduledFuture&amp;lt;?&amp;gt; take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 保证线程安全
    lock.lockInterruptibly();
    try {
        for (;;) {
            // 头节点
            RunnableScheduledFuture&amp;lt;?&amp;gt; first = queue[0];
            if (first == null)
                // 等待队列不空，直至有任务通过 offer 入队并唤醒
                available.await();
            else {
                // 获取头节点的延迟时间是否到时
                long delay = first.getDelay(NANOSECONDS);
                if (delay &amp;lt;= 0)
                    // 到达触发时间，获取头节点并调整堆，重新选择延迟时间最小的节点放入头部
                    return finishPoll(first);
                
                // 逻辑到这说明头节点的延迟时间还没到
                first = null;
                // 说明有 leader 线程在等待获取头节点，当前线程直接去阻塞等待
                if (leader != null)
                    available.await();
                else {
                    // 没有 leader 线程，【当前线程作为leader线程，并设置头结点的延迟时间作为阻塞时间】
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 在条件队列 available 使用带超时的挂起（堆顶任务.time - now() 纳秒值..）
                        available.awaitNanos(delay);
                        // 到达阻塞时间时，当前线程会从这里醒来来
                    } finally {
                        // t堆顶更新，leader 置为 null，offer 方法释放锁后，
                        // 有其它线程通过 take/poll 拿到锁,读到 leader == null，然后将自身更新为leader。
                        if (leader == thisThread)
                            // leader 置为 null 用以接下来判断是否需要唤醒后继线程
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 没有 leader 线程，头结点不为 null，唤醒阻塞获取头节点的线程，
        // 【如果没有这一步，就会出现有了需要执行的任务，但是没有线程去执行】
        if (leader == null &amp;amp;&amp;amp; queue[0] != null)
            available.signal();
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：删除节点，堆移除一个元素的时间复杂度是 O(log n)，&lt;strong&gt;延迟任务维护了 heapIndex&lt;/strong&gt;，直接访问的时间复杂度是 O(1)，从而可以更快的移除元素，任务在队列中被取消后会进入该逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 查找对象在队列数组中的下标
        int i = indexOf(x);
        // 节点不存在，返回 false
        if (i &amp;lt; 0)
            return false;
		// 修改元素的 heapIndex，-1 代表删除
        setIndex(queue[i], -1);
        // 尾索引是长度-1
        int s = --size;
        // 尾节点作为替代节点
        RunnableScheduledFuture&amp;lt;?&amp;gt; replacement = queue[s];
        queue[s] = null;
        // s == i 说明头节点就是尾节点，队列空了
        if (s != i) {
            // 向下调整
            siftDown(i, replacement);
            // 说明没发生调整
            if (queue[i] == replacement)
                // 上移和下移不可能同时发生，替代节点大于子节点时下移，否则上移
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;h5&gt;提交任务&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;schedule()：延迟执行方法，并指定执行的时间，默认是当前时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void execute(Runnable command) {
    // 以零延时任务的形式实现
    schedule(command, 0, NANOSECONDS);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public ScheduledFuture&amp;lt;?&amp;gt; schedule(Runnable command, long delay, TimeUnit unit) {
    // 判空
    if (command == null || unit == null) throw new NullPointerException();
    // 没有做任何操作，直接将 task 返回，该方法主要目的是用于子类扩展，并且【根据延迟时间设置任务触发的时间点】
    RunnableScheduledFuture&amp;lt;?&amp;gt; t = decorateTask(command, new ScheduledFutureTask&amp;lt;Void&amp;gt;(
        											command, null, triggerTime(delay, unit)));
    // 延迟执行
    delayedExecute(t);
    return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 返回【当前时间 + 延迟时间】，就是触发当前任务执行的时间
private long triggerTime(long delay, TimeUnit unit) {
    // 设置触发的时间
    return triggerTime(unit.toNanos((delay &amp;lt; 0) ? 0 : delay));
}
long triggerTime(long delay) {
    // 如果 delay &amp;lt; Long.Max_VALUE/2，则下次执行时间为当前时间 +delay
    // 否则为了避免队列中出现由于溢出导致的排序紊乱,需要调用overflowFree来修正一下delay
    return now() + ((delay &amp;lt; (Long.MAX_VALUE &amp;gt;&amp;gt; 1)) ? delay : overflowFree(delay));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;overflowFree 的原因：如果某个任务的 delay 为负数，说明当前可以执行（其实早该执行了）。阻塞队列中维护任务顺序是基于 compareTo 比较的，比较两个任务的顺序会用 time 相减。那么可能出现一个 delay 为正数减去另一个为负数的 delay，结果上溢为负数，则会导致 compareTo 产生错误的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long overflowFree(long delay) {
    Delayed head = (Delayed) super.getQueue().peek();
    if (head != null) {
        long headDelay = head.getDelay(NANOSECONDS);
        // 判断一下队首的delay是不是负数，如果是正数就不用管，怎么减都不会溢出
        // 否则拿当前 delay 减去队首的 delay 来比较看，如果不出现上溢，排序不会乱
		// 不然就把当前 delay 值给调整为 Long.MAX_VALUE + 队首 delay
        if (headDelay &amp;lt; 0 &amp;amp;&amp;amp; (delay - headDelay &amp;lt; 0))
            delay = Long.MAX_VALUE + headDelay;
    }
    return delay;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scheduleAtFixedRate()：定时执行，一次任务的启动到下一次任务的启动的间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ScheduledFuture&amp;lt;?&amp;gt; scheduleAtFixedRate(Runnable command, long initialDelay, long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period &amp;lt;= 0)
        throw new IllegalArgumentException();
    // 任务封装，【指定初始的延迟时间和周期时间】
    ScheduledFutureTask&amp;lt;Void&amp;gt; sft =new ScheduledFutureTask&amp;lt;Void&amp;gt;(command, null,
                                      triggerTime(initialDelay, unit), unit.toNanos(period));
    // 默认返回本身
    RunnableScheduledFuture&amp;lt;Void&amp;gt; t = decorateTask(command, sft);
    sft.outerTask = t;
    // 开始执行这个任务
    delayedExecute(t);
    return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scheduleWithFixedDelay()：定时执行，一次任务的结束到下一次任务的启动的间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ScheduledFuture&amp;lt;?&amp;gt; scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null) 
        throw new NullPointerException();
    if (delay &amp;lt;= 0)
        throw new IllegalArgumentException();
    // 任务封装，【指定初始的延迟时间和周期时间】，周期时间为 - 表示是 fixed-delay 模式
    ScheduledFutureTask&amp;lt;Void&amp;gt; sft = new ScheduledFutureTask&amp;lt;Void&amp;gt;(command, null,
                                      triggerTime(initialDelay, unit), unit.toNanos(-delay));
    RunnableScheduledFuture&amp;lt;Void&amp;gt; t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;运行任务&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;delayedExecute()：&lt;strong&gt;校验线程池状态&lt;/strong&gt;，延迟或周期性任务的主要执行方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void delayedExecute(RunnableScheduledFuture&amp;lt;?&amp;gt; task) {
    // 线程池是 SHUTDOWN 状态，需要执行拒绝策略
    if (isShutdown())
        reject(task);
    else {
        // 把当前任务放入阻塞队列，因为需要【获取执行时间最近的】，当前任务需要比较
        super.getQueue().add(task);
        // 线程池状态为 SHUTDOWN 并且不允许执行任务了，就从队列删除该任务，并设置任务的状态为取消状态
        if (isShutdown() &amp;amp;&amp;amp; !canRunInCurrentRunState(task.isPeriodic()) &amp;amp;&amp;amp; remove(task))
            task.cancel(false);
        else
            // 可以执行
            ensurePrestart();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ensurePrestart()：&lt;strong&gt;开启线程执行任务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ThreadPoolExecutor#ensurePrestart
void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    // worker数目小于corePoolSize，则添加一个worker。
    if (wc &amp;lt; corePoolSize)
        // 第二个参数 true 表示采用核心线程数量限制，false 表示采用 maximumPoolSize
        addWorker(null, true);
    // corePoolSize = 0的情况，至少开启一个线程，【担保机制】
    else if (wc == 0)
        addWorker(null, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;canRunInCurrentRunState()：任务运行时都会被调用以校验当前状态是否可以运行任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean canRunInCurrentRunState(boolean periodic) {
    // 根据是否是周期任务判断，在线程池 shutdown 后是否继续执行该任务，默认非周期任务是继续执行的
    return isRunningOrShutdown(periodic ? continueExistingPeriodicTasksAfterShutdown :
                               executeExistingDelayedTasksAfterShutdown);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onShutdown()：删除并取消工作队列中的不需要再执行的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void onShutdown() {
    BlockingQueue&amp;lt;Runnable&amp;gt; q = super.getQueue();
    // shutdown 后是否仍然执行延时任务
    boolean keepDelayed = getExecuteExistingDelayedTasksAfterShutdownPolicy();
    // shutdown 后是否仍然执行周期任务
    boolean keepPeriodic = getContinueExistingPeriodicTasksAfterShutdownPolicy();
    // 如果两者皆不可，则对队列中【所有任务】调用 cancel 取消并清空队列
    if (!keepDelayed &amp;amp;&amp;amp; !keepPeriodic) {
        for (Object e : q.toArray())
            if (e instanceof RunnableScheduledFuture&amp;lt;?&amp;gt;)
                ((RunnableScheduledFuture&amp;lt;?&amp;gt;) e).cancel(false);
        q.clear();
    }
    else {
        for (Object e : q.toArray()) {
            if (e instanceof RunnableScheduledFuture) {
                RunnableScheduledFuture&amp;lt;?&amp;gt; t = (RunnableScheduledFuture&amp;lt;?&amp;gt;)e;
                // 不需要执行的任务删除并取消，已经取消的任务也需要从队列中删除
                if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
                    t.isCancelled()) {
                    if (q.remove(t))
                        t.cancel(false);
                }
            }
        }
    }
    // 因为任务被从队列中清理掉，所以需要调用 tryTerminate 尝试【改变线程池的状态】
    tryTerminate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ForkJoin&lt;/h3&gt;
&lt;p&gt;Fork/Join：线程池的实现，体现是分治思想，适用于能够进行任务拆分的 CPU 密集型运算，用于&lt;strong&gt;并行计算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;任务拆分：将一个大任务拆分为算法上相同的小任务，直至不能拆分可以直接求解。跟递归相关的一些计算，如归并排序、斐波那契数列都可以用分治思想进行求解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fork/Join 在&lt;strong&gt;分治的基础上加入了多线程&lt;/strong&gt;，把每个任务的分解和合并交给不同的线程来完成，提升了运算效率&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ForkJoin 使用 ForkJoinPool 来启动，是一个特殊的线程池，默认会创建与 CPU 核心数大小相同的线程池&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务有返回值继承 RecursiveTask，没有返回值继承 RecursiveAction&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ForkJoinPool pool = new ForkJoinPool(4);
    System.out.println(pool.invoke(new MyTask(5)));
    //拆分  5 + MyTask(4) --&amp;gt; 4 + MyTask(3) --&amp;gt;
}

// 1~ n 之间整数的和
class MyTask extends RecursiveTask&amp;lt;Integer&amp;gt; {
    private int n;

    public MyTask(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return &quot;MyTask{&quot; + &quot;n=&quot; + n + &apos;}&apos;;
    }

    @Override
    protected Integer compute() {
        // 如果 n 已经为 1，可以求得结果了
        if (n == 1) {
            return n;
        }
        // 将任务进行拆分(fork)
        MyTask t1 = new MyTask(n - 1);
        t1.fork();
        // 合并(join)结果
        int result = n + t1.join();
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续拆分优化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AddTask extends RecursiveTask&amp;lt;Integer&amp;gt; {
    int begin;
    int end;
    public AddTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }
    
    @Override
    public String toString() {
        return &quot;{&quot; + begin + &quot;,&quot; + end + &apos;}&apos;;
    }
    
    @Override
    protected Integer compute() {
        // 5, 5
        if (begin == end) {
            return begin;
        }
        // 4, 5  防止多余的拆分  提高效率
        if (end - begin == 1) {
            return end + begin;
        }
        // 1 5
        int mid = (end + begin) / 2; // 3
        AddTask t1 = new AddTask(begin, mid); // 1,3
        t1.fork();
        AddTask t2 = new AddTask(mid + 1, end); // 4,5
        t2.fork();
        int result = t1.join() + t2.join();
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ForkJoinPool 实现了&lt;strong&gt;工作窃取算法&lt;/strong&gt;来提高 CPU 的利用率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程都维护了一个&lt;strong&gt;双端队列&lt;/strong&gt;，用来存储需要执行的任务&lt;/li&gt;
&lt;li&gt;工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行&lt;/li&gt;
&lt;li&gt;窃取的必须是&lt;strong&gt;最晚的任务&lt;/strong&gt;，避免和队列所属线程发生竞争，但是队列中只有一个任务时还是会发生竞争&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;享元模式&lt;/h3&gt;
&lt;p&gt;享元模式（Flyweight pattern）： 用于减少创建对象的数量，以减少内存占用和提高性能，这种类型的设计模式属于结构型模式，它提供了减少对象数量从而改善应用所需的对象结构的方式&lt;/p&gt;
&lt;p&gt;异步模式：让有限的工作线程（Worker Thread）来轮流异步处理无限多的任务，也可将其归类为分工模式，典型实现就是线程池&lt;/p&gt;
&lt;p&gt;工作机制：享元模式尝试重用现有的同类对象，如果未找到匹配的对象，则创建新对象&lt;/p&gt;
&lt;p&gt;自定义连接池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Pool pool = new Pool(2);
    for (int i = 0; i &amp;lt; 5; i++) {
        new Thread(() -&amp;gt; {
            Connection con = pool.borrow();
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pool.free(con);
        }).start();
    }
}
class Pool {
    //连接池的大小
    private final int poolSize;
    //连接对象的数组
    private Connection[] connections;
    //连接状态数组 0表示空闲  1表示繁忙
    private AtomicIntegerArray states;  //int[] -&amp;gt; AtomicIntegerArray

    //构造方法
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i &amp;lt; poolSize; i++) {
            connections[i] = new MockConnection(&quot;连接&quot; + (i + 1));
        }
    }

    //使用连接
    public Connection borrow() {
        while (true) {
            for (int i = 0; i &amp;lt; poolSize; i++) {
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        System.out.println(Thread.currentThread().getName() + &quot; borrow &quot; +  connections[i]);
                        return connections[i];
                    }
                }
            }
            //如果没有空闲连接，当前线程等待
            synchronized (this) {
                try {
                    System.out.println(Thread.currentThread().getName() + &quot; wait...&quot;);
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //归还连接
    public void free(Connection con) {
        for (int i = 0; i &amp;lt; poolSize; i++) {
            if (connections[i] == con) {//判断是否是同一个对象
                states.set(i, 0);//不用cas的原因是只会有一个线程使用该连接
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName() + &quot; free &quot; + con);
                    this.notifyAll();
                }
                break;
            }
        }
    }

}

class MockConnection implements Connection {
    private String name;
    //.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;同步器&lt;/h2&gt;
&lt;h3&gt;AQS&lt;/h3&gt;
&lt;h4&gt;核心思想&lt;/h4&gt;
&lt;p&gt;AQS：AbstractQueuedSynchronizer，是阻塞式锁和相关的同步器工具的框架，许多同步类实现都依赖于该同步器&lt;/p&gt;
&lt;p&gt;AQS 用状态属性来表示资源的状态（分&lt;strong&gt;独占模式和共享模式&lt;/strong&gt;），子类需要定义如何维护这个状态，控制如何获取锁和释放锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;独占模式是只有一个线程能够访问资源，如 ReentrantLock&lt;/li&gt;
&lt;li&gt;共享模式允许多个线程访问资源，如 Semaphore，ReentrantReadWriteLock 是组合式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AQS 核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果被请求的共享资源空闲，则将当前请求资源的线程设置为有效的工作线程，并将共享资源设置锁定状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求的共享资源被占用，AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制，将暂时获取不到锁的线程加入到队列中&lt;/p&gt;
&lt;p&gt;CLH 是一种基于单向链表的&lt;strong&gt;高性能、公平的自旋锁&lt;/strong&gt;，AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点（Node）来实现锁的分配&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-AQS原理图.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设计原理&lt;/h4&gt;
&lt;p&gt;设计原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(state 状态不允许获取) {	// tryAcquire(arg)
    if(队列中还没有此线程) {
        入队并阻塞 park
    }
}
当前线程出队
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(state 状态允许了) {	// tryRelease(arg)
	恢复阻塞的线程(s) unpark
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AbstractQueuedSynchronizer 中 state 设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;state 使用了 32bit int 来维护同步状态，独占模式 0 表示未加锁状态，大于 0 表示已经加锁状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int state;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;state &lt;strong&gt;使用 volatile 修饰配合 cas&lt;/strong&gt; 保证其修改时的原子性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;state 表示&lt;strong&gt;线程重入的次数（独占模式）或者剩余许可数（共享模式）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;state API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;protected final int getState()&lt;/code&gt;：获取 state 状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected final void setState(int newState)&lt;/code&gt;：设置 state 状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected final boolean compareAndSetState(int expect,int update)&lt;/code&gt;：&lt;strong&gt;CAS&lt;/strong&gt; 安全设置 state&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;封装线程的 Node 节点中 waitstate 设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;strong&gt;volatile 修饰配合 CAS&lt;/strong&gt; 保证其修改时的原子性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表示 Node 节点的状态，有以下几种状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 默认为 0
volatile int waitStatus;
// 由于超时或中断，此节点被取消，不会再改变状态
static final int CANCELLED =  1;
// 此节点后面的节点已（或即将）被阻止（通过park），【当前节点在释放或取消时必须唤醒后面的节点】
static final int SIGNAL    = -1;
// 此节点当前在条件队列中
static final int CONDITION = -2;
// 将releaseShared传播到其他节点
static final int PROPAGATE = -3;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阻塞恢复设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 park &amp;amp; unpark 来实现线程的暂停和恢复，因为命令的先后顺序不影响结果&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark 是针对线程的，而不是针对同步器的，因此控制粒度更为精细&lt;/li&gt;
&lt;li&gt;park 线程可以通过 interrupt 打断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;队列设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用了 FIFO 先入先出队列，并不支持优先级队列，&lt;strong&gt;同步队列是双向链表，便于出队入队&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 头结点，指向哑元节点
private transient volatile Node head;
// 阻塞队列的尾节点，阻塞队列不包含头结点，从 head.next → tail 认为是阻塞队列
private transient volatile Node tail;

static final class Node {
    // 枚举：共享模式
    static final Node SHARED = new Node();
    // 枚举：独占模式
    static final Node EXCLUSIVE = null;
    // node 需要构建成 FIFO 队列，prev 指向前继节点
    volatile Node prev;
    // next 指向后继节点
    volatile Node next;
    // 当前 node 封装的线程
    volatile Thread thread;
    // 条件队列是单向链表，只有后继指针，条件队列使用该属性
    Node nextWaiter;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-AQS%E9%98%9F%E5%88%97%E8%AE%BE%E8%AE%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件变量来实现等待、唤醒机制，支持多个条件变量，类似于 Monitor 的 WaitSet，&lt;strong&gt;条件队列是单向链表&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public class ConditionObject implements Condition, java.io.Serializable {
     // 指向条件队列的第一个 node 节点
     private transient Node firstWaiter;
     // 指向条件队列的最后一个 node 节点
     private transient Node lastWaiter;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;模板对象&lt;/h4&gt;
&lt;p&gt;同步器的设计是基于模板方法模式，该模式是基于继承的，主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用者继承 &lt;code&gt;AbstractQueuedSynchronizer&lt;/code&gt; 并重写指定的方法&lt;/li&gt;
&lt;li&gt;将 AQS 组合在自定义同步组件的实现中，并调用其模板方法，这些模板方法会调用使用者重写的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AQS 使用了模板方法模式，自定义同步器时需要重写下面几个 AQS 提供的模板方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isHeldExclusively()		//该线程是否正在独占资源。只有用到condition才需要去实现它
tryAcquire(int)			//独占方式。尝试获取资源，成功则返回true，失败则返回false
tryRelease(int)			//独占方式。尝试释放资源，成功则返回true，失败则返回false
tryAcquireShared(int)	//共享方式。尝试获取资源。负数表示失败；0表示成功但没有剩余可用资源；正数表示成功且有剩余资源
tryReleaseShared(int)	//共享方式。尝试释放资源，成功则返回true，失败则返回false
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，每个方法都抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这些方法的实现必须是内部线程安全的&lt;/li&gt;
&lt;li&gt;AQS 类中的其他方法都是 final ，所以无法被其他类使用，只有这几个方法可以被其他类使用&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;自定义&lt;/h4&gt;
&lt;p&gt;自定义一个不可重入锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyLock implements Lock {
    //独占锁 不可重入
    class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                // 加上锁 设置 owner 为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        @Override   //解锁
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);//volatile 修饰的变量放在后面，防止指令重排
            return true;
        }
        @Override   //是否持有独占锁
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    @Override   //加锁（不成功进入等待队列等待）
    public void lock() {
        sync.acquire(1);
    }

    @Override   //加锁 可打断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override   //尝试加锁，尝试一次
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override   //尝试加锁，带超时
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    
    @Override   //解锁
    public void unlock() {
        sync.release(1);
    }
    
    @Override   //条件变量
    public Condition newCondition() {
        return sync.newCondition();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Re-Lock&lt;/h3&gt;
&lt;h4&gt;锁对比&lt;/h4&gt;
&lt;p&gt;ReentrantLock 相对于 synchronized 具备如下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;锁的实现：synchronized 是 JVM 实现的，而 ReentrantLock 是 JDK 实现的&lt;/li&gt;
&lt;li&gt;性能：新版本 Java 对 synchronized 进行了很多优化，synchronized 与 ReentrantLock 大致相同&lt;/li&gt;
&lt;li&gt;使用：ReentrantLock 需要手动解锁，synchronized 执行完代码块自动解锁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可中断&lt;/strong&gt;：ReentrantLock 可中断，而 synchronized 不行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平锁&lt;/strong&gt;：公平锁是指多个线程在等待同一个锁时，必须按照申请锁的时间顺序来依次获得锁
&lt;ul&gt;
&lt;li&gt;ReentrantLock 可以设置公平锁，synchronized 中的锁是非公平的&lt;/li&gt;
&lt;li&gt;不公平锁的含义是阻塞队列内公平，队列外非公平&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;锁超时：尝试获取锁，超时获取不到直接放弃，不进入阻塞队列
&lt;ul&gt;
&lt;li&gt;ReentrantLock 可以设置超时时间，synchronized 会一直等待&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;锁绑定多个条件：一个 ReentrantLock 可以同时绑定多个 Condition 对象，更细粒度的唤醒线程&lt;/li&gt;
&lt;li&gt;两者都是可重入锁&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;使用锁&lt;/h4&gt;
&lt;p&gt;构造方法：&lt;code&gt;ReentrantLock lock = new ReentrantLock();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;ReentrantLock 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public void lock()&lt;/code&gt;：获得锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果锁没有被另一个线程占用，则将锁定计数设置为 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前线程已经保持锁定，则保持计数增加 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果锁被另一个线程保持，则当前线程被禁用线程调度，并且在锁定已被获取之前处于休眠状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public void unlock()&lt;/code&gt;：尝试释放锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前线程是该锁的持有者，则保持计数递减&lt;/li&gt;
&lt;li&gt;如果保持计数现在为零，则锁定被释放&lt;/li&gt;
&lt;li&gt;如果当前线程不是该锁的持有者，则抛出异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;公平锁&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;构造方法：&lt;code&gt;ReentrantLock lock = new ReentrantLock(true)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ReentrantLock 默认是不公平的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ReentrantLock() {
    sync = new NonfairSync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：公平锁一般没有必要，会降低并发度&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;非公原理&lt;/h5&gt;
&lt;h6&gt;加锁&lt;/h6&gt;
&lt;p&gt;NonfairSync 继承自 AQS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void lock() {
    sync.lock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没有竞争：ExclusiveOwnerThread 属于 Thread-0，state 设置为 1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ReentrantLock.NonfairSync#lock
final void lock() {
    // 用 cas 尝试（仅尝试一次）将 state 从 0 改为 1, 如果成功表示【获得了独占锁】
    if (compareAndSetState(0, 1))
        // 设置当前线程为独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);//失败进入
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一个竞争出现：Thread-1 执行，CAS 尝试将 state 由 0 改为 1，结果失败（第一次），进入 acquire 逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
    // tryAcquire 尝试获取锁失败时, 会调用 addWaiter 将当前线程封装成node入队，acquireQueued 阻塞当前线程，
    // acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过，false 表示未被中断过
    if (!tryAcquire(arg) &amp;amp;&amp;amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果线程被中断了逻辑来到这，完成一次真正的打断效果
        selfInterrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁1.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进入 tryAcquire 尝试获取锁逻辑，这时 state 已经是1，结果仍然失败（第二次），加锁成功有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前 AQS 处于无锁状态&lt;/li&gt;
&lt;li&gt;加锁线程就是当前线程，说明发生了锁重入&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// 抢占成功返回 true，抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // state 值
    int c = getState();
    // 条件成立说明当前处于【无锁状态】
    if (c == 0) {
        //如果还没有获得锁，尝试用cas获得，这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁        
    	if (compareAndSetState(0, acquires)) {
            // 获取锁成功设置当前线程为独占锁线程。
            setExclusiveOwnerThread(current);
            return true;
         }    
	}    
   	// 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】
	else if (current == getExclusiveOwnerThread()) {
        // 更新锁重入的值
        int nextc = c + acquires;
        // 越界判断，当重入的深度很深时，会导致 nextc &amp;lt; 0，int值达到最大之后再 + 1 变负数
        if (nextc &amp;lt; 0) // overflow
            throw new Error(&quot;Maximum lock count exceeded&quot;);
        // 更新 state 的值，这里不使用 cas 是因为当前线程正在持有锁，所以这里的操作相当于在一个管程内
        setState(nextc);
        return true;
    }
    // 获取失败
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来进入 addWaiter 逻辑，构造 Node 队列（不是阻塞队列），前置条件是当前线程获取锁失败，说明有线程占用了锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图中黄色三角表示该 Node 的 waitStatus 状态，其中 0 为默认&lt;strong&gt;正常状态&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Node 的创建是懒惰的，其中第一个 Node 称为 &lt;strong&gt;Dummy（哑元）或哨兵&lt;/strong&gt;，用来占位，并不关联线程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// AbstractQueuedSynchronizer#addWaiter，返回当前线程的 node 节点
private Node addWaiter(Node mode) {
    // 将当前线程关联到一个 Node 对象上, 模式为独占模式   
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 快速入队，如果 tail 不为 null，说明存在队列
    if (pred != null) {
        // 将当前节点的前驱节点指向 尾节点
        node.prev = pred;
        // 通过 cas 将 Node 对象加入 AQS 队列，成为尾节点，【尾插法】
        if (compareAndSetTail(pred, node)) {
            pred.next = node;// 双向链表
            return node;
        }
    }
    // 初始时队列为空，或者 CAS 失败进入这里
    enq(node);
    return node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
    // 自旋入队，必须入队成功才结束循环
    for (;;) {
        Node t = tail;
        // 说明当前锁被占用，且当前线程可能是【第一个获取锁失败】的线程，【还没有建立队列】
        if (t == null) {
            // 设置一个【哑元节点】，头尾指针都指向该节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 自旋到这，普通入队方式，首先赋值尾节点的前驱节点【尾插法】
            node.prev = t;
            // 【在设置完尾节点后，才更新的原始尾节点的后继节点，所以此时从前往后遍历会丢失尾节点】
            if (compareAndSetTail(t, node)) {
                //【此时 t.next  = null，并且这里已经 CAS 结束，线程并不是安全的】
                t.next = node;
                return t;	// 返回当前 node 的前驱节点
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁2.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程节点加入队列成功，进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;acquireQueued 会在一个自旋中不断尝试获得锁，失败后进入 park 阻塞&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前线程是在 head 节点后，会再次 tryAcquire 尝试获取锁，state 仍为 1 则失败（第三次）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;final boolean acquireQueued(final Node node, int arg) {
    // true 表示当前线程抢占锁失败，false 表示成功
    boolean failed = true;
    try {
        // 中断标记，表示当前线程是否被中断
        boolean interrupted = false;
        for (;;) {
            // 获得当前线程节点的前驱节点
            final Node p = node.predecessor();
            // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
            if (p == head &amp;amp;&amp;amp; tryAcquire(arg)) {
                // 获取成功, 设置当前线程自己的 node 为 head
                setHead(node);
                p.next = null; // help GC
                // 表示抢占锁成功
                failed = false;
                // 返回当前线程是否被中断
                return interrupted;
            }
            // 判断是否应当 park，返回 false 后需要新一轮的循环，返回 true 进入条件二阻塞线程
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt())
                // 条件二返回结果是当前线程是否被打断，没有被打断返回 false 不进入这里的逻辑
                // 【就算被打断了，也会继续循环，并不会返回】
                interrupted = true;
        }
    } finally {
        // 【可打断模式下才会进入该逻辑】
        if (failed)
            cancelAcquire(node);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;进入 shouldParkAfterFailedAcquire 逻辑，&lt;strong&gt;将前驱 node 的 waitStatus 改为 -1&lt;/strong&gt;，返回 false；waitStatus 为 -1 的节点用来唤醒下一个节点&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 表示前置节点是个可以唤醒当前节点的节点，返回 true
    if (ws == Node.SIGNAL)
        return true;
    // 前置节点的状态处于取消状态，需要【删除前面所有取消的节点】, 返回到外层循环重试
    if (ws &amp;gt; 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus &amp;gt; 0);
        // 获取到非取消的节点，连接上当前节点
        pred.next = node;
    // 默认情况下 node 的 waitStatus 是 0，进入这里的逻辑
    } else {
        // 【设置上一个节点状态为 Node.SIGNAL】，返回外层循环重试
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回不应该 park，再次尝试一次
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ，再次 tryAcquire 尝试获取锁，这时 state 仍为 1 获取失败（第四次）&lt;/li&gt;
&lt;li&gt;当再次进入 shouldParkAfterFailedAcquire 时，这时其前驱 node 的 waitStatus 已经是 -1 了，返回 true&lt;/li&gt;
&lt;li&gt;进入 parkAndCheckInterrupt， Thread-1 park（灰色表示）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程，如果打断标记已经是 true, 则 park 会失效
    LockSupport.park(this);
    // 判断当前线程是否被打断，清除打断标记
    return Thread.interrupted();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再有多个线程经历竞争失败后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%813.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;解锁&lt;/h6&gt;
&lt;p&gt;ReentrantLock#unlock：释放锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unlock() {
    sync.release(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thread-0 释放锁，进入 release 流程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进入 tryRelease，设置 exclusiveOwnerThread 为 null，state = 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前队列不为 null，并且 head 的 waitStatus = -1，进入 unparkSuccessor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
    // 尝试释放锁，tryRelease 返回 true 表示当前线程已经【完全释放锁，重入的释放了】
    if (tryRelease(arg)) {
        // 队列头节点
        Node h = head;
        // 头节点什么时候是空？没有发生锁竞争，没有竞争线程创建哑元节点
        // 条件成立说明阻塞队列有等待线程，需要唤醒 head 节点后面的线程
        if (h != null &amp;amp;&amp;amp; h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }    
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
    // 减去释放的值，可能重入
    int c = getState() - releases;
    // 如果当前线程不是持有锁的线程直接报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否已经完全释放锁
    boolean free = false;
    // 支持锁重入, 只有 state 减为 0, 才完全释放锁成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 当前线程就是持有锁线程，所以可以直接更新锁，不需要使用 CAS
    setState(c);
    return free;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入 AbstractQueuedSynchronizer#unparkSuccessor 方法，唤醒当前节点的后继节点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到队列中距离 head 最近的一个没取消的 Node，unpark 恢复其运行，本例中即为 Thread-1&lt;/li&gt;
&lt;li&gt;回到 Thread-1 的 acquireQueued 流程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private void unparkSuccessor(Node node) {
    // 当前节点的状态
    int ws = node.waitStatus;    
    if (ws &amp;lt; 0)        
        // 【尝试重置状态为 0】，因为当前节点要完成对后续节点的唤醒任务了，不需要 -1 了
        compareAndSetWaitStatus(node, ws, 0);    
    // 找到需要 unpark 的节点，当前节点的下一个    
    Node s = node.next;    
    // 已取消的节点不能唤醒，需要找到距离头节点最近的非取消的节点
    if (s == null || s.waitStatus &amp;gt; 0) {
        s = null;
        // AQS 队列【从后至前】找需要 unpark 的节点，直到 t == 当前的 node 为止，找不到就不唤醒了
        for (Node t = tail; t != null &amp;amp;&amp;amp; t != node; t = t.prev)
            // 说明当前线程状态需要被唤醒
            if (t.waitStatus &amp;lt;= 0)
                // 置换引用
                s = t;
    }
    // 【找到合适的可以被唤醒的 node，则唤醒线程】
    if (s != null)
        LockSupport.unpark(s.thread);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从后向前的唤醒的原因&lt;/strong&gt;：enq 方法中，节点是尾插法，首先赋值的是尾节点的前驱节点，此时前驱节点的 next 并没有指向尾节点，从前遍历会丢失尾节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;唤醒的线程会从 park 位置开始执行，如果加锁成功（没有竞争），会设置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exclusiveOwnerThread 为 Thread-1，state = 1&lt;/li&gt;
&lt;li&gt;head 指向刚刚 Thread-1 所在的 Node，该 Node 会清空 Thread&lt;/li&gt;
&lt;li&gt;原本的 head 因为从链表断开，而可被垃圾回收（图中有错误，原来的头节点的 waitStatus 被改为 0 了）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%814.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果这时有其它线程来竞争**（非公平）**，例如这时有 Thread-4 来了并抢占了锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thread-4 被设置为 exclusiveOwnerThread，state = 1&lt;/li&gt;
&lt;li&gt;Thread-1 再次进入 acquireQueued 流程，获取锁失败，重新进入 park 阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%815.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;公平原理&lt;/h5&gt;
&lt;p&gt;与非公平锁主要区别在于 tryAcquire 方法：先检查 AQS 队列中是否有前驱节点，没有才去 CAS 竞争&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
            if (!hasQueuedPredecessors() &amp;amp;&amp;amp;
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 锁重入
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public final boolean hasQueuedPredecessors() {    
    Node t = tail;
    Node h = head;
    Node s;    
    // 头尾指向一个节点，链表为空，返回false
    return h != t &amp;amp;&amp;amp;
        // 头尾之间有节点，判断头节点的下一个是不是空
        // 不是空进入最后的判断，第二个节点的线程是否是本线程，不是返回 true，表示当前节点有前驱节点
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;可重入&lt;/h4&gt;
&lt;p&gt;可重入是指同一个线程如果首次获得了这把锁，那么它是这把锁的拥有者，因此有权利再次获取这把锁，如果不可重入锁，那么第二次获得锁时，自己也会被锁挡住，直接造成死锁&lt;/p&gt;
&lt;p&gt;源码解析参考：&lt;code&gt;nonfairTryAcquire(int acquires)) &lt;/code&gt; 和 &lt;code&gt;tryRelease(int releases)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
    method1();
}
public static void method1() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + &quot; execute method1&quot;);
        method2();
    } finally {
        lock.unlock();
    }
}
public static void method2() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + &quot; execute method2&quot;);
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Lock 方法加两把锁会是什么情况呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加锁两次解锁两次：正常执行&lt;/li&gt;
&lt;li&gt;加锁两次解锁一次：程序直接卡死，线程不能出来，也就说明&lt;strong&gt;申请几把锁，最后需要解除几把锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;加锁一次解锁两次：运行程序会直接报错&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void getLock() {
    lock.lock();
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + &quot;\t get Lock&quot;);
    } finally {
        lock.unlock();
        //lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;可打断&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;public void lockInterruptibly()&lt;/code&gt;：获得可打断的锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有竞争此方法就会获取 lock 对象锁&lt;/li&gt;
&lt;li&gt;如果有竞争就进入阻塞队列，可以被其他线程用 interrupt 打断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：如果是不可中断模式，那么即使使用了 interrupt 也不会让等待状态中的线程中断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();    
    Thread t1 = new Thread(() -&amp;gt; {        
        try {            
            System.out.println(&quot;尝试获取锁&quot;);            
            lock.lockInterruptibly();        
        } catch (InterruptedException e) {            
            System.out.println(&quot;没有获取到锁，被打断，直接返回&quot;);            
            return;        
        }        
        try {            
            System.out.println(&quot;获取到锁&quot;);        
        } finally {            
            lock.unlock();        
        }    
    }, &quot;t1&quot;);    
    lock.lock();    
    t1.start();    
    Thread.sleep(2000);    
    System.out.println(&quot;主线程进行打断锁&quot;);    
    t1.interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不可打断模式：即使它被打断，仍会驻留在 AQS 阻塞队列中，一直要&lt;strong&gt;等到获得锁后才能得知自己被打断&lt;/strong&gt;了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void acquire(int arg) {    
    if (!tryAcquire(arg) &amp;amp;&amp;amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待        
        // 如果acquireQueued返回true，打断状态 interrupted = true        
        selfInterrupt();
}
static void selfInterrupt() {
    // 知道自己被打断了，需要重新产生一次中断完成中断效果
    Thread.currentThread().interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;final boolean acquireQueued(final Node node, int arg) {    
    try {        
        boolean interrupted = false;        
        for (;;) {            
            final Node p = node.predecessor();            
            if (p == head &amp;amp;&amp;amp; tryAcquire(arg)) {                
                setHead(node);                
                p.next = null; // help GC                
                failed = false;                
                // 还是需要获得锁后, 才能返回打断状态
                return interrupted;            
            }            
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt()){
                // 条件二中判断当前线程是否被打断，被打断返回true，设置中断标记为 true，【获取锁后返回】
                interrupted = true;  
            }                  
        } 
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
 private final boolean parkAndCheckInterrupt() {    
     // 阻塞当前线程，如果打断标记已经是 true, 则 park 会失效
     LockSupport.park(this);    
     // 判断当前线程是否被打断，清除打断标记，被打断返回true
     return Thread.interrupted();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可打断模式：AbstractQueuedSynchronizer#acquireInterruptibly，&lt;strong&gt;被打断后会直接抛出异常&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void lockInterruptibly() throws InterruptedException {    
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) {
    // 被其他线程打断了直接返回 false
    if (Thread.interrupted())
		throw new InterruptedException();
    if (!tryAcquire(arg))
        // 没获取到锁，进入这里
        doAcquireInterruptibly(arg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void doAcquireInterruptibly(int arg) throws InterruptedException {
    // 返回封装当前线程的节点
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            //...
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt())
                // 【在 park 过程中如果被 interrupt 会抛出异常】, 而不会再次进入循环获取锁后才完成打断效果
                throw new InterruptedException();
        }    
    } finally {
        // 抛出异常前会进入这里
        if (failed)
            // 取消当前线程的节点
            cancelAcquire(node);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 取消节点出队的逻辑
private void cancelAcquire(Node node) {
    // 判空
    if (node == null)
        return;
	// 把当前节点封装的 Thread 置为空
    node.thread = null;
	// 获取当前取消的 node 的前驱节点
    Node pred = node.prev;
    // 前驱节点也被取消了，循环找到前面最近的没被取消的节点
    while (pred.waitStatus &amp;gt; 0)
        node.prev = pred = pred.prev;
    
	// 获取前驱节点的后继节点，可能是当前 node，也可能是 waitStatus &amp;gt; 0 的节点
    Node predNext = pred.next;
    
	// 把当前节点的状态设置为 【取消状态 1】
    node.waitStatus = Node.CANCELLED;
    
	// 条件成立说明当前节点是尾节点，把当前节点的前驱节点设置为尾节点
    if (node == tail &amp;amp;&amp;amp; compareAndSetTail(node, pred)) {
        // 把前驱节点的后继节点置空，这里直接把所有的取消节点出队
        compareAndSetNext(pred, predNext, null);
    } else {
        // 说明当前节点不是 tail 节点
        int ws;
        // 条件一成立说明当前节点不是 head.next 节点
        if (pred != head &amp;amp;&amp;amp;
            // 判断前驱节点的状态是不是 -1，不成立说明前驱状态可能是 0 或者刚被其他线程取消排队了
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             // 如果状态不是 -1，设置前驱节点的状态为 -1
             (ws &amp;lt;= 0 &amp;amp;&amp;amp; compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &amp;amp;&amp;amp;
            // 前驱节点的线程不为null
            pred.thread != null) {
            
            Node next = node.next;
            // 当前节点的后继节点是正常节点
            if (next != null &amp;amp;&amp;amp; next.waitStatus &amp;lt;= 0)
                // 把 前驱节点的后继节点 设置为 当前节点的后继节点，【从队列中删除了当前节点】
                compareAndSetNext(pred, predNext, next);
        } else {
            // 当前节点是 head.next 节点，唤醒当前节点的后继节点
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁超时&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;public boolean tryLock()&lt;/code&gt;：尝试获取锁，获取到返回 true，获取不到直接放弃，不进入阻塞队列&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public boolean tryLock(long timeout, TimeUnit unit)&lt;/code&gt;：在给定时间内获取锁，获取不到就退出&lt;/p&gt;
&lt;p&gt;注意：tryLock 期间也可以被打断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -&amp;gt; {
        try {
            if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                System.out.println(&quot;获取不到锁&quot;);
                return;
            }
        } catch (InterruptedException e) {
            System.out.println(&quot;被打断，获取不到锁&quot;);
            return;
        }
        try {
            log.debug(&quot;获取到锁&quot;);
        } finally {
            lock.unlock();
        }
    }, &quot;t1&quot;);
    lock.lock();
    System.out.println(&quot;主线程获取到锁&quot;);
    t1.start();
    
    Thread.sleep(1000);
    try {
        System.out.println(&quot;主线程释放了锁&quot;);
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;成员变量：指定超时限制的阈值，小于该值的线程不会被挂起&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final long spinForTimeoutThreshold = 1000L;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超时时间设置的小于该值，就会被禁止挂起，因为阻塞在唤醒的成本太高，不如选择自旋空转&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tryLock()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean tryLock() {   
    // 只尝试一次
    return sync.nonfairTryAcquire(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tryLock(long timeout, TimeUnit unit)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final boolean tryAcquireNanos(int arg, long nanosTimeout) {
    if (Thread.interrupted())        
        throw new InterruptedException();    
    // tryAcquire 尝试一次
    return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
protected final boolean tryAcquire(int acquires) {    
    return nonfairTryAcquire(acquires);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private boolean doAcquireNanos(int arg, long nanosTimeout) {    
    if (nanosTimeout &amp;lt;= 0L)
        return false;
    // 获取最后期限的时间戳
    final long deadline = System.nanoTime() + nanosTimeout;
    //...
    try {
        for (;;) {
            //...
            // 计算还需等待的时间
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout &amp;lt;= 0L)	//时间已到     
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp;
                // 如果 nanosTimeout 大于该值，才有阻塞的意义，否则直接自旋会好点
                nanosTimeout &amp;gt; spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 【被打断会报异常】
            if (Thread.interrupted())
                throw new InterruptedException();
        }    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;哲学家就餐&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Chopstick c1 = new Chopstick(&quot;1&quot;);//...
    Chopstick c5 = new Chopstick(&quot;5&quot;);
    new Philosopher(&quot;苏格拉底&quot;, c1, c2).start();
    new Philosopher(&quot;柏拉图&quot;, c2, c3).start();
    new Philosopher(&quot;亚里士多德&quot;, c3, c4).start();
    new Philosopher(&quot;赫拉克利特&quot;, c4, c5).start();    
    new Philosopher(&quot;阿基米德&quot;, c5, c1).start();
}
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            System.out.println(&quot;eating...&quot;);
                            Thread.sleep(1000);
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }
}
class Chopstick extends ReentrantLock {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return &quot;筷子{&quot; + name + &apos;}&apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;条件变量&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;synchronized 的条件变量，是当条件不满足时进入 WaitSet 等待；ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量&lt;/p&gt;
&lt;p&gt;ReentrantLock 类获取 Condition 对象：&lt;code&gt;public Condition newCondition()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Condition 类 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void await()&lt;/code&gt;：当前线程从运行状态进入等待状态，释放锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void signal()&lt;/code&gt;：唤醒一个等待在 Condition 上的线程，但是必须获得与该 Condition 相关的锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;await / signal 前需要获得锁&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;await 执行后，会释放锁进入 ConditionObject 等待&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;await 的线程被唤醒去重新竞争 lock 锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程在条件队列被打断会抛出中断异常&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;竞争 lock 锁成功后，从 await 后继续执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();
    //创建一个新的条件变量
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    new Thread(() -&amp;gt; {
        try {
            lock.lock();
            System.out.println(&quot;进入等待&quot;);
            //进入休息室等待
            condition1.await();
            System.out.println(&quot;被唤醒了&quot;);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }    
    }).start();
    Thread.sleep(1000);
    //叫醒
    new Thread(() -&amp;gt; {
        try {            
            lock.lock();
            //唤醒
            condition2.signal();
        } finally {
            lock.unlock();
        }
    }).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;h6&gt;await&lt;/h6&gt;
&lt;p&gt;总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列，如果被唤醒就将 node 转移到 AQS 的执行阻塞队列，等待获取锁，&lt;strong&gt;每个 Condition 对象都包含一个等待队列&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开始 Thread-0 持有锁，调用 await，线程进入 ConditionObject 等待，直到被唤醒或打断，调用 await 方法的线程都是持锁状态的，所以说逻辑里&lt;strong&gt;不存在并发&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void await() throws InterruptedException {
     // 判断当前线程是否是中断状态，是就直接给个中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将调用 await 的线程包装成 Node，添加到条件队列并返回
    Node node = addConditionWaiter();
    // 完全释放节点持有的锁，因为其他线程唤醒当前线程的前提是【持有锁】
    int savedState = fullyRelease(node);
    
    // 设置打断模式为没有被打断，状态码为 0
    int interruptMode = 0;
    
    // 如果该节点还没有转移至 AQS 阻塞队列, park 阻塞，等待进入阻塞队列
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        // 如果被打断，退出等待队列，对应的 node 【也会被迁移到阻塞队列】尾部，状态设置为 0
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 逻辑到这说明当前线程退出等待队列，进入【阻塞队列】
    
    // 尝试枪锁，释放了多少锁就【重新获取多少锁】，获取锁成功判断打断模式
    if (acquireQueued(node, savedState) &amp;amp;&amp;amp; interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    
    // node 在条件队列时 如果被外部线程中断唤醒，会加入到阻塞队列，但是并未设 nextWaiter = null
    if (node.nextWaiter != null)
        // 清理条件队列内所有已取消的 Node
        unlinkCancelledWaiters();
    // 条件成立说明挂起期间发生过中断
    if (interruptMode != 0)
        // 应用打断模式
        reportInterruptAfterWait(interruptMode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 打断模式 - 在退出等待时重新设置打断状态
private static final int REINTERRUPT = 1;
// 打断模式 - 在退出等待时抛出异常
private static final int THROW_IE = -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建新的 Node 状态为 -2（Node.CONDITION）&lt;/strong&gt;，关联 Thread-0，加入等待队列尾部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Node addConditionWaiter() {
    // 获取当前条件队列的尾节点的引用，保存到局部变量 t 中
    Node t = lastWaiter;
    // 当前队列中不是空，并且节点的状态不是 CONDITION（-2），说明当前节点发生了中断
    if (t != null &amp;amp;&amp;amp; t.waitStatus != Node.CONDITION) {
        // 清理条件队列内所有已取消的 Node
        unlinkCancelledWaiters();
        // 清理完成重新获取 尾节点 的引用
        t = lastWaiter;
    }
    // 创建一个关联当前线程的新 node, 设置状态为 CONDITION(-2)，添加至队列尾部
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;		// 空队列直接放在队首【不用CAS因为执行线程是持锁线程，并发安全】
    else
        t.nextWaiter = node;	// 非空队列队尾追加
    lastWaiter = node;			// 更新队尾的引用
    return node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 清理条件队列内所有已取消（不是CONDITION）的 node，【链表删除的逻辑】
private void unlinkCancelledWaiters() {
    // 从头节点开始遍历【FIFO】
    Node t = firstWaiter;
    // 指向正常的 CONDITION 节点
    Node trail = null;
    // 等待队列不空
    while (t != null) {
        // 获取当前节点的后继节点
        Node next = t.nextWaiter;
        // 判断 t 节点是不是 CONDITION 节点，条件队列内不是 CONDITION 就不是正常的
        if (t.waitStatus != Node.CONDITION) { 
            // 不是正常节点，需要 t 与下一个节点断开
            t.nextWaiter = null;
            // 条件成立说明遍历到的节点还未碰到过正常节点
            if (trail == null)
                // 更新 firstWaiter 指针为下个节点
                firstWaiter = next;
            else
                // 让上一个正常节点指向 当前取消节点的 下一个节点，【删除非正常的节点】
                trail.nextWaiter = next;
            // t 是尾节点了，更新 lastWaiter 指向最后一个正常节点
            if (next == null)
                lastWaiter = trail;
        } else {
            // trail 指向的是正常节点 
            trail = t;
        }
        // 把 t.next 赋值给 t，循环遍历
        t = next; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来 Thread-0 进入 AQS 的 fullyRelease 流程，释放同步器上的锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 线程可能重入，需要将 state 全部释放
final int fullyRelease(Node node) {
    // 完全释放锁是否成功，false 代表成功
    boolean failed = true;
    try {
        // 获取当前线程所持有的 state 值总数
        int savedState = getState();
        // release -&amp;gt; tryRelease 解锁重入锁
        if (release(savedState)) {
            // 释放成功
            failed = false;
            // 返回解锁的深度
            return savedState;
        } else {
            // 解锁失败抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 没有释放成功，将当前 node 设置为取消状态
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁，假设 Thread-1 竞争成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Thread-0 进入 isOnSyncQueue 逻辑判断节点&lt;strong&gt;是否移动到阻塞队列&lt;/strong&gt;，没有就 park 阻塞 Thread-0&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final boolean isOnSyncQueue(Node node) {
    // node 的状态是 CONDITION，signal 方法是先修改状态再迁移，所以前驱节点为空证明还【没有完成迁移】
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 说明当前节点已经成功入队到阻塞队列，且当前节点后面已经有其它 node，因为条件队列的 next 指针为 null
    if (node.next != null)
        return true;
	// 说明【可能在阻塞队列，但是是尾节点】
    // 从阻塞队列的尾节点开始向前【遍历查找 node】，如果查找到返回 true，查找不到返回 false
    return findNodeFromTail(node);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;await 线程 park 后如果被 unpark 或者被打断，都会进入 checkInterruptWhileWaiting 判断线程是否被打断：&lt;strong&gt;在条件队列被打断的线程需要抛出异常&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int checkInterruptWhileWaiting(Node node) {
    // Thread.interrupted() 返回当前线程中断标记位，并且重置当前标记位 为 false
    // 如果被中断了，根据是否在条件队列被中断的，设置中断状态码
    return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 这个方法只有在线程是被打断唤醒时才会调用
final boolean transferAfterCancelledWait(Node node) {
    // 条件成立说明当前node一定是在条件队列内，因为 signal 迁移节点到阻塞队列时，会将节点的状态修改为 0
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 把【中断唤醒的 node 加入到阻塞队列中】
        enq(node);
        // 表示是在条件队列内被中断了，设置为 THROW_IE 为 -1
        return true;
    }

    //执行到这里的情况：
    //1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了
    //2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态
    
    // 如果当前线程还没到阻塞队列，一直释放 CPU
    while (!isOnSyncQueue(node))
        Thread.yield();

    // 表示当前节点被中断唤醒时不在条件队列了，设置为 REINTERRUPT 为 1
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后开始处理中断状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
    // 条件成立说明【在条件队列内发生过中断，此时 await 方法抛出中断异常】
    if (interruptMode == THROW_IE)
        throw new InterruptedException();

    // 条件成立说明【在条件队列外发生的中断，此时设置当前线程的中断标记位为 true】
    else if (interruptMode == REINTERRUPT)
        // 进行一次自己打断，产生中断的效果
        selfInterrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;signal&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设 Thread-1 要来唤醒 Thread-0，进入 ConditionObject 的 doSignal 流程，&lt;strong&gt;取得等待队列中第一个 Node&lt;/strong&gt;，即 Thread-0 所在 Node，必须持有锁才能唤醒, 因此 doSignal 内线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void signal() {
    // 判断调用 signal 方法的线程是否是独占锁持有线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取条件队列中第一个 Node
    Node first = firstWaiter;
    // 不为空就将第该节点【迁移到阻塞队列】
    if (first != null)
        doSignal(first);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】
private void doSignal(Node first) {
    do {
        // 成立说明当前节点的下一个节点是 null，当前节点是尾节点了，队列中只有当前一个节点了
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 将等待队列中的 Node 转移至 AQS 队列，不成功且还有节点则继续循环
    } while (!transferForSignal(first) &amp;amp;&amp;amp; (first = firstWaiter) != null);
}

// signalAll() 会调用这个函数，唤醒所有的节点
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    // 唤醒所有的节点，都放到阻塞队列中
    } while (first != null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 transferForSignal，&lt;strong&gt;先将节点的 waitStatus 改为 0，然后加入 AQS 阻塞队列尾部&lt;/strong&gt;，将 Thread-3 的 waitStatus 改为 -1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功
final boolean transferForSignal(Node node) {
    // CAS 修改当前节点的状态，修改为 0，因为当前节点马上要迁移到阻塞队列了
    // 如果状态已经不是 CONDITION, 说明线程被取消（await 释放全部锁失败）或者被中断（可打断 cancelAcquire）
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        // 返回函数调用处继续寻找下一个节点
        return false;
    
    // 【先改状态，再进行迁移】
    // 将当前 node 入阻塞队列，p 是当前节点在阻塞队列的【前驱节点】
    Node p = enq(node);
    int ws = p.waitStatus;
    
    // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL，就 unpark 取消当前节点线程的阻塞状态, 
    // 让 thread-0 线程竞争锁，重新同步状态
    if (ws &amp;gt; 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Thread-1 释放锁，进入 unlock 流程&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ReadWrite&lt;/h3&gt;
&lt;h4&gt;读写锁&lt;/h4&gt;
&lt;p&gt;独占锁：指该锁一次只能被一个线程所持有，对 ReentrantLock 和 Synchronized 而言都是独占锁&lt;/p&gt;
&lt;p&gt;共享锁：指该锁可以被多个线程锁持有&lt;/p&gt;
&lt;p&gt;ReentrantReadWriteLock 其&lt;strong&gt;读锁是共享锁，写锁是独占锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：多个线程同时读一个资源类没有任何问题，为了满足并发量，读取共享资源应该同时进行，但是如果一个线程想去写共享资源，就不应该再有其它线程可以对该资源进行读或写&lt;/p&gt;
&lt;p&gt;使用规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加锁解锁格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r.lock();
try {
    // 临界区
} finally {
	r.unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读-读能共存、读-写不能共存、写-写不能共存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读锁不支持条件变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重入时升级不支持&lt;/strong&gt;：持有读锁的情况下去获取写锁会导致获取写锁永久等待，需要先释放读，再去获得写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重入时降级支持&lt;/strong&gt;：持有写锁的情况下去获取读锁，造成只有当前线程会持有读锁，因为写锁会互斥其他的锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;w.lock();
try {
    r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
    try {
        // ...
    } finally{
    	w.unlock();// 要在写锁释放之前获取读锁
    }
} finally{
	r.unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public ReentrantReadWriteLock()&lt;/code&gt;：默认构造方法，非公平锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public ReentrantReadWriteLock(boolean fair)&lt;/code&gt;：true 为公平锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public ReentrantReadWriteLock.ReadLock readLock()&lt;/code&gt;：返回读锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public ReentrantReadWriteLock.WriteLock writeLock()&lt;/code&gt;：返回写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void lock()&lt;/code&gt;：加锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void unlock()&lt;/code&gt;：解锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean tryLock()&lt;/code&gt;：尝试获取锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读读并发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock r = rw.readLock();
    ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    new Thread(() -&amp;gt; {
        r.lock();
        try {
            Thread.sleep(2000);
            System.out.println(&quot;Thread 1 running &quot; + new Date());
        } finally {
            r.unlock();
        }
    },&quot;t1&quot;).start();
    new Thread(() -&amp;gt; {
        r.lock();
        try {
            Thread.sleep(2000);
            System.out.println(&quot;Thread 2 running &quot; + new Date());
        } finally {
            r.unlock();
        }
    },&quot;t2&quot;).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存应用&lt;/h4&gt;
&lt;p&gt;缓存更新时，是先清缓存还是先更新数据库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;先清缓存：可能造成刚清理缓存还没有更新数据库，线程直接查询了数据库更新过期数据到缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;先更新据库：可能造成刚更新数据库，还没清空缓存就有线程从缓存拿到了旧数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;补充情况：查询线程 A 查询数据时恰好缓存数据由于时间到期失效，或是第一次查询&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock缓存.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以使用读写锁进行操作&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;读写锁用的是同一个 Sycn 同步器，因此等待队列、state 等也是同一个，原理与 ReentrantLock 加锁相比没有特殊之处，不同是&lt;strong&gt;写锁状态占了 state 的低 16 位，而读锁使用的是 state 的高 16 位&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;读写锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final ReentrantReadWriteLock.ReadLock readerLock;		
private final ReentrantReadWriteLock.WriteLock writerLock;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：默认是非公平锁，可以指定参数创建公平锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ReentrantReadWriteLock(boolean fair) {
    // true 为公平锁
    sync = fair ? new FairSync() : new NonfairSync();
    // 这两个 lock 共享同一个 sync 实例，都是由 ReentrantReadWriteLock 的 sync 提供同步实现
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sync 类的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;统计变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用来移位
static final int SHARED_SHIFT   = 16;
// 高16位的1
static final int SHARED_UNIT    = (1 &amp;lt;&amp;lt; SHARED_SHIFT);
// 65535，16个1，代表写锁的最大重入次数
static final int MAX_COUNT      = (1 &amp;lt;&amp;lt; SHARED_SHIFT) - 1;
// 低16位掩码：0b 1111 1111 1111 1111，用来获取写锁重入的次数
static final int EXCLUSIVE_MASK = (1 &amp;lt;&amp;lt; SHARED_SHIFT) - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取读写锁的次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取读写锁的读锁分配的总次数
static int sharedCount(int c)    { return c &amp;gt;&amp;gt;&amp;gt; SHARED_SHIFT; }
// 写锁（独占）锁的重入次数
static int exclusiveCount(int c) { return c &amp;amp; EXCLUSIVE_MASK; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 记录读锁线程自己的持有读锁的数量（重入次数），因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}
// 线程安全的存放线程各自的 HoldCounter 对象
static final class ThreadLocalHoldCounter extends ThreadLocal&amp;lt;HoldCounter&amp;gt; {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部类实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当前线程持有的可重入读锁的数量，计数为 0 时删除
private transient ThreadLocalHoldCounter readHolds;
// 记录最后一个获取【读锁】线程的 HoldCounter 对象
private transient HoldCounter cachedHoldCounter;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;首次获取锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第一个获取读锁的线程
private transient Thread firstReader = null;
// 记录该线程持有的读锁次数（读锁重入次数）
private transient int firstReaderHoldCount;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sync 构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sync() {
    readHolds = new ThreadLocalHoldCounter();
    // 确保其他线程的数据可见性，state 是 volatile 修饰的变量，重写该值会将线程本地缓存数据【同步至主存】
    setState(getState()); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;加锁原理&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;t1 线程：w.lock（&lt;strong&gt;写锁&lt;/strong&gt;），成功上锁 state = 0_1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// lock()  -&amp;gt; sync.acquire(1);
public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
    // 尝试获得写锁，获得写锁失败，将当前线程关联到一个 Node 对象上, 模式为独占模式 
    if (!tryAcquire(arg) &amp;amp;&amp;amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 获得低 16 位, 代表写锁的 state 计数
    int w = exclusiveCount(c);
    // 说明有读锁或者写锁
    if (c != 0) {
        // c != 0 and w == 0 表示有读锁，【读锁不能升级】，直接返回 false
        // w != 0 说明有写锁，写锁的拥有者不是自己，获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        // 执行到这里只有一种情况：【写锁重入】，所以下面几行代码不存在并发
        if (w + exclusiveCount(acquires) &amp;gt; MAX_COUNT)
            throw new Error(&quot;Maximum lock count exceeded&quot;);
        // 写锁重入, 获得锁成功，没有并发，所以不使用 CAS
        setState(c + acquires);
        return true;
    }
    
    // c == 0，说明没有任何锁，判断写锁是否该阻塞，是 false 就尝试获取锁，失败返回 false
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    // 获得锁成功，设置锁的持有线程为当前线程
    setExclusiveOwnerThread(current);
    return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
    return false; 
}
// 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;t2 r.lock（&lt;strong&gt;读锁&lt;/strong&gt;），进入 tryAcquireShared 流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回 -1 表示失败&lt;/li&gt;
&lt;li&gt;如果返回 0 表示成功&lt;/li&gt;
&lt;li&gt;返回正数表示还有多少后继节点支持共享模式，读写锁返回 1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    // tryAcquireShared 返回负数, 表示获取读锁失败
    if (tryAcquireShared(arg) &amp;lt; 0)
        doAcquireShared(arg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 尝试以共享模式获取
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // exclusiveCount(c) 代表低 16 位, 写锁的 state，成立说明有线程持有写锁
    // 写锁的持有者不是当前线程，则获取读锁失败，【写锁允许降级】
    if (exclusiveCount(c) != 0 &amp;amp;&amp;amp; getExclusiveOwnerThread() != current)
        return -1;
    
    // 高 16 位，代表读锁的 state，共享锁分配出去的总次数
    int r = sharedCount(c);
    // 读锁是否应该阻塞
    if (!readerShouldBlock() &amp;amp;&amp;amp;	r &amp;lt; MAX_COUNT &amp;amp;&amp;amp;
        compareAndSetState(c, c + SHARED_UNIT)) {	// 尝试增加读锁计数
        // 加锁成功
        // 加锁之前读锁为 0，说明当前线程是第一个读锁线程
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        // 第一个读锁线程是自己就发生了读锁重入
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            // cachedHoldCounter 设置为当前线程的 holdCounter 对象，即最后一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            // 说明还没设置 rh
            if (rh == null || rh.tid != getThreadId(current))
                // 获取当前线程的锁重入的对象，赋值给 cachedHoldCounter
                cachedHoldCounter = rh = readHolds.get();
            // 还没重入
            else if (rh.count == 0)
                readHolds.set(rh);
            // 重入 + 1
            rh.count++;
        }
        // 读锁加锁成功
        return 1;
    }
    // 逻辑到这 应该阻塞，或者 cas 加锁失败
    // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
    return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 偏向写锁一些，看 AQS 阻塞队列中第一个节点是否是写锁，是则阻塞，反之不阻塞
// 防止一直有读锁线程，导致写锁线程饥饿
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;final int fullTryAcquireShared(Thread current) {
    // 当前读锁线程持有的读锁次数对象
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        // 说明有线程持有写锁
        if (exclusiveCount(c) != 0) {
            // 写锁不是自己则获取锁失败
            if (getExclusiveOwnerThread() != current)
                return -1;
        } else if (readerShouldBlock()) {
            // 条件成立说明当前线程是 firstReader，当前锁是读忙碌状态，而且当前线程也是读锁重入
            if (firstReader == current) {
                // assert firstReaderHoldCount &amp;gt; 0;
            } else {
                if (rh == null) {
                    // 最后一个读锁的 HoldCounter
                    rh = cachedHoldCounter;
                    // 说明当前线程也不是最后一个读锁
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 获取当前线程的 HoldCounter
                        rh = readHolds.get();
                        // 条件成立说明 HoldCounter 对象是上一步代码新建的
                        // 当前线程不是锁重入，在 readerShouldBlock() 返回 true 时需要去排队
                        if (rh.count == 0)
                            // 防止内存泄漏
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        // 越界判断
        if (sharedCount(c) == MAX_COUNT)
            throw new Error(&quot;Maximum lock count exceeded&quot;);
        // 读锁加锁，条件内的逻辑与 tryAcquireShared 相同
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取读锁失败，进入 sync.doAcquireShared(1) 流程开始阻塞，首先也是调用 addWaiter 添加节点，不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式，注意此时 t2 仍处于活跃状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doAcquireShared(int arg) {
    // 将当前线程关联到一个 Node 对象上, 模式为共享模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点就头节点就去尝试获取锁
            if (p == head) {
                // 再一次尝试获取读锁
                int r = tryAcquireShared(arg);
                // r &amp;gt;= 0 表示获取成功
                if (r &amp;gt;= 0) {
                    //【这里会设置自己为头节点，唤醒相连的后序的共享节点】
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 是否在获取读锁失败时阻塞      					 park 当前线程
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有成功，在 doAcquireShared 内 for (;;) 循环一次，shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1，再 for (;;) 循环一次尝试 tryAcquireShared，不成功在 parkAndCheckInterrupt() 处 park&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock加锁1.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这种状态下，假设又有 t3 r.lock，t4 w.lock，这期间 t1 仍然持有锁，就变成了下面的样子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock%E5%8A%A0%E9%94%812.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;解锁原理&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;t1 w.unlock， 写锁解锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unlock() {
    // 释放锁
    sync.release(1);
}
public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空并且不是等待状态不是 0，唤醒后继的非取消节点
        if (h != null &amp;amp;&amp;amp; h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    // 因为可重入的原因, 写锁计数为 0, 才算释放成功
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;唤醒流程 sync.unparkSuccessor，这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行，继续循环，执行 tryAcquireShared 成功则让读锁计数加一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来 t2 调用 setHeadAndPropagate(node, 1)，它原本所在节点被置为头节点；还会检查下一个节点是否是 shared，如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点，这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行，&lt;strong&gt;唤醒连续的所有的共享节点&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    // 设置自己为 head 节点
    setHead(node);
    // propagate 表示有共享资源（例如共享读锁或信号量），为 0 就没有资源
    if (propagate &amp;gt; 0 || h == null || h.waitStatus &amp;lt; 0 ||
        (h = head) == null || h.waitStatus &amp;lt; 0) {
        // 获取下一个节点
        Node s = node.next;
        // 如果当前是最后一个节点，或者下一个节点是【等待共享读锁的节点】
        if (s == null || s.isShared())
            // 唤醒后继节点
            doReleaseShared();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void doReleaseShared() {
    // 如果 head.waitStatus == Node.SIGNAL ==&amp;gt; 0 成功, 下一个节点 unpark
	// 如果 head.waitStatus == 0 ==&amp;gt; Node.PROPAGATE
    for (;;) {
        Node h = head;
        if (h != null &amp;amp;&amp;amp; h != tail) {
            int ws = h.waitStatus;
            // SIGNAL 唤醒后继
            if (ws == Node.SIGNAL) {
                // 因为读锁共享，如果其它线程也在释放读锁，那么需要将 waitStatus 先改为 0
            	// 防止 unparkSuccessor 被多次执行
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;  
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果已经是 0 了，改为 -3，用来解决传播性
            else if (ws == 0 &amp;amp;&amp;amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }
        // 条件不成立说明被唤醒的节点非常积极，直接将自己设置为了新的 head，
        // 此时唤醒它的节点（前驱）执行 h == head 不成立，所以不会跳出循环，会继续唤醒新的 head 节点的后继节点
        if (h == head)                   
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock解锁1.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下一个节点不是 shared 了，因此不会继续唤醒 t4 所在节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;t2 读锁解锁，进入 sync.releaseShared(1) 中，调用 tryReleaseShared(1) 让计数减一，但计数还不为零，t3 同样让计数减一，计数为零，进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;protected final boolean tryReleaseShared(int unused) {

    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程，计数为 0 才是真正释放
        if (compareAndSetState(c, nextc))
            // 返回是否已经完全释放了 
            return nextc == 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行，再次 for (;;) 这次自己是头节点的临节点，并且没有其他节点竞争，tryAcquire(1) 成功，修改头结点，流程结束&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock解锁2.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Stamped&lt;/h4&gt;
&lt;p&gt;StampedLock：读写锁，该类自 JDK 8 加入，是为了进一步优化读性能&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在使用读锁、写锁时都必须配合戳使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StampedLock 不支持条件变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StampedLock &lt;strong&gt;不支持重入&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本用法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加解读锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long stamp = lock.readLock();
lock.unlockRead(stamp);			// 类似于 unpark，解指定的锁
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加解写锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long stamp = lock.writeLock();
lock.unlockWrite(stamp);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;乐观读，StampedLock 支持 &lt;code&gt;tryOptimisticRead()&lt;/code&gt; 方法，读取完毕后做一次&lt;strong&gt;戳校验&lt;/strong&gt;，如果校验通过，表示这期间没有其他线程的写操作，数据可以安全使用，如果校验没通过，需要重新获取读锁，保证数据一致性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
	// 锁升级
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提供一个数据容器类内部分别使用读锁保护数据的 read() 方法，写锁保护数据的 write() 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读-读可以优化&lt;/li&gt;
&lt;li&gt;读-写优化读，补加读锁&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -&amp;gt; {
    	dataContainer.read(1000);
    },&quot;t1&quot;).start();
    Thread.sleep(500);
    
    new Thread(() -&amp;gt; {
        dataContainer.write(1000);
    },&quot;t2&quot;).start();
}

class DataContainerStamped {
    private int data;
    private final StampedLock lock = new StampedLock();

    public int read(int readTime) throws InterruptedException {
        long stamp = lock.tryOptimisticRead();
        System.out.println(new Date() + &quot; optimistic read locking&quot; + stamp);
        Thread.sleep(readTime);
        // 戳有效，直接返回数据
        if (lock.validate(stamp)) {
            Sout(new Date() + &quot; optimistic read finish...&quot; + stamp);
            return data;
        }

        // 说明其他线程更改了戳，需要锁升级了，从乐观读升级到读锁
        System.out.println(new Date() + &quot; updating to read lock&quot; + stamp);
        try {
            stamp = lock.readLock();
            System.out.println(new Date() + &quot; read lock&quot; + stamp);
            Thread.sleep(readTime);
            System.out.println(new Date() + &quot; read finish...&quot; + stamp);
            return data;
        } finally {
            System.out.println(new Date() + &quot; read unlock &quot; +  stamp);
            lock.unlockRead(stamp);
        }
    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        System.out.println(new Date() + &quot; write lock &quot; + stamp);
        try {
            Thread.sleep(2000);
            this.data = newData;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(new Date() + &quot; write unlock &quot; + stamp);
            lock.unlockWrite(stamp);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;CountDown&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;CountDownLatch：计数器，用来进行线程同步协作，&lt;strong&gt;等待所有线程完成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public CountDownLatch(int count)&lt;/code&gt;：初始化唤醒需要的 down 几步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void await() &lt;/code&gt;：让当前线程等待，必须 down 完初始化的数字才可以被唤醒，否则进入无限等待&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void countDown()&lt;/code&gt;：计数器进行减 1（down 1）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用：同步等待多个 Rest 远程调用结束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// LOL 10人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(10);
    ExecutorService service = Executors.newFixedThreadPool(10);
    String[] all = new String[10];
    Random random = new Random();

    for (int j = 0; j &amp;lt; 10; j++) {
        int finalJ = j;//常量
        service.submit(() -&amp;gt; {
            for (int i = 0; i &amp;lt;= 100; i++) {
                Thread.sleep(random.nextInt(100));	//随机休眠
                all[finalJ] = i + &quot;%&quot;;
                System.out.print(&quot;\r&quot; + Arrays.toString(all));	// \r代表覆盖
            }
            latch.countDown();
        });
    }
    latch.await();
    System.out.println(&quot;\n游戏开始&quot;);
    service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;p&gt;阻塞等待：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程调用 await() 等待其他线程完成任务：支持打断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    // 判断线程是否被打断，抛出打断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取共享锁，条件成立说明 state &amp;gt; 0，此时线程入队阻塞等待，等待其他线程获取共享资源
    // 条件不成立说明 state = 0，此时不需要阻塞线程，直接结束函数调用
    if (tryAcquireShared(arg) &amp;lt; 0)
        doAcquireSharedInterruptibly(arg);
}
// CountDownLatch.Sync#tryAcquireShared
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起，等待 latch 变为 0：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    // 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 前驱节点时头节点就可以尝试获取锁
            if (p == head) {
                // 再次尝试获取锁，获取成功返回 1
                int r = tryAcquireShared(arg);
                if (r &amp;gt;= 0) {
                    // 获取锁成功，设置当前节点为 head 节点，并且向后传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 阻塞在这里
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        // 阻塞线程被中断后抛出异常，进入取消节点的逻辑
        if (failed)
            cancelAcquire(node);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取共享锁成功，进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    // 将当前节点设置为新的 head 节点，前驱节点和持有线程置为 null
    setHead(node);
	// propagate = 1，条件一成立
    if (propagate &amp;gt; 0 || h == null || h.waitStatus &amp;lt; 0 || (h = head) == null || h.waitStatus &amp;lt; 0) {
        // 获取当前节点的后继节点
        Node s = node.next;
        // 当前节点是尾节点时 next 为 null，或者后继节点是 SHARED 共享模式
        if (s == null || s.isShared())
            // 唤醒所有的等待共享锁的节点
            doReleaseShared();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计数减一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程进入 countDown() 完成计数器减一（释放锁）的操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 尝试释放共享锁
    if (tryReleaseShared(arg)) {
        // 释放锁成功开始唤醒阻塞节点
        doReleaseShared();
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新 state 值，每调用一次，state 值减一，当 state -1 正好为 0 时，返回 true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        // 条件成立说明前面【已经有线程触发唤醒操作】了，这里返回 false
        if (c == 0)
            return false;
        // 计数器减一
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            // 计数器为 0 时返回 true
            return nextc == 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;state = 0 时，当前线程需要执行&lt;strong&gt;唤醒阻塞节点的任务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 判断队列是否是空队列
        if (h != null &amp;amp;&amp;amp; h != tail) {
            int ws = h.waitStatus;
            // 头节点的状态为 signal，说明后继节点没有被唤醒过
            if (ws == Node.SIGNAL) {
                // cas 设置头节点的状态为 0，设置失败继续自旋
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果有其他线程已经设置了头节点的状态，重新设置为 PROPAGATE 传播属性
            else if (ws == 0 &amp;amp;&amp;amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        // 条件不成立说明被唤醒的节点非常积极，直接将自己设置为了新的head，
        // 此时唤醒它的节点（前驱）执行 h == head 不成立，所以不会跳出循环，会继续唤醒新的 head 节点的后继节点
        if (h == head)
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;CyclicBarrier&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;CyclicBarrier：循环屏障，用来进行线程协作，等待线程满足某个计数，才能触发自己执行&lt;/p&gt;
&lt;p&gt;常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public CyclicBarrier(int parties, Runnable barrierAction)&lt;/code&gt;：用于在线程到达屏障 parties 时，执行 barrierAction
&lt;ul&gt;
&lt;li&gt;parties：代表多少个线程到达屏障开始触发线程任务&lt;/li&gt;
&lt;li&gt;barrierAction：线程任务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int await()&lt;/code&gt;：线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与 CountDownLatch 的区别：CyclicBarrier 是可以重用的&lt;/p&gt;
&lt;p&gt;应用：可以实现多线程中，某个任务在等待其他线程执行完毕以后触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ExecutorService service = Executors.newFixedThreadPool(2);
    CyclicBarrier barrier = new CyclicBarrier(2, () -&amp;gt; {
        System.out.println(&quot;task1 task2 finish...&quot;);
    });

    for (int i = 0; i &amp;lt; 3; i++) { // 循环重用
        service.submit(() -&amp;gt; {
            System.out.println(&quot;task1 begin...&quot;);
            try {
                Thread.sleep(1000);
                barrier.await();    // 2 - 1 = 1
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        service.submit(() -&amp;gt; {
            System.out.println(&quot;task2 begin...&quot;);
            try {
                Thread.sleep(2000);
                barrier.await();    // 1 - 1 = 0
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
    }
    service.shutdown();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全局锁：利用可重入锁实现的工具类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// barrier 实现是依赖于Condition条件队列，condition 条件队列必须依赖lock才能使用
private final ReentrantLock lock = new ReentrantLock();
// 线程挂起实现使用的 condition 队列，当前代所有线程到位，这个条件队列内的线程才会被唤醒
private final Condition trip = lock.newCondition();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int parties;	// 代表多少个线程到达屏障开始触发线程任务
private int count;			// 表示当前“代”还有多少个线程未到位，初始值为 parties
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前代中最后一个线程到位后要执行的事件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Runnable barrierCommand;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示 barrier 对象当前 代
private Generation generation = new Generation();
private static class Generation {
    // 表示当前“代”是否被打破，如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常
    // 且在这一代挂起的线程都会被唤醒，然后抛出 BrokerException 异常。
    boolean broken = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CyclicBarrie(int parties, Runnable barrierAction) {
    // 因为小于等于 0 的 barrier 没有任何意义
    if (parties &amp;lt;= 0) throw new IllegalArgumentException();

    this.parties = parties;
    this.count = parties;
    // 可以为 null
    this.barrierCommand = barrierAction;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-CyclicBarrier工作原理.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;await()：阻塞等待所有线程到位&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// timed：表示当前调用await方法的线程是否指定了超时时长，如果 true 表示线程是响应超时的
// nanos：线程等待超时时长，单位是纳秒
private int dowait(boolean timed, long nanos) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取当前代
        final Generation g = generation;

        // 【如果当前代是已经被打破状态，则当前调用await方法的线程，直接抛出Broken异常】
        if (g.broken)
            throw new BrokenBarrierException();
		// 如果当前线程被中断了，则打破当前代，然后当前线程抛出中断异常
        if (Thread.interrupted()) {
            // 设置当前代的状态为 broken 状态，唤醒在 trip 条件队列内的线程
            breakBarrier();
            throw new InterruptedException();
        }

        // 逻辑到这说明，当前线程中断状态是 false， 当前代的 broken 为 false（未打破状态）
        
        // 假设 parties 给的是 5，那么index对应的值为 4,3,2,1,0
        int index = --count;
        // 条件成立说明当前线程是最后一个到达 barrier 的线程，【需要开启新代，唤醒阻塞线程】
        if (index == 0) {
            // 栅栏任务启动标记
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    // 启动触发的任务
                    command.run();
                // run()未抛出异常的话，启动标记设置为 true
                ranAction = true;
                // 开启新的一代，这里会【唤醒所有的阻塞队列】
                nextGeneration();
                // 返回 0 因为当前线程是此代最后一个到达的线程，index == 0
                return 0;
            } finally {
                // 如果 command.run() 执行抛出异常的话，会进入到这里
                if (!ranAction)
                    breakBarrier();
            }
        }

        // 自旋，一直到条件满足、当前代被打破、线程被中断，等待超时
        for (;;) {
            try {
                // 根据是否需要超时等待选择阻塞方法
                if (!timed)
                    // 当前线程释放掉 lock，【进入到 trip 条件队列的尾部挂起自己】，等待被唤醒
                    trip.await();
                else if (nanos &amp;gt; 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 被中断后来到这里的逻辑
                
                // 当前代没有变化并且没有被打破
                if (g == generation &amp;amp;&amp;amp; !g.broken) {
                    // 打破屏障
                    breakBarrier();
                    // node 节点在【条件队列】内收到中断信号时 会抛出中断异常
                    throw ie;
                } else {
                    // 等待过程中代变化了，完成一次自我打断
                    Thread.currentThread().interrupt();
                }
            }
			// 唤醒后的线程，【判断当前代已经被打破，线程唤醒后依次抛出 BrokenBarrier 异常】
            if (g.broken)
                throw new BrokenBarrierException();

            // 当前线程挂起期间，最后一个线程到位了，然后触发了开启新的一代的逻辑
            if (g != generation)
                return index;
			// 当前线程 trip 中等待超时，然后主动转移到阻塞队列
            if (timed &amp;amp;&amp;amp; nanos &amp;lt;= 0L) {
                breakBarrier();
                // 抛出超时异常
                throw new TimeoutException();
            }
        }
    } finally {
        // 解锁
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;breakBarrier()：打破 Barrier 屏障&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void breakBarrier() {
    // 将代中的 broken 设置为 true，表示这一代是被打破了，再来到这一代的线程，直接抛出异常
    generation.broken = true;
    // 重置 count 为 parties
    count = parties;
    // 将在trip条件队列内挂起的线程全部唤醒，唤醒后的线程会检查当前是否是打破的，然后抛出异常
    trip.signalAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nextGeneration()：开启新的下一代&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void nextGeneration() {
    // 将在 trip 条件队列内挂起的线程全部唤醒
    trip.signalAll();
    // 重置 count 为 parties
    count = parties;

    // 开启新的一代，使用一个新的generation对象，表示新的一代，新的一代和上一代【没有任何关系】
    generation = new Generation();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/457326371/&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Semaphore&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;synchronized 可以起到锁的作用，但某个时间段内，只能有一个线程允许执行&lt;/p&gt;
&lt;p&gt;Semaphore（信号量）用来限制能同时访问共享资源的线程上限，非重入锁&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Semaphore(int permits)&lt;/code&gt;：permits 表示许可线程的数量（state）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Semaphore(int permits, boolean fair)&lt;/code&gt;：fair 表示公平性，如果设为 true，下次执行的线程会是等待最久的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void acquire()&lt;/code&gt;：表示获取许可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void release()&lt;/code&gt;：表示释放许可，acquire() 和 release() 方法之间的代码为同步代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    // 1.创建Semaphore对象
    Semaphore semaphore = new Semaphore(3);

    // 2. 10个线程同时运行
    for (int i = 0; i &amp;lt; 10; i++) {
        new Thread(() -&amp;gt; {
            try {
                // 3. 获取许可
                semaphore.acquire();
                sout(Thread.currentThread().getName() + &quot; running...&quot;);
                Thread.sleep(1000);
                sout(Thread.currentThread().getName() + &quot; end...&quot;);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 4. 释放许可
                semaphore.release();
            }
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;p&gt;加锁流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Semaphore 的 permits（state）为 3，这时 5 个线程来获取资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sync(int permits) {
    setState(permits);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设其中 Thread-1，Thread-2，Thread-4 CAS 竞争成功，permits 变为 0，而 Thread-0 和 Thread-3 竞争失败，进入 AQS 队列park 阻塞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// acquire() -&amp;gt; sync.acquireSharedInterruptibly(1)，可中断
public final void acquireSharedInterruptibly(int arg) {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取通行证，获取成功返回 &amp;gt;= 0的值
    if (tryAcquireShared(arg) &amp;lt; 0)
        // 获取许可证失败，进入阻塞
        doAcquireSharedInterruptibly(arg);
}

// tryAcquireShared() -&amp;gt; nonfairTryAcquireShared()
// 非公平，公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点)
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        // 获取 state ，state 这里【表示通行证】
        int available = getState();
        // 计算当前线程获取通行证完成之后，通行证还剩余数量
        int remaining = available - acquires;
        // 如果许可已经用完, 返回负数, 表示获取失败,
        if (remaining &amp;lt; 0 ||
            // 许可证足够分配的，如果 cas 重试成功, 返回正数, 表示获取成功
            compareAndSetState(available, remaining))
            return remaining;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void doAcquireSharedInterruptibly(int arg) {
    // 将调用 Semaphore.aquire 方法的线程，包装成 node 加入到 AQS 的阻塞队列中
    final Node node = addWaiter(Node.SHARED);
    // 获取标记
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 前驱节点是头节点可以再次获取许可
            if (p == head) {
                // 再次尝试获取许可，【返回剩余的许可证数量】
                int r = tryAcquireShared(arg);
                if (r &amp;gt;= 0) {
                    // 成功后本线程出队（AQS）, 所在 Node设置为 head
                    // r 表示【可用资源数】, 为 0 则不会继续传播
                    setHeadAndPropagate(node, r); 
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
            if (shouldParkAfterFailedAcquire(p, node) &amp;amp;&amp;amp; parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        // 被打断后进入该逻辑
        if (failed)
            cancelAcquire(node);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void setHeadAndPropagate(Node node, int propagate) {    
    Node h = head;
    // 设置自己为 head 节点
    setHead(node);
    // propagate 表示有【共享资源】（例如共享读锁或信号量）
    // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE，doReleaseShared 函数中设置的
    if (propagate &amp;gt; 0 || h == null || h.waitStatus &amp;lt; 0 ||
        (h = head) == null || h.waitStatus &amp;lt; 0) {
        Node s = node.next;
        // 如果是最后一个节点或者是等待共享读锁的节点，做一次唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这时 Thread-4 释放了 permits，状态如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// release() -&amp;gt; releaseShared()
public final boolean releaseShared(int arg) {
    // 尝试释放锁
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }    
    return false;
}
protected final boolean tryReleaseShared(int releases) {    
    for (;;) {
        // 获取当前锁资源的可用许可证数量
        int current = getState();
        int next = current + releases;
        // 索引越界判断
        if (next &amp;lt; current)            
            throw new Error(&quot;Maximum permit count exceeded&quot;);        
        // 释放锁
        if (compareAndSetState(current, next))            
            return true;    
    }
}
private void doReleaseShared() {    
    // PROPAGATE 详解    
    // 如果 head.waitStatus == Node.SIGNAL ==&amp;gt; 0 成功, 下一个节点 unpark	
    // 如果 head.waitStatus == 0 ==&amp;gt; Node.PROPAGATE
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来 Thread-0 竞争成功，permits 再次设置为 0，设置自己为 head 节点，并且 unpark 接下来的共享状态的 Thread-3 节点，但由于 permits 是 0，因此 Thread-3 在尝试不成功后再次进入 park 状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;PROPAGATE&lt;/h4&gt;
&lt;p&gt;假设存在某次循环中队列里排队的结点情况为 &lt;code&gt;head(-1) → t1(-1) → t2(0)&lt;/code&gt;，存在将要释放信号量的 T3 和 T4，释放顺序为先 T3 后 T4&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 老版本代码
private void setHeadAndPropagate(Node node, int propagate) {    
    setHead(node);    
    // 有空闲资源    
    if (propagate &amp;gt; 0 &amp;amp;&amp;amp; node.waitStatus != 0) {    	
        Node s = node.next;        
        // 下一个        
        if (s == null || s.isShared())            
            unparkSuccessor(node);        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T3 调用 releaseShared(1)，直接调用了 unparkSuccessor(head)，head.waitStatus 从 -1 变为 0&lt;/li&gt;
&lt;li&gt;T1 由于 T3 释放信号量被唤醒，然后 T4 释放，唤醒 T2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BUG 流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T3 调用 releaseShared(1)，直接调用了 unparkSuccessor(head)，head.waitStatus 从 -1 变为 0&lt;/li&gt;
&lt;li&gt;T1 由于 T3 释放信号量被唤醒，调用 tryAcquireShared，返回值为 0（获取锁成功，但没有剩余资源量）&lt;/li&gt;
&lt;li&gt;T1 还没调用 setHeadAndPropagate 方法，T4 调用 releaseShared(1)，此时 head.waitStatus 为 0（此时读到的 head 和 1 中为同一个 head），不满足条件，因此不调用 unparkSuccessor(head)&lt;/li&gt;
&lt;li&gt;T1 获取信号量成功，调用 setHeadAndPropagate(t1.node, 0) 时，因为不满足 propagate &amp;gt; 0（剩余资源量 == 0），从而不会唤醒后继结点， &lt;strong&gt;T2 线程得不到唤醒&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更新后流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;T3 调用 releaseShared(1)，直接调用了 unparkSuccessor(head)，head.waitStatus 从 -1 变为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;T1 由于 T3 释放信号量被唤醒，调用 tryAcquireShared，返回值为 0（获取锁成功，但没有剩余资源量）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;T1 还没调用 setHeadAndPropagate 方法，T4 调用 releaseShared()，此时 head.waitStatus 为 0（此时读到的 head 和 1 中为同一个 head），调用 doReleaseShared() 将等待状态置为 &lt;strong&gt;PROPAGATE（-3）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;T1 获取信号量成功，调用 setHeadAndPropagate 时，读到 h.waitStatus &amp;lt; 0，从而调用 doReleaseShared() 唤醒 T2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private void setHeadAndPropagate(Node node, int propagate) {    
    Node h = head;
    // 设置自己为 head 节点
    setHead(node);
    // propagate 表示有共享资源（例如共享读锁或信号量）
    // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
    if (propagate &amp;gt; 0 || h == null || h.waitStatus &amp;lt; 0 ||
        (h = head) == null || h.waitStatus &amp;lt; 0) {
        Node s = node.next;
        // 如果是最后一个节点或者是等待共享读锁的节点，做一次唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 唤醒
private void doReleaseShared() {
    // 如果 head.waitStatus == Node.SIGNAL ==&amp;gt; 0 成功, 下一个节点 unpark	
    // 如果 head.waitStatus == 0 ==&amp;gt; Node.PROPAGATE    
    for (;;) {
        Node h = head;
        if (h != null &amp;amp;&amp;amp; h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 防止 unparkSuccessor 被多次执行
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果已经是 0 了，改为 -3，用来解决传播性
            else if (ws == 0 &amp;amp;&amp;amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Exchanger&lt;/h3&gt;
&lt;p&gt;Exchanger：交换器，是一个用于线程间协作的工具类，用于进行线程间的数据交换&lt;/p&gt;
&lt;p&gt;工作流程：两个线程通过 exchange 方法交换数据，如果第一个线程先执行 exchange() 方法，它会一直等待第二个线程也执行 exchange 方法，当两个线程都到达同步点时，这两个线程就可以交换数据&lt;/p&gt;
&lt;p&gt;常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Exchanger()&lt;/code&gt;：创建一个新的交换器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V exchange(V x)&lt;/code&gt;：等待另一个线程到达此交换点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V exchange(V x, long timeout, TimeUnit unit)&lt;/code&gt;：等待一定的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ExchangerDemo {
    public static void main(String[] args) {
        // 创建交换对象（信使）
        Exchanger&amp;lt;String&amp;gt; exchanger = new Exchanger&amp;lt;&amp;gt;();
        new ThreadA(exchanger).start();
        new ThreadB(exchanger).start();
    } 
}
class ThreadA extends Thread{
    private Exchanger&amp;lt;String&amp;gt; exchanger();
    
    public ThreadA(Exchanger&amp;lt;String&amp;gt; exchanger){
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        try{
            sout(&quot;线程A，做好了礼物A，等待线程B送来的礼物B&quot;);
            //如果等待了5s还没有交换就死亡（抛出异常）！
            String s = exchanger.exchange(&quot;礼物A&quot;,5,TimeUnit.SECONDS);
            sout(&quot;线程A收到线程B的礼物：&quot; + s);
        } catch (Exception e) {
            System.out.println(&quot;线程A等待了5s，没有收到礼物,最终就执行结束了!&quot;);
        }
    }
}
class ThreadB extends Thread{
    private Exchanger&amp;lt;String&amp;gt; exchanger;
    
    public ThreadB(Exchanger&amp;lt;String&amp;gt; exchanger) {
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        try {
            sout(&quot;线程B,做好了礼物B,等待线程A送来的礼物A.....&quot;);
            // 开始交换礼物。参数是送给其他线程的礼物!
            sout(&quot;线程B收到线程A的礼物：&quot; + exchanger.exchange(&quot;礼物B&quot;));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;并发包&lt;/h2&gt;
&lt;h3&gt;ConHashMap&lt;/h3&gt;
&lt;h4&gt;并发集合&lt;/h4&gt;
&lt;h5&gt;集合对比&lt;/h5&gt;
&lt;p&gt;三种集合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HashMap 是线程不安全的，性能好&lt;/li&gt;
&lt;li&gt;Hashtable 线程安全基于 synchronized，综合性能差，已经被淘汰&lt;/li&gt;
&lt;li&gt;ConcurrentHashMap 保证了线程安全，综合性能较好，不止线程安全，而且效率高，性能好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集合对比：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hashtable 继承 Dictionary 类，HashMap、ConcurrentHashMap 继承 AbstractMap，均实现 Map 接口&lt;/li&gt;
&lt;li&gt;Hashtable 底层是数组 + 链表，JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树&lt;/li&gt;
&lt;li&gt;HashMap 线程非安全，Hashtable 线程安全，Hashtable 的方法都加了 synchronized 关来确保线程同步&lt;/li&gt;
&lt;li&gt;ConcurrentHashMap、Hashtable &lt;strong&gt;不允许 null 值&lt;/strong&gt;，HashMap 允许 null 值&lt;/li&gt;
&lt;li&gt;ConcurrentHashMap、HashMap 的初始容量为 16，Hashtable 初始容量为11，填充因子默认都是 0.75，两种 Map 扩容是当前容量翻倍：capacity * 2，Hashtable 扩容时是容量翻倍 + 1：capacity*2 + 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/ConcurrentHashMap%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png&quot; alt=&quot;ConcurrentHashMap数据结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;工作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化，使用 cas 来保证并发安全，懒惰初始化 table&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;树化，当 table.length &amp;lt; 64 时，先尝试扩容，超过 64 时，并且 bin.length &amp;gt; 8 时，会将&lt;strong&gt;链表树化&lt;/strong&gt;，树化过程会用 synchronized 锁住链表头&lt;/p&gt;
&lt;p&gt;说明：锁住某个槽位的对象头，是一种很好的&lt;strong&gt;细粒度的加锁&lt;/strong&gt;方式，类似 MySQL 中的行锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;put，如果该 bin 尚未创建，只需要使用 cas 创建 bin；如果已经有了，锁住链表头进行后续 put 操作，元素添加至 bin 的尾部&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get，无锁操作仅需要保证可见性，扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩容，扩容时以 bin 为单位进行，需要对 bin 进行 synchronized，但这时其它竞争线程也不是无事可做，它们会帮助把其它 bin 进行扩容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;size，元素个数保存在 baseCount 中，并发时的个数变动保存在 CounterCell[] 当中，最后统计数量时累加&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;//需求：多个线程同时往HashMap容器中存入数据会出现安全问题
public class ConcurrentHashMapDemo{
    public static Map&amp;lt;String,String&amp;gt; map = new ConcurrentHashMap();
    
    public static void main(String[] args){
        new AddMapDataThread().start();
        new AddMapDataThread().start();
        
        Thread.sleep(1000 * 5);//休息5秒，确保两个线程执行完毕
        System.out.println(&quot;Map大小：&quot; + map.size());//20万
    }
}

public class AddMapDataThread extends Thread{
    @Override
    public void run() {
        for(int i = 0 ; i &amp;lt; 1000000 ; i++ ){
            ConcurrentHashMapDemo.map.put(&quot;键：&quot;+i , &quot;值&quot;+i);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;并发死链&lt;/h5&gt;
&lt;p&gt;JDK1.7 的 HashMap 采用的头插法（拉链法）进行节点的添加，HashMap 的扩容长度为原来的 2 倍&lt;/p&gt;
&lt;p&gt;resize() 中节点（Entry）转移的源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;//得到新数组的长度   
    // 遍历整个数组对应下标下的链表，e代表一个节点
    for (Entry&amp;lt;K,V&amp;gt; e : table) {   
        // 当e == null时，则该链表遍历完了，继续遍历下一数组下标的链表 
        while(null != e) { 
            // 先把e节点的下一节点存起来
            Entry&amp;lt;K,V&amp;gt; next = e.next; 
            if (rehash) {              //得到新的hash值
                e.hash = null == e.key ? 0 : hash(e.key);  
            }
            // 在新数组下得到新的数组下标
            int i = indexFor(e.hash, newCapacity);  
             // 将e的next指针指向新数组下标的位置
            e.next = newTable[i];   
            // 将该数组下标的节点变为e节点
            newTable[i] = e; 
            // 遍历链表的下一节点
            e = next;                                   
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDK 8 虽然将扩容算法做了调整，改用了尾插法，但仍不意味着能够在多线程环境下能够安全扩容，还会出现其它问题（如扩容丢数据）&lt;/p&gt;
&lt;p&gt;B站视频解析：https://www.bilibili.com/video/BV1n541177Ea&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;成员属性&lt;/h4&gt;
&lt;h5&gt;变量&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存储数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile Node&amp;lt;K,V&amp;gt;[] table;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;散列表的长度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int MAXIMUM_CAPACITY = 1 &amp;lt;&amp;lt; 30;	// 最大长度
private static final int DEFAULT_CAPACITY = 16;			// 默认长度
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并发级别，JDK7 遗留下来，1.8 中不代表并发级别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负载因子，JDK1.8 的 ConcurrentHashMap 中是固定值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final float LOAD_FACTOR = 0.75f;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阈值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int TREEIFY_THRESHOLD = 8;		// 链表树化的阈值
static final int UNTREEIFY_THRESHOLD = 6;	// 红黑树转化为链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64;	// 当数组长度达到64且某个桶位中的链表长度超过8，才会真正树化
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩容相关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int MIN_TRANSFER_STRIDE = 16;	// 线程迁移数据【最小步长】，控制线程迁移任务的最小区间
private static int RESIZE_STAMP_BITS = 16;			// 用来计算扩容时生成的【标识戳】
private static final int MAX_RESIZERS = (1 &amp;lt;&amp;lt; (32 - RESIZE_STAMP_BITS)) - 1;// 65535-1并发扩容最多线程数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;		// 扩容时使用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点哈希值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int MOVED     = -1; 			// 表示当前节点是 FWD 节点
static final int TREEBIN   = -2; 			// 表示当前节点已经树化，且当前节点为 TreeBin 对象
static final int RESERVED  = -3; 			// 表示节点时临时节点
static final int HASH_BITS = 0x7fffffff; 	// 正常节点的哈希值的可用的位数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩容过程：volatile 修饰保证多线程的可见性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 扩容过程中，会将扩容中的新 table 赋值给 nextTable 保持引用，扩容结束之后，这里会被设置为 null
private transient volatile Node&amp;lt;K,V&amp;gt;[] nextTable;
// 记录扩容进度，所有线程都要从 0 - transferIndex 中分配区间任务，简单说就是老表转移到哪了，索引从高到低转移
private transient volatile int transferIndex;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;累加统计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// LongAdder 中的 baseCount 未发生竞争时或者当前LongAdder处于加锁状态时，增量累到到 baseCount 中
private transient volatile long baseCount;
// LongAdder 中的 cellsBuzy，0 表示当前 LongAdder 对象无锁状态，1 表示当前 LongAdder 对象加锁状态
private transient volatile int cellsBusy;
// LongAdder 中的 cells 数组，
private transient volatile CounterCell[] counterCells;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制变量：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sizeCtl&lt;/strong&gt; &amp;lt; 0：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-1 表示当前 table 正在初始化（有线程在创建 table 数组），当前线程需要自旋等待&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他负数表示当前 map 的 table 数组正在进行扩容，高 16 位表示扩容的标识戳；低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + 1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;sizeCtl = 0，表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小&lt;/p&gt;
&lt;p&gt;sizeCtl &amp;gt; 0：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 table 未初始化，表示初始化大小&lt;/li&gt;
&lt;li&gt;如果 table 已经初始化，表示下次扩容时的触发条件（阈值，元素个数，不是数组的长度）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile int sizeCtl;		// volatile 保持可见性
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;内部类&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Node 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static class Node&amp;lt;K,V&amp;gt; implements Entry&amp;lt;K,V&amp;gt; {
    // 节点哈希值
    final int hash;
    final K key;
    volatile V val;
    // 单向链表
    volatile Node&amp;lt;K,V&amp;gt; next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TreeBin 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; static final class TreeBin&amp;lt;K,V&amp;gt; extends Node&amp;lt;K,V&amp;gt; {
     // 红黑树根节点
     TreeNode&amp;lt;K,V&amp;gt; root;
     // 链表的头节点
     volatile TreeNode&amp;lt;K,V&amp;gt; first;
     // 等待者线程
     volatile Thread waiter;

     volatile int lockState;
     // 写锁状态 写锁是独占状态，以散列表来看，真正进入到 TreeBin 中的写线程同一时刻只有一个线程
     static final int WRITER = 1;
     // 等待者状态（写线程在等待），当 TreeBin 中有读线程目前正在读取数据时，写线程无法修改数据
     static final int WAITER = 2;
     // 读锁状态是共享，同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据，每一个线程都给 lockState + 4
     static final int READER = 4;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TreeNode 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class TreeNode&amp;lt;K,V&amp;gt; extends Node&amp;lt;K,V&amp;gt; {
    TreeNode&amp;lt;K,V&amp;gt; parent;  // red-black tree links
    TreeNode&amp;lt;K,V&amp;gt; left;
    TreeNode&amp;lt;K,V&amp;gt; right;
    TreeNode&amp;lt;K,V&amp;gt; prev;   //双向链表
    boolean red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ForwardingNode 节点：转移节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; static final class ForwardingNode&amp;lt;K,V&amp;gt; extends Node&amp;lt;K,V&amp;gt; {
     // 持有扩容后新的哈希表的引用
     final Node&amp;lt;K,V&amp;gt;[] nextTable;
     ForwardingNode(Node&amp;lt;K,V&amp;gt;[] tab) {
         // ForwardingNode 节点的 hash 值设为 -1
         super(MOVED, null, null, null);
         this.nextTable = tab;
     }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;代码块&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示sizeCtl属性在 ConcurrentHashMap 中内存偏移地址
private static final long SIZECTL;
// 表示transferIndex属性在 ConcurrentHashMap 中内存偏移地址
private static final long TRANSFERINDEX;
// 表示baseCount属性在 ConcurrentHashMap 中内存偏移地址
private static final long BASECOUNT;
// 表示cellsBusy属性在 ConcurrentHashMap 中内存偏移地址
private static final long CELLSBUSY;
// 表示cellValue属性在 CounterCell 中内存偏移地址
private static final long CELLVALUE;
// 表示数组第一个元素的偏移地址
private static final long ABASE;
// 用位移运算替代乘法
private static final int ASHIFT;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示数组单元所占用空间大小，scale 表示 Node[] 数组中每一个单元所占用空间大小，int 是 4 字节
int scale = U.arrayIndexScale(ak);
// 判断一个数是不是 2 的 n 次幂，比如 8：1000 &amp;amp; 0111 = 0000
if ((scale &amp;amp; (scale - 1)) != 0)
    throw new Error(&quot;data type scale not a power of two&quot;);

// numberOfLeadingZeros(n)：返回当前数值转换为二进制后，从高位到低位开始统计，看有多少个0连续在一起
// 8 → 1000 numberOfLeadingZeros(8) = 28
// 4 → 100 numberOfLeadingZeros(4) = 29   int 值就是占4个字节
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

// ASHIFT = 31 - 29 = 2 ，int 的大小就是 2 的 2 次方，获取次方数
// ABASE + （5 &amp;lt;&amp;lt; ASHIFT） 用位移运算替代了乘法，获取 arr[5] 的值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;构造方法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无参构造， 散列表结构延迟初始化，默认的数组大小是 16：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentHashMap() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有参构造：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentHashMap(int initialCapacity) {
    // 指定容量初始化
    if (initialCapacity &amp;lt; 0) throw new IllegalArgumentException();
    int cap = ((initialCapacity &amp;gt;= (MAXIMUM_CAPACITY &amp;gt;&amp;gt;&amp;gt; 1)) ?
               MAXIMUM_CAPACITY :
               // 假如传入的参数是 16，16 + 8 + 1 ，最后得到 32
               // 传入 12， 12 + 6 + 1 = 19，最后得到 32，尽可能的大，与 HashMap不一样
               tableSizeFor(initialCapacity + (initialCapacity &amp;gt;&amp;gt;&amp;gt; 1) + 1));
    // sizeCtl &amp;gt; 0，当目前 table 未初始化时，sizeCtl 表示初始化容量
    this.sizeCtl = cap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n &amp;gt;&amp;gt;&amp;gt; 1;
    n |= n &amp;gt;&amp;gt;&amp;gt; 2;
    n |= n &amp;gt;&amp;gt;&amp;gt; 4;
    n |= n &amp;gt;&amp;gt;&amp;gt; 8;
    n |= n &amp;gt;&amp;gt;&amp;gt; 16;
    return (n &amp;lt; 0) ? 1 : (n &amp;gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HashMap 部分详解了该函数，核心思想就是&lt;strong&gt;把最高位是 1 的位以及右边的位全部置 1&lt;/strong&gt;，结果加 1 后就是 2 的 n 次幂&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个参数构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor &amp;gt; 0.0f) || initialCapacity &amp;lt; 0 || concurrencyLevel &amp;lt;= 0)
        throw new IllegalArgumentException();
    // 初始容量小于并发级别
    if (initialCapacity &amp;lt; concurrencyLevel)  
        // 把并发级别赋值给初始容量
        initialCapacity = concurrencyLevel; 
	// loadFactor 默认是 0.75
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size &amp;gt;= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    // sizeCtl &amp;gt; 0，当目前 table 未初始化时，sizeCtl 表示初始化容量
    this.sizeCtl = cap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentHashMap(Map&amp;lt;? extends K, ? extends V&amp;gt; m) {
    this.sizeCtl = DEFAULT_CAPACITY;	// 默认16
    putAll(m);
}
public void putAll(Map&amp;lt;? extends K, ? extends V&amp;gt; m) {
    // 尝试触发扩容
    tryPresize(m.size());
    for (Entry&amp;lt;? extends K, ? extends V&amp;gt; e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private final void tryPresize(int size) {
    // 扩容为大于 2 倍的最小的 2 的 n 次幂
    int c = (size &amp;gt;= (MAXIMUM_CAPACITY &amp;gt;&amp;gt;&amp;gt; 1)) ? MAXIMUM_CAPACITY :
    	tableSizeFor(size + (size &amp;gt;&amp;gt;&amp;gt; 1) + 1);
    int sc;
    while ((sc = sizeCtl) &amp;gt;= 0) {
        Node&amp;lt;K,V&amp;gt;[] tab = table; int n;
        // 数组还未初始化，【一般是调用集合构造方法才会成立，put 后调用该方法都是不成立的】
        if (tab == null || (n = tab.length) == 0) {
            n = (sc &amp;gt; c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        Node&amp;lt;K,V&amp;gt;[] nt = (Node&amp;lt;K,V&amp;gt;[])new Node&amp;lt;?,?&amp;gt;[n];
                        table = nt;
                        sc = n - (n &amp;gt;&amp;gt;&amp;gt; 2);// 扩容阈值：n - 1/4 n
                    }
                } finally {
                    sizeCtl = sc;	// 扩容阈值赋值给sizeCtl
                }
            }
        }
        // 未达到扩容阈值或者数组长度已经大于最大长度
        else if (c &amp;lt;= sc || n &amp;gt;= MAXIMUM_CAPACITY)
            break;
        // 与 addCount 逻辑相同
        else if (tab == table) {
           
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;h5&gt;数据访存&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tabAt()：获取数组某个槽位的&lt;strong&gt;头节点&lt;/strong&gt;，类似于数组中的直接寻址 arr[i]&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// i 是数组索引
static final &amp;lt;K,V&amp;gt; Node&amp;lt;K,V&amp;gt; tabAt(Node&amp;lt;K,V&amp;gt;[] tab, int i) {
    // (i &amp;lt;&amp;lt; ASHIFT) + ABASE == ABASE + i * 4 （一个 int 占 4 个字节），这就相当于寻址，替代了乘法
    return (Node&amp;lt;K,V&amp;gt;)U.getObjectVolatile(tab, ((long)i &amp;lt;&amp;lt; ASHIFT) + ABASE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;casTabAt()：指定数组索引位置修改原值为指定的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final &amp;lt;K,V&amp;gt; boolean casTabAt(Node&amp;lt;K,V&amp;gt;[] tab, int i, Node&amp;lt;K,V&amp;gt; c, Node&amp;lt;K,V&amp;gt; v) {
    return U.compareAndSwapObject(tab, ((long)i &amp;lt;&amp;lt; ASHIFT) + ABASE, c, v);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;setTabAt()：指定数组索引位置设置值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final &amp;lt;K,V&amp;gt; void setTabAt(Node&amp;lt;K,V&amp;gt;[] tab, int i, Node&amp;lt;K,V&amp;gt; v) {
    U.putObjectVolatile(tab, ((long)i &amp;lt;&amp;lt; ASHIFT) + ABASE, v);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;添加方法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public V put(K key, V value) {
    // 第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时【用当前数据覆盖旧数据】
    return putVal(key, value, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;putVal()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 【ConcurrentHashMap 不能存放 null 值】
    if (key == null || value == null) throw new NullPointerException();
    // 扰动运算，高低位都参与寻址运算
    int hash = spread(key.hashCode());
    // 表示当前 k-v 封装成 node 后插入到指定桶位后，在桶位中的所属链表的下标位置
    int binCount = 0;
    // tab 引用当前 map 的数组 table，开始自旋
    for (Node&amp;lt;K,V&amp;gt;[] tab = table;;) {
        // f 表示桶位的头节点，n 表示哈希表数组的长度
        // i 表示 key 通过寻址计算后得到的桶位下标，fh 表示桶位头结点的 hash 值
        Node&amp;lt;K,V&amp;gt; f; int n, i, fh;
        
        // 【CASE1】：表示当前 map 中的 table 尚未初始化
        if (tab == null || (n = tab.length) == 0)
            //【延迟初始化】
            tab = initTable();
        
        // 【CASE2】：i 表示 key 使用【寻址算法】得到 key 对应数组的下标位置，tabAt 获取指定桶位的头结点f
        else if ((f = tabAt(tab, i = (n - 1) &amp;amp; hash)) == null) {
            // 对应的数组为 null 说明没有哈希冲突，直接新建节点添加到表中
            if (casTabAt(tab, i, null, new Node&amp;lt;K,V&amp;gt;(hash, key, value, null)))
                break;
        }
        // 【CASE3】：逻辑说明数组已经被初始化，并且当前 key 对应的位置不为 null
        // 条件成立表示当前桶位的头结点为 FWD 结点，表示目前 map 正处于扩容过程中
        else if ((fh = f.hash) == MOVED)
            // 当前线程【需要去帮助哈希表完成扩容】
            tab = helpTransfer(tab, f);
        
        // 【CASE4】：哈希表没有在扩容，当前桶位可能是链表也可能是红黑树
        else {
            // 当插入 key 存在时，会将旧值赋值给 oldVal 返回
            V oldVal = null;
            // 【锁住当前 key 寻址的桶位的头节点】
            synchronized (f) {
                // 这里重新获取一下桶的头节点有没有被修改，因为可能被其他线程修改过，这里是线程安全的获取
                if (tabAt(tab, i) == f) {
                    // 【头节点的哈希值大于 0 说明当前桶位是普通的链表节点】
                    if (fh &amp;gt;= 0) {
                        // 当前的插入操作没出现重复的 key，追加到链表的末尾，binCount表示链表长度 -1
                        // 插入的key与链表中的某个元素的 key 一致，变成替换操作，binCount 表示第几个节点冲突
                        binCount = 1;
                        // 迭代循环当前桶位的链表，e 是每次循环处理节点，e 初始是头节点
                        for (Node&amp;lt;K,V&amp;gt; e = f;; ++binCount) {
                            // 当前循环节点 key
                            K ek;
                            // key 的哈希值与当前节点的哈希一致，并且 key 的值也相同
                            if (e.hash == hash &amp;amp;&amp;amp;
                                ((ek = e.key) == key ||
                                 (ek != null &amp;amp;&amp;amp; key.equals(ek)))) {
                                // 把当前节点的 value 赋值给 oldVal
                                oldVal = e.val;
                                // 允许覆盖
                                if (!onlyIfAbsent)
                                    // 新数据覆盖旧数据
                                    e.val = value;
                                // 跳出循环
                                break;
                            }
                            Node&amp;lt;K,V&amp;gt; pred = e;
                            // 如果下一个节点为空，把数据封装成节点插入链表尾部，【binCount 代表长度 - 1】
                            if ((e = e.next) == null) {
                                pred.next = new Node&amp;lt;K,V&amp;gt;(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 当前桶位头节点是红黑树
                    else if (f instanceof TreeBin) {
                        Node&amp;lt;K,V&amp;gt; p;
                        binCount = 2;
                        if ((p = ((TreeBin&amp;lt;K,V&amp;gt;)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            
            // 条件成立说明当前是链表或者红黑树
            if (binCount != 0) {
                // 如果 binCount &amp;gt;= 8 表示处理的桶位一定是链表，说明长度是 9
                if (binCount &amp;gt;= TREEIFY_THRESHOLD)
                    // 树化
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 统计当前 table 一共有多少数据，判断是否达到扩容阈值标准，触发扩容
    // binCount = 0 表示当前桶位为 null，node 可以直接放入，2 表示当前桶位已经是红黑树
    addCount(1L, binCount);
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;spread()：扰动函数&lt;/p&gt;
&lt;p&gt;将 hashCode 无符号右移 16 位，高 16bit 和低 16bit 做异或，最后与 HASH_BITS 相与变成正数，&lt;strong&gt;与树化节点和转移节点区分&lt;/strong&gt;，把高低位都利用起来减少哈希冲突，保证散列的均匀性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int spread(int h) {
    return (h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)) &amp;amp; HASH_BITS; // 0111 1111 1111 1111 1111 1111 1111 1111
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;initTable()：初始化数组，延迟初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Node&amp;lt;K,V&amp;gt;[] initTable() {
    // tab 引用 map.table，sc 引用 sizeCtl
    Node&amp;lt;K,V&amp;gt;[] tab; int sc;
    // table 尚未初始化，开始自旋
    while ((tab = table) == null || tab.length == 0) {
        // sc &amp;lt; 0 说明 table 正在初始化或者正在扩容，当前线程可以释放 CPU 资源
        if ((sc = sizeCtl) &amp;lt; 0)
            Thread.yield();
        // sizeCtl 设置为 -1，相当于加锁，【设置的是 SIZECTL 位置的数据】，
        // 因为是 sizeCtl 是基本类型，不是引用类型，所以 sc 保存的是数据的副本
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 线程安全的逻辑，再进行一次判断
                if ((tab = table) == null || tab.length == 0) {
                    // sc &amp;gt; 0 创建 table 时使用 sc 为指定大小，否则使用 16 默认值
                    int n = (sc &amp;gt; 0) ? sc : DEFAULT_CAPACITY;
                    // 创建哈希表数组
                    Node&amp;lt;K,V&amp;gt;[] nt = (Node&amp;lt;K,V&amp;gt;[])new Node&amp;lt;?,?&amp;gt;[n];
                    table = tab = nt;
                    // 扩容阈值，n &amp;gt;&amp;gt;&amp;gt; 2  =&amp;gt; 等于 1/4 n ，n - (1/4)n = 3/4 n =&amp;gt; 0.75 * n
                    sc = n - (n &amp;gt;&amp;gt;&amp;gt; 2);
                }
            } finally {
                // 解锁，把下一次扩容的阈值赋值给 sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;treeifyBin()：树化方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final void treeifyBin(Node&amp;lt;K,V&amp;gt;[] tab, int index) {
    Node&amp;lt;K,V&amp;gt; b; int n, sc;
    if (tab != null) {
        // 条件成立：【说明当前 table 数组长度未达到 64，此时不进行树化操作，进行扩容操作】
        if ((n = tab.length) &amp;lt; MIN_TREEIFY_CAPACITY)
            // 当前容量的 2 倍
            tryPresize(n &amp;lt;&amp;lt; 1);

        // 条件成立：说明当前桶位有数据，且是普通 node 数据。
        else if ((b = tabAt(tab, index)) != null &amp;amp;&amp;amp; b.hash &amp;gt;= 0) {
            // 【树化加锁】
            synchronized (b) {
                // 条件成立：表示加锁没问题。
                if (tabAt(tab, index) == b) {
                    TreeNode&amp;lt;K,V&amp;gt; hd = null, tl = null;
                    for (Node&amp;lt;K,V&amp;gt; e = b; e != null; e = e.next) {
                        TreeNode&amp;lt;K,V&amp;gt; p = new TreeNode&amp;lt;K,V&amp;gt;(e.hash, e.key, e.val,null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin&amp;lt;K,V&amp;gt;(hd));
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addCount()：添加计数，&lt;strong&gt;代表哈希表中的数据总量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final void addCount(long x, int check) {
    // 【上面这部分的逻辑就是 LongAdder 的累加逻辑】
    CounterCell[] as; long b, s;
    // 判断累加数组 cells 是否初始化，没有就去累加 base 域，累加失败进入条件内逻辑
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        // true 未竞争，false 发生竞争
        boolean uncontended = true;
        // 判断 cells 是否被其他线程初始化
        if (as == null || (m = as.length - 1) &amp;lt; 0 ||
            // 前面的条件为 fasle 说明 cells 被其他线程初始化，通过 hash 寻址对应的槽位
            (a = as[ThreadLocalRandom.getProbe() &amp;amp; m]) == null ||
            // 尝试去对应的槽位累加，累加失败进入 fullAddCount 进行重试或者扩容
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 与 Striped64#longAccumulate 方法相同
            fullAddCount(x, uncontended);
            return;
        }
        // 表示当前桶位是 null，或者一个链表节点
        if (check &amp;lt;= 1)	
            return;
    	// 【获取当前散列表元素个数】，这是一个期望值
        s = sumCount();
    }
    
    // 表示一定 【是一个 put 操作调用的 addCount】
    if (check &amp;gt;= 0) {
        Node&amp;lt;K,V&amp;gt;[] tab, nt; int n, sc;
        
        // 条件一：true 说明当前 sizeCtl 可能为一个负数表示正在扩容中，或者 sizeCtl 是一个正数，表示扩容阈值
        //        false 表示哈希表的数据的数量没达到扩容条件
        // 然后判断当前 table 数组是否初始化了，当前 table 长度是否小于最大值限制，就可以进行扩容
        while (s &amp;gt;= (long)(sc = sizeCtl) &amp;amp;&amp;amp; (tab = table) != null &amp;amp;&amp;amp;
               (n = tab.length) &amp;lt; MAXIMUM_CAPACITY) {
            // 16 -&amp;gt; 32 扩容 标识为：1000 0000 0001 1011，【负数，扩容批次唯一标识戳】
            int rs = resizeStamp(n);
            
            // 表示当前 table，【正在扩容】，sc 高 16 位是扩容标识戳，低 16 位是线程数 + 1
            if (sc &amp;lt; 0) {
                // 条件一：判断扩容标识戳是否一样，fasle 代表一样
                // 勘误两个条件：
                // 条件二是：sc == (rs &amp;lt;&amp;lt; 16 ) + 1，true 代表扩容完成，因为低16位是1代表没有线程扩容了
                // 条件三是：sc == (rs &amp;lt;&amp;lt; 16) + MAX_RESIZERS，判断是否已经超过最大允许的并发扩容线程数
                // 条件四：判断新表的引用是否是 null，代表扩容完成
                // 条件五：【扩容是从高位到低位转移】，transferIndex &amp;lt; 0 说明没有区间需要扩容了
                if ((sc &amp;gt;&amp;gt;&amp;gt; RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex &amp;lt;= 0)
                    break;
                
                // 设置当前线程参与到扩容任务中，将 sc 低 16 位值加 1，表示多一个线程参与扩容
                // 设置失败其他线程或者 transfer 内部修改了 sizeCtl 值
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    //【协助扩容线程】，持有nextTable参数
                    transfer(tab, nt);
            }
            // 逻辑到这说明当前线程是触发扩容的第一个线程，线程数量 + 2
            // 1000 0000 0001 1011 0000 0000 0000 0000 +2 =&amp;gt; 1000 0000 0001 1011 0000 0000 0000 0010
            else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs &amp;lt;&amp;lt; RESIZE_STAMP_SHIFT) + 2))
                //【触发扩容条件的线程】，不持有 nextTable，初始线程会新建 nextTable
                transfer(tab, null);
            s = sumCount();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resizeStamp()：扩容标识符，&lt;strong&gt;每次扩容都会产生一个，不是每个线程都产生&lt;/strong&gt;，16 扩容到 32 产生一个，32 扩容到 64 产生一个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 扩容的标识符
 * 16 -&amp;gt; 32 从16扩容到32
 * numberOfLeadingZeros(16) =&amp;gt; 1 0000 =&amp;gt; 32 - 5 = 27 =&amp;gt; 0000 0000 0001 1011
 * (1 &amp;lt;&amp;lt; (RESIZE_STAMP_BITS - 1)) =&amp;gt; 1000 0000 0000 0000 =&amp;gt; 32768
 * ---------------------------------------------------------------
 * 0000 0000 0001 1011
 * 1000 0000 0000 0000
 * 1000 0000 0001 1011
 * 永远是负数
 */
static final int resizeStamp(int n) {
    // 或运算
    return Integer.numberOfLeadingZeros(n) | (1 &amp;lt;&amp;lt; (RESIZE_STAMP_BITS - 1)); // (16 -1 = 15)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;扩容方法&lt;/h5&gt;
&lt;p&gt;扩容机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当链表中元素个数超过 8 个，数组的大小还未超过 64 时，此时进行数组的扩容，如果超过则将链表转化成红黑树&lt;/li&gt;
&lt;li&gt;put 数据后调用 addCount() 方法，判断当前哈希表的容量超过阈值 sizeCtl，超过进行扩容&lt;/li&gt;
&lt;li&gt;增删改线程发现其他线程正在扩容，帮其扩容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;transfer()：数据转移到新表中，完成扩容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final void transfer(Node&amp;lt;K,V&amp;gt;[] tab, Node&amp;lt;K,V&amp;gt;[] nextTab) {
    // n 表示扩容之前 table 数组的长度
    int n = tab.length, stride;
    // stride 表示分配给线程任务的步长，默认就是 16 
    if ((stride = (NCPU &amp;gt; 1) ? (n &amp;gt;&amp;gt;&amp;gt; 3) / NCPU : n) &amp;lt; MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;
    // 如果当前线程为触发本次扩容的线程，需要做一些扩容准备工作，【协助线程不做这一步】
    if (nextTab == null) {
        try {
            // 创建一个容量是之前【二倍的 table 数组】
            Node&amp;lt;K,V&amp;gt;[] nt = (Node&amp;lt;K,V&amp;gt;[])new Node&amp;lt;?,?&amp;gt;[n &amp;lt;&amp;lt; 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 把新表赋值给对象属性 nextTable，方便其他线程获取新表
        nextTable = nextTab;
        // 记录迁移数据整体位置的一个标记，transferIndex 计数从1开始不是 0，所以这里是长度，不是长度-1
        transferIndex = n;
    }
    // 新数组的长度
    int nextn = nextTab.length;
    // 当某个桶位数据处理完毕后，将此桶位设置为 fwd 节点，其它写线程或读线程看到后，可以从中获取到新表
    ForwardingNode&amp;lt;K,V&amp;gt; fwd = new ForwardingNode&amp;lt;K,V&amp;gt;(nextTab);
    // 推进标记
    boolean advance = true;
    // 完成标记
    boolean finishing = false;
    
    // i 表示分配给当前线程任务，执行到的桶位
    // bound 表示分配给当前线程任务的下界限制，因为是倒序迁移，16 迁移完 迁移 15，15完成去迁移14
    for (int i = 0, bound = 0;;) {
        Node&amp;lt;K,V&amp;gt; f; int fh;
        
        // 给当前线程【分配任务区间】
        while (advance) {
            // 分配任务的开始下标，分配任务的结束下标
            int nextIndex, nextBound;
         
            // --i 让当前线程处理下一个索引，true说明当前的迁移任务尚未完成，false说明线程已经完成或者还未分配
            if (--i &amp;gt;= bound || finishing)
                advance = false;
            // 迁移的开始下标，小于0说明没有区间需要迁移了，设置当前线程的 i 变量为 -1 跳出循环
            else if ((nextIndex = transferIndex) &amp;lt;= 0) {
                i = -1;
                advance = false;
            }
            // 逻辑到这说明还有区间需要分配，然后给当前线程分配任务，
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
                      // 判断区间是否还够一个步长，不够就全部分配
                      nextBound = (nextIndex &amp;gt; stride ? nextIndex - stride : 0))) {
                // 当前线程的结束下标
                bound = nextBound;
                // 当前线程的开始下标，上一个线程结束的下标的下一个索引就是这个线程开始的下标
                i = nextIndex - 1;
                // 任务分配结束，跳出循环执行迁移操作
                advance = false;
            }
        }
        
        // 【分配完成，开始数据迁移操作】
        // 【CASE1】：i &amp;lt; 0 成立表示当前线程未分配到任务，或者任务执行完了
        if (i &amp;lt; 0 || i &amp;gt;= n || i + n &amp;gt;= nextn) {
            int sc;
            // 如果迁移完成
            if (finishing) {
                nextTable = null;	// help GC
                table = nextTab;	// 新表赋值给当前对象
                sizeCtl = (n &amp;lt;&amp;lt; 1) - (n &amp;gt;&amp;gt;&amp;gt; 1);// 扩容阈值为 2n - n/2 = 3n/2 = 0.75*(2n)
                return;
            }
            // 当前线程完成了分配的任务区间，可以退出，先把 sizeCtl 赋值给 sc 保留
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 判断当前线程是不是最后一个线程，不是的话直接 return，
                if ((sc - 2) != resizeStamp(n) &amp;lt;&amp;lt; RESIZE_STAMP_SHIFT)
                    return;
                // 所以最后一个线程退出的时候，sizeCtl 的低 16 位为 1
                finishing = advance = true;
                // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的区间】
                i = n;
            }
        }
        
        // 【CASE2】：当前桶位未存放数据，只需要将此处设置为 fwd 节点即可。
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 【CASE3】：说明当前桶位已经迁移过了，当前线程不用再处理了，直接处理下一个桶位即可
        else if ((fh = f.hash) == MOVED)
            advance = true; 
        // 【CASE4】：当前桶位有数据，而且 node 节点不是 fwd 节点，说明这些数据需要迁移
        else {
            // 【锁住头节点】
            synchronized (f) {
                // 二次检查，防止头节点已经被修改了，因为这里才是线程安全的访问
                if (tabAt(tab, i) == f) {
                    // 【迁移数据的逻辑，和 HashMap 相似】
                        
                    // ln 表示低位链表引用
                    // hn 表示高位链表引用
                    Node&amp;lt;K,V&amp;gt; ln, hn;
                    // 哈希 &amp;gt; 0 表示当前桶位是链表桶位
                    if (fh &amp;gt;= 0) {
                        // 和 HashMap 的处理方式一致，与老数组长度相与，16 是 10000
                        // 判断对应的 1 的位置上是 0 或 1 分成高低位链表
                        int runBit = fh &amp;amp; n;
                        Node&amp;lt;K,V&amp;gt; lastRun = f;
                        // 遍历链表，寻找【逆序看】最长的对应位相同的链表，看下面的图更好的理解
                        for (Node&amp;lt;K,V&amp;gt; p = f.next; p != null; p = p.next) {
                            // 将当前节点的哈希 与 n
                            int b = p.hash &amp;amp; n;
                            // 如果当前值与前面节点的值 对应位 不同，则修改 runBit，把 lastRun 指向当前节点
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 判断筛选出的链表是低位的还是高位的
                        if (runBit == 0) {
                            ln = lastRun;	// ln 指向该链表
                            hn = null;		// hn 为 null
                        }
                        // 说明 lastRun 引用的链表为高位链表，就让 hn 指向高位链表头节点
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 从头开始遍历所有的链表节点，迭代到 p == lastRun 节点跳出循环
                        for (Node&amp;lt;K,V&amp;gt; p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph &amp;amp; n) == 0)
                                // 【头插法】，从右往左看，首先 ln 指向的是上一个节点，
                                // 所以这次新建的节点的 next 指向上一个节点，然后更新 ln 的引用
                                ln = new Node&amp;lt;K,V&amp;gt;(ph, pk, pv, ln);
                            else
                                hn = new Node&amp;lt;K,V&amp;gt;(ph, pk, pv, hn);
                        }
                        // 高低位链设置到新表中的指定位置
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 老表中的该桶位设置为 fwd 节点
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 条件成立：表示当前桶位是 红黑树结点
                    else if (f instanceof TreeBin) {
                        TreeBin&amp;lt;K,V&amp;gt; t = (TreeBin&amp;lt;K,V&amp;gt;)f;
                        TreeNode&amp;lt;K,V&amp;gt; lo = null, loTail = null;
                        TreeNode&amp;lt;K,V&amp;gt; hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        // 迭代 TreeBin 中的双向链表，从头结点至尾节点
                        for (Node&amp;lt;K,V&amp;gt; e = t.first; e != null; e = e.next) {
                            // 迭代的当前元素的 hash
                            int h = e.hash;
                            TreeNode&amp;lt;K,V&amp;gt; p = new TreeNode&amp;lt;K,V&amp;gt;
                                (h, e.key, e.val, null, null);
                            // 条件成立表示当前循环节点属于低位链节点
                            if ((h &amp;amp; n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    //【尾插法】
                                    loTail.next = p;
                                // loTail 指向尾节点
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 拆成的高位低位两个链，【判断是否需要需要转化为链表】，反之保持树化
                        ln = (lc &amp;lt;= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                        (hc != 0) ? new TreeBin&amp;lt;K,V&amp;gt;(lo) : t;
                        hn = (hc &amp;lt;= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                        (lc != 0) ? new TreeBin&amp;lt;K,V&amp;gt;(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;链表处理的 LastRun 机制，&lt;strong&gt;可以减少节点的创建&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentHashMap-LastRun%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;helpTransfer()：帮助扩容机制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final Node&amp;lt;K,V&amp;gt;[] helpTransfer(Node&amp;lt;K,V&amp;gt;[] tab, Node&amp;lt;K,V&amp;gt; f) {
    Node&amp;lt;K,V&amp;gt;[] nextTab; int sc;
    // 数组不为空，节点是转发节点，获取转发节点指向的新表开始协助主线程扩容
    if (tab != null &amp;amp;&amp;amp; (f instanceof ForwardingNode) &amp;amp;&amp;amp;
        (nextTab = ((ForwardingNode&amp;lt;K,V&amp;gt;)f).nextTable) != null) {
        // 扩容标识戳
        int rs = resizeStamp(tab.length);
        // 判断数据迁移是否完成，迁移完成会把 新表赋值给 nextTable 属性
        while (nextTab == nextTable &amp;amp;&amp;amp; table == tab &amp;amp;&amp;amp; (sc = sizeCtl) &amp;lt; 0) {
            if ((sc &amp;gt;&amp;gt;&amp;gt; RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex &amp;lt;= 0)
                break;
            // 设置扩容线程数量 + 1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 协助扩容
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;获取方法&lt;/h5&gt;
&lt;p&gt;ConcurrentHashMap 使用 get()  方法获取指定 key 的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get()：获取指定数据的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V get(Object key) {
    Node&amp;lt;K,V&amp;gt;[] tab; Node&amp;lt;K,V&amp;gt; e, p; int n, eh; K ek;
    // 扰动运算，获取 key 的哈希值
    int h = spread(key.hashCode());
    // 判断当前哈希表的数组是否初始化
    if ((tab = table) != null &amp;amp;&amp;amp; (n = tab.length) &amp;gt; 0 &amp;amp;&amp;amp;
        // 如果 table 已经初始化，进行【哈希寻址】，映射到数组对应索引处，获取该索引处的头节点
        (e = tabAt(tab, (n - 1) &amp;amp; h)) != null) {
        // 对比头结点 hash 与查询 key 的 hash 是否一致
        if ((eh = e.hash) == h) {
            // 进行值的判断，如果成功就说明当前节点就是要查询的节点，直接返回
            if ((ek = e.key) == key || (ek != null &amp;amp;&amp;amp; key.equals(ek)))
                return e.val;
        }
        // 当前槽位的【哈希值小于0】说明是红黑树节点或者是正在扩容的 fwd 节点
        else if (eh &amp;lt; 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 当前桶位是【链表】，循环遍历查找
        while ((e = e.next) != null) {
            if (e.hash == h &amp;amp;&amp;amp;
                ((ek = e.key) == key || (ek != null &amp;amp;&amp;amp; key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ForwardingNode#find：转移节点的查找方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node&amp;lt;K,V&amp;gt; find(int h, Object k) {
    // 获取新表的引用
    outer: for (Node&amp;lt;K,V&amp;gt;[] tab = nextTable;;)  {
        // e 表示在扩容而创建新表使用寻址算法得到的桶位头结点，n 表示为扩容而创建的新表的长度
        Node&amp;lt;K,V&amp;gt; e; int n;
 
        if (k == null || tab == null || (n = tab.length) == 0 ||
            // 在新表中重新定位 hash 对应的头结点，表示在 oldTable 中对应的桶位在迁移之前就是 null
            (e = tabAt(tab, (n - 1) &amp;amp; h)) == null)
            return null;

        for (;;) {
            int eh; K ek;
            // 【哈希相同值也相同】，表示新表当前命中桶位中的数据，即为查询想要数据
            if ((eh = e.hash) == h &amp;amp;&amp;amp; ((ek = e.key) == k || (ek != null &amp;amp;&amp;amp; k.equals(ek))))
                return e;

            // eh &amp;lt; 0 说明当前新表中该索引的头节点是 TreeBin 类型，或者是 FWD 类型
            if (eh &amp;lt; 0) {
                // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】，在此方法处再次拿到 FWD 类型
                if (e instanceof ForwardingNode) {
                    // 继续获取新的 fwd 指向的新数组的地址，递归了
                    tab = ((ForwardingNode&amp;lt;K,V&amp;gt;)e).nextTable;
                    continue outer;
                }
                else
                    // 说明此桶位为 TreeBin 节点，使用TreeBin.find 查找红黑树中相应节点。
                    return e.find(h, k);
            }

            // 逻辑到这说明当前桶位是链表，将当前元素指向链表的下一个元素，判断当前元素的下一个位置是否为空
            if ((e = e.next) == null)
                // 条件成立说明迭代到链表末尾，【未找到对应的数据，返回 null】
                return null;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;删除方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;remove()：删除指定元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V remove(Object key) {
    return replaceNode(key, null, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;replaceNode()：替代指定的元素，会协助扩容，&lt;strong&gt;增删改（写）都会协助扩容，查询（读）操作不会&lt;/strong&gt;，因为读操作不涉及加锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final V replaceNode(Object key, V value, Object cv) {
    // 计算 key 扰动运算后的 hash
    int hash = spread(key.hashCode());
    // 开始自旋
    for (Node&amp;lt;K,V&amp;gt;[] tab = table;;) {
        Node&amp;lt;K,V&amp;gt; f; int n, i, fh;
        
        // 【CASE1】：table 还未初始化或者哈希寻址的数组索引处为 null，直接结束自旋，返回 null
        if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) &amp;amp; hash)) == null)
            break;
        // 【CASE2】：条件成立说明当前 table 正在扩容，【当前是个写操作，所以当前线程需要协助 table 完成扩容】
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 【CASE3】：当前桶位可能是 链表 也可能是 红黑树 
        else {
            // 保留替换之前数据引用
            V oldVal = null;
            // 校验标记
            boolean validated = false;
            // 【加锁当前桶位头结点】，加锁成功之后会进入代码块
            synchronized (f) {
                // 双重检查
                if (tabAt(tab, i) == f) {
                    // 说明当前节点是链表节点
                    if (fh &amp;gt;= 0) {
                        validated = true;
                        //遍历所有的节点
                        for (Node&amp;lt;K,V&amp;gt; e = f, pred = null;;) {
                            K ek;
                            // hash 和值都相同，定位到了具体的节点
                            if (e.hash == hash &amp;amp;&amp;amp;
                                ((ek = e.key) == key ||
                                 (ek != null &amp;amp;&amp;amp; key.equals(ek)))) {
                                // 当前节点的value
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null &amp;amp;&amp;amp; cv.equals(ev))) {
                                    // 将当前节点的值 赋值给 oldVal 后续返回会用到
                                    oldVal = ev;
                                    if (value != null)		// 条件成立说明是替换操作
                                        e.val = value;	
                                    else if (pred != null)	// 非头节点删除操作，断开链表
                                        pred.next = e.next;	
                                    else
                                        // 说明当前节点即为头结点，将桶位头节点设置为以前头节点的下一个节点
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    // 说明是红黑树节点
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin&amp;lt;K,V&amp;gt; t = (TreeBin&amp;lt;K,V&amp;gt;)f;
                        TreeNode&amp;lt;K,V&amp;gt; r, p;
                        if ((r = t.root) != null &amp;amp;&amp;amp;
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null &amp;amp;&amp;amp; cv.equals(pv))) {
                                oldVal = pv;
                                // 条件成立说明替换操作
                                if (value != null)
                                    p.val = value;
                                // 删除操作
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            // 其他线程修改过桶位头结点时，当前线程 sync 头结点锁错对象，validated 为 false，会进入下次 for 自旋
            if (validated) {
                if (oldVal != null) {
                    // 替换的值为 null，【说明当前是一次删除操作，更新当前元素个数计数器】
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/457326371/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;JDK7原理&lt;/h4&gt;
&lt;p&gt;ConcurrentHashMap 对锁粒度进行了优化，&lt;strong&gt;分段锁技术&lt;/strong&gt;，将整张表分成了多个数组（Segment），每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行，Segment 是一种可重入锁，继承 ReentrantLock，并发时锁住的是每个 Segment，其他 Segment 还是可以操作的，这样不同 Segment 之间就可以实现并发，大大提高效率。&lt;/p&gt;
&lt;p&gt;底层结构： &lt;strong&gt;Segment 数组 + HashEntry 数组 + 链表&lt;/strong&gt;（数组 + 链表是 HashMap 的结构）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：如果多个线程访问不同的 segment，实际是没有冲突的，这与 JDK8 中是类似的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：Segments 数组默认大小为16，这个容量初始化指定后就不能改变了，并且不是懒惰初始化&lt;/p&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentHashMap 1.7底层结构.png)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;CopyOnWrite&lt;/h3&gt;
&lt;h4&gt;原理分析&lt;/h4&gt;
&lt;p&gt;CopyOnWriteArrayList 采用了&lt;strong&gt;写入时拷贝&lt;/strong&gt;的思想，增删改操作会将底层数组拷贝一份，在新数组上执行操作，不影响其它线程的&lt;strong&gt;并发读，读写分离&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装，装饰器模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList&amp;lt;E&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存储结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile Object[] array;	// volatile 保证了读写线程之间的可见性
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局锁：保证线程的执行安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final transient ReentrantLock lock = new ReentrantLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增数据：需要加锁，&lt;strong&gt;创建新的数组操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁，保证线程安全
    lock.lock();
    try {
        // 获取旧的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 【拷贝新的数组（这里是比较耗时的操作，但不影响其它读线程）】
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 添加新元素
        newElements[len] = e;
        // 替换旧的数组，【这个操作以后，其他线程获取数组就是获取的新数组了】
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读操作：不加锁，&lt;strong&gt;在原数组上操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;适合读多写少的应用场景&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;迭代器：CopyOnWriteArrayList 在返回迭代器时，&lt;strong&gt;创建一个内部数组当前的快照（引用）&lt;/strong&gt;，即使其他线程替换了原始数组，迭代器遍历的快照依然引用的是创建快照时的数组，所以这种实现方式也存在一定的数据延迟性，对其他线程并行添加的数据不可见&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Iterator&amp;lt;E&amp;gt; iterator() {
    // 获取到数组引用，整个遍历的过程该数组都不会变，一直引用的都是老数组，
    return new COWIterator&amp;lt;E&amp;gt;(getArray(), 0);
}

// 迭代器会创建一个底层array的快照，故主类的修改不影响该快照
static final class COWIterator&amp;lt;E&amp;gt; implements ListIterator&amp;lt;E&amp;gt; {
    // 内部数组快照
    private final Object[] snapshot;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        // 数组的引用在迭代过程不会改变
        snapshot = elements;
    }
    // 【不支持写操作】，因为是在快照上操作，无法同步回去
    public void remove() {
        throw new UnsupportedOperationException();
    } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;弱一致性&lt;/h4&gt;
&lt;p&gt;数据一致性就是读到最新更新的数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;强一致性：当更新操作完成之后，任何多个后续进程或者线程的访问都会返回最新的更新过的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弱一致性：系统并不保证进程或者线程的访问都会返回最新的更新过的值，也不会承诺多久之后可以读到&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-CopyOnWriteArrayList弱一致性.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间点&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Thread-0 getArray()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Thread-1 getArray()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Thread-1 setArray(arrayCopy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Thread-0 array[index]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Thread-0 读到了脏数据&lt;/p&gt;
&lt;p&gt;不一定弱一致性就不好&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库的&lt;strong&gt;事务隔离级别&lt;/strong&gt;就是弱一致性的表现&lt;/li&gt;
&lt;li&gt;并发高和一致性是矛盾的，需要权衡&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;安全失败&lt;/h4&gt;
&lt;p&gt;在 java.util 包的集合类就都是快速失败的，而 java.util.concurrent 包下的类都是安全失败&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快速失败：在 A 线程使用&lt;strong&gt;迭代器&lt;/strong&gt;对集合进行遍历的过程中，此时 B 线程对集合进行修改（增删改），或者 A 线程在遍历过程中对集合进行修改，都会导致 A 线程抛出 ConcurrentModificationException 异常&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbstractList 类中的成员变量 modCount，用来记录 List 结构发生变化的次数，&lt;strong&gt;结构发生变化&lt;/strong&gt;是指添加或者删除至少一个元素的操作，或者是调整内部数组的大小，仅仅设置元素的值不算结构发生变化&lt;/li&gt;
&lt;li&gt;在进行序列化或者迭代等操作时，需要比较操作前后 modCount 是否改变，如果改变了抛出 CME 异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全失败：采用安全失败机制的集合容器，在&lt;strong&gt;迭代器&lt;/strong&gt;遍历时直接在原集合数组内容上访问，但其他线程的增删改都会新建数组进行修改，就算修改了集合底层的数组容器，迭代器依然引用着以前的数组（&lt;strong&gt;快照思想&lt;/strong&gt;），所以不会出现异常&lt;/p&gt;
&lt;p&gt;ConcurrentHashMap 不会出现并发时的迭代异常，因为在迭代过程中 CHM 的迭代器并没有判断结构的变化，迭代器还可以根据迭代的节点状态去寻找并发扩容时的新表进行迭代&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ConcurrentHashMap map = new ConcurrentHashMap();
// KeyIterator
Iterator iterator = map.keySet().iterator();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; Traverser(Node&amp;lt;K,V&amp;gt;[] tab, int size, int index, int limit) {
     // 引用还是原来集合的 Node 数组，所以其他线程对数据的修改是可见的
     this.tab = tab;
     this.baseSize = size;
     this.baseIndex = this.index = index;
     this.baseLimit = limit;
     this.next = null;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public final boolean hasNext() { return next != null; }
public final K next() {
    Node&amp;lt;K,V&amp;gt; p;
    if ((p = next) == null)
        throw new NoSuchElementException();
    K k = p.key;
    lastReturned = p;
    // 在方法中进行下一个节点的获取，会进行槽位头节点的状态判断
    advance();
    return k;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Collections&lt;/h3&gt;
&lt;p&gt;Collections类是用来操作集合的工具类，提供了集合转换成线程安全的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public static &amp;lt;T&amp;gt; Collection&amp;lt;T&amp;gt; synchronizedCollection(Collection&amp;lt;T&amp;gt; c) {
     return new SynchronizedCollection&amp;lt;&amp;gt;(c);
 }
public static &amp;lt;K,V&amp;gt; Map&amp;lt;K,V&amp;gt; synchronizedMap(Map&amp;lt;K,V&amp;gt; m) {
    return new SynchronizedMap&amp;lt;&amp;gt;(m);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码：底层也是对方法进行加锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean add(E e) {
    synchronized (mutex) {return c.add(e);}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;SkipListMap&lt;/h3&gt;
&lt;h4&gt;底层结构&lt;/h4&gt;
&lt;p&gt;跳表 SkipList 是一个&lt;strong&gt;有序的链表&lt;/strong&gt;，默认升序，底层是链表加多级索引的结构。跳表可以对元素进行快速查询，类似于平衡树，是一种利用空间换时间的算法&lt;/p&gt;
&lt;p&gt;对于单链表，即使链表是有序的，如果查找数据也只能从头到尾遍历链表，所以采用链表上建索引的方式提高效率，跳表的查询时间复杂度是 &lt;strong&gt;O(logn)&lt;/strong&gt;，空间复杂度 O(n)&lt;/p&gt;
&lt;p&gt;ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表，内部是跳表结构实现，通过 CAS + volatile 保证线程安全&lt;/p&gt;
&lt;p&gt;平衡树和跳表的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整；而对跳表的插入和删除，&lt;strong&gt;只需要对整个结构的局部进行操作&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在高并发的情况下，保证整个平衡树的线程安全需要一个全局锁；对于跳表则只需要部分锁，拥有更好的性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BaseHeader 存储数据，headIndex 存储索引，纵向上&lt;strong&gt;所有索引都指向链表最下面的节点&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;成员变量&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标识索引头节点位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final Object BASE_HEADER = new Object();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跳表的顶层索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile HeadIndex&amp;lt;K,V&amp;gt; head;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较器，为 null 则使用自然排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final Comparator&amp;lt;? super K&amp;gt; comparator;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Node 节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class Node&amp;lt;K, V&amp;gt;{
    final K key;  				// key 是 final 的, 说明节点一旦定下来, 除了删除, 一般不会改动 key
    volatile Object value; 		// 对应的 value
    volatile Node&amp;lt;K, V&amp;gt; next; 	// 下一个节点，单向链表
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;索引节点 Index，只有向下和向右的指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static class Index&amp;lt;K, V&amp;gt;{
    final Node&amp;lt;K, V&amp;gt; node; 		// 索引指向的节点，每个都会指向数据节点
    final Index&amp;lt;K, V&amp;gt; down; 	// 下边level层的Index，分层索引
    volatile Index&amp;lt;K, V&amp;gt; right; // 右边的Index，单向

    // 在 index 本身和 succ 之间插入一个新的节点 newSucc
    final boolean link(Index&amp;lt;K, V&amp;gt; succ, Index&amp;lt;K, V&amp;gt; newSucc){
        Node&amp;lt;K, V&amp;gt; n = node;
        newSucc.right = succ;
        // 把当前节点的右指针从 succ 改为 newSucc
        return n.value != null &amp;amp;&amp;amp; casRight(succ, newSucc);
    }

    // 断开当前节点和 succ 节点，将当前的节点 index 设置其的 right 为 succ.right，就是把 succ 删除
    final boolean unlink(Index&amp;lt;K, V&amp;gt; succ){
        return node.value != null &amp;amp;&amp;amp; casRight(succ, succ.right);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;头索引节点 HeadIndex&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class HeadIndex&amp;lt;K,V&amp;gt; extends Index&amp;lt;K,V&amp;gt; {
    final int level;	// 表示索引层级，所有的 HeadIndex 都指向同一个 Base_header 节点
    HeadIndex(Node&amp;lt;K,V&amp;gt; node, Index&amp;lt;K,V&amp;gt; down, Index&amp;lt;K,V&amp;gt; right, int level) {
        super(node, down, right);
        this.level = level;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;h5&gt;其他方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentSkipListMap() {
    this.comparator = null;	// comparator 为 null，使用 key 的自然序，如字典序
    initialize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void initialize() {
    keySet = null;
    entrySet = null;
    values = null;
    descendingMap = null;
    // 初始化索引头节点，Node 的 key 为 null，value 为 BASE_HEADER 对象，下一个节点为 null
    // head 的分层索引 down 为 null，链表的后续索引 right 为 null，层级 level 为第 1 层
    head = new HeadIndex&amp;lt;K,V&amp;gt;(new Node&amp;lt;K,V&amp;gt;(null, BASE_HEADER, null), null, null, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cpr：排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//　x 是比较者，y 是被比较者，比较者大于被比较者 返回正数，小于返回负数，相等返回 0
static final int cpr(Comparator c, Object x, Object y) {
    return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;添加方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;findPredecessor()：寻找前置节点&lt;/p&gt;
&lt;p&gt;从最上层的头索引开始向右查找（链表的后续索引），如果后续索引的节点的 key 大于要查找的 key，则头索引移到下层链表，在下层链表查找，以此反复，一直查找到没有下层的分层索引为止，返回该索引的节点。如果后续索引的节点的 key 小于要查找的 key，则在该层链表中向后查找。由于查找的 key 可能永远大于索引节点的 key，所以只能找到目标的前置索引节点。如果遇到空值索引的存在，通过 CAS 来断开索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Node&amp;lt;K,V&amp;gt; findPredecessor(Object key, Comparator&amp;lt;? super K&amp;gt; cmp) {
    if (key == null)
        throw new NullPointerException(); // don&apos;t postpone errors
    for (;;) {
        // 1.初始数据 q 是 head，r 是最顶层 h 的右 Index 节点
        for (Index&amp;lt;K,V&amp;gt; q = head, r = q.right, d;;) {
            // 2.右索引节点不为空，则进行向下查找
            if (r != null) {
                Node&amp;lt;K,V&amp;gt; n = r.node;
                K k = n.key;
                // 3.n.value 为 null 说明节点 n 正在删除的过程中，此时【当前线程帮其删除索引】
                if (n.value == null) {
                    // 在 index 层直接删除 r 索引节点
                    if (!q.unlink(r))
                        // 删除失败重新从 head 节点开始查找，break 一个 for 到步骤 1，又从初始值开始
                        break;
                    
                    // 删除节点 r 成功，获取新的 r 节点,
                    r = q.right;
                    // 回到步骤 2，还是从这层索引开始向右遍历
                    continue;
                }
                // 4.若参数 key &amp;gt; r.node.key，则继续向右遍历, continue 到步骤 2 处获取右节点
                //   若参数 key &amp;lt; r.node.key，说明需要进入下层索引，到步骤 5
                if (cpr(cmp, key, k) &amp;gt; 0) {
                    q = r;
                    r = r.right;
                    continue;
                }
            }
            // 5.先让 d 指向 q 的下一层，判断是否是 null，是则说明已经到了数据层，也就是第一层
            if ((d = q.down) == null) 
                return q.node;
            // 6.未到数据层, 进行重新赋值向下扫描
            q = d;		// q 指向 d
            r = d.right;// r 指向 q 的后续索引节点，此时(q.key &amp;lt; key &amp;lt; r.key)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-Put%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;put()：添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V put(K key, V value) {
    // 非空判断，value不能为空
    if (value == null)
        throw new NullPointerException();
    return doPut(key, value, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private V doPut(K key, V value, boolean onlyIfAbsent) {
    Node&amp;lt;K,V&amp;gt; z;
    // 非空判断，key 不能为空
    if (key == null)
        throw new NullPointerException();
    Comparator&amp;lt;? super K&amp;gt; cmp = comparator;
    // outer 循环，【把待插入数据插入到数据层的合适的位置，并在扫描过程中处理已删除(value = null)的数据】
    outer: for (;;) {
        //0.for (;;)
        //1.将 key 对应的前继节点找到, b 为前继节点，是数据层的, n 是前继节点的 next, 
		//  若没发生条件竞争，最终 key 在 b 与 n 之间 (找到的 b 在 base_level 上)
        for (Node&amp;lt;K,V&amp;gt; b = findPredecessor(key, cmp), n = b.next;;) {
            // 2.n 不为 null 说明 b 不是链表的最后一个节点
            if (n != null) {
                Object v; int c;
                // 3.获取 n 的右节点
                Node&amp;lt;K,V&amp;gt; f = n.next;
                // 4.条件竞争，并发下其他线程在 b 之后插入节点或直接删除节点 n, break 到步骤 0
                if (n != b.next)              
                    break;
                //  若节点 n 已经删除, 则调用 helpDelete 进行【帮助删除节点】
                if ((v = n.value) == null) {
                    n.helpDelete(b, f);
                    break;
                }
                // 5.节点 b 被删除中，则 break 到步骤 0,
				//  【调用findPredecessor帮助删除index层的数据, node层的数据会通过helpDelete方法进行删除】
                if (b.value == null || v == n) 
                    break;
                // 6.若 key &amp;gt; n.key，则进行向后扫描
                //   若 key &amp;lt; n.key，则证明 key 应该存储在 b 和 n 之间
                if ((c = cpr(cmp, key, n.key)) &amp;gt; 0) {
                    b = n;
                    n = f;
                    continue;
                }
                // 7.key 的值和 n.key 相等，则可以直接覆盖赋值
                if (c == 0) {
                    // onlyIfAbsent 默认 false，
                    if (onlyIfAbsent || n.casValue(v, value)) {
                        @SuppressWarnings(&quot;unchecked&quot;) V vv = (V)v;
                        // 返回被覆盖的值
                        return vv;
                    }
                    // cas失败，break 一层循环，返回 0 重试
                    break;
                }
                // else c &amp;lt; 0; fall through
            }
            // 8.此时的情况 b.key &amp;lt; key &amp;lt; n.key，对应流程图1中的7，创建z节点指向n
            z = new Node&amp;lt;K,V&amp;gt;(key, value, n);
            // 9.尝试把 b.next 从 n 设置成 z
            if (!b.casNext(n, z))
                // cas失败，返回到步骤0，重试
                break;
            // 10.break outer 后, 上面的 for 循环不会再执行, 而后执行下面的代码
            break outer;
        }
    }
	// 【以上插入节点已经完成，剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引】
    
    // 随机数
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    
    // 如果随机数的二进制与 10000000000000000000000000000001 进行与运算为 0
    // 即随机数的二进制最高位与最末尾必须为 0，其他位无所谓，就进入该循环
    // 如果随机数的二进制最高位与最末位不为 0，不增加新节点的层数
    
    // 11.判断是否需要添加 level，32 位
    if ((rnd &amp;amp; 0x80000001) == 0) {
        // 索引层 level，从 1 开始，就是最底层
        int level = 1, max;
        // 12.判断最低位前面有几个 1，有几个leve就加几：0..0 0001 1110，这是4个，则1+4=5
        //    【最大有30个就是 1 + 30 = 31
        while (((rnd &amp;gt;&amp;gt;&amp;gt;= 1) &amp;amp; 1) != 0)
            ++level;
        // 最终会指向 z 节点，就是添加的节点 
        Index&amp;lt;K,V&amp;gt; idx = null;
        // 指向头索引节点
        HeadIndex&amp;lt;K,V&amp;gt; h = head;
        
        // 13.判断level是否比当前最高索引小，图中 max 为 3
        if (level &amp;lt;= (max = h.level)) {
            for (int i = 1; i &amp;lt;= level; ++i)
                // 根据层数level不断创建新增节点的上层索引，索引的后继索引留空
                // 第一次idx为null，也就是下层索引为空，第二次把上次的索引作为下层索引，【类似头插法】
                idx = new Index&amp;lt;K,V&amp;gt;(z, idx, null);
            // 循环以后的索引结构
            // index-3	← idx
            //   ↓
            // index-2
            //   ↓
            // index-1
            //   ↓
            //  z-node
        }
        // 14.若 level &amp;gt; max，则【只增加一层 index 索引层】，3 + 1 = 4
        else { 
            level = max + 1;
            //创建一个 index 数组，长度是 level+1，假设 level 是 4，创建的数组长度为 5
            Index&amp;lt;K,V&amp;gt;[] idxs = (Index&amp;lt;K,V&amp;gt;[])new Index&amp;lt;?,?&amp;gt;[level+1];
            // index[0]的数组 slot 并没有使用，只使用 [1,level] 这些数组的 slot
            for (int i = 1; i &amp;lt;= level; ++i)
                idxs[i] = idx = new Index&amp;lt;K,V&amp;gt;(z, idx, null);
              		// index-4   ← idx
                    //   ↓
                  	// ......
                    //   ↓
                    // index-1
                    //   ↓
                    //  z-node
            
            for (;;) {
                h = head;
                // 获取头索引的层数，3
                int oldLevel = h.level;
                // 如果 level &amp;lt;= oldLevel，说明其他线程进行了 index 层增加操作，退出循环
                if (level &amp;lt;= oldLevel)
                    break;
                // 定义一个新的头索引节点
                HeadIndex&amp;lt;K,V&amp;gt; newh = h;
                // 获取头索引的节点，就是 BASE_HEADER
                Node&amp;lt;K,V&amp;gt; oldbase = h.node;
                // 升级 baseHeader 索引，升高一级，并发下可能升高多级
                for (int j = oldLevel + 1; j &amp;lt;= level; ++j)
                    // 参数1：底层node，参数二：down，为以前的头节点，参数三：right，新建
                    newh = new HeadIndex&amp;lt;K,V&amp;gt;(oldbase, newh, idxs[j], j);
                // 执行完for循环之后，baseHeader 索引长这个样子，这里只升高一级
                // index-4             →             index-4	← idx
                //   ↓                                  ↓
                // index-3                           index-3     
                //   ↓                                  ↓
                // index-2                           index-2
                //   ↓                                  ↓
                // index-1                           index-1
                //   ↓                                  ↓
                // baseHeader    →    ....      →     z-node
                
                // cas 成功后，head 字段指向最新的 headIndex，baseHeader 的 index-4
                if (casHead(h, newh)) {
                    // h 指向最新的 index-4 节点
                    h = newh;
                    // 让 idx 指向 z-node 的 index-3 节点，
					// 因为从 index-3 - index-1 的这些 z-node 索引节点 都没有插入到索引链表
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }
        // 15.【把新加的索引插入索引链表中】，有上述两种情况，一种索引高度不变，另一种是高度加 1
        // 要插入的是第几层的索引
        splice: for (int insertionLevel = level;;) {
            // 获取头索引的层数，情况 1 是 3，情况 2 是 4
            int j = h.level;
            // 【遍历 insertionLevel 层的索引，找到合适的插入位置】
            for (Index&amp;lt;K,V&amp;gt; q = h, r = q.right, t = idx;;) {
                // 如果头索引为 null 或者新增节点索引为 null，退出插入索引的总循环
                if (q == null || t == null)
                    // 此处表示有其他线程删除了头索引或者新增节点的索引
                    break splice;
                // 头索引的链表后续索引存在，如果是新层则为新节点索引，如果是老层则为原索引
                if (r != null) {
                    // 获取r的节点
                    Node&amp;lt;K,V&amp;gt; n = r.node;
                    // 插入的key和n.key的比较值
                    int c = cpr(cmp, key, n.key);
                    // 【删除空值索引】
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right;
                        continue;
                    }
                    // key &amp;gt; r.node.key，向右扫描
                    if (c &amp;gt; 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }
                // 执行到这里，说明 key &amp;lt; r.node.key，判断是否是第 j 层插入新增节点的前置索引
                if (j == insertionLevel) {
                    // 【将新索引节点 t 插入 q r 之间】
                    if (!q.link(r, t))
                        break; 
                    // 如果新增节点的值为 null，表示该节点已经被其他线程删除
                    if (t.node.value == null) {
                        // 找到该节点
                        findNode(key);
                        break splice;
                    }
                    // 插入层逐层自减，当为最底层时退出循环
                    if (--insertionLevel == 0)
                        break splice;
                }
				// 其他节点随着插入节点的层数下移而下移
                if (--j &amp;gt;= insertionLevel &amp;amp;&amp;amp; j &amp;lt; level)
                    t = t.down;
                q = q.down;
                r = q.right;
            }
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;findNode()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Node&amp;lt;K,V&amp;gt; findNode(Object key) {
    // 原理与doGet相同，无非是 findNode 返回节点，doGet 返回 value
    if ((c = cpr(cmp, key, n.key)) == 0)
        return n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;获取方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get(key)：获取对应的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V get(Object key) {
    return doGet(key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;doGet()：扫描过程会对已 value = null 的元素进行删除处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private V doGet(Object key) {
    if (key == null)
        throw new NullPointerException();
    Comparator&amp;lt;? super K&amp;gt; cmp = comparator;
    outer: for (;;) {
        // 1.找到最底层节点的前置节点
        for (Node&amp;lt;K,V&amp;gt; b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            // 2.【如果该前置节点的链表后续节点为 null，说明不存在该节点】
            if (n == null)
                break outer;
            // b → n → f
            Node&amp;lt;K,V&amp;gt; f = n.next;
            // 3.如果n不为前置节点的后续节点，表示已经有其他线程删除了该节点
            if (n != b.next) 
                break;
            // 4.如果后续节点的值为null，【需要帮助删除该节点】
            if ((v = n.value) == null) {
                n.helpDelete(b, f);
                break;
            }
            // 5.如果前置节点已被其他线程删除，重新循环
            if (b.value == null || v == n)
                break;
             // 6.如果要获取的key与后续节点的key相等，返回节点的value
            if ((c = cpr(cmp, key, n.key)) == 0) {
                @SuppressWarnings(&quot;unchecked&quot;) V vv = (V)v;
                return vv;
            }
            // 7.key &amp;lt; n.key，因位 key &amp;gt; b.key，b 和 n 相连，说明不存在该节点或者被其他线程删除了
            if (c &amp;lt; 0)
                break outer;
            b = n;
            n = f;
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;删除方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;remove()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V remove(Object key) {
    return doRemove(key, null);
}
final V doRemove(Object key, Object value) {
    if (key == null)
        throw new NullPointerException();
    Comparator&amp;lt;? super K&amp;gt; cmp = comparator;
    outer: for (;;) {
        // 1.找到最底层目标节点的前置节点，b.key &amp;lt; key
        for (Node&amp;lt;K,V&amp;gt; b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            // 2.如果该前置节点的链表后续节点为 null，退出循环，说明不存在这个元素
            if (n == null)
                break outer;
            // b → n → f
            Node&amp;lt;K,V&amp;gt; f = n.next;
            if (n != b.next)                    // inconsistent read
                break;
            if ((v = n.value) == null) {        // n is deleted
                n.helpDelete(b, f);
                break;
            }
            if (b.value == null || v == n)      // b is deleted
                break;
            //3.key &amp;lt; n.key，说明被其他线程删除了，或者不存在该节点
            if ((c = cpr(cmp, key, n.key)) &amp;lt; 0)
                break outer;
            //4.key &amp;gt; n.key，继续向后扫描
            if (c &amp;gt; 0) {
                b = n;
                n = f;
                continue;
            }
            //5.到这里是 key = n.key，value 不为空的情况下判断 value 和 n.value 是否相等
            if (value != null &amp;amp;&amp;amp; !value.equals(v))
                break outer;
            //6.【把 n 节点的 value 置空】
            if (!n.casValue(v, null))
                break;
            //7.【给 n 添加一个删除标志 mark】，mark.next = f，然后把 b.next 设置为 f，成功后n出队
            if (!n.appendMarker(f) || !b.casNext(n, f))
                // 对 key 对应的 index 进行删除，调用了 findPredecessor 方法
                findNode(key);
            else {
                // 进行操作失败后通过 findPredecessor 中进行 index 的删除
                findPredecessor(key, cmp);
                if (head.right == null)
                    // 进行headIndex 对应的index 层的删除
                    tryReduceLevel();
            }
            @SuppressWarnings(&quot;unchecked&quot;) V vv = (V)v;
            return vv;
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过 findPredecessor() 中的 unlink() 后索引已经被删除&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-remove%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;appendMarker()：添加删除标记节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean appendMarker(Node&amp;lt;K,V&amp;gt; f) {
    // 通过 CAS 让 n.next 指向一个 key 为 null，value 为 this，next 为 f 的标记节点
    return casNext(f, new Node&amp;lt;K,V&amp;gt;(f));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;helpDelete()：将添加了删除标记的节点清除，参数是该节点的前驱和后继节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void helpDelete(Node&amp;lt;K,V&amp;gt; b, Node&amp;lt;K,V&amp;gt; f) {
    // this 节点的后续节点为 f，且本身为 b 的后续节点，一般都是正确的，除非被别的线程删除
    if (f == next &amp;amp;&amp;amp; this == b.next) {
        // 如果 n 还还没有被标记
        if (f == null || f.value != f) 
            casNext(f, new Node&amp;lt;K,V&amp;gt;(f));
        else
            // 通过 CAS，将 b 的下一个节点 n 变成 f.next，即成为图中的样式
            b.casNext(this, f.next);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tryReduceLevel()：删除索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void tryReduceLevel() {
    HeadIndex&amp;lt;K,V&amp;gt; h = head;
    HeadIndex&amp;lt;K,V&amp;gt; d;
    HeadIndex&amp;lt;K,V&amp;gt; e;
    if (h.level &amp;gt; 3 &amp;amp;&amp;amp;
        (d = (HeadIndex&amp;lt;K,V&amp;gt;)h.down) != null &amp;amp;&amp;amp;
        (e = (HeadIndex&amp;lt;K,V&amp;gt;)d.down) != null &amp;amp;&amp;amp;
        e.right == null &amp;amp;&amp;amp;
        d.right == null &amp;amp;&amp;amp;
        h.right == null &amp;amp;&amp;amp;
        // 设置头索引
        casHead(h, d) &amp;amp;&amp;amp; 
        // 重新检查
        h.right != null) 
        // 重新检查返回true，说明其他线程增加了索引层级，把索引头节点设置回来
        casHead(d, h);   
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://my.oschina.net/u/3768341/blog/3135659&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1Er4y1P7k1&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;NoBlocking&lt;/h3&gt;
&lt;h4&gt;非阻塞队列&lt;/h4&gt;
&lt;p&gt;并发编程中，需要用到安全的队列，实现安全队列可以使用 2 种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加锁，这种实现方式是阻塞队列&lt;/li&gt;
&lt;li&gt;使用循环 CAS 算法实现，这种方式是非阻塞队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列，采用先进先出的规则对节点进行排序，当添加一个元素时，会添加到队列的尾部，当获取一个元素时，会返回队列头部的元素&lt;/p&gt;
&lt;p&gt;补充：ConcurrentLinkedDeque 是双向链表结构的无界并发队列&lt;/p&gt;
&lt;p&gt;ConcurrentLinkedQueue 使用约定：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不允许 null 入列&lt;/li&gt;
&lt;li&gt;队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到&lt;/li&gt;
&lt;li&gt;删除节点是将 item 设置为 null，队列迭代时跳过 item 为 null 节点&lt;/li&gt;
&lt;li&gt;head 节点跟 tail 不一定指向头节点或尾节点，可能&lt;strong&gt;存在滞后性&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ConcurrentLinkedQueue 由 head 节点和 tail 节点组成，每个节点由节点元素和指向下一个节点的引用组成，组成一张链表结构的队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile Node&amp;lt;E&amp;gt; head;
private transient volatile Node&amp;lt;E&amp;gt; tail;

private static class Node&amp;lt;E&amp;gt; {
    volatile E item;
    volatile Node&amp;lt;E&amp;gt; next;
    //.....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;构造方法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无参构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentLinkedQueue() {
    // 默认情况下 head 节点存储的元素为空，dummy 节点，tail 节点等于 head 节点
    head = tail = new Node&amp;lt;E&amp;gt;(null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有参构造方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConcurrentLinkedQueue(Collection&amp;lt;? extends E&amp;gt; c) {
    Node&amp;lt;E&amp;gt; h = null, t = null;
    // 遍历节点
    for (E e : c) {
        checkNotNull(e);
        Node&amp;lt;E&amp;gt; newNode = new Node&amp;lt;E&amp;gt;(e);
        if (h == null)
            h = t = newNode;
        else {
            // 单向链表
            t.lazySetNext(newNode);
            t = newNode;
        }
    }
    if (h == null)
        h = t = new Node&amp;lt;E&amp;gt;(null);
    head = h;
    tail = t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;入队方法&lt;/h4&gt;
&lt;p&gt;与传统的链表不同，单线程入队的工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将入队节点设置成当前队列尾节点的下一个节点&lt;/li&gt;
&lt;li&gt;更新 tail 节点，如果 tail 节点的 next 节点不为空，则将入队节点设置成 tail 节点；如果 tail 节点的 next 节点为空，则将入队节点设置成 tail 的 next 节点，所以 tail 节点不总是尾节点，&lt;strong&gt;存在滞后性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public boolean offer(E e) {
    checkNotNull(e);
    // 创建入队节点
    final Node&amp;lt;E&amp;gt; newNode = new Node&amp;lt;E&amp;gt;(e);
	
    // 循环 CAS 直到入队成功
    for (Node&amp;lt;E&amp;gt; t = tail, p = t;;) {
        // p 用来表示队列的尾节点，初始情况下等于 tail 节点，q 是 p 的 next 节点
        Node&amp;lt;E&amp;gt; q = p.next;
        // 条件成立说明 p 是尾节点
        if (q == null) {
            // p 是尾节点，设置 p 节点的下一个节点为新节点
            // 设置成功则 casNext 返回 true，否则返回 false，说明有其他线程更新过尾节点，继续寻找尾节点，继续 CAS
            if (p.casNext(null, newNode)) {
                // 首次添加时，p 等于 t，不进行尾节点更新，所以尾节点存在滞后性
                if (p != t)
                    // 将 tail 设置成新入队的节点，设置失败表示其他线程更新了 tail 节点
                    casTail(t, newNode); 
                return true;
            }
        }
        else if (p == q)
            // 当 tail 不指向最后节点时，如果执行出列操作，可能将 tail 也移除，tail 不在链表中 
        	// 此时需要对 tail 节点进行复位，复位到 head 节点
            p = (t != (t = tail)) ? t : head;
        else
            // 推动 tail 尾节点往队尾移动
            p = (p != t &amp;amp;&amp;amp; t != (t = tail)) ? t : q;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图解入队：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%85%A5%E9%98%9F%E6%93%8D%E4%BD%9C1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%85%A5%E9%98%9F%E6%93%8D%E4%BD%9C2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%85%A5%E9%98%9F%E6%93%8D%E4%BD%9C3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当 tail 节点和尾节点的距离&lt;strong&gt;大于等于 1&lt;/strong&gt; 时（每入队两次）更新 tail，可以减少 CAS 更新 tail 节点的次数，提高入队效率&lt;/p&gt;
&lt;p&gt;线程安全问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程 1 线程 2 同时入队，无论从哪个位置开始并发入队，都可以循环 CAS，直到入队成功，线程安全&lt;/li&gt;
&lt;li&gt;线程 1 遍历，线程 2 入队，所以造成 ConcurrentLinkedQueue 的 size 是变化，需要加锁保证安全&lt;/li&gt;
&lt;li&gt;线程 1 线程 2 同时出列，线程也是安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;出队方法&lt;/h4&gt;
&lt;p&gt;出队列的就是从队列里返回一个节点元素，并清空该节点对元素的引用，并不是每次出队都更新 head 节点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 head 节点里有元素时，直接弹出 head 节点里的元素，而不会更新 head 节点&lt;/li&gt;
&lt;li&gt;当 head 节点里没有元素时，出队操作才会更新 head 节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;批处理方式&lt;/strong&gt;可以减少使用 CAS 更新 head 节点的消耗，从而提高出队效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public E poll() {
    restartFromHead:
    for (;;) {
        // p 节点表示首节点，即需要出队的节点，FIFO
        for (Node&amp;lt;E&amp;gt; h = head, p = h, q;;) {
            E item = p.item;
			// 如果 p 节点的元素不为 null，则通过 CAS 来设置 p 节点引用元素为 null，成功返回 item
            if (item != null &amp;amp;&amp;amp; p.casItem(item, null)) {
                if (p != h)	
                   	// 对 head 进行移动
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
           	// 逻辑到这说明头节点的元素为空或头节点发生了变化，头节点被另外一个线程修改了
            // 那么获取 p 节点的下一个节点，如果 p 节点的下一节点也为 null，则表明队列已经空了
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
      		// 第一轮操作失败，下一轮继续，调回到循环前
            else if (p == q)
                continue restartFromHead;
            // 如果下一个元素不为空，则将头节点的下一个节点设置成头节点
            else
                p = q;
        }
    }
}
final void updateHead(Node&amp;lt;E&amp;gt; h, Node&amp;lt;E&amp;gt; p) {
    if (h != p &amp;amp;&amp;amp; casHead(h, p))
        // 将旧结点 h 的 next 域指向为 h，help gc
        h.lazySetNext(h);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在更新完 head 之后，会将旧的头结点 h 的 next 域指向为 h，图中所示的虚线也就表示这个节点的自引用，被移动的节点（item 为 null 的节点）会被 GC 回收&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%87%BA%E9%98%9F%E6%93%8D%E4%BD%9C1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%87%BA%E9%98%9F%E6%93%8D%E4%BD%9C2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue%E5%87%BA%E9%98%9F%E6%93%8D%E4%BD%9C3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果这时，有一个线程来添加元素，通过 tail 获取的 next 节点则仍然是它本身，这就出现了p == q 的情况，出现该种情况之后，则会触发执行 head 的更新，将 p 节点重新指向为 head&lt;/p&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/231caf90f30b&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;peek()：会改变 head 指向，执行 peek() 方法后 head 会指向第一个具有非空元素的节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取链表的首部元素，只读取而不移除
public E peek() {
    restartFromHead:
    for (;;) {
        for (Node&amp;lt;E&amp;gt; h = head, p = h, q;;) {
            E item = p.item;
            if (item != null || (q = p.next) == null) {
                // 更改h的位置为非空元素节点
                updateHead(h, p);
                return item;
            }
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;size()：用来获取当前队列的元素个数，因为整个过程都没有加锁，在并发环境中从调用 size 方法到返回结果期间有可能增删元素，导致统计的元素个数不精确&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int size() {
    int count = 0;
    // first() 获取第一个具有非空元素的节点，若不存在，返回 null
    // succ(p) 方法获取 p 的后继节点，若 p == p.next，则返回 head
    // 类似遍历链表
    for (Node&amp;lt;E&amp;gt; p = first(); p != null; p = succ(p))
        if (p.item != null)
            // 最大返回Integer.MAX_VALUE
            if (++count == Integer.MAX_VALUE)
                break;
    return count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：移除元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean remove(Object o) {
    // 删除的元素不能为null
    if (o != null) {
        Node&amp;lt;E&amp;gt; next, pred = null;
        for (Node&amp;lt;E&amp;gt; p = first(); p != null; pred = p, p = next) {
            boolean removed = false;
            E item = p.item;
            // 节点元素不为null
            if (item != null) {
                // 若不匹配，则获取next节点继续匹配
                if (!o.equals(item)) {
                    next = succ(p);
                    continue;
                }
                // 若匹配，则通过 CAS 操作将对应节点元素置为 null
                removed = p.casItem(item, null);
            }
            // 获取删除节点的后继节点
            next = succ(p);
            // 将被删除的节点移除队列
            if (pred != null &amp;amp;&amp;amp; next != null) // unlink
                pred.casNext(p, next);
            if (removed)
                return true;
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;NET&lt;/h1&gt;
&lt;h2&gt;DES&lt;/h2&gt;
&lt;h3&gt;网络编程&lt;/h3&gt;
&lt;p&gt;网络编程，就是在一定的协议下，实现两台计算机的通信的技术&lt;/p&gt;
&lt;p&gt;通信一定是基于软件结构实现的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C/S 结构 ：全称为 Client/Server 结构，是指客户端和服务器结构，常见程序有 QQ、IDEA 等软件&lt;/li&gt;
&lt;li&gt;B/S 结构 ：全称为 Browser/Server 结构，是指浏览器和服务器结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种架构各有优势，但是无论哪种架构，都离不开网络的支持&lt;/p&gt;
&lt;p&gt;网络通信的三要素：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;协议：计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则，HTTP、FTP、TCP、UDP、SMTP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IP 地址：互联网协议地址（Internet Protocol Address），用来给一个网络中的计算机设备做唯一的编号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;IPv4：4 个字节，32 位组成，192.168.1.1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPv6：可以实现为所有设备分配 IP，128 位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ipconfig：查看本机的 IP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ping 检查本机与某个 IP 指定的机器是否联通，或者说是检测对方是否在线。&lt;/li&gt;
&lt;li&gt;ping 空格 IP地址 ：ping 220.181.57.216，ping www.baidu.com&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特殊的IP地址： 本机IP地址，&lt;strong&gt;127.0.0.1 == localhost&lt;/strong&gt;，回环测试&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;端口：端口号就可以唯一标识设备中的进程（应用程序）。端口号是用两个字节表示的整数，取值范围是 0-65535，0-1023 之间的端口号用于一些知名的网络服务和应用普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用，会导致当前程序启动失败，报出端口被占用异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;利用&lt;strong&gt;协议+IP 地址+端口号&lt;/strong&gt;三元组合，就可以标识网络中的进程了，那么进程间的通信就可以利用这个标识与其它进程进行交互&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1kT4y1M7vt&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;通信协议&lt;/h3&gt;
&lt;p&gt;网络通信协议：对计算机必须遵守的规则，只有遵守这些规则，计算机之间才能进行通信&lt;/p&gt;
&lt;p&gt;通信&lt;strong&gt;是进程与进程之间的通信&lt;/strong&gt;，不是主机与主机之间的通信&lt;/p&gt;
&lt;p&gt;TCP/IP协议：传输控制协议 (Transmission Control Protocol)&lt;/p&gt;
&lt;p&gt;传输控制协议 TCP（Transmission Control Protocol）是面向连接的，提供可靠交付，有流量控制，拥塞控制，提供全双工通信，面向字节流，每一条 TCP 连接只能是点对点的（一对一）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在通信之前必须确定对方在线并且连接成功才可以通信&lt;/li&gt;
&lt;li&gt;例如下载文件、浏览网页等（要求可靠传输）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户数据报协议 UDP（User Datagram Protocol）是无连接的，尽最大可能交付，不可靠，没有拥塞控制，面向报文，支持一对一、一对多、多对一和多对多的交互通信&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接发消息给对方，不管对方是否在线，发消息后也不需要确认&lt;/li&gt;
&lt;li&gt;无线（视频会议，通话），性能好，可能丢失一些数据&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Java模型&lt;/h3&gt;
&lt;p&gt;相关概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步：当前线程要自己进行数据的读写操作（自己去银行取钱）&lt;/li&gt;
&lt;li&gt;异步：当前线程可以去做其他事情（委托别人拿银行卡到银行取钱，然后给你）&lt;/li&gt;
&lt;li&gt;阻塞：在数据没有的情况下，还是要继续等待着读（排队等待）&lt;/li&gt;
&lt;li&gt;非阻塞：在数据没有的情况下，会去做其他事情，一旦有了数据再来获取（柜台取款，取个号，然后坐在椅子上做其它事，等号广播会通知你办理）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 中的通信模型:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;BIO 表示同步阻塞式通信，服务器实现模式为一个连接一个线程，即客户端有连接请求时服务器端就需要启动一个线程进行处理，如果这个连接不做任何事情会造成不必要的线程开销，可以通过线程池机制改善&lt;/p&gt;
&lt;p&gt;同步阻塞式性能极差：大量线程，大量阻塞&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪异步通信：引入线程池，不需要一个客户端一个线程，实现线程复用来处理很多个客户端，线程可控&lt;/p&gt;
&lt;p&gt;高并发下性能还是很差：线程数量少，数据依然是阻塞的，数据没有来线程还是要等待&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NIO 表示&lt;strong&gt;同步非阻塞 IO&lt;/strong&gt;，服务器实现模式为请求对应一个线程，客户端发送的连接会注册到多路复用器上，多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理&lt;/p&gt;
&lt;p&gt;工作原理：1 个主线程专门负责接收客户端，1 个线程轮询所有的客户端，发来了数据才会开启线程处理&lt;/p&gt;
&lt;p&gt;同步：线程还要不断的接收客户端连接，以及处理数据&lt;/p&gt;
&lt;p&gt;非阻塞：如果一个管道没有数据，不需要等待，可以轮询下一个管道是否有数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AIO 表示异步非阻塞 IO，AIO 引入异步通道的概念，采用了 Proactor 模式，有效的请求才启动线程，特点是先由操作系统完成后才通知服务端程序启动线程去处理，一般适用于连接数较多且连接时间较长的应用&lt;/p&gt;
&lt;p&gt;异步：服务端线程接收到了客户端管道以后就交给底层处理 IO 通信，线程可以做其他事情&lt;/p&gt;
&lt;p&gt;非阻塞：底层也是客户端有数据才会处理，有了数据以后处理好通知服务器应用来启动线程进行处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;各种模型应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BIO 适用于连接数目比较小且固定的架构，该方式对服务器资源要求比较高，并发局限于应用中，程序简单&lt;/li&gt;
&lt;li&gt;NIO 适用于连接数目多且连接比较短（轻操作）的架构，如聊天服务器，并发局限于应用中，编程复杂，JDK 1.4 开始支持&lt;/li&gt;
&lt;li&gt;AIO 适用于连接数目多且连接比较长（重操作）的架构，如相册服务器，充分调用操作系统参与并发操作，JDK 1.7 开始支持&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;I/O&lt;/h2&gt;
&lt;h3&gt;IO模型&lt;/h3&gt;
&lt;h4&gt;五种模型&lt;/h4&gt;
&lt;p&gt;对于一个套接字上的输入操作，第一步是等待数据从网络中到达，当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区&lt;/p&gt;
&lt;p&gt;Linux 有五种 I/O 模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞式 I/O&lt;/li&gt;
&lt;li&gt;非阻塞式 I/O&lt;/li&gt;
&lt;li&gt;I/O 复用（select 和 poll）&lt;/li&gt;
&lt;li&gt;信号驱动式 I/O（SIGIO）&lt;/li&gt;
&lt;li&gt;异步 I/O（AIO）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;五种模型对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ，它们的主要区别在第一个阶段，非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;同步 I/O：将数据从内核缓冲区复制到应用进程缓冲区的阶段（第二阶段），应用进程会阻塞&lt;/li&gt;
&lt;li&gt;异步 I/O：第二阶段应用进程不会阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;阻塞式IO&lt;/h4&gt;
&lt;p&gt;应用进程通过系统调用 recvfrom 接收数据，会被阻塞，直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞，其它应用进程还可以执行，只是当前阻塞进程不消耗 CPU 时间，这种模型的 CPU 利用率会比较高&lt;/p&gt;
&lt;p&gt;recvfrom() 用于&lt;strong&gt;接收 Socket 传来的数据，并复制到应用进程的缓冲区 buf 中&lt;/strong&gt;，把 recvfrom() 当成系统调用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO%E6%A8%A1%E5%9E%8B-%E9%98%BB%E5%A1%9E%E5%BC%8FIO.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;非阻塞式&lt;/h4&gt;
&lt;p&gt;应用进程通过 recvfrom 调用不停的去和内核交互，直到内核准备好数据。如果没有准备好数据，内核返回一个错误码，过一段时间应用进程再执行 recvfrom 系统调用，在两次发送请求的时间段，进程可以进行其他任务，这种方式称为轮询（polling）&lt;/p&gt;
&lt;p&gt;由于 CPU 要处理更多的系统调用，因此这种模型的 CPU 利用率比较低&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO%E6%A8%A1%E5%9E%8B-%E9%9D%9E%E9%98%BB%E5%A1%9E%E5%BC%8FIO.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;信号驱动&lt;/h4&gt;
&lt;p&gt;应用进程使用 sigaction 系统调用，内核立即返回，应用进程可以继续执行，等待数据阶段应用进程是非阻塞的。当内核数据准备就绪时向应用进程发送 SIGIO 信号，应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中&lt;/p&gt;
&lt;p&gt;相比于非阻塞式 I/O 的轮询方式，信号驱动 I/O 的 CPU 利用率更高&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO%E6%A8%A1%E5%9E%8B-%E4%BF%A1%E5%8F%B7%E9%A9%B1%E5%8A%A8IO.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;IO 复用&lt;/h4&gt;
&lt;p&gt;IO 复用模型使用 select 或者 poll 函数等待数据，select 会监听所有注册好的 IO，&lt;strong&gt;等待多个套接字中的任何一个变为可读&lt;/strong&gt;，等待过程会被阻塞，当某个套接字准备好数据变为可读时 select 调用就返回，然后调用 recvfrom 把数据从内核复制到进程中&lt;/p&gt;
&lt;p&gt;IO 复用让单个进程具有处理多个 I/O 事件的能力，又被称为 Event Driven I/O，即&lt;strong&gt;事件驱动 I/O&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果一个 Web 服务器没有 I/O 复用，那么每一个 Socket 连接都要创建一个线程去处理，如果同时有几万个连接，就需要创建相同数量的线程。相比于多进程和多线程技术，I/O 复用不需要进程线程创建和切换的开销，系统开销更小&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO%E6%A8%A1%E5%9E%8B-IO%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;异步 IO&lt;/h4&gt;
&lt;p&gt;应用进程执行 aio_read 系统调用会立即返回，给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞，内核会在所有操作完成之后向应用进程发送信号&lt;/p&gt;
&lt;p&gt;异步 I/O 与信号驱动 I/O 的区别在于，异步 I/O 的信号是通知应用进程 I/O 完成，而信号驱动 I/O 的信号是通知应用进程可以开始 I/O&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO%E6%A8%A1%E5%9E%8B-%E5%BC%82%E6%AD%A5IO%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;多路复用&lt;/h3&gt;
&lt;h4&gt;select&lt;/h4&gt;
&lt;h5&gt;函数&lt;/h5&gt;
&lt;p&gt;Socket 不是文件，只是一个标识符，但是 Unix 操作系统把所有东西都&lt;strong&gt;看作&lt;/strong&gt;是文件，所以 Socket 说成 file descriptor，也就是 fd&lt;/p&gt;
&lt;p&gt;select 允许应用程序监视一组文件描述符，等待一个或者多个描述符成为就绪状态，从而完成 I/O 操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;fd_set 使用 &lt;strong&gt;bitmap 数组&lt;/strong&gt;实现，数组大小用 FD_SETSIZE 定义，&lt;strong&gt;单进程&lt;/strong&gt;只能监听少于 FD_SETSIZE 数量的描述符，32 位机默认是 1024 个，64 位机默认是 2048，可以对进行修改，然后重新编译内核&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fd_set 有三种类型的描述符：readset、writeset、exceptset，对应读、写、异常条件的描述符集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;n 是监测的 socket 的最大数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;timeout 为超时参数，调用 select 会一直&lt;strong&gt;阻塞&lt;/strong&gt;直到有描述符的事件到达或者等待的时间超过 timeout&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct timeval{
    long tv_sec; 	//秒
    long tv_usec;	//微秒
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;timeout == null：等待无限长的时间&lt;/li&gt;
&lt;li&gt;tv_sec == 0 &amp;amp;&amp;amp; tv_usec == 0：获取后直接返回，不阻塞等待&lt;/li&gt;
&lt;li&gt;tv_sec != 0 || tv_usec != 0：等待指定时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法成功调用返回结果为&lt;strong&gt;就绪的文件描述符个数&lt;/strong&gt;，出错返回结果为 -1，超时返回结果为 0&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux 提供了一组宏为 fd_set 进行赋值操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int FD_ZERO(fd_set *fdset);			// 将一个 fd_set 类型变量的所有值都置为 0
int FD_CLR(int fd, fd_set *fdset);	// 将一个 fd_set 类型变量的 fd 位置为 0
int FD_SET(int fd, fd_set *fdset);	// 将一个 fd_set 类型变量的 fd 位置为 1
int FD_ISSET(int fd, fd_set *fdset);// 判断 fd 位是否被置为 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&amp;amp;addr, 0, sizeof(addr)));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr*)&amp;amp;addr, sizeof(addr));//绑定连接
listen(sockfd, 5);//监听5个端口
for(i = 0; i &amp;lt; 5; i++) {
	memset(&amp;amp;client, e, sizeof(client));
    addrlen = sizeof(client);
	fds[i] = accept(sockfd, (struct sockaddr*)&amp;amp;client, &amp;amp;addrlen);
    //将监听的对应的文件描述符fd存入fds：[3,4,5,6,7]
    if(fds[i] &amp;gt; max)
		max = fds[i];
}
while(1) {
    FD_ZERO(&amp;amp;rset);//置为0
    for(i = 0; i &amp;lt; 5; i++) {
    	FD_SET(fds[i], &amp;amp;rset);//对应位置1 [0001 1111 00.....]
	}
	print(&quot;round again&quot;);
	select(max + 1, &amp;amp;rset, NULL, NULL, NULL);//监听
    
	for(i = 0; i &amp;lt;5; i++) {
        if(FD_ISSET(fds[i], &amp;amp;rset)) {//判断监听哪一个端口
            memset(buffer, 0, MAXBUF);
            read(fds[i], buffer, MAXBUF);//进入内核态读数据
            print(buffer);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV19D4y1o797&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;流程&lt;/h5&gt;
&lt;p&gt;select 调用流程图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-select%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间，进程阻塞&lt;/li&gt;
&lt;li&gt;注册回调函数 _pollwait&lt;/li&gt;
&lt;li&gt;遍历所有 fd，调用其对应的 poll 方法判断当前请求是否准备就绪，对于 socket，这个 poll 方法是 sock_poll，sock_poll 根据情况会调用到 tcp_poll、udp_poll 或者 datagram_poll，以 tcp_poll 为例，其核心实现就是 _pollwait&lt;/li&gt;
&lt;li&gt;_pollwait 把 **current（调用 select 的进程）**挂到设备的等待队列，不同设备有不同的等待队列，对于 tcp_poll ，其等待队列是 sk → sk_sleep（把进程挂到等待队列中并不代表进程已经睡眠），在设备收到消息（网络设备）或填写完文件数据（磁盘设备）后，会唤醒设备等待队列上睡眠的进程，这时 current 便被唤醒，进入就绪队列&lt;/li&gt;
&lt;li&gt;poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码，根据这个 mask 掩码给 fd_set 赋值&lt;/li&gt;
&lt;li&gt;如果遍历完所有的 fd，还没有返回一个可读写的 mask 掩码，则会调用 schedule_timeout 让 current 进程进入睡眠。当设备驱动发生自身资源可读写后，会唤醒其等待队列上睡眠的进程，如果超过一定的超时时间（schedule_timeout）没有其他线程唤醒，则调用 select 的进程会重新被唤醒获得 CPU，进而重新遍历 fd，判断有没有就绪的 fd&lt;/li&gt;
&lt;li&gt;把 fd_set 从内核空间拷贝到用户空间，阻塞进程继续执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/anker/p/3265058.html&lt;/p&gt;
&lt;p&gt;其他流程图：https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;poll&lt;/h4&gt;
&lt;p&gt;poll 的功能与 select 类似，也是等待一组描述符中的一个成为就绪状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int poll(struct pollfd *fds, unsigned int nfds, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;poll 中的描述符是 pollfd 类型的数组，pollfd 的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 和 poll 对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select 会修改描述符，而 poll 不会&lt;/li&gt;
&lt;li&gt;select 的描述符类型使用数组实现，有描述符的限制；而 poll 使用&lt;strong&gt;链表&lt;/strong&gt;实现，没有描述符数量的限制&lt;/li&gt;
&lt;li&gt;poll 提供了更多的事件类型，并且对描述符的重复利用上比 select 高&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;select 和 poll 速度都比较慢，&lt;strong&gt;每次调用&lt;/strong&gt;都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区，同时每次都需要在内核遍历传递进来的所有 fd ，这个开销在 fd 很多时会很大&lt;/li&gt;
&lt;li&gt;几乎所有的系统都支持 select，但是只有比较新的系统支持 poll&lt;/li&gt;
&lt;li&gt;select 和 poll 的时间复杂度 O(n)，对 socket 进行扫描时是线性扫描，即采用轮询的方法，效率较低，因为并不知道具体是哪个 socket 具有事件，所以随着 fd 数量的增加会造成遍历速度慢的&lt;strong&gt;线性下降&lt;/strong&gt;性能问题&lt;/li&gt;
&lt;li&gt;poll 还有一个特点是水平触发，如果报告了 fd 后，没有被处理，那么下次 poll 时会再次报告该 fd&lt;/li&gt;
&lt;li&gt;如果一个线程对某个描述符调用了 select 或者 poll，另一个线程关闭了该描述符，会导致调用结果不确定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;epoll&lt;/h4&gt;
&lt;h5&gt;函数&lt;/h5&gt;
&lt;p&gt;epoll 使用事件的就绪通知方式，通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵&lt;strong&gt;红黑树&lt;/strong&gt;上，一旦该 fd 就绪，&lt;strong&gt;内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个链表中&lt;/strong&gt;管理，进程调用 epoll_wait() 便可以得到事件就绪的描述符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;epall_create：一个系统函数，函数将在内核空间内创建一个 epoll 数据结构，可以理解为 epoll 结构空间，返回值为 epoll 的文件描述符编号，以后有 client 连接时，向该 epoll 结构中添加监听，所以 epoll 使用一个文件描述符管理多个描述符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epall_ctl：epoll 的事件注册函数，select 函数是调用时指定需要监听的描述符和事件，epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数，用来增删改 epoll 空间内的描述符，参数解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;epfd：epoll 结构的进程 fd 编号，函数将依靠该编号找到对应的 epoll 结构&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;op：表示当前请求类型，有三个宏定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EPOLL_CTL_ADD：注册新的 fd 到 epfd 中&lt;/li&gt;
&lt;li&gt;EPOLL_CTL_MOD：修改已经注册的 fd 的监听事件&lt;/li&gt;
&lt;li&gt;EPOLL_CTI_DEL：从 epfd 中删除一个 fd&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fd：需要监听的文件描述符，一般指 socket_fd&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;event：告诉内核对该 fd 资源感兴趣的事件，epoll_event 的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct epoll_event {
    _uint32_t events;	/*epoll events*/
    epoll_data_t data;	/*user data variable*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;events 可以是以下几个宏集合：EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP（挂断）、EPOLET（边缘触发）、EPOLLONESHOT（只监听一次，事件触发后自动清除该 fd，从 epoll 列表）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll_wait：等待事件的产生，类似于 select() 调用，返回值为本次就绪的 fd 个数，直接从就绪链表获取，时间复杂度 O(1)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;epfd：&lt;strong&gt;指定感兴趣的 epoll 事件列表&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;events：指向一个 epoll_event 结构数组，当函数返回时，内核会把就绪状态的数据拷贝到该数组&lt;/li&gt;
&lt;li&gt;maxevents：标明 epoll_event 数组最多能接收的数据量，即本次操作最多能获取多少就绪数据&lt;/li&gt;
&lt;li&gt;timeout：单位为毫秒
&lt;ul&gt;
&lt;li&gt;0：表示立即返回，非阻塞调用&lt;/li&gt;
&lt;li&gt;-1：阻塞调用，直到有用户感兴趣的事件就绪为止&lt;/li&gt;
&lt;li&gt;大于 0：阻塞调用，阻塞指定时间内如果有事件就绪则提前返回，否则等待指定时间后返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;epoll 的描述符事件有两种触发模式：LT（level trigger）和 ET（edge trigger）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LT 模式：当 epoll_wait() 检测到描述符事件到达时，将此事件通知进程，进程可以不立即处理该事件，下次调用 epoll_wait() 会再次通知进程，是默认的一种模式，并且同时支持 Blocking 和 No-Blocking&lt;/li&gt;
&lt;li&gt;ET 模式：通知之后进程必须立即处理事件，下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数，因此效率要比 LT 模式高；只支持 No-Blocking，以避免由于一个 fd 的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 创建 epoll 描述符，每个应用程序只需要一个，用于监控所有套接字
int pollingfd = epoll_create(0xCAFE);
if ( pollingfd &amp;lt; 0 )// report error
// 初始化 epoll 结构
struct epoll_event ev = { 0 };

// 将连接类实例与事件相关联，可以关联任何想要的东西
ev.data.ptr = pConnection1;

// 监视输入，并且在事件发生后不自动重新准备描述符
ev.events = EPOLLIN | EPOLLONESHOT;
// 将描述符添加到监控列表中，即使另一个线程在epoll_wait中等待，描述符将被正确添加
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1-&amp;gt;getSocket(), &amp;amp;ev) != 0 )
    // report error

// 最多等待 20 个事件
struct epoll_event pevents[20];

// 等待10秒，检索20个并存入epoll_event数组
int ready = epoll_wait(pollingfd, pevents, 20, 10000);
// 检查epoll是否成功
if ( ret == -1)// report error and abort
else if ( ret == 0)// timeout; no event detected
else
{
    for (int i = 0; i &amp;lt; ready; i+ )
    {
        if ( pevents[i].events &amp;amp; EPOLLIN )
        {
            // 获取连接指针
            Connection * c = (Connection*) pevents[i].data.ptr;
            c-&amp;gt;handleReadEvent();
         }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程图：https://gitee.com/seazean/images/blob/master/Java/IO-epoll%E5%8E%9F%E7%90%86%E5%9B%BE.jpg&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV19D4y1o797&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;特点&lt;/h5&gt;
&lt;p&gt;epoll 的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;epoll 仅适用于 Linux 系统&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 使用&lt;strong&gt;一个文件描述符管理多个描述符&lt;/strong&gt;，将用户关心的文件描述符的事件存放到内核的一个事件表（个人理解成哑元节点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有最大描述符数量（并发连接）的限制，打开 fd 的上限远大于1024（1G 内存能监听约 10 万个端口）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 的时间复杂度 O(1)，epoll 理解为 event poll，不同于忙轮询和无差别轮询，调用 epoll_wait &lt;strong&gt;只是轮询就绪链表&lt;/strong&gt;。当监听列表有设备就绪时调用回调函数，把就绪 fd 放入就绪链表中，并唤醒在 epoll_wait 中阻塞的进程，所以 epoll 实际上是&lt;strong&gt;事件驱动&lt;/strong&gt;（每个事件关联上fd）的，降低了 system call 的时间复杂度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 内核中根据每个 fd 上的 callback 函数来实现，只有活跃的 socket 才会主动调用 callback，所以使用 epoll 没有前面两者的线性下降的性能问题，效率提高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 注册新的事件是注册到到内核中 epoll 句柄中，不需要每次调用 epoll_wait 时重复拷贝，对比前面两种只需要将描述符从进程缓冲区向内核缓冲区&lt;strong&gt;拷贝一次&lt;/strong&gt;，也可以利用 &lt;strong&gt;mmap() 文件映射内存&lt;/strong&gt;加速与内核空间的消息传递（只是可以用，并没有用）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前面两者要把 current 往设备等待队列中挂一次，epoll 也只把 current 往等待队列上挂一次，但是这里的等待队列并不是设备等待队列，只是一个 epoll 内部定义的等待队列，这样可以节省开销&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 对多线程编程更有友好，一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符，也不会产生像 select 和 poll 的不确定情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/dfd940e7fca2&lt;/p&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/anker/p/3265058.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;select 应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select 的 timeout 参数精度为微秒，poll 和 epoll 为毫秒，因此 select 适用&lt;strong&gt;实时性要求比较高&lt;/strong&gt;的场景，比如核反应堆的控制&lt;/li&gt;
&lt;li&gt;select 可移植性更好，几乎被所有主流平台所支持&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;poll 应用场景：poll 没有最大描述符数量的限制，适用于平台支持并且对实时性要求不高的情况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll 应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行在 Linux 平台上，有大量的描述符需要同时轮询，并且这些连接最好是&lt;strong&gt;长连接&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;需要同时监控小于 1000 个描述符，没必要使用 epoll，因为这个应用场景下并不能体现 epoll 的优势&lt;/li&gt;
&lt;li&gt;需要监控的描述符状态变化多，而且是非常短暂的，就没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中，每次对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用，频繁系统调用降低效率，并且 epoll 的描述符存储在内核，不容易调试&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;系统调用&lt;/h3&gt;
&lt;h4&gt;内核态&lt;/h4&gt;
&lt;p&gt;用户空间：用户代码、用户堆栈&lt;/p&gt;
&lt;p&gt;内核空间：内核代码、内核调度程序、进程描述符（内核堆栈、thread_info 进程描述符）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程描述符和用户的进程是一一对应的&lt;/li&gt;
&lt;li&gt;SYS_API 系统调用：如 read、write，系统调用就是 0X80 中断&lt;/li&gt;
&lt;li&gt;进程描述符 pd：进程从用户态切换到内核态时，需要&lt;strong&gt;保存用户态时的上下文信息在 PCB 中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程上下文：用户程序基地址，程序计数器、cpu cache、寄存器等，方便程序切回用户态时恢复现场&lt;/li&gt;
&lt;li&gt;内核堆栈：**系统调用函数也是要创建变量的，**这些变量在内核堆栈上分配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-%E7%94%A8%E6%88%B7%E6%80%81%E5%92%8C%E5%86%85%E6%A0%B8%E6%80%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;80中断&lt;/h4&gt;
&lt;p&gt;在用户程序中调用操作系统提供的核心态级别的子功能，为了系统安全需要进行用户态和内核态转换，状态的转换需要进行 CPU 中断，中断分为硬中断和软中断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬中断：如网络传输中，数据到达网卡后，网卡经过一系列操作后发起硬件中断&lt;/li&gt;
&lt;li&gt;软中断：如程序运行过程中本身产生的一些中断
&lt;ul&gt;
&lt;li&gt;发起 &lt;code&gt;0X80&lt;/code&gt; 中断&lt;/li&gt;
&lt;li&gt;程序执行碰到除 0 异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统调用 system_call 函数所对应的中断指令编号是 0X80（十进制是 8×16=128），而该指令编号对应的就是系统调用程序的入口，所以称系统调用为 80 中断&lt;/p&gt;
&lt;p&gt;系统调用的流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 CPU 寄存器里存一个系统调用号，表示哪个系统函数，比如 read&lt;/li&gt;
&lt;li&gt;将 CPU 的临时数据都保存到 thread_info 中&lt;/li&gt;
&lt;li&gt;执行 80 中断处理程序，找到刚刚存的系统调用号（read），先检查缓存中有没有对应的数据，没有就去磁盘中加载到内核缓冲区，然后从内核缓冲区拷贝到用户空间&lt;/li&gt;
&lt;li&gt;最后恢复到用户态，通过 thread_info 恢复现场，用户态继续执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E7%9A%84%E8%BF%87%E7%A8%8B.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV19D4y1o797&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;零拷贝&lt;/h3&gt;
&lt;h4&gt;DMA&lt;/h4&gt;
&lt;p&gt;DMA (Direct Memory Access) ：直接存储器访问，让外部设备不通过 CPU 直接与系统内存交换数据的接口技术&lt;/p&gt;
&lt;p&gt;作用：可以解决批量数据的输入/输出问题，使数据的传送速度取决于存储器和外设的工作速度&lt;/p&gt;
&lt;p&gt;把内存数据传输到网卡然后发送：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有 DMA：CPU 读内存数据到 CPU 高速缓存，再写到网卡，这样就把 CPU 的速度拉低到和网卡一个速度&lt;/li&gt;
&lt;li&gt;使用 DMA：把数据读到 Socket 内核缓存区（CPU 复制），CPU 分配给 DMA 开始&lt;strong&gt;异步&lt;/strong&gt;操作，DMA 读取 Socket 缓冲区到 DMA 缓冲区，然后写到网卡。DMA 执行完后&lt;strong&gt;中断&lt;/strong&gt;（就是通知） CPU，这时 Socket 内核缓冲区为空，CPU 从用户态切换到内核态，执行中断处理程序，将需要使用 Socket 缓冲区的阻塞进程移到就绪队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-DMA.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;DMA 方式是一种完全由硬件进行信息传送的控制方式，通常系统总线由 CPU 管理，在 DMA 方式中，CPU 的主存控制信号被禁止使用，CPU 把总线（地址总线、数据总线、控制总线）让出来由 DMA 控制器接管，用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号，所以 DMA 控制器必须有以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接受外设发出的 DMA 请求，并向 CPU 发出总线接管请求&lt;/li&gt;
&lt;li&gt;当 CPU 发出允许接管信号后，进入 DMA 操作周期&lt;/li&gt;
&lt;li&gt;确定传送数据的主存单元地址及长度，并自动修改主存地址计数和传送长度计数&lt;/li&gt;
&lt;li&gt;规定数据在主存和外设间的传送方向，发出读写等控制信号，执行数据传送操作&lt;/li&gt;
&lt;li&gt;判断 DMA 传送是否结束，发出 DMA 结束信号，使 CPU 恢复正常工作状态（中断）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;BIO&lt;/h4&gt;
&lt;p&gt;传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换，以及 4 次数据拷贝：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JVM 发出 read 系统调用，OS 上下文切换到内核模式（切换 1）并将数据从网卡或硬盘等设备通过 DMA 读取到内核空间缓冲区（拷贝 1），内核缓冲区实际上是&lt;strong&gt;磁盘高速缓存（PageCache）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;OS 内核将数据复制到用户空间缓冲区（拷贝 2），然后 read 系统调用返回，又会导致一次内核空间到用户空间的上下文切换（切换 2）&lt;/li&gt;
&lt;li&gt;JVM 处理代码逻辑并发送 write() 系统调用，OS 上下文切换到内核模式（切换3）并从用户空间缓冲区复制数据到内核空间缓冲区（拷贝3）&lt;/li&gt;
&lt;li&gt;将内核空间缓冲区中的数据写到 hardware（拷贝4），write 系统调用返回，导致内核空间到用户空间的再次上下文切换（切换4）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流程图中的箭头反过来也成立，可以从网卡获取数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-BIO%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;read 调用图示：read、write 都是系统调用指令&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-缓冲区读写.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;mmap&lt;/h4&gt;
&lt;p&gt;mmap（Memory Mapped Files）内存映射加 write 实现零拷贝，&lt;strong&gt;零拷贝就是没有数据从内核空间复制到用户空间&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户空间和内核空间都使用内存，所以可以共享同一块物理内存地址，省去用户态和内核态之间的拷贝。写网卡时，共享空间的内容拷贝到 Socket 缓冲区，然后交给 DMA 发送到网卡，只需要 3 次复制&lt;/p&gt;
&lt;p&gt;进行了 4 次用户空间与内核空间的上下文切换，以及 3 次数据拷贝（2 次 DMA，一次 CPU 复制）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发出 mmap 系统调用，DMA 拷贝到内核缓冲区，映射到共享缓冲区；mmap 系统调用返回，无需拷贝&lt;/li&gt;
&lt;li&gt;发出 write 系统调用，将数据从内核缓冲区拷贝到内核 Socket 缓冲区；write 系统调用返回，DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-mmap%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原理：利用操作系统的 Page 来实现文件到物理内存的直接映射，完成映射后对物理内存的操作会&lt;strong&gt;被同步&lt;/strong&gt;到硬盘上&lt;/p&gt;
&lt;p&gt;缺点：不可靠，写到 mmap 中的数据并没有被真正的写到硬盘，操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘&lt;/p&gt;
&lt;p&gt;Java NIO 提供了 &lt;strong&gt;MappedByteBuffer&lt;/strong&gt; 类可以用来实现 mmap 内存映射，MappedByteBuffer 类对象&lt;strong&gt;只能通过调用 &lt;code&gt;FileChannel.map()&lt;/code&gt; 获取&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;sendfile&lt;/h4&gt;
&lt;p&gt;sendfile 实现零拷贝，打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile，然后经过 3 次复制和 2 次用户态和内核态的切换&lt;/p&gt;
&lt;p&gt;原理：数据根本不经过用户态，直接从内核缓冲区进入到 Socket Buffer，由于和用户态完全无关，就减少了两次上下文切换&lt;/p&gt;
&lt;p&gt;说明：零拷贝技术是不允许进程对文件内容作进一步的加工的，比如压缩数据再发送&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-sendfile%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;sendfile2.4 之后，sendfile 实现了更简单的方式，文件到达内核缓冲区后，不必再将数据全部复制到 socket buffer 缓冲区，而是只&lt;strong&gt;将记录数据位置和长度相关等描述符信息&lt;/strong&gt;保存到 socket buffer，DMA 根据 Socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上（2 次复制 2 次切换）&lt;/p&gt;
&lt;p&gt;Java NIO 对 sendfile 的支持是 &lt;code&gt;FileChannel.transferTo()/transferFrom()&lt;/code&gt;，把磁盘文件读取 OS 内核缓冲区后的 fileChannel，直接转给 socketChannel 发送，底层就是 sendfile&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/hancoder/article/details/112149121&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;BIO&lt;/h2&gt;
&lt;h3&gt;Inet&lt;/h3&gt;
&lt;p&gt;一个 InetAddress 类的对象就代表一个 IP 地址对象&lt;/p&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static InetAddress getLocalHost()&lt;/code&gt;：获得本地主机 IP 地址对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static InetAddress getByName(String host)&lt;/code&gt;：根据 IP 地址字符串或主机名获得对应的 IP 地址对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getHostName()&lt;/code&gt;：获取主机名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String getHostAddress()&lt;/code&gt;：获得 IP 地址字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class InetAddressDemo {
    public static void main(String[] args) throws Exception {
        // 1.获取本机地址对象
        InetAddress ip = InetAddress.getLocalHost();
        System.out.println(ip.getHostName());//DESKTOP-NNMBHQR
        System.out.println(ip.getHostAddress());//192.168.11.1
        // 2.获取域名ip对象
        InetAddress ip2 = InetAddress.getByName(&quot;www.baidu.com&quot;);
        System.out.println(ip2.getHostName());//www.baidu.com
        System.out.println(ip2.getHostAddress());//14.215.177.38
        // 3.获取公网IP对象。
        InetAddress ip3 = InetAddress.getByName(&quot;182.61.200.6&quot;);
        System.out.println(ip3.getHostName());//182.61.200.6
        System.out.println(ip3.getHostAddress());//182.61.200.6
        
        // 4.判断是否能通： ping  5s之前测试是否可通
        System.out.println(ip2.isReachable(5000)); // ping百度
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;UDP&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;UDP（User Datagram Protocol）协议的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面向无连接的协议，发送端只管发送，不确认对方是否能收到，速度快，但是不可靠，会丢失数据&lt;/li&gt;
&lt;li&gt;尽最大努力交付，没有拥塞控制&lt;/li&gt;
&lt;li&gt;基于数据包进行数据传输，发送数据的包的大小限制 &lt;strong&gt;64KB&lt;/strong&gt; 以内&lt;/li&gt;
&lt;li&gt;支持一对一、一对多、多对一、多对多的交互通信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UDP 协议的使用场景：在线视频、网络语音、电话&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现UDP&lt;/h4&gt;
&lt;p&gt;UDP 协议相关的两个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DatagramPacket（数据包对象）：用来封装要发送或要接收的数据，比如：集装箱&lt;/li&gt;
&lt;li&gt;DatagramSocket（发送对象）：用来发送或接收数据包，比如：码头&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DatagramPacket&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DatagramPacket 类：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)&lt;/code&gt;：创建发送端数据包对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;buf：要发送的内容，字节数组&lt;/li&gt;
&lt;li&gt;length：要发送内容的长度，单位是字节&lt;/li&gt;
&lt;li&gt;address：接收端的IP地址对象&lt;/li&gt;
&lt;li&gt;port：接收端的端口号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;public new DatagramPacket(byte[] buf, int length)&lt;/code&gt;：创建接收端的数据包对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;buf：用来存储接收到内容&lt;/li&gt;
&lt;li&gt;length：能够接收内容的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DatagramPacket 类常用方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public int getLength()&lt;/code&gt;：获得实际接收到的字节个数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public byte[] getData()&lt;/code&gt;：返回数据缓冲区&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DatagramSocket&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DatagramSocket 类构造方法：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;protected DatagramSocket()&lt;/code&gt;：创建发送端的 Socket 对象，系统会随机分配一个端口号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected DatagramSocket(int port)&lt;/code&gt;：创建接收端的 Socket 对象并指定端口号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DatagramSocket 类成员方法：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void send(DatagramPacket dp)&lt;/code&gt;：发送数据包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void receive(DatagramPacket p)&lt;/code&gt;：接收数据包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void close()&lt;/code&gt;：关闭数据报套接字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UDPClientDemo {
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;===启动客户端===&quot;);
        // 1.创建一个集装箱对象，用于封装需要发送的数据包!
        byte[] buffer = &quot;我学Java&quot;.getBytes();
        DatagramPacket packet = new DatagramPacket(buffer,bubffer.length,InetAddress.getLoclHost,8000);
        // 2.创建一个码头对象
        DatagramSocket socket = new DatagramSocket();
        // 3.开始发送数据包对象
        socket.send(packet);
        socket.close();
    }
}
public class UDPServerDemo{
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;==启动服务端程序==&quot;);
        // 1.创建一个接收客户都端的数据包对象（集装箱）
        byte[] buffer = new byte[1024*64];
        DatagramPacket packet = new DatagramPacket(buffer, bubffer.length);
        // 2.创建一个接收端的码头对象
        DatagramSocket socket = new DatagramSocket(8000);
        // 3.开始接收
        socket.receive(packet);
        // 4.从集装箱中获取本次读取的数据量
        int len = packet.getLength();
        // 5.输出数据
        // String rs = new String(socket.getData(), 0, len)
        String rs = new String(buffer , 0 , len);
        System.out.println(rs);
        // 6.服务端还可以获取发来信息的客户端的IP和端口。
        String ip = packet.getAddress().getHostAdress();
        int port = packet.getPort();
        socket.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;通讯方式&lt;/h4&gt;
&lt;p&gt;UDP 通信方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单播：用于两个主机之间的端对端通信&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;组播：用于对一组特定的主机进行通信&lt;/p&gt;
&lt;p&gt;IP : 224.0.1.0&lt;/p&gt;
&lt;p&gt;Socket 对象 : MulticastSocket&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;广播：用于一个主机对整个局域网上所有主机上的数据通信&lt;/p&gt;
&lt;p&gt;IP : 255.255.255.255&lt;/p&gt;
&lt;p&gt;Socket 对象 : DatagramSocket&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;TCP&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;TCP/IP (Transfer Control Protocol) 协议，传输控制协议&lt;/p&gt;
&lt;p&gt;TCP/IP 协议的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面向连接的协议，提供可靠交互，速度慢&lt;/li&gt;
&lt;li&gt;点对点的全双工通信&lt;/li&gt;
&lt;li&gt;通过&lt;strong&gt;三次握手&lt;/strong&gt;建立连接，连接成功形成数据传输通道；通过&lt;strong&gt;四次挥手&lt;/strong&gt;断开连接&lt;/li&gt;
&lt;li&gt;基于字节流进行数据传输，传输数据大小没有限制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TCP 协议的使用场景：文件上传和下载、邮件发送和接收、远程登录&lt;/p&gt;
&lt;p&gt;注意：&lt;strong&gt;TCP 不会为没有数据的 ACK 超时重传&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/三次握手.png&quot; alt=&quot;三次握手&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/四次挥手.png&quot; alt=&quot;四次挥手&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;推荐阅读：https://yuanrengu.com/2020/77eef79f.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Socket&lt;/h4&gt;
&lt;p&gt;TCP 通信也叫 &lt;strong&gt;Socket 网络编程&lt;/strong&gt;，只要代码基于 Socket 开发，底层就是基于了可靠传输的 TCP 通信&lt;/p&gt;
&lt;p&gt;双向通信：Java Socket 是全双工的，在任意时刻，线路上存在 &lt;code&gt;A -&amp;gt; B&lt;/code&gt; 和 &lt;code&gt;B -&amp;gt; A&lt;/code&gt; 的双向信号传输，即使是阻塞 IO，读和写也是可以同时进行的，只要分别采用读线程和写线程即可，读不会阻塞写、写也不会阻塞读&lt;/p&gt;
&lt;p&gt;TCP 协议相关的类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Socket：一个该类的对象就代表一个客户端程序。&lt;/li&gt;
&lt;li&gt;ServerSocket：一个该类的对象就代表一个服务器端程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Socket 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Socket(InetAddress address,int port)&lt;/code&gt;：创建流套接字并将其连接到指定 IP 指定端口号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Socket(String host, int port)&lt;/code&gt;：根据 IP 地址字符串和端口号创建客户端 Socket 对象&lt;/p&gt;
&lt;p&gt;注意事项：&lt;strong&gt;执行该方法，就会立即连接指定的服务器，连接成功，则表示三次握手通过&lt;/strong&gt;，反之抛出异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OutputStream getOutputStream()&lt;/code&gt;：获得字节输出流对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InputStream getInputStream()&lt;/code&gt;：获得字节输入流对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void shutdownInput()&lt;/code&gt;：停止接受&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void shutdownOutput()&lt;/code&gt;：停止发送数据，终止通信&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SocketAddress getRemoteSocketAddress() &lt;/code&gt;：返回套接字连接到的端点的地址，未连接返回 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ServerSocket 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;code&gt;public ServerSocket(int port)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用 API：&lt;code&gt;public Socket accept()&lt;/code&gt;，&lt;strong&gt;阻塞等待&lt;/strong&gt;接收一个客户端的 Socket 管道连接请求，连接成功返回一个 Socket 对象&lt;/p&gt;
&lt;p&gt;三次握手后 TCP 连接建立成功，服务器内核会把连接从 SYN 半连接队列（一次握手时在服务端建立的队列）中移出，移入 accept 全连接队列，等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数，就会造成 accept 队列溢出，最终导致建立好的 TCP 连接被丢弃&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-TCP三次握手.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相当于&lt;/strong&gt;客户端和服务器建立一个数据管道（虚连接，不是真正的物理连接），管道一般不用 close&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现TCP&lt;/h4&gt;
&lt;h5&gt;开发流程&lt;/h5&gt;
&lt;p&gt;客户端的开发流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端要请求于服务端的 Socket 管道连接&lt;/li&gt;
&lt;li&gt;从 Socket 通信管道中得到一个字节输出流&lt;/li&gt;
&lt;li&gt;通过字节输出流给服务端写出数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务端的开发流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 ServerSocket 注册端口&lt;/li&gt;
&lt;li&gt;接收客户端的 Socket 管道连接&lt;/li&gt;
&lt;li&gt;从 Socket 通信管道中得到一个字节输入流&lt;/li&gt;
&lt;li&gt;从字节输入流中读取客户端发来的数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/BIO%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/TCP-%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果输出缓冲区空间不够存放主机发送的数据，则会被阻塞，输入缓冲区同理&lt;/li&gt;
&lt;li&gt;缓冲区不属于应用程序，属于内核&lt;/li&gt;
&lt;li&gt;TCP 从输出缓冲区读取数据会加锁阻塞线程&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;实现通信&lt;/h5&gt;
&lt;p&gt;需求一：客户端发送一行数据，服务端接收一行数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClientDemo {
    public static void main(String[] args) throws Exception {
        // 1.客户端要请求于服务端的socket管道连接。
        Socket socket = new Socket(&quot;127.0.0.1&quot;, 8080);
        // 2.从socket通信管道中得到一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 3.把低级的字节输出流包装成高级的打印流。
        PrintStream ps = new PrintStream(os);
        // 4.开始发消息出去
        ps.println(&quot;我是客户端&quot;);
        ps.flush();//一般不关闭IO流
        System.out.println(&quot;客户端发送完毕~~~~&quot;);
    }
}
public class ServerDemo{
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;----服务端启动----&quot;);
        // 1.注册端口: public ServerSocket(int port)
        ServerSocket serverSocket = new ServerSocket(8080);
        // 2.开始等待接收客户端的Socket管道连接。
        Socket socket = serverSocket.accept();
        // 3.从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();
        // 4.把字节输入流转换成字符输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        // 6.按照行读取消息 。
        String line;
        if((line = br.readLine()) != null){
            System.out.println(line);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需求二：客户端可以反复发送数据，服务端可以反复数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClientDemo {
    public static void main(String[] args) throws Exception {
        // 1.客户端要请求于服务端的socket管道连接。
        Socket socket = new Socket(&quot;127.0.0.1&quot;,8080);
        // 2.从socket通信管道中得到一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 3.把低级的字节输出流包装成高级的打印流。
        PrintStream ps = new PrintStream(os);
        // 4.开始发消息出去
         while(true){
            Scanner sc = new Scanner(System.in);
            System.out.print(&quot;请说：&quot;);
            ps.println(sc.nextLine());
            ps.flush();
        }
    }
}
public class ServerDemo{
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;----服务端启动----&quot;);
        // 1.注册端口: public ServerSocket(int port)
        ServerSocket serverSocket = new ServerSocket(8080);
        // 2.开始等待接收客户端的Socket管道连接。
        Socket socket = serverSocket.accept();
        // 3.从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();
        // 4.把字节输入流转换成字符输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        // 6.按照行读取消息 。
        String line;
        while((line = br.readLine()) != null){
            System.out.println(line);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需求三：实现一个服务端可以同时接收多个客户端的消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClientDemo {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket(&quot;127.0.0.1&quot;,8080);
        OutputStream os = new socket.getOutputStream();
        PrintStream ps = new PrintStream(os);
		while(true){
            Scanner sc = new Scanner(System.in);
            System.out.print(&quot;请说：&quot;);
            ps.println(sc.nextLine());
            ps.flush();
        }
    }
}
public class ServerDemo{
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;----服务端启动----&quot;);
        ServerSocket serverSocket = new ServerSocket(8080);
        while(true){
            // 开始等待接收客户端的Socket管道连接。
             Socket socket = serverSocket.accept();
            // 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。
            new ServerReaderThread(socket).start();
        }
    }
}
class ServerReaderThread extends Thread{
    privat Socket socket;
    public ServerReaderThread(Socket socket){this.socket = socket;}
    @Override
    public void run() {
        try(InputStream is = socket.getInputStream();
           	BufferedReader br = new BufferedReader(new InputStreamReader(is))
           ){
            String line;
            while((line = br.readLine()) != null){
                sout(socket.getRemoteSocketAddress() + &quot;:&quot; + line);
            }
        }catch(Exception e){
            sout(socket.getRemoteSocketAddress() + &quot;下线了~~~~~~&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;伪异步&lt;/h5&gt;
&lt;p&gt;一个客户端要一个线程，并发越高系统瘫痪的越快，可以在服务端引入线程池，使用线程池来处理与客户端的消息通信&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优势：不会引起系统的死机，可以控制并发线程的数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;劣势：同时可以并发的线程将受到限制&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BIOServer {
    public static void main(String[] args) throws Exception {
        //线程池机制
        //创建一个线程池，如果有客户端连接，就创建一个线程，与之通讯(单独写一个方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println(&quot;服务器启动了&quot;);
        while (true) {
            System.out.println(&quot;线程名字 = &quot; + Thread.currentThread().getName());
            //监听，等待客户端连接
            System.out.println(&quot;等待连接....&quot;);
            final Socket socket = serverSocket.accept();
            System.out.println(&quot;连接到一个客户端&quot;);
            //创建一个线程，与之通讯
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法，和客户端通讯
    public static void handler(Socket socket) {
        try {
            System.out.println(&quot;线程名字 = &quot; + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            int len;
            //循环的读取客户端发送的数据
            while ((len = inputStream.read(bytes)) != -1) {
                System.out.println(&quot;线程名字 = &quot; + Thread.currentThread().getName());
                //输出客户端发送的数据
                System.out.println(new String(bytes, 0, read));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(&quot;关闭和client的连接&quot;);
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;文件传输&lt;/h4&gt;
&lt;h5&gt;字节流&lt;/h5&gt;
&lt;p&gt;客户端：本地图片:  ‪E:\seazean\图片资源\beautiful.jpg
服务端：服务器路径：E:\seazean\图片服务器&lt;/p&gt;
&lt;p&gt;UUID. randomUUID() : 方法生成随机的文件名&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;socket.shutdownOutput()&lt;/strong&gt;：这个必须执行，不然服务器会一直循环等待数据，最后文件损坏，程序报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//常量包
public class Constants {
    public static final String SRC_IMAGE = &quot;D:\\seazean\\图片资源\\beautiful.jpg&quot;;
    public static final String SERVER_DIR = &quot;D:\\seazean\\图片服务器\\&quot;;
    public static final String SERVER_IP = &quot;127.0.0.1&quot;;
    public static final int SERVER_PORT = 8888;

}
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket(Constants.ERVER_IP,Constants.SERVER_PORT);
        BufferedOutputStream bos=new BufferedOutputStream(socket.getOutputStream());
        //提取本机的图片上传给服务端。Constants.SRC_IMAGE
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream());
        byte[] buffer = new byte[1024];
        int len ;
        while((len = bis.read(buffer)) != -1) {
            bos.write(buffer, 0 ,len);
        }
        bos.flush();// 刷新图片数据到服务端！！
        socket.shutdownOutput();// 告诉服务端我的数据已经发送完毕，不要在等我了！
        bis.close();
        
        //等待着服务端的响应数据！！
        BufferedReader br = new BufferedReader(
           				 new InputStreamReader(socket.getInputStream()));
        System.out.println(&quot;收到服务端响应：&quot;+br.readLine());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ServerDemo {
    public static void main(String[] args) throws Exception {
        System.out.println(&quot;----服务端启动----&quot;);
        // 1.注册端口: 
        ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT);
        // 2.定义一个循环不断的接收客户端的连接请求
        while(true){
            // 3.开始等待接收客户端的Socket管道连接。
            Socket socket = serverSocket.accept();
            // 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。
            new ServerReaderThread(socket).start();
        }
    }
}
class ServerReaderThread extends Thread{
    private Socket socket ;
    public ServerReaderThread(Socket socket){this.socket = socket;}
    @Override
    public void run() {
        try{
            InputStream is = socket.getInputStream();
           	BufferedInputStream bis = new BufferedInputStream(is);
            BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream
                (Constants.SERVER_DIR+UUID.randomUUID().toString()+&quot;.jpg&quot;));
            byte[] buffer = new byte[1024];
            int len;
            while((len = bis.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
            bos.close();
            System.out.println(&quot;服务端接收完毕了！&quot;);
            
            // 4.响应数据给客户端
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println(&quot;您好，已成功接收您上传的图片！&quot;);
            ps.flush();
            Thread.sleep(10000);
        }catch (Exception e){
            sout(socket.getRemoteSocketAddress() + &quot;下线了&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;数据流&lt;/h5&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DataOutputStream(OutputStream out)&lt;/code&gt; : 创建一个新的数据输出流，以将数据写入指定的底层输出流&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DataInputStream(InputStream in) &lt;/code&gt; : 创建使用指定的底层 InputStream 的 DataInputStream&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final void writeUTF(String str)&lt;/code&gt; : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final String readUTF()&lt;/code&gt; : 读取以 modified UTF-8 格式编码的 Unicode 字符串，返回 String 类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
		InputStream is = new FileInputStream(&quot;path&quot;);
            //  1、请求与服务端的Socket链接
            Socket socket = new Socket(&quot;127.0.0.1&quot; , 8888);
            //  2、把字节输出流包装成一个数据输出流
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //  3、先发送上传文件的后缀给服务端
            dos.writeUTF(&quot;.png&quot;);
            //  4、把文件数据发送给服务端进行接收
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) &amp;gt; 0 ){
                dos.write(buffer , 0 , len);
            }
            dos.flush();
            Thread.sleep(10000);
    }
}

public class Server {
    public static void main(String[] args) {
        ServerSocket ss = new ServerSocket(8888);
        Socket socket = ss.accept();
 		// 1、得到一个数据输入流读取客户端发送过来的数据
		DataInputStream dis = new DataInputStream(socket.getInputStream());
		// 2、读取客户端发送过来的文件类型
		String suffix = dis.readUTF();
		// 3、定义一个字节输出管道负责把客户端发来的文件数据写出去
		OutputStream os = new FileOutputStream(&quot;path&quot;+
                    UUID.randomUUID().toString()+suffix);
		// 4、从数据输入流中读取文件数据，写出到字节输出流中去
		byte[] buffer = new byte[1024];
		int len;
		while((len = dis.read(buffer)) &amp;gt; 0){
 			os.write(buffer,0, len);
		}
		os.close();
		System.out.println(&quot;服务端接收文件保存成功！&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;NIO&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;NIO的介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;Java NIO（New IO、Java non-blocking IO），从 Java 1.4 版本开始引入的一个新的 IO API，可以替代标准的 Java IO API，NIO 支持面向缓冲区的、基于通道的 IO 操作，以更加高效的方式进行文件的读写操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NIO 有三大核心部分：&lt;strong&gt;Channel（通道），Buffer（缓冲区），Selector（选择器）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;NIO 是非阻塞 IO，传统 IO 的 read 和 write 只能阻塞执行，线程在读写 IO 期间不能干其他事情，比如调用 socket.accept()，如果服务器没有数据传输过来，线程就一直阻塞，而 NIO 中可以配置 Socket 为非阻塞模式&lt;/li&gt;
&lt;li&gt;NIO 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来，根据实际情况可以分配 20 或者 80 个线程来处理，不像之前的阻塞 IO 那样分配 1000 个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NIO 和 BIO 的比较：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BIO 以流的方式处理数据，而 NIO 以块的方式处理数据，块 I/O 的效率比流 I/O 高很多&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BIO 是阻塞的，NIO 则是非阻塞的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BIO 基于字节流和字符流进行操作，而 NIO 基于 Channel 和 Buffer 进行操作，数据从通道读取到缓冲区中，或者从缓冲区写入到通道中。Selector 用于监听多个通道的事件（比如：连接请求，数据到达等），因此使用单个线程就可以监听多个客户端通道&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NIO&lt;/th&gt;
&lt;th&gt;BIO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;面向缓冲区（Buffer）&lt;/td&gt;
&lt;td&gt;面向流（Stream）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非阻塞（Non Blocking IO）&lt;/td&gt;
&lt;td&gt;阻塞IO(Blocking IO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;选择器（Selectors）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;实现原理&lt;/h3&gt;
&lt;p&gt;NIO 三大核心部分：Channel (通道)、Buffer (缓冲区)、Selector (选择器)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Buffer 缓冲区&lt;/p&gt;
&lt;p&gt;缓冲区本质是一块可以写入数据、读取数据的内存，&lt;strong&gt;底层是一个数组&lt;/strong&gt;，这块内存被包装成 NIO Buffer 对象，并且提供了方法用来操作这块内存，相比较直接对数组的操作，Buffer 的 API 更加容易操作和管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Channel 通道&lt;/p&gt;
&lt;p&gt;Java NIO 的通道类似流，不同的是既可以从通道中读取数据，又可以写数据到通道，流的读写通常是单向的，通道可以非阻塞读取和写入通道，支持读取或写入缓冲区，也支持异步地读写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Selector 选择器&lt;/p&gt;
&lt;p&gt;Selector 是一个 Java NIO 组件，能够检查一个或多个 NIO 通道，并确定哪些通道已经准备好进行读取或写入，这样一个单独的线程可以管理多个 channel，从而管理多个网络连接，提高效率&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NIO 的实现框架：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO%E6%A1%86%E6%9E%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 Channel 对应一个 Buffer&lt;/li&gt;
&lt;li&gt;一个线程对应 Selector ， 一个 Selector 对应多个 Channel（连接）&lt;/li&gt;
&lt;li&gt;程序切换到哪个 Channel 是由事件决定的，Event 是一个重要的概念&lt;/li&gt;
&lt;li&gt;Selector 会根据不同的事件，在各个通道上切换&lt;/li&gt;
&lt;li&gt;Buffer 是一个内存块 ， 底层是一个数组&lt;/li&gt;
&lt;li&gt;数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流，或者是输出流，不能双向，NIO 的 Buffer 是可以读也可以写， flip() 切换 Buffer 的工作模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java NIO 系统的核心在于：通道和缓冲区，通道表示打开的 IO 设备（例如：文件、 套接字）的连接。若要使用 NIO 系统，获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区，然后操作缓冲区，对数据进行处理。简而言之，Channel 负责传输， Buffer 负责存取数据&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;缓冲区&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;缓冲区（Buffer）：缓冲区本质上是一个&lt;strong&gt;可以读写数据的内存块&lt;/strong&gt;，用于特定基本数据类型的容器，用于与 NIO 通道进行交互，数据是从通道读入缓冲区，从缓冲区写入通道中的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Buffer.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Buffer 底层是一个数组&lt;/strong&gt;，可以保存多个相同类型的数据，根据数据类型不同 ，有以下 Buffer 常用子类：ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本属性&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容量（capacity）：作为一个内存块，Buffer 具有固定大小，缓冲区容量不能为负，并且创建后不能更改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;限制 （limit）：表示缓冲区中可以操作数据的大小（limit 后数据不能进行读写），缓冲区的限制不能为负，并且不能大于其容量。写入模式，limit 等于 buffer 的容量；读取模式下，limit 等于写入的数据量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置（position）：&lt;strong&gt;下一个要读取或写入的数据的索引&lt;/strong&gt;，缓冲区的位置不能为负，并且不能大于其限制&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标记（mark）与重置（reset）：标记是一个索引，通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的位置，可以通过调用 reset() 方法恢复到这个 position&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置、限制、容量遵守以下不变式： &lt;strong&gt;0 &amp;lt;= position &amp;lt;= limit &amp;lt;= capacity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Buffer操作.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;static XxxBuffer allocate(int capacity)&lt;/code&gt;：创建一个容量为 capacity 的 XxxBuffer 对象&lt;/p&gt;
&lt;p&gt;Buffer 基本操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer clear()&lt;/td&gt;
&lt;td&gt;清空缓冲区，不清空内容，将位置设置为零，限制设置为容量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer flip()&lt;/td&gt;
&lt;td&gt;翻转缓冲区，将缓冲区的界限设置为当前位置，position 置 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public int capacity()&lt;/td&gt;
&lt;td&gt;返回 Buffer的 capacity 大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int limit()&lt;/td&gt;
&lt;td&gt;返回 Buffer 的界限 limit 的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer limit(int n)&lt;/td&gt;
&lt;td&gt;设置缓冲区界限为 n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer mark()&lt;/td&gt;
&lt;td&gt;在此位置对缓冲区设置标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int position()&lt;/td&gt;
&lt;td&gt;返回缓冲区的当前位置 position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer position(int n)&lt;/td&gt;
&lt;td&gt;设置缓冲区的当前位置为n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer reset()&lt;/td&gt;
&lt;td&gt;将位置 position 重置为先前 mark 标记的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public Buffer rewind()&lt;/td&gt;
&lt;td&gt;将位置设为为 0，取消设置的 mark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int remaining()&lt;/td&gt;
&lt;td&gt;返回当前位置 position 和 limit 之间的元素个数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final boolean hasRemaining()&lt;/td&gt;
&lt;td&gt;判断缓冲区中是否还有元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static ByteBuffer wrap(byte[] array)&lt;/td&gt;
&lt;td&gt;将一个字节数组包装到缓冲区中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abstract ByteBuffer asReadOnlyBuffer()&lt;/td&gt;
&lt;td&gt;创建一个新的只读字节缓冲区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract ByteBuffer compact()&lt;/td&gt;
&lt;td&gt;缓冲区当前位置与其限制（如果有）之间的字节被复制到缓冲区的开头&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Buffer 数据操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public abstract byte get()&lt;/td&gt;
&lt;td&gt;读取该缓冲区当前位置的单个字节，然后位置 + 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public ByteBuffer get(byte[] dst)&lt;/td&gt;
&lt;td&gt;读取多个字节到字节数组 dst 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract byte get(int index)&lt;/td&gt;
&lt;td&gt;读取指定索引位置的字节，不移动 position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract ByteBuffer put(byte b)&lt;/td&gt;
&lt;td&gt;将给定单个字节写入缓冲区的当前位置，position+1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final ByteBuffer put(byte[] src)&lt;/td&gt;
&lt;td&gt;将 src 字节数组写入缓冲区的当前位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract ByteBuffer put(int index, byte b)&lt;/td&gt;
&lt;td&gt;将指定字节写入缓冲区的索引位置，不移动 position&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;提示：&quot;\n&quot;，占用两个字节&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;读写数据&lt;/h4&gt;
&lt;p&gt;使用 Buffer 读写数据一般遵循以下四个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写入数据到 Buffer&lt;/li&gt;
&lt;li&gt;调用 flip()方法，转换为读取模式&lt;/li&gt;
&lt;li&gt;从 Buffer 中读取数据&lt;/li&gt;
&lt;li&gt;调用 buffer.clear() 方法清除缓冲区（不是清空了数据，只是重置指针）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class TestBuffer {
	@Test
    public void test(){
		String str = &quot;seazean&quot;;
		//1. 分配一个指定大小的缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		System.out.println(&quot;-----------------allocate()----------------&quot;);
		System.out.println(bufferf.position());//0
		System.out.println(buffer.limit());//1024
		System.out.println(buffer.capacity());//1024
        
        //2. 利用 put() 存入数据到缓冲区中
      	buffer.put(str.getBytes());
     	System.out.println(&quot;-----------------put()----------------&quot;);
		System.out.println(bufferf.position());//7
		System.out.println(buffer.limit());//1024
		System.out.println(buffer.capacity());//1024
        
        //3. 切换读取数据模式
        buffer.flip();
        System.out.println(&quot;-----------------flip()----------------&quot;);
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//7
        System.out.println(buffer.capacity());//1024
        
        //4. 利用 get() 读取缓冲区中的数据
        byte[] dst = new byte[buffer.limit()];
        buffer.get(dst);
        System.out.println(dst.length);
        System.out.println(new String(dst, 0, dst.length));
        System.out.println(buffer.position());//7
        System.out.println(buffer.limit());//7
       
        //5. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在，但是处于“被遗忘”状态
        System.out.println(buffer.hasRemaining());//true
        buffer.clear();
        System.out.println(buffer.hasRemaining());//true
      	System.out.println(&quot;-----------------clear()----------------&quot;);
      	System.out.println(buffer.position());//0
      	System.out.println(buffer.limit());//1024
      	System.out.println(buffer.capacity());//1024
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;粘包拆包&lt;/h4&gt;
&lt;p&gt;网络上有多条数据发送给服务端，数据之间使用 \n 进行分隔，但这些数据在接收时，被进行了重新组合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Hello,world\n
// I&apos;m zhangsan\n
// How are you?\n
------ &amp;gt; 黏包，半包
// Hello,world\nI&apos;m zhangsan\nHo
// w are you?\n
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ByteBuffer source = ByteBuffer.allocate(32);
    //                     11            24
    source.put(&quot;Hello,world\nI&apos;m zhangsan\nHo&quot;.getBytes());
    split(source);

    source.put(&quot;w are you?\nhaha!\n&quot;.getBytes());
    split(source);
}

private static void split(ByteBuffer source) {
    source.flip();
    int oldLimit = source.limit();
    for (int i = 0; i &amp;lt; oldLimit; i++) {
        if (source.get(i) == &apos;\n&apos;) {
            // 根据数据的长度设置缓冲区
            ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position());
            // 0 ~ limit
            source.limit(i + 1);
            target.put(source); // 从source 读，向 target 写
            // debugAll(target); 访问 buffer 的方法
            source.limit(oldLimit);
        }
    }
    // 访问过的数据复制到开头
    source.compact();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;直接内存&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Byte Buffer 有两种类型，一种是基于直接内存（也就是非堆内存），另一种是非直接内存（也就是堆内存）&lt;/p&gt;
&lt;p&gt;Direct Memory 优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 的 NIO 库允许 Java 程序使用直接内存，使用 native 函数直接分配堆外内存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读写性能高&lt;/strong&gt;，读写频繁的场合可能会考虑使用直接内存&lt;/li&gt;
&lt;li&gt;大大提高 IO 性能，避免了在 Java 堆和 native 堆来回复制数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接内存缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能使用内核缓冲区 Page Cache 的缓存优势，无法缓存最近被访问的数据和使用预读功能&lt;/li&gt;
&lt;li&gt;分配回收成本较高，不受 JVM 内存回收管理&lt;/li&gt;
&lt;li&gt;可能导致 OutOfMemoryError 异常：OutOfMemoryError: Direct buffer memory&lt;/li&gt;
&lt;li&gt;回收依赖 System.gc() 的调用，但这个调用 JVM 不保证执行、也不保证何时执行，行为是不可控的。程序一般需要自行管理，成对去调用 malloc、free&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传输很大的数据文件，数据的生命周期很长，导致 Page Cache 没有起到缓存的作用，一般采用直接 IO 的方式&lt;/li&gt;
&lt;li&gt;适合频繁的 IO 操作，比如网络并发场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据流的角度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非直接内存的作用链：本地 IO → 内核缓冲区→ 用户（JVM）缓冲区 →内核缓冲区 → 本地 IO&lt;/li&gt;
&lt;li&gt;直接内存是：本地 IO → 直接内存 → 本地 IO&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JVM 直接内存图解：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-直接内存直接缓冲区.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-直接内存非直接缓冲区.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;通信原理&lt;/h4&gt;
&lt;p&gt;堆外内存不受 JVM GC 控制，可以使用堆外内存进行通信，防止 GC 后缓冲区位置发生变化的情况&lt;/p&gt;
&lt;p&gt;NIO 使用的 SocketChannel 也是使用的堆外内存，源码解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int write(ByteBuffer var1) throws IOException {
     do {
         var3 = IOUtil.write(this.fd, var1, -1L, nd);
     } while(var3 == -3 &amp;amp;&amp;amp; this.isOpen());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IOUtil#write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) {
    // 【判断是否是直接内存，是则直接写出，不是则封装到直接内存】
    if (var1 instanceof DirectBuffer) {
        return writeFromNativeBuffer(var0, var1, var2, var4);
    } else {
        //....
        // 从堆内buffer拷贝到堆外buffer
        ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
        var8.put(var1);
        //...
        // 从堆外写到内核缓冲区
		int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读操作相同&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分配回收&lt;/h4&gt;
&lt;p&gt;直接内存创建 Buffer 对象：&lt;code&gt;static XxxBuffer allocateDirect(int capacity)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;DirectByteBuffer 源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DirectByteBuffer(int cap) { 
    //....
    long base = 0;
    try {
        // 分配直接内存
        base = unsafe.allocateMemory(size);
    }
    // 内存赋值
    unsafe.setMemory(base, size, (byte) 0);
    if (pa &amp;amp;&amp;amp; (base % ps != 0)) {
        address = base + ps - (base &amp;amp; (ps - 1));
    } else {
        address = base;
    }
    // 创建回收函数
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
private static class Deallocator implements Runnable {
    public void run() {
        unsafe.freeMemory(address);
		//...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;分配和回收原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配，setMemory 方法完成赋值&lt;/li&gt;
&lt;li&gt;ByteBuffer 的实现类内部，使用了 Cleaner（虚引用）来监测 ByteBuffer 对象，一旦 ByteBuffer 对象被垃圾回收，那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法，最后通过 freeMemory 来释放直接内存&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 直接内存分配的底层原理：Unsafe
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();
        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;共享内存&lt;/h4&gt;
&lt;p&gt;FileChannel 提供 map 方法返回 MappedByteBuffer 对象，把文件映射到内存，通常情况可以映射整个文件，如果文件比较大，可以进行分段映射，完成映射后对物理内存的操作会被&lt;strong&gt;同步&lt;/strong&gt;到硬盘上&lt;/p&gt;
&lt;p&gt;FileChannel 中的成员属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MapMode.mode：内存映像文件访问的方式，共三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MapMode.READ_ONLY&lt;/code&gt;：只读，修改得到的缓冲区将导致抛出异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapMode.READ_WRITE&lt;/code&gt;：读/写，对缓冲区的更改最终将写入文件，但此次修改对映射到同一文件的其他程序不一定是可见&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapMode.PRIVATE&lt;/code&gt;：私用，可读可写，但是修改的内容不会写入文件，只是 buffer 自身的改变&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final FileLock lock()&lt;/code&gt;：获取此文件通道的排他锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MappedByteBuffer，可以让文件在直接内存（堆外内存）中进行修改，这种方式叫做&lt;strong&gt;内存映射&lt;/strong&gt;，可以直接调用系统底层的缓存，没有 JVM 和 OS 之间的复制操作，提高了传输效率，作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可以用于进程间的通信，能达到共享内存页的作用&lt;/strong&gt;，但在高并发下要对文件内存进行加锁，防止出现读写内容混乱和不一致性，Java 提供了文件锁 FileLock，但在父/子进程中锁定后另一进程会一直等待，效率不高&lt;/li&gt;
&lt;li&gt;读写那些太大而不能放进内存中的文件，&lt;strong&gt;分段映射&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MappedByteBuffer 较之 ByteBuffer 新增的三个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final MappedByteBuffer force()&lt;/code&gt;：缓冲区是 READ_WRITE 模式下，对缓冲区内容的修改&lt;strong&gt;强制写入文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final MappedByteBuffer load()&lt;/code&gt;：将缓冲区的内容载入物理内存，并返回该缓冲区的引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final boolean isLoaded()&lt;/code&gt;：如果缓冲区的内容在物理内存中，则返回真，否则返回假&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        // 读写模式
        RandomAccessFile ra = new RandomAccessFile(&quot;1.txt&quot;, &quot;rw&quot;);
        // 获取对应的通道
        FileChannel channel = ra.getChannel();

        /**
         * 参数1	FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数2	0: 文件映射时的起始位置
         * 参数3	5: 是映射到内存的大小（不是索引位置），即将 1.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        buffer.put(0, (byte) &apos;H&apos;);
        buffer.put(3, (byte) &apos;9&apos;);
        buffer.put(5, (byte) &apos;Y&apos;);	//IndexOutOfBoundsException

        ra.close();
        System.out.println(&quot;修改成功~~&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从硬盘上将文件读入内存，要经过文件系统进行数据拷贝，拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件，拷贝数据的效率要比 read 和 write 系统调用高：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;read() 是系统调用，首先将文件从硬盘拷贝到内核空间的一个缓冲区，再将这些数据拷贝到用户空间，实际上进行了两次数据拷贝&lt;/li&gt;
&lt;li&gt;mmap() 也是系统调用，但没有进行数据拷贝，当缺页中断发生时，直接将文件从硬盘拷贝到共享内存，只进行了一次数据拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：mmap 的文件映射，在 Full GC 时才会进行释放，如果需要手动清除内存映射文件，可以反射调用 sun.misc.Cleaner 方法&lt;/p&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/f90866dcbffc&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;通道&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;通道（Channel）：表示 IO 源与目标打开的连接，Channel 类似于传统的流，只不过 Channel 本身不能直接访问数据，Channel 只能与 Buffer &lt;strong&gt;进行交互&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;NIO 的通道类似于流，但有些区别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通道可以同时进行读写，而流只能读或者只能写&lt;/li&gt;
&lt;li&gt;通道可以实现异步读写数据&lt;/li&gt;
&lt;li&gt;通道可以从缓冲读数据，也可以写数据到缓冲&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BIO 中的 Stream 是单向的，NIO 中的 Channel 是双向的，可以读操作，也可以写操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Channel 在 NIO 中是一个接口：&lt;code&gt;public interface Channel extends Closeable{}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Channel 实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;FileChannel：用于读取、写入、映射和操作文件的通道，&lt;strong&gt;只能工作在阻塞模式下&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 FileInputStream 获取的 Channel 只能读&lt;/li&gt;
&lt;li&gt;通过 FileOutputStream 获取的 Channel 只能写&lt;/li&gt;
&lt;li&gt;通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DatagramChannel：通过 UDP 读写网络中的数据通道&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SocketChannel：通过 TCP 读写网络中的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServerSocketChannel：可以&lt;strong&gt;监听&lt;/strong&gt;新进来的 TCP 连接，对每一个新进来的连接都会创建一个 SocketChannel&lt;/p&gt;
&lt;p&gt;提示：ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;获取 Channel 方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对支持通道的对象调用 &lt;code&gt;getChannel()&lt;/code&gt; 方法&lt;/li&gt;
&lt;li&gt;通过通道的静态方法 &lt;code&gt;open()&lt;/code&gt; 打开并返回指定通道&lt;/li&gt;
&lt;li&gt;使用 Files 类的静态方法 &lt;code&gt;newByteChannel()&lt;/code&gt; 获取字节通道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Channel 基本操作：&lt;strong&gt;读写都是相对于内存来看，也就是缓冲区&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public abstract int read(ByteBuffer dst)&lt;/td&gt;
&lt;td&gt;从 Channel 中读取数据到 ByteBuffer，从 position 开始储存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final long read(ByteBuffer[] dsts)&lt;/td&gt;
&lt;td&gt;将 Channel 中的数据分散到 ByteBuffer[]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract int write(ByteBuffer src)&lt;/td&gt;
&lt;td&gt;将 ByteBuffer 中的数据写入 Channel，从 position 开始写出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final long write(ByteBuffer[] srcs)&lt;/td&gt;
&lt;td&gt;将 ByteBuffer[] 到中的数据聚集到 Channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract long position()&lt;/td&gt;
&lt;td&gt;返回此通道的文件位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FileChannel position(long newPosition)&lt;/td&gt;
&lt;td&gt;设置此通道的文件位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract long size()&lt;/td&gt;
&lt;td&gt;返回此通道的文件的当前大小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;SelectableChannel 的操作 API&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SocketChannel accept()&lt;/td&gt;
&lt;td&gt;如果通道处于非阻塞模式，没有请求连接时此方法将立即返回 NULL，否则将阻塞直到有新的连接或发生 I/O 错误，&lt;strong&gt;通过该方法返回的套接字通道将处于阻塞模式&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SelectionKey register(Selector sel, int ops)&lt;/td&gt;
&lt;td&gt;将通道注册到选择器上，并指定监听事件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SelectionKey register(Selector sel, int ops, Object att)&lt;/td&gt;
&lt;td&gt;将通道注册到选择器上，并在当前通道&lt;strong&gt;绑定一个附件对象&lt;/strong&gt;，Object 代表可以是任何类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;文件读写&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class ChannelTest {
    @Test
	public void write() throws Exception{
 		// 1、字节输出流通向目标文件
        FileOutputStream fos = new FileOutputStream(&quot;data01.txt&quot;);
        // 2、得到字节输出流对应的通道  【FileChannel】
        FileChannel channel = fos.getChannel();
        // 3、分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(&quot;hello,黑马Java程序员！&quot;.getBytes());
        // 4、把缓冲区切换成写出模式
        buffer.flip();
        channel.write(buffer);
        channel.close();
        System.out.println(&quot;写数据到文件中！&quot;);
    }
    @Test
    public void read() throws Exception {
        // 1、定义一个文件字节输入流与源文件接通
        FileInputStream fis = new FileInputStream(&quot;data01.txt&quot;);
        // 2、需要得到文件字节输入流的文件通道
        FileChannel channel = fis.getChannel();
        // 3、定义一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、读取数据到缓冲区
        channel.read(buffer);
        buffer.flip();
        // 5、读取出缓冲区中的数据并输出即可
        String rs = new String(buffer.array(),0,buffer.remaining());
        System.out.println(rs);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;文件复制&lt;/h4&gt;
&lt;p&gt;Channel 的方法：&lt;strong&gt;sendfile 实现零拷贝&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;abstract long transferFrom(ReadableByteChannel src, long position, long count)&lt;/code&gt;：从给定的可读字节通道将字节传输到该通道的文件中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src：源通道&lt;/li&gt;
&lt;li&gt;position：文件中要进行传输的位置，必须是非负的&lt;/li&gt;
&lt;li&gt;count：要传输的最大字节数，必须是非负的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;abstract long transferTo(long position, long count, WritableByteChannel target)&lt;/code&gt;：将该通道文件的字节传输到给定的可写字节通道。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;position：传输开始的文件中的位置; 必须是非负的&lt;/li&gt;
&lt;li&gt;count：要传输的最大字节数; 必须是非负的&lt;/li&gt;
&lt;li&gt;target：目标通道&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件复制的两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Buffer&lt;/li&gt;
&lt;li&gt;使用上述两种方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ChannelTest {
    @Test
    public void copy1() throws Exception {
        File srcFile = new File(&quot;C:\\壁纸.jpg&quot;);
        File destFile = new File(&quot;C:\\Users\\壁纸new.jpg&quot;);
        // 得到一个字节字节输入流
        FileInputStream fis = new FileInputStream(srcFile);
        // 得到一个字节输出流
        FileOutputStream fos = new FileOutputStream(destFile);
        // 得到的是文件通道
        FileChannel isChannel = fis.getChannel();
        FileChannel osChannel = fos.getChannel();
        // 分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while(true){
            // 必须先清空缓冲然后再写入数据到缓冲区
            buffer.clear();
            // 开始读取一次数据
            int flag = isChannel.read(buffer);
            if(flag == -1){
                break;
            }
            // 已经读取了数据 ，把缓冲区的模式切换成可读模式
            buffer.flip();
            // 把数据写出到
            osChannel.write(buffer);
        }
        isChannel.close();
        osChannel.close();
        System.out.println(&quot;复制完成！&quot;);
    }
    
	@Test
	public void copy02() throws Exception {
    	// 1、字节输入管道
   	 	FileInputStream fis = new FileInputStream(&quot;data01.txt&quot;);
   	 	FileChannel isChannel = fis.getChannel();
    	// 2、字节输出流管道
    	FileOutputStream fos = new FileOutputStream(&quot;data03.txt&quot;);
    	FileChannel osChannel = fos.getChannel();
    	// 3、复制
    	osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
    	isChannel.close();
    	osChannel.close();
	}
    
	@Test
	public void copy03() throws Exception {
    	// 1、字节输入管道
    	FileInputStream fis = new FileInputStream(&quot;data01.txt&quot;);
    	FileChannel isChannel = fis.getChannel();
    	// 2、字节输出流管道
    	FileOutputStream fos = new FileOutputStream(&quot;data04.txt&quot;);
    	FileChannel osChannel = fos.getChannel();
    	// 3、复制
    	isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
    	isChannel.close();
    	osChannel.close();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;分散聚集&lt;/h4&gt;
&lt;p&gt;分散读取（Scatter ）：是指把 Channel 通道的数据读入到多个缓冲区中去&lt;/p&gt;
&lt;p&gt;聚集写入（Gathering ）：是指将多个 Buffer 中的数据聚集到 Channel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ChannelTest {
    @Test
    public void test() throws IOException{
    	// 1、字节输入管道
        FileInputStream is = new FileInputStream(&quot;data01.txt&quot;);
        FileChannel isChannel = is.getChannel();
        // 2、字节输出流管道
        FileOutputStream fos = new FileOutputStream(&quot;data02.txt&quot;);
        FileChannel osChannel = fos.getChannel();
        // 3、定义多个缓冲区做数据分散
        ByteBuffer buffer1 = ByteBuffer.allocate(4);
        ByteBuffer buffer2 = ByteBuffer.allocate(1024);
        ByteBuffer[] buffers = {buffer1 , buffer2};
        // 4、从通道中读取数据分散到各个缓冲区
        isChannel.read(buffers);
        // 5、从每个缓冲区中查询是否有数据读取到了
        for(ByteBuffer buffer : buffers){
            buffer.flip();// 切换到读数据模式
            System.out.println(new String(buffer.array() , 0 , buffer.remaining()));
        }
        // 6、聚集写入到通道
        osChannel.write(buffers);
        isChannel.close();
        osChannel.close();
        System.out.println(&quot;文件复制~~&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;选择器&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;选择器（Selector） 是 SelectableChannle 对象的&lt;strong&gt;多路复用器&lt;/strong&gt;，Selector 可以同时监控多个通道的状况，利用 Selector 可使一个单独的线程管理多个 Channel，&lt;strong&gt;Selector 是非阻塞 IO 的核心&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Selector.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Selector 能够检测多个注册的通道上是否有事件发生（多个 Channel 以事件的方式可以注册到同一个 Selector)，如果有事件发生，就获取事件然后针对每个事件进行相应的处理，就可以只用一个单线程去管理多个通道，也就是管理多个连接和请求&lt;/li&gt;
&lt;li&gt;只有在连接/通道真正有读写事件发生时，才会进行读写，就大大地减少了系统开销，并且不必为每个连接都创建一个线程，不用去维护多个线程&lt;/li&gt;
&lt;li&gt;避免了多线程之间的上下文切换导致的开销&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;创建 Selector：&lt;code&gt;Selector selector = Selector.open();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;向选择器注册通道：&lt;code&gt;SelectableChannel.register(Selector sel, int ops, Object att)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数一：选择器，指定当前 Channel 注册到的选择器&lt;/li&gt;
&lt;li&gt;参数二：选择器对通道的监听事件，监听的事件类型用四个常量表示
&lt;ul&gt;
&lt;li&gt;读 : SelectionKey.OP_READ （1）&lt;/li&gt;
&lt;li&gt;写 : SelectionKey.OP_WRITE （4）&lt;/li&gt;
&lt;li&gt;连接 : SelectionKey.OP_CONNECT （8）&lt;/li&gt;
&lt;li&gt;接收 : SelectionKey.OP_ACCEPT （16）&lt;/li&gt;
&lt;li&gt;若不止监听一个事件，使用位或操作符连接：&lt;code&gt;int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参数三：可以关联一个附件，可以是任何对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Selector API&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public static Selector open()&lt;/td&gt;
&lt;td&gt;打开选择器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract void close()&lt;/td&gt;
&lt;td&gt;关闭此选择器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract int select()&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;阻塞&lt;/strong&gt;选择一组通道准备好进行 I/O 操作的键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract int select(long timeout)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;阻塞&lt;/strong&gt;等待 timeout 毫秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract int selectNow()&lt;/td&gt;
&lt;td&gt;获取一下，&lt;strong&gt;不阻塞&lt;/strong&gt;，立刻返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract Selector wakeup()&lt;/td&gt;
&lt;td&gt;唤醒正在阻塞的 selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract Set&amp;lt;SelectionKey&amp;gt; selectedKeys()&lt;/td&gt;
&lt;td&gt;返回此选择器的选择键集&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SelectionKey API:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public abstract void cancel()&lt;/td&gt;
&lt;td&gt;取消该键的通道与其选择器的注册&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract SelectableChannel channel()&lt;/td&gt;
&lt;td&gt;返回创建此键的通道，该方法在取消键之后仍将返回通道&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final Object attachment()&lt;/td&gt;
&lt;td&gt;返回当前 key 关联的附件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final boolean isAcceptable()&lt;/td&gt;
&lt;td&gt;检测此密钥的通道是否已准备好接受新的套接字连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final boolean isConnectable()&lt;/td&gt;
&lt;td&gt;检测此密钥的通道是否已完成或未完成其套接字连接操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final boolean isReadable()&lt;/td&gt;
&lt;td&gt;检测此密钥的频道是否可以阅读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final boolean isWritable()&lt;/td&gt;
&lt;td&gt;检测此密钥的通道是否准备好进行写入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;基本步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2.切换非阻塞模式
ssChannel.configureBlocking(false);
//3.绑定连接
ssChannel.bin(new InetSocketAddress(9999));
//4.获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器上，并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;NIO实现&lt;/h3&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SelectableChannel_API&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public final SelectableChannel configureBlocking(boolean block)&lt;/td&gt;
&lt;td&gt;设置此通道的阻塞模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final SelectionKey register(Selector sel, int ops)&lt;/td&gt;
&lt;td&gt;向给定的选择器注册此通道，并选择关注的的事件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SocketChannel_API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public static SocketChannel open()&lt;/td&gt;
&lt;td&gt;打开套接字通道&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static SocketChannel open(SocketAddress remote)&lt;/td&gt;
&lt;td&gt;打开套接字通道并连接到远程地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract boolean connect(SocketAddress remote)&lt;/td&gt;
&lt;td&gt;连接此通道的到远程地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract SocketChannel bind(SocketAddress local)&lt;/td&gt;
&lt;td&gt;将通道的套接字绑定到本地地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract SocketAddress getLocalAddress()&lt;/td&gt;
&lt;td&gt;返回套接字绑定的本地套接字地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract SocketAddress getRemoteAddress()&lt;/td&gt;
&lt;td&gt;返回套接字连接的远程套接字地址&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServerSocketChannel_API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public static ServerSocketChannel open()&lt;/td&gt;
&lt;td&gt;打开服务器套接字通道&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final ServerSocketChannel bind(SocketAddress local)&lt;/td&gt;
&lt;td&gt;将通道的套接字绑定到本地地址，并配置套接字以监听连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public abstract SocketChannel accept()&lt;/td&gt;
&lt;td&gt;接受与此通道套接字的连接，通过此方法返回的套接字通道将处于阻塞模式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;如果 ServerSocketChannel 处于非阻塞模式，如果没有挂起连接，则此方法将立即返回 null&lt;/li&gt;
&lt;li&gt;如果通道处于阻塞模式，如果没有挂起连接将无限期地阻塞，直到有新的连接或发生 I/O 错误&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;服务端 ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取通道，当客户端连接服务端时，服务端会通过 &lt;code&gt;ServerSocketChannel.accept&lt;/code&gt; 得到 SocketChannel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换非阻塞模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取选择器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将通道注册到选择器上，并且指定监听接收事件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;轮询式&lt;/strong&gt;的获取选择器上已经准备就绪的事件&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;客户端：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取通道：&lt;code&gt;SocketChannel sc = SocketChannel.open(new InetSocketAddress(HOST, PORT))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;切换非阻塞模式&lt;/li&gt;
&lt;li&gt;分配指定大小的缓冲区：&lt;code&gt;ByteBuffer buffer = ByteBuffer.allocate(1024)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;发送数据给服务端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;37 行代码，如果判断条件改为 !=-1，需要客户端 close 一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Server {
    public static void main(String[] args){
        // 1、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2、切换为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 3、绑定连接的端口
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 4、获取选择器Selector
        Selector selector = Selector.open();
        // 5、将通道都注册到选择器上去，并且开始指定监听接收事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		// 6、使用Selector选择器阻塞等待轮已经就绪好的事件
        while (selector.select() &amp;gt; 0) {
            System.out.println(&quot;----开始新一轮的时间处理----&quot;);
            // 7、获取选择器中的所有注册的通道中已经就绪好的事件
            Set&amp;lt;SelectionKey&amp;gt; selectionKeys = selector.selectedKeys();
            Iterator&amp;lt;SelectionKey&amp;gt; it = selectionKeys.iterator();
            // 8、开始遍历这些准备好的事件
            while (it.hasNext()) {
                SelectionKey key = it.next();// 提取当前这个事件
                // 9、判断这个事件具体是什么
                if (key.isAcceptable()) {
                    // 10、直接获取当前接入的客户端通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 11 、切换成非阻塞模式
                    socketChannel.configureBlocking(false);
                    /*
                     ByteBuffer buffer = ByteBuffer.allocate(16);
                	 // 将一个 byteBuffer 作为附件【关联】到 selectionKey 上
                	 SelectionKey scKey = sc.register(selector, 0, buffer);
                    */
                    // 12、将本客户端通道注册到选择器
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 13、获取当前选择器上的读就绪事件
                    SelectableChannel channel = key.channel();
                    SocketChannel socketChannel = (SocketChannel) channel;
                    // 14、读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 获取关联的附件
                    // ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int len;
                    while ((len = socketChannel.read(buffer)) &amp;gt; 0) {
                        buffer.flip();
                        System.out.println(socketChannel.getRemoteAddress() + &quot;:&quot; + new String(buffer.array(), 0, len));
                        buffer.clear();// 清除之前的数据
                    }
                }
                // 删除当前的 selectionKey，防止重复操作
                it.remove();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) throws Exception {
        // 1、获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(&quot;127.0.0.1&quot;, 9999));
        // 2、切换成非阻塞模式
        socketChannel.configureBlocking(false);
        // 3、分配指定缓冲区大小
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、发送数据给服务端
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.print(&quot;请说：&quot;);
            String msg = sc.nextLine();
            buffer.put((&quot;Client：&quot; + msg).getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;AIO&lt;/h2&gt;
&lt;p&gt;Java AIO(NIO.2) ： AsynchronousI/O，异步非阻塞，采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程，客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AIO异步非阻塞，基于NIO的，可以称之为NIO2.0
  BIO                     NIO                                AIO        
Socket                SocketChannel                    AsynchronousSocketChannel
ServerSocket          ServerSocketChannel	       AsynchronousServerSocketChannel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当进行读写操作时，调用 API 的 read 或 write 方法，这两种方法均为异步的，完成后会主动调用回调函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于读操作，当有流可读取时，操作系统会将可读的流传入 read 方法的缓冲区&lt;/li&gt;
&lt;li&gt;对于写操作，当操作系统将 write 方法传递的流写入完毕时，操作系统主动通知应用程序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 JDK1.7 中，这部分内容被称作 NIO.2，主要在 Java.nio.channels 包下增加了下面四个异步通道：
AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>数据库笔记合集</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/db/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/db/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;h3&gt;数据库&lt;/h3&gt;
&lt;p&gt;数据库：DataBase，简称 DB，存储和管理数据的仓库&lt;/p&gt;
&lt;p&gt;数据库的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以持久化存储数据&lt;/li&gt;
&lt;li&gt;方便存储和管理数据&lt;/li&gt;
&lt;li&gt;使用了统一的方式操作数据库 SQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据库、数据表、数据的关系介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于存储和管理数据的仓库&lt;/li&gt;
&lt;li&gt;一个库中可以包含多个数据表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库最重要的组成部分之一&lt;/li&gt;
&lt;li&gt;由纵向的列和横向的行组成（类似 excel 表格）&lt;/li&gt;
&lt;li&gt;可以指定列名、数据类型、约束等&lt;/li&gt;
&lt;li&gt;一个表中可以存储多条数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据：想要永久化存储的数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1zJ411M7TB&lt;/p&gt;
&lt;p&gt;参考专栏：https://time.geekbang.org/column/intro/139&lt;/p&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/35231266/&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;MySQL&lt;/h3&gt;
&lt;p&gt;MySQL 数据库是一个最流行的关系型数据库管理系统之一，关系型数据库是将数据保存在不同的数据表中，而且表与表之间可以有关联关系，提高了灵活性&lt;/p&gt;
&lt;p&gt;缺点：数据存储在磁盘中，导致读写性能差，而且数据关系复杂，扩展性差&lt;/p&gt;
&lt;p&gt;MySQL 所使用的 SQL 语句是用于访问数据库最常用的标准化语言&lt;/p&gt;
&lt;p&gt;MySQL 配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 安装：https://www.jianshu.com/p/ba48f1e386f0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改 MySQL 默认字符集：安装 MySQL 之后第一件事就是修改字符集编码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/mysql/my.cnf

添加如下内容：
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci

[client]
default-character-set=utf8
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 MySQL 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start/restart mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录 MySQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p  敲回车，输入密码
初始密码查看：cat /var/log/mysqld.log
在root@localhost:   后面的就是初始密码
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看默认字符集命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;char%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改MySQL登录密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set global validate_password_policy=0;
set global validate_password_length=1;
  
set password=password(&apos;密码&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;授予远程连接权限（MySQL 内输入）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 授权
grant all privileges on *.* to &apos;root&apos; @&apos;%&apos; identified by &apos;密码&apos;;
-- 刷新
flush privileges;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 MySQL 绑定 IP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/mysql/mysql.conf.d
sudo chmod 666 mysqld.cnf 
vim mysqld.cnf 
# bind-address = 127.0.0.1注释该行
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭 Linux 防火墙&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop firewalld.service
# 放行3306端口
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;体系架构&lt;/h2&gt;
&lt;h3&gt;整体架构&lt;/h3&gt;
&lt;p&gt;体系结构详解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一层：网络连接层
&lt;ul&gt;
&lt;li&gt;一些客户端和链接服务，包含本地 Socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信，主要完成一些类似于连接处理、授权认证、及相关的安全方案&lt;/li&gt;
&lt;li&gt;在该层上引入了&lt;strong&gt;连接池&lt;/strong&gt; Connection Pool 的概念，管理缓冲用户连接，线程处理等需要缓存的需求&lt;/li&gt;
&lt;li&gt;在该层上实现基于 SSL 的安全链接，服务器也会为安全接入的每个客户端验证它所具有的操作权限&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;第二层：核心服务层
&lt;ul&gt;
&lt;li&gt;查询缓存、分析器、优化器、执行器等，涵盖 MySQL 的大多数核心服务功能，所有的内置函数（日期、数学、加密函数等）
&lt;ul&gt;
&lt;li&gt;Management Serveices &amp;amp; Utilities：系统管理和控制工具，备份、安全、复制、集群等&lt;/li&gt;
&lt;li&gt;SQL Interface：接受用户的 SQL 命令，并且返回用户需要查询的结果&lt;/li&gt;
&lt;li&gt;Parser：SQL 语句分析器&lt;/li&gt;
&lt;li&gt;Optimizer：查询优化器&lt;/li&gt;
&lt;li&gt;Caches &amp;amp; Buffers：查询缓存，服务器会查询内部的缓存，如果缓存空间足够大，可以在大量读操作的环境中提升系统性能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所有&lt;strong&gt;跨存储引擎的功能&lt;/strong&gt;在这一层实现，如存储过程、触发器、视图等&lt;/li&gt;
&lt;li&gt;在该层服务器会解析查询并创建相应的内部解析树，并对其完成相应的优化如确定表的查询顺序，是否利用索引等， 最后生成相应的执行操作&lt;/li&gt;
&lt;li&gt;MySQL 中服务器层不管理事务，&lt;strong&gt;事务是由存储引擎实现的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第三层：存储引擎层
&lt;ul&gt;
&lt;li&gt;Pluggable Storage Engines：存储引擎接口，MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的（存储引擎是基于表的，而不是数据库）&lt;/li&gt;
&lt;li&gt;存储引擎&lt;strong&gt;真正的负责了 MySQL 中数据的存储和提取&lt;/strong&gt;，服务器通过 API 和存储引擎进行通信&lt;/li&gt;
&lt;li&gt;不同的存储引擎具有不同的功能，共用一个 Server 层，可以根据开发的需要，来选取合适的存储引擎&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第四层：系统文件层
&lt;ul&gt;
&lt;li&gt;数据存储层，主要是将数据存储在文件系统之上，并完成与存储引擎的交互&lt;/li&gt;
&lt;li&gt;File System：文件系统，保存配置文件、数据文件、日志文件、错误文件、二进制文件等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;建立连接&lt;/h3&gt;
&lt;h4&gt;连接器&lt;/h4&gt;
&lt;p&gt;池化技术：对于访问数据库来说，建立连接的代价是比较昂贵的，因为每个连接对应一个用来交互的线程，频繁的创建关闭连接比较耗费资源，有必要建立数据库连接池，以提高访问的性能&lt;/p&gt;
&lt;p&gt;连接建立 TCP 以后需要做&lt;strong&gt;权限验证&lt;/strong&gt;，验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改，也不会影响已经存在连接的权限，只有再新建的连接才会使用新的权限设置&lt;/p&gt;
&lt;p&gt;MySQL 服务器可以同时和多个客户端进行交互，所以要保证每个连接会话的隔离性（事务机制部分详解）&lt;/p&gt;
&lt;p&gt;整体的执行流程：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL的执行流程.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;权限信息&lt;/h4&gt;
&lt;p&gt;grant 语句会同时修改数据表和内存，判断权限的时候使用的是内存数据&lt;/p&gt;
&lt;p&gt;flush privileges 语句本身会用数据表（磁盘）的数据重建一份内存权限数据，所以在权限数据可能存在不一致的情况下使用，这种不一致往往是由于直接用 DML 语句操作系统权限表导致的，所以尽量不要使用这类语句&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%9D%83%E9%99%90%E8%8C%83%E5%9B%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;连接状态&lt;/h4&gt;
&lt;p&gt;客户端如果长时间没有操作，连接器就会自动断开，时间是由参数 wait_timeout 控制的，默认值是 8 小时。如果在连接被断开之后，客户端&lt;strong&gt;再次发送请求&lt;/strong&gt;的话，就会收到一个错误提醒：&lt;code&gt;Lost connection to MySQL server during query&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数据库里面，长连接是指连接成功后，如果客户端持续有请求，则一直使用同一个连接；短连接则是指每次执行完很少的几次查询就断开连接，下次查询再重新建立一个&lt;/p&gt;
&lt;p&gt;为了减少连接的创建，推荐使用长连接，但是&lt;strong&gt;过多的长连接会造成 OOM&lt;/strong&gt;，解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定期断开长连接，使用一段时间，或者程序里面判断执行过一个占用内存的大查询后，断开连接，之后要查询再重连&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KILL CONNECTION id
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 5.7 版本，可以在每次执行一个比较大的操作后，通过执行 mysql_reset_connection 来重新初始化连接资源，这个过程不需要重连和重新做权限验证，但是会将连接恢复到刚刚创建完时的状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SHOW PROCESSLIST：查看当前 MySQL 在进行的线程，可以实时地查看 SQL 的执行情况，其中的 Command 列显示为 Sleep 的这一行，就表示现在系统里面有一个空闲连接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST%E5%91%BD%E4%BB%A4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;用户登录 mysql 时系统分配的 connection_id，可以使用函数 connection_id() 查看&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;显示当前用户，如果不是 root，这个命令就只显示用户权限范围的 sql 语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;显示这个语句是从哪个 ip 的哪个端口上发的，可以用来跟踪出现问题语句的用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db&lt;/td&gt;
&lt;td&gt;显示这个进程目前连接的是哪个数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command&lt;/td&gt;
&lt;td&gt;显示当前连接的执行的命令，一般取值为休眠 Sleep、查询 Query、连接 Connect 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;显示这个状态持续的时间，单位是秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;显示使用当前连接的 sql 语句的状态，以查询为例，需要经过 copying to tmp table、sorting result、sending data等状态才可以完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;显示执行的 sql 语句，是判断问题语句的一个重要依据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Sending data 状态&lt;/strong&gt;表示 MySQL 线程开始访问数据行并把结果返回给客户端，而不仅仅只是返回给客户端，是处于执行器过程中的任意阶段。由于在 Sending data 状态下，MySQL 线程需要做大量磁盘读取操作，所以是整个查询中耗时最长的状态&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;执行流程&lt;/h3&gt;
&lt;h4&gt;查询缓存&lt;/h4&gt;
&lt;h5&gt;工作流程&lt;/h5&gt;
&lt;p&gt;当执行完全相同的 SQL 语句的时候，服务器就会直接从缓存中读取结果，当数据被修改，之前的缓存会失效，修改比较频繁的表不适合做查询缓存&lt;/p&gt;
&lt;p&gt;查询过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发送一条查询给服务器&lt;/li&gt;
&lt;li&gt;服务器先会检查查询缓存，如果命中了缓存，则立即返回存储在缓存中的结果（一般是 K-V 键值对），否则进入下一阶段&lt;/li&gt;
&lt;li&gt;分析器进行 SQL 分析，再由优化器生成对应的执行计划&lt;/li&gt;
&lt;li&gt;执行器根据优化器生成的执行计划，调用存储引擎的 API 来执行查询&lt;/li&gt;
&lt;li&gt;将结果返回给客户端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;大多数情况下不建议使用查询缓存，因为查询缓存往往弊大于利&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询缓存的&lt;strong&gt;失效非常频繁&lt;/strong&gt;，只要有对一个表的更新，这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来，还没使用就被一个更新全清空了，对于更新压力大的数据库来说，查询缓存的命中率会非常低&lt;/li&gt;
&lt;li&gt;除非业务就是有一张静态表，很长时间才会更新一次，比如一个系统配置表，那这张表上的查询才适合使用查询缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存配置&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看当前 MySQL 数据库是否支持查询缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;have_query_cache&apos;;	-- YES
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看当前 MySQL 是否开启了查询缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;query_cache_type&apos;;	-- OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OFF 或 0：查询缓存功能关闭&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ON 或 1：查询缓存功能打开，查询结果符合缓存条件即会缓存，否则不予缓存；可以显式指定 SQL_NO_CACHE 不予缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DEMAND 或 2：查询缓存功能按需进行，显式指定 SQL_CACHE 的 SELECT 语句才缓存，其它不予缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT SQL_CACHE id, name FROM customer; -- SQL_CACHE:查询结果可缓存
SELECT SQL_NO_CACHE id, name FROM customer;-- SQL_NO_CACHE:不使用查询缓存
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看查询缓存的占用大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;query_cache_size&apos;;-- 单位是字节 1048576 / 1024 = 1024 = 1KB
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看查询缓存的状态变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;Qcache%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查询缓存的状态变量.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_free_blocks&lt;/td&gt;
&lt;td&gt;查询缓存中的可用内存块数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_free_memory&lt;/td&gt;
&lt;td&gt;查询缓存的可用内存量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_hits&lt;/td&gt;
&lt;td&gt;查询缓存命中数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_inserts&lt;/td&gt;
&lt;td&gt;添加到查询缓存的查询数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_lowmen_prunes&lt;/td&gt;
&lt;td&gt;由于内存不足而从查询缓存中删除的查询数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_not_cached&lt;/td&gt;
&lt;td&gt;非缓存查询的数量（由于 query_cache_type 设置而无法缓存或未缓存）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_queries_in_cache&lt;/td&gt;
&lt;td&gt;查询缓存中注册的查询数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qcache_total_blocks&lt;/td&gt;
&lt;td&gt;查询缓存中的块总数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 my.cnf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chmod 666 /etc/mysql/my.cnf
vim my.cnf
# mysqld中配置缓存
query_cache_type=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启服务既可生效，执行 SQL 语句进行验证 ，执行一条比较耗时的 SQL 语句，然后再多执行几次，查看后面几次的执行时间；获取通过查看查询缓存的缓存命中数，来判定是否走查询缓存&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存失效&lt;/h5&gt;
&lt;p&gt;查询缓存失效的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL 语句不一致，要想命中查询缓存，查询的 SQL 语句必须一致，因为&lt;strong&gt;缓存中 key 是查询的语句&lt;/strong&gt;，value 是查询结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select count(*) from tb_item;
Select count(*) from tb_item;	-- 不走缓存，首字母不一致
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当查询语句中有一些不确定查询时，则不会缓存，比如：now()、current_date()、curdate()、curtime()、rand()、uuid()、user()、database()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM tb_item WHERE updatetime &amp;lt; NOW() LIMIT 1;
SELECT USER();
SELECT DATABASE();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不使用任何表查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT &apos;A&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询 mysql、information_schema、performance_schema 等系统表时，不走查询缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.engines;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;strong&gt;跨存储引擎&lt;/strong&gt;的存储过程、触发器或存储函数的主体内执行的查询，缓存失效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果表更改，则使用该表的&lt;strong&gt;所有高速缓存查询都将变为无效&lt;/strong&gt;并从高速缓存中删除，包括使用 MERGE 映射到已更改表的表的查询，比如：INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分析器&lt;/h4&gt;
&lt;p&gt;没有命中查询缓存，就开始了 SQL 的真正执行，分析器会对 SQL 语句做解析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM t WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析器：处理语法和解析查询，生成一课对应的解析树&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先做&lt;strong&gt;词法分析&lt;/strong&gt;，输入的是由多个字符串和空格组成的一条 SQL 语句，MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句；把字符串 t 识别成 表名 t，把字符串 id 识别成列 id&lt;/li&gt;
&lt;li&gt;然后做&lt;strong&gt;语法分析&lt;/strong&gt;，根据词法分析的结果，语法分析器会根据语法规则，判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对，就会收到 &lt;code&gt;You have an error in your SQL syntax&lt;/code&gt; 的错误提醒&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;预处理器：进一步检查解析树的合法性，比如数据表和数据列是否存在、别名是否有歧义等&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;优化器&lt;/h4&gt;
&lt;h5&gt;成本分析&lt;/h5&gt;
&lt;p&gt;优化器是在表里面有多个索引的时候，决定使用哪个索引；或者在一个语句有多表关联（join）的时候，决定各个表的连接顺序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据搜索条件找出所有可能的使用的索引&lt;/li&gt;
&lt;li&gt;成本分析，执行成本由 I/O 成本和 CPU 成本组成，计算全表扫描和使用不同索引执行 SQL 的代价&lt;/li&gt;
&lt;li&gt;找到一个最优的执行方案，用最小的代价去执行语句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在数据库里面，扫描行数是影响执行代价的因素之一，扫描的行数越少意味着访问磁盘的次数越少，消耗的 CPU 资源越少，优化器还会结合是否使用临时表、是否排序等因素进行综合判断&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;统计数据&lt;/h5&gt;
&lt;p&gt;MySQL 中保存着两种统计数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;innodb_table_stats 存储了表的统计数据，每一条记录对应着一个表的统计数据&lt;/li&gt;
&lt;li&gt;innodb_index_stats 存储了索引的统计数据，每一条记录对应着一个索引的一个统计项的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 在真正执行语句之前，并不能精确地知道满足条件的记录有多少条，只能根据统计信息来估算记录，统计信息就是索引的区分度，一个索引上不同的值的个数（比如性别只能是男女，就是 2 ），称之为基数（cardinality），&lt;strong&gt;基数越大说明区分度越好&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过&lt;strong&gt;采样统计&lt;/strong&gt;来获取基数，InnoDB 默认会选择 N 个数据页，统计这些页面上的不同值得到一个平均值，然后乘以这个索引的页面数，就得到了这个索引的基数&lt;/p&gt;
&lt;p&gt;在 MySQL 中，有两种存储统计数据的方式，可以通过设置参数 &lt;code&gt;innodb_stats_persistent&lt;/code&gt; 的值来选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ON：表示统计信息会持久化存储（默认），采样页数 N 默认为 20，可以通过 &lt;code&gt;innodb_stats_persistent_sample_pages&lt;/code&gt; 指定，页数越多统计的数据越准确，但消耗的资源更大&lt;/li&gt;
&lt;li&gt;OFF：表示统计信息只存储在内存，采样页数 N 默认为 8，也可以通过系统变量设置（不推荐，每次重新计算浪费资源）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据表是会持续更新的，两种统计信息的更新方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 &lt;code&gt;innodb_stats_auto_recalc&lt;/code&gt; 为 1，当发生变动的记录数量超过表大小的 10% 时，自动触发重新计算，不过是&lt;strong&gt;异步进行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;ANALYZE TABLE t&lt;/code&gt; 手动更新统计信息，只对信息做&lt;strong&gt;重新统计&lt;/strong&gt;（不是重建表），没有修改数据，这个过程中加了 MDL 读锁并且是同步进行，所以会暂时阻塞系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;EXPLAIN 执行计划在优化器阶段生成&lt;/strong&gt;，如果 explain 的结果预估的 rows 值跟实际情况差距比较大，可以执行 analyze 命令重新修正信息&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;错选索引&lt;/h5&gt;
&lt;p&gt;采样统计本身是估算数据，或者 SQL 语句中的字段选择有问题时，可能导致 MySQL 没有选择正确的执行索引&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;采用 force index 强行选择一个索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM user FORCE INDEX(name) WHERE NAME=&apos;seazean&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以考虑修改 SQL 语句，引导 MySQL 使用期望的索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新建一个更合适的索引，来提供给优化器做选择，或删掉误用的索引&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;执行器&lt;/h4&gt;
&lt;p&gt;开始执行的时候，要先判断一下当前连接对表有没有&lt;strong&gt;执行查询的权限&lt;/strong&gt;，如果没有就会返回没有权限的错误，在工程实现上，如果命中查询缓存，会在查询缓存返回结果的时候，做权限验证。如果有权限，就打开表继续执行，执行器就会根据表的引擎定义，去使用这个引擎提供的接口&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;引擎层&lt;/h4&gt;
&lt;p&gt;Server 层和存储引擎层的交互是&lt;strong&gt;以记录为单位的&lt;/strong&gt;，存储引擎会将单条记录返回给 Server 层做进一步处理，并不是直接返回所有的记录&lt;/p&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先根据二级索引选择扫描范围，获取第一条符合二级索引条件的记录，进行回表查询，将聚簇索引的记录返回 Server 层，由 Server 判断记录是否符合要求&lt;/li&gt;
&lt;li&gt;然后在二级索引上继续扫描下一个符合条件的记录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐阅读：https://mp.weixin.qq.com/s/YZ-LckObephrP1f15mzHpA&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;终止流程&lt;/h3&gt;
&lt;h4&gt;终止语句&lt;/h4&gt;
&lt;p&gt;终止线程中正在执行的语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KILL QUERY thread_id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KILL 不是马上终止的意思，而是告诉执行线程这条语句已经不需要继续执行，可以开始执行停止的逻辑（类似于打断）。因为对表做增删改查操作，会在表上加 MDL 读锁，如果线程被 KILL 时就直接终止，那这个 MDL 读锁就没机会被释放了&lt;/p&gt;
&lt;p&gt;命令 &lt;code&gt;KILL QUERYthread_id_A&lt;/code&gt; 的执行流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 session A 的运行状态改成 THD::KILL_QUERY（将变量 killed 赋值为 THD::KILL_QUERY）&lt;/li&gt;
&lt;li&gt;给 session A 的执行线程发一个信号，让 session A 来处理这个 THD::KILL_QUERY 状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;会话处于等待状态（锁阻塞），必须满足是一个可以被唤醒的等待，必须有机会去&lt;strong&gt;判断线程的状态&lt;/strong&gt;，如果不满足就会造成 KILL 失败&lt;/p&gt;
&lt;p&gt;典型场景：innodb_thread_concurrency 为 2，代表并发线程上限数设置为 2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;session A 执行事务，session B 执行事务，达到线程上限；此时 session C 执行事务会阻塞等待，session D 执行 kill query C 无效&lt;/li&gt;
&lt;li&gt;C 的逻辑是每 10 毫秒判断是否可以进入 InnoDB 执行，如果不行就调用 nanosleep 函数进入 sleep 状态，没有去判断线程状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：执行 Ctrl+C 的时候，是 MySQL 客户端另外启动一个连接，然后发送一个 KILL QUERY 命令&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;终止连接&lt;/h4&gt;
&lt;p&gt;断开线程的连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KILL CONNECTION id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;断开连接后执行 SHOW PROCESSLIST 命令，如果这条语句的 Command 列显示 Killed，代表线程的状态是 KILL_CONNECTION，说明这个线程有语句正在执行，当前状态是停止语句执行中，终止逻辑耗时较长&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超大事务执行期间被 KILL，这时回滚操作需要对事务执行期间生成的所有新数据版本做回收操作，耗时很长&lt;/li&gt;
&lt;li&gt;大查询回滚，如果查询过程中生成了比较大的临时文件，删除临时文件可能需要等待 IO 资源，导致耗时较长&lt;/li&gt;
&lt;li&gt;DDL 命令执行到最后阶段被 KILL，需要删除中间过程的临时文件，也可能受 IO 资源影响耗时较久&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：KILL CONNECTION 本质上只是把客户端的 SQL 连接断开，后面的终止流程还是要走 KILL QUERY&lt;/p&gt;
&lt;p&gt;一个事务被 KILL 之后，持续处于回滚状态，不应该强行重启整个 MySQL 进程，应该等待事务自己执行完成，因为重启后依然继续做回滚操作的逻辑&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;常用工具&lt;/h3&gt;
&lt;h4&gt;mysql&lt;/h4&gt;
&lt;p&gt;mysql 不是指 mysql 服务，而是指 mysql 的客户端工具&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql [options] [database]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-u  --user=name：指定用户名&lt;/li&gt;
&lt;li&gt;-p  --password[=name]：指定密码&lt;/li&gt;
&lt;li&gt;-h  --host=name：指定服务器IP或域名&lt;/li&gt;
&lt;li&gt;-P  --port=#：指定连接端口&lt;/li&gt;
&lt;li&gt;-e  --execute=name：执行SQL语句并退出，在控制台执行SQL语句，而不用连接到数据库执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -h 127.0.0.1 -P 3306 -u root -p
mysql -uroot -p2143 db01 -e &quot;select * from tb_book&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;admin&lt;/h4&gt;
&lt;p&gt;mysqladmin 是一个执行管理操作的客户端程序，用来检查服务器的配置和当前状态、创建并删除数据库等&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;mysqladmin --help&lt;/code&gt; 指令查看帮助文档&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqladmin -uroot -p2143 create &apos;test01&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;binlog&lt;/h4&gt;
&lt;p&gt;服务器生成的日志文件以二进制格式保存，如果需要检查这些文本，就要使用 mysqlbinlog 日志管理工具&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog [options]  log-files1 log-files2 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;-d  --database=name：指定数据库名称，只列出指定的数据库相关操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-o  --offset=#：忽略掉日志中的前 n 行命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-r  --result-file=name：将输出的文本格式日志输出到指定文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-s  --short-form：显示简单格式，省略掉一些信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;--start-datatime=date1  --stop-datetime=date2：指定日期间隔内的所有日志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;--start-position=pos1 --stop-position=pos2：指定位置间隔内的所有日志&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;dump&lt;/h4&gt;
&lt;h5&gt;命令介绍&lt;/h5&gt;
&lt;p&gt;mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移，备份内容包含创建表，及插入表的 SQL 语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-u  --user=name：指定用户名&lt;/li&gt;
&lt;li&gt;-p  --password[=name]：指定密码&lt;/li&gt;
&lt;li&gt;-h  --host=name：指定服务器 IP 或域名&lt;/li&gt;
&lt;li&gt;-P  --port=#：指定连接端口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;输出内容选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;--add-drop-database：在每个数据库创建语句前加上 Drop database 语句&lt;/li&gt;
&lt;li&gt;--add-drop-table：在每个表创建语句前加上 Drop table 语句 , 默认开启，不开启 (--skip-add-drop-table)&lt;/li&gt;
&lt;li&gt;-n  --no-create-db：不包含数据库的创建语句&lt;/li&gt;
&lt;li&gt;-t  --no-create-info：不包含数据表的创建语句&lt;/li&gt;
&lt;li&gt;-d --no-data：不包含数据&lt;/li&gt;
&lt;li&gt;-T, --tab=name：自动生成两个文件：一个 .sql 文件，创建表结构的语句；一个 .txt 文件，数据文件，相当于 select into outfile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table &amp;gt; a
mysqldump -uroot -p2143 -T /tmp test city
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;数据备份&lt;/h5&gt;
&lt;p&gt;命令行方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;备份命令：mysqldump -u root -p 数据库名称 &amp;gt; 文件保存路径&lt;/li&gt;
&lt;li&gt;恢复
&lt;ol&gt;
&lt;li&gt;登录MySQL数据库：&lt;code&gt;mysql -u root p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除已经备份的数据库&lt;/li&gt;
&lt;li&gt;重新创建与备份数据库名称相同的数据库&lt;/li&gt;
&lt;li&gt;使用该数据库&lt;/li&gt;
&lt;li&gt;导入文件执行：&lt;code&gt;source 备份文件全路径&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更多方式参考：https://time.geekbang.org/column/article/81925&lt;/p&gt;
&lt;p&gt;图形化界面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;备份&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%9B%BE%E5%BD%A2%E5%8C%96%E7%95%8C%E9%9D%A2%E5%A4%87%E4%BB%BD.png&quot; alt=&quot;图形化界面备份&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恢复&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%9B%BE%E5%BD%A2%E5%8C%96%E7%95%8C%E9%9D%A2%E6%81%A2%E5%A4%8D.png&quot; alt=&quot;图形化界面恢复&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;import&lt;/h4&gt;
&lt;p&gt;mysqlimport 是客户端数据导入工具，用来导入mysqldump 加 -T 参数后导出的文本文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlimport [options]  db_name  textfile1  [textfile2...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlimport -uroot -p2143 test /tmp/city.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入 sql 文件，可以使用 MySQL 中的 source 指令 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source 文件全路径
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;show&lt;/h4&gt;
&lt;p&gt;mysqlshow 客户端对象查找工具，用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlshow [options] [db_name [table_name [col_name]]]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;--count：显示数据库及表的统计信息（数据库，表 均可以不指定）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-i：显示指定数据库或者指定表的状态信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#查询每个数据库的表的数量及表中记录的数量
mysqlshow -uroot -p1234 --count
#查询test库中每个表中的字段书，及行数
mysqlshow -uroot -p1234 test --count
#查询test库中book表的详细情况
mysqlshow -uroot -p1234 test book --count
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;单表操作&lt;/h2&gt;
&lt;h3&gt;SQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured Query Language：结构化查询语言&lt;/li&gt;
&lt;li&gt;定义了操作所有关系型数据库的规则，每种数据库操作的方式可能会存在不一样的地方，称为“方言”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL 通用语法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL 语句可以单行或多行书写，以&lt;strong&gt;分号结尾&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;可使用空格和缩进来增强语句的可读性。&lt;/li&gt;
&lt;li&gt;MySQL 数据库的 SQL 语句不区分大小写，&lt;strong&gt;关键字建议使用大写&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;数据库的注释：
&lt;ul&gt;
&lt;li&gt;单行注释：-- 注释内容       #注释内容（MySQL 特有）&lt;/li&gt;
&lt;li&gt;多行注释：/* 注释内容 */&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL 分类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DDL（Data Definition Language）数据定义语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用来定义数据库对象：数据库，表，列等。关键字：create、drop,、alter 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DML（Data Manipulation Language）数据操作语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用来对数据库中表的数据进行增删改。关键字：insert、delete、update 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DQL（Data Query Language）数据查询语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用来查询数据库中表的记录(数据)。关键字：select、where 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DCL（Data Control Language）数据控制语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用来定义数据库的访问权限和安全级别，及创建用户。关键字：grant， revoke等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL%E5%88%86%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;DDL&lt;/h3&gt;
&lt;h4&gt;数据库&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;R(Retrieve)：查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询所有数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW DATABASES;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询某个数据库的创建语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW CREATE DATABASE 数据库名称;  -- 标准语法

SHOW CREATE DATABASE mysql;     -- 查看mysql数据库的创建格式
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C(Create)：创建&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE 数据库名称;-- 标准语法

CREATE DATABASE db1;     -- 创建db1数据库
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据库（判断，如果不存在则创建）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE IF NOT EXISTS 数据库名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据库，并指定字符集&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE 数据库名称 CHARACTER SET 字符集名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：创建db4数据库、如果不存在则创建，指定字符集为gbk&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建db4数据库、如果不存在则创建，指定字符集为gbk
CREATE DATABASE IF NOT EXISTS db4 CHARACTER SET gbk;

-- 查看db4数据库的字符集
SHOW CREATE DATABASE db4;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;U(Update)：修改&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改数据库的字符集&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER DATABASE 数据库名称 CHARACTER SET 字符集名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用字符集：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--查询所有支持的字符集
SHOW CHARSET;
--查看所有支持的校对规则
SHOW COLLATION;

-- 字符集: utf8,latinI,GBK,,GBK是utf8的子集
-- 校对规则: ci 大小定不敏感，cs或bin大小写敏感
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;D(Delete)：删除&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;删除数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP DATABASE 数据库名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除数据库(判断，如果存在则删除)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP DATABASE IF EXISTS 数据库名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用数据库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询当前正在使用的数据库名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT DATABASE();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE 数据库名称； -- 标准语法
USE db4;	   -- 使用db4数据库
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数据表&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;R(Retrieve)：查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询数据库中所有的数据表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE mysql;-- 使用mysql数据库

SHOW TABLES;-- 查询库中所有的表
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询表结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DESC 表名;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
- 查询表字符集

```mysql
SHOW TABLE STATUS FROM 库名 LIKE &apos;表名&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C(Create)：创建&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建数据表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
    列名1 数据类型1,
    列名2 数据类型2,
    ....
    列名n 数据类型n
);
-- 注意：最后一列，不需要加逗号
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名 LIKE 被复制的表名;  -- 标准语法

CREATE TABLE product2 LIKE product; -- 复制product表到product2表
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据类型&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INT&lt;/td&gt;
&lt;td&gt;整数类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOUBLE&lt;/td&gt;
&lt;td&gt;小数类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;日期，只包含年月日：yyyy-MM-dd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATETIME&lt;/td&gt;
&lt;td&gt;日期，包含年月日时分秒：yyyy-MM-dd HH:mm:ss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMESTAMP&lt;/td&gt;
&lt;td&gt;时间戳类型，包含年月日时分秒：yyyy-MM-dd HH:mm:ss&amp;lt;br /&amp;gt;如果不给这个字段赋值或赋值为 NULL，则默认使用当前的系统时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHAR&lt;/td&gt;
&lt;td&gt;字符串，定长类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR&lt;/td&gt;
&lt;td&gt;字符串，&lt;strong&gt;变长类型&lt;/strong&gt;&amp;lt;br /&amp;gt;name varchar(20) 代表姓名最大 20 个字符：zhangsan 8 个字符，张三 2 个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;INT(n)&lt;/code&gt;：n 代表位数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3：int（9）显示结果为 000000010&lt;/li&gt;
&lt;li&gt;3：int（3）显示结果为 010&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;varchar(n)&lt;/code&gt;：n 表示的是字符数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 使用db3数据库
USE db3;

-- 创建一个product商品表
CREATE TABLE product(
	id INT,				-- 商品编号
	NAME VARCHAR(30),	-- 商品名称
	price DOUBLE,		-- 商品价格
	stock INT,			-- 商品库存
	insert_time DATE    -- 上架时间
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;U(Update)：修改&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改表名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 RENAME TO 新的表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改表的字符集&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ALTER TABLE 表名 CHARACTER SET 字符集名称;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
- 添加一列

```mysql
ALTER TABLE 表名 ADD 列名 数据类型;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改列数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 新数据类型;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改列名称和数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 CHANGE 列名 新列名 新数据类型;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 DROP 列名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;D(Delete)：删除&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;删除数据表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除数据表(判断，如果存在则删除)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE IF EXISTS 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;DML&lt;/h3&gt;
&lt;h4&gt;INSERT&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新增表数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新增格式 1：给指定列添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO 表名(列名1,列名2...) VALUES (值1,值2...);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增格式 2：默认给全部列添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO 表名 VALUES (值1,值2,值3,...);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增格式 3：批量添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 给指定列批量添加数据
INSERT INTO 表名(列名1,列名2,...) VALUES (值1,值2,...),(值1,值2,...)...;

-- 默认给所有列批量添加数据 
INSERT INTO 表名 VALUES (值1,值2,值3,...),(值1,值2,值3,...)...;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串拼接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONCAT(string1,string2,&apos;&apos;,...)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列名和值的数量以及数据类型要对应&lt;/li&gt;
&lt;li&gt;除了数字类型，其他数据类型的数据都需要加引号(单引双引都可以，推荐单引)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;UPDATE&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改表数据语法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标准语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE 表名 SET 列名1 = 值1,列名2 = 值2,... [where 条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改电视的价格为1800、库存为36&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE product SET price=1800,stock=36 WHERE NAME=&apos;电视&apos;;
SELECT * FROM product;-- 查看所有商品信息
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改语句中必须加条件&lt;/li&gt;
&lt;li&gt;如果不加条件，则将所有数据都修改&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;DELETE&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;删除表数据语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM 表名 [WHERE 条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除语句中必须加条件&lt;/li&gt;
&lt;li&gt;如果不加条件，则将所有数据删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;DQL&lt;/h3&gt;
&lt;h4&gt;查询语法&lt;/h4&gt;
&lt;p&gt;数据库查询遵循条件在前的原则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT DISTINCT
	&amp;lt;select list&amp;gt;
FROM
	&amp;lt;left_table&amp;gt; &amp;lt;join_type&amp;gt;
JOIN
	&amp;lt;right_table&amp;gt; ON &amp;lt;join_condition&amp;gt;	-- 连接查询在多表查询部分详解
WHERE
	&amp;lt;where_condition&amp;gt;
GROUP BY
	&amp;lt;group_by_list&amp;gt;
HAVING
	&amp;lt;having_condition&amp;gt;
ORDER BY
	&amp;lt;order_by_condition&amp;gt;
LIMIT
	&amp;lt;limit_params&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM	&amp;lt;left_table&amp;gt;

ON 		&amp;lt;join_condition&amp;gt;

&amp;lt;join_type&amp;gt;		JOIN	&amp;lt;right_table&amp;gt;

WHERE		&amp;lt;where_condition&amp;gt;

GROUP BY 	&amp;lt;group_by_list&amp;gt;

HAVING		&amp;lt;having_condition&amp;gt;

SELECT DISTINCT		&amp;lt;select list&amp;gt;

ORDER BY	&amp;lt;order_by_condition&amp;gt;

LIMIT		&amp;lt;limit_params&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;查询全部&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询全部的表数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 标准语法
SELECT * FROM 表名;

-- 查询product表所有数据(常用)
SELECT * FROM product;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询指定字段的表数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名1,列名2,... FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;去除重复查询&lt;/strong&gt;：只有值全部重复的才可以去除，需要创建临时表辅助查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT DISTINCT 列名1,列名2,... FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算列的值（四则运算）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名1 运算符(+ - * /) 列名2 FROM 表名;

/*如果某一列值为null，可以进行替换
	ifnull(表达式1,表达式2)
	表达式1：想替换的列
	表达式2：想替换的值*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查询商品名称和库存，库存数量在原有基础上加10
SELECT NAME,stock+10 FROM product;

-- 查询商品名称和库存，库存数量在原有基础上加10。进行null值判断
SELECT NAME,IFNULL(stock,0)+10 FROM product;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;起别名&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名1,列名2,... AS 别名 FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查询商品名称和库存，库存数量在原有基础上加10。进行null值判断，起别名为getSum,AS可以省略。
SELECT NAME,IFNULL(stock,0)+10 AS getsum FROM product;
SELECT NAME,IFNULL(stock,0)+10 getsum FROM product;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;条件查询&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;条件查询语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 WHERE 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件分类&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;=&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;=&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&amp;gt; 或 !=&lt;/td&gt;
&lt;td&gt;不等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BETWEEN ... AND ...&lt;/td&gt;
&lt;td&gt;在某个范围之内(都包含)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IN(...)&lt;/td&gt;
&lt;td&gt;多选一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LIKE&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;模糊查询&lt;/strong&gt;：_单个任意字符、%任意个字符、[] 匹配集合内的字符&amp;lt;br/&amp;gt;&lt;code&gt;LIKE &apos;[^AB]%&apos; &lt;/code&gt;：不以 A 和 B 开头的任意文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IS NULL&lt;/td&gt;
&lt;td&gt;是NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IS NOT NULL&lt;/td&gt;
&lt;td&gt;不是NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AND 或 &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;并且&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OR 或 ||&lt;/td&gt;
&lt;td&gt;或者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT 或 !&lt;/td&gt;
&lt;td&gt;非，不是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNION&lt;/td&gt;
&lt;td&gt;对两个结果集进行&lt;strong&gt;并集操作并进行去重，同时进行默认规则的排序&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNION ALL&lt;/td&gt;
&lt;td&gt;对两个结果集进行并集操作不进行去重，不进行排序&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查询库存大于20的商品信息
SELECT * FROM product WHERE stock &amp;gt; 20;

-- 查询品牌为华为的商品信息
SELECT * FROM product WHERE brand=&apos;华为&apos;;

-- 查询金额在4000 ~ 6000之间的商品信息
SELECT * FROM product WHERE price &amp;gt;= 4000 AND price &amp;lt;= 6000;
SELECT * FROM product WHERE price BETWEEN 4000 AND 6000;

-- 查询库存为14、30、23的商品信息
SELECT * FROM product WHERE stock=14 OR stock=30 OR stock=23;
SELECT * FROM product WHERE stock IN(14,30,23);

-- 查询库存为null的商品信息
SELECT * FROM product WHERE stock IS NULL;
-- 查询库存不为null的商品信息
SELECT * FROM product WHERE stock IS NOT NULL;

-- 查询名称以&apos;小米&apos;为开头的商品信息
SELECT * FROM product WHERE NAME LIKE &apos;小米%&apos;;

-- 查询名称第二个字是&apos;为&apos;的商品信息
SELECT * FROM product WHERE NAME LIKE &apos;_为%&apos;;

-- 查询名称为四个字符的商品信息 4个下划线
SELECT * FROM product WHERE NAME LIKE &apos;____&apos;;

-- 查询名称中包含电脑的商品信息
SELECT * FROM product WHERE NAME LIKE &apos;%电脑%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-DQL数据准备.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;函数查询&lt;/h4&gt;
&lt;h5&gt;聚合函数&lt;/h5&gt;
&lt;p&gt;聚合函数：将一列数据作为一个整体，进行纵向的计算&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚合函数语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 函数名(列名) FROM 表名 [WHERE 条件]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;聚合函数分类&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;COUNT(列名)&lt;/td&gt;
&lt;td&gt;统计数量（一般选用不为 null 的列）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAX(列名)&lt;/td&gt;
&lt;td&gt;最大值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MIN(列名)&lt;/td&gt;
&lt;td&gt;最小值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUM(列名)&lt;/td&gt;
&lt;td&gt;求和&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVG(列名)&lt;/td&gt;
&lt;td&gt;平均值（会忽略 null 行）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 计算product表中总记录条数 7
SELECT COUNT(*) FROM product;

-- 获取最高价格
SELECT MAX(price) FROM product;
-- 获取最高价格的商品名称
SELECT NAME,price FROM product WHERE price = (SELECT MAX(price) FROM product);

-- 获取最低库存
SELECT MIN(stock) FROM product;
-- 获取最低库存的商品名称
SELECT NAME,stock FROM product WHERE stock = (SELECT MIN(stock) FROM product);

-- 获取总库存数量
SELECT SUM(stock) FROM product;
-- 获取品牌为小米的平均商品价格
SELECT AVG(price) FROM product WHERE brand=&apos;小米&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;文本函数&lt;/h5&gt;
&lt;p&gt;CONCAT()：用于连接两个字段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT CONCAT(TRIM(col1), &apos;(&apos;, TRIM(col2), &apos;)&apos;) AS concat_col FROM mytable
-- 许多数据库会使用空格把一个值填充为列宽，连接的结果出现一些不必要的空格，使用TRIM()可以去除首尾空格
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名称&lt;/th&gt;
&lt;th&gt;作 用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LENGTH&lt;/td&gt;
&lt;td&gt;计算字符串长度函数，返回字符串的字节长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONCAT&lt;/td&gt;
&lt;td&gt;合并字符串函数，返回结果为连接参数产生的字符串，参数可以使一个或多个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT&lt;/td&gt;
&lt;td&gt;替换字符串函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOWER&lt;/td&gt;
&lt;td&gt;将字符串中的字母转换为小写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPPER&lt;/td&gt;
&lt;td&gt;将字符串中的字母转换为大写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEFT&lt;/td&gt;
&lt;td&gt;从左侧字截取符串，返回字符串左边的若干个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RIGHT&lt;/td&gt;
&lt;td&gt;从右侧字截取符串，返回字符串右边的若干个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TRIM&lt;/td&gt;
&lt;td&gt;删除字符串左右两侧的空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPLACE&lt;/td&gt;
&lt;td&gt;字符串替换函数，返回替换后的新字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUBSTRING&lt;/td&gt;
&lt;td&gt;截取字符串，返回从指定位置开始的指定长度的字符换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REVERSE&lt;/td&gt;
&lt;td&gt;字符串反转（逆序）函数，返回与原始字符串顺序相反的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;数字函数&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名称&lt;/th&gt;
&lt;th&gt;作 用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ABS&lt;/td&gt;
&lt;td&gt;求绝对值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQRT&lt;/td&gt;
&lt;td&gt;求二次方根&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MOD&lt;/td&gt;
&lt;td&gt;求余数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CEIL 和 CEILING&lt;/td&gt;
&lt;td&gt;两个函数功能相同，都是返回不小于参数的最小整数，即向上取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLOOR&lt;/td&gt;
&lt;td&gt;向下取整，返回值转化为一个BIGINT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAND&lt;/td&gt;
&lt;td&gt;生成一个0~1之间的随机数，传入整数参数是，用来产生重复序列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROUND&lt;/td&gt;
&lt;td&gt;对所传参数进行四舍五入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGN&lt;/td&gt;
&lt;td&gt;返回参数的符号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POW 和 POWER&lt;/td&gt;
&lt;td&gt;两个函数的功能相同，都是所传参数的次方的结果值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIN&lt;/td&gt;
&lt;td&gt;求正弦值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASIN&lt;/td&gt;
&lt;td&gt;求反正弦值，与函数 SIN 互为反函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COS&lt;/td&gt;
&lt;td&gt;求余弦值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACOS&lt;/td&gt;
&lt;td&gt;求反余弦值，与函数 COS 互为反函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TAN&lt;/td&gt;
&lt;td&gt;求正切值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ATAN&lt;/td&gt;
&lt;td&gt;求反正切值，与函数 TAN 互为反函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COT&lt;/td&gt;
&lt;td&gt;求余切值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;日期函数&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数名称&lt;/th&gt;
&lt;th&gt;作 用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CURDATE 和 CURRENT_DATE&lt;/td&gt;
&lt;td&gt;两个函数作用相同，返回当前系统的日期值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CURTIME 和 CURRENT_TIME&lt;/td&gt;
&lt;td&gt;两个函数作用相同，返回当前系统的时间值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOW 和  SYSDATE&lt;/td&gt;
&lt;td&gt;两个函数作用相同，返回当前系统的日期和时间值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MONTH&lt;/td&gt;
&lt;td&gt;获取指定日期中的月份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MONTHNAME&lt;/td&gt;
&lt;td&gt;获取指定日期中的月份英文名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAYNAME&lt;/td&gt;
&lt;td&gt;获取指定曰期对应的星期几的英文名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAYOFWEEK&lt;/td&gt;
&lt;td&gt;获取指定日期对应的一周的索引位置值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WEEK&lt;/td&gt;
&lt;td&gt;获取指定日期是一年中的第几周，返回值的范围是否为 0〜52 或 1〜53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAYOFYEAR&lt;/td&gt;
&lt;td&gt;获取指定曰期是一年中的第几天，返回值范围是1~366&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAYOFMONTH&lt;/td&gt;
&lt;td&gt;获取指定日期是一个月中是第几天，返回值范围是1~31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YEAR&lt;/td&gt;
&lt;td&gt;获取年份，返回值范围是 1970〜2069&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIME_TO_SEC&lt;/td&gt;
&lt;td&gt;将时间参数转换为秒数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEC_TO_TIME&lt;/td&gt;
&lt;td&gt;将秒数转换为时间，与TIME_TO_SEC 互为反函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE_ADD 和 ADDDATE&lt;/td&gt;
&lt;td&gt;两个函数功能相同，都是向日期添加指定的时间间隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE_SUB 和 SUBDATE&lt;/td&gt;
&lt;td&gt;两个函数功能相同，都是向日期减去指定的时间间隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADDTIME&lt;/td&gt;
&lt;td&gt;时间加法运算，在原始时间上添加指定的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUBTIME&lt;/td&gt;
&lt;td&gt;时间减法运算，在原始时间上减去指定的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATEDIFF&lt;/td&gt;
&lt;td&gt;获取两个日期之间间隔，返回参数 1 减去参数 2 的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE_FORMAT&lt;/td&gt;
&lt;td&gt;格式化指定的日期，根据参数返回指定格式的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WEEKDAY&lt;/td&gt;
&lt;td&gt;获取指定日期在一周内的对应的工作日索引&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;正则查询&lt;/h4&gt;
&lt;p&gt;正则表达式（Regular Expression）是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM emp WHERE name REGEXP &apos;^T&apos;;	-- 匹配以T开头的name值
SELECT * FROM emp WHERE name REGEXP &apos;2$&apos;;	-- 匹配以2结尾的name值
SELECT * FROM emp WHERE name REGEXP &apos;[uvw]&apos;;-- 匹配包含 uvw 的name值
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;^&lt;/td&gt;
&lt;td&gt;在字符串开始处进行匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;在字符串末尾处进行匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;匹配任意单个字符, 包括换行符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[...]&lt;/td&gt;
&lt;td&gt;匹配出括号内的任意字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[^...]&lt;/td&gt;
&lt;td&gt;匹配不出括号内的任意字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a*&lt;/td&gt;
&lt;td&gt;匹配零个或者多个a(包括空串)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a+&lt;/td&gt;
&lt;td&gt;匹配一个或者多个a(不包括空串)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a?&lt;/td&gt;
&lt;td&gt;匹配零个或者一个a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a1|a2&lt;/td&gt;
&lt;td&gt;匹配a1或a2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a(m)&lt;/td&gt;
&lt;td&gt;匹配m个a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a(m,)&lt;/td&gt;
&lt;td&gt;至少匹配m个a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a(m,n)&lt;/td&gt;
&lt;td&gt;匹配m个a 到 n个a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a(,n)&lt;/td&gt;
&lt;td&gt;匹配0到n个a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(...)&lt;/td&gt;
&lt;td&gt;将模式元素组成单一元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;排序查询&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;排序查询语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 [WHERE 条件] ORDER BY 列名1 排序方式1,列名2 排序方式2;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;排序方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ASC:升序
DESC:降序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：多个排序条件，当前边的条件值一样时，才会判断第二条件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 按照库存升序排序
SELECT * FROM product ORDER BY stock ASC;

-- 查询名称中包含手机的商品信息。按照金额降序排序
SELECT * FROM product WHERE NAME LIKE &apos;%手机%&apos; ORDER BY price DESC;

-- 按照金额升序排序，如果金额相同，按照库存降序排列
SELECT * FROM product ORDER BY price ASC,stock DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分组查询&lt;/h4&gt;
&lt;p&gt;分组查询会进行去重&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分组查询语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 [WHERE 条件] GROUP BY 分组列名 [HAVING 分组后条件过滤] [ORDER BY 排序列名 排序方式];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WHERE 过滤行，HAVING 过滤分组，行过滤应当先于分组过滤&lt;/p&gt;
&lt;p&gt;分组规定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GROUP BY 子句出现在 WHERE 子句之后，ORDER BY 子句之前&lt;/li&gt;
&lt;li&gt;NULL 的行会单独分为一组&lt;/li&gt;
&lt;li&gt;大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 按照品牌分组，获取每组商品的总金额
SELECT brand,SUM(price) FROM product GROUP BY brand;

-- 对金额大于4000元的商品，按照品牌分组,获取每组商品的总金额
SELECT brand,SUM(price) FROM product WHERE price &amp;gt; 4000 GROUP BY brand;

-- 对金额大于4000元的商品，按照品牌分组，获取每组商品的总金额，只显示总金额大于7000元的
SELECT brand,SUM(price) AS getSum FROM product WHERE price &amp;gt; 4000 GROUP BY brand HAVING getSum &amp;gt; 7000;

-- 对金额大于4000元的商品，按照品牌分组，获取每组商品的总金额，只显示总金额大于7000元的、并按照总金额的降序排列
SELECT brand,SUM(price) AS getSum FROM product WHERE price &amp;gt; 4000 GROUP BY brand HAVING getSum &amp;gt; 7000 ORDER BY getSum DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分页查询&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分页查询语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 [WHERE 条件] GROUP BY 分组列名 [HAVING 分组后条件过滤] [ORDER BY 排序列名 排序方式] LIMIT 开始索引,查询条数;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公式：开始索引 = (当前页码-1) * 每页显示的条数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM product LIMIT 0,2;  -- 第一页 开始索引=(1-1) * 2
SELECT * FROM product LIMIT 2,2;  -- 第二页 开始索引=(2-1) * 2
SELECT * FROM product LIMIT 4,2;  -- 第三页 开始索引=(3-1) * 2
SELECT * FROM product LIMIT 6,2;  -- 第四页 开始索引=(4-1) * 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-DQL%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A2%E5%9B%BE%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;多表操作&lt;/h2&gt;
&lt;h3&gt;约束分类&lt;/h3&gt;
&lt;h4&gt;约束介绍&lt;/h4&gt;
&lt;p&gt;约束：对表中的数据进行限定，保证数据的正确性、有效性、完整性&lt;/p&gt;
&lt;p&gt;约束的分类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;约束&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRIMARY KEY&lt;/td&gt;
&lt;td&gt;主键约束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRIMARY KEY AUTO_INCREMENT&lt;/td&gt;
&lt;td&gt;主键、自动增长&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNIQUE&lt;/td&gt;
&lt;td&gt;唯一约束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;非空约束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOREIGN KEY&lt;/td&gt;
&lt;td&gt;外键约束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOREIGN KEY ON UPDATE CASCADE&lt;/td&gt;
&lt;td&gt;外键级联更新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOREIGN KEY ON DELETE CASCADE&lt;/td&gt;
&lt;td&gt;外键级联删除&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;主键约束&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主键约束特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主键约束默认包含&lt;strong&gt;非空和唯一&lt;/strong&gt;两个功能&lt;/li&gt;
&lt;li&gt;一张表只能有一个主键&lt;/li&gt;
&lt;li&gt;主键一般用于表中数据的唯一标识&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表时添加主键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名 数据类型 PRIMARY KEY,
    列名 数据类型,
    ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除主键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 DROP PRIMARY KEY;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表后单独添加主键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型 PRIMARY KEY;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建student表
CREATE TABLE student(
	id INT PRIMARY KEY  -- 给id添加主键约束
);

-- 添加数据
INSERT INTO student VALUES (1),(2);
-- 主键默认唯一，添加重复数据，会报错
INSERT INTO student VALUES (2);
-- 主键默认非空，不能添加null的数据
INSERT INTO student VALUES (NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;主键自增&lt;/h4&gt;
&lt;p&gt;主键自增约束可以为空，并自动增长。删除某条数据不影响自增的下一个数值，依然按照前一个值自增&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;建表时添加主键自增约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名 数据类型 PRIMARY KEY AUTO_INCREMENT,
    列名 数据类型,
    ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除主键自增约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表后单独添加主键自增约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型 AUTO_INCREMENT;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建student2表
CREATE TABLE student2(
	id INT PRIMARY KEY AUTO_INCREMENT    -- 给id添加主键自增约束
);

-- 添加数据
INSERT INTO student2 VALUES (1),(2);
-- 添加null值，会自动增长
INSERT INTO student2 VALUES (NULL),(NULL);-- 3，4
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;唯一约束&lt;/h4&gt;
&lt;p&gt;唯一约束：约束不能有重复的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;建表时添加唯一约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名 数据类型 UNIQUE,
    列名 数据类型,
    ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除唯一约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 DROP INDEX 列名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表后单独添加唯一约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型 UNIQUE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;非空约束&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;建表时添加非空约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名 数据类型 NOT NULL,
    列名 数据类型,
    ...
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除非空约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表后单独添加非空约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 MODIFY 列名 数据类型 NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;外键约束&lt;/h4&gt;
&lt;p&gt;外键约束：让表和表之间产生关系，从而保证数据的准确性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;建表时添加外键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名 数据类型 约束,
    ...
    CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除外键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 DROP FOREIGN KEY 外键名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表后单独添加外键约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建user用户表
CREATE TABLE USER(
	id INT PRIMARY KEY AUTO_INCREMENT,    -- id
	name VARCHAR(20) NOT NULL             -- 姓名
);
-- 添加用户数据
INSERT INTO USER VALUES (NULL,&apos;张三&apos;),(NULL,&apos;李四&apos;),(NULL,&apos;王五&apos;);

-- 创建orderlist订单表
CREATE TABLE orderlist(
	id INT PRIMARY KEY AUTO_INCREMENT,    -- id
	number VARCHAR(20) NOT NULL,          -- 订单编号
	uid INT,                              -- 订单所属用户
	CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id)   -- 添加外键约束
);
-- 添加订单数据
INSERT INTO orderlist VALUES (NULL,&apos;hm001&apos;,1),(NULL,&apos;hm002&apos;,1),
(NULL,&apos;hm003&apos;,2),(NULL,&apos;hm004&apos;,2),
(NULL,&apos;hm005&apos;,3),(NULL,&apos;hm006&apos;,3);

-- 添加一个订单，但是没有所属用户。无法添加
INSERT INTO orderlist VALUES (NULL,&apos;hm007&apos;,8);
-- 删除王五这个用户，但是订单表中王五还有很多个订单呢。无法删除
DELETE FROM USER WHERE NAME=&apos;王五&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;外键级联&lt;/h4&gt;
&lt;p&gt;级联操作：当把主表中的数据进行删除或更新时，从表中有关联的数据的相应操作，包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RESTRICT 和 NO ACTION相同， 是指限制在子表有关联记录的情况下， 父表不能更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CASCADE 表示父表在更新或者删除时，更新或者删除子表对应的记录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SET NULL 则表示父表在更新或者删除的时候，子表的对应字段被SET NULL&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;级联操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加级联更新&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE [CASCADE | RESTRICT | SET NULL];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加级联删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON DELETE CASCADE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时添加级联更新和级联删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE CASCADE ON DELETE CASCADE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;多表设计&lt;/h3&gt;
&lt;h4&gt;一对一&lt;/h4&gt;
&lt;p&gt;多表：有多张数据表，而表与表之间有一定的关联关系，通过外键约束实现，分为一对一、一对多、多对多三类&lt;/p&gt;
&lt;p&gt;举例：人和身份证&lt;/p&gt;
&lt;p&gt;实现原则：在任意一个表建立外键，去关联另外一个表的主键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建person表
CREATE TABLE person(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	NAME VARCHAR(20)                        -- 姓名
);
-- 添加数据
INSERT INTO person VALUES (NULL,&apos;张三&apos;),(NULL,&apos;李四&apos;);

-- 创建card表
CREATE TABLE card(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	number VARCHAR(20) UNIQUE NOT NULL,	-- 身份证号
	pid INT UNIQUE,                         -- 外键列
	CONSTRAINT cp_fk1 FOREIGN KEY (pid) REFERENCES person(id)
);
-- 添加数据
INSERT INTO card VALUES (NULL,&apos;12345&apos;,1),(NULL,&apos;56789&apos;,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%A4%9A%E8%A1%A8%E8%AE%BE%E8%AE%A1%E4%B8%80%E5%AF%B9%E4%B8%80.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;一对多&lt;/h4&gt;
&lt;p&gt;举例：用户和订单、商品分类和商品&lt;/p&gt;
&lt;p&gt;实现原则：在多的一方，建立外键约束，来关联一的一方主键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建user表
CREATE TABLE USER(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	NAME VARCHAR(20)                        -- 姓名
);
-- 添加数据
INSERT INTO USER VALUES (NULL,&apos;张三&apos;),(NULL,&apos;李四&apos;);

-- 创建orderlist表
CREATE TABLE orderlist(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	number VARCHAR(20),                     -- 订单编号
	uid INT,				-- 外键列
	CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id)
);
-- 添加数据
INSERT INTO orderlist VALUES (NULL,&apos;hm001&apos;,1),(NULL,&apos;hm002&apos;,1),(NULL,&apos;hm003&apos;,2),(NULL,&apos;hm004&apos;,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%A4%9A%E8%A1%A8%E8%AE%BE%E8%AE%A1%E4%B8%80%E5%AF%B9%E5%A4%9A.png&quot; alt=&quot;多表设计一对多&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;多对多&lt;/h4&gt;
&lt;p&gt;举例：学生和课程。一个学生可以选择多个课程，一个课程也可以被多个学生选择&lt;/p&gt;
&lt;p&gt;实现原则：借助第三张表中间表，中间表至少包含两个列，这两个列作为中间表的外键，分别关联两张表的主键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建student表
CREATE TABLE student(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	NAME VARCHAR(20)			-- 学生姓名
);
-- 添加数据
INSERT INTO student VALUES (NULL,&apos;张三&apos;),(NULL,&apos;李四&apos;);

-- 创建course表
CREATE TABLE course(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	NAME VARCHAR(10)			-- 课程名称
);
-- 添加数据
INSERT INTO course VALUES (NULL,&apos;语文&apos;),(NULL,&apos;数学&apos;);

-- 创建中间表
CREATE TABLE stu_course(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 主键id
	sid INT,  -- 用于和student表中的id进行外键关联
	cid INT,  -- 用于和course表中的id进行外键关联
	CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id), -- 添加外键约束
	CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id)   -- 添加外键约束
);
-- 添加数据
INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%A4%9A%E8%A1%A8%E8%AE%BE%E8%AE%A1%E5%A4%9A%E5%AF%B9%E5%A4%9A.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;连接查询&lt;/h3&gt;
&lt;h4&gt;内外连接&lt;/h4&gt;
&lt;h5&gt;内连接&lt;/h5&gt;
&lt;p&gt;连接查询的是两张表有交集的部分数据，两张表分为&lt;strong&gt;驱动表和被驱动表&lt;/strong&gt;，如果结果集中的每条记录都是两个表相互匹配的组合，则称这样的结果集为笛卡尔积&lt;/p&gt;
&lt;p&gt;内连接查询，若驱动表中的记录在被驱动表中找不到匹配的记录时，则该记录不会加到最后的结果集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;显式内连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名1 [INNER] JOIN 表名2 ON 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐式内连接：内连接中 WHERE 子句和 ON 子句是等价的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名1,表名2 WHERE 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;STRAIGHT_JOIN与 JOIN 类似，只不过左表始终在右表之前读取，只适用于内连接&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;外连接&lt;/h5&gt;
&lt;p&gt;外连接查询，若驱动表中的记录在被驱动表中找不到匹配的记录时，则该记录也会加到最后的结果集，只是对于被驱动表中&lt;strong&gt;不匹配过滤条件&lt;/strong&gt;的记录，各个字段使用 NULL 填充&lt;/p&gt;
&lt;p&gt;应用实例：查学生成绩，也想展示出缺考的人的成绩&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左外连接：选择左侧的表为驱动表，查询左表的全部数据，和左右两张表有交集部分的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名1 LEFT [OUTER] JOIN 表名2 ON 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右外连接：选择右侧的表为驱动表，查询右表的全部数据，和左右两张表有交集部分的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-JOIN%E6%9F%A5%E8%AF%A2%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;关联查询&lt;/h4&gt;
&lt;p&gt;自关联查询：同一张表中有数据关联，可以多次查询这同一个表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建员工表
CREATE TABLE employee(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 员工编号
	NAME VARCHAR(20),					-- 员工姓名
	mgr INT,							-- 上级编号
	salary DOUBLE						-- 员工工资
);
-- 添加数据
INSERT INTO employee VALUES (1001,&apos;孙悟空&apos;,1005,9000.00),..,(1009,&apos;宋江&apos;,NULL,16000.00);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E8%87%AA%E5%85%B3%E8%81%94%E6%9F%A5%E8%AF%A2%E6%95%B0%E6%8D%AE%E5%87%86%E5%A4%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查询所有员工的姓名及其直接上级的姓名，没有上级的员工也需要查询
/*
分析
	员工信息 employee表
	条件：employee.mgr = employee.id
	查询左表的全部数据，和左右两张表有交集部分数据，左外连接
*/
SELECT
	e1.id,
	e1.name,
	e1.mgr,
	e2.id,
	e2.name
FROM
	employee e1
LEFT OUTER JOIN
	employee e2
ON
	e1.mgr = e2.id;	
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id		name	mgr	   id	  name
1001	孙悟空	  1005	1005	唐僧
1002	猪八戒	  1005	1005	唐僧
1003	沙和尚	  1005	1005	唐僧
1004	小白龙	  1005	1005	唐僧
1005	唐僧	   NULL	 NULL	 NULL
1006	武松	   1009	 1009	 宋江
1007	李逵	   1009	 1009	 宋江
1008	林冲	   1009	 1009	 宋江
1009	宋江	   NULL	 NULL	 NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;连接原理&lt;/h4&gt;
&lt;p&gt;Index Nested-Loop Join 算法：查询驱动表得到&lt;strong&gt;数据集&lt;/strong&gt;，然后根据数据集中的每一条记录的&lt;strong&gt;关联字段再分别&lt;/strong&gt;到被驱动表中查找匹配（&lt;strong&gt;走索引&lt;/strong&gt;），所以驱动表只需要访问一次，被驱动表要访问多次&lt;/p&gt;
&lt;p&gt;MySQL 将查询驱动表后得到的记录成为驱动表的扇出，连接查询的成本：单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本，优化器会选择成本最小的表连接顺序（确定谁是驱动表，谁是被驱动表）生成执行计划，进行连接查询，优化方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少驱动表的扇出（让数据量小的表来做驱动表）&lt;/li&gt;
&lt;li&gt;降低访问被驱动表的成本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：STRAIGHT_JOIN 是查一条驱动表，然后根据关联字段去查被驱动表，要访问多次驱动表，所以需要优化为 INL 算法&lt;/p&gt;
&lt;p&gt;Block Nested-Loop Join 算法：一种&lt;strong&gt;空间换时间&lt;/strong&gt;的优化方式，基于块的循环连接，执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer，先把若干条驱动表中的扇出暂存在缓冲区，每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配（扫描全部数据，一条一条的匹配），因为是在内存中完成，所以速度快，并且降低了 I/O 成本&lt;/p&gt;
&lt;p&gt;Join Buffer 可以通过参数 &lt;code&gt;join_buffer_size&lt;/code&gt; 进行配置，默认大小是 256 KB&lt;/p&gt;
&lt;p&gt;在成本分析时，对于很多张表的连接查询，连接顺序有非常多，MySQL 如果挨着进行遍历计算成本，会消耗很多资源&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;提前结束某种连接顺序的成本评估：维护一个全局变量记录当前成本最小的连接方式，如果一种顺序只计算了一部分就已经超过了最小成本，可以提前结束计算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统变量 optimizer_search_depth：如果连接表的个数小于该变量，就继续穷举分析每一种连接数量，反之只对数量与 depth 值相同的表进行分析，该值越大成本分析的越精确&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统变量 optimizer_prune_level：控制启发式规则的启用，这些规则就是根据以往经验指定的，不满足规则的连接顺序不分析成本&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;连接优化&lt;/h4&gt;
&lt;h5&gt;BKA&lt;/h5&gt;
&lt;p&gt;Batched Key Access 算法是对 NLJ 算法的优化，在读取被驱动表的记录时使用顺序 IO，Extra 信息中会有 Batched Key Access 信息&lt;/p&gt;
&lt;p&gt;使用 BKA 的表的 JOIN 过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接驱动表将满足条件的记录放入 Join Buffer，并将两表连接的字段放入一个 DYNAMIC_ARRAY ranges 中&lt;/li&gt;
&lt;li&gt;在进行表的过接过程中，会将 ranges 相关的信息传入 Buffer 中，进行被驱动表主建的查找及排序操作&lt;/li&gt;
&lt;li&gt;调用步骤 2 中产生的有序主建，&lt;strong&gt;顺序读取被驱动表的数据&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当缓冲区的数据被读完后，会重复进行步骤 2、3，直到记录被读取完&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 BKA 优化需要设进行设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET optimizer_switch=&apos;mrr=on,mrr_cost_based=off,batched_key_access=on&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：前两个参数的作用是启用 MRR，因为 BKA 算法的优化要依赖于 MRR（系统优化 → 内存优化 → Read 详解）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;BNL&lt;/h5&gt;
&lt;h6&gt;问题&lt;/h6&gt;
&lt;p&gt;BNL 即 Block Nested-Loop Join 算法，由于要访问多次被驱动表，会产生两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Join 语句多次扫描一个冷表，并且语句执行时间小于 1 秒，就会在再次扫描冷表时，把冷表的数据页移到 LRU 链表头部，导致热数据被淘汰，影响业务的正常运行&lt;/p&gt;
&lt;p&gt;这种情况冷表的数据量要小于整个 Buffer Pool 的 old 区域，能够完全放入 old 区，才会再次被读时加到 young，否则读取下一段时就已经把上一段淘汰&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Join 语句在循环读磁盘和淘汰内存页，进入 old 区域的数据页很可能在 1 秒之内就被淘汰，就会导致 MySQL 实例的 Buffer Pool 在这段时间内 young 区域的数据页没有被合理地淘汰&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大表 Join 操作虽然对 IO 有影响，但是在语句执行结束后对 IO 的影响随之结束。但是对 Buffer Pool 的影响就是持续性的，需要依靠后续的查询请求慢慢恢复内存命中率&lt;/p&gt;
&lt;h6&gt;优化&lt;/h6&gt;
&lt;p&gt;将 BNL 算法转成 BKA 算法，优化方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在被驱动表上建索引，这样就可以根据索引进行顺序 IO&lt;/li&gt;
&lt;li&gt;使用临时表，&lt;strong&gt;在临时表上建立索引&lt;/strong&gt;，将被驱动表和临时表进行连接查询&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;驱动表 t1，被驱动表 t2，使用临时表的工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把表 t1 中满足条件的数据放在临时表 tmp_t 中&lt;/li&gt;
&lt;li&gt;给临时表 tmp_t 的关联字段加上索引，使用 BKA 算法&lt;/li&gt;
&lt;li&gt;让表 t2 和 tmp_t 做 Join 操作（临时表是被驱动表）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：MySQL 8.0 支持 hash join，join_buffer 维护的不再是一个无序数组，而是一个哈希表，查询效率更高，执行效率比临时表更高&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;嵌套查询&lt;/h3&gt;
&lt;h4&gt;查询分类&lt;/h4&gt;
&lt;p&gt;查询语句中嵌套了查询语句，&lt;strong&gt;将嵌套查询称为子查询&lt;/strong&gt;，FROM 子句后面的子查询的结果集称为派生表&lt;/p&gt;
&lt;p&gt;根据结果分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;结果是单行单列：可以将查询的结果作为另一条语句的查询条件，使用运算符判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结果是多行单列：可以作为条件，使用运算符 IN 或 NOT IN 进行判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结果是多行多列：查询的结果可以作为一张虚拟表参与查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件];

-- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息
SELECT 
	* 
FROM 
	USER u,
	(SELECT * FROM orderlist WHERE id&amp;gt;4) o 
WHERE 
	u.id=o.uid;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关性分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不相关子查询：子查询不依赖外层查询的值，可以单独运行出结果&lt;/li&gt;
&lt;li&gt;相关子查询：子查询的执行需要依赖外层查询的值&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;查询优化&lt;/h4&gt;
&lt;p&gt;不相关子查询的结果集会被写入一个临时表，并且在写入时&lt;strong&gt;去重&lt;/strong&gt;，该过程称为&lt;strong&gt;物化&lt;/strong&gt;，存储结果集的临时表称为物化表&lt;/p&gt;
&lt;p&gt;系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小于系统变量时，内存中可以保存，会为建立&lt;strong&gt;基于内存&lt;/strong&gt;的 MEMORY 存储引擎的临时表，并建立哈希索引&lt;/li&gt;
&lt;li&gt;大于任意一个系统变量时，物化表会使用&lt;strong&gt;基于磁盘&lt;/strong&gt;的 InnoDB 存储引擎来保存结果集中的记录，索引类型为 B+ 树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;物化后，嵌套查询就相当于外层查询的表和物化表进行内连接查询，然后经过优化器选择成本最小的表连接顺序执行查询&lt;/p&gt;
&lt;p&gt;子查询物化会产生建立临时表的成本，但是将子查询转化为连接查询可以充分发挥优化器的作用，所以引入：半连接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t1 和 t2 表进行半连接，对于 t1 表中的某条记录，只需要关心在 t2 表中是否存在，而不需要关心有多少条记录与之匹配，最终结果集只保留 t1 的记录&lt;/li&gt;
&lt;li&gt;半连接只是执行子查询的一种方式，MySQL 并没有提供面向用户的半连接语法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/35231266/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;联合查询&lt;/h4&gt;
&lt;p&gt;UNION 是取这两个子查询结果的并集，并进行去重，同时进行默认规则的排序（union 是行加起来，join 是列加起来）&lt;/p&gt;
&lt;p&gt;UNION ALL 是对两个结果集进行并集操作不进行去重，不进行排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(select 1000 as f) union (select id from t1 order by id desc limit 2); #t1表中包含id 为 1-1000 的数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语句的执行流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个内存临时表，这个临时表只有一个整型字段 f，并且 f 是主键字段&lt;/li&gt;
&lt;li&gt;执行第一个子查询，得到 1000 这个值，并存入临时表中&lt;/li&gt;
&lt;li&gt;执行第二个子查询，拿到第一行 id=1000，试图插入临时表中，但由于 1000 这个值已经存在于临时表了，违反了唯一性约束，所以插入失败，然后继续执行&lt;/li&gt;
&lt;li&gt;取到第二行 id=999，插入临时表成功&lt;/li&gt;
&lt;li&gt;从临时表中按行取出数据，返回结果并删除临时表，结果中包含两行数据分别是 1000 和 999&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;查询练习&lt;/h3&gt;
&lt;p&gt;数据准备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建db4数据库
CREATE DATABASE db4;
-- 使用db4数据库
USE db4;

-- 创建user表
CREATE TABLE USER(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 用户id
	NAME VARCHAR(20),					-- 用户姓名
	age INT                             -- 用户年龄
);

-- 订单表
CREATE TABLE orderlist(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 订单id
	number VARCHAR(30),					-- 订单编号
	uid INT,   							-- 外键字段
	CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id)
);

-- 商品分类表
CREATE TABLE category(
	id INT PRIMARY KEY AUTO_INCREMENT,  -- 商品分类id
	NAME VARCHAR(10)                    -- 商品分类名称
);

-- 商品表
CREATE TABLE product(
	id INT PRIMARY KEY AUTO_INCREMENT,   -- 商品id
	NAME VARCHAR(30),                    -- 商品名称
	cid INT, -- 外键字段
	CONSTRAINT cp_fk1 FOREIGN KEY (cid) REFERENCES category(id)
);

-- 中间表
CREATE TABLE us_pro(
	upid INT PRIMARY KEY AUTO_INCREMENT,  -- 中间表id
	uid INT, 							  -- 外键字段。需要和用户表的主键产生关联
	pid INT,							  -- 外键字段。需要和商品表的主键产生关联
	CONSTRAINT up_fk1 FOREIGN KEY (uid) REFERENCES USER(id),
	CONSTRAINT up_fk2 FOREIGN KEY (pid) REFERENCES product(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E5%A4%9A%E8%A1%A8%E7%BB%83%E4%B9%A0%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.png&quot; alt=&quot;多表练习架构设计&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据查询：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查询用户的编号、姓名、年龄、订单编号&lt;/p&gt;
&lt;p&gt;数据：用户的编号、姓名、年龄在 user 表，订单编号在 orderlist 表&lt;/p&gt;
&lt;p&gt;条件：user.id = orderlist.uid&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.*,
	o.number
FROM
	USER u,
	orderlist o
WHERE
	u.id = o.uid;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询所有的用户，显示用户的编号、姓名、年龄、订单编号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.*,
	o.number
FROM
	USER u
LEFT OUTER JOIN
	orderlist o
ON
	u.id = o.uid;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询用户年龄大于 23 岁的信息，显示用户的编号、姓名、年龄、订单编号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.*,
	o.number
FROM
	USER u,
	orderlist o
WHERE
	u.id = o.uid
	AND
	u.age &amp;gt; 23;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.*,
	o.number
FROM
	(SELECT * FROM USER WHERE age &amp;gt; 23) u,-- 嵌套查询
	orderlist o
WHERE
	u.id = o.uid;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询张三和李四用户的信息，显示用户的编号、姓名、年龄、订单编号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.*,
	o.number
FROM
	USER u,
	orderlist o
WHERE
	u.id=o.uid
	AND
	u.name IN (&apos;张三&apos;,&apos;李四&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询所有的用户和该用户能查看的所有的商品，显示用户的编号、姓名、年龄、商品名称&lt;/p&gt;
&lt;p&gt;数据：用户的编号、姓名、年龄在 user 表，商品名称在 product 表，中间表 us_pro&lt;/p&gt;
&lt;p&gt;条件：us_pro.uid = user.id AND us_pro.pid = product.id&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.id,
	u.name,
	u.age,
	p.name
FROM
	USER u,
	product p,
	us_pro up
WHERE
	up.uid = u.id
	AND
	up.pid=p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询张三和李四这两个用户可以看到的商品，显示用户的编号、姓名、年龄、商品名称。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	u.id,
	u.name,
	u.age,
	p.name
FROM
	USER u,
	product p,
	us_pro up
WHERE
	up.uid=u.id
	AND
	up.pid=p.id
	AND
	u.name IN (&apos;张三&apos;,&apos;李四&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;高级结构&lt;/h2&gt;
&lt;h3&gt;视图&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;视图概念：视图是一种虚拟存在的数据表，这个虚拟的表并不在数据库中实际存在&lt;/p&gt;
&lt;p&gt;本质：将一条 SELECT 查询语句的结果封装到了一个虚拟表中，所以在创建视图的时候，工作重心要放在这条 SELECT 查询语句上&lt;/p&gt;
&lt;p&gt;作用：将一些比较复杂的查询语句的结果，封装到一个虚拟表中，再有相同查询需求时，直接查询该虚拟表&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;简单：使用视图的用户不需要关心表的结构、关联条件和筛选条件，因为虚拟表中已经是过滤好的结果集&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全：使用视图的用户只能访问查询的结果集，对表的权限管理并不能限制到某个行某个列&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据独立，一旦视图的结构确定，可以屏蔽表结构变化对用户的影响，源表增加列对视图没有影响；源表修改列名，则可以通过修改视图来解决，不会造成对访问者的影响&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;视图创建&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建视图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE [OR REPLACE] 
VIEW 视图名称 [(列名列表)] 
AS 查询语句
[WITH [CASCADED | LOCAL] CHECK OPTION];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WITH [CASCADED | LOCAL] CHECK OPTION&lt;/code&gt; 决定了是否允许更新数据使记录不再满足视图的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LOCAL：只要满足本视图的条件就可以更新&lt;/li&gt;
&lt;li&gt;CASCADED：必须满足所有针对该视图的所有视图的条件才可以更新， 默认值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 数据准备 city
id	NAME	cid
1	深圳	 	1
2	上海		1
3	纽约		2
4	莫斯科	    3

-- 数据准备 country
id	NAME
1	中国
2	美国
3	俄罗斯

-- 创建city_country视图，保存城市和国家的信息(使用指定列名)
CREATE 
VIEW 
	city_country (city_id,city_name,country_name)
AS
    SELECT
        c1.id,
        c1.name,
        c2.name
    FROM
        city c1,
        country c2
    WHERE
        c1.cid=c2.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;视图查询&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询所有数据表，视图也会查询出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW TABLES;
SHOW TABLE STATUS [\G];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询视图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM 视图名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询某个视图创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW CREATE VIEW 视图名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;视图修改&lt;/h4&gt;
&lt;p&gt;视图表数据修改，会&lt;strong&gt;自动修改源表中的数据&lt;/strong&gt;，因为更新的是视图中的基表中的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改视图表中的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE 视图名称 SET 列名 = 值 WHERE 条件;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改视图的结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
VIEW 视图名称 [(列名列表)] 
AS 查询语句
[WITH [CASCADED | LOCAL] CHECK OPTION]

-- 将视图中的country_name修改为name
ALTER 
VIEW 
	city_country (city_id,city_name,name) 
AS
    SELECT
        c1.id,
        c1.name,
        c2.name
    FROM
        city c1,
        country c2
    WHERE
        c1.cid=c2.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;视图删除&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;删除视图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP VIEW 视图名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果存在则删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP VIEW IF EXISTS 视图名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;存储过程&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;存储过程和函数：存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合&lt;/p&gt;
&lt;p&gt;存储过程和函数的好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提高代码的复用性&lt;/li&gt;
&lt;li&gt;减少数据在数据库和应用服务器之间的传输，提高传输效率&lt;/li&gt;
&lt;li&gt;减少代码层面的业务处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一次编译永久有效&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储过程和函数的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存储函数必须有返回值&lt;/li&gt;
&lt;li&gt;存储过程可以没有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;p&gt;DELIMITER：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DELIMITER 关键字用来声明 sql 语句的分隔符，告诉 MySQL 该段命令已经结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 语句默认的分隔符是分号，但是有时需要一条功能 sql 语句中包含分号，但是并不作为结束标识，这时使用 DELIMITER 来指定分隔符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER 分隔符
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储过程的创建调用查看和删除：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建存储过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 修改分隔符为$
DELIMITER $

-- 标准语法
CREATE PROCEDURE 存储过程名称(参数...)
BEGIN
	sql语句;
END$

-- 修改分隔符为分号
DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用存储过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CALL 存储过程名称(实际参数);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看存储过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM mysql.proc WHERE db=&apos;数据库名称&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除存储过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP PROCEDURE [IF EXISTS] 存储过程名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;练习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id	NAME	age		gender	score
1	张三		23		男		95
2	李四		24		男		98
3	王五		25		女		100
4	赵六		26		女		90
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 stu_group() 存储过程，封装分组查询总成绩，并按照总成绩升序排序的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE PROCEDURE stu_group()
BEGIN
	SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; 
END$

DELIMITER ;

-- 调用存储过程
CALL stu_group();
-- 删除存储过程
DROP PROCEDURE IF EXISTS stu_group;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;存储语法&lt;/h4&gt;
&lt;h5&gt;变量使用&lt;/h5&gt;
&lt;p&gt;存储过程是可以进行编程的，意味着可以使用变量、表达式、条件控制语句等，来完成比较复杂的功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义变量：DECLARE 定义的是局部变量，只能用在 BEGIN END 范围之内&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DECLARE 变量名 数据类型 [DEFAULT 默认值];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变量的赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET 变量名 = 变量值;
SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据准备：表 student&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id	NAME	age		gender	score
1	张三		23		男		95
2	李四		24		男		98
3	王五		25		女		100
4	赵六		26		女		90
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义两个 int 变量，用于存储男女同学的总分数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test3()
BEGIN
	-- 定义两个变量
	DECLARE men,women INT;
	-- 查询男同学的总分数，为men赋值
	SELECT SUM(score) INTO men FROM student WHERE gender=&apos;男&apos;;
	-- 查询女同学的总分数，为women赋值
	SELECT SUM(score) INTO women FROM student WHERE gender=&apos;女&apos;;
	-- 使用变量
	SELECT men,women;
END$
DELIMITER ;
-- 调用存储过程
CALL pro_test3();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;IF语句&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;if 语句标准语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IF 判断条件1 THEN 执行的sql语句1;
[ELSEIF 判断条件2 THEN 执行的sql语句2;]
...
[ELSE 执行的sql语句n;]
END IF;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据准备：表 student&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id	NAME	age		gender	score
1	张三		23		男		95
2	李四		24		男		98
3	王五		25		女		100
4	赵六		26		女		90
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据总成绩判断：全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test4()
BEGIN
	DECLARE total INT;							-- 定义总分数变量
	DECLARE description VARCHAR(10);			-- 定义分数描述变量
	SELECT SUM(score) INTO total FROM student; 	-- 为总分数变量赋值
	-- 判断总分数
	IF total &amp;gt;= 380 THEN
		SET description = &apos;学习优秀&apos;;
	ELSEIF total &amp;gt;=320 AND total &amp;lt; 380 THEN
		SET description = &apos;学习良好&apos;;
	ELSE
		SET description = &apos;学习一般&apos;;
	END IF;
END$
DELIMITER ;
-- 调用pro_test4存储过程
CALL pro_test4();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;参数传递&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数传递的语法&lt;/p&gt;
&lt;p&gt;IN：代表输入参数，需要由调用者传递实际数据，默认的
OUT：代表输出参数，该参数可以作为返回值
INOUT：代表既可以作为输入参数，也可以作为输出参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

-- 标准语法
CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型)
BEGIN
	执行的sql语句;
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入总成绩变量，代表学生总成绩，输出分数描述变量，代表学生总成绩的描述&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10))
BEGIN
	-- 判断总分数
	IF total &amp;gt;= 380 THEN 
		SET description = &apos;学习优秀&apos;;
	ELSEIF total &amp;gt;= 320 AND total &amp;lt; 380 THEN 
		SET description = &apos;学习不错&apos;;
	ELSE 
		SET description = &apos;学习一般&apos;;
	END IF;
END$

DELIMITER ;
-- 调用pro_test6存储过程
CALL pro_test6(310,@description);
CALL pro_test6((SELECT SUM(score) FROM student), @description);
-- 查询总成绩描述
SELECT @description;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看参数方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@变量名 : &lt;strong&gt;用户会话变量&lt;/strong&gt;，代表整个会话过程他都是有作用的，类似于全局变量&lt;/li&gt;
&lt;li&gt;@@变量名 : &lt;strong&gt;系统变量&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;CASE&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标准语法 1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CASE 表达式
    WHEN 值1 THEN 执行sql语句1;
    [WHEN 值2 THEN 执行sql语句2;]
    ...
    [ELSE 执行sql语句n;]
END CASE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标准语法 2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sCASE
    WHEN 判断条件1 THEN 执行sql语句1;
    [WHEN 判断条件2 THEN 执行sql语句2;]
    ...
    [ELSE 执行sql语句n;]
END CASE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;演示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test7(IN total INT)
BEGIN
	-- 定义变量
	DECLARE description VARCHAR(10);
	-- 使用case判断
	CASE
	WHEN total &amp;gt;= 380 THEN
		SET description = &apos;学习优秀&apos;;
	WHEN total &amp;gt;= 320 AND total &amp;lt; 380 THEN
		SET description = &apos;学习不错&apos;;
	ELSE 
		SET description = &apos;学习一般&apos;;
	END CASE;
	
	-- 查询分数描述信息
	SELECT description;
END$
DELIMITER ;
-- 调用pro_test7存储过程
CALL pro_test7(390);
CALL pro_test7((SELECT SUM(score) FROM student));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;WHILE&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;while 循环语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WHILE 条件判断语句 DO
	循环体语句;
	条件控制语句;
END WHILE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 1~100 之间的偶数和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test6()
BEGIN
	-- 定义求和变量
	DECLARE result INT DEFAULT 0;
	-- 定义初始化变量
	DECLARE num INT DEFAULT 1;
	-- while循环
	WHILE num &amp;lt;= 100 DO
		IF num % 2 = 0 THEN
			SET result = result + num;
		END IF;
		SET num = num + 1;
	END WHILE;
	-- 查询求和结果
	SELECT result;
END$
DELIMITER ;

-- 调用pro_test6存储过程
CALL pro_test6();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;REPEAT&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;repeat 循环标准语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化语句;
REPEAT
	循环体语句;
	条件控制语句;
	UNTIL 条件判断语句
END REPEAT;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 1~10 之间的和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test9()
BEGIN
	-- 定义求和变量
	DECLARE result INT DEFAULT 0;
	-- 定义初始化变量
	DECLARE num INT DEFAULT 1;
	-- repeat循环
	REPEAT
		-- 累加
		SET result = result + num;
		-- 让num+1
		SET num = num + 1;
		-- 停止循环
		UNTIL num &amp;gt; 10
	END REPEAT;
	-- 查询求和结果
	SELECT result;
END$

DELIMITER ;
-- 调用pro_test9存储过程
CALL pro_test9();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;LOOP&lt;/h5&gt;
&lt;p&gt;LOOP 实现简单的循环，退出循环的条件需要使用其他的语句定义，通常可以使用 LEAVE 语句实现，如果不加退出循环的语句，那么就变成了死循环&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;loop 循环标准语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[循环名称:] LOOP
	条件判断语句
		[LEAVE 循环名称;]
	循环体语句;
	条件控制语句;
END LOOP 循环名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 1~10 之间的和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE PROCEDURE pro_test10()
BEGIN
	-- 定义求和变量
	DECLARE result INT DEFAULT 0;
	-- 定义初始化变量
	DECLARE num INT DEFAULT 1;
	-- loop循环
	l:LOOP
		-- 条件成立，停止循环
		IF num &amp;gt; 10 THEN
			LEAVE l;
		END IF;
		-- 累加
		SET result = result + num;
		-- 让num+1
		SET num = num + 1;
	END LOOP l;
	-- 查询求和结果
	SELECT result;
END$
DELIMITER ;
-- 调用pro_test10存储过程
CALL pro_test10();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;游标&lt;/h5&gt;
&lt;p&gt;游标是用来存储查询结果集的数据类型，在存储过程和函数中可以使用光标对结果集进行循环的处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;游标可以遍历返回的多行结果，每次拿到一整行数据&lt;/li&gt;
&lt;li&gt;简单来说游标就类似于集合的迭代器遍历&lt;/li&gt;
&lt;li&gt;MySQL 中的游标只能用在存储过程和函数中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游标的语法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建游标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DECLARE 游标名称 CURSOR FOR 查询sql语句;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开游标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OPEN 游标名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用游标获取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FETCH 游标名称 INTO 变量名1,变量名2,...;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭游标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLOSE 游标名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mysql 通过一个 Error handler 声明来判断指针是否到尾部，并且必须和创建游标的 SQL 语句声明在一起：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DECLARE EXIT HANDLER FOR NOT FOUND (do some action，一般是设置标志变量)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游标的基本使用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备：表 student&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id	NAME	age		gender	score
1	张三		23		男		95
2	李四		24		男		98
3	王五		25		女		100
4	赵六		26		女		90
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 stu_score 表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE stu_score(
	id INT PRIMARY KEY AUTO_INCREMENT,
	score INT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将student表中所有的成绩保存到stu_score表中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE PROCEDURE pro_test12()
BEGIN
	-- 定义成绩变量
	DECLARE s_score INT;
	-- 定义标记变量
	DECLARE flag INT DEFAULT 0;
	
	-- 创建游标，查询所有学生成绩数据
	DECLARE stu_result CURSOR FOR SELECT score FROM student;
	-- 游标结束后，将标记变量改为1  这两个必须声明在一起
	DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1;
	
	-- 开启游标
	OPEN stu_result;
	-- 循环使用游标
	REPEAT
		-- 使用游标，遍历结果,拿到数据
		FETCH stu_result INTO s_score;
		-- 将数据保存到stu_score表中
		INSERT INTO stu_score VALUES (NULL,s_score);
	UNTIL flag=1
	END REPEAT;
	-- 关闭游标
	CLOSE stu_result;
END$

DELIMITER ;

-- 调用pro_test12存储过程
CALL pro_test12();
-- 查询stu_score表
SELECT * FROM stu_score;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;存储函数&lt;/h4&gt;
&lt;p&gt;存储函数和存储过程是非常相似的，存储函数可以做的事情，存储过程也可以做到&lt;/p&gt;
&lt;p&gt;存储函数有返回值，存储过程没有返回值（参数的 out 其实也相当于是返回数据了）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建存储函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
-- 标准语法
CREATE FUNCTION 函数名称(参数 数据类型)
RETURNS 返回值类型
BEGIN
	执行的sql语句;
	RETURN 结果;
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用存储函数，因为有返回值，所以使用 SELECT 调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT 函数名称(实际参数);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除存储函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP FUNCTION 函数名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义存储函数，获取学生表中成绩大于95分的学生数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $
CREATE FUNCTION fun_test()
RETURN INT
BEGIN
	-- 定义统计变量
	DECLARE result INT;
	-- 查询成绩大于95分的学生数量，给统计变量赋值
	SELECT COUNT(score) INTO result FROM student WHERE score &amp;gt; 95;
	-- 返回统计结果
	SELECT result;
END
DELIMITER ;
-- 调用fun_test存储函数
SELECT fun_test();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;触发器&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;触发器是与表有关的数据库对象，在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容，这与其他的数据库是相似的&lt;/li&gt;
&lt;li&gt;现在触发器还只支持行级触发，不支持语句级触发&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;触发器类型&lt;/th&gt;
&lt;th&gt;OLD的含义&lt;/th&gt;
&lt;th&gt;NEW的含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INSERT 型触发器&lt;/td&gt;
&lt;td&gt;无 (因为插入前状态无数据)&lt;/td&gt;
&lt;td&gt;NEW 表示将要或者已经新增的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE 型触发器&lt;/td&gt;
&lt;td&gt;OLD 表示修改之前的数据&lt;/td&gt;
&lt;td&gt;NEW 表示将要或已经修改后的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE 型触发器&lt;/td&gt;
&lt;td&gt;OLD 表示将要或者已经删除的数据&lt;/td&gt;
&lt;td&gt;无 (因为删除后状态无数据)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建触发器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE TRIGGER 触发器名称
BEFORE|AFTER  INSERT|UPDATE|DELETE
ON 表名
[FOR EACH ROW]  -- 行级触发器
BEGIN
	触发器要执行的功能;
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看触发器的状态、语法等信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW TRIGGERS;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除触发器，如果没有指定 schema_name，默认为当前数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TRIGGER [schema_name.]trigger_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;触发演示&lt;/h4&gt;
&lt;p&gt;通过触发器记录账户表的数据变更日志。包含：增加、修改、删除&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建db9数据库
CREATE DATABASE db9;
-- 使用db9数据库
USE db9;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 创建账户表account
CREATE TABLE account(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 账户id
	NAME VARCHAR(20),					-- 姓名
	money DOUBLE						-- 余额
);
-- 添加数据
INSERT INTO account VALUES (NULL,&apos;张三&apos;,1000),(NULL,&apos;李四&apos;,2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 创建日志表account_log
CREATE TABLE account_log(
	id INT PRIMARY KEY AUTO_INCREMENT,	-- 日志id
	operation VARCHAR(20),				-- 操作类型 (insert update delete)
	operation_time DATETIME,			-- 操作时间
	operation_id INT,					-- 操作表的id
	operation_params VARCHAR(200)       -- 操作参数
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 INSERT 型触发器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE TRIGGER account_insert
AFTER INSERT
ON account
FOR EACH ROW
BEGIN
	INSERT INTO account_log VALUES (NULL,&apos;INSERT&apos;,NOW(),new.id,CONCAT(&apos;插入后{id=&apos;,new.id,&apos;,name=&apos;,new.name,&apos;,money=&apos;,new.money,&apos;}&apos;));
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 向account表添加记录
INSERT INTO account VALUES (NULL,&apos;王五&apos;,3000);

-- 查询日志表
SELECT * FROM account_log;
/*
id	operation	operation_time		operation_id	operation_params
1	INSERT	   	2021-01-26 19:51:11		3	     插入后{id=3,name=王五money=2000}
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 UPDATE 型触发器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE TRIGGER account_update
AFTER UPDATE
ON account
FOR EACH ROW
BEGIN
	INSERT INTO account_log VALUES (NULL,&apos;UPDATE&apos;,NOW(),new.id,CONCAT(&apos;修改前{id=&apos;,old.id,&apos;,name=&apos;,old.name,&apos;,money=&apos;,old.money,&apos;}&apos;,&apos;修改后{id=&apos;,new.id,&apos;,name=&apos;,new.name,&apos;,money=&apos;,new.money,&apos;}&apos;));
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 修改account表
UPDATE account SET money=3500 WHERE id=3;

-- 查询日志表
SELECT * FROM account_log;
/*
id	operation	operation_time		operation_id	  operation_params
2	UPDATE	   	2021-01-26 19:58:54		2		 更新前{id=2,name=李四money=1000}
												 更新后{id=2,name=李四money=200}
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 DELETE 型触发器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELIMITER $

CREATE TRIGGER account_delete
AFTER DELETE
ON account
FOR EACH ROW
BEGIN
	INSERT INTO account_log VALUES (NULL,&apos;DELETE&apos;,NOW(),old.id,CONCAT(&apos;删除前{id=&apos;,old.id,&apos;,name=&apos;,old.name,&apos;,money=&apos;,old.money,&apos;}&apos;));
END$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 删除account表数据
DELETE FROM account WHERE id=3;

-- 查询日志表
SELECT * FROM account_log;
/*
id	operation	operation_time		operation_id	operation_params
3	DELETE		2021-01-26 20:02:48		3	    删除前{id=3,name=王五money=2000}
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;存储引擎&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;对比其他数据库，MySQL 的架构可以在不同场景应用并发挥良好作用，主要体现在存储引擎，插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离，可以针对不同的存储需求可以选择最优的存储引擎&lt;/p&gt;
&lt;p&gt;存储引擎的介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力，在 MySQL 中，将这些不同的技术及配套的功能称为存储引擎&lt;/li&gt;
&lt;li&gt;Oracle、SqlServer 等数据库只有一种存储引擎，MySQL &lt;strong&gt;提供了插件式的存储引擎架构&lt;/strong&gt;，所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能&lt;/li&gt;
&lt;li&gt;在关系型数据库中数据的存储是以表的形式存进行，所以存储引擎也称为表类型（存储和操作此表的类型）&lt;/li&gt;
&lt;li&gt;通过选择不同的引擎，能够获取最佳的方案,  也能够获得额外的速度或者功能，提高程序的整体效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 支持的存储引擎：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 支持的引擎包括：InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等&lt;/li&gt;
&lt;li&gt;MySQL5.5 之前的默认存储引擎是 MyISAM，5.5 之后就改为了 InnoDB&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;引擎对比&lt;/h3&gt;
&lt;p&gt;MyISAM 存储引擎：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特点：不支持事务和外键，读取速度快，节约资源&lt;/li&gt;
&lt;li&gt;应用场景：&lt;strong&gt;适用于读多写少的场景&lt;/strong&gt;，对事务的完整性要求不高，比如一些数仓、离线数据、支付宝的年度总结之类的场景，业务进行只读操作，查询起来会更快&lt;/li&gt;
&lt;li&gt;存储方式：
&lt;ul&gt;
&lt;li&gt;每个 MyISAM 在磁盘上存储成 3 个文件，其文件名都和表名相同，拓展名不同&lt;/li&gt;
&lt;li&gt;表的定义保存在 .frm 文件，表数据保存在 .MYD (MYData) 文件中，索引保存在 .MYI (MYIndex) 文件中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB 存储引擎：(MySQL5.5 版本后默认的存储引擎)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特点：&lt;strong&gt;支持事务&lt;/strong&gt;和外键操作，支持并发控制。对比 MyISAM 的存储引擎，InnoDB 写的处理效率差一些，并且会占用更多的磁盘空间以保留数据和索引&lt;/li&gt;
&lt;li&gt;应用场景：对事务的完整性有比较高的要求，在并发条件下要求数据的一致性，读写频繁的操作&lt;/li&gt;
&lt;li&gt;存储方式：
&lt;ul&gt;
&lt;li&gt;使用共享表空间存储， 这种方式创建的表的表结构保存在 .frm 文件中， 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中，可以是多个文件&lt;/li&gt;
&lt;li&gt;使用多表空间存储，创建的表的表结构存在 .frm 文件中，每个表的数据和索引单独保存在 .ibd 中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MEMORY 存储引擎：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特点：每个 MEMORY 表实际对应一个磁盘文件 ，该文件中只存储表的结构，表数据保存在内存中，且默认&lt;strong&gt;使用 HASH 索引&lt;/strong&gt;，所以数据默认就是无序的，但是在需要快速定位记录可以提供更快的访问，&lt;strong&gt;服务一旦关闭，表中的数据就会丢失&lt;/strong&gt;，存储不安全&lt;/li&gt;
&lt;li&gt;应用场景：&lt;strong&gt;缓存型存储引擎&lt;/strong&gt;，通常用于更新不太频繁的小表，用以快速得到访问结果&lt;/li&gt;
&lt;li&gt;存储方式：表结构保存在 .frm 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MERGE 存储引擎：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是一组 MyISAM 表的组合，这些 MyISAM 表必须结构完全相同，通过将不同的表分布在多个磁盘上&lt;/li&gt;
&lt;li&gt;MERGE 表本身并没有存储数据，对 MERGE 类型的表可以进行查询、更新、删除操作，这些操作实际上是对内部的 MyISAM 表进行的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用场景：将一系列等同的 MyISAM 表以逻辑方式组合在一起，并作为一个对象引用他们，适合做数据仓库&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入操作是通过 INSERT_METHOD 子句定义插入的表，使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上；不定义这个子句或者定义为 NO，表示不能对 MERGE 表执行插入操作&lt;/li&gt;
&lt;li&gt;对 MERGE 表进行 DROP 操作，但是这个操作只是删除 MERGE 表的定义，对内部的表是没有任何影响的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE order_1(
)ENGINE = MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE order_2(
)ENGINE = MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE order_all(
	-- 结构与MyISAM表相同
)ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MERGE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;MEMORY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;存储限制&lt;/td&gt;
&lt;td&gt;有（平台对文件系统大小的限制）&lt;/td&gt;
&lt;td&gt;64TB&lt;/td&gt;
&lt;td&gt;有（平台的内存限制）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;事务安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不支持&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;锁机制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;表锁&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;表锁/行锁&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;表锁&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B+Tree 索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;哈希索引&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全文索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;集群索引&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据索引&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据缓存&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引缓存&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据可压缩&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间使用&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存使用&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量插入速度&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;外键&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不支持&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只读场景 MyISAM 比 InnoDB 更快：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;底层存储结构有差别，MyISAM 是非聚簇索引，叶子节点保存的是数据的具体地址，不用回表查询&lt;/li&gt;
&lt;li&gt;InnoDB 每次查询需要维护 MVCC 版本状态，保证并发状态下的读写冲突问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;引擎操作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询数据库支持的存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW ENGINES;
SHOW VARIABLES LIKE &apos;%storage_engine%&apos;; -- 查看Mysql数据库默认的存储引擎 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询某个数据库中所有数据表的存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW TABLE STATUS FROM 数据库名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询某个数据库中某个数据表的存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = &apos;数据表名称&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据表，指定存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE 表名(
	列名,数据类型,
    ...
)ENGINE = 引擎名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改数据表的存储引擎&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE 表名 ENGINE = 引擎名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;索引机制&lt;/h2&gt;
&lt;h3&gt;索引介绍&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;MySQL 官方对索引的定义为：索引（index）是帮助 MySQL 高效获取数据的一种数据结构，**本质是排好序的快速查找数据结构。**在表数据之外，数据库系统还维护着满足特定查找算法的数据结构，这些数据结构以某种方式指向数据， 这样就可以在这些数据结构上实现高级查找算法，这种数据结构就是索引&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;索引是在存储引擎层实现的&lt;/strong&gt;，所以并没有统一的索引标准，即不同存储引擎的索引的工作方式并不一样&lt;/p&gt;
&lt;p&gt;索引使用：一张数据表，用于保存数据；一个索引配置文件，用于保存索引；每个索引都指向了某一个数据
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;左边是数据表，一共有两列七条记录，最左边的是数据记录的物理地址（注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的）。为了加快 Col2 的查找，可以维护一个右边所示的二叉查找树，每个节点分别包含索引键值和一个指向对应数据的物理地址的指针，这样就可以运用二叉查找快速获取到相应数据&lt;/p&gt;
&lt;p&gt;索引的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类似于书籍的目录索引，提高数据检索的效率，降低数据库的 IO 成本&lt;/li&gt;
&lt;li&gt;通过索引列对数据进行排序，降低数据排序的成本，降低 CPU 的消耗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;索引的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般来说索引本身也很大，不可能全部存储在内存中，因此索引往往以索引文件的形式&lt;strong&gt;存储在磁盘&lt;/strong&gt;上&lt;/li&gt;
&lt;li&gt;虽然索引大大提高了查询效率，同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作，MySQL 不仅要保存数据，还要保存一下索引文件每次更新添加了索引列的字段，还会调整因为更新所带来的键值变化后的索引信息，&lt;strong&gt;但是更新数据也需要先从数据库中获取&lt;/strong&gt;，索引加快了获取速度，所以可以相互抵消一下。&lt;/li&gt;
&lt;li&gt;索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;索引分类&lt;/h4&gt;
&lt;p&gt;索引一般的分类如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;功能分类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主键索引：一种特殊的唯一索引，不允许有空值，一般在建表时同时创建主键索引&lt;/li&gt;
&lt;li&gt;单列索引：一个索引只包含单个列，一个表可以有多个单列索引（普通索引）&lt;/li&gt;
&lt;li&gt;联合索引：顾名思义，就是将单列索引进行组合&lt;/li&gt;
&lt;li&gt;唯一索引：索引列的值必须唯一，&lt;strong&gt;允许有空值&lt;/strong&gt;，如果是联合索引，则列值组合必须唯一
&lt;ul&gt;
&lt;li&gt;NULL 值可以出现多次，因为两个 NULL 比较的结果既不相等，也不不等，结果仍然是未知&lt;/li&gt;
&lt;li&gt;可以声明不允许存储 NULL 值的非空唯一索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;外键索引：只有 InnoDB 引擎支持外键索引，用来保证数据的一致性、完整性和实现级联操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结构分类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BTree 索引：MySQL 使用最频繁的一个索引数据结构，是 InnoDB 和 MyISAM 存储引擎默认的索引类型，底层基于 B+Tree&lt;/li&gt;
&lt;li&gt;Hash 索引：MySQL中 Memory 存储引擎默认支持的索引类型&lt;/li&gt;
&lt;li&gt;R-tree 索引（空间索引）：空间索引是 MyISAM 引擎的一个特殊索引类型，主要用于地理空间数据类型&lt;/li&gt;
&lt;li&gt;Full-text 索引（全文索引）：快速匹配全部文档的方式。MyISAM 支持， InnoDB 不支持 FULLTEXT 类型的索引，但是 InnoDB 可以使用 sphinx 插件支持全文索引，MEMORY 引擎不支持&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;索引&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BTREE&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HASH&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R-tree&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text&lt;/td&gt;
&lt;td&gt;5.6 版本之后支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;联合索引图示：根据身高年龄建立的组合索引（height、age）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E7%BB%84%E5%90%88%E7%B4%A2%E5%BC%95%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;索引操作&lt;/h3&gt;
&lt;p&gt;索引在创建表的时候可以同时创建， 也可以随时增加新的索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建索引：如果一个表中有一列是主键，那么会&lt;strong&gt;默认为其创建主键索引&lt;/strong&gt;（主键列不需要单独创建索引）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...);
-- 索引类型默认是 B+TREE
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW INDEX FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 单列索引
ALTER TABLE 表名 ADD INDEX 索引名称(列名);

-- 组合索引
ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...);

-- 主键索引
ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); 

-- 外键索引(添加外键约束，就是外键索引)
ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名);

-- 唯一索引
ALTER TABLE 表名 ADD UNIQUE 索引名称(列名);

-- 全文索引(mysql只支持文本类型)
ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP INDEX 索引名称 ON 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;案例练习&lt;/p&gt;
&lt;p&gt;数据准备：student&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id	NAME	 age	score
1	张三		23		99
2	李四		24		95
3	王五		25		98
4	赵六		26		97
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;索引操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 为student表中姓名列创建一个普通索引
CREATE INDEX idx_name ON student(NAME);

-- 为student表中年龄列创建一个唯一索引
CREATE UNIQUE INDEX idx_age ON student(age);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;聚簇索引&lt;/h3&gt;
&lt;h4&gt;索引对比&lt;/h4&gt;
&lt;p&gt;聚簇索引是一种数据存储方式，并不是一种单独的索引类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;聚簇索引的叶子节点存放的是主键值和数据行，支持覆盖索引&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非聚簇索引的叶子节点存放的是主键值或指向数据行的指针（由存储引擎决定）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Innodb 下主键索引是聚簇索引，在 MyISAM 下主键索引是非聚簇索引&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Innodb&lt;/h4&gt;
&lt;h5&gt;聚簇索引&lt;/h5&gt;
&lt;p&gt;在 Innodb 存储引擎，B+ 树索引可以分为聚簇索引（也称聚集索引、clustered index）和辅助索引（也称非聚簇索引或二级索引、secondary index、non-clustered index）&lt;/p&gt;
&lt;p&gt;InnoDB 中，聚簇索引是按照每张表的主键构造一颗 B+ 树，叶子节点中存放的就是整张表的数据，将聚簇索引的叶子节点称为数据页&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个特性决定了&lt;strong&gt;数据也是索引的一部分&lt;/strong&gt;，所以一张表只能有一个聚簇索引&lt;/li&gt;
&lt;li&gt;辅助索引的存在不影响聚簇索引中数据的组织，所以一张表可以有多个辅助索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;聚簇索引的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据访问更快，聚簇索引将索引和数据保存在同一个 B+ 树中，因此从聚簇索引中获取数据比非聚簇索引更快&lt;/li&gt;
&lt;li&gt;聚簇索引对于主键的排序查找和范围查找速度非常快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;聚簇索引的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;插入速度严重依赖于插入顺序，按照主键的顺序（递增）插入是最快的方式，否则将会出现页分裂，严重影响性能，所以对于 InnoDB 表，一般都会定义一个自增的 ID 列为主键&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新主键的代价很高，将会导致被更新的行移动，所以对于 InnoDB 表，一般定义主键为不可更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二级索引访问需要两次索引查找，第一次找到主键值，第二次根据主键值找到行数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;辅助索引&lt;/h5&gt;
&lt;p&gt;在聚簇索引之上创建的索引称之为辅助索引，非聚簇索引都是辅助索引，像复合索引、前缀索引、唯一索引等&lt;/p&gt;
&lt;p&gt;辅助索引叶子节点存储的是主键值，而不是数据的物理地址，所以访问数据需要二次查找，推荐使用覆盖索引，可以减少回表查询&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;检索过程&lt;/strong&gt;：辅助索引找到主键值，再通过聚簇索引（二分）找到数据页，最后通过数据页中的 Page Directory（二分）找到对应的数据分组，遍历组内所所有的数据找到数据行&lt;/p&gt;
&lt;p&gt;补充：无索引走全表查询，查到数据页后和上述步骤一致&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;索引实现&lt;/h5&gt;
&lt;p&gt;InnoDB 使用 B+Tree 作为索引结构，并且 InnoDB 一定有索引&lt;/p&gt;
&lt;p&gt;主键索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 InnoDB 中，表数据文件本身就是按 B+Tree 组织的一个索引结构，这个索引的 key 是数据表的主键，叶子节点 data 域保存了完整的数据记录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InnoDB 的表数据文件&lt;strong&gt;通过主键聚集数据&lt;/strong&gt;，如果没有定义主键，会选择非空唯一索引代替，如果也没有这样的列，MySQL 会自动为 InnoDB 表生成一个&lt;strong&gt;隐含字段 row_id&lt;/strong&gt; 作为主键，这个字段长度为 6 个字节，类型为长整形&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;辅助索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;InnoDB 的所有辅助索引（二级索引）都引用主键作为 data 域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InnoDB 表是基于聚簇索引建立的，因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列，所以不建议使用过长的字段作为主键，&lt;strong&gt;过长的主索引会令辅助索引变得过大&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB%E8%81%9A%E7%B0%87%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;MyISAM&lt;/h4&gt;
&lt;h5&gt;非聚簇&lt;/h5&gt;
&lt;p&gt;MyISAM 的主键索引使用的是非聚簇索引，索引文件和数据文件是分离的，&lt;strong&gt;索引文件仅保存数据的地址&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主键索引 B+ 树的节点存储了主键，辅助键索引 B+ 树存储了辅助键，表数据存储在独立的地方，这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据，对于表数据来说，这两个键没有任何差别&lt;/li&gt;
&lt;li&gt;由于索引树是独立的，通过辅助索引检索&lt;strong&gt;无需回表查询&lt;/strong&gt;访问主键的索引树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E6%A3%80%E9%94%81%E6%95%B0%E6%8D%AE%E5%9B%BE.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;索引实现&lt;/h5&gt;
&lt;p&gt;MyISAM 的索引方式也叫做非聚集的，之所以这么称呼是为了与 InnoDB 的聚集索引区分&lt;/p&gt;
&lt;p&gt;主键索引：MyISAM 引擎使用 B+Tree 作为索引结构，叶节点的 data 域存放的是数据记录的地址&lt;/p&gt;
&lt;p&gt;辅助索引：MyISAM 中主索引和辅助索引（Secondary key）在结构上没有任何区别，只是主索引要求 key 是唯一的，而辅助索引的 key 可以重复&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM%E4%B8%BB%E9%94%AE%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/lm1060891265/article/details/81482136&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;索引结构&lt;/h3&gt;
&lt;h4&gt;数据页&lt;/h4&gt;
&lt;p&gt;文件系统的最小单元是块（block），一个块的大小是 4K，系统从磁盘读取数据到内存时是以磁盘块为基本单位的，位于同一个磁盘块中的数据会被一次性读取出来，而不是需要什么取什么&lt;/p&gt;
&lt;p&gt;InnoDB 存储引擎中有页（Page）的概念，页是 MySQL 磁盘管理的最小单位&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;InnoDB 存储引擎中默认每个页的大小为 16KB，索引中一个节点就是一个数据页&lt;/strong&gt;，所以会一次性读取 16KB 的数据到内存&lt;/li&gt;
&lt;li&gt;InnoDB 引擎将若干个地址连接磁盘块，以此来达到页的大小 16KB&lt;/li&gt;
&lt;li&gt;在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置，这将会减少磁盘 I/O 次数，提高查询效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;超过 16KB 的一条记录，主键索引页只会存储部分数据和指向&lt;strong&gt;溢出页&lt;/strong&gt;的指针，剩余数据都会分散存储在溢出页中&lt;/p&gt;
&lt;p&gt;数据页物理结构，从上到下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File Header：上一页和下一页的指针、该页的类型（索引页、数据页、日志页等）、&lt;strong&gt;校验和&lt;/strong&gt;、LSN（最近一次修改当前页面时的系统 lsn 值，事务持久性部分详解）等信息&lt;/li&gt;
&lt;li&gt;Page Header：记录状态信息&lt;/li&gt;
&lt;li&gt;Infimum + Supremum：当前页的最小记录和最大记录（头尾指针），Infimum 所在分组只有一条记录，Supremum 所在分组可以有 1 ~ 8 条记录，剩余的分组可以有 4 ~ 8 条记录&lt;/li&gt;
&lt;li&gt;User Records：存储数据的记录&lt;/li&gt;
&lt;li&gt;Free Space：尚未使用的存储空间&lt;/li&gt;
&lt;li&gt;Page Directory：分组的目录，可以通过目录快速定位（二分法）数据的分组&lt;/li&gt;
&lt;li&gt;File Trailer：检验和字段，在刷脏过程中，页首和页尾的校验和一致才能说明页面刷新成功，二者不同说明刷新期间发生了错误；LSN 字段，也是用来校验页面的完整性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据页中包含数据行，数据的存储是基于数据行的，数据行有 next_record 属性指向下一个行数据，所以是可以遍历的，但是一组数据至多 8 个行，通过 Page Directory 先定位到组，然后遍历获取所需的数据行即可&lt;/p&gt;
&lt;p&gt;数据行中有三个隐藏字段：trx_id、roll_pointer、row_id（在事务章节会详细介绍它们的作用）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;BTree&lt;/h4&gt;
&lt;p&gt;BTree 的索引类型是基于 B+Tree 树型数据结构的，B+Tree 又是 BTree 数据结构的变种，用在数据库和操作系统中的文件系统，特点是能够保持数据稳定有序&lt;/p&gt;
&lt;p&gt;BTree 又叫多路平衡搜索树，一颗 m 叉的 BTree 特性如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树中每个节点最多包含 m 个孩子&lt;/li&gt;
&lt;li&gt;除根节点与叶子节点外，每个节点至少有 [ceil(m/2)] 个孩子&lt;/li&gt;
&lt;li&gt;若根节点不是叶子节点，则至少有两个孩子&lt;/li&gt;
&lt;li&gt;所有的叶子节点都在同一层&lt;/li&gt;
&lt;li&gt;每个非叶子节点由 n 个 key 与 n+1 个指针组成，其中 [ceil(m/2)-1] &amp;lt;= n &amp;lt;= m-1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;5 叉，key 的数量 [ceil(m/2)-1] &amp;lt;= n &amp;lt;= m-1 为 2 &amp;lt;= n &amp;lt;=4 ，当 n&amp;gt;4 时中间节点分裂到父节点，两边节点分裂&lt;/p&gt;
&lt;p&gt;插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;插入前 4 个字母 C N G A&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入 H，n&amp;gt;4，中间元素 G 字母向上分裂到新的节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入 E、K、Q 不需要分裂&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入 M，中间元素 M 字母向上分裂到父节点 G&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入 F，W，L，T 不需要分裂&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入 Z，中间元素 T 向上分裂到父节点中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;插入 D，中间元素 D 向上分裂到父节点中，然后插入 P，R，X，Y 不需要分裂&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后插入 S，NPQR 节点 n&amp;gt;5，中间节点 Q 向上分裂，但分裂后父节点 DGMT 的 n&amp;gt;5，中间节点 M 向上分裂&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BTree 树就已经构建完成了，BTree 树和二叉树相比， 查询数据的效率更高， 因为对于相同的数据量来说，&lt;strong&gt;BTree 的层级结构比二叉树少&lt;/strong&gt;，所以搜索速度快&lt;/p&gt;
&lt;p&gt;BTree 结构的数据可以让系统高效的找到数据所在的磁盘块，定义一条记录为一个二元组 [key, data] ，key 为记录的键值，对应表中的主键值，data 为一行记录中除主键外的数据。对于不同的记录，key 值互不相同，BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;缺点：当进行范围查找时会出现回旋查找&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;B+Tree&lt;/h4&gt;
&lt;h5&gt;数据结构&lt;/h5&gt;
&lt;p&gt;BTree 数据结构中每个节点中不仅包含数据的 key 值，还有 data 值。磁盘中每一页的存储空间是有限的，如果 data 数据较大时将会导致每个节点（即一个页）能存储的 key 的数量很小，当存储的数据量很大时同样会导致 B-Tree 的深度较大，增大查询时的磁盘 I/O 次数，进而影响查询效率，所以引入 B+Tree&lt;/p&gt;
&lt;p&gt;B+Tree 为 BTree 的变种，B+Tree 与 BTree 的区别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;n 叉 B+Tree 最多含有 n 个 key（哈希值），而 BTree 最多含有 n-1 个 key&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;所有&lt;strong&gt;非叶子节点只存储键值 key&lt;/strong&gt; 信息，只进行数据索引，使每个非叶子节点所能保存的关键字大大增加&lt;/li&gt;
&lt;li&gt;所有&lt;strong&gt;数据都存储在叶子节点&lt;/strong&gt;，所以每次数据查询的次数都一样&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶子节点按照 key 大小顺序排列，左边结尾数据都会保存右边节点开始数据的指针，形成一个链表&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;所有节点中的 key 在叶子节点中也存在（比如 5)，&lt;strong&gt;key 允许重复&lt;/strong&gt;，B 树不同节点不存在重复的 key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-B加Tree数据结构.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;B* 树：是 B+ 树的变体，在 B+ 树的非根和非叶子结点再增加指向兄弟的指针&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;优化结构&lt;/h5&gt;
&lt;p&gt;MySQL 索引数据结构对经典的 B+Tree 进行了优化，在原 B+Tree 的基础上，增加一个指向相邻叶子节点的链表指针，就形成了带有顺序指针的 B+Tree，&lt;strong&gt;提高区间访问的性能，防止回旋查找&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;区间访问的意思是访问索引为 5 - 15 的数据，可以直接根据相邻节点的指针遍历&lt;/p&gt;
&lt;p&gt;B+ 树的&lt;strong&gt;叶子节点是数据页&lt;/strong&gt;（page），一个页里面可以存多个数据行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通常在 B+Tree 上有两个头指针，&lt;strong&gt;一个指向根节点，另一个指向关键字最小的叶子节点&lt;/strong&gt;，而且所有叶子节点（即数据节点）之间是一种链式环结构。可以对 B+Tree 进行两种查找运算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有范围：对于主键的范围查找和分页查找&lt;/li&gt;
&lt;li&gt;有顺序：从根节点开始，进行随机查找，顺序查找&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB 中每个数据页的大小默认是 16KB，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引行：一般表的主键类型为 INT（4 字节）或 BIGINT（8 字节），指针大小在 InnoDB 中设置为 6 字节节，也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值（估值）。则一个深度为 3 的 B+Tree 索引可以维护 &lt;code&gt;10^3 * 10^3 * 10^3 = 10亿&lt;/code&gt; 条记录&lt;/li&gt;
&lt;li&gt;数据行：一行数据的大小可能是 1k，一个数据页可以存储 16 行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际情况中每个节点可能不能填充满，因此在数据库中，B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是&lt;strong&gt;将根节点常驻内存的&lt;/strong&gt;，也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作&lt;/p&gt;
&lt;p&gt;B+Tree 优点：提高查询速度，减少磁盘的 IO 次数，树形结构较小&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;索引维护&lt;/h5&gt;
&lt;p&gt;B+ 树为了保持索引的有序性，在插入新值的时候需要做相应的维护&lt;/p&gt;
&lt;p&gt;每个索引中每个块存储在磁盘页中，可能会出现以下两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果所在的数据页已经满了，这时候需要申请一个新的数据页，然后挪动部分数据过去，这个过程称为&lt;strong&gt;页分裂&lt;/strong&gt;，原本放在一个页的数据现在分到两个页中，降低了空间利用率&lt;/li&gt;
&lt;li&gt;当相邻两个页由于删除了数据，利用率很低之后，会将数据页做&lt;strong&gt;页合并&lt;/strong&gt;，合并的过程可以认为是分裂过程的逆过程&lt;/li&gt;
&lt;li&gt;这两个情况都是由 B+ 树的结构决定的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般选用数据小的字段做索引，字段长度越小，普通索引的叶子节点就越小，普通索引占用的空间也就越小&lt;/p&gt;
&lt;p&gt;自增主键的插入数据模式，可以让主键索引尽量地保持递增顺序插入，不涉及到挪动其他记录，&lt;strong&gt;避免了页分裂&lt;/strong&gt;，页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大&lt;/p&gt;
&lt;p&gt;参考文章：https://developer.aliyun.com/article/919861&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;设计原则&lt;/h3&gt;
&lt;p&gt;索引的设计可以遵循一些已有的原则，创建索引的时候请尽量考虑符合这些原则，便于提升索引的使用效率&lt;/p&gt;
&lt;p&gt;创建索引时的原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对查询频次较高，且数据量比较大的表建立索引&lt;/li&gt;
&lt;li&gt;使用唯一索引，区分度越高，使用索引的效率越高&lt;/li&gt;
&lt;li&gt;索引字段的选择，最佳候选列应当从 where 子句的条件中提取，使用覆盖索引&lt;/li&gt;
&lt;li&gt;使用短索引，索引创建之后也是使用硬盘来存储的，因此提升索引访问的 I/O 效率，也可以提升总体的访问效率。假如构成索引的字段总长度比较短，那么在给定大小的存储块内可以存储更多的索引值，相应的可以有效的提升 MySQL 访问索引的 I/O 效率&lt;/li&gt;
&lt;li&gt;索引可以有效的提升查询数据的效率，但索引数量不是多多益善，索引越多，维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说，索引过多，会引入相当高的维护代价，降低 DML 操作的效率，增加相应操作的时间消耗；另外索引过多的话，MySQL 也会犯选择困难病，虽然最终仍然会找到一个可用的索引，但提高了选择的代价&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 建立联合索引时会遵守&lt;strong&gt;最左前缀匹配原则&lt;/strong&gt;，即最左优先，在检索数据时从联合索引的最左边开始匹配&lt;/p&gt;
&lt;p&gt;N 个列组合而成的组合索引，相当于创建了 N 个索引，如果查询时 where 句中使用了组成该索引的&lt;strong&gt;前&lt;/strong&gt;几个字段，那么这条查询 SQL 可以利用组合索引来提升查询效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 对name、address、phone列建一个联合索引
ALTER TABLE user ADD INDEX index_three(name,address,phone);
-- 查询语句执行时会依照最左前缀匹配原则，检索时分别会使用索引进行数据匹配。
(name,address,phone)
(name,address)
(name,phone)	-- 只有name字段走了索引
(name)

-- 索引的字段可以是任意顺序的，优化器会帮助我们调整顺序，下面的SQL语句可以命中索引
SELECT * FROM user WHERE address = &apos;北京&apos; AND phone = &apos;12345&apos; AND name = &apos;张三&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 如果联合索引中最左边的列不包含在条件查询中，SQL语句就不会命中索引，比如：
SELECT * FROM user WHERE address = &apos;北京&apos; AND phone = &apos;12345&apos;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哪些情况不要建立索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记录太少的表&lt;/li&gt;
&lt;li&gt;经常增删改的表&lt;/li&gt;
&lt;li&gt;频繁更新的字段不适合创建索引&lt;/li&gt;
&lt;li&gt;where 条件里用不到的字段不创建索引&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;索引优化&lt;/h3&gt;
&lt;h4&gt;覆盖索引&lt;/h4&gt;
&lt;p&gt;覆盖索引：包含所有满足查询需要的数据的索引（SELECT 后面的字段刚好是索引字段），可以利用该索引返回 SELECT 列表的字段，而不必根据索引去聚簇索引上读取数据文件&lt;/p&gt;
&lt;p&gt;回表查询：要查找的字段不在非主键索引树上时，需要通过叶子节点的主键值去主键索引上获取对应的行数据&lt;/p&gt;
&lt;p&gt;使用覆盖索引，防止回表查询：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表 user 主键为 id，普通索引为 age，查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM user WHERE age = 30;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询过程：先通过普通索引 age=30 定位到主键值 id=1，再通过聚集索引 id=1 定位到行记录数据，需要两次扫描 B+ 树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用覆盖索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP INDEX idx_age ON user;
CREATE INDEX idx_age_name ON user(age,name);
SELECT id,age FROM user WHERE age = 30;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一棵索引树上就能获取查询所需的数据，无需回表速度更快&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用覆盖索引，要注意 SELECT 列表中只取出需要的列，不可用 SELECT *，所有字段一起做索引会导致索引文件过大，查询性能下降&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;索引下推&lt;/h4&gt;
&lt;p&gt;索引条件下推优化（Index Condition Pushdown，ICP）是 MySQL5.6 添加，可以在索引遍历过程中，对索引中包含的字段先做判断，直接过滤掉不满足条件的记录，减少回表次数&lt;/p&gt;
&lt;p&gt;索引下推充分利用了索引中的数据，在查询出整行数据之前过滤掉无效的数据，再去主键索引树上查找&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不使用索引下推优化时存储引擎通过索引检索到数据，然后回表查询记录返回给 Server 层，&lt;strong&gt;服务器判断数据是否符合条件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%B8%8D%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用索引下推优化时，如果&lt;strong&gt;存在某些被索引的列的判断条件&lt;/strong&gt;时，由存储引擎在索引遍历的过程中判断数据是否符合传递的条件，将符合条件的数据进行回表，检索出来返回给服务器，由此减少 IO 次数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;适用条件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要存储引擎将索引中的数据与条件进行判断（所以&lt;strong&gt;条件列必须都在同一个索引中&lt;/strong&gt;），所以优化是基于存储引擎的，只有特定引擎可以使用，适用于 InnoDB 和 MyISAM&lt;/li&gt;
&lt;li&gt;存储引擎没有调用跨存储引擎的能力，跨存储引擎的功能有存储过程、触发器、视图，所以调用这些功能的不可以进行索引下推优化&lt;/li&gt;
&lt;li&gt;对于 InnoDB 引擎只适用于二级索引，InnoDB 的聚簇索引会将整行数据读到缓冲区，不再需要去回表查询了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作过程：用户表 user，(name, age) 是联合索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM user WHERE name LIKE &apos;张%&apos; AND　age = 10;	-- 头部模糊匹配会造成索引失效
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优化前：在非主键索引树上找到满足第一个条件的行，然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据，再对比 AND 后的条件是否符合，符合返回数据，需要 4 次回表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8%E4%BC%98%E5%8C%961.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化后：检查索引中存储的列信息是否符合索引条件，然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求，&lt;strong&gt;不满足条件的不去读取表中的数据&lt;/strong&gt;，满足下推条件的就根据主键值进行回表查询，2 次回表
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8%E4%BC%98%E5%8C%962.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当使用 EXPLAIN 进行分析时，如果使用了索引条件下推，Extra 会显示 Using index condition&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/sinat_29774479/article/details/103470244&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/69636&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;前缀索引&lt;/h4&gt;
&lt;p&gt;当要索引的列字符很多时，索引会变大变慢，可以只索引列开始的部分字符串，节约索引空间，提高索引效率&lt;/p&gt;
&lt;p&gt;注意：使用前缀索引就系统就忽略覆盖索引对查询性能的优化了&lt;/p&gt;
&lt;p&gt;优化原则：&lt;strong&gt;降低重复的索引值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如地区表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area			gdp		code
chinaShanghai	100		aaa
chinaDalian		200		bbb
usaNewYork		300		ccc
chinaFuxin		400		ddd
chinaBeijing	500		eee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现 area 字段很多都是以 china 开头的，那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况，索引值重复性越低，查询效率也就越高，所以需要建立前 6 位字符的索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX idx_area ON table_name(area(7));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;场景：存储身份证&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接创建完整索引，这样可能比较占用空间&lt;/li&gt;
&lt;li&gt;创建前缀索引，节省空间，但会增加查询扫描次数，并且不能使用覆盖索引&lt;/li&gt;
&lt;li&gt;倒序存储，再创建前缀索引，用于绕过字符串本身前缀的区分度不够的问题（前 6 位相同的很多）&lt;/li&gt;
&lt;li&gt;创建 hash 字段索引，查询性能稳定，有额外的存储和计算消耗，跟第三种方式一样，都不支持范围扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;索引合并&lt;/h4&gt;
&lt;p&gt;使用多个索引来完成一次查询的执行方法叫做索引合并 index merge&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Intersection 索引合并：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM table_test WHERE key1 = &apos;a&apos; AND key3 = &apos;b&apos;; # key1 和 key3 列都是单列索引、二级索引
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从不同索引中扫描到的记录的 id 值取&lt;strong&gt;交集&lt;/strong&gt;（相同 id），然后执行回表操作，要求从每个二级索引获取到的记录都是按照主键值排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Union 索引合并：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM table_test WHERE key1 = &apos;a&apos; OR key3 = &apos;b&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从不同索引中扫描到的记录的 id 值取&lt;strong&gt;并集&lt;/strong&gt;，然后执行回表操作，要求从每个二级索引获取到的记录都是按照主键值排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sort-Union 索引合并&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM table_test WHERE key1 &amp;lt; &apos;a&apos; OR key3 &amp;gt; &apos;b&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先将从不同索引中扫描到的记录的主键值进行排序，再按照 Union 索引合并的方式进行查询&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;索引合并算法的效率并不好，通过将其中的一个索引改成联合索引会优化效率&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;系统优化&lt;/h2&gt;
&lt;h3&gt;表优化&lt;/h3&gt;
&lt;h4&gt;分区表&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;分区表是将大表的数据按分区字段分成许多小的子集，建立一个以 ftime 年份为分区的表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `t` (
    `ftime` datetime NOT NULL,
    `c` int(11) DEFAULT NULL,
    KEY (`ftime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
 PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
 PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
 PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
INSERT INTO t VALUES(&apos;2017-4-1&apos;,1),(&apos;2018-4-1&apos;,1);-- 这两行记录分别落在 p_2018 和 p_2019 这两个分区上
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个表包含了一个.frm 文件和 4 个.ibd 文件，每个分区对应一个.ibd 文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于引擎层来说，这是 4 个表，针对每个分区表的操作不会相互影响&lt;/li&gt;
&lt;li&gt;对于 Server 层来说，这是 1 个表&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;分区策略&lt;/h5&gt;
&lt;p&gt;打开表行为：第一次访问一个分区表时，MySQL 需要&lt;strong&gt;把所有的分区都访问一遍&lt;/strong&gt;，如果分区表的数量很多，超过了 open_files_limit 参数（默认值 1024），那么就会在访问这个表时打开所有的文件，导致打开表文件的个数超过了上限而报错&lt;/p&gt;
&lt;p&gt;通用分区策略：MyISAM 分区表使用的分区策略，每次访问分区都由 Server 层控制，在文件管理、表管理的实现上很粗糙，因此有比较严重的性能问题&lt;/p&gt;
&lt;p&gt;本地分区策略：从 MySQL 5.7.9 开始，InnoDB 引擎内部自己管理打开分区的行为，InnoDB 引擎打开文件超过 innodb_open_files 时就会&lt;strong&gt;关掉一些之前打开的文件&lt;/strong&gt;，所以即使分区个数大于 open_files_limit，也不会报错&lt;/p&gt;
&lt;p&gt;从 MySQL 8.0 版本开始，就不允许创建 MyISAM 分区表，只允许创建已经实现了本地分区策略的引擎，目前只有 InnoDB 和 NDB 这两个引擎支持了本地分区策略&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;Server 层&lt;/h5&gt;
&lt;p&gt;从 Server 层看一个分区表就只是一个表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Session A：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM t WHERE ftime = &apos;2018-4-1&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session B：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE t TRUNCATE PARTITION p_2017; -- blocked
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现象：Session B 只操作 p_2017 分区，但是由于 Session A 持有整个表 t 的 MDL 读锁，就导致 B 的 ALTER 语句获取 MDL 写锁阻塞&lt;/p&gt;
&lt;p&gt;分区表的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次访问的时候需要访问所有分区&lt;/li&gt;
&lt;li&gt;在 Server 层认为这是同一张表，因此&lt;strong&gt;所有分区共用同一个 MDL 锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在引擎层认为这是不同的表，因此 MDL 锁之后的执行过程，会根据分区表规则，只访问需要的分区&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;应用场景&lt;/h5&gt;
&lt;p&gt;分区表的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对业务透明，相对于用户分表来说，使用分区表的业务代码更简洁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分区表可以很方便的清理历史数据。按照时间分区的分区表，就可以直接通过 &lt;code&gt;alter table t drop partition&lt;/code&gt; 这个语法直接删除分区文件，从而删掉过期的历史数据，与使用 drop 语句删除数据相比，优势是速度快、对系统影响小&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用分区表，不建议创建太多的分区，注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分区并不是越细越好，单表或者单分区的数据一千万行，只要没有特别大的索引，对于现在的硬件能力来说都已经是小表&lt;/li&gt;
&lt;li&gt;分区不要提前预留太多，在使用之前预先创建即可。比如是按月分区，每年年底时再把下一年度的 12 个新分区创建上即可，并且对于没有数据的历史分区，要及时的 drop 掉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文档：https://time.geekbang.org/column/article/82560&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;临时表&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;临时表分为内部临时表和用户临时表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内部临时表：系统执行 SQL 语句优化时产生的表，例如 Join 连接查询、去重查询等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户临时表：用户主动创建的临时表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TEMPORARY TABLE temp_t like table_1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;临时表可以是内存表，也可以是磁盘表（多表操作 → 嵌套查询章节提及）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存表指的是使用 Memory 引擎的表，建立哈希索引，建表语法是 &lt;code&gt;create table … engine=memory&lt;/code&gt;，这种表的数据都保存在内存里，系统重启时会被清空，但是表结构还在&lt;/li&gt;
&lt;li&gt;磁盘表是使用 InnoDB 引擎或者 MyISAM 引擎的临时表，建立 B+ 树索引，写数据的时候是写到磁盘上的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;临时表的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个临时表只能被创建它的 session 访问，对其他线程不可见，所以不同 session 的临时表是&lt;strong&gt;可以重名&lt;/strong&gt;的&lt;/li&gt;
&lt;li&gt;临时表可以与普通表同名，会话内有同名的临时表和普通表时，执行 show create 语句以及增删改查语句访问的都是临时表&lt;/li&gt;
&lt;li&gt;show tables 命令不显示临时表&lt;/li&gt;
&lt;li&gt;数据库发生异常重启不需要担心数据删除问题，临时表会&lt;strong&gt;自动回收&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;重名原理&lt;/h5&gt;
&lt;p&gt;执行创建临时表的 SQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create temporary table temp_t(id int primary key)engine=innodb;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 给 InnoDB 表创建一个 frm 文件保存表结构定义，在 ibd 保存表数据。frm 文件放在临时文件目录下，文件名的后缀是 .frm，&lt;strong&gt;前缀是&lt;/strong&gt; &lt;code&gt;#sql{进程 id}_{线程 id}_ 序列号&lt;/code&gt;，使用 &lt;code&gt;select @@tmpdir&lt;/code&gt; 命令，来显示实例的临时文件目录&lt;/p&gt;
&lt;p&gt;MySQL 维护数据表，除了物理磁盘上的文件外，内存里也有一套机制区别不同的表，每个表都对应一个 table_def_key&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个普通表的 table_def_key 的值是由 &lt;code&gt;库名 + 表名&lt;/code&gt; 得到的，所以如果在同一个库下创建两个同名的普通表，创建第二个表的过程中就会发现 table_def_key 已经存在了&lt;/li&gt;
&lt;li&gt;对于临时表，table_def_key 在 &lt;code&gt;库名 + 表名&lt;/code&gt; 基础上，又加入了 &lt;code&gt;server_id + thread_id&lt;/code&gt;，所以不同线程之间，临时表可以重名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现原理：每个线程都维护了自己的临时表链表，每次 session 内操作表时，先遍历链表，检查是否有这个名字的临时表，如果有就&lt;strong&gt;优先操作临时表&lt;/strong&gt;，如果没有再操作普通表；在 session 结束时对链表里的每个临时表，执行 &lt;code&gt;DROP TEMPORARY TABLE + 表名&lt;/code&gt; 操作&lt;/p&gt;
&lt;p&gt;执行 rename table 语句无法修改临时表，因为会按照 &lt;code&gt;库名 / 表名.frm&lt;/code&gt; 的规则去磁盘找文件，但是临时表文件名的规则是 &lt;code&gt;#sql{进程 id}_{线程 id}_ 序列号.frm&lt;/code&gt;，因此会报找不到文件名的错误&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;主备复制&lt;/h5&gt;
&lt;p&gt;创建临时表的语句会传到备库执行，因此备库的同步线程就会创建这个临时表。主库在线程退出时会自动删除临时表，但备库同步线程是持续在运行的并不会退出，所以这时就需要在主库上再写一个 DROP TEMPORARY TABLE 传给备库执行&lt;/p&gt;
&lt;p&gt;binlog 日志写入规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;binlog_format=row，跟临时表有关的语句就不会记录到 binlog&lt;/li&gt;
&lt;li&gt;binlog_format=statment/mixed，binlog 中才会记录临时表的操作，也就会记录 &lt;code&gt;DROP TEMPORARY TABLE&lt;/code&gt; 这条命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主库上不同的线程创建同名的临时表是不冲突的，但是备库只有一个执行线程，所以 MySQL 在记录 binlog 时会把主库执行这个语句的线程 id 写到 binlog 中，在备库的应用线程就可以获取执行每个语句的主库线程 id，并利用这个线程 id 来构造临时表的 table_def_key&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;session A 的临时表 t1，在备库的 table_def_key 就是：&lt;code&gt;库名 + t1 +“M 的 serverid&quot; + &quot;session A 的 thread_id”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;session B 的临时表 t1，在备库的 table_def_key 就是 ：&lt;code&gt;库名 + t1 +&quot;M 的 serverid&quot; + &quot;session B 的 thread_id&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 在记录 binlog 的时不论是 create table 还是 alter table 语句都是原样记录，但是如果执行 drop table，系统记录 binlog 就会被服务端改写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE `t_normal` /* generated by server */
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;跨库查询&lt;/h5&gt;
&lt;p&gt;分库分表系统的跨库查询使用临时表不用担心线程之间的重名冲突，分库分表就是要把一个逻辑上的大表分散到不同的数据库实例上&lt;/p&gt;
&lt;p&gt;比如将一个大表 ht，按照字段 f，拆分成 1024 个分表，分布到 32 个数据库实例上，一般情况下都有一个中间层 proxy 解析 SQL 语句，通过分库规则通过分表规则（比如 N%1024）确定将这条语句路由到哪个分表做查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select v from ht where f=N;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这个表上还有另外一个索引 k，并且查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select v from ht where k &amp;gt;= M order by t_modified desc limit 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询条件里面没有用到分区字段 f，只能&lt;strong&gt;到所有的分区&lt;/strong&gt;中去查找满足条件的所有行，然后统一做 order by 操作，两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 proxy 层的进程代码中实现排序，拿到分库的数据以后，直接在内存中参与计算，但是对 proxy 端的压力比较大，很容易出现内存不够用和 CPU 瓶颈问题&lt;/li&gt;
&lt;li&gt;把各个分库拿到的数据，汇总到一个 MySQL 实例的一个表中，然后在这个汇总实例上做逻辑操作，执行流程：
&lt;ul&gt;
&lt;li&gt;在汇总库上创建一个临时表 temp_ht，表里包含三个字段 v、k、t_modified&lt;/li&gt;
&lt;li&gt;在各个分库执行：&lt;code&gt;select v,k,t_modified from ht_x where k &amp;gt;= M order by t_modified desc limit 100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把分库执行的结果插入到 temp_ht 表中&lt;/li&gt;
&lt;li&gt;在临时表上执行：&lt;code&gt;select v from temp_ht order by t_modified desc limit 100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;优化步骤&lt;/h3&gt;
&lt;h4&gt;执行频率&lt;/h4&gt;
&lt;p&gt;MySQL 客户端连接成功后，查询服务器状态信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW [SESSION|GLOBAL] STATUS LIKE &apos;&apos;;
-- SESSION: 显示当前会话连接的统计结果，默认参数
-- GLOBAL: 显示自数据库上次启动至今的统计结果
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看 SQL 执行频率：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;Com_____&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Com_xxx 表示每种语句执行的次数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E9%A2%91%E7%8E%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询 SQL 语句影响的行数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;Innodb_rows_%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL%E8%AF%AD%E5%8F%A5%E5%BD%B1%E5%93%8D%E7%9A%84%E8%A1%8C%E6%95%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Com_xxxx：这些参数对于所有存储引擎的表操作都会进行累计&lt;/p&gt;
&lt;p&gt;Innodb_xxxx：这几个参数只是针对 InnoDB 存储引擎的，累加的算法也略有不同&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Com_select&lt;/td&gt;
&lt;td&gt;执行 SELECT 操作的次数，一次查询只累加 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Com_insert&lt;/td&gt;
&lt;td&gt;执行 INSERT 操作的次数，对于批量插入的 INSERT 操作，只累加一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Com_update&lt;/td&gt;
&lt;td&gt;执行 UPDATE 操作的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Com_delete&lt;/td&gt;
&lt;td&gt;执行 DELETE 操作的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innodb_rows_read&lt;/td&gt;
&lt;td&gt;执行 SELECT 查询返回的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innodb_rows_inserted&lt;/td&gt;
&lt;td&gt;执行 INSERT 操作插入的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innodb_rows_updated&lt;/td&gt;
&lt;td&gt;执行 UPDATE 操作更新的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innodb_rows_deleted&lt;/td&gt;
&lt;td&gt;执行 DELETE 操作删除的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connections&lt;/td&gt;
&lt;td&gt;试图连接 MySQL 服务器的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime&lt;/td&gt;
&lt;td&gt;服务器工作时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow_queries&lt;/td&gt;
&lt;td&gt;慢查询的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;定位低效&lt;/h4&gt;
&lt;p&gt;SQL 执行慢有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;偶尔慢：DB 在刷新脏页（学完事务就懂了）
&lt;ul&gt;
&lt;li&gt;redo log 写满了&lt;/li&gt;
&lt;li&gt;内存不够用，要从 LRU 链表中淘汰&lt;/li&gt;
&lt;li&gt;MySQL 认为系统空闲的时候&lt;/li&gt;
&lt;li&gt;MySQL 关闭时&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一直慢的原因：索引没有设计好、SQL 语句没写好、MySQL 选错了索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过以下两种方式定位执行效率较低的 SQL 语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;慢日志查询： 慢查询日志在查询结束以后才记录，执行效率出现问题时查询日志并不能定位问题&lt;/p&gt;
&lt;p&gt;配置文件修改：修改 .cnf 文件 &lt;code&gt;vim /etc/mysql/my.cnf&lt;/code&gt;，重启 MySQL 服务器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slow_query_log=ON
slow_query_log_file=/usr/local/mysql/var/localhost-slow.log
long_query_time=1	#记录超过long_query_time秒的SQL语句的日志
log-queries-not-using-indexes = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用命令配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; SET slow_query_log=ON;
mysql&amp;gt; SET GLOBAL slow_query_log=ON;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看是否配置成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;%query%&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SHOW PROCESSLIST：&lt;strong&gt;实时查看&lt;/strong&gt;当前 MySQL 在进行的连接线程，包括线程的状态、是否锁表、SQL 的执行情况，同时对一些锁表操作进行优化&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST%E5%91%BD%E4%BB%A4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;EXPLAIN&lt;/h4&gt;
&lt;h5&gt;执行计划&lt;/h5&gt;
&lt;p&gt;通过 EXPLAIN 命令获取执行 SQL 语句的信息，包括在 SELECT 语句执行过程中如何连接和连接的顺序，执行计划在优化器优化完成后、执行器之前生成，然后执行器会调用存储引擎检索数据&lt;/p&gt;
&lt;p&gt;查询 SQL 语句的执行计划：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM table_1 WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain%E6%9F%A5%E8%AF%A2SQL%E8%AF%AD%E5%8F%A5%E7%9A%84%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;SELECT 的序列号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;select_type&lt;/td&gt;
&lt;td&gt;表示 SELECT 的类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;table&lt;/td&gt;
&lt;td&gt;访问数据库中表名称，有时可能是简称或者临时表名称（&amp;lt;table_name&amp;gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;表示表的连接类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;possible_keys&lt;/td&gt;
&lt;td&gt;表示查询时，可能使用的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;表示实际使用的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key_len&lt;/td&gt;
&lt;td&gt;索引字段的长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref&lt;/td&gt;
&lt;td&gt;表示与索引列进行等值匹配的对象，常数、某个列、函数等，type 必须在（range, const] 之间，左闭右开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows&lt;/td&gt;
&lt;td&gt;扫描出的行数，表示 MySQL 根据表统计信息及索引选用情况，&lt;strong&gt;估算&lt;/strong&gt;的找到所需的记录扫描的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filtered&lt;/td&gt;
&lt;td&gt;条件过滤的行百分比，单表查询没意义，用于连接查询中对驱动表的扇出进行过滤，查询优化器预测所有扇出值满足剩余查询条件的百分比，相乘以后表示多表查询中还要对被驱动执行查询的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extra&lt;/td&gt;
&lt;td&gt;执行情况的说明和描述&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MySQL &lt;strong&gt;执行计划的局限&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只是计划，不是执行 SQL 语句，可以随着底层优化器输入的更改而更改&lt;/li&gt;
&lt;li&gt;EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况， 不考虑各种 Cache&lt;/li&gt;
&lt;li&gt;EXPLAIN 不能显示 MySQL 在执行查询时的动态，因为执行计划在执行&lt;strong&gt;查询之前生成&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;EXPALIN 只能解释 SELECT 操作，其他操作要重写为 SELECT 后查看执行计划&lt;/li&gt;
&lt;li&gt;EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句，由于执行环境和 EXPLAIN PLAN 环境的不同，此计划可能与 SQL 语句&lt;strong&gt;实际的执行计划不同&lt;/strong&gt;，部分统计信息是估算的，并非精确值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SHOW WARINGS：在使用 EXPALIN 命令后执行该语句，可以查询与执行计划相关的拓展信息，展示出 Level、Code、Message 三个字段，当 Code 为 1003 时，Message 字段展示的信息类似于将查询语句重写后的信息，但是不是等价，不能执行复制过来运行&lt;/p&gt;
&lt;p&gt;环境准备：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;id&lt;/h5&gt;
&lt;p&gt;id 代表 SQL 执行的顺序的标识，每个 SELECT 关键字对应一个唯一 id，所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表，每个表都会对应一条记录，这些记录的 id 都是相同的，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;id 相同时，执行顺序由上至下。连接查询的执行计划，记录的 id 值都是相同的，出现在前面的表为驱动表，后面为被驱动表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain%E4%B9%8Bid%E7%9B%B8%E5%90%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;id 不同时，id 值越大优先级越高，越先被执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = &apos;stu1&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain%E4%B9%8Bid%E4%B8%8D%E5%90%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;id 有相同也有不同时，id 相同的可以认为是一组，从上往下顺序执行；在所有的组中，id 的值越大的组，优先级越高，越先执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = &apos;2&apos;) a WHERE r.id = a.role_id ; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain%E4%B9%8Bid%E7%9B%B8%E5%90%8C%E5%92%8C%E4%B8%8D%E5%90%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;id 为 NULL 时代表的是临时表&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;select&lt;/h5&gt;
&lt;p&gt;表示查询中每个 select 子句的类型（简单 OR 复杂）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;select_type&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SIMPLE&lt;/td&gt;
&lt;td&gt;简单的 SELECT 查询，查询中不包含子查询或者 UNION&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;td&gt;查询中若包含任何复杂的子查询，最外层（也就是最左侧）查询标记为该标识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNION&lt;/td&gt;
&lt;td&gt;对于 UNION 或者 UNION ALL 的复杂查询，除了最左侧的查询，其余的小查询都是 UNION&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNION RESULT&lt;/td&gt;
&lt;td&gt;UNION 需要使用临时表进行去重，临时表的是 UNION RESULT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEPENDENT UNION&lt;/td&gt;
&lt;td&gt;对于 UNION 或者 UNION ALL 的复杂查询，如果各个小查询都依赖外层查询，是相关子查询，除了最左侧的小查询为 DEPENDENT SUBQUERY，其余都是 DEPENDENT UNION&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUBQUERY&lt;/td&gt;
&lt;td&gt;子查询不是相关子查询，该子查询第一个 SELECT 代表的查询就是这种类型，会进行物化（该子查询只需要执行一次）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEPENDENT SUBQUERY&lt;/td&gt;
&lt;td&gt;子查询是相关子查询，该子查询第一个 SELECT 代表的查询就是这种类型，不会物化（该子查询需要执行多次）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DERIVED&lt;/td&gt;
&lt;td&gt;在 FROM 列表中包含的子查询，被标记为 DERIVED（衍生），也就是生成物化派生表的这个子查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MATERIALIZED&lt;/td&gt;
&lt;td&gt;将子查询物化后与与外层进行连接查询，生成物化表的子查询&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;子查询为 DERIVED：&lt;code&gt;SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 &amp;gt; 10&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;子查询为 MATERIALIZED：&lt;code&gt;SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;type&lt;/h5&gt;
&lt;p&gt;对表的访问方式，表示 MySQL 在表中找到所需行的方式，又称访问类型&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ALL&lt;/td&gt;
&lt;td&gt;全表扫描，如果是 InnoDB 引擎是扫描聚簇索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;td&gt;可以使用覆盖索引，但需要扫描全部索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;range&lt;/td&gt;
&lt;td&gt;索引范围扫描，常见于 between、&amp;lt;、&amp;gt; 等的查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index_subquery&lt;/td&gt;
&lt;td&gt;子查询可以普通索引，则子查询的 type 为 index_subquery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unique_subquery&lt;/td&gt;
&lt;td&gt;子查询可以使用主键或唯一二级索引，则子查询的 type 为 index_subquery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index_merge&lt;/td&gt;
&lt;td&gt;索引合并&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref_or_null&lt;/td&gt;
&lt;td&gt;非唯一性索引（普通二级索引）并且可以存储 NULL，进行等值匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref&lt;/td&gt;
&lt;td&gt;非唯一性索引与常量等值匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eq_ref&lt;/td&gt;
&lt;td&gt;唯一性索引（主键或不存储 NULL 的唯一二级索引）进行等值匹配，如果二级索引是联合索引，那么所有联合的列都要进行等值匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;通过主键或者唯一二级索引与常量进行等值匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system&lt;/td&gt;
&lt;td&gt;system 是 const 类型的特例，当查询的表只有一条记录的情况下，使用 system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;MySQL 在优化过程中分解语句，执行时甚至不用访问表或索引&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;从上到下，性能从差到好，一般来说需要保证查询至少达到 range 级别， 最好达到 ref&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;key&lt;/h5&gt;
&lt;p&gt;possible_keys：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指出 MySQL 能使用哪个索引在表中找到记录，查询涉及到的字段上若存在索引，则该索引将被列出，但不一定被查询使用&lt;/li&gt;
&lt;li&gt;如果该列是 NULL，则没有相关的索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;key：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示 MySQL 在查询中实际使用的索引，若没有使用索引，显示为 NULL&lt;/li&gt;
&lt;li&gt;查询中若使用了&lt;strong&gt;覆盖索引&lt;/strong&gt;，则该索引可能出现在 key 列表，不出现在 possible_keys&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;key_len：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表示索引中使用的字节数，可通过该列计算查询中使用的索引的长度&lt;/li&gt;
&lt;li&gt;key_len 显示的值为索引字段的最大可能长度，并非实际使用长度，即 key_len 是根据表定义计算而得，不是通过表内检索出的&lt;/li&gt;
&lt;li&gt;在不损失精确性的前提下，长度越短越好&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Extra&lt;/h5&gt;
&lt;p&gt;其他的额外的执行计划信息，在该列展示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No tables used：查询语句中使用 FROM dual 或者没有 FROM 语句&lt;/li&gt;
&lt;li&gt;Impossible WHERE：查询语句中的 WHERE 子句条件永远为 FALSE，会导致没有符合条件的行&lt;/li&gt;
&lt;li&gt;Using index：该值表示相应的 SELECT 操作中使用了&lt;strong&gt;覆盖索引&lt;/strong&gt;（Covering Index）&lt;/li&gt;
&lt;li&gt;Using index condition：第一种情况是搜索条件中虽然出现了索引列，但是部分条件无法形成扫描区间（&lt;strong&gt;索引失效&lt;/strong&gt;），会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件，回表查询数据；第二种是使用了&lt;strong&gt;索引条件下推&lt;/strong&gt;优化&lt;/li&gt;
&lt;li&gt;Using where：搜索的数据需要在 Server 层判断，无法使用索引下推&lt;/li&gt;
&lt;li&gt;Using join buffer：连接查询被驱动表无法利用索引，需要连接缓冲区来存储中间结果&lt;/li&gt;
&lt;li&gt;Using filesort：无法利用索引完成排序（优化方向），需要对数据使用外部排序算法，将取得的数据在内存或磁盘中进行排序&lt;/li&gt;
&lt;li&gt;Using temporary：表示 MySQL 需要使用临时表来存储结果集，常见于&lt;strong&gt;排序、去重（UNION）、分组&lt;/strong&gt;等场景&lt;/li&gt;
&lt;li&gt;Select tables optimized away：说明仅通过使用索引，优化器可能仅从聚合函数结果中返回一行&lt;/li&gt;
&lt;li&gt;No tables used：Query 语句中使用 from dual 或不含任何 from 子句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;PROFILES&lt;/h4&gt;
&lt;p&gt;SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的&lt;strong&gt;资源消耗&lt;/strong&gt;情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过 have_profiling 参数，能够看到当前 MySQL 是否支持 profile：
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-have_profiling.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认 profiling 是关闭的，可以通过 set 语句在 Session 级别开启 profiling：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-profiling.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET profiling=1; #开启profiling 开关；
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 SHOW PROFILES 指令， 来查看 SQL 语句执行的耗时:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW PROFILES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%9F%A5%E7%9C%8BSQL%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E8%80%97%E6%97%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看到该 SQL 执行过程中每个线程的状态和消耗的时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW PROFILE FOR QUERY query_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL%E6%89%A7%E8%A1%8C%E6%AF%8F%E4%B8%AA%E7%8A%B6%E6%80%81%E6%B6%88%E8%80%97%E7%9A%84%E6%97%B6%E9%97%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在获取到最消耗时间的线程状态后，MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如，选择查看 CPU 的耗费时间：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL%E6%89%A7%E8%A1%8C%E6%AF%8F%E4%B8%AA%E7%8A%B6%E6%80%81%E6%B6%88%E8%80%97%E7%9A%84CPU.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Status：SQL 语句执行的状态&lt;/li&gt;
&lt;li&gt;Durationsql：执行过程中每一个步骤的耗时&lt;/li&gt;
&lt;li&gt;CPU_user：当前用户占有的 CPU&lt;/li&gt;
&lt;li&gt;CPU_system：系统占有的 CPU&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;TRACE&lt;/h4&gt;
&lt;p&gt;MySQL 提供了对 SQL 的跟踪， 通过 trace 文件可以查看优化器&lt;strong&gt;生成执行计划的过程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打开 trace 功能，设置格式为 JSON，并设置 trace 的最大使用内存，避免解析过程中因默认内存过小而不能够完整展示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET optimizer_trace=&quot;enabled=on&quot;,end_markers_in_json=ON;	-- 会话内有效
SET optimizer_trace_max_mem_size=1000000;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 SQL 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM tb_item WHERE id &amp;lt; 4;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查 information_schema.optimizer_trace：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行信息主要有三个阶段：prepare 阶段、optimize 阶段（成本分析）、execute 阶段（执行）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;索引优化&lt;/h3&gt;
&lt;h4&gt;创建索引&lt;/h4&gt;
&lt;p&gt;索引是数据库优化最重要的手段之一，通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_seller` (
	`sellerid` varchar (100),
	`name` varchar (100),
	`nickname` varchar (50),
	`password` varchar (60),
	`status` varchar (1),
	`address` varchar (100),
	`createtime` datetime,
    PRIMARY KEY(`sellerid`)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values(&apos;xiaomi&apos;,&apos;小米科技&apos;,&apos;小米官方旗舰店&apos;,&apos;e10adc3949ba59abbe56e057f20f883e&apos;,&apos;1&apos;,&apos;西安市&apos;,&apos;2088-01-01 12:00:00&apos;);
CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联合索引
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;避免失效&lt;/h4&gt;
&lt;h5&gt;语句错误&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全值匹配：对索引中所有列都指定具体值，这种情况索引生效，执行效率高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; AND status=&apos;1&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%951.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最左前缀法则&lt;/strong&gt;：联合索引遵守最左前缀法则&lt;/p&gt;
&lt;p&gt;匹配最左前缀法则，走索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos;;
EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; AND status=&apos;1&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%952.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;违法最左前缀法则 ， 索引失效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE status=&apos;1&apos;;
EXPLAIN SELECT * FROM tb_seller WHERE status=&apos;1&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%953.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果符合最左法则，但是出现跳跃某一列，只有最左列索引生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%954.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然索引列失效，但是系统会&lt;strong&gt;使用了索引下推进行了优化&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;范围查询&lt;/strong&gt;右边的列，不能使用索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; AND status&amp;gt;&apos;1&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据前面的两个字段 name ， status 查询是走索引的， 但是最后一个条件 address 没有用到索引，使用了索引下推&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%955.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在索引列上&lt;strong&gt;函数或者运算（+ - 数值）操作&lt;/strong&gt;， 索引将失效：会破坏索引值的有序性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = &apos;科技&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%956.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;字符串不加单引号&lt;/strong&gt;，造成索引失效：隐式类型转换，当字符串和数字比较时会&lt;strong&gt;把字符串转化为数字&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有对字符串加单引号，查询优化器会调用 CAST 函数将 status 转换为 int 进行比较，造成索引失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; AND status = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%957.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果 status 是 int 类型，SQL 为 &lt;code&gt;SELECT * FROM tb_seller WHERE status = &apos;1&apos; &lt;/code&gt; 并不会造成索引失效，因为会将 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 转换为 &lt;code&gt;1&lt;/code&gt;，并&lt;strong&gt;不会对索引列产生操作&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多表连接查询时，如果两张表的&lt;strong&gt;字符集不同&lt;/strong&gt;，会造成索引失效，因为会进行类型转换&lt;/p&gt;
&lt;p&gt;解决方法：CONVERT 函数是加在输入参数上、修改表的字符集&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用 OR 分割条件，索引失效&lt;/strong&gt;，导致全表查询：&lt;/p&gt;
&lt;p&gt;OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引，都造成索引失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;阿里巴巴&apos; OR createtime = &apos;2088-01-01 12:00:00&apos;;
EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;小米科技&apos; OR status=&apos;1&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9510.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AND 分割的条件不影响&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name=&apos;阿里巴巴&apos; AND createtime = &apos;2088-01-01 12:00:00&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9511.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;以 % 开头的 LIKE 模糊查询&lt;/strong&gt;，索引失效：&lt;/p&gt;
&lt;p&gt;如果是尾部模糊匹配，索引不会失效；如果是头部模糊匹配，索引失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name like &apos;%科技%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9512.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方案：通过覆盖索引来解决&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like &apos;%科技%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9513.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原因：在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配，或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;系统优化&lt;/h5&gt;
&lt;p&gt;系统优化为全表扫描：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 MySQL 评估使用索引比全表更慢，则不使用索引，索引失效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX idx_address ON tb_seller(address);
EXPLAIN SELECT * FROM tb_seller WHERE address=&apos;西安市&apos;;
EXPLAIN SELECT * FROM tb_seller WHERE address=&apos;北京市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;北京市的键值占 9/10（区分度低），所以优化为全表扫描，type = ALL&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9514.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IS  NULL、IS NOT NULL  &lt;strong&gt;有时&lt;/strong&gt;索引失效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL;
EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NOT NULL 失效的原因是 name 列全部不是 null，优化为全表扫描，当 NULL 过多时，IS NULL 失效&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9515.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IN 肯定会走索引，但是当 IN 的取值范围较大时会导致索引失效，走全表扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN (&apos;alibaba&apos;,&apos;huawei&apos;);-- 都走索引
EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN (&apos;alibaba&apos;,&apos;huawei&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://time.geekbang.org/column/article/74687&quot;&gt;MySQL 实战 45 讲&lt;/a&gt;该章节最后提出了一种慢查询场景，获取到数据以后 Server 层还会做判断&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;底层原理&lt;/h4&gt;
&lt;p&gt;索引失效一般是针对联合索引，联合索引一般由几个字段组成，排序方式是先按照第一个字段进行排序，然后排序第二个，依此类推，图示（a, b）索引，&lt;strong&gt;a 相等的情况下 b 是有序的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引失效底层原理1.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最左前缀法则：当不匹配前面的字段的时候，后面的字段都是无序的。这种无序不仅体现在叶子节点，也会&lt;strong&gt;导致查询时扫描的非叶子节点也是无序的&lt;/strong&gt;，因为索引树相当于忽略的第一个字段，就无法使用二分查找&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;范围查询右边的列，不能使用索引，比如语句： &lt;code&gt;WHERE a &amp;gt; 1 AND b = 1 &lt;/code&gt;，在 a 大于 1 的时候，b 是无序的，a &amp;gt; 1 是扫描时有序的，但是找到以后进行寻找 b 时，索引树就不是有序的了&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引失效底层原理2.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以 % 开头的 LIKE 模糊查询，索引失效，比如语句：&lt;code&gt;WHERE a LIKE &apos;%d&apos;&lt;/code&gt;，前面的不确定，导致不符合最左匹配，直接去索引中搜索以 d 结尾的节点，所以没有顺序
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%863.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;查看索引&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;Handler_read%&apos;;	
SHOW GLOBAL STATUS LIKE &apos;Handler_read%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E6%9F%A5%E7%9C%8B%E7%B4%A2%E5%BC%95%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_first：索引中第一条被读的次数，如果较高，表示服务器正执行大量全索引扫描（这个值越低越好）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_key：如果索引正在工作，这个值代表一个行被索引值读的次数，值越低表示索引不经常使用（这个值越高越好）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_next：按照键顺序读下一行的请求数，如果范围约束或执行索引扫描来查询索引列，值增加&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_prev：按照键顺序读前一行的请求数，该读方法主要用于优化 ORDER BY ... DESC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_rnd：根据固定位置读一行的请求数，如果执行大量查询并对结果进行排序则该值较高，可能是使用了大量需要 MySQL 扫描整个表的查询或连接，这个值较高意味着运行效率低，应该建立索引来解决&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler_read_rnd_next：在数据文件中读下一行的请求数，如果正进行大量的表扫描，该值较高，说明表索引不正确或写入的查询没有利用索引&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;SQL 优化&lt;/h3&gt;
&lt;h4&gt;自增主键&lt;/h4&gt;
&lt;h5&gt;自增机制&lt;/h5&gt;
&lt;p&gt;自增主键可以让主键索引尽量地保持在数据页中递增顺序插入，不自增需要寻找其他页插入，导致随机 IO 和页分裂的情况&lt;/p&gt;
&lt;p&gt;表的结构定义存放在后缀名为.frm 的文件中，但是并不会保存自增值，不同的引擎对于自增值的保存策略不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyISAM 引擎的自增值保存在数据文件中&lt;/li&gt;
&lt;li&gt;InnoDB 引擎的自增值保存在了内存里，每次打开表都会去找自增值的最大值 max(id)，然后将 max(id)+1 作为当前的自增值；8.0 版本后，才有了自增值持久化的能力，将自增值的变更记录在了 redo log 中，重启的时候依靠 redo log 恢复重启之前的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在插入一行数据的时候，自增值的行为如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果插入数据时 id 字段指定为 0、null 或未指定值，那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段&lt;/li&gt;
&lt;li&gt;如果插入数据时 id 字段指定了具体的值，比如某次要插入的值是 X，当前的自增值是 Y
&lt;ul&gt;
&lt;li&gt;如果 X&amp;lt;Y，那么这个表的自增值不变&lt;/li&gt;
&lt;li&gt;如果 X≥Y，就需要把当前自增值修改为新的自增值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参数说明：auto_increment_offset 和 auto_increment_increment 分别表示自增的初始值和步长，默认值都是 1&lt;/p&gt;
&lt;p&gt;语句执行失败也不回退自增 id，所以保证了自增 id 是递增的，但不保证是连续的（不能回退，所以有些回滚事务的自增 id 就不会重新使用，导致出现不连续）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;自增 ID&lt;/h5&gt;
&lt;p&gt;MySQL 不同的自增 id 在达到上限后的表现不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表的自增 id 如果是 int 类型，达到上限 2^32-1 后，再申请时值就不会改变，进而导致继续插入数据时报主键冲突的错误&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;row_id 长度为 6 个字节，达到上限后则会归 0 再重新递增，如果出现相同的 row_id，后写的数据会覆盖之前的数据，造成旧数据丢失，影响的是数据可靠性，所以应该在 InnoDB 表中主动创建自增主键报主键冲突，插入失败影响的是可用性，而一般情况下，&lt;strong&gt;可靠性优先于可用性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Xid 长度 8 字节，由 Server 层维护，只需要不在同一个 binlog 文件中出现重复值即可，虽然理论上会出现重复值，但是概率极小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来，重启也不会重置为 0，所以会导致一直增加到达上限，然后从 0 开始，这时原事务 0 修改的数据对当前事务就是可见的，产生脏读的现象&lt;/p&gt;
&lt;p&gt;只读事务不分配 trx_id，所以 trx_id 的增加速度变慢了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;thread_id 长度 4 个字节，到达上限后就会重置为 0，MySQL 设计了一个唯一数组的逻辑，给新线程分配 thread_id 时做判断，保证不会出现两个相同的 thread_id：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;do {
	new_id = thread_id_counter++;
} while (!thread_ids.insert_unique(new_id).second);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/83183&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;覆盖索引&lt;/h4&gt;
&lt;p&gt;复合索引叶子节点不仅保存了复合索引的值，还有主键索引，所以使用覆盖索引的时候，加上主键也会用到索引&lt;/p&gt;
&lt;p&gt;尽量使用覆盖索引，避免 SELECT *：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT name,status,address FROM tb_seller WHERE name=&apos;小米科技&apos; AND status=&apos;1&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%958.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果查询列，超出索引列，也会降低性能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name=&apos;小米科技&apos; AND status=&apos;1&apos; AND address=&apos;西安市&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%959.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;减少访问&lt;/h4&gt;
&lt;p&gt;避免对数据进行重复检索：能够一次连接就获取到结果的，就不用两次连接，这样可以大大减少对数据库无用的重复请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT id,name FROM tb_book;
SELECT id,status FROM tb_book; -- 向数据库提交两次请求，数据库就要做两次查询操作
-- &amp;gt; 优化为:
SELECT id,name,statu FROM tb_book;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO tb_test VALUES(1,&apos;Tom&apos;);
INSERT INTO tb_test VALUES(2,&apos;Cat&apos;);
INSERT INTO tb_test VALUES(3,&apos;Jerry&apos;);	-- 连接三次数据库
-- &amp;gt;优化为
INSERT INTO tb_test VALUES(1,&apos;Tom&apos;),(2,&apos;Cat&apos;)，(3,&apos;Jerry&apos;);	-- 连接一次
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在事务中进行数据插入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start transaction;
INSERT INTO tb_test VALUES(1,&apos;Tom&apos;);
INSERT INTO tb_test VALUES(2,&apos;Cat&apos;);
INSERT INTO tb_test VALUES(3,&apos;Jerry&apos;);
commit;	-- 手动提交，分段提交
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据有序插入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO tb_test VALUES(1,&apos;Tom&apos;);
INSERT INTO tb_test VALUES(2,&apos;Cat&apos;);
INSERT INTO tb_test VALUES(3,&apos;Jerry&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;增加 cache 层：在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储，或者使用框架（Mybatis）提供的一级缓存 / 二级缓存，或者使用 Redis 数据库来缓存数据&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;数据插入&lt;/h4&gt;
&lt;p&gt;当使用 load 命令导入数据的时候，适当的设置可以提高导入的效率：&lt;/p&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL load data.png)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA LOCAL INFILE = &apos;/home/seazean/sql1.log&apos; INTO TABLE `tb_user_1` FIELD TERMINATED BY &apos;,&apos; LINES TERMINATED BY &apos;\n&apos;; -- 文件格式如上图
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 InnoDB 类型的表，有以下几种方式可以提高导入的效率：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主键顺序插入&lt;/strong&gt;：因为 InnoDB 类型的表是按照主键的顺序保存的，所以将导入的数据按照主键的顺序排列，可以有效的提高导入数据的效率，如果 InnoDB 表没有主键，那么系统会自动默认创建一个内部列作为主键&lt;/p&gt;
&lt;p&gt;主键是否连续对性能影响不大，只要是递增的就可以，比如雪花算法产生的 ID 不是连续的，但是是递增的，因为递增可以让主键索引尽量地保持顺序插入，&lt;strong&gt;避免了页分裂&lt;/strong&gt;，因此索引更紧凑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入 ID 顺序排列数据：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E6%8F%92%E5%85%A5ID%E9%A1%BA%E5%BA%8F%E6%8E%92%E5%88%97%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入 ID 无序排列数据：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E6%8F%92%E5%85%A5ID%E6%97%A0%E5%BA%8F%E6%8E%92%E5%88%97%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关闭唯一性校验&lt;/strong&gt;：在导入数据前执行 &lt;code&gt;SET UNIQUE_CHECKS=0&lt;/code&gt;，关闭唯一性校验；导入结束后执行 &lt;code&gt;SET UNIQUE_CHECKS=1&lt;/code&gt;，恢复唯一性校验，可以提高导入的效率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E6%8F%92%E5%85%A5%E6%95%B0%E6%8D%AE%E5%85%B3%E9%97%AD%E5%94%AF%E4%B8%80%E6%80%A7%E6%A0%A1%E9%AA%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;手动提交事务&lt;/strong&gt;：如果应用使用自动提交的方式，建议在导入前执行&lt;code&gt;SET AUTOCOMMIT=0&lt;/code&gt;，关闭自动提交；导入结束后再打开自动提交，可以提高导入的效率。&lt;/p&gt;
&lt;p&gt;事务需要控制大小，事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项，超过这个值的日志会写入磁盘数据，效率会下降，所以在事务大小达到配置项数据级前进行事务提交可以提高效率&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E6%8F%92%E5%85%A5%E6%95%B0%E6%8D%AE%E6%89%8B%E5%8A%A8%E6%8F%90%E4%BA%A4%E4%BA%8B%E5%8A%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;分组排序&lt;/h4&gt;
&lt;h5&gt;ORDER&lt;/h5&gt;
&lt;p&gt;数据准备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `emp` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  `age` INT(3) NOT NULL,
  `salary` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES(&apos;1&apos;,&apos;Tom&apos;,&apos;25&apos;,&apos;2300&apos;);-- ...
CREATE INDEX idx_emp_age_salary ON emp(age, salary);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一种是通过对返回数据进行排序，所有不通过索引直接返回结果的排序都叫 FileSort 排序，会在内存中重新排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM emp ORDER BY age DESC;	-- 年龄降序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序1.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二种通过有序索引顺序扫描直接返回&lt;strong&gt;有序数据&lt;/strong&gt;，这种情况为 Using index，不需要额外排序，操作效率高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序2.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多字段排序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC;
EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC;
EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序3.png)&lt;/p&gt;
&lt;p&gt;尽量减少额外的排序，通过索引直接返回有序数据。&lt;strong&gt;需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order  by 的字段都是升序或都是降序&lt;/strong&gt;，否则需要额外的操作，就会出现 FileSort&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ORDER BY RAND() 命令用来进行随机排序，会使用了临时内存表，临时内存表排序的时使用 rowid 排序方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优化方式：创建合适的索引能够减少 Filesort 的出现，但是某些情况下条件限制不能让 Filesort 消失，就要加快 Filesort 的排序操作&lt;/p&gt;
&lt;p&gt;内存临时表，MySQL 有两种 Filesort 排序算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;rowid 排序：首先根据条件取出排序字段和信息，然后在&lt;strong&gt;排序区 sort buffer（Server 层）&lt;strong&gt;中排序，如果 sort buffer 不够，则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针&lt;/strong&gt;回表读取记录&lt;/strong&gt;，该操作可能会导致大量随机 I/O 操作&lt;/p&gt;
&lt;p&gt;说明：对于临时内存表，回表过程只是简单地根据数据行的位置，直接访问内存得到数据，不会导致多访问磁盘，优先选择该方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全字段排序：一次性取出满足条件的所有数据，需要回表，然后在排序区 sort  buffer 中排序后直接输出结果集。排序时内存开销较大，但是排序效率比两次扫描算法高&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体的选择方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小，来判定使用哪种排序算法。如果前者大，则说明 sort  buffer 空间足够，使用第二种优化之后的算法，否则使用第一种。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以适当提高 sort_buffer_size  和 max_length_for_sort_data 系统变量，来增大排序区的大小，提高排序的效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET @@max_length_for_sort_data = 10000; 		-- 设置全局变量
SET max_length_for_sort_data = 10240; 			-- 设置会话变量
SHOW VARIABLES LIKE &apos;max_length_for_sort_data&apos;;	-- 默认1024
SHOW VARIABLES LIKE &apos;sort_buffer_size&apos;;			-- 默认262114
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;磁盘临时表：排序使用优先队列（堆）的方式&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;GROUP&lt;/h5&gt;
&lt;p&gt;GROUP BY 也会进行排序操作，与 ORDER BY 相比，GROUP BY 主要只是多了排序之后的分组操作，所以在 GROUP BY 的实现过程中，与 ORDER BY 一样也可以利用到索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分组查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP INDEX idx_emp_age_salary ON emp;
EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序1.png)&lt;/p&gt;
&lt;p&gt;Using temporary：表示 MySQL 需要使用临时表（不是 sort buffer）来存储结果集，常见于排序和分组查询&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询包含 GROUP BY 但是用户想要避免排序结果的消耗， 则可以执行 ORDER BY NULL 禁止排序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序2.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建索引：索引本身有序，不需要临时表，也不需要再额外排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX idx_emp_age_salary ON emp(age, salary);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据量很大时，使用 SQL_BIG_RESULT 提示优化器直接使用直接用磁盘临时表&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;联合查询&lt;/h4&gt;
&lt;p&gt;对于包含 OR 的查询子句，如果要利用索引，则 OR 之间的&lt;strong&gt;每个条件列都必须用到索引，而且不能使用到条件之间的复合索引&lt;/strong&gt;，如果没有索引，则应该考虑增加索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;执行查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30;	-- 两个索引，并且不是复合索引
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询1.png)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 UNION 替换 OR，求并集：&lt;/p&gt;
&lt;p&gt;注意：该优化只针对多个索引列有效，如果有列没有被索引，查询效率可能会因为没有选择 OR 而降低&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询2.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UNION 要优于 OR 的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNION 语句的 type 值为 ref，OR 语句的 type 值为 range&lt;/li&gt;
&lt;li&gt;UNION 语句的 ref 值为 const，OR 语句的 ref 值为 null，const 表示是常量值引用，非常快&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;嵌套查询&lt;/h4&gt;
&lt;p&gt;MySQL 4.1 版本之后，开始支持 SQL 的子查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以使用 SELECT 语句来创建一个单列的查询结果，然后把结果作为过滤条件用在另一个查询中&lt;/li&gt;
&lt;li&gt;使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作，同时也可以避免事务或者表锁死&lt;/li&gt;
&lt;li&gt;在有些情况下，&lt;strong&gt;子查询是可以被更高效的连接（JOIN）替代&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如查找有角色的所有的用户信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;执行计划：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E5%B5%8C%E5%A5%97%E6%9F%A5%E8%AF%A21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E5%B5%8C%E5%A5%97%E6%9F%A5%E8%AF%A22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;连接查询之所以效率更高 ，是因为&lt;strong&gt;不需要在内存中创建临时表&lt;/strong&gt;来完成逻辑上需要两个步骤的查询工作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;分页查询&lt;/h4&gt;
&lt;p&gt;一般分页查询时，通过创建覆盖索引能够比较好地提高性能&lt;/p&gt;
&lt;p&gt;一个常见的问题是 &lt;code&gt;LIMIT 200000,10&lt;/code&gt;，此时需要 MySQL 扫描前 200010 记录，仅仅返回 200000 - 200010 之间的记录，其他记录丢弃，查询排序的代价非常大&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分页查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化方式一：内连接查询，在索引列 id 上完成排序分页操作，最后根据主键关联回原表查询所需要的其他列内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化方式二：方案适用于主键自增的表，可以把 LIMIT 查询转换成某个位置的查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_user_1 WHERE id &amp;gt; 200000 LIMIT 10;			-- 写法 1
EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010;	-- 写法 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;使用提示&lt;/h4&gt;
&lt;p&gt;SQL 提示，是优化数据库的一个重要手段，就是在 SQL 语句中加入一些提示来达到优化操作的目的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;USE INDEX：在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表，可以让 MySQL 不再考虑其他可用的索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE INDEX idx_seller_name ON tb_seller(name);
EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name=&apos;小米科技&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E6%8F%90%E7%A4%BA1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IGNORE INDEX：让 MySQL 忽略一个或者多个索引，则可以使用 IGNORE INDEX 作为提示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = &apos;小米科技&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E6%8F%90%E7%A4%BA2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FORCE INDEX：强制 MySQL 使用一个特定的索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME=&apos;小米科技&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E6%8F%90%E7%A4%BA3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;统计计数&lt;/h4&gt;
&lt;p&gt;在不同的 MySQL 引擎中，count(*) 有不同的实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyISAM 引擎把一个表的总行数存在了磁盘上，因此执行 count(*) 的时候会直接返回这个数，效率很高，但不支持事务&lt;/li&gt;
&lt;li&gt;show table status 命令通过采样估算可以快速获取，但是不准确&lt;/li&gt;
&lt;li&gt;InnoDB 表执行 count(*) 会遍历全表，虽然结果准确，但会导致性能问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;计数保存在 Redis 中，但是更新 MySQL 和 Redis 的操作不是原子的，会存在数据一致性的问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计数直接放到数据库里单独的一张计数表中，利用事务解决计数精确问题：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-计数count优化.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;会话 B 的读操作在 T3 执行的，这时更新事务还没有提交，所以计数值加 1 这个操作对会话 B 还不可见，因此会话 B 查询的计数值和最近 100 条记录，返回的结果逻辑上就是一致的&lt;/p&gt;
&lt;p&gt;并发系统性能的角度考虑，应该先插入操作记录再更新计数表，因为更新计数表涉及到行锁的竞争，&lt;strong&gt;先插入再更新能最大程度地减少事务之间的锁等待，提升并发度&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;count 函数的按照效率排序：&lt;code&gt;count(字段) &amp;lt; count(主键id) &amp;lt; count(1) ≈ count(*)&lt;/code&gt;，所以建议尽量使用 count(*)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;count(主键 id)：InnoDB 引擎会遍历整张表，把每一行的 id 值都取出来返回给 Server 层，Server 判断 id 不为空就按行累加&lt;/li&gt;
&lt;li&gt;count(1)：InnoDB 引擎遍历整张表但不取值，Server 层对于返回的每一行，放一个数字 1 进去，判断不为空就按行累加&lt;/li&gt;
&lt;li&gt;count(字段)：如果这个字段是定义为 not null 的话，一行行地从记录里面读出这个字段，判断不能为 null，按行累加；如果这个字段定义允许为 null，那么执行的时候，判断到有可能是 null，还要把值取出来再判断一下，不是 null 才累加&lt;/li&gt;
&lt;li&gt;count(*)：不取值，按行累加&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/72775&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;缓冲优化&lt;/h3&gt;
&lt;h4&gt;优化原则&lt;/h4&gt;
&lt;p&gt;三个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将尽量多的内存分配给 MySQL 做缓存，但也要给操作系统和其他程序预留足够内存&lt;/li&gt;
&lt;li&gt;MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存，如果有 MyISAM 表，就要预留更多的内存给操作系统做 IO 缓存&lt;/li&gt;
&lt;li&gt;排序区、连接区等缓存是分配给每个数据库会话（Session）专用的，值的设置要根据最大连接数合理分配，如果设置太大，不但浪费资源，而且在并发数较高时会导致物理内存耗尽&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓冲内存&lt;/h4&gt;
&lt;p&gt;Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写，每个数据页的大小默认是 16KB。数据是存放在磁盘中，每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作，效率会很低，所以提供了 Buffer Pool 来暂存这些数据页，缓存中的这些页又叫缓冲页&lt;/p&gt;
&lt;p&gt;工作原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从数据库读取数据时，会首先从缓存中读取，如果缓存中没有，则从磁盘读取后放入 Buffer Pool&lt;/li&gt;
&lt;li&gt;向数据库写入数据时，会写入缓存，缓存中修改的数据会&lt;strong&gt;定期刷新&lt;/strong&gt;到磁盘，这一过程称为刷脏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Buffer Pool 中每个缓冲页都有对应的控制信息，包括表空间编号、页号、偏移量、链表信息等，控制信息存放在占用的内存称为控制块，控制块与缓冲页是一一对应的，但并不是物理上相连的，都在缓冲池中&lt;/p&gt;
&lt;p&gt;MySQL 提供了缓冲页的快速查找方式：&lt;strong&gt;哈希表&lt;/strong&gt;，使用表空间号和页号作为 Key，缓冲页控制块的地址作为 Value 创建一个哈希表，获取数据页时根据 Key 进行哈希寻址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不存在对应的缓存页，就从 free 链表中选一个空闲缓冲页，把磁盘中的对应页加载到该位置&lt;/li&gt;
&lt;li&gt;如果存在对应的缓存页，直接获取使用，提高查询数据的效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当内存数据页跟磁盘数据页内容不一致时，称这个内存页为脏页；内存数据写入磁盘后，内存和磁盘上的数据页一致，称为干净页&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存管理&lt;/h4&gt;
&lt;h5&gt;Free 链表&lt;/h5&gt;
&lt;p&gt;MySQL 启动时完成对 Buffer Pool 的初始化，先向操作系统申请连续的内存空间，然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页，将所有空闲缓冲页对应的&lt;strong&gt;控制块作为一个节点&lt;/strong&gt;放入一个链表中，就是 Free 链表（&lt;strong&gt;空闲链表&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-空闲链表.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;基节点：是一块单独申请的内存空间（占 40 字节），并不在 Buffer Pool 的那一大片连续内存空间里&lt;/p&gt;
&lt;p&gt;磁盘加载页的流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 Free 链表中取出一个空闲的缓冲页&lt;/li&gt;
&lt;li&gt;把缓冲页对应的控制块的信息填上（页所在的表空间、页号之类的信息）&lt;/li&gt;
&lt;li&gt;把缓冲页对应的 Free 链表节点（控制块）从链表中移除，表示该缓冲页已经被使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/li1325169021/article/details/121124440&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;Flush 链表&lt;/h5&gt;
&lt;p&gt;Flush 链表是一个用来&lt;strong&gt;存储脏页&lt;/strong&gt;的链表，对于已经修改过的缓冲脏页，第一次修改后加入到&lt;strong&gt;链表头部&lt;/strong&gt;，以后每次修改都不会重新加入，只修改部分控制信息，出于性能考虑并不是直接更新到磁盘，而是在未来的某个时间进行刷脏&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-脏页链表.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后台有专门的线程每隔一段时间把脏页刷新到磁盘&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 Flush 链表中刷新一部分页面到磁盘：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后台线程定时&lt;/strong&gt;从 Flush 链表刷脏，根据系统的繁忙程度来决定刷新速率，这种方式称为 BUF_FLUSH_LIST&lt;/li&gt;
&lt;li&gt;线程刷脏的比较慢，导致用户线程加载一个新的数据页时发现没有空闲缓冲页，此时会尝试从 LRU 链表尾部寻找缓冲页直接释放，如果该页面是已经修改过的脏页就&lt;strong&gt;同步刷新&lt;/strong&gt;到磁盘，速度较慢，这种方式称为 BUF_FLUSH_SINGLE_PAGE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从 LRU 链表的冷数据中刷新一部分页面到磁盘，即：BUF_FLUSH_LRU
&lt;ul&gt;
&lt;li&gt;后台线程会定时从 LRU 链表的尾部开始扫描一些页面，扫描的页面数量可以通过系统变量 &lt;code&gt;innodb_lru_scan_depth&lt;/code&gt; 指定，如果在 LRU 链表中发现脏页，则把它们刷新到磁盘，这种方式称为 BUF_FLUSH_LRU&lt;/li&gt;
&lt;li&gt;控制块里会存储该缓冲页是否被修改的信息，所以可以很容易的获取到某个缓冲页是否是脏页&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/li1325169021/article/details/121125765&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;LRU 链表&lt;/h5&gt;
&lt;p&gt;Buffer Pool 需要保证缓存的命中率，所以 MySQL 创建了一个 LRU 链表，当访问某个页时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果该页不在 Buffer Pool 中，把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 &lt;strong&gt;LRU 链表的头部&lt;/strong&gt;，保证热点数据在链表头&lt;/li&gt;
&lt;li&gt;如果该页在 Buffer Pool 中，则直接把该页对应的控制块移动到 LRU 链表的头部，所以 LRU 链表尾部就是最近最少使用的缓冲页&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 基于局部性原理提供了预读功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线性预读：系统变量 &lt;code&gt;innodb_read_ahead_threshold&lt;/code&gt;，如果顺序访问某个区（extent：16 KB 的页，连续 64 个形成一个区，一个区默认 1MB 大小）的页面数超过了该系统变量值，就会触发一次&lt;strong&gt;异步读取&lt;/strong&gt;下一个区中全部的页面到 Buffer Pool 中&lt;/li&gt;
&lt;li&gt;随机预读：如果某个区 13 个连续的页面都被加载到 Buffer Pool，无论这些页面是否是顺序读取，都会触发一次&lt;strong&gt;异步读取&lt;/strong&gt;本区所有的其他页面到 Buffer Pool 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;预读会造成加载太多用不到的数据页，造成那些使用频率很高的数据页被挤到 LRU 链表尾部，所以 InnoDB 将 LRU 链表分成两段，&lt;strong&gt;冷热数据隔离&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一部分存储使用频率很高的数据页，这部分链表也叫热数据，young 区，靠近链表头部的区域&lt;/li&gt;
&lt;li&gt;一部分存储使用频率不高的冷数据，old 区，靠近链表尾部，默认占 37%，可以通过系统变量 &lt;code&gt;innodb_old_blocks_pct&lt;/code&gt; 指定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区，淘汰时优先淘汰 old 区&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当对 old 区的数据进行访问时，会在控制块记录下访问时间，等待后续的访问时间与第一次访问的时间是否在某个时间间隔内，通过系统变量 &lt;code&gt;innodb_old_blocks_time&lt;/code&gt; 指定时间间隔，默认 1000ms，成立就&lt;strong&gt;移动到 young 区的链表头部&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innodb_old_blocks_time&lt;/code&gt; 为 0 时，每次访问一个页面都会放入 young 区的头部&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;参数优化&lt;/h4&gt;
&lt;p&gt;InnoDB 用一块内存区做 IO 缓存池，该缓存池不仅用来缓存 InnoDB 的索引块，也用来缓存 InnoDB 的数据块，可以通过下面的指令查看 Buffer Pool 的状态信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS\G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Buffer pool hit rate&lt;/code&gt; 字段代表&lt;strong&gt;内存命中率&lt;/strong&gt;，表示 Buffer Pool 对查询的加速效果&lt;/p&gt;
&lt;p&gt;核心参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;：该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小，默认 128M&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;innodb_buffer_pool_size&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在保证操作系统及其他程序有足够内存可用的情况下，&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 的值越大，缓存命中率越高，建议设置成可用物理内存的 60%~80%&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;innodb_buffer_pool_size=512M
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;innodb_log_buffer_size&lt;/code&gt;：该值决定了 Innodb 日志缓冲区的大小，保存要写入磁盘上的日志文件数据&lt;/p&gt;
&lt;p&gt;对于可能产生大量更新记录的大事务，增加该值的大小，可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作，影响执行效率，通过配置文件修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;innodb_log_buffer_size=10M
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在多线程下，访问 Buffer Pool 中的各种链表都需要加锁，所以将 Buffer Pool 拆成若干个小实例，&lt;strong&gt;每个线程对应一个实例&lt;/strong&gt;，独立管理内存空间和各种链表（类似 ThreadLocal），多线程访问各自实例互不影响，提高了并发能力&lt;/p&gt;
&lt;p&gt;MySQL 5.7.5 之前 &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 只支持在系统启动时修改，现在已经支持运行时修改 Buffer Pool 的大小，但是每次调整参数都会重新向操作系统申请一块连续的内存空间，&lt;strong&gt;将旧的缓冲池的内容拷贝到新空间&lt;/strong&gt;非常耗时，所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存，所以一个 Buffer Pool 实例可以由多个 chunk 组成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在系统启动时设置系统变量 &lt;code&gt;innodb_buffer_pool_instance&lt;/code&gt; 可以指定 Buffer Pool 实例的个数，但是当 Buffer Pool 小于 1GB 时，设置多个实例时无效的&lt;/li&gt;
&lt;li&gt;指定系统变量 &lt;code&gt;innodb_buffer_pool_chunk_size&lt;/code&gt; 来改变 chunk 的大小，只能在启动时修改，运行中不能修改，而且该变量并不包含缓冲页的控制块的内存大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 必须是 &lt;code&gt;innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance&lt;/code&gt; 的倍数，默认值是 &lt;code&gt;128M × 16 = 2G&lt;/code&gt;，Buffer Pool 必须是 2G 的整数倍，如果指定 5G，会自动调整成 6G&lt;/li&gt;
&lt;li&gt;如果启动时 &lt;code&gt;chunk × instances&lt;/code&gt; &amp;gt; &lt;code&gt;pool_size&lt;/code&gt;，那么 chunk 的值会自动设置为 &lt;code&gt;pool_size ÷ instances&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;内存优化&lt;/h3&gt;
&lt;h4&gt;Change&lt;/h4&gt;
&lt;p&gt;InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对&lt;strong&gt;增删改操作&lt;/strong&gt;提供缓存，可以通过参数来动态设置，设置为 50 时表示 Change Buffer 的大小最多占用 Buffer Pool 的 50%&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一索引的更新不能使用 Change Buffer，需要将数据页读入内存，判断没有冲突在写入&lt;/li&gt;
&lt;li&gt;普通索引可以使用 Change Buffer，&lt;strong&gt;直接写入 Buffer 就结束&lt;/strong&gt;，不用校验唯一性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Change Buffer 并不是数据页，只是对操作的缓存，所以需要将 Change Buffer 中的操作应用到旧数据页，得到新的数据页（脏页）的过程称为 Merge&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发时机：访问数据页时会触发 Merge、后台有定时线程进行 Merge、在数据库正常关闭（shutdown）的过程中也会触发&lt;/li&gt;
&lt;li&gt;工作流程：首先从磁盘读入数据页到内存（因为 Buffer Pool 中不一定存在对应的数据页），从 Change Buffer 中找到对应的操作应用到数据页，得到新的数据页即为脏页，然后写入 redo log，等待刷脏即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：Change Buffer 中的记录，在事务提交时也会写入 redo log，所以是可以保证不丢失的&lt;/p&gt;
&lt;p&gt;业务场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;strong&gt;写多读少&lt;/strong&gt;的业务来说，页面在写完以后马上被访问到的概率比较小，此时 Change Buffer 的使用效果最好，常见的就是账单类、日志类的系统&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个业务的更新模式是写入后马上做查询，那么即使满足了条件，将更新先记录在 Change Buffer，但之后由于马上要访问这个数据页，会立即触发 Merge 过程，这样随机访问 IO 的次数不会减少，并且增加了 Change Buffer 的维护代价&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：Change Buffer 的前身是 Insert Buffer，只能对 Insert 操作优化，后来增加了 Update/Delete 的支持，改为 Change Buffer&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Net&lt;/h4&gt;
&lt;p&gt;Server 层针对优化&lt;strong&gt;查询&lt;/strong&gt;的内存为 Net Buffer，内存的大小是由参数 &lt;code&gt;net_buffer_length&lt;/code&gt;定义，默认 16k，实现流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取一行数据写入 Net Buffer，重复获取直到 Net Buffer 写满，调用网络接口发出去&lt;/li&gt;
&lt;li&gt;若发送成功就清空 Net Buffer，然后继续取下一行；若发送函数返回 EAGAIN 或 WSAEWOULDBLOCK，表示本地网络栈 &lt;code&gt;socket send buffer&lt;/code&gt; 写满了，&lt;strong&gt;进入等待&lt;/strong&gt;，直到网络栈重新可写再继续发送&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 采用的是边读边发的逻辑，因此对于数据量很大的查询来说，不会在 Server 端保存完整的结果集，如果客户端读结果不及时，会堵住 MySQL 的查询过程，但是&lt;strong&gt;不会把内存打爆导致 OOM&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查询内存优化.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;SHOW PROCESSLIST 获取线程信息后，处于 Sending to client 状态代表服务器端的网络栈写满，等待客户端接收数据&lt;/p&gt;
&lt;p&gt;假设有一个业务的逻辑比较复杂，每读一行数据以后要处理很久的逻辑，就会导致客户端要过很久才会去取下一行数据，导致 MySQL 的阻塞，一直处于 Sending to client 的状态&lt;/p&gt;
&lt;p&gt;解决方法：如果一个查询的返回结果很是很多，建议使用 mysql_store_result 这个接口，直接把查询结果保存到本地内存&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/qq_33589510/article/details/117673449&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Read&lt;/h4&gt;
&lt;p&gt;read_rnd_buffer 是 MySQL 的随机读缓冲区，当按任意顺序读取记录行时将分配一个随机读取缓冲区，进行排序查询时，MySQL 会首先扫描一遍该缓冲，以避免磁盘搜索，提高查询速度，大小是由 read_rnd_buffer_size 参数控制的&lt;/p&gt;
&lt;p&gt;Multi-Range Read 优化，&lt;strong&gt;将随机 IO 转化为顺序 IO&lt;/strong&gt; 以降低查询过程中 IO 开销，因为大多数的数据都是按照主键递增顺序插入得到，所以按照主键的递增顺序查询的话，对磁盘的读比较接近顺序读，能够提升读性能&lt;/p&gt;
&lt;p&gt;二级索引为 a，聚簇索引为 id，优化回表流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据索引 a，定位到满足条件的记录，将 id 值放入 read_rnd_buffer 中&lt;/li&gt;
&lt;li&gt;将 read_rnd_buffer 中的 id 进行&lt;strong&gt;递增排序&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;排序后的 id 数组，依次回表到主键 id 索引中查记录，并作为结果返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：如果步骤 1 中 read_rnd_buffer 放满了，就会先执行步骤 2 和 3，然后清空 read_rnd_buffer，之后继续找索引 a 的下个记录&lt;/p&gt;
&lt;p&gt;使用 MRR 优化需要设进行设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET optimizer_switch=&apos;mrr_cost_based=off&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Key&lt;/h4&gt;
&lt;p&gt;MyISAM 存储引擎使用 key_buffer 缓存索引块，加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制，完全依赖于操作系统的 IO 缓存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;key_buffer_size：该变量决定 MyISAM 索引块缓存区的大小，直接影响到 MyISAM 表的存取效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;key_buffer_size&apos;;	-- 单位是字节
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 MySQL 配置文件中设置该值，建议至少将1/4可用内存分配给 key_buffer_size：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/mysql/my.cnf
key_buffer_size=1024M
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;read_buffer_size：如果需要经常顺序扫描 MyISAM 表，可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的，如果默认值设置太大，并发环境就会造成内存浪费&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;read_rnd_buffer_size：对于需要做排序的 MyISAM 表的查询，如带有 ORDER BY 子句的语句，适当增加该的值，可以改善此类的 SQL 的性能，但是 read_rnd_buffer_size 是每个 Session 独占的，如果默认值设置太大，就会造成内存浪费&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;存储优化&lt;/h3&gt;
&lt;h4&gt;数据存储&lt;/h4&gt;
&lt;p&gt;系统表空间是用来放系统信息的，比如数据字典什么的，对应的磁盘文件是 ibdata，数据表空间是一个个的表数据文件，对应的磁盘文件就是表名.ibd&lt;/p&gt;
&lt;p&gt;表数据既可以存在共享表空间里，也可以是单独的文件，这个行为是由参数 innodb_file_per_table 控制的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OFF：表示表的数据放在系统共享表空间，也就是跟数据字典放在一起&lt;/li&gt;
&lt;li&gt;ON ：表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中（默认）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个表单独存储为一个文件更容易管理，在不需要这个表时通过 drop table 命令，系统就会直接删除这个文件；如果是放在共享表空间中，即使表删掉了，空间也是不会回收的&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;数据删除&lt;/h4&gt;
&lt;p&gt;MySQL 的数据删除就是移除掉某个记录后，该位置就被标记为&lt;strong&gt;可复用&lt;/strong&gt;，如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4，之后要再插入一个 ID 在 300 和 600 之间的记录时，就会复用这个位置&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-删除数据.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录，整个数据页就可以被复用了，如果相邻的两个数据页利用率都很小，系统就会把这两个页上的数据合到其中一个页上，另外一个数据页就被标记为可复用&lt;/p&gt;
&lt;p&gt;删除命令其实只是把记录的位置，或者&lt;strong&gt;数据页标记为了可复用，但磁盘文件的大小是不会变的&lt;/strong&gt;，这些可以复用还没有被使用的空间，看起来就像是空洞，造成数据库的稀疏，因此需要进行紧凑处理&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;重建数据&lt;/h4&gt;
&lt;p&gt;重建表就是按照主键 ID 递增的顺序，把数据一行一行地从旧表中读出来再插入到新表中，让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作，线上操作会阻塞大量的线程增删改查的操作&lt;/p&gt;
&lt;p&gt;重建命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE A ENGINE=InnoDB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作流程：新建临时表 tmp_table B（在 Server 层创建的），把表 A 中的数据导入到表 B 中，操作完成后用表 B 替换表 A，完成重建&lt;/p&gt;
&lt;p&gt;重建表的步骤需要 DDL 不是 Online 的，因为在导入数据的过程有新的数据要写入到表 A 的话，就会造成数据丢失&lt;/p&gt;
&lt;p&gt;MySQL 5.6 版本开始引入的 &lt;strong&gt;Online DDL&lt;/strong&gt;，重建表的命令默认执行此步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建立一个临时文件 tmp_file（InnoDB 创建），扫描表 A 主键的所有数据页&lt;/li&gt;
&lt;li&gt;用数据页中表 A 的记录生成 B+ 树，存储到临时文件中&lt;/li&gt;
&lt;li&gt;生成临时文件的过程中，将所有对 A 的操作记录在一个日志文件（row log）中，对应的是图中 state2 的状态&lt;/li&gt;
&lt;li&gt;临时文件生成后，将日志文件中的操作应用到临时文件，得到一个逻辑数据上与表 A 相同的数据文件，对应的就是图中 state3&lt;/li&gt;
&lt;li&gt;用临时文件替换表 A 的数据文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-重建表.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Online DDL 操作会先获取 MDL 写锁，再退化成 MDL 读锁。但 MDL 写锁持有时间比较短，所以可以称为 Online； 而 MDL 读锁，不阻止数据增删查改，但会阻止其它线程修改表结构（可以对比 &lt;code&gt;ANALYZE TABLE t&lt;/code&gt;  命令）&lt;/p&gt;
&lt;p&gt;问题：重建表可以收缩表空间，但是执行指令后整体占用空间增大&lt;/p&gt;
&lt;p&gt;原因：在重建表后 InnoDB 不会把整张表占满，每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上，收缩之后需要保持数据占用空间在 15/16，所以文件占用空间更大才能保持&lt;/p&gt;
&lt;p&gt;注意：临时文件也要占用空间，如果空间不足会重建失败&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;原地置换&lt;/h4&gt;
&lt;p&gt;DDL 中的临时表 tmp_table 是在 Server 层创建的，Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的，整个 DDL 过程都在 InnoDB 内部完成，对于 Server 层来说，没有把数据挪动到临时表，是一个原地操作，这就是 inplace&lt;/p&gt;
&lt;p&gt;两者的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDL 过程如果是 Online 的，就一定是 inplace 的&lt;/li&gt;
&lt;li&gt;inplace 的 DDL，有可能不是 Online 的，截止到 MySQL 8.0，全文索引（FULLTEXT）和空间索引（SPATIAL）属于这种情况&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;并发优化&lt;/h3&gt;
&lt;p&gt;MySQL Server 是多线程结构，包括后台线程和客户服务线程。多线程可以有效利用服务器资源，提高数据库的并发性能。在 MySQL 中，控制并发连接和线程的主要参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;max_connections：控制允许连接到 MySQL 数据库的最大连接数，默认值是 151&lt;/p&gt;
&lt;p&gt;如果状态变量 connection_errors_max_connections 不为零，并且一直增长，则说明不断有连接请求因数据库连接数已达到允许最大值而失败，这时可以考虑增大 max_connections 的值&lt;/p&gt;
&lt;p&gt;MySQL 最大可支持的连接数取决于很多因素，包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下，性能好的服务器，可以支持 500-1000 个连接，需要根据服务器性能进行评估设定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;innodb_thread_concurrency：并发线程数，代表系统内同时运行的线程数量（已经被移除）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;back_log：控制 MySQL 监听 TCP 端口时的积压请求栈的大小&lt;/p&gt;
&lt;p&gt;如果 Mysql 的连接数达到 max_connections 时，新来的请求将会被存在堆栈中，以等待某一连接释放资源，该堆栈的数量即 back_log。如果等待连接的数量超过 back_log，将不被授予连接资源直接报错&lt;/p&gt;
&lt;p&gt;5.6.6 版本之前默认值为 50，之后的版本默认为 &lt;code&gt;50 + (max_connections/5)&lt;/code&gt;，但最大不超过900，如果需要数据库在较短的时间内处理大量连接请求， 可以考虑适当增大 back_log 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;table_open_cache：控制所有 SQL 语句执行线程可打开表缓存的数量&lt;/p&gt;
&lt;p&gt;在执行 SQL 语句时，每个执行线程至少要打开1个表缓存，该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定：&lt;code&gt;max_connections * N&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;thread_cache_size：可控制 MySQL 缓存客户服务线程的数量&lt;/p&gt;
&lt;p&gt;为了加快连接数据库的速度，MySQL 会缓存一定数量的客户服务线程以备重用，池化思想&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;innodb_lock_wait_timeout：设置 InnoDB 事务等待行锁的时间，默认值是 50ms&lt;/p&gt;
&lt;p&gt;对于需要快速反馈的业务系统，可以将行锁的等待时间调小，以避免事务被长时间挂起； 对于后台运行的批量处理程序来说，可以将行锁的等待时间调大，以避免发生大的回滚操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;事务机制&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;事务（Transaction）是访问和更新数据库的程序执行单元；事务中可能包含一个或多个 SQL 语句，这些语句要么都执行，要么都不执行，作为一个关系型数据库，MySQL 支持事务。&lt;/p&gt;
&lt;p&gt;单元中的每条 SQL 语句都相互依赖，形成一个整体&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果某条 SQL 语句执行失败或者出现错误，那么整个单元就会回滚，撤回到事务最初的状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果单元中所有的 SQL 语句都执行成功，则事务就顺利执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务的四大特征：ACID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子性 (atomicity)&lt;/li&gt;
&lt;li&gt;一致性 (consistency)&lt;/li&gt;
&lt;li&gt;隔离性 (isolaction)&lt;/li&gt;
&lt;li&gt;持久性 (durability)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务的几种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;活动的（active）：事务对应的数据库操作正在执行中&lt;/li&gt;
&lt;li&gt;部分提交的（partially committed）：事务的最后一个操作执行完，但是内存还没刷新至磁盘&lt;/li&gt;
&lt;li&gt;失败的（failed）：当事务处于活动状态或部分提交状态时，如果数据库遇到了错误或刷脏失败，或者用户主动停止当前的事务&lt;/li&gt;
&lt;li&gt;中止的（aborted）：失败状态的事务回滚完成后的状态&lt;/li&gt;
&lt;li&gt;提交的（committed）：当处于部分提交状态的事务刷脏成功，就处于提交状态&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;事务管理&lt;/h3&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;p&gt;事务管理的三个步骤&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;开启事务：记录回滚点，并通知服务器，将要执行一组操作，要么同时成功、要么同时失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 SQL 语句：执行具体的一条或多条 SQL 语句&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结束事务（提交|回滚）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提交：没出现问题，数据进行更新&lt;/li&gt;
&lt;li&gt;回滚：出现问题，数据恢复到开启事务时的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;事务操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;显式开启事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态，最后的是一致性读
BEGIN [WORK];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：不填状态默认是读写事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回滚事务，用来手动中止事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提交事务，显示执行是手动提交，MySQL 默认为自动提交&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存点：在事务的执行过程中设置的还原点，调用 ROLLBACK 时可以指定回滚到哪个点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SAVEPOINT point_name;						#设置保存点
RELEASE point_name							#删除保存点
ROLLBACK [WORK] TO [SAVEPOINT] point_name	#回滚至某个保存点，不填默认回滚到事务执行之前的状态
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作演示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 开启事务
START TRANSACTION;

-- 张三给李四转账500元
-- 1.张三账户-500
UPDATE account SET money=money-500 WHERE NAME=&apos;张三&apos;;
-- 2.李四账户+500
UPDATE account SET money=money+500 WHERE NAME=&apos;李四&apos;;

-- 回滚事务(出现问题)
ROLLBACK;

-- 提交事务(没出现问题)
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;提交方式&lt;/h4&gt;
&lt;p&gt;提交方式的相关语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看事务提交方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT @@AUTOCOMMIT;  		-- 会话，1 代表自动提交    0 代表手动提交
SELECT @@GLOBAL.AUTOCOMMIT;	-- 系统
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改事务提交方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET @@AUTOCOMMIT=数字;	-- 系统
SET AUTOCOMMIT=数字;		-- 会话
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统变量的操作&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET [GLOBAL|SESSION] 变量名 = 值;					-- 默认是会话
SET @@[(GLOBAL|SESSION).]变量名 = 值;				-- 默认是系统
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SHOW [GLOBAL|SESSION] VARIABLES [LIKE &apos;变量%&apos;];	  -- 默认查看会话内系统变量值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动提交：如果没有 START TRANSACTION 显式地开始一个事务，那么&lt;strong&gt;每条 SQL 语句都会被当做一个事务执行提交操作&lt;/strong&gt;；显式开启事务后，会在本次事务结束（提交或回滚）前暂时关闭自动提交&lt;/li&gt;
&lt;li&gt;手动提交：不需要显式的开启事务，所有的 SQL 语句都在一个事务中，直到执行了提交或回滚，然后进入下一个事务&lt;/li&gt;
&lt;li&gt;隐式提交：存在一些特殊的命令，在事务中执行了这些命令会马上&lt;strong&gt;强制执行 COMMIT 提交事务&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DDL 语句&lt;/strong&gt; (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等&lt;/li&gt;
&lt;li&gt;当一个事务还没提交或回滚，显式的开启一个事务会隐式的提交上一个事务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;事务 ID&lt;/h4&gt;
&lt;p&gt;事务在执行过程中对某个表执行了&lt;strong&gt;增删改操作或者创建表&lt;/strong&gt;，就会为当前事务分配一个独一无二的事务 ID（对临时表并不会分配 ID），如果当前事务没有被分配 ID，默认是 0&lt;/p&gt;
&lt;p&gt;说明：只读事务不能对普通的表进行增删改操作，但是可以对临时表增删改，读写事务可以对数据表执行增删改查操作&lt;/p&gt;
&lt;p&gt;事务 ID 本质上就是一个数字，服务器在内存中维护一个全局变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每当需要为某个事务分配 ID，就会把全局变量的值赋值给事务 ID，然后变量自增 1&lt;/li&gt;
&lt;li&gt;每当变量值为 256 的倍数时，就将该变量的值刷新到系统表空间的 Max Trx ID 属性中，该属性占 8 字节&lt;/li&gt;
&lt;li&gt;系统再次启动后，会读取表空间的 Max Trx ID 属性到内存，加上 256 后赋值给全局变量，因为关机时的事务 ID 可能并不是 256 的倍数，会比 Max Trx ID 大，所以需要加上 256 保持事务 ID 是一个&lt;strong&gt;递增的数字&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;聚簇索引&lt;/strong&gt;的行记录除了完整的数据，还会自动添加 trx_id、roll_pointer 隐藏列，如果表中没有主键并且没有非空唯一索引，也会添加一个 row_id 的隐藏列作为聚簇索引&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;隔离级别&lt;/h3&gt;
&lt;h4&gt;四种级别&lt;/h4&gt;
&lt;p&gt;事务的隔离级别：多个客户端操作时，各个客户端的事务之间应该是隔离的，&lt;strong&gt;不同的事务之间不该互相影响&lt;/strong&gt;，而如果多个事务操作同一批数据时，则需要设置不同的隔离级别，否则就会产生问题。&lt;/p&gt;
&lt;p&gt;隔离级别分类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隔离级别&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;会引发的问题&lt;/th&gt;
&lt;th&gt;数据库默认隔离级别&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;读未提交&lt;/td&gt;
&lt;td&gt;脏读、不可重复读、幻读&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;读已提交&lt;/td&gt;
&lt;td&gt;不可重复读、幻读&lt;/td&gt;
&lt;td&gt;Oracle / SQL Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;可重复读&lt;/td&gt;
&lt;td&gt;幻读&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;可串行化&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一般来说，隔离级别越低，系统开销越低，可支持的并发越高，但隔离性也越差&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;脏写 (Dirty Write)：当两个或多个事务选择同一行，最初的事务修改的值被后面事务修改的值覆盖，所有的隔离级别都可以避免脏写（又叫丢失更新），因为有行锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;脏读 (Dirty Reads)：在一个事务处理过程中读取了另一个&lt;strong&gt;未提交&lt;/strong&gt;的事务中修改过的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可重复读 (Non-Repeatable Reads)：在一个事务处理过程中读取了另一个事务中修改并&lt;strong&gt;已提交&lt;/strong&gt;的数据&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可重复读的意思是不管读几次，结果都一样，可以重复的读，可以理解为快照读，要读的数据集不会发生变化&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;幻读 (Phantom Reads)：在事务中按某个条件先后两次查询数据库，后一次查询查到了前一次查询没有查到的行，&lt;strong&gt;数据条目&lt;/strong&gt;发生了变化。比如查询某数据不存在，准备插入此记录，但执行插入时发现此记录已存在，无法插入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;隔离级别操作语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询数据库隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT @@TX_ISOLATION;			-- 会话
SELECT @@GLOBAL.TX_ISOLATION;	-- 系统
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改数据库隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加锁分析&lt;/h4&gt;
&lt;p&gt;InnoDB 存储引擎支持事务，所以加锁分析是基于该存储引擎&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Read Uncommitted 级别，任何操作都不会加锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Read Committed 级别，增删改操作会加写锁（行锁），读操作不加锁&lt;/p&gt;
&lt;p&gt;在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁，保证最后只有满足条件的记录加锁，但是扫表过程中每条记录的&lt;strong&gt;加锁操作不能省略&lt;/strong&gt;。所以对数据量很大的表做批量修改时，如果无法使用相应的索引（全表扫描），在 Server 过滤数据时就会特别慢，出现虽然没有修改某些行的数据，但是还是被锁住了的现象（锁表），这种情况同样适用于  RR&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Repeatable Read 级别，增删改操作会加写锁，读操作不加锁。因为读写锁不兼容，&lt;strong&gt;加了读锁后其他事务就无法修改数据&lt;/strong&gt;，影响了并发性能，为了保证隔离性和并发性，MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种，锁机制章节详解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Serializable 级别，读加共享锁，写加排他锁，读写互斥，使用的悲观锁的理论，实现简单，数据更加安全，但是并发能力非常差&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;串行化：让所有事务按顺序单独执行，写操作会加写锁，读操作会加读锁&lt;/li&gt;
&lt;li&gt;可串行化：让所有操作相同数据的事务顺序执行，通过加锁实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://tech.meituan.com/2014/08/20/innodb-lock.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;原子特性&lt;/h3&gt;
&lt;h4&gt;实现方式&lt;/h4&gt;
&lt;p&gt;原子性是指事务是一个不可分割的工作单位，事务的操作如果成功就必须要完全应用到数据库，失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败，则已执行的语句也必须回滚，数据库退回到事务前的状态&lt;/p&gt;
&lt;p&gt;InnoDB 存储引擎提供了两种事务日志：redo log（重做日志）和 undo log（回滚日志）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redo log 用于保证事务持久性&lt;/li&gt;
&lt;li&gt;undo log 用于保证事务原子性和隔离性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;undo log 属于&lt;strong&gt;逻辑日志&lt;/strong&gt;，根据每行操作进行记录，记录了 SQL 执行相关的信息，用来回滚行记录到某个版本&lt;/p&gt;
&lt;p&gt;当事务对数据库进行修改时，InnoDB 会先记录对应的 undo log，如果事务执行失败或调用了 rollback 导致事务回滚，InnoDB 会根据 undo log 的内容&lt;strong&gt;做与之前相反的操作&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于每个 insert，回滚时会执行 delete&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个 delete，回滚时会执行 insert&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个 update，回滚时会执行一个相反的 update，把数据修改回去&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/kismetv/p/10331633.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;DML 解析&lt;/h4&gt;
&lt;h5&gt;INSERT&lt;/h5&gt;
&lt;p&gt;乐观插入：当前数据页的剩余空间充足，直接将数据进行插入&lt;/p&gt;
&lt;p&gt;悲观插入：当前数据页的剩余空间不足，需要进行页分裂，申请一个新的页面来插入数据，会造成更多的 redo log，undo log 影响不大&lt;/p&gt;
&lt;p&gt;当向某个表插入一条记录，实际上需要向聚簇索引和所有二级索引都插入一条记录，但是 undo log &lt;strong&gt;只针对聚簇索引记录&lt;/strong&gt;，在回滚时会根据聚簇索引去所有的二级索引进行回滚操作&lt;/p&gt;
&lt;p&gt;roll_pointer 是一个指针，&lt;strong&gt;指向记录对应的 undo log 日志&lt;/strong&gt;，一条记录就是一个数据行，行格式中的 roll_pointer 就指向 undo log&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;DELETE&lt;/h5&gt;
&lt;p&gt;插入到页面中的记录会根据 next_record 属性组成一个单向链表，这个链表称为正常链表，被删除的记录也会通过 next_record 组成一个垃圾链表，该链表中所占用的存储空间可以被重新利用，并不会直接清除数据&lt;/p&gt;
&lt;p&gt;在页面 Page Header 中，PAGE_FREE 属性指向垃圾链表的头节点，删除的工作过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将要删除的记录的 delete_flag 位置为 1，其他不做修改，这个过程叫 &lt;strong&gt;delete mark&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在事务提交前，delete_flag = 1 的记录一直都会处于中间状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务提交后，有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表，这个过程叫 &lt;strong&gt;purge&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;purge 线程在执行删除操作时会创建一个 ReadView，根据事务的可见性移除数据（隔离特性部分详解）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当有新插入的记录时，首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果可以容纳新纪录，就会直接重用已删除的记录的存储空间，然后让 PAGE_FREE 指向垃圾链表的下一个节点&lt;/li&gt;
&lt;li&gt;如果不能容纳新纪录，就直接向页面申请新的空间存储，并不会遍历垃圾链表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重用已删除的记录空间，可能会造成空间碎片，当数据页容纳不了一条记录时，会判断将碎片空间加起来是否可以容纳，判断为真就会重新组织页内的记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开辟一个临时页面，将页内记录一次插入到临时页面，此时临时页面时没有碎片的&lt;/li&gt;
&lt;li&gt;把临时页面的内容复制到本页，这样就解放出了内存碎片，但是会耗费很大的性能资源&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;UPDATE&lt;/h5&gt;
&lt;p&gt;执行 UPDATE 语句，对于更新主键和不更新主键有两种不同的处理方式&lt;/p&gt;
&lt;p&gt;不更新主键的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;就地更新（in-place update），如果更新后的列和更新前的列占用的存储空间一样大，就可以直接在原记录上修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;先删除旧纪录，再插入新纪录，这里的删除不是 delete mark，而是直接将记录加入垃圾链表，并且修改页面的相应的控制信息，执行删除的线程不是 purge，是执行更新的用户线程，插入新记录时可能造成页空间不足，从而导致页分裂&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更新主键的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将旧纪录进行 delete mark，在更新语句提交后由 purge 线程移入垃圾链表&lt;/li&gt;
&lt;li&gt;根据更新的各列的值创建一条新纪录，插入到聚簇索引中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在对一条记录修改前会&lt;strong&gt;将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到当前 undo log 对应的属性中&lt;/strong&gt;，这样当前记录的 roll_pointer 指向当前 undo log 记录，当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录，&lt;strong&gt;形成一个版本链&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作，所以不能立即删除，INSERT 可以删除的原因是 MVCC 是对现有数据的快照&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;回滚日志&lt;/h4&gt;
&lt;p&gt;undo log 是采用段的方式来记录，Rollback Segement 称为回滚段，本质上就是一个类型是 Rollback Segement Header 的页面&lt;/p&gt;
&lt;p&gt;每个回滚段中有 1024 个 undo slot，每个 slot 存放 undo 链表页面的头节点页号，每个链表对应一个叫 undo log segment 的段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在以前老版本，只支持 1 个 Rollback Segement，只能记录 1024 个 undo log segment&lt;/li&gt;
&lt;li&gt;MySQL5.5 开始支持 128 个 Rollback Segement，支持 128*1024 个 undo 操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事务执行前需要到系统表空间第 5 号页面中分配一个回滚段（页），获取一个 Rollback Segement Header 页面的地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回滚段页面有 1024 个 undo slot，首先去回滚段的两个 cached 链表获取缓存的 slot，缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是缓存中获取的 slot，则该 slot 对应的 undo log segment 已经分配了，需要重新分配，然后从 undo log segment 中申请一个页面作为日志链表的头节点，并填入对应的 slot 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个事务 undo 日志在记录的时候&lt;strong&gt;占用两个 undo 页面的组成链表&lt;/strong&gt;，分别为 insert undo 链表和 update undo 链表，链表的头节点页面为 first undo page 会包含一些管理信息，其他页面为 normal undo page&lt;/p&gt;
&lt;p&gt;说明：事务执行过程的临时表也需要两个 undo 链表，不和普通表共用，这些链表并不是事务开始就分配，而是按需分配&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;隔离特性&lt;/h3&gt;
&lt;h4&gt;实现方式&lt;/h4&gt;
&lt;p&gt;隔离性是指，事务内部的操作与其他事务是隔离的，多个并发事务之间要相互隔离，不能互相干扰&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;严格的隔离性，对应了事务隔离级别中的 serializable，实际应用中对性能考虑很少使用可串行化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与原子性、持久性侧重于研究事务本身不同，隔离性研究的是&lt;strong&gt;不同事务&lt;/strong&gt;之间的相互影响&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;隔离性让并发情形下的事务之间互不干扰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个事务的写操作对另一个事务的写操作（写写）：锁机制保证隔离性&lt;/li&gt;
&lt;li&gt;一个事务的写操作对另一个事务的读操作（读写）：MVCC 保证隔离性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;锁机制：事务在修改数据之前，需要先获得相应的锁，获得锁之后，事务便可以修改数据；该事务操作期间，这部分数据是锁定的，其他事务如果需要修改数据，需要等待当前事务提交或回滚后释放锁（详解见锁机制）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;并发控制&lt;/h4&gt;
&lt;p&gt;MVCC 全称 Multi-Version Concurrency Control，即多版本并发控制，用来&lt;strong&gt;解决读写冲突的无锁并发控制&lt;/strong&gt;，可以在发生读写请求冲突时不用加锁解决，这个读是指的快照读（也叫一致性读或一致性无锁读），而不是当前读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快照读：实现基于 MVCC，因为是多版本并发，所以快照读读到的数据不一定是当前最新的数据，有可能是历史版本的数据&lt;/li&gt;
&lt;li&gt;当前读：又叫加锁读，读取数据库记录是当前&lt;strong&gt;最新的版本&lt;/strong&gt;（产生幻读、不可重复读），可以对读取的数据进行加锁，防止其他事务修改数据，是悲观锁的一种操作，读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据库并发场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;读-读：不存在任何问题，也不需要并发控制&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读-写：有线程安全问题，可能会造成事务隔离性问题，可能遇到脏读，幻读，不可重复读&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写-写：有线程安全问题，可能会存在脏写（丢失更新）问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MVCC 的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在并发读写数据库时，做到在读操作时不用阻塞写操作，写操作也不用阻塞读操作，提高了并发读写的性能&lt;/li&gt;
&lt;li&gt;可以解决脏读，不可重复读等事务隔离问题（加锁也能解决），但不能解决更新丢失问题（写锁会解决）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提高读写和写写的并发性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MVCC + 悲观锁：MVCC 解决读写冲突，悲观锁解决写写冲突&lt;/li&gt;
&lt;li&gt;MVCC + 乐观锁：MVCC 解决读写冲突，乐观锁解决写写冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.jianshu.com/p/8845ddca3b23&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;h5&gt;隐藏字段&lt;/h5&gt;
&lt;p&gt;实现原理主要是隐藏字段，undo日志，Read View 来实现的&lt;/p&gt;
&lt;p&gt;InnoDB 存储引擎，数据库中的&lt;strong&gt;聚簇索引&lt;/strong&gt;每行数据，除了自定义的字段，还有数据库隐式定义的字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB_TRX_ID：最近修改事务 ID，记录创建该数据或最后一次修改该数据的事务 ID&lt;/li&gt;
&lt;li&gt;DB_ROLL_PTR：回滚指针，&lt;strong&gt;指向记录对应的 undo log 日志&lt;/strong&gt;，undo log 中又指向上一个旧版本的 undo log&lt;/li&gt;
&lt;li&gt;DB_ROW_ID：隐含的自增 ID（&lt;strong&gt;隐藏主键&lt;/strong&gt;），如果数据表没有主键，InnoDB 会自动以 DB_ROW_ID 作为聚簇索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC%E7%89%88%E6%9C%AC%E9%93%BE%E9%9A%90%E8%97%8F%E5%AD%97%E6%AE%B5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;版本链&lt;/h5&gt;
&lt;p&gt;undo log 是逻辑日志，记录的是每个事务对数据执行的操作，而不是记录的全部数据，要&lt;strong&gt;根据 undo log 逆推出以往事务的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;undo log 的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证事务进行 rollback 时的原子性和一致性，当事务进行回滚的时候可以用 undo log 的数据进行恢复&lt;/li&gt;
&lt;li&gt;用于 MVCC 快照读，通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;undo log 主要分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;insert undo log：事务在 insert 新记录时产生的 undo log，只在事务回滚时需要，并且在事务提交后可以被立即丢弃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;update undo log：事务在进行 update 或 delete 时产生的 undo log，在事务回滚时需要，在快照读时也需要。不能随意删除，只有在当前读或事务回滚不涉及该日志时，对应的日志才会被 purge 线程统一清除&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次对数据库记录进行改动，都会产生的新版本的 undo log，随着更新次数的增多，所有的版本都会被 roll_pointer 属性连接成一个链表，把这个链表称之为&lt;strong&gt;版本链&lt;/strong&gt;，版本链的头节点就是当前的最新的 undo log，链尾就是最早的旧 undo log&lt;/p&gt;
&lt;p&gt;说明：因为 DELETE 删除记录，都是移动到垃圾链表中，不是真正的删除，所以才可以通过版本链访问原始数据&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC版本链.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;注意：undo 是逻辑日志，这里只是直观的展示出来&lt;/p&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有个事务插入 persion 表一条新记录，name 为 Jerry，age 为 24&lt;/li&gt;
&lt;li&gt;事务 1 修改该行数据时，数据库会先对该行加排他锁，然后先记录 undo log，然后修改该行 name 为 Tom，并且修改隐藏字段的事务 ID 为当前事务 1 的 ID（默认为 1 之后递增），回滚指针指向拷贝到 undo log 的副本记录，事务提交后，释放锁&lt;/li&gt;
&lt;li&gt;以此类推&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;读视图&lt;/h5&gt;
&lt;p&gt;Read View 是事务进行读数据操作时产生的读视图，该事务执行快照读的那一刻会生成数据库系统当前的一个快照，记录并维护系统当前活跃事务的 ID，用来做可见性判断，根据视图判断当前事务能够看到哪个版本的数据&lt;/p&gt;
&lt;p&gt;注意：这里的快照并不是把所有的数据拷贝一份副本，而是由 undo log 记录的逻辑日志，根据库中的数据进行计算出历史数据&lt;/p&gt;
&lt;p&gt;工作流程：将版本链的头节点的事务 ID（最新数据事务 ID，大概率不是当前线程）DB_TRX_ID 取出来，与系统当前活跃事务的 ID 对比进行可见性分析，不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较，直到找到最近的满足可见性的 DB_TRX_ID，该事务 ID 所在的旧记录就是当前事务能看见的最新的记录&lt;/p&gt;
&lt;p&gt;Read View 几个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;m_ids：生成 Read View 时当前系统中活跃的事务 id 列表（未提交的事务集合，当前事务也在其中）&lt;/li&gt;
&lt;li&gt;min_trx_id：生成 Read View 时当前系统中活跃的最小的事务 id，也就是 m_ids 中的最小值（已提交的事务集合）&lt;/li&gt;
&lt;li&gt;max_trx_id：生成 Read View 时当前系统应该分配给下一个事务的 id 值，m_ids 中的最大值加 1（未开始事务）&lt;/li&gt;
&lt;li&gt;creator_trx_id：生成该 Read View 的事务的事务 id，就是判断该 id 的事务能读到什么数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;creator 创建一个 Read View，进行可见性算法分析：（解决了读未提交）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;db_trx_id == creator_trx_id：表示这个数据就是当前事务自己生成的，自己生成的数据自己肯定能看见，所以此数据对 creator 是可见的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;db_trx_id &amp;lt;  min_trx_id：该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID，则这个事务在当前事务之前就已经被提交了，对 creator 可见（因为比已提交的最大事务 ID 小的并不一定已经提交，所以应该判断是否在活跃事务列表）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;db_trx_id &amp;gt;= max_trx_id：该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID，则说明该数据是在当前 Read view 创建之后才产生的，对 creator 不可见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;min_trx_id&amp;lt;= db_trx_id &amp;lt; max_trx_id：判断 db_trx_id 是否在活跃事务列表 m_ids 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在列表中，说明该版本对应的事务正在运行，数据不能显示（&lt;strong&gt;不能读到未提交的数据&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;不在列表中，说明该版本对应的事务已经被提交，数据可以显示（&lt;strong&gt;可以读到已经提交的数据&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;工作流程&lt;/h5&gt;
&lt;p&gt;表 user 数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id		name		age
1		张三		   18	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transaction 20：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START TRANSACTION;	-- 开启事务
UPDATE user SET name = &apos;李四&apos; WHERE id = 1;
UPDATE user SET name = &apos;王五&apos; WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transaction 60：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START TRANSACTION;	-- 开启事务
-- 操作表的其他数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ID 为 0 的事务创建 Read View：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;m_ids：20、60&lt;/li&gt;
&lt;li&gt;min_trx_id：20&lt;/li&gt;
&lt;li&gt;max_trx_id：61&lt;/li&gt;
&lt;li&gt;creator_trx_id：0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;只有红框部分才复合条件，所以只有张三对应的版本的数据可以被看到&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1t5411u7Fg&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;二级索引&lt;/h5&gt;
&lt;p&gt;只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列，对于二级索引判断可见性的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性，代表修改当前页面的最大的事务 ID，SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性，大于说明该页面的所有属性对 ReadView 可见&lt;/li&gt;
&lt;li&gt;如果属性判断不可见，就需要利用二级索引获取主键值进行&lt;strong&gt;回表操作&lt;/strong&gt;，得到聚簇索引后按照聚簇索引的可见性判断的方法操作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;RC RR&lt;/h4&gt;
&lt;p&gt;Read View 用于支持 RC（Read Committed，读已提交）和 RR（Repeatable Read，可重复读）隔离级别的实现，所以 &lt;strong&gt;SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RR、RC 生成时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RC 隔离级别下，每次读取数据前都会生成最新的 Read View（当前读）&lt;/li&gt;
&lt;li&gt;RR 隔离级别下，在第一次数据读取时才会创建 Read View（快照读）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RC、RR 级别下的 InnoDB 快照读区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RC 级别下，事务中每次快照读都会新生成一个 Read View，这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RR 级别下，某个事务的对某条记录的&lt;strong&gt;第一次快照读&lt;/strong&gt;会创建一个 Read View， 将当前系统活跃的其他事务记录起来，此后在调用快照读的时候，使用的是同一个 Read View，所以一个事务的查询结果每次都是相同的&lt;/p&gt;
&lt;p&gt;RR 级别下，通过 &lt;code&gt;START TRANSACTION WITH CONSISTENT SNAPSHOT&lt;/code&gt; 开启事务，会在执行该语句后立刻生成一个 Read View，不是在执行第一条 SELECT 语句时生成（所以说 &lt;code&gt;START TRANSACTION&lt;/code&gt; 并不是事务的起点，执行第一条语句才算起点）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决幻读问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快照读：通过 MVCC 来进行控制的，在可重复读隔离级别下，普通查询是快照读，是不会看到别的事务插入的数据的，但是&lt;strong&gt;并不能完全避免幻读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;场景：RR 级别，T1 事务开启，创建 Read View，此时 T2 去 INSERT 新的一行然后提交，然后 T1 去 UPDATE 该行会发现更新成功，并且把这条新记录的 trx_id 变为当前的事务 id，所以对当前事务就是可见的。因为 &lt;strong&gt;Read View 并不能阻止事务去更新数据，更新数据都是先读后写并且是当前读&lt;/strong&gt;，读取到的是最新版本的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前读：通过 next-key 锁（行锁 + 间隙锁）来解决问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;持久特性&lt;/h3&gt;
&lt;h4&gt;实现方式&lt;/h4&gt;
&lt;p&gt;持久性是指一个事务一旦被提交了，那么对数据库中数据的改变就是永久性的，接下来的其他操作或故障不应该对其有任何影响。&lt;/p&gt;
&lt;p&gt;Buffer Pool 的使用提高了读写数据的效率，但是如果 MySQL 宕机，此时 Buffer Pool 中修改的数据还没有刷新到磁盘，就会导致数据的丢失，事务的持久性无法保证，所以引入了 redo log 日志：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redo log &lt;strong&gt;记录数据页的物理修改&lt;/strong&gt;，而不是某一行或某几行的修改，用来恢复提交后的数据页，只能&lt;strong&gt;恢复到最后一次提交&lt;/strong&gt;的位置&lt;/li&gt;
&lt;li&gt;redo log 采用的是 WAL（Write-ahead logging，&lt;strong&gt;预写式日志&lt;/strong&gt;），所有修改要先写入日志，再更新到磁盘，保证了数据不会因 MySQL 宕机而丢失，从而满足了持久性要求&lt;/li&gt;
&lt;li&gt;简单的 redo log 是纯粹的物理日志，复杂的 redo log 会存在物理日志和逻辑日志&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作过程：MySQL 发生了宕机，InnoDB 会判断一个数据页在崩溃恢复时丢失了更新，就会将它读到内存，然后根据 redo log 内容更新内存，更新完成后，内存页变成脏页，然后进行刷脏&lt;/p&gt;
&lt;p&gt;缓冲池的&lt;strong&gt;刷脏策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redo log 文件是固定大小的，如果写满了就要擦除以前的记录，在擦除之前需要把对应的更新持久化到磁盘中&lt;/li&gt;
&lt;li&gt;Buffer Pool 内存不足，需要淘汰部分数据页（LRU 链表尾部），如果淘汰的是脏页，就要先将脏页写到磁盘（要避免大事务）&lt;/li&gt;
&lt;li&gt;系统空闲时，后台线程会自动进行刷脏（Flush 链表部分已经详解）&lt;/li&gt;
&lt;li&gt;MySQL 正常关闭时，会把内存的脏页都刷新到磁盘上&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;重做日志&lt;/h4&gt;
&lt;h5&gt;日志缓冲&lt;/h5&gt;
&lt;p&gt;服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer（重做日志缓冲区），可以通过 &lt;code&gt;innodb_log_buffer_size&lt;/code&gt; 系统变量指定 redo log buffer 的大小，默认是 16MB&lt;/p&gt;
&lt;p&gt;log buffer 被划分为若干 redo log block（块，类似数据页的概念），每个默认大小 512 字节，每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当数据修改时，先修改 Change Buffer 中的数据，然后在 redo log buffer 记录这次操作，写入 log buffer 的过程是&lt;strong&gt;顺序写入&lt;/strong&gt;的（先写入前面的 block，写满后继续写下一个）&lt;/li&gt;
&lt;li&gt;log buffer 中有一个指针 buf_free，来标识该位置之前都是填满的 block，该位置之后都是空闲区域&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction（MTR），比如在 B+ 树上插入一条数据就算一个 MTR&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个事务包含若干个 MTR，一个 MTR 对应一组若干条 redo log，一组 redo log 是不可分割的，在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不是每生成一条 redo 日志就将其插入到 log buffer 中，而是一个 MTR 结束后&lt;strong&gt;将一组 redo 日志写入&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB 的 redo log 是&lt;strong&gt;固定大小&lt;/strong&gt;的，redo 日志在磁盘中以文件组的形式存储，同一组中的每个文件大小一样格式一样&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;innodb_log_group_home_dir&lt;/code&gt; 代表磁盘存储 redo log 的文件目录，默认是当前数据目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innodb_log_file_size&lt;/code&gt; 代表文件大小，默认 48M，&lt;code&gt;innodb_log_files_in_group&lt;/code&gt; 代表文件个数，默认 2 最大 100，所以日志的文件大小为 &lt;code&gt;innodb_log_file_size * innodb_log_files_in_group&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;redo 日志文件也是由若干个 512 字节的 block 组成，日志文件的前 2048 个字节（前 4 个 block）用来存储一些管理信息，以后的用来存储 log buffer 中的 block 镜像&lt;/p&gt;
&lt;p&gt;注意：block 并不代表一组 redo log，一组日志可能占用不到一个 block 或者几个 block，依赖于 MTR 的大小&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;日志刷盘&lt;/h5&gt;
&lt;p&gt;redo log 需要在事务提交时将日志写入磁盘，但是比 Buffer Pool 修改的数据写入磁盘的速度快，原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;刷脏是随机 IO，因为每次修改的数据位置随机；redo log 和 binlog 都是&lt;strong&gt;顺序写&lt;/strong&gt;，磁盘的顺序 IO 比随机 IO 速度要快&lt;/li&gt;
&lt;li&gt;刷脏是以数据页（Page）为单位的，一个页上的一个小修改都要整页写入；redo log 中只包含真正需要写入的部分，好几页的数据修改可能只记录在一个 redo log 页中，减少无效 IO&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组提交机制&lt;/strong&gt;，可以大幅度降低磁盘的 IO 消耗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB 引擎会在适当的时候，把内存中 redo log buffer 持久化（fsync）到磁盘，具体的&lt;strong&gt;刷盘策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在事务提交时需要进行刷盘，通过修改参数 &lt;code&gt;innodb_flush_log_at_trx_commit&lt;/code&gt; 设置：
&lt;ul&gt;
&lt;li&gt;0：表示当提交事务时，并不将缓冲区的 redo 日志写入磁盘，而是等待&lt;strong&gt;后台线程每秒刷新一次&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;1：在事务提交时将缓冲区的 redo 日志&lt;strong&gt;同步写入&lt;/strong&gt;到磁盘，保证一定会写入成功（默认值）&lt;/li&gt;
&lt;li&gt;2：在事务提交时将缓冲区的 redo 日志异步写入到磁盘，不能保证提交时肯定会写入，只是有这个动作。日志已经在操作系统的缓存，如果操作系统没有宕机而 MySQL 宕机，也是可以恢复数据的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;写入 redo log buffer 的日志超过了总容量的一半，就会将日志刷入到磁盘文件，这会影响执行效率，所以开发中应&lt;strong&gt;避免大事务&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;服务器关闭时&lt;/li&gt;
&lt;li&gt;并行的事务提交（组提交）时，会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log  buffer 中，这时另外一个线程的事务 B 提交，如果 innodb_flush_log_at_trx_commit 设置的是 1，那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘，&lt;strong&gt;因为多个事务共用一个 redo log buffer&lt;/strong&gt;，所以一次 fsync 可以刷盘多个事务的 redo log，提升了并发量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器启动后 redo 磁盘空间不变，所以 redo 磁盘中的日志文件是被&lt;strong&gt;循环使用&lt;/strong&gt;的，采用循环写数据的方式，写完尾部重新写头部，所以要确保头部 log 对应的修改已经持久化到磁盘&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;日志序号&lt;/h5&gt;
&lt;p&gt;lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量，两者都是&lt;strong&gt;全局变量&lt;/strong&gt;，如果两者的值相同，说明 log buffer 中所有的 redo 日志都已经持久化到磁盘&lt;/p&gt;
&lt;p&gt;工作过程：写入 log buffer 数据时，buf_free 会进行偏移，偏移量就会加到 lsn 上&lt;/p&gt;
&lt;p&gt;MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中，链表中脏页是按照第一次修改的时间进行排序的（头插），控制块中有两个指针用来记录脏页被修改的时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;oldest_modification：第一次修改 Buffer Pool 中某个缓冲页时，将修改该页的 MTR &lt;strong&gt;开始时&lt;/strong&gt;对应的 lsn 值写入这个属性&lt;/li&gt;
&lt;li&gt;newest_modification：每次修改页面，都将 MTR 结束时全局的 lsn 值写入这个属性，所以该值是该页面最后一次修改后的 lsn 值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全局变量 checkpoint_lsn 表示&lt;strong&gt;当前系统可以被覆盖的 redo 日志总量&lt;/strong&gt;，当 redo 日志对应的脏页已经被刷新到磁盘后，该文件空间就可以被覆盖重用，此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息（刷脏和执行一次 checkpoint 并不是同一个线程），该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量，所以该值是一直增大的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;checkpoint&lt;/strong&gt;：从 flush 链表尾部中找出还未刷脏的页面，该页面是当前系统中最早被修改的脏页，该页面之前产生的脏页都已经刷脏，然后将该页 oldest_modification 值赋值给 checkpoint_lsn，因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了&lt;/p&gt;
&lt;p&gt;但是在系统忙碌时，后台线程的刷脏操作不能将脏页快速刷出，导致系统无法及时执行 checkpoint ，这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中，然后执行 checkpoint&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;write pos ------- checkpoint_lsn // 两值之间的部分表示可以写入的日志量，当 pos 追赶上 lsn 时必须执行 checkpoint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS\G
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;崩溃恢复&lt;/h5&gt;
&lt;p&gt;恢复的起点：在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息，&lt;strong&gt;从 checkpoint_lsn 对应的日志文件开始恢复&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;恢复的终点：扫描日志文件的 block，block 的头部记录着当前 block 使用了多少字节，填满的 block 总是 512 字节， 如果某个 block 不是 512 字节，说明该 block 就是需要恢复的最后一个 block&lt;/p&gt;
&lt;p&gt;恢复的过程：按照 redo log 依次执行恢复数据，优化方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用哈希表：根据 redo log 的 space id 和 page number 属性计算出哈希值，将对同一页面的修改放入同一个槽里，可以一次性完成对某页的恢复，&lt;strong&gt;避免了随机 IO&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;跳过已经刷新到磁盘中的页面：数据页的 File Header 中的 FILE_PAGE_LSN 属性（类似 newest_modification）表示最近一次修改页面时的 lsn 值，数据页被刷新到磁盘中，那么该页 lsn 属性肯定大于 checkpoint_lsn&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/35231266/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;工作流程&lt;/h4&gt;
&lt;h5&gt;日志对比&lt;/h5&gt;
&lt;p&gt;MySQL 中还存在 binlog（二进制日志）也可以记录写操作并用于数据的恢复，&lt;strong&gt;保证数据不丢失&lt;/strong&gt;，二者的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作用不同：redo log 是用于 crash recovery （故障恢复），保证 MySQL 宕机也不会影响持久性；binlog 是用于 point-in-time recovery 的，保证服务器可以基于时间点恢复数据，此外 binlog 还用于主从复制&lt;/li&gt;
&lt;li&gt;层次不同：redo log 是 InnoDB 存储引擎实现的，而 binlog 是MySQL的 Server 层实现的，同时支持 InnoDB 和其他存储引擎&lt;/li&gt;
&lt;li&gt;内容不同：redo log 是物理日志，内容基于磁盘的 Page；binlog 的内容是二进制的，根据 binlog_format 参数的不同，可能基于SQL 语句、基于数据本身或者二者的混合（日志部分详解）&lt;/li&gt;
&lt;li&gt;写入时机不同：binlog 在事务提交时一次写入；redo log 的写入时机相对多元&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;binlog 为什么不支持崩溃恢复？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;binlog 记录的是语句，并不记录数据页级的数据（哪个页改了哪些地方），所以没有能力恢复数据页&lt;/li&gt;
&lt;li&gt;binlog 是追加写，保存全量的日志，没有标志确定从哪个点开始的数据是已经刷盘了，而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;更新记录&lt;/h5&gt;
&lt;p&gt;更新一条记录的过程：写之前一定先读&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 B+ 树中定位到该记录，如果该记录所在的页面不在 Buffer Pool 里，先将其加载进内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;首先更新该记录对应的聚簇索引，更新聚簇索引记录时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;更新记录前向 undo 页面写 undo 日志，由于这是更改页面，所以需要记录一下相应的 redo 日志&lt;/p&gt;
&lt;p&gt;注意：修改 undo 页面也是在&lt;strong&gt;修改页面&lt;/strong&gt;，事务只要修改页面就需要先记录相应的 redo 日志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后&lt;strong&gt;记录对应的 redo 日志&lt;/strong&gt;（等待 MTR 提交后写入 redo log buffer），&lt;strong&gt;最后进行真正的更新记录&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新其他的二级索引记录，不会再记录 undo log，只记录 redo log 到 buffer 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一条更新语句执行完成后（也就是将所有待更新记录都更新完了），就会开始记录该语句对应的 binlog 日志，此时记录的 binlog 并没有刷新到硬盘上，还在内存中，在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设表中有字段 id 和 a，存在一条 &lt;code&gt;id = 1, a = 2&lt;/code&gt; 的记录，此时执行更新语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;update table set a=2 where id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;InnoDB 会真正的去执行把值修改成 (1,2) 这个操作，先加行锁，在去更新，并不会提前判断相同就不修改了&lt;/p&gt;
&lt;p&gt;参考文章：https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;两段提交&lt;/h5&gt;
&lt;p&gt;当客户端执行 COMMIT 语句或者在自动提交的情况下，MySQL 内部开启一个 XA 事务，分两阶段来完成 XA 事务的提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;update T set c=c+1 where ID=2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-update的执行流程.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;流程说明：执行引擎将这行新数据读入到内存中（Buffer Pool）后，先将此次更新操作记录到 redo log buffer 里，然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态，执行器会生成这个操作的 binlog，并&lt;strong&gt;把 binlog 写入磁盘&lt;/strong&gt;，完成提交&lt;/p&gt;
&lt;p&gt;两阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prepare 阶段：存储引擎将该事务的 &lt;strong&gt;redo 日志刷盘&lt;/strong&gt;，并且将本事务的状态设置为 PREPARE，代表执行完成随时可以提交事务&lt;/li&gt;
&lt;li&gt;Commit 阶段：先将事务执行过程中产生的 binlog 刷新到硬盘，再执行存储引擎的提交工作，引擎把 redo log 改成提交状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储引擎层的 redo log 和 server 层的 binlog 可以认为是一个分布式事务， 都可以用于表示事务的提交状态，而&lt;strong&gt;两阶段提交就是让这两个状态保持逻辑上的一致&lt;/strong&gt;，也有利于主从复制，更好的保持主从数据的一致性&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;数据恢复&lt;/h5&gt;
&lt;p&gt;系统崩溃前没有提交的事务的 redo log 可能已经刷盘（定时线程或者 checkpoint），怎么处理崩溃恢复？&lt;/p&gt;
&lt;p&gt;工作流程：获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性，表示当前链表的事务属性，&lt;strong&gt;事务状态是活跃（未提交）的就全部回滚&lt;/strong&gt;，如果是 PREPARE 状态，就需要根据 binlog 的状态进行判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果在时刻 A 发生了崩溃（crash），由于此时 binlog 还没完成，所以需要进行回滚&lt;/li&gt;
&lt;li&gt;如果在时刻 B 发生了崩溃，redo log 和 binlog 有一个共&lt;strong&gt;同的数据字段叫 XID&lt;/strong&gt;，崩溃恢复的时候，会按顺序扫描 redo log：
&lt;ul&gt;
&lt;li&gt;如果 redo log 里面的事务是完整的，也就是已经有了 commit 标识，说明 binlog 也已经记录完整，直接从 redo log 恢复数据&lt;/li&gt;
&lt;li&gt;如果 redo log 里面的事务只有 prepare，就根据 XID 去 binlog 中判断对应的事务是否存在并完整，如果完整可以恢复数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断一个事务的 binlog 是否完整的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;statement 格式的 binlog，最后会有 COMMIT&lt;/li&gt;
&lt;li&gt;row 格式的 binlog，最后会有一个 XID event&lt;/li&gt;
&lt;li&gt;MySQL 5.6.2 版本以后，引入了 binlog-checksum 参数用来验证 binlog 内容的正确性（可能日志中间出错）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/73161&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;刷脏优化&lt;/h4&gt;
&lt;p&gt;系统在进行刷脏时会占用一部分系统资源，会影响系统的性能，&lt;strong&gt;产生系统抖动&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个查询要淘汰的脏页个数太多，会导致查询的响应时间明显变长&lt;/li&gt;
&lt;li&gt;日志写满，更新全部堵住，写性能跌为 0，这种情况对敏感业务来说，是不能接受的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;InnoDB 刷脏页的控制策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;innodb_io_capacity&lt;/code&gt; 参数代表磁盘的读写能力，建议设置成磁盘的 IOPS（每秒的 IO 次数）&lt;/li&gt;
&lt;li&gt;刷脏速度参考两个因素：脏页比例和 redo log 写盘速度
&lt;ul&gt;
&lt;li&gt;参数 &lt;code&gt;innodb_max_dirty_pages_pct&lt;/code&gt; 是脏页比例上限，默认值是 75%，InnoDB 会根据当前的脏页比例，算出一个范围在 0 到 100 之间的数字&lt;/li&gt;
&lt;li&gt;InnoDB 每次写入的日志都有一个序号，当前写入的序号跟 checkpoint 对应的序号之间的差值，InnoDB 根据差值算出一个范围在 0 到 100 之间的数字&lt;/li&gt;
&lt;li&gt;两者较大的值记为 R，执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innodb_flush_neighbors&lt;/code&gt; 参数置为 1 代表控制刷脏时检查相邻的数据页，如果也是脏页就一起刷脏，并检查邻居的邻居，这个行为会一直蔓延直到不是脏页，在 MySQL 8.0 中该值的默认值是 0，不建议开启此功能&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;一致特性&lt;/h3&gt;
&lt;p&gt;一致性是指事务执行前后，数据库的完整性约束没有被破坏，事务执行的前后都是合法的数据状态。&lt;/p&gt;
&lt;p&gt;数据库的完整性约束包括但不限于：实体完整性（如行的主键存在且唯一）、列完整性（如字段的类型、大小、长度要符合要求）、外键约束、用户自定义完整性（如转账前后，两个账户余额的和应该不变）&lt;/p&gt;
&lt;p&gt;实现一致性的措施：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证原子性、持久性和隔离性，如果这些特性无法保证，事务的一致性也无法保证&lt;/li&gt;
&lt;li&gt;数据库本身提供保障，例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等&lt;/li&gt;
&lt;li&gt;应用层面进行保障，例如如果转账操作只扣除转账者的余额，而没有增加接收者的余额，无论数据库实现的多么完美，也无法保证状态的一致&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;锁机制&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;锁机制：数据库为了保证数据的一致性，在共享的资源被并发访问时变得安全有序所设计的一种规则&lt;/p&gt;
&lt;p&gt;利用 MVCC 性质进行读取的操作叫&lt;strong&gt;一致性读&lt;/strong&gt;，读取数据前加锁的操作叫&lt;strong&gt;锁定读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;锁的分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按操作分类：
&lt;ul&gt;
&lt;li&gt;共享锁：也叫读锁。对同一份数据，多个事务读操作可以同时加锁而不互相影响 ，但不能修改数据&lt;/li&gt;
&lt;li&gt;排他锁：也叫写锁。当前的操作没有完成前，会阻断其他操作的读取和写入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按粒度分类：
&lt;ul&gt;
&lt;li&gt;表级锁：会锁定整个表，开销小，加锁快；不会出现死锁；锁定力度大，发生锁冲突概率高，并发度最低，偏向 MyISAM&lt;/li&gt;
&lt;li&gt;行级锁：会锁定当前操作行，开销大，加锁慢；会出现死锁；锁定力度小，发生锁冲突概率低，并发度高，偏向 InnoDB&lt;/li&gt;
&lt;li&gt;页级锁：锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间，会出现死锁，并发性能一般&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按使用方式分类：
&lt;ul&gt;
&lt;li&gt;悲观锁：每次查询数据时都认为别人会修改，很悲观，所以查询时加锁&lt;/li&gt;
&lt;li&gt;乐观锁：每次查询数据时都认为别人不会修改，很乐观，但是更新时会判断一下在此期间别人有没有去更新这个数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不同存储引擎支持的锁&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;存储引擎&lt;/th&gt;
&lt;th&gt;表级锁&lt;/th&gt;
&lt;th&gt;行级锁&lt;/th&gt;
&lt;th&gt;页级锁&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MyISAM&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InnoDB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEMORY&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BDB&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从锁的角度来说：表级锁更适合于以查询为主，只有少量按索引条件更新数据的应用，如 Web 应用；而行级锁则更适合于有大量按索引条件并发更新少量不同数据，同时又有并查询的应用，如一些在线事务处理系统&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;内存结构&lt;/h3&gt;
&lt;p&gt;对一条记录加锁的本质就是&lt;strong&gt;在内存中&lt;/strong&gt;创建一个锁结构与之关联，结构包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务信息：锁对应的事务信息，一个锁属于一个事务&lt;/li&gt;
&lt;li&gt;索引信息：对于行级锁，需要记录加锁的记录属于哪个索引&lt;/li&gt;
&lt;li&gt;表锁和行锁信息：表锁记录着锁定的表，行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特&lt;/li&gt;
&lt;li&gt;type_mode：一个 32 比特的数，被分成 lock_mode、lock_type、rec_lock_type 三个部分
&lt;ul&gt;
&lt;li&gt;lock_mode：锁模式，记录是共享锁、排他锁、意向锁之类&lt;/li&gt;
&lt;li&gt;lock_type：代表表级锁还是行级锁&lt;/li&gt;
&lt;li&gt;rec_lock_type：代表行锁的具体类型和 is_waiting 属性，is_waiting = true 时表示当前事务尚未获取到锁，处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false，释放锁时会检查是否与当前记录关联的锁结构，如果有就唤醒对应事务的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个事务可能操作多条记录，为了节省内存，满足下面条件的锁使用同一个锁结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在同一个事务中的加锁操作&lt;/li&gt;
&lt;li&gt;被加锁的记录在同一个页面中&lt;/li&gt;
&lt;li&gt;加锁的类型是一样的&lt;/li&gt;
&lt;li&gt;加锁的状态是一样的&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Server&lt;/h3&gt;
&lt;p&gt;MySQL 里面表级别的锁有两种：一种是表锁，一种是元数据锁（meta data lock，MDL)&lt;/p&gt;
&lt;p&gt;MDL 叫元数据锁，主要用来保护 MySQL 内部对象的元数据，保证数据读写的正确性，&lt;strong&gt;当对一个表做增删改查的时候，加 MDL 读锁；当要对表做结构变更操作 DDL 的时候，加 MDL 写锁&lt;/strong&gt;，两种锁不相互兼容，所以可以保证 DDL、DML、DQL 操作的安全&lt;/p&gt;
&lt;p&gt;说明：DDL 操作执行前会隐式提交当前会话的事务，因为 DDL 一般会在若干个特殊事务中完成，开启特殊事务前需要提交到其他事务&lt;/p&gt;
&lt;p&gt;MDL 锁的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MDL 锁不需要显式使用，在访问一个表的时候会被自动加上，在事务开始时申请，整个事务提交后释放（执行完单条语句不释放）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MDL 锁是在 Server 中实现，不是 InnoDB 存储引擎层能直接实现的锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MDL 锁还能实现其他粒度级别的锁，比如全局锁、库级别的锁、表空间级别的锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FLUSH TABLES WITH READ LOCK 简称（FTWRL），全局读锁，让整个库处于只读状态，DDL DML 都被阻塞，工作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上全局读锁（lock_global_read_lock）&lt;/li&gt;
&lt;li&gt;清理表缓存（close_cached_tables）&lt;/li&gt;
&lt;li&gt;上全局 COMMIT 锁（make_global_read_lock_block_commit）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;该命令主要用于备份工具做&lt;strong&gt;一致性备份&lt;/strong&gt;，由于 FTWRL 需要持有两把全局的 MDL 锁，并且还要关闭所有表对象，因此杀伤性很大&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;MyISAM&lt;/h3&gt;
&lt;h4&gt;表级锁&lt;/h4&gt;
&lt;p&gt;MyISAM 存储引擎只支持表锁，这也是 MySQL 开始几个版本中唯一支持的锁类型&lt;/p&gt;
&lt;p&gt;MyISAM 引擎在执行查询语句之前，会&lt;strong&gt;自动&lt;/strong&gt;给涉及到的所有表加读锁，在执行增删改之前，会&lt;strong&gt;自动&lt;/strong&gt;给涉及的表加写锁，这个过程并不需要用户干预，所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加锁命令：（对 InnoDB 存储引擎也适用）&lt;/p&gt;
&lt;p&gt;读锁：所有连接只能读取数据，不能修改&lt;/p&gt;
&lt;p&gt;写锁：其他连接不能查询和修改数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 读锁
LOCK TABLE table_name READ;

-- 写锁
LOCK TABLE table_name WRITE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解锁命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 将当前会话所有的表进行解锁
UNLOCK TABLES;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;锁的兼容性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 MyISAM 表的读操作，不会阻塞其他用户对同一表的读请求，但会阻塞对同一表的写请求&lt;/li&gt;
&lt;li&gt;对 MyISAM 表的写操作，则会阻塞其他用户对同一表的读和写操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁的兼容性.png)&lt;/p&gt;
&lt;p&gt;锁调度：&lt;strong&gt;MyISAM 的读写锁调度是写优先&lt;/strong&gt;，因为写锁后其他线程不能做任何操作，大量的更新会使查询很难得到锁，从而造成永远阻塞，所以 MyISAM 不适合做写为主的表的存储引擎&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;锁操作&lt;/h4&gt;
&lt;h5&gt;读锁&lt;/h5&gt;
&lt;p&gt;两个客户端操作 Client 1和 Client 2，简化为 C1、C2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_book` (
  `id` INT(11) AUTO_INCREMENT,
  `name` VARCHAR(50) DEFAULT NULL,
  `publish_time` DATE DEFAULT NULL,
  `status` CHAR(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MYISAM DEFAULT CHARSET=utf8 ;

INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,&apos;java编程思想&apos;,&apos;2088-08-01&apos;,&apos;1&apos;);
INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,&apos;mysql编程思想&apos;,&apos;2088-08-08&apos;,&apos;0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1、C2 加读锁，同时查询可以正常查询出数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLE tb_book READ;	-- C1、C2
SELECT * FROM tb_book;		-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁1.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1 加读锁，C1、C2 查询未锁定的表，C1 报错，C2 正常查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLE tb_book READ;	-- C1
SELECT * FROM tb_user;		-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁2.png)&lt;/p&gt;
&lt;p&gt;C1、C2 执行插入操作，C1 报错，C2 等待获取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO tb_book VALUES(NULL,&apos;Spring高级&apos;,&apos;2088-01-01&apos;,&apos;1&apos;);	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁3.png)&lt;/p&gt;
&lt;p&gt;当在 C1 中释放锁指令 UNLOCK TABLES，C2 中的 INSERT 语句立即执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;写锁&lt;/h5&gt;
&lt;p&gt;两个客户端操作 Client 1和 Client 2，简化为 C1、C2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C1 加写锁，C1、C2查询表，C1 正常查询，C2 需要等待&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLE tb_book WRITE;	-- C1
SELECT * FROM tb_book;		-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁1.png)&lt;/p&gt;
&lt;p&gt;当在 C1 中释放锁指令 UNLOCK TABLES，C2 中的 SELECT 语句立即执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1、C2 同时加写锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLE tb_book WRITE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁2.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1 加写锁，C1、C2查询未锁定的表，C1 报错，C2 正常查询&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁状态&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看锁竞争：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW OPEN TABLES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E9%94%81%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5%E6%9F%A5%E7%9C%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In_user：表当前被查询使用的次数，如果该数为零，则表是打开的，但是当前没有被使用&lt;/p&gt;
&lt;p&gt;Name_locked：表名称是否被锁定，名称锁定用于取消表或对表进行重命名等操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOCK TABLE tb_book READ;	-- 执行命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E9%94%81%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5%E6%9F%A5%E7%9C%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看锁状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;Table_locks%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁状态.png)&lt;/p&gt;
&lt;p&gt;Table_locks_immediate：指的是能立即获得表级锁的次数，每立即获取锁，值加 1&lt;/p&gt;
&lt;p&gt;Table_locks_waited：指的是不能立即获取表级锁而需要等待的次数，每等待一次，该值加 1，此值高说明存在着较为严重的表级锁争用情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;InnoDB&lt;/h3&gt;
&lt;h4&gt;行级锁&lt;/h4&gt;
&lt;h5&gt;记录锁&lt;/h5&gt;
&lt;p&gt;InnoDB 与 MyISAM 的最大不同有两点：一是支持事务；二是采用了行级锁，&lt;strong&gt;InnoDB 同时支持表锁和行锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;行级锁，也称为记录锁（Record Lock），InnoDB  实现了以下两种类型的行锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享锁 (S)：又称为读锁，简称 S 锁，多个事务对于同一数据可以共享一把锁，都能访问到数据，但是只能读不能修改&lt;/li&gt;
&lt;li&gt;排他锁 (X)：又称为写锁，简称 X 锁，不能与其他锁并存，获取排他锁的事务是可以对数据读取和修改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RR 隔离界别下，对于 UPDATE、DELETE 和 INSERT 语句，InnoDB 会&lt;strong&gt;自动给涉及数据集加排他锁&lt;/strong&gt;（行锁），在 commit 时自动释放；对于普通 SELECT 语句，不会加任何锁（只是针对 InnoDB 层来说的，因为在 Server 层会&lt;strong&gt;加 MDL 读锁&lt;/strong&gt;），通过 MVCC 防止并发冲突&lt;/p&gt;
&lt;p&gt;在事务中加的锁，并不是不需要了就释放，而是在事务中止或提交时自动释放，这个就是&lt;strong&gt;两阶段锁协议&lt;/strong&gt;。所以一般将更新共享资源（并发高）的 SQL 放到事务的最后执行，可以让其他线程尽量的减少等待时间&lt;/p&gt;
&lt;p&gt;锁的兼容性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享锁和共享锁     兼容&lt;/li&gt;
&lt;li&gt;共享锁和排他锁     冲突&lt;/li&gt;
&lt;li&gt;排他锁和排他锁     冲突&lt;/li&gt;
&lt;li&gt;排他锁和共享锁     冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显式给数据集加共享锁或排他锁：&lt;strong&gt;加锁读就是当前读，读取的是最新数据&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE	-- 共享锁
SELECT * FROM table_name WHERE ... FOR UPDATE			-- 排他锁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;strong&gt;锁默认会锁聚簇索引（锁就是加在索引上）&lt;/strong&gt;，但是当使用覆盖索引时，加共享锁只锁二级索引，不锁聚簇索引&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;锁操作&lt;/h5&gt;
&lt;p&gt;两个客户端操作 Client 1和 Client 2，简化为 C1、C2&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环境准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE test_innodb_lock(
	id INT(11),
	name VARCHAR(16),
	sex VARCHAR(1)
)ENGINE = INNODB DEFAULT CHARSET=utf8;

INSERT INTO test_innodb_lock VALUES(1,&apos;100&apos;,&apos;1&apos;);
-- ..........

CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id);
CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭自动提交功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET AUTOCOMMIT=0;	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常查询数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM test_innodb_lock;	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询 id 为 3 的数据，正常查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM test_innodb_lock WHERE id=3;	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作1.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1 更新 id 为 3 的数据，但不提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE test_innodb_lock SET name=&apos;300&apos; WHERE id=3;	-- C1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作2.png)&lt;/p&gt;
&lt;p&gt;C2 查询不到 C1 修改的数据，因为隔离界别为 REPEATABLE READ，C1 提交事务，C2 查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COMMIT;	-- C1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作3.png)&lt;/p&gt;
&lt;p&gt;提交后仍然查询不到 C1 修改的数据，因为隔离级别可以防止脏读、不可重复读，所以 C2 需要提交才可以查询到其他事务对数据的修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COMMIT;	-- C2
SELECT * FROM test_innodb_lock WHERE id=3;	-- C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作4.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1 更新 id 为 3 的数据，但不提交，C2 也更新 id 为 3 的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE test_innodb_lock SET name=&apos;3&apos; WHERE id=3;	-- C1
UPDATE test_innodb_lock SET name=&apos;30&apos; WHERE id=3;	-- C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作5.png)&lt;/p&gt;
&lt;p&gt;当 C1 提交，C2 直接解除阻塞，直接更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作不同行的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE test_innodb_lock SET name=&apos;10&apos; WHERE id=1;	-- C1
UPDATE test_innodb_lock SET name=&apos;30&apos; WHERE id=3;	-- C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作6.png)&lt;/p&gt;
&lt;p&gt;由于 C1、C2 操作的不同行，获取不同的行锁，所以都可以正常获取行锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁分类&lt;/h4&gt;
&lt;h5&gt;间隙锁&lt;/h5&gt;
&lt;p&gt;InnoDB 会对间隙（GAP）进行加锁，就是间隙锁 （RR 隔离级别下才有该锁）。间隙锁之间不存在冲突关系，&lt;strong&gt;多个事务可以同时对一个间隙加锁&lt;/strong&gt;，但是间隙锁会阻止往这个间隙中插入一个记录的操作&lt;/p&gt;
&lt;p&gt;InnoDB 加锁的基本单位是 next-key lock，该锁是行锁和 gap lock 的组合（X or S 锁），但是加锁过程是分为间隙锁和行锁两段执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以&lt;strong&gt;保护当前记录和前面的间隙&lt;/strong&gt;，遵循左开右闭原则，单纯的间隙锁是左开右开&lt;/li&gt;
&lt;li&gt;假设有 10、11、13，那么可能的间隙锁包括：(负无穷,10]、(10,11]、(11,13]、(13,正无穷)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;几种索引的加锁情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一索引加锁在值存在时是行锁，next-key lock 会退化为行锁，值不存在会变成间隙锁&lt;/li&gt;
&lt;li&gt;普通索引加锁会继续向右遍历到不满足条件的值为止，next-key lock 退化为间隙锁&lt;/li&gt;
&lt;li&gt;范围查询无论是否是唯一索引，都需要访问到不满足条件的第一个值为止&lt;/li&gt;
&lt;li&gt;对于联合索引且是唯一索引，如果 where 条件只包括联合索引的一部分，那么会加间隙锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;间隙锁优点：RR 级别下间隙锁可以&lt;strong&gt;解决事务的一部分的幻读问题&lt;/strong&gt;，通过对间隙加锁，可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁，只能加锁一部分的间隙&lt;/p&gt;
&lt;p&gt;间隙锁危害：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当锁定一个范围的键值后，即使某些不存在的键值也会被无辜的锁定，造成在锁定的时候无法插入锁定键值范围内的任何数据，在某些场景下这可能会对性能造成很大的危害，影响并发度&lt;/li&gt;
&lt;li&gt;事务 A B 同时锁住一个间隙后，A 往当前间隙插入数据时会被 B 的间隙锁阻塞，B 也执行插入间隙数据的操作时就会&lt;strong&gt;产生死锁&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现场演示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;关闭自动提交功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET AUTOCOMMIT=0;	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询数据表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM test_innodb_lock;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁1.png)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C1 根据 id 范围更新数据，C2 插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE test_innodb_lock SET name=&apos;8888&apos; WHERE id &amp;lt; 4;	-- C1
INSERT INTO test_innodb_lock VALUES(2,&apos;200&apos;,&apos;2&apos;);		-- C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁2.png)&lt;/p&gt;
&lt;p&gt;出现间隙锁，C2 被阻塞，等待 C1 提交事务后才能更新&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;意向锁&lt;/h5&gt;
&lt;p&gt;InnoDB 为了支持多粒度的加锁，允许行锁和表锁同时存在，支持在不同粒度上的加锁操作，InnoDB 增加了意向锁（Intention Lock）&lt;/p&gt;
&lt;p&gt;意向锁是将锁定的对象分为多个层次，意向锁意味着事务希望在更细粒度上进行加锁，意向锁分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意向共享锁（IS）：事务有意向对表加共享锁&lt;/li&gt;
&lt;li&gt;意向排他锁（IX）：事务有意向对表加排他锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;IX，IS 是表级锁&lt;/strong&gt;，不会和行级的 X，S 锁发生冲突，意向锁是在加表级锁之前添加，为了在加表级锁时可以快速判断表中是否有记录被上锁，比如向一个表添加表级 X 锁的时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有意向锁，则需要遍历整个表判断是否有锁定的记录&lt;/li&gt;
&lt;li&gt;有了意向锁，首先判断是否存在意向锁，然后判断该意向锁与即将添加的表级锁是否兼容即可，因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;兼容性如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%84%8F%E5%90%91%E9%94%81%E5%85%BC%E5%AE%B9%E6%80%A7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;插入意向锁&lt;/strong&gt; Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁，是行级锁&lt;/p&gt;
&lt;p&gt;插入意向锁释放了一种插入信号，即多个事务在相同的索引间隙插入时如果不是插入相同的间隙位置就不需要互相等待。假设某列有索引，只要两个事务插入位置不同，如事务 A 插入 3，事务 B 插入 4，那么就可以同时插入&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;自增锁&lt;/h5&gt;
&lt;p&gt;系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值，实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AUTO_INC 锁：表级锁，执行插入语句时会自动添加，在该语句执行完成后释放，并不是事务结束&lt;/li&gt;
&lt;li&gt;轻量级锁：为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁，生成以后释放掉，不需要等到插入语句执行完后释放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统变量 &lt;code&gt;innodb_autoinc_lock_mode&lt;/code&gt; 控制采取哪种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0：全部采用 AUTO_INC 锁&lt;/li&gt;
&lt;li&gt;1：全部采用轻量级锁&lt;/li&gt;
&lt;li&gt;2：混合使用，在插入记录的数量确定时采用轻量级锁，不确定时采用 AUTO_INC 锁&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;隐式锁&lt;/h5&gt;
&lt;p&gt;一般情况下 INSERT 语句是不需要在内存中生成锁结构的，会进行隐式的加锁，保护的是插入后的安全&lt;/p&gt;
&lt;p&gt;注意：如果插入的间隙被其他事务加了间隙锁，此次插入会被阻塞，并在该间隙插入一个插入意向锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚簇索引：索引记录有 trx_id 隐藏列，表示最后改动该记录的事务 id，插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的，如果不是就可以正常加锁；如果是就创建一个 X 的锁结构，该锁的 is_waiting 是 false，为自己的事务创建一个锁结构，is_waiting 是 true（类似 Java 中的锁升级）&lt;/li&gt;
&lt;li&gt;二级索引：获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性，代表修改当前页面的最大的事务 ID，如果小于当前活跃的最小事务 id，就证明插入该数据的事务已经提交，否则就需要获取到主键值进行回表操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;隐式锁起到了延迟生成锁的效果，如果其他事务与隐式锁没有冲突，就可以避免锁结构的生成，节省了内存资源&lt;/p&gt;
&lt;p&gt;INSERT 在两种情况下会生成锁结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重复键：在插入主键或唯一二级索引时遇到重复的键值会报错，在报错前需要对对应的聚簇索引进行加锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隔离级别 &amp;lt;= Read Uncommitted，加 S 型 Record Lock&lt;/li&gt;
&lt;li&gt;隔离级别 &amp;gt;= Repeatable Read，加 S 型 next_key 锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;外键检查：如果待插入的记录在父表中可以找到，会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隔离级别 &amp;lt;= Read Committed，不加锁&lt;/li&gt;
&lt;li&gt;隔离级别 &amp;gt;= Repeatable Read，加间隙锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁优化&lt;/h4&gt;
&lt;h5&gt;优化锁&lt;/h5&gt;
&lt;p&gt;InnoDB 存储引擎实现了行级锁定，虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高，但是在整体并发处理能力方面要远远优于 MyISAM 的表锁，当系统并发量较高的时候，InnoDB 的整体性能远远好于 MyISAM&lt;/p&gt;
&lt;p&gt;但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高，甚至可能会更差&lt;/p&gt;
&lt;p&gt;优化建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽可能让所有数据检索都能通过索引来完成，避免无索引行锁升级为表锁&lt;/li&gt;
&lt;li&gt;合理设计索引，尽量缩小锁的范围&lt;/li&gt;
&lt;li&gt;尽可能减少索引条件及索引范围，避免间隙锁&lt;/li&gt;
&lt;li&gt;尽量控制事务大小，减少锁定资源量和时间长度&lt;/li&gt;
&lt;li&gt;尽可使用低级别事务隔离（需要业务层面满足需求）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;锁升级&lt;/h5&gt;
&lt;p&gt;索引失效造成&lt;strong&gt;行锁升级为表锁&lt;/strong&gt;，不通过索引检索数据，全局扫描的过程中 InnoDB 会将对表中的所有记录加锁，实际效果和&lt;strong&gt;表锁&lt;/strong&gt;一样，实际开发过程应避免出现索引失效的状况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看当前表的索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW INDEX FROM test_innodb_lock;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭自动提交功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET AUTOCOMMIT=0;	-- C1、C2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行更新语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE test_innodb_lock SET sex=&apos;2&apos; WHERE name=10;	-- C1
UPDATE test_innodb_lock SET sex=&apos;2&apos; WHERE id=3;		-- C2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁升级.png)&lt;/p&gt;
&lt;p&gt;索引失效：执行更新时 name 字段为 varchar 类型，造成索引失效，最终行锁变为表锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;死锁&lt;/h5&gt;
&lt;p&gt;不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁&lt;/p&gt;
&lt;p&gt;死锁情况：线程 A 修改了 id = 1 的数据，请求修改 id = 2 的数据，线程 B 修改了 id = 2 的数据，请求修改 id = 1 的数据，产生死锁&lt;/p&gt;
&lt;p&gt;解决策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接进入等待直到超时，超时时间可以通过参数 innodb_lock_wait_timeout 来设置，默认 50 秒，但是时间的设置不好控制，超时可能不是因为死锁，而是因为事务处理比较慢，所以一般不采取该方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主动死锁检测，发现死锁后&lt;strong&gt;主动回滚死锁链条中较小的一个事务&lt;/strong&gt;，让其他事务得以继续执行，将参数 &lt;code&gt;innodb_deadlock_detect&lt;/code&gt; 设置为 on，表示开启该功能（事务较小的意思就是事务执行过程中插入、删除、更新的记录条数）&lt;/p&gt;
&lt;p&gt;死锁检测并不是每个语句都要检测，只有在加锁访问的行上已经有锁时，当前事务被阻塞了才会检测，也是从当前事务开始进行检测&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过执行 &lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt; 可以查看最近发生的一次死循环，全局系统变量 &lt;code&gt;innodb_print_all_deadlocks&lt;/code&gt; 设置为 on，就可以将每个死锁信息都记录在 MySQL 错误日志中&lt;/p&gt;
&lt;p&gt;死锁一般是行级锁，当表锁发生死锁时，会在事务中访问其他表时&lt;strong&gt;直接报错&lt;/strong&gt;，破坏了持有并等待的死锁条件&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;锁状态&lt;/h4&gt;
&lt;p&gt;查看锁信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW STATUS LIKE &apos;innodb_row_lock%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁争用.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Innodb_row_lock_current_waits：当前正在等待锁定的数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Innodb_row_lock_time：从系统启动到现在锁定总时间长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Innodb_row_lock_time_avg：每次等待所花平均时长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Innodb_row_lock_time_max：从系统启动到现在等待最长的一次所花的时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Innodb_row_lock_waits：系统启动后到现在总共等待的次数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当等待的次数很高，而且每次等待的时长也不短的时候，就需要分析系统中为什么会有如此多的等待，然后根据分析结果制定优化计划&lt;/p&gt;
&lt;p&gt;查看锁状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.innodb_locks;	#锁的概况
SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态，其中包括锁的情况
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB%E6%9F%A5%E7%9C%8B%E9%94%81%E7%8A%B6%E6%80%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;lock_id 是锁 id；lock_trx_id 为事务 id；lock_mode 为 X 代表排它锁（写锁）；lock_type 为 RECORD 代表锁为行锁（记录锁）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;乐观锁&lt;/h3&gt;
&lt;p&gt;悲观锁：在整个数据处理过程中，将数据处于锁定状态，为了保证事务的隔离性，就需要一致性锁定读。读取数据时给加锁，其它事务无法修改这些数据，修改删除数据时也加锁，其它事务同样无法读取这些数据&lt;/p&gt;
&lt;p&gt;悲观锁和乐观锁使用前提：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于读的操作远多于写的操作的时候，一个更新操作加锁会阻塞所有的读取操作，降低了吞吐量，最后需要释放锁，锁是需要一些开销的，这时候可以选择乐观锁&lt;/li&gt;
&lt;li&gt;如果是读写比例差距不是非常大或者系统没有响应不及时，吞吐量瓶颈的问题，那就不要去使用乐观锁，它增加了复杂度，也带来了业务额外的风险，这时候可以选择悲观锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;乐观锁的实现方式：就是 CAS，比较并交换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;版本号&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;给数据表中添加一个 version 列，每次更新后都将这个列的值加 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读取数据时，将版本号读取出来，在执行更新的时候，比较版本号&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果相同则执行更新，如果不相同，说明此条数据已经发生了变化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户自行根据这个通知来决定怎么处理，比如重新开始一遍，或者放弃本次更新&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建city表
CREATE TABLE city(
	id INT PRIMARY KEY AUTO_INCREMENT,  -- 城市id
	NAME VARCHAR(20),                   -- 城市名称
	VERSION INT                         -- 版本号
);

-- 添加数据
INSERT INTO city VALUES (NULL,&apos;北京&apos;,1),(NULL,&apos;上海&apos;,1),(NULL,&apos;广州&apos;,1),(NULL,&apos;深圳&apos;,1);

-- 修改北京为北京市
-- 1.查询北京的version
SELECT VERSION FROM city WHERE NAME=&apos;北京&apos;;
-- 2.修改北京为北京市，版本号+1。并对比版本号
UPDATE city SET NAME=&apos;北京市&apos;,VERSION=VERSION+1 WHERE NAME=&apos;北京&apos; AND VERSION=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间戳&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和版本号方式基本一样，给数据表中添加一个列，名称无所谓，数据类型需要是 &lt;strong&gt;timestamp&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每次更新后都将最新时间插入到此列&lt;/li&gt;
&lt;li&gt;读取数据时，将时间读取出来，在执行更新的时候，比较时间&lt;/li&gt;
&lt;li&gt;如果相同则执行更新，如果不相同，说明此条数据已经发生了变化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;乐观锁的异常情况：如果 version 被其他事务抢先更新，则在当前事务中更新失败，trx_id 没有变成当前事务的 ID，当前事务再次查询还是旧值，就会出现&lt;strong&gt;值没变但是更新不了&lt;/strong&gt;的现象（anomaly）&lt;/p&gt;
&lt;p&gt;解决方案：每次 CAS 更新不管成功失败，就结束当前事务；如果失败则重新起一个事务进行查询更新&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;主从&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中，然后在从库上对这些日志重新执行（也叫重做），从而使得从库和主库的数据保持同步&lt;/p&gt;
&lt;p&gt;MySQL 支持一台主库同时向多台从库进行复制，从库同时也可以作为其他从服务器的主库，实现链状复制&lt;/p&gt;
&lt;p&gt;MySQL 复制的优点主要包含以下三个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主库出现问题，可以快速切换到从库提供服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在从库上执行查询操作，从主库中更新，实现读写分离&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在从库中执行备份，以避免备份期间影响主库的服务（备份时会加全局读锁）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;主从复制&lt;/h3&gt;
&lt;h4&gt;主从结构&lt;/h4&gt;
&lt;p&gt;MySQL 的主从之间维持了一个&lt;strong&gt;长连接&lt;/strong&gt;。主库内部有一个线程，专门用于服务从库的长连接，连接过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从库执行 change master 命令，设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog，这个位置包含文件名和日志偏移量&lt;/li&gt;
&lt;li&gt;从库执行 start slave 命令，这时从库会启动两个线程，就是图中的 io_thread 和 sql_thread，其中 io_thread 负责与主库建立连接&lt;/li&gt;
&lt;li&gt;主库校验完用户名、密码后，开始按照从传过来的位置，从本地读取 binlog 发给从库，开始主从复制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主从复制原理图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86%E5%9B%BE.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主从复制主要依赖的是 binlog，MySQL 默认是异步复制，需要三个线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;binlog thread：在主库事务提交时，把数据变更记录在日志文件 binlog 中，并通知 slave 有数据更新&lt;/li&gt;
&lt;li&gt;I/O thread：负责从主服务器上&lt;strong&gt;拉取二进制日志&lt;/strong&gt;，并将 binlog 日志内容依次写到 relay log 中转日志的最末端，并将新的 binlog 文件名和 offset 记录到 master-info 文件中，以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容&lt;/li&gt;
&lt;li&gt;SQL thread：监测本地 relay log 中新增了日志内容，读取中继日志并重做其中的 SQL 语句，从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步与异步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步复制有数据丢失风险，例如数据还未同步到从库，主库就给客户端响应，然后主库挂了，此时从库晋升为主库的话数据是缺失的&lt;/li&gt;
&lt;li&gt;同步复制，主库需要将 binlog 复制到所有从库，等所有从库响应了之后主库才进行其他逻辑，这样的话性能很差，一般不会选择&lt;/li&gt;
&lt;li&gt;MySQL 5.7 之后出现了半同步复制，有参数可以选择成功同步几个从库就返回响应&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;主主结构&lt;/h4&gt;
&lt;p&gt;主主结构就是两个数据库之间总是互为主从关系，这样在切换的时候就不用再修改主从关系&lt;/p&gt;
&lt;p&gt;循环复制：在库 A 上更新了一条语句，然后把生成的 binlog 发给库 B，库 B 执行完这条更新语句后也会生成 binlog，会再发给 A&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个库的 server id 必须不同，如果相同则它们之间不能设定为主主关系&lt;/li&gt;
&lt;li&gt;一个库接到 binlog 并在重放的过程中，生成与原 binlog 的 server id 相同的新的 binlog&lt;/li&gt;
&lt;li&gt;每个库在收到从主库发过来的日志后，先判断 server id，如果跟自己的相同，表示这个日志是自己生成的，就直接丢弃这个日志&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;主从延迟&lt;/h3&gt;
&lt;h4&gt;延迟原因&lt;/h4&gt;
&lt;p&gt;正常情况主库执行更新生成的所有 binlog，都可以传到从库并被正确地执行，从库就能达到跟主库一致的状态，这就是最终一致性&lt;/p&gt;
&lt;p&gt;主从延迟是主从之间是存在一定时间的数据不一致，就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值，即 T2-T1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主库 A 执行完成一个事务，写入 binlog，该时刻记为 T1&lt;/li&gt;
&lt;li&gt;日志传给从库 B，从库 B 执行完这个事务，该时刻记为 T2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过在从库执行 &lt;code&gt;show slave status&lt;/code&gt; 命令，返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一个事务的 binlog 都有一个时间字段，用于记录主库上写入的时间&lt;/li&gt;
&lt;li&gt;从库取出当前正在执行的事务的时间字段，跟系统的时间进行相减，得到的就是 seconds_behind_master&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主从延迟的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从库的机器性能比主库的差，导致从库的复制能力弱&lt;/li&gt;
&lt;li&gt;从库的查询压力大，建立一主多从的结构&lt;/li&gt;
&lt;li&gt;大事务的执行，主库必须要等到事务完成之后才会写入 binlog，导致从节点出现应用 binlog 延迟&lt;/li&gt;
&lt;li&gt;主库的 DDL，从库与主库的 DDL 同步是串行进行，DDL 在主库执行时间很长，那么从库也会消耗同样的时间&lt;/li&gt;
&lt;li&gt;锁冲突问题也可能导致从节点的 SQL 线程执行慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主从同步问题永远都是&lt;strong&gt;一致性和性能的权衡&lt;/strong&gt;，需要根据实际的应用场景，可以采取下面的办法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优化 SQL，避免慢 SQL，减少批量操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;降低多线程大事务并发的概率，优化业务逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务中大多数情况查询操作要比更新操作更多，搭建&lt;strong&gt;一主多从&lt;/strong&gt;结构，让这些从库来分担读的压力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量采用短的链路，主库和从库服务器的距离尽量要短，提升端口带宽，减少 binlog 传输的网络延时&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实时性要求高的业务读强制走主库，从库只做备份&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;并行复制&lt;/h4&gt;
&lt;h5&gt;MySQL5.6&lt;/h5&gt;
&lt;p&gt;高并发情况下，主库的会产生大量的 binlog，在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行，会导致主库延迟变大。为了改善复制延迟问题，MySQL 5.6 版本增加了并行复制功能，以采用多线程机制来促进执行&lt;/p&gt;
&lt;p&gt;coordinator 就是原来的 SQL Thread，并行复制中它不再直接更新数据，&lt;strong&gt;只负责读取中转日志和分发事务&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程分配完成并不是立即执行，为了防止造成更新覆盖，更新同一 DB 的两个事务必须被分发到同一个工作线程&lt;/li&gt;
&lt;li&gt;同一个事务不能被拆开，必须放到同一个工作线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 5.6 版本的策略：每个线程对应一个 hash 表，用于保存当前这个线程的执行队列里的事务所涉及的表，hash 表的 key 是数据库名，value 是一个数字，表示队列中有多少个事务修改这个库，适用于主库上有多个 DB 的情况&lt;/p&gt;
&lt;p&gt;每个事务在分发的时候，跟线程的&lt;strong&gt;冲突&lt;/strong&gt;（事务操作的是同一个库）关系包括以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果跟所有线程都不冲突，coordinator 线程就会把这个事务分配给最空闲的线程&lt;/li&gt;
&lt;li&gt;如果只跟一个线程冲突，coordinator 线程就会把这个事务分配给这个存在冲突关系的线程&lt;/li&gt;
&lt;li&gt;如果跟多于一个线程冲突，coordinator 线程就进入等待状态，直到和这个事务存在冲突关系的线程只剩下 1 个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造 hash 值的时候很快，只需要库名，而且一个实例上 DB 数也不会很多，不会出现需要构造很多项的情况&lt;/li&gt;
&lt;li&gt;不要求 binlog 的格式，statement 格式的 binlog 也可以很容易拿到库名（日志章节详解了 binlog）&lt;/li&gt;
&lt;li&gt;主库上的表都放在同一个 DB 里面，这个策略就没有效果了；或者不同 DB 的热点不同，比如一个是业务逻辑库，一个是系统配置库，那也起不到并行的效果，需要&lt;strong&gt;把相同热度的表均匀分到这些不同的 DB 中&lt;/strong&gt;，才可以使用这个策略&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;MySQL5.7&lt;/h5&gt;
&lt;p&gt;MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置为 DATABASE，表示使用 MySQL 5.6 版本的&lt;strong&gt;按库（DB）并行策略&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;配置为 LOGICAL_CLOCK，表示的&lt;strong&gt;按提交状态并行&lt;/strong&gt;执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按提交状态并行复制策略的思想是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有处于 commit 状态的事务可以并行执行；同时处于 prepare 状态的事务，在从库执行时是可以并行的&lt;/li&gt;
&lt;li&gt;处于 prepare 状态的事务，与处于 commit 状态的事务之间，在从库执行时也是可以并行的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 5.7.22 版本里，MySQL 增加了一个新的并行复制策略，基于 WRITESET 的并行复制，新增了一个参数 binlog-transaction-dependency-tracking，用来控制是否启用这个新策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;COMMIT_ORDER：表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WRITESET：表示的是对于每个事务涉及更新的每一行，计算出这一行的 hash 值，组成该事务的 writeset 集合，如果两个事务没有操作相同的行，也就是说它们的 writeset 没有交集，就可以并行（&lt;strong&gt;按行并行&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;为了唯一标识，这个 hash 表的值是通过 &lt;code&gt;库名 + 表名 + 索引名 + 值&lt;/code&gt;（表示的是某一行）计算出来的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WRITESET_SESSION：是在 WRITESET 的基础上多了一个约束，即在主库上同一个线程先后执行的两个事务，在备库执行的时候，要保证相同的先后顺序&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 5.7.22 按行并发的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;writeset 是在主库生成后直接写入到 binlog 里面的，这样在备库执行的时候，不需要解析 binlog 内容，节省了计算量&lt;/li&gt;
&lt;li&gt;不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程，更省内存&lt;/li&gt;
&lt;li&gt;从库的分发策略不依赖于 binlog 内容，所以 binlog 是 statement 格式也可以，更节约内存（因为 row 才记录更改的行）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL 5.7.22 的并行复制策略在通用性上是有保证的，但是对于表上没主键、唯一和外键约束的场景，WRITESET 策略也是没法并行的，也会暂时退化为单线程模型&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/77083&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;读写分离&lt;/h3&gt;
&lt;h4&gt;读写延迟&lt;/h4&gt;
&lt;p&gt;读写分离：可以降低主库的访问压力，提高系统的并发能力&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主库不建查询的索引，从库建查询的索引。因为索引需要维护的，比如插入一条数据，不仅要在聚簇索引上面插入，对应的二级索引也得插入&lt;/li&gt;
&lt;li&gt;将读操作分到从库了之后，可以在主库把查询要用的索引删了，减少写操作对主库的影响&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读写分离产生了读写延迟，造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询，如果查询选择的是从库的话，可能读到的还是以前的数据，叫过期读&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;强制将写之后&lt;strong&gt;立刻读的操作转移到主库&lt;/strong&gt;，比如刚注册的用户，直接登录从库查询可能查询不到，先走主库登录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二次查询&lt;/strong&gt;，如果从库查不到数据，则再去主库查一遍，由 API 封装，比较简单，但导致主库压力大&lt;/li&gt;
&lt;li&gt;更新主库后，读从库之前先 sleep 一下，类似于执行一条 &lt;code&gt;select sleep(1)&lt;/code&gt; 命令，大多数情况下主备延迟在 1 秒之内&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;确保机制&lt;/h4&gt;
&lt;h5&gt;无延迟&lt;/h5&gt;
&lt;p&gt;确保主备无延迟的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次从库执行查询请求前，先判断 seconds_behind_master 是否已经等于 0，如果不等于那就等到参数变为 0 执行查询请求&lt;/li&gt;
&lt;li&gt;对比位点，Master_Log_File 和 Read_Master_Log_Pos 表示的是读到的主库的最新位点，Relay_Master_Log_File 和 Exec_Master_Log_Pos 表示的是备库执行的最新位点，这两组值完全相同就说明接收到的日志已经同步完成&lt;/li&gt;
&lt;li&gt;对比 GTID 集合，Retrieved_Gtid_Set 是备库收到的所有日志的 GTID 集合，Executed_Gtid_Set 是备库所有已经执行完成的 GTID 集合，如果这两个集合相同也表示备库接收到的日志都已经同步完成&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;半同步&lt;/h5&gt;
&lt;p&gt;半同步复制就是 semi-sync replication，适用于一主一备的场景，工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务提交的时候，主库把 binlog 发给从库&lt;/li&gt;
&lt;li&gt;从库收到 binlog 以后，发回给主库一个 ack，表示收到了&lt;/li&gt;
&lt;li&gt;主库收到这个 ack 以后，才能给客户端返回事务完成的确认&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一主多从场景中，主库只要等到一个从库的 ack，就开始给客户端返回确认，这时在从库上执行查询请求，有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果查询是落在这个响应了 ack 的从库上，是能够确保读到最新数据&lt;/li&gt;
&lt;li&gt;如果查询落到其他从库上，它们可能还没有收到最新的日志，就会产生过期读的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在业务更新的高峰期，主库的位点或者 GTID 集合更新很快，导致从库来不及处理，那么两个位点等值判断就会一直不成立，很可能出现从库上迟迟无法响应查询请求的情况&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;等位点&lt;/h5&gt;
&lt;p&gt;在&lt;strong&gt;从库执行判断位点&lt;/strong&gt;的命令，参数 file 和 pos 指的是主库上的文件名和位置，timeout 可选，设置为正整数 N 表示最多等待 N 秒&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT master_pos_wait(file, pos[, timeout]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令正常返回的结果是一个正整数 M，表示从命令开始执行，到应用完 file 和 pos 表示的 binlog 位置，执行了多少事务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果执行期间，备库同步线程发生异常，则返回 NULL&lt;/li&gt;
&lt;li&gt;如果等待超过 N 秒，就返回 -1&lt;/li&gt;
&lt;li&gt;如果刚开始执行的时候，就发现已经执行过这个位置了，则返回 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作流程：先执行 trx1，再执行一个查询请求的逻辑，要&lt;strong&gt;保证能够查到正确的数据&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trx1 事务更新完成后，马上执行 &lt;code&gt;show master status&lt;/code&gt; 得到当前主库执行到的 File 和 Position&lt;/li&gt;
&lt;li&gt;选定一个从库执行判断位点语句，如果返回值是 &amp;gt;=0 的正整数，说明从库已经同步完事务，可以在这个从库执行查询语句&lt;/li&gt;
&lt;li&gt;如果出现其他情况，需要到主库执行查询语句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：如果所有的从库都延迟超过 timeout 秒，查询压力就都跑到主库上，所以需要进行权衡&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;等GTID&lt;/h5&gt;
&lt;p&gt;数据库开启了 GTID 模式，MySQL 提供了判断 GTID 的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT wait_for_executed_gtid_set(gtid_set [, timeout])
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;等待直到这个库执行的事务中包含传入的 gtid_set，返回 0&lt;/li&gt;
&lt;li&gt;超时返回 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作流程：先执行 trx1，再执行一个查询请求的逻辑，要保证能够查到正确的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trx1 事务更新完成后，从返回包直接获取这个事务的 GTID，记为 gtid&lt;/li&gt;
&lt;li&gt;选定一个从库执行查询语句，如果返回值是 0，则在这个从库执行查询语句，否则到主库执行查询语句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比等待位点方法，减少了一次 &lt;code&gt;show master status&lt;/code&gt; 的方法，将参数 session_track_gtids 设置为 OWN_GTID，然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可&lt;/p&gt;
&lt;p&gt;总结：所有的等待无延迟的方法，都需要根据具体的业务场景去判断实施&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/77636&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;负载均衡&lt;/h4&gt;
&lt;p&gt;负载均衡是应用中使用非常普遍的一种优化方法，机制就是利用某种均衡算法，将固定的负载量分布到不同的服务器上，以此来降低单台服务器的负载，达到优化的效果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分流查询：通过 MySQL 的主从复制，实现读写分离，使增删改操作走主节点，查询操作走从节点，从而可以降低单台服务器的读写压力&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分布式数据库架构：适合大数据量、负载高的情况，具有良好的拓展性和高可用性。通过在多台服务器之间分布数据，可以实现在多台服务器之间的负载均衡，提高访问效率&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;主从搭建&lt;/h3&gt;
&lt;h4&gt;master&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在master 的配置文件（/etc/mysql/my.cnf）中，配置如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#mysql 服务ID,保证整个集群环境中唯一
server-id=1

#mysql binlog 日志的存储路径和文件名
log-bin=/var/lib/mysql/mysqlbin

#错误日志,默认已经开启
#log-err

#mysql的安装目录
#basedir

#mysql的临时目录
#tmpdir

#mysql的数据存放目录
#datadir

#是否只读,1 代表只读, 0 代表读写
read-only=0

#忽略的数据, 指不需要同步的数据库
binlog-ignore-db=mysql

#指定同步的数据库
#binlog-do-db=db01
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行完毕之后，需要重启 MySQL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建同步数据的账户，并且进行授权操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT REPLICATION SLAVE ON *.* TO &apos;seazean&apos;@&apos;192.168.0.137&apos; IDENTIFIED BY &apos;123456&apos;;
FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看 master 状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW MASTER STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%9F%A5%E7%9C%8Bmaster%E7%8A%B6%E6%80%81.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File：从哪个日志文件开始推送日志文件&lt;/li&gt;
&lt;li&gt;Position：从哪个位置开始推送日志&lt;/li&gt;
&lt;li&gt;Binlog_Ignore_DB：指定不需要同步的数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;slave&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 slave 端配置文件中，配置如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#mysql服务端ID,唯一
server-id=2

#指定binlog日志
log-bin=/var/lib/mysql/mysqlbin
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行完毕之后，需要重启 MySQL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定当前从库对应的主库的IP地址、用户名、密码，从哪个日志文件开始的那个位置开始同步推送日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CHANGE MASTER TO MASTER_HOST= &apos;192.168.0.138&apos;, MASTER_USER=&apos;seazean&apos;, MASTER_PASSWORD=&apos;seazean&apos;, MASTER_LOG_FILE=&apos;mysqlbin.000001&apos;, MASTER_LOG_POS=413;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开启同步操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START SLAVE;
SHOW SLAVE STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止同步操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;STOP SLAVE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;验证&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在主库中创建数据库，创建表并插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE db01;
USE db01;
CREATE TABLE user(
	id INT(11) NOT NULL AUTO_INCREMENT,
	name VARCHAR(50) NOT NULL,
	sex VARCHAR(1),
	PRIMARY KEY (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO user(id,NAME,sex) VALUES(NULL,&apos;Tom&apos;,&apos;1&apos;);
INSERT INTO user(id,NAME,sex) VALUES(NULL,&apos;Trigger&apos;,&apos;0&apos;);
INSERT INTO user(id,NAME,sex) VALUES(NULL,&apos;Dawn&apos;,&apos;1&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在从库中查询数据，进行验证：&lt;/p&gt;
&lt;p&gt;在从库中，可以查看到刚才创建的数据库：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%AA%8C%E8%AF%811.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在该数据库中，查询表中的数据：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%AA%8C%E8%AF%812.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;主从切换&lt;/h3&gt;
&lt;h4&gt;正常切换&lt;/h4&gt;
&lt;p&gt;正常切换步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在开始切换之前先对主库进行锁表 &lt;code&gt;flush tables with read lock&lt;/code&gt;，然后等待所有语句执行完成，切换完成后可以释放锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查 slave 同步状态，在 slave 执行 &lt;code&gt;show processlist&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止 slave io 线程，执行命令 &lt;code&gt;STOP SLAVE IO_THREAD&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提升 slave 为 master&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stop slave;
Reset master;
Reset slave all;
set global read_only=off;	-- 设置为可更新状态
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将原来 master 变为 slave（参考搭建流程中的 slave 方法）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;可靠性优先策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判断备库 B 现在的 seconds_behind_master，如果小于某个值（比如 5 秒）继续下一步，否则持续重试这一步&lt;/li&gt;
&lt;li&gt;把主库 A 改成只读状态，即把 readonly 设置为 true&lt;/li&gt;
&lt;li&gt;判断备库 B 的 seconds_behind_master 的值，直到这个值变成 0 为止（该步骤比较耗时，所以步骤 1 中要尽量等待该值变小）&lt;/li&gt;
&lt;li&gt;把备库 B 改成可读写状态，也就是把 readonly 设置为 false&lt;/li&gt;
&lt;li&gt;把业务请求切到备库 B&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可用性优先策略：先做最后两步，会造成主备数据不一致的问题&lt;/p&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/76795&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;健康检测&lt;/h4&gt;
&lt;p&gt;主库发生故障后从库会上位，&lt;strong&gt;其他从库指向新的主库&lt;/strong&gt;，所以需要一个健康检测的机制来判断主库是否宕机&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;select 1 判断，但是高并发下检测不出线程的锁等待的阻塞问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查表判断，在系统库（mysql 库）里创建一个表，比如命名为 health_check，里面只放一行数据，然后定期执行。但是当 binlog 所在磁盘的空间占用率达到 100%，所有的更新和事务提交语句都被阻塞，查询语句可以继续运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新判断，在健康检测表中放一个 timestamp 字段，用来表示最后一次执行检测的时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE mysql.health_check SET t_modified=now();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;节点可用性的检测都应该包含主库和备库，为了让主备之间的更新不产生冲突，可以在 mysql.health_check 表上存入多行数据，并用主备的 server_id 做主键，保证主、备库各自的检测命令不会发生冲突&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基于位点&lt;/h4&gt;
&lt;p&gt;主库上位后，从库 B 执行 CHANGE MASTER TO 命令，指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库 A 的哪个文件的哪个位点开始同步，这个位置就是&lt;strong&gt;同步位点&lt;/strong&gt;，对应主库的文件名和日志偏移量&lt;/p&gt;
&lt;p&gt;寻找位点需要找一个稍微往前的，然后再通过判断跳过那些在从库 B 上已经执行过的事务，获取位点方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等待新主库 A 把中转日志（relay log）全部同步完成&lt;/li&gt;
&lt;li&gt;在 A 上执行 show master status 命令，得到当前 A 上最新的 File 和 Position&lt;/li&gt;
&lt;li&gt;取原主库故障的时刻 T，用 mysqlbinlog 工具解析新主库 A 的 File，得到 T 时刻的位点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常情况下该值并不准确，在切换的过程中会发生错误，所以要先主动跳过这些错误：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;切换过程中，可能会重复执行一个事务，所以需要主动跳过所有重复的事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL sql_slave_skip_counter=1;
START SLAVE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置 slave_skip_errors 参数，直接设置跳过指定的错误，保证主从切换的正常进行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1062 错误是插入数据时唯一键冲突&lt;/li&gt;
&lt;li&gt;1032 错误是删除数据时找不到行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该方法针对的是主备切换时，由于找不到精确的同步位点，只能采用这种方法来创建从库和新主库的主备关系。等到主备间的同步关系建立完成并稳定执行一段时间后，还需要把这个参数设置为空，以免真的出现了主从数据不一致也跳过了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基于GTID&lt;/h4&gt;
&lt;h5&gt;GTID&lt;/h5&gt;
&lt;p&gt;GTID 的全称是 Global Transaction Identifier，全局事务 ID，是一个事务&lt;strong&gt;在提交时生成&lt;/strong&gt;的，是这个事务的唯一标识，组成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GTID=source_id:transaction_id
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;source_id：是一个实例第一次启动时自动生成的，是一个全局唯一的值&lt;/li&gt;
&lt;li&gt;transaction_id：初始值是 1，每次提交事务的时候分配给这个事务，并加 1，是连续的（区分事务 ID，事务 ID 是在执行时生成）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动 MySQL 实例时，加上参数 &lt;code&gt;gtid_mode=on&lt;/code&gt; 和 &lt;code&gt;enforce_gtid_consistency=on&lt;/code&gt; 就可以启动 GTID 模式，每个事务都会和一个 GTID 一一对应，每个 MySQL 实例都维护了一个 GTID 集合，用来存储当前实例&lt;strong&gt;执行过的所有事务&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GTID 有两种生成方式，使用哪种方式取决于 session 变量 gtid_next：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gtid_next=automatic&lt;/code&gt;：使用默认值，把 source_id:transaction_id （递增）分配给这个事务，然后加入本实例的 GTID 集合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@@SESSION.GTID_NEXT = &apos;source_id:transaction_id&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gtid_next=GTID&lt;/code&gt;：指定的 GTID 的值，如果该值已经存在于实例的 GTID 集合中，接下来执行的事务会直接被系统忽略；反之就将该值分配给接下来要执行的事务，系统不需要给这个事务生成新的 GTID，也不用加 1&lt;/p&gt;
&lt;p&gt;注意：一个 GTID 只能给一个事务使用，所以执行下一个事务，要把 gtid_next 设置成另外一个 GTID 或者 automatic&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;业务场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主库 X 和从库 Y 执行一条相同的指令后进行事务同步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO t VALUES(1,1);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 Y 同步 X 时，会出现主键冲突，导致实例 X 的同步线程停止，解决方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET gtid_next=&apos;(这里是主库 X 的 GTID 值)&apos;;
BEGIN;
COMMIT;
SET gtid_next=automatic;
START SLAVE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前三条语句通过&lt;strong&gt;提交一个空事务&lt;/strong&gt;，把 X 的 GTID 加到实例 Y 的 GTID 集合中，实例 Y 就会直接跳过这个事务&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;切换&lt;/h5&gt;
&lt;p&gt;在 GTID 模式下，CHANGE MASTER TO 不需要指定日志名和日志偏移量，指定 &lt;code&gt;master_auto_position=1&lt;/code&gt; 代表使用 GTID 模式&lt;/p&gt;
&lt;p&gt;新主库实例 A 的 GTID 集合记为 set_a，从库实例 B 的 GTID 集合记为 set_b，主备切换逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例 B 指定主库 A，基于主备协议建立连接，实例 B 并把 set_b 发给主库 A&lt;/li&gt;
&lt;li&gt;实例 A 算出 set_a 与 set_b 的差集，就是所有存在于 set_a 但不存在于 set_b 的 GTID 的集合，判断 A 本地是否包含了这个&lt;strong&gt;差集&lt;/strong&gt;需要的所有 binlog 事务
&lt;ul&gt;
&lt;li&gt;如果不包含，表示 A 已经把实例 B 需要的 binlog 给删掉了，直接返回错误&lt;/li&gt;
&lt;li&gt;如果确认全部包含，A 从自己的 binlog 文件里面，找出第一个不在 set_b 的事务，发给 B&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实例 A 之后就从这个事务开始，往后读文件，按顺序取 binlog 发给 B 去执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://time.geekbang.org/column/article/77427&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;日志&lt;/h2&gt;
&lt;h3&gt;日志分类&lt;/h3&gt;
&lt;p&gt;在任何一种数据库中，都会有各种各样的日志，记录着数据库工作的过程，可以帮助数据库管理员追踪数据库曾经发生过的各种事件&lt;/p&gt;
&lt;p&gt;MySQL日志主要包括六种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重做日志（redo log）&lt;/li&gt;
&lt;li&gt;回滚日志（undo log）&lt;/li&gt;
&lt;li&gt;归档日志（binlog）（二进制日志）&lt;/li&gt;
&lt;li&gt;错误日志（errorlog）&lt;/li&gt;
&lt;li&gt;慢查询日志（slow query log）&lt;/li&gt;
&lt;li&gt;一般查询日志（general log）&lt;/li&gt;
&lt;li&gt;中继日志（relay log）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;错误日志&lt;/h3&gt;
&lt;p&gt;错误日志是 MySQL 中最重要的日志之一，记录了当 mysqld 启动和停止时，以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时，可以首先查看此日志&lt;/p&gt;
&lt;p&gt;该日志是默认开启的，默认位置是：&lt;code&gt;/var/log/mysql/error.log&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;查看指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;log_error%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看日志内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -f /var/log/mysql/error.log
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;归档日志&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;归档日志（BINLOG）也叫二进制日志，是因为采用二进制进行存储，记录了所有的 DDL（数据定义语言）语句和 DML（数据操作语言）语句，但&lt;strong&gt;不包括数据查询语句，在事务提交前的最后阶段写入&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：&lt;strong&gt;灾难时的数据恢复和 MySQL 的主从复制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;归档日志默认情况下是没有开启的，需要在 MySQL 配置文件中开启，并配置 MySQL 日志的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/mysql
vim my.cnf

# 配置开启binlog日志， 日志的文件前缀为 mysqlbin -----&amp;gt; 生成的文件名如: mysqlbin.000001
log_bin=mysqlbin
# 配置二进制日志的格式
binlog_format=STATEMENT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志存放位置：配置时给定了文件名但是没有指定路径，日志默认写入MySQL 的数据目录&lt;/p&gt;
&lt;p&gt;日志格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;STATEMENT：该日志格式在日志文件中记录的都是 &lt;strong&gt;SQL 语句&lt;/strong&gt;，每一条对数据进行修改的 SQL 都会记录在日志文件中，通过 mysqlbinlog 工具，可以查看到每条语句的文本。主从复制时，从库会将日志解析为原语句，并在从库重新执行一遍&lt;/p&gt;
&lt;p&gt;缺点：可能会导致主备不一致，因为记录的 SQL 在不同的环境中可能选择的索引不同，导致结果不同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ROW：该日志格式在日志文件中记录的是每一行的&lt;strong&gt;数据变更&lt;/strong&gt;，而不是记录 SQL 语句。比如执行 SQL 语句 &lt;code&gt;update tb_book set status=&apos;1&apos;&lt;/code&gt;，如果是 STATEMENT，在日志中会记录一行 SQL 语句； 如果是 ROW，由于是对全表进行更新，就是每一行记录都会发生变更，ROW 格式的日志中会记录每一行的数据变更&lt;/p&gt;
&lt;p&gt;缺点：记录的数据比较多，占用很多的存储空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MIXED：这是 MySQL 默认的日志格式，混合了STATEMENT 和 ROW 两种格式，MIXED 格式能尽量利用两种模式的优点，而避开它们的缺点&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日志刷盘&lt;/h4&gt;
&lt;p&gt;事务执行过程中，先将日志写（write）到 binlog cache，事务提交时再把 binlog cache 写（fsync）到 binlog 文件中，一个事务的 binlog 是不能被拆开的，所以不论这个事务多大也要确保一次性写入&lt;/p&gt;
&lt;p&gt;事务提交时执行器把 binlog cache 里的完整事务写入到 binlog 中，并清空 binlog cache&lt;/p&gt;
&lt;p&gt;write 和 fsync 的时机由参数 sync_binlog 控制的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sync_binlog=0：表示每次提交事务都只 write，不 fsync&lt;/li&gt;
&lt;li&gt;sync_binlog=1：表示每次提交事务都会执行 fsync&lt;/li&gt;
&lt;li&gt;sync_binlog=N(N&amp;gt;1)：表示每次提交事务都 write，但累积 N 个事务后才 fsync，但是如果主机发生异常重启，会丢失最近 N 个事务的 binlog 日志&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日志读取&lt;/h4&gt;
&lt;p&gt;日志文件存储位置：/var/lib/mysql&lt;/p&gt;
&lt;p&gt;由于日志以二进制方式存储，不能直接读取，需要用 mysqlbinlog 工具来查看，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog log-file;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 STATEMENT 格式日志：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;执行插入语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO tb_book VALUES(NULL,&apos;Lucene&apos;,&apos;2088-05-01&apos;,&apos;0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cd /var/lib/mysql&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-rw-r-----  1 mysql mysql      177 5月  23 21:08 mysqlbin.000001
-rw-r-----  1 mysql mysql       18 5月  23 21:04 mysqlbin.index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mysqlbin.index：该文件是日志索引文件 ， 记录日志的文件名；&lt;/p&gt;
&lt;p&gt;mysqlbing.000001：日志文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看日志内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog mysqlbing.000001;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%961.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;日志结尾有 COMMIT&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查看 ROW 格式日志：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置二进制日志的格式
binlog_format=ROW
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO tb_book VALUES(NULL,&apos;SpringCloud实战&apos;,&apos;2088-05-05&apos;,&apos;0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看日志内容：日志格式 ROW，直接查看数据是乱码，可以在 mysqlbinlog 后面加上参数 -vv&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlbinlog -vv mysqlbin.000002
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%962.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日志删除&lt;/h4&gt;
&lt;p&gt;对于比较繁忙的系统，生成日志量大，这些日志如果长时间不清除，将会占用大量的磁盘空间，需要删除日志&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Reset Master 指令删除全部 binlog 日志，删除之后，日志编号将从 xxxx.000001重新开始&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reset Master	-- MySQL指令
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行指令 &lt;code&gt;PURGE MASTER LOGS TO &apos;mysqlbin.***&lt;/code&gt;，该命令将删除 &lt;code&gt; ***&lt;/code&gt; 编号之前的所有日志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行指令 &lt;code&gt;PURGE MASTER LOGS BEFORE &apos;yyyy-mm-dd hh:mm:ss&apos;&lt;/code&gt; ，该命令将删除日志为 &lt;code&gt;yyyy-mm-dd hh:mm:ss&lt;/code&gt; 之前产生的日志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置参数 &lt;code&gt;--expire_logs_days=#&lt;/code&gt;，此参数的含义是设置日志的过期天数，过了指定的天数后日志将会被自动删除，这样做有利于减少管理日志的工作量，配置 my.cnf 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log_bin=mysqlbin
binlog_format=ROW
--expire_logs_days=3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数据恢复&lt;/h4&gt;
&lt;p&gt;误删库或者表时，需要根据 binlog 进行数据恢复&lt;/p&gt;
&lt;p&gt;一般情况下数据库有定时的全量备份，假如每天 0 点定时备份，12 点误删了库，恢复流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;取最近一次全量备份，用备份恢复出一个临时库&lt;/li&gt;
&lt;li&gt;从日志文件中取出凌晨 0 点之后的日志&lt;/li&gt;
&lt;li&gt;把除了误删除数据的语句外日志，全部应用到临时库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跳过误删除语句日志的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果原实例没有使用 GTID 模式，只能在应用到包含 12 点的 binlog 文件的时候，先用 –stop-position 参数执行到误操作之前的日志，然后再用 –start-position 从误操作之后的日志继续执行&lt;/li&gt;
&lt;li&gt;如果实例使用了 GTID 模式，假设误操作命令的 GTID 是 gtid1，那么只需要提交一个空事务先将这个 GTID 加到临时实例的 GTID 集合，之后按顺序执行 binlog 的时就会自动跳过误操作的语句&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;查询日志&lt;/h3&gt;
&lt;p&gt;查询日志中记录了客户端的所有操作语句，而二进制日志不包含查询数据的 SQL 语句&lt;/p&gt;
&lt;p&gt;默认情况下，查询日志是未开启的。如果需要开启查询日志，配置 my.cnf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 该选项用来开启查询日志，可选值0或者1，0代表关闭，1代表开启 
general_log=1
# 设置日志的文件名，如果没有指定，默认的文件名为host_name.log，存放在/var/lib/mysql
general_log_file=mysql_query.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置完毕之后，在数据库执行以下操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM tb_book;
SELECT * FROM tb_book WHERE id = 1;
UPDATE tb_book SET name = &apos;lucene入门指南&apos; WHERE id = 5;
SELECT * FROM tb_book WHERE id &amp;lt; 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完毕之后， 再次来查询日志文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;慢日志&lt;/h3&gt;
&lt;p&gt;慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒，最小为 0， 精度到微秒&lt;/p&gt;
&lt;p&gt;慢查询日志默认是关闭的，可以通过两个参数来控制慢查询日志，配置文件 &lt;code&gt;/etc/mysql/my.cnf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 该参数用来控制慢查询日志是否开启，可选值0或者1，0代表关闭，1代表开启 
slow_query_log=1 

# 该参数用来指定慢查询日志的文件名，存放在 /var/lib/mysql
slow_query_log_file=slow_query.log

# 该选项用来配置查询的时间限制，超过这个时间将认为值慢查询，将需要进行日志记录，默认10s
long_query_time=10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志读取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接通过 cat 指令查询该日志文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat slow_query.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%85%A2%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%961.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果慢查询日志内容很多，直接查看文件比较繁琐，可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldumpslow slow_query.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-%E6%85%A2%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%962.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;范式&lt;/h2&gt;
&lt;h3&gt;第一范式&lt;/h3&gt;
&lt;p&gt;建立科学的，&lt;strong&gt;规范的数据表&lt;/strong&gt;就需要满足一些规则来优化数据的设计和存储，这些规则就称为范式&lt;/p&gt;
&lt;p&gt;**1NF：**数据库表的每一列都是不可分割的原子数据项，不能是集合、数组等非原子数据项。即表中的某个列有多个值时，必须拆分为不同的列。简而言之，&lt;strong&gt;第一范式每一列不可再拆分，称为原子性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;基本表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E6%99%AE%E9%80%9A%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一范式表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E7%AC%AC%E4%B8%80%E8%8C%83%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第二范式&lt;/h3&gt;
&lt;p&gt;**2NF：**在满足第一范式的基础上，非主属性完全依赖于主码（主关键字、主键），消除非主属性对主码的部分函数依赖。简而言之，&lt;strong&gt;表中的每一个字段 （所有列）都完全依赖于主键，记录的唯一性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：遵守第二范式减少数据冗余，通过主键区分相同数据。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数依赖：A → B，如果通过 A 属性(属性组)的值，可以确定唯一 B 属性的值，则称 B 依赖于 A
&lt;ul&gt;
&lt;li&gt;学号 → 姓名；(学号，课程名称) → 分数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;完全函数依赖：A → B，如果A是一个属性组，则 B 属性值的确定需要依赖于 A 属性组的所有属性值
&lt;ul&gt;
&lt;li&gt;(学号，课程名称) → 分数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;部分函数依赖：A → B，如果 A 是一个属性组，则 B 属性值的确定只需要依赖于 A 属性组的某些属性值
&lt;ul&gt;
&lt;li&gt;(学号，课程名称) → 姓名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;传递函数依赖：A → B，B → C，如果通过A属性(属性组)的值，可以确定唯一 B 属性的值，在通过 B 属性(属性组)的值，可以确定唯一 C 属性的值，则称 C 传递函数依赖于 A
&lt;ul&gt;
&lt;li&gt;学号 → 系名，系名 → 系主任&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;码：如果在一张表中，一个属性或属性组，被其他所有属性所完全依赖，则称这个属性(属性组)为该表的码
&lt;ul&gt;
&lt;li&gt;该表中的码：(学号，课程名称)&lt;/li&gt;
&lt;li&gt;主属性：码属性组中的所有属性&lt;/li&gt;
&lt;li&gt;非主属性：除码属性组以外的属性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E7%AC%AC%E4%BA%8C%E8%8C%83%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第三范式&lt;/h3&gt;
&lt;p&gt;**3NF：**在满足第二范式的基础上，表中的任何属性不依赖于其它非主属性，消除传递依赖。简而言之，&lt;strong&gt;非主键都直接依赖于主键，而不是通过其它的键来间接依赖于主键&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;作用：可以通过主键 id 区分相同数据，修改数据的时候只需要修改一张表（方便修改），反之需要修改多表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E7%AC%AC%E4%B8%89%E8%8C%83%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Redis&lt;/h1&gt;
&lt;h2&gt;NoSQL&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;NoSQL（Not-Only SQL）：泛指非关系型的数据库，作为关系型数据库的补充&lt;/p&gt;
&lt;p&gt;MySQL 支持 ACID 特性，保证可靠性和持久性，读取性能不高，因此需要缓存的来减缓数据库的访问压力&lt;/p&gt;
&lt;p&gt;作用：应对基于海量用户和海量数据前提下的数据处理问题&lt;/p&gt;
&lt;p&gt;特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可扩容，可伸缩，SQL 数据关系过于复杂，Nosql 不存关系，只存数据&lt;/li&gt;
&lt;li&gt;大数据量下高性能，数据不存取在磁盘 IO，存取在内存&lt;/li&gt;
&lt;li&gt;灵活的数据模型，设计了一些数据存储格式，能保证效率上的提高&lt;/li&gt;
&lt;li&gt;高可用，集群&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见的 NoSQL：Redis、memcache、HBase、MongoDB&lt;/p&gt;
&lt;p&gt;参考书籍：https://book.douban.com/subject/25900156/&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1CJ411m7Gc&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Redis&lt;/h3&gt;
&lt;p&gt;Redis (REmote DIctionary Server) ：用 C 语言开发的一个开源的高性能键值对（key-value）数据库&lt;/p&gt;
&lt;p&gt;特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据间没有必然的关联关系，&lt;strong&gt;不存关系，只存数据&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据&lt;strong&gt;存储在内存&lt;/strong&gt;，存取速度快，解决了磁盘 IO 速度慢的问题&lt;/li&gt;
&lt;li&gt;内部采用&lt;strong&gt;单线程&lt;/strong&gt;机制进行工作&lt;/li&gt;
&lt;li&gt;高性能，官方测试数据，50 个并发执行 100000 个请求，读的速度是 110000 次/s，写的速度是 81000 次/s&lt;/li&gt;
&lt;li&gt;多数据类型支持
&lt;ul&gt;
&lt;li&gt;字符串类型：string（String）&lt;/li&gt;
&lt;li&gt;列表类型：list（LinkedList）&lt;/li&gt;
&lt;li&gt;散列类型：hash（HashMap）&lt;/li&gt;
&lt;li&gt;集合类型：set（HashSet）&lt;/li&gt;
&lt;li&gt;有序集合类型：zset/sorted_set（TreeSet）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;支持持久化，可以进行数据灾难恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;安装启动&lt;/h3&gt;
&lt;p&gt;安装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查 Redis 状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动服务器——参数启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server [--port port]
#redis-server --port 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动服务器——配置文件启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server config_file_name
#redis-server /etc/redis/conf/redis-6397.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli [-h host] [-p port]
#redis-cli -h 192.168.2.185 -p 6397
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：服务器启动指定端口使用的是--port，客户端启动指定端口使用的是-p&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;h4&gt;系统目录&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建文件结构&lt;/p&gt;
&lt;p&gt;创建配置文件存储目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建服务器文件存储目录（包含日志、数据、临时配置文件等）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建配置文件副本放入 conf 目录，Ubuntu 系统配置文件 redis.conf 在目录 &lt;code&gt;/etc/redis&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat redis.conf | grep -v &quot;#&quot; | grep -v &quot;^$&quot; -&amp;gt; /conf/redis-6379.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去除配置文件的注释和空格，输出到新的文件，命令方式采用 redis-port.conf&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;服务器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置服务器以守护进程的方式运行，关闭后服务器控制台中将打印服务器运行信息（同日志内容相同）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;daemonize yes|no
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定主机地址，绑定本地IP地址，否则SSH无法访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bind ip
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置服务器端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;port port
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置服务器文件保存地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dir path
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置数据库的数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;databases 16
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多服务器快捷配置：&lt;/p&gt;
&lt;p&gt;导入并加载指定配置文件信息，用于快速创建 redis 公共配置较多的 redis 实例配置文件，便于维护&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include /path/conf_name.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;客户端&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器允许客户端连接最大数量，默认 0，表示无限制，当客户端连接到达上限后，Redis 会拒绝新的连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maxclients count
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端闲置等待最大时长，达到最大值后关闭对应连接，如需关闭该功能，设置为 0：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timeout seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日志配置&lt;/h4&gt;
&lt;p&gt;设置日志记录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置服务器以指定日志记录级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loglevel debug|verbose|notice|warning
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;日志记录文件名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logfile filename
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：日志级别开发期设置为 verbose 即可，生产环境中配置为 notice，简化日志输出量，降低写日志 IO 的频度&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配置文件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bind 192.168.2.185
port 6379
#timeout 0
daemonize no
logfile /etc/redis/data/redis-6379.log
dir /etc/redis/data
dbfilename &quot;dump-6379.rdb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;基本指令&lt;/h4&gt;
&lt;p&gt;帮助信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取命令帮助文档&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;help [command]
#help set
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取组中所有命令信息名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;help [@group-name]
#help @string
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;退出服务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;退出客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;quit
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出客户端服务器快捷键：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ctrl+C
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;数据库&lt;/h2&gt;
&lt;h3&gt;服务器&lt;/h3&gt;
&lt;p&gt;Redis 服务器将所有数据库保存在&lt;strong&gt;服务器状态 redisServer 结构&lt;/strong&gt;的 db 数组中，数组的每一项都是 redisDb 结构，代表一个数据库，每个数据库之间相互独立，**共用 **Redis 内存，不区分大小。在初始化服务器时，根据 dbnum 属性决定创建数据库的数量，该属性由服务器配置的 database 选项决定，默认 16&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 保存服务器所有的数据库
    redisDB *db;
    
    // 服务器数据库的数量
    int dbnum;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器数据库.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在服务器内部&lt;/strong&gt;，客户端状态 redisClient 结构的 db 属性记录了目标数据库，是一个指向 redisDb 结构的指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisClient {
    // 记录客户端正在使用的数据库，指向 redisServer.db 数组中的某一个 db
    redisDB *db;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 Redis 客户端都有目标数据库，执行数据库读写命令时目标数据库就会成为这些命令的操作对象，默认情况下 Redis 客户端的目标数据库为 0 号数据库，客户端可以执行 SELECT 命令切换目标数据库，原理是通过修改 redisClient.db 指针指向服务器中不同数据库&lt;/p&gt;
&lt;p&gt;命令操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select index	#切换数据库，index从0-15取值
move key db		#数据移动到指定数据库，db是数据库编号
ping			#测试数据库是否连接正常，返回PONG
echo message	#控制台输出信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 没有可以返回客户端目标数据库的命令，但是 redis-cli 客户端旁边会提示当前所使用的目标数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SELECT 1 
OK 
redis[1]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;键空间&lt;/h3&gt;
&lt;h4&gt;key space&lt;/h4&gt;
&lt;p&gt;Redis 是一个键值对（key-value pair）数据库服务器，每个数据库都由一个 redisDb 结构表示，redisDb.dict &lt;strong&gt;字典中保存了数据库的所有键值对&lt;/strong&gt;，将这个字典称为键空间（key space）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisDB {
    // 数据库键空间，保存所有键值对
    dict *dict
} redisDB;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;键空间和用户所见的数据库是直接对应的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;键空间的键就是数据库的键，每个键都是一个字符串对象&lt;/li&gt;
&lt;li&gt;键空间的值就是数据库的值，每个值可以是任意一种 Redis 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%95%B0%E6%8D%AE%E5%BA%93%E9%94%AE%E7%A9%BA%E9%97%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当使用 Redis 命令对数据库进行读写时，服务器不仅会对键空间执行指定的读写操作，还会&lt;strong&gt;进行一些维护操作&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在读取一个键后（读操作和写操作都要对键进行读取），服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数，这两个值可以在 &lt;code&gt;INFO stats&lt;/code&gt; 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看&lt;/li&gt;
&lt;li&gt;更新键的 LRU（最后使用）时间，该值可以用于计算键的闲置时间，使用 &lt;code&gt;OBJECT idletime key&lt;/code&gt; 查看键 key 的闲置时间&lt;/li&gt;
&lt;li&gt;如果在读取一个键时发现该键已经过期，服务器会&lt;strong&gt;先删除过期键&lt;/strong&gt;，再执行其他操作&lt;/li&gt;
&lt;li&gt;如果客户端使用 WATCH 命令监视了某个键，那么服务器在对被监视的键进行修改之后，会将这个键标记为脏（dirty），从而让事务注意到这个键已经被修改过&lt;/li&gt;
&lt;li&gt;服务器每次修改一个键之后，都会对 dirty 键计数器的值增1，该计数器会触发服务器的持久化以及复制操作&lt;/li&gt;
&lt;li&gt;如果服务器开启了数据库通知功能，那么在对键进行修改之后，服务器将按配置发送相应的数据库通知&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;读写指令&lt;/h4&gt;
&lt;p&gt;常见键操作指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;增加指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set key value				#添加一个字符串类型的键值对

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;del key						#删除指定key
unlink key   				#非阻塞删除key，真正的删除会在后续异步操作
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rename key newkey			#改名
renamenx key newkey			#改名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得更新需要参看具体得 Redis 对象得操作方式，比如字符串对象执行 &lt;code&gt;SET key value&lt;/code&gt; 就可以完成修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exists key					#获取key是否存在
randomkey					#随机返回一个键
keys pattern				#查询key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KEYS 命令需要&lt;strong&gt;遍历存储的键值对&lt;/strong&gt;，操作延时高，一般不被建议用于生产环境中&lt;/p&gt;
&lt;p&gt;查询模式规则：* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keys *						#查询所有key
keys aa*					#查询所有以aa开头
keys *bb					#查询所有以bb结尾
keys ??cc					#查询所有前面两个字符任意，后面以cc结尾 
keys user:?					#查询所有以user:开头，最后一个字符任意
keys u[st]er:1				#查询所有以u开头，以er:1结尾，中间包含一个字母，s或t
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type key					#获取key的类型
dbsize						#获取当前数据库的数据总量，即key的个数
flushdb						#清除当前数据库的所有数据(慎用)
flushall					#清除所有数据(慎用)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行 FLUSHDB 这样的危险命令之前，最好先执行一个 SELECT 命令，保证当前所操作的数据库是目标数据库&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;时效设置&lt;/h4&gt;
&lt;p&gt;客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间（TimeTo Live, TTL），在经过指定时间之后，服务器就会自动删除生存时间为 0 的键；也可以以 UNIX 时间戳的方式设置过期时间（expire time），当键的过期时间到达，服务器会自动删除这个键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;expire key seconds			#为指定key设置生存时间，单位为秒
pexpire key milliseconds	#为指定key设置生存时间，单位为毫秒
expireat key timestamp		#为指定key设置过期时间，单位为时间戳
pexpireat key mil-timestamp	#为指定key设置过期时间，单位为毫秒时间戳
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令&lt;strong&gt;底层都是转换为 PEXPIREAT 命令&lt;/strong&gt;来实现的&lt;/li&gt;
&lt;li&gt;SETEX 命令可以在设置一个字符串键的同时为键设置过期时间，但是该命令是一个类型限定命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;redisDb 结构的 expires 字典保存了数据库中所有键的过期时间，字典称为过期字典：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;键是一个指针，指向键空间中的某个键对象（复用键空间的对象，不会产生内存浪费）&lt;/li&gt;
&lt;li&gt;值是一个 long long 类型的整数，保存了键的过期时间，是一个毫秒精度的 UNIX 时间戳&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisDB {
    // 过期字典，保存所有键的过期时间
    dict *expires
} redisDB;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端执行 PEXPIREAT 命令，服务器会在数据库的过期字典中关联给定的数据库键和过期时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def PEXPIREAT(key, expire_time_in_ms):
	# 如果给定的键不存在于键空间，那么不能设置过期时间
	if key not in redisDb.dict:
		return 0
		
	# 在过期字典中关联键和过期时间
	redisDB.expires[key] = expire_time_in_ms
	
	# 过期时间设置成功
	return 1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;时效状态&lt;/h4&gt;
&lt;p&gt;TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差，返回这个键的剩余生存时间&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回正数代表该数据在内存中还能存活的时间&lt;/li&gt;
&lt;li&gt;返回 -1 代表永久性，返回 -2 代表键不存在&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ttl key			#获取key的剩余时间，每次获取会自动变化(减小)，类似于倒计时
pttl key		#获取key的剩余时间，单位是毫秒，每次获取会自动变化(减小)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PERSIST 是 PEXPIREAT 命令的反操作，在过期字典中查找给定的键，并解除键和值（过期时间）在过期字典中的关联&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;persist key		#切换key从时效性转换为永久性
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 通过过期字典可以检查一个给定键是否过期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查给定键是否存在于过期字典：如果存在，那么取得键的过期时间&lt;/li&gt;
&lt;li&gt;检查当前 UNIX 时间戳是否大于键的过期时间：如果是那么键已经过期，否则键未过期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：AOF、RDB 和复制功能对过期键的处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RDB ：
&lt;ul&gt;
&lt;li&gt;生成 RDB 文件，程序会对数据库中的键进行检查，已过期的键不会被保存到新创建的 RDB 文件中&lt;/li&gt;
&lt;li&gt;载入 RDB 文件，如果服务器以主服务器模式运行，那么在载入时会对键进行检查，过期键会被忽略；如果服务器以从服务器模式运行，会载入所有键，包括过期键，但是主从服务器进行数据同步时就会删除这些键&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AOF：
&lt;ul&gt;
&lt;li&gt;写入 AOF 文件，如果数据库中的某个键已经过期，但还没有被删除，那么 AOF 文件不会因为这个过期键而产生任何影响；当该过期键被删除，程序会向 AOF 文件追加一条 DEL 命令，显式的删除该键&lt;/li&gt;
&lt;li&gt;AOF 重写，会对数据库中的键进行检查，忽略已经过期的键&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;复制：当服务器运行在复制模式下时，从服务器的过期键删除动作由主服务器控制
&lt;ul&gt;
&lt;li&gt;主服务器在删除一个过期键之后，会显式地向所有从服务器发送一个 DEL 命令，告知从服务器删除这个过期键&lt;/li&gt;
&lt;li&gt;从服务器在执行客户端发送的读命令时，即使碰到过期键也不会将过期键删除，会当作未过期键处理，只有在接到主服务器发来的 DEL 命令之后，才会删除过期键（数据不一致）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;过期删除&lt;/h3&gt;
&lt;h4&gt;删除策略&lt;/h4&gt;
&lt;p&gt;删除策略就是&lt;strong&gt;针对已过期数据的处理策略&lt;/strong&gt;，已过期的数据不一定被立即删除，在不同的场景下使用不同的删除方式会有不同效果，在内存占用与 CPU 占用之间寻找一种平衡，顾此失彼都会造成整体 Redis 性能的下降，甚至引发服务器宕机或内存泄露&lt;/p&gt;
&lt;p&gt;针对过期数据有三种删除策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定时删除&lt;/li&gt;
&lt;li&gt;惰性删除（被动删除）&lt;/li&gt;
&lt;li&gt;定期删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 采用惰性删除和定期删除策略的结合使用&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;定时删除&lt;/h4&gt;
&lt;p&gt;在设置键的过期时间的同时，创建一个定时器（timer），让定时器在键的过期时间到达时，立即执行对键的删除操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：节约内存，到时就删除，快速释放掉不必要的内存占用&lt;/li&gt;
&lt;li&gt;缺点：对 CPU 不友好，无论 CPU 此时负载多高均占用 CPU，会影响 Redis 服务器响应时间和指令吞吐量&lt;/li&gt;
&lt;li&gt;总结：用处理器性能换取存储空间（拿时间换空间）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建一个定时器需要用到 Redis 服务器中的时间事件，而时间事件的实现方式是无序链表，查找一个事件的时间复杂度为 O(N)，并不能高效地处理大量时间事件，所以采用这种方式并不现实&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;惰性删除&lt;/h4&gt;
&lt;p&gt;数据到达过期时间不做处理，等下次访问到该数据时执行 &lt;strong&gt;expireIfNeeded()&lt;/strong&gt; 判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果输入键已经过期，那么 expireIfNeeded 函数将输入键从数据库中删除，接着访问就会返回空&lt;/li&gt;
&lt;li&gt;如果输入键未过期，那么 expireIfNeeded 函数不做动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查，该函数就像一个过滤器，在命令真正执行之前过滤掉过期键&lt;/p&gt;
&lt;p&gt;惰性删除的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：节约 CPU 性能，删除的目标仅限于当前处理的键，不会在删除其他无关的过期键上花费任何 CPU 时间&lt;/li&gt;
&lt;li&gt;缺点：内存压力很大，出现长期占用内存的数据，如果过期键永远不被访问，这种情况相当于内存泄漏&lt;/li&gt;
&lt;li&gt;总结：用存储空间换取处理器性能（拿空间换时间）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;定期删除&lt;/h4&gt;
&lt;p&gt;定期删除策略是每隔一段时间执行一次删除过期键操作，并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果删除操作执行得太频繁，或者执行时间太长，就会退化成定时删除策略，将 CPU 时间过多地消耗在删除过期键上&lt;/li&gt;
&lt;li&gt;如果删除操作执行得太少，或者执行时间太短，定期删除策略又会和惰性删除策略一样，出现浪费内存的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定期删除是&lt;strong&gt;周期性轮询 Redis 库中的时效性&lt;/strong&gt;数据，从过期字典中随机抽取一部分键检查，利用过期数据占比的方式控制删除频度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Redis 启动服务器初始化时，读取配置 server.hz 的值，默认为 10，执行指令 info server 可以查看，每秒钟执行 server.hz 次 &lt;code&gt;serverCron() → activeExpireCycle()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;activeExpireCycle() 对某个数据库中的每个 expires 进行检测，工作模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;轮询每个数据库，从数据库中取出一定数量的随机键进行检查，并删除其中的过期键，如果过期 key 的比例超过了 25%，则继续重复此过程，直到过期 key 的比例下降到 25% 以下，或者这次任务的执行耗时超过了 25 毫秒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局变量 current_db 用于记录 activeExpireCycle() 的检查进度（哪一个数据库），下一次调用时接着该进度处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随着函数的不断执行，服务器中的所有数据库都会被检查一遍，这时将 current_db 重置为 0，然后再次开始新一轮的检查&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定期删除特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 性能占用设置有峰值，检测频度可自定义设置&lt;/li&gt;
&lt;li&gt;内存压力不是很大，长期占用内存的&lt;strong&gt;冷数据会被持续清理&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;周期性抽查存储空间（随机抽查，重点抽查）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;数据淘汰&lt;/h3&gt;
&lt;h4&gt;逐出算法&lt;/h4&gt;
&lt;p&gt;数据淘汰策略：当新数据进入 Redis 时，在执行每一个命令前，会调用 &lt;strong&gt;freeMemoryIfNeeded()&lt;/strong&gt; 检测内存是否充足。如果内存不满足新加入数据的最低存储要求，Redis 要临时删除一些数据为当前指令清理存储空间，清理数据的策略称为&lt;strong&gt;逐出算法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间，如果不成功则反复执行，当对所有数据尝试完毕，如不能达到内存清理的要求，&lt;strong&gt;出现 Redis 内存打满异常&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(error) OOM command not allowed when used memory &amp;gt;&apos;maxmemory&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;策略配置&lt;/h4&gt;
&lt;p&gt;Redis 如果不设置最大内存大小或者设置最大内存大小为 0，在 64 位操作系统下不限制内存大小，在 32 位操作系统默认为 3GB 内存，一般推荐设置 Redis 内存为最大物理内存的四分之三&lt;/p&gt;
&lt;p&gt;内存配置方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过修改文件配置（永久生效）：修改配置文件 maxmemory 字段，单位为字节&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过命令修改（重启失效）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;config set maxmemory 104857600&lt;/code&gt;：设置 Redis 最大占用内存为 100MB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;config get maxmemory&lt;/code&gt;：获取 Redis 最大占用内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;info&lt;/code&gt; ：可以查看 Redis 内存使用情况，&lt;code&gt;used_memory_human&lt;/code&gt; 字段表示实际已经占用的内存，&lt;code&gt;maxmemory&lt;/code&gt; 表示最大占用内存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;影响数据淘汰的相关配置如下，配置 conf 文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每次选取待删除数据的个数，采用随机获取数据的方式作为待检测删除数据，防止全库扫描，导致严重的性能消耗，降低读写性能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maxmemory-samples count
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;达到最大内存后的，对被挑选出来的数据进行删除的策略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maxmemory-policy policy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据删除的策略 policy：3 类 8 种&lt;/p&gt;
&lt;p&gt;第一类：检测易失数据（可能会过期的数据集 server.db[i].expires）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile-lru	# 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰
volatile-lfu	# 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰
volatile-ttl	# 对设置了过期时间的 key 选择将要过期的数据淘汰
volatile-random	# 对设置了过期时间的 key 选择任意数据淘汰
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二类：检测全库数据（所有数据集 server.db[i].dict ）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;allkeys-lru		# 对所有 key 选择最近最少使用的数据淘汰
allkeLyRs-lfu	# 对所有 key 选择最近使用次数最少的数据淘汰
allkeys-random	# 对所有 key 选择任意数据淘汰，相当于随机
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三类：放弃数据驱逐&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no-enviction	#禁止驱逐数据(redis4.0中默认策略)，会引发OOM(Out Of Memory)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据淘汰策略配置依据：使用 INFO 命令输出监控信息，查询缓存 hit 和 miss 的次数，根据需求调优 Redis 配置&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;排序机制&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序，并不更改集合中的数据位置，只是查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SORT key [ASC/DESC]			#对key中数据排序，默认对数字排序，并不更改集合中的数据位置，只是查询
SORT key ALPHA				#对key中字母排序，按照字典序
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;SORT&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SORT &amp;lt;key&amp;gt;&lt;/code&gt; 命令可以对一个包含数字值的键 key 进行排序&lt;/p&gt;
&lt;p&gt;假设 &lt;code&gt;RPUSH numbers 3 1 2&lt;/code&gt;，执行 &lt;code&gt;SORT numbers&lt;/code&gt; 的详细步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个和 key 列表长度相同的数组，数组每项都是 redisSortObject 结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisSortObject {
    // 被排序键的值
    robj *obj;
    
    // 权重
    union {
        // 排序数字值时使用
        double score;
        // 排序带有 BY 选项的字符串
        robj *cmpobj;
    } u;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历数组，将各个数组项的 obj 指针分别指向 numbers 列表的各个项&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历数组，将 obj 指针所指向的列表项转换成一个 double 类型的浮点数，并将浮点数保存在对应数组项的 u.score 属性里&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据数组项 u.score 属性的值，对数组进行数字值排序，排序后的数组项按 u.score 属性的值&lt;strong&gt;从小到大排列&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历数组，将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端，程序首先访问数组的索引 0，依次向后访问&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-sort%E6%8E%92%E5%BA%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;SORT key [ASC/DESC]&lt;/code&gt; 函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在执行升序排序时，排序算法使用的对比函数产生升序对比结果&lt;/li&gt;
&lt;li&gt;在执行降序排序时，排序算法使用的对比函数产生降序对比结果&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;BY&lt;/h4&gt;
&lt;p&gt;SORT 命令默认使用被排序键中包含的元素作为排序的权重，元素本身决定了元素在排序之后所处的位置，通过使用 BY 选项，SORT 命令可以指定某些字符串键，或者某个哈希键所包含的某些域（field）来作为元素的权重，对一个键进行排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SORT &amp;lt;key&amp;gt; BY &amp;lt;pattern&amp;gt;			# 数值
SORT &amp;lt;key&amp;gt; BY &amp;lt;pattern&amp;gt; ALPHA	# 字符
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SADD fruits &quot;apple&quot; &quot;banana&quot; &quot;cherry&quot; 
(integer) 3
redis&amp;gt; SORT fruits ALPHA
1)	&quot;apple&quot;
2)	&quot;banana&quot;
3)	&quot;cherry&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; MSET apple-price 8 banana-price 5.5 cherry-price 7 
OK
# 使用水果的价钱进行排序
redis&amp;gt; SORT fruits BY *-price
1)	&quot;banana&quot;
2)	&quot;cherry&quot;
3)	&quot;apple&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现原理：排序时的 u.score 属性就会被设置为对应的权重&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;LIMIT&lt;/h4&gt;
&lt;p&gt;SORT 命令默认会将排序后的所有元素都返回给客户端，通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LIMIT &amp;lt;offset&amp;gt; &amp;lt;count&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;offset 参数表示要跳过的已排序元素数量&lt;/li&gt;
&lt;li&gt;count 参数表示跳过给定数量的元素后，要返回的已排序元素数量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 对应 a b c d e f  g
redis&amp;gt; SORT alphabet ALPHA LIMIT 2 3
1) 	&quot;c&quot;
2) 	&quot;d&quot;
3) 	&quot;e&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现原理：在排序后的 redisSortObject 结构数组中，将指针移动到数组的索引 2 上，依次访问 array[2]、array[3]、array[4] 这 3 个数组项，并将数组项的 obj 指针所指向的元素返回给客户端&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;GET&lt;/h4&gt;
&lt;p&gt;SORT 命令默认在对键进行排序后，返回被排序键本身所包含的元素，通过使用 GET 选项， 可以在对键进行排序后，根据被排序的元素以及 GET 选项所指定的模式，查找并返回某些键的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SORT &amp;lt;key&amp;gt; GET &amp;lt;pattern&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SADD students &quot;tom&quot; &quot;jack&quot; &quot;sea&quot;
#设置全名
redis&amp;gt; SET tom-name &quot;Tom Li&quot; 
OK 
redis&amp;gt; SET jack-name &quot;Jack Wang&quot; 
OK 
redis&amp;gt; SET sea-name &quot;Sea Zhang&quot;
OK 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SORT students ALPHA GET *-name
1)	&quot;Jack Wang&quot;
2)	&quot;Sea Zhang&quot;
3) 	&quot;Tom Li&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现原理：对 students 进行排序后，对于 jack 元素和 *-name 模式，查找程序返回键 jack-name，然后获取 jack-name 键对应的值&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;STORE&lt;/h4&gt;
&lt;p&gt;SORT 命令默认只向客户端返回排序结果，而不保存排序结果，通过使用 STORE 选项可以将排序结果保存在指定的键里面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SORT &amp;lt;key&amp;gt; STORE &amp;lt;sort_key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SADD students &quot;tom&quot; &quot;jack&quot; &quot;sea&quot;
(integer) 3 
redis&amp;gt; SORT students ALPHA STORE sorted_students 
(integer) 3 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现原理：排序后，检查 sorted_students 键是否存在，如果存在就删除该键，设置 sorted_students 为空白的列表键，遍历排序数组将元素依次放入&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;执行顺序&lt;/h4&gt;
&lt;p&gt;调用 SORT 命令，除了 GET 选项之外，改变其他选项的摆放顺序并不会影响命令执行选项的顺序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SORT &amp;lt;key&amp;gt; ALPHA [ASC/DESC] BY &amp;lt;by-pattern&amp;gt; LIMIT &amp;lt;offset&amp;gt; &amp;lt;count&amp;gt; GET &amp;lt;get-pattern&amp;gt; STORE &amp;lt;store_key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排序：命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项，对输入键进行排序，并得到一个排序结果集&lt;/li&gt;
&lt;li&gt;限制排序结果集的长度：使用 LIMIT 选项，对排序结果集的长度进行限制&lt;/li&gt;
&lt;li&gt;获取外部键：根据排序结果集中的元素以及 GET 选项指定的模式，查找并获取指定键的值，并用这些值来作为新的排序结果集&lt;/li&gt;
&lt;li&gt;保存排序结果集：使用 STORE 选项，将排序结果集保存到指定的键上面去&lt;/li&gt;
&lt;li&gt;向客户端返回排序结果集：最后一步命令遍历排序结果集，并依次向客户端返回排序结果集中的元素&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;通知机制&lt;/h3&gt;
&lt;p&gt;数据库通知是可以让客户端通过订阅给定的频道或者模式，来获知数据库中键的变化，以及数据库中命令的执行情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关注某个键执行了什么命令的通知称为键空间通知（key-space notification）&lt;/li&gt;
&lt;li&gt;关注某个命令被什么键执行的通知称为键事件通知（key-event notification）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图示订阅 0 号数据库 message 键：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-数据库通知.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AKE 代表服务器发送所有类型的键空间通知和键事件通知&lt;/li&gt;
&lt;li&gt;AK 代表服务器发送所有类型的键空间通知&lt;/li&gt;
&lt;li&gt;AE 代表服务器发送所有类型的键事件通知&lt;/li&gt;
&lt;li&gt;K$ 代表服务器只发送和字符串键有关的键空间通知&lt;/li&gt;
&lt;li&gt;EL 代表服务器只发送和列表键有关的键事件通知&lt;/li&gt;
&lt;li&gt;.....&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果给定的通知类型 type 不是服务器允许发送的通知类型，那么函数会直接返回&lt;/li&gt;
&lt;li&gt;如果给定的通知是服务器允许发送的通知
&lt;ul&gt;
&lt;li&gt;检测服务器是否允许发送键空间通知，允许就会构建并发送事件通知&lt;/li&gt;
&lt;li&gt;检测服务器是否允许发送键事件通知，允许就会构建并发送事件通知&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;体系架构&lt;/h2&gt;
&lt;h3&gt;事件驱动&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Redis 服务器是一个事件驱动程序，服务器需要处理两类事件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件事件 (file event)：服务器通过套接字与客户端（或其他 Redis 服务器）进行连接，而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件，服务器通过监听并处理这些事件完成一系列网络通信操作&lt;/li&gt;
&lt;li&gt;时间事件 (time event)：Redis 服务器中的一些操作（比如 serverCron 函数）需要在指定时间执行，而时间事件就是服务器对这类定时操作的抽象&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;文件事件&lt;/h4&gt;
&lt;h5&gt;基本组成&lt;/h5&gt;
&lt;p&gt;Redis 基于 Reactor 模式开发了网络事件处理器，这个处理器被称为文件事件处理器 (file event handler)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字，并根据套接字执行的任务来为套接字关联不同的事件处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时，与操作相对应的文件事件就会产生，这时文件事件分派器会调用套接字关联好的事件处理器来处理事件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件事件处理器&lt;strong&gt;以单线程方式运行&lt;/strong&gt;，但通过使用  I/O 多路复用程序来监听多个套接字， 既实现了高性能的网络通信模型，又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接，保持了 Redis 内部单线程设计的简单性&lt;/p&gt;
&lt;p&gt;文件事件处理器的组成结构：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-文件事件处理器.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;I/O 多路复用程序将所有产生事件的套接字处理请求放入一个&lt;strong&gt;单线程的执行队列&lt;/strong&gt;中，通过队列有序、同步的向文件事件分派器传送套接字，上一个套接字产生的事件处理完后，才会继续向分派器传送下一个&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-IO多路复用程序.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Redis 单线程也能高效的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;纯内存操作&lt;/li&gt;
&lt;li&gt;核心是基于非阻塞的 IO 多路复用机制，单线程可以高效处理多个请求&lt;/li&gt;
&lt;li&gt;底层使用 C 语言实现，C 语言实现的程序距离操作系统更近，执行速度相对会更快&lt;/li&gt;
&lt;li&gt;单线程同时也&lt;strong&gt;避免了多线程的上下文频繁切换问题&lt;/strong&gt;，预防了多线程可能产生的竞争问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;多路复用&lt;/h5&gt;
&lt;p&gt;Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的，Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则，编译时自动选择系统中&lt;strong&gt;性能最高的多路复用函数&lt;/strong&gt;来作为底层实现&lt;/p&gt;
&lt;p&gt;I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件，这两类事件和套接字操作之间的对应关系如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当套接字变得&lt;strong&gt;可读&lt;/strong&gt;时（客户端对套接字执行 write 操作或者 close 操作），或者有新的&lt;strong&gt;可应答&lt;/strong&gt;（acceptable）套接字出现时（客户端对服务器的监听套接字执行 connect 连接操作），套接字产生 AE_READABLE 事件&lt;/li&gt;
&lt;li&gt;当套接字变得可写时（客户端对套接字执行 read 操作，对于服务器来说就是可以写了），套接字产生 AE_WRITABLE 事件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件， 如果一个套接字同时产生了这两种事件，那么文件事件分派器会优先处理 AE_READABLE  事件， 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;处理器&lt;/h5&gt;
&lt;p&gt;Redis 为文件事件编写了多个处理器，这些事件处理器分别用于实现不同的网络通信需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接应答处理器，用于对连接服务器的各个客户端进行应答，Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联&lt;/li&gt;
&lt;li&gt;命令请求处理器，用于接收客户端传来的命令请求，执行套接字的读入操作，与 AE_READABLE 事件关联&lt;/li&gt;
&lt;li&gt;命令回复处理器，用于向客户端返回命令的执行结果，执行套接字的写入操作，与 AE_WRITABLE 事件关联&lt;/li&gt;
&lt;li&gt;复制处理器，当主服务器和从服务器进行复制操作时，主从服务器都需要关联该处理器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 客户端与服务器进行连接并发送命令的整个过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 服务器正在运作监听套接字的 AE_READABLE 事件，关联连接应答处理器&lt;/li&gt;
&lt;li&gt;当 Redis 客户端向服务器发起连接，监听套接字将产生 AE_READABLE 事件，触发连接应答处理器执行，对客户端的连接请求进行应答，创建客户端套接字以及客户端状态，并将客户端套接字的 &lt;strong&gt;AE_READABLE 事件与命令请求处理器&lt;/strong&gt;进行关联&lt;/li&gt;
&lt;li&gt;客户端向服务器发送命令请求，客户端套接字产生 AE_READABLE 事件，引发命令请求处理器执行，读取客户端的命令内容传给相关程序去执行&lt;/li&gt;
&lt;li&gt;执行命令会产生相应的命令回复，为了将这些命令回复传送回客户端，服务器会将客户端套接字的 &lt;strong&gt;AE_WRITABLE 事件与命令回复处理器&lt;/strong&gt;进行关联&lt;/li&gt;
&lt;li&gt;当客户端尝试读取命令回复时，客户端套接字产生 AE_WRITABLE 事件，触发命令回复处理器执行，在命令回复全部写入套接字后，服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;时间事件&lt;/h4&gt;
&lt;p&gt;Redis 的时间事件分为以下两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定时事件：在指定的时间之后执行一次（Redis 中暂时未使用）&lt;/li&gt;
&lt;li&gt;周期事件：每隔指定时间就执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个时间事件主要由以下三个属性组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：服务器为时间事件创建的全局唯一 ID（标识号），从小到大顺序递增，新事件的 ID 比旧事件的 ID 号要大&lt;/li&gt;
&lt;li&gt;when：毫秒精度的 UNIX 时间戳，记录了时间事件的到达（arrive）时间&lt;/li&gt;
&lt;li&gt;timeProc：时间事件处理器，当时间事件到达时，服务器就会调用相应的处理器来处理事件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定时事件：事件处理器返回 AE_NOMORE，该事件在到达一次后就会被删除&lt;/li&gt;
&lt;li&gt;周期事件：事件处理器返回非 AE_NOMORE 的整数值，服务器根据该值对事件的 when 属性更新，让该事件在一段时间后再次交付&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器将所有时间事件都放在一个&lt;strong&gt;无序链表&lt;/strong&gt;中，新的时间事件插入到链表的表头：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-时间事件.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;无序链表指是链表不按 when 属性的大小排序，每当时间事件执行器运行时就必须遍历整个链表，查找所有已到达的时间事件，并调用相应的事件处理器处理&lt;/p&gt;
&lt;p&gt;无序链表并不影响时间事件处理器的性能，因为正常模式下的 Redis 服务器&lt;strong&gt;只使用 serverCron 一个时间事件&lt;/strong&gt;，在 benchmark 模式下服务器也只使用两个时间事件，所以无序链表不会影响服务器的性能，几乎可以按照一个指针处理&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;事件调度&lt;/h4&gt;
&lt;p&gt;服务器中同时存在文件事件和时间事件两种事件类型，调度伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 事件调度伪代码
def aeProcessEvents():
	# 获取到达时间离当前时间最接近的时间事件 
    time_event = aeSearchNearestTime()
    
    # 计算最接近的时间事件距离到达还有多少亳秒
    remaind_ms = time_event.when - unix_ts_now()
    # 如果事件已到达，那么 remaind_ms 的值可能为负数，设置为 0
    if remaind_ms &amp;lt; 0:
        remaind_ms = 0
	
    # 根据 remaind_ms 的值，创建 timeval 结构
	timeval = create_timeval_with_ms(remaind_ms) 
    # 【阻塞并等待文件事件】产生，最大阻塞时间由传入的timeval结构决定，remaind_ms的值为0时调用后马上返回，不阻塞
    aeApiPoll(timeval)
    
    # 处理所有已产生的文件事件
	processFileEvents() 
	# 处理所有已到达的时间事件
	processTimeEvents()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事件的调度和执行规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定，可以避免服务器对时间事件进行频繁的轮询（忙等待），也可以确保 aeApiPoll 函数不会阻塞过长时间&lt;/li&gt;
&lt;li&gt;对文件事件和时间事件的处理都是&lt;strong&gt;同步、有序、原子地执行&lt;/strong&gt;，服务器不会中途中断事件处理，也不会对事件进行抢占，所以两种处理器都要尽可地减少程序的阻塞时间，并在有需要时&lt;strong&gt;主动让出执行权&lt;/strong&gt;，从而降低事件饥饿的可能性
&lt;ul&gt;
&lt;li&gt;命令回复处理器在写入字节数超过了某个预设常量，就会主动用 break 跳出写入循环，将余下的数据留到下次再写&lt;/li&gt;
&lt;li&gt;时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;时间事件在文件事件之后执行，并且事件之间不会出现抢占，所以时间事件的实际处理时间通常会比设定的到达时间稍晚&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;多线程&lt;/h4&gt;
&lt;p&gt;Redis6.0 引入多线程主要是为了提高网络 IO 读写性能，因为这是 Redis 的一个性能瓶颈（Redis 的瓶颈主要受限于内存和网络），多线程只是用来&lt;strong&gt;处理网络数据的读写和协议解析&lt;/strong&gt;， 执行命令仍然是单线程顺序执行，因此不需要担心线程安全问题。&lt;/p&gt;
&lt;p&gt;Redis6.0 的多线程默认是禁用的，只使用主线程。如需开启需要修改 redis 配置文件 &lt;code&gt;redis.conf&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;io-threads-do-reads yesCopy to clipboardErrorCopied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启多线程后，还需要设置线程数，否则是不生效的，同样需要修改 redis 配置文件 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;io-threads 4 #官网建议4核的机器建议设置为2或3个线程，8核的建议设置为6个线程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-多线程.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Redis 服务器是典型的一对多程序，一个服务器可以与多个客户端建立网络连接，服务器对每个连接的客户端建立了相应的 redisClient 结构（客户端状态，&lt;strong&gt;在服务器端的存储结构&lt;/strong&gt;），保存了客户端当前的状态信息，以及执行相关功能时需要用到的数据结构&lt;/p&gt;
&lt;p&gt;Redis 服务器状态结构的 clients 属性是一个链表，这个链表保存了所有与服务器连接的客户端的状态结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 一个链表，保存了所有客户端状态
    list *clients;
    
    //...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%9C%8D%E5%8A%A1%E5%99%A8clients%E9%93%BE%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;数据结构&lt;/h4&gt;
&lt;h5&gt;redisClient&lt;/h5&gt;
&lt;p&gt;客户端的数据结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisClient {
    //...
    
    // 套接字
    int fd;
    // 名字
    robj *name;
    // 标志
    int flags;
    
    // 输入缓冲区
    sds querybuf;
    // 输出缓冲区 buf 数组
    char buf[REDIS_REPLY_CHUNK_BYTES];
    // 记录了 buf 数组目前已使用的字节数量
    int bufpos; 
    // 可变大小的输出缓冲区，链表 + 字符串对象
    list *reply;
    
    // 命令数组
    rboj **argv;
    // 命令数组的长度
   	int argc;
    // 命令的信息
    struct redisCommand  *cmd;
    
    // 是否通过身份验证
    int authenticated;
    
    // 创建客户端的时间
    time_t ctime;
    // 客户端与服务器最后一次进行交互的时间
    time_t lastinteraction;
    // 输出缓冲区第一次到达软性限制 (soft limit) 的时间
    time_t obuf_soft_limit_reached_time;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端状态包括两类属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一类是比较通用的属性，这些属性很少与特定功能相关，无论客户端执行的是什么工作，都要用到这些属性&lt;/li&gt;
&lt;li&gt;另一类是和特定功能相关的属性，比如操作数据库时用到的 db 属性和 dict id 属性，执行事务时用到的 mstate 属性，以及执行 WATCH 命令时用到的 watched_keys 属性等，代码中没有列出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;套接字&lt;/h5&gt;
&lt;p&gt;客户端状态的 fd 属性记录了客户端正在使用的套接字描述符，根据客户端类型的不同，fd 属性的值可以是 -1 或者大于 -1 的整数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;伪客户端 (fake client) 的 fd 属性的值为 -1，命令请求来源于 AOF 文件或者 Lua 脚本，而不是网络，所以不需要套接字连接&lt;/li&gt;
&lt;li&gt;普通客户端的 fd 属性的值为大于 -1 的整数，因为合法的套接字描述符不能是 -1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行 &lt;code&gt;CLIENT list&lt;/code&gt; 命令可以列出目前所有连接到服务器的普通客户端，不包括伪客户端&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;名字&lt;/h5&gt;
&lt;p&gt;在默认情况下，一个连接到服务器的客户端是没有名字的，使用 &lt;code&gt;CLIENT setname&lt;/code&gt; 命令可以为客户端设置一个名字&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;标志&lt;/h5&gt;
&lt;p&gt;客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态，每个标志使用一个常量表示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flags 的值可以是单个标志：&lt;code&gt;flags = &amp;lt;flag&amp;gt; &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;flags 的值可以是多个标志的二进制：&lt;code&gt;flags = &amp;lt;flagl&amp;gt; | &amp;lt;flag2&amp;gt; | ... &lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一部分标志记录&lt;strong&gt;客户端的角色&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REDIS_MASTER 表示客户端是一个从服务器，REDIS_SLAVE 表示客户端是一个从服务器，在主从复制时使用&lt;/li&gt;
&lt;li&gt;REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器，主服务器不能使用 PSYNC 命令与该从服务器进行同步，这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用&lt;/li&gt;
&lt;li&gt;REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一部分标志记录目前&lt;strong&gt;客户端所处的状态&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端&lt;/li&gt;
&lt;li&gt;REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞&lt;/li&gt;
&lt;li&gt;REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离，在 REDIS_BLOCKED 标志打开的情况下使用&lt;/li&gt;
&lt;li&gt;REDIS_MULTI 标志表示客户端正在执行事务&lt;/li&gt;
&lt;li&gt;REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改&lt;/li&gt;
&lt;li&gt;.....&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;缓冲区&lt;/h5&gt;
&lt;p&gt;客户端状态的输入缓冲区用于保存客户端发送的命令请求，输入缓冲区的大小会根据输入内容动态地缩小或者扩大，但最大大小不能超过 1GB，否则服务器将关闭这个客户端，比如执行 &lt;code&gt;SET key value &lt;/code&gt;，那么缓冲区 querybuf 的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n # 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出缓冲区是服务器用于保存执行客户端命令所得的命令回复，每个客户端都有两个输出缓冲区可用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是固定大小的缓冲区，保存长度比较小的回复，比如 OK、简短的字符串值、整数值、错误回复等&lt;/li&gt;
&lt;li&gt;一个是可变大小的缓冲区，保存那些长度比较大的回复， 比如一个非常长的字符串值或者一个包含了很多元素的集合等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组，bufpos 属性记录了 buf 数组目前已使用的字节数量，当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里，服务器就会开始使用可变大小的缓冲区&lt;/p&gt;
&lt;p&gt;通过使用 reply 链表连接多个字符串对象，可以为客户端保存一个非常长的命令回复，而不必受到固定大小缓冲区 16KB 大小的限制&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8F%AF%E5%8F%98%E8%BE%93%E5%87%BA%E7%BC%93%E5%86%B2%E5%8C%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;命令&lt;/h5&gt;
&lt;p&gt;服务器对 querybuf 中的命令请求的内容进行分析，得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;argv 属性是一个数组，数组中的每项都是字符串对象，其中 argv[0] 是要执行的命令，而之后的其他项则是命令的参数&lt;/li&gt;
&lt;li&gt;argc 属性负责记录 argv 数组的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-命令数组.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;服务器将根据项 argv[0] 的值，在命令表中查找命令所对应的命令的 redisCommand，将客户端状态的 cmd 指向该结构&lt;/p&gt;
&lt;p&gt;命令表是一个字典结构，键是 SDS 结构保存命令的名字；值是命令所对应的 redisCommand 结构，保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-命令查找.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;验证&lt;/h5&gt;
&lt;p&gt;客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authenticated 值为 0，表示客户端未通过身份验证&lt;/li&gt;
&lt;li&gt;authenticated 值为 1，表示客户端已通过身份验证&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当客户端 authenticated = 0 时，除了 AUTH 命令之外， 客户端发送的所有其他命令都会被服务器拒绝执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; PING 
(error) NOAUTH Authentication required.
redis&amp;gt; AUTH 123321 
OK
redis&amp;gt; PING 
PONG 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;时间&lt;/h5&gt;
&lt;p&gt;ctime 属性记录了创建客户端的时间，这个时间可以用来计算客户端与服务器已经连接了多少秒，&lt;code&gt;CLIENT list&lt;/code&gt; 命令的 age 域记录了这个秒数&lt;/p&gt;
&lt;p&gt;lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间，互动可以是客户端向服务器发送命令请求，也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长， 就是距离客户端与服务器最后一次进行互动已经过去了多少秒，&lt;code&gt;CLIENT list&lt;/code&gt; 命令的 idle 域记录了这个秒数&lt;/p&gt;
&lt;p&gt;obuf_soft_limit_reached_time 属性记录了&lt;strong&gt;输出缓冲区第一次到达软性限制&lt;/strong&gt; (soft limit) 的时间&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;h5&gt;创建&lt;/h5&gt;
&lt;p&gt;服务器使用不同的方式来创建和关闭不同类型的客户端&lt;/p&gt;
&lt;p&gt;如果客户端是通过网络连接与服务器进行连接的普通客户端，那么在客户端使用 connect 函数连接到服务器时，服务器就会调用连接应答处理器为客户端创建相应的客户端状态，并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%9C%8D%E5%8A%A1%E5%99%A8clients%E9%93%BE%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端，并将伪客户端关联在服务器状态的 lua_client 属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 保存伪客户端
    redisClient *lua_client；
    
    //...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lua_client 伪客户端在服务器运行的整个生命周期会一直存在，只有服务器被关闭时，这个客户端才会被关闭&lt;/p&gt;
&lt;p&gt;载入 AOF 文件时， 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端，并在载入完成之后，关闭这个伪客户端&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;关闭&lt;/h5&gt;
&lt;p&gt;一个普通客户端可以因为多种原因而被关闭：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端进程退出或者被杀死，那么客户端与服务器之间的网络连接将被关闭，从而造成客户端被关闭&lt;/li&gt;
&lt;li&gt;客户端向服务器发送了带有不符合协议格式的命令请求，那么这个客户端会&lt;strong&gt;被服务器关闭&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;客户端是 &lt;code&gt;CLIENT KILL&lt;/code&gt; 命令的目标&lt;/li&gt;
&lt;li&gt;如果用户为服务器设置了 timeout 配置选项，那么当客户端的空转时间超过该值时将被关闭，特殊情况不会被关闭：
&lt;ul&gt;
&lt;li&gt;客户端是主服务器（REDIS_MASTER ）或者从服务器（打开了 REDIS_SLAVE 标志）&lt;/li&gt;
&lt;li&gt;正在被 BLPOP 等命令阻塞（REDIS_BLOCKED）&lt;/li&gt;
&lt;li&gt;正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;客户端发送的命令请求的大小超过了输入缓冲区的限制大小（默认为 1GB）&lt;/li&gt;
&lt;li&gt;发送给客户端的命令回复的大小超过了输出缓冲区的限制大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理论上来说，可变缓冲区可以保存任意长的命令回复，但是为了回复过大占用过多的服务器资源，服务器会时刻检查客户端的输出缓冲区的大小，并在缓冲区的大小超出范围时，执行相应的限制操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬性限制 (hard limit)：输出缓冲区的大小超过了硬性限制所设置的大小，那么服务器会关闭客户端（serverCron 函数中执行），积存在输出缓冲区中的所有内容会被&lt;strong&gt;直接释放&lt;/strong&gt;，不会返回给客户端&lt;/li&gt;
&lt;li&gt;软性限制 (soft limit)：输出缓冲区的大小超过了软性限制所设置的大小，小于硬性限制的大小，服务器的操作：
&lt;ul&gt;
&lt;li&gt;用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间，继续监视客户端&lt;/li&gt;
&lt;li&gt;如果输出缓冲区的大小一直超出软性限制，并且持续时间超过服务器设定的时长，那么服务器将关闭客户端&lt;/li&gt;
&lt;li&gt;如果在指定时间内不再超出软性限制，那么客户端就不会被关闭，并且 o_s_l_r_t 属性清零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制，格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client-output-buffer-limit &amp;lt;class&amp;gt; &amp;lt;hard limit&amp;gt; &amp;lt;soft limit&amp;gt; &amp;lt;soft seconds&amp;gt;

client-output-buffer-limit normal 0 0 0 
client-output-buffer-limit slave 256mb 64mb 60 
client-output-buffer-limit pubsub 32mb 8mb 60
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一行：将普通客户端的硬性限制和软性限制都设置为 0，表示不限制客户端的输出缓冲区大小&lt;/li&gt;
&lt;li&gt;第二行：将从服务器客户端的硬性限制设置为 256MB，软性限制设置为 64MB，软性限制的时长为 60 秒&lt;/li&gt;
&lt;li&gt;第三行：将执行发布与订阅功能的客户端的硬性限制设置为 32MB，软性限制设置为 8MB，软性限制的时长为 60 秒&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;服务器&lt;/h3&gt;
&lt;h4&gt;执行流程&lt;/h4&gt;
&lt;p&gt;Redis 服务器与多个客户端建立网络连接，处理客户端发送的命令请求，在数据库中保存客户端执行命令所产生的数据，并通过资源管理来维持服务器自身的运转，所以一个命令请求从发送到获得回复的过程中，客户端和服务器需要完成一系列操作&lt;/p&gt;
&lt;h5&gt;命令请求&lt;/h5&gt;
&lt;p&gt;Redis 服务器的命令请求来自 Redis 客户端，当用户在客户端中键入一个命令请求时，客户端会将这个命令请求转换成协议格式，通过连接到服务器的套接字，将协议格式的命令请求发送给服务器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET KEY VALUE -&amp;gt;	# 命令
*3\r\nS3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n	# 协议格式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当客户端与服务器之间的连接套接字因为客户端的写入而变得可读，服务器调用&lt;strong&gt;命令请求处理器&lt;/strong&gt;来执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取套接字中协议格式的命令请求，并将其保存到客户端状态的输入缓冲区里面&lt;/li&gt;
&lt;li&gt;对输入缓冲区中的命令请求进行分析，提取出命令请求中包含的命令参数以及命令参数的个数，然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里&lt;/li&gt;
&lt;li&gt;调用命令执行器，执行客户端指定的命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后客户端接收到协议格式的命令回复之后，会将这些回复转换成用户可读的格式打印给用户观看，至此整体流程结束&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;命令执行&lt;/h5&gt;
&lt;p&gt;命令执行器开始对命令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查找命令：首先根据客户端状态的 argv[0] 参数，在&lt;strong&gt;命令表 (command table)&lt;/strong&gt; 中查找参数所指定的命令，并将找到的命令保存到客户端状态的 cmd 属性里面，是一个 redisCommand 结构&lt;/p&gt;
&lt;p&gt;命令查找算法与字母的大小写无关，所以命令名字的大小写不影响命令表的查找结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行预备操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查客户端状态的 cmd 指针是否指向 NULL，根据 redisCommand 检查请求参数的数量是否正确&lt;/li&gt;
&lt;li&gt;检查客户端是否通过身份验证&lt;/li&gt;
&lt;li&gt;如果服务器打开了 maxmemory 功能，执行命令之前要先检查服务器的内存占用，在有需要时进行内存回收（&lt;strong&gt;逐出算法&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;如果服务器上一次执行 BGSAVE 命令出错，并且服务器打开了 stop-writes-on-bgsave-error 功能，那么如果本次执行的是写命令，服务会拒绝执行，并返回错误&lt;/li&gt;
&lt;li&gt;如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道，那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命令&lt;/li&gt;
&lt;li&gt;如果服务器正在进行载入数据，只有 sflags 带有 1 标识（比如 INFO、SHUTDOWN、PUBLISH等）的命令才会被执行&lt;/li&gt;
&lt;li&gt;如果服务器执行 Lua 脚本而超时并进入阻塞状态，那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令&lt;/li&gt;
&lt;li&gt;如果客户端正在执行事务，那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令，其他命令都会被&lt;strong&gt;放进事务队列&lt;/strong&gt;中&lt;/li&gt;
&lt;li&gt;如果服务器打开了监视器功能，那么会将要执行的命令和参数等信息发送给监视器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用命令的实现函数：被调用的函数会执行指定的操作并产生相应的命令回复，回复会被保存在客户端状态的输出缓冲区里面（buf 和 reply 属性），然后实现函数还会&lt;strong&gt;为客户端的套接字关联命令回复处理器&lt;/strong&gt;，这个处理器负责将命令回复返回给客户端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行后续工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果服务器开启了慢查询日志功能，那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志&lt;/li&gt;
&lt;li&gt;根据执行命令所耗费的时长，更新命令的 redisCommand 结构的 milliseconds 属性，并将命令 calls 计数器的值增一&lt;/li&gt;
&lt;li&gt;如果服务器开启了 AOF 持久化功能，那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面&lt;/li&gt;
&lt;li&gt;如果有其他从服务器正在复制当前这个服务器，那么服务器会将执行的命令传播给所有从服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将命令回复发送给客户端：客户端&lt;strong&gt;套接字变为可写状态&lt;/strong&gt;时，服务器就会执行命令回复处理器，将客户端输出缓冲区中的命令回复发送给客户端，发送完毕之后回复处理器会清空客户端状态的输出缓冲区，为处理下一个命令请求做好准备&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Command&lt;/h5&gt;
&lt;p&gt;每个 redisCommand 结构记录了一个Redis 命令的实现信息，主要属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisCommand {
    // 命令的名字，比如&quot;set&quot;
    char *name;
    
    // 函数指针，指向命令的实现函数，比如setCommand
    // redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c)
    redisCommandProc *proc;
    
    // 命令参数的个数，用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。
    // 注意命令的名字本身也是一个参数，比如 SET msg &quot;hello&quot;，命令的参数是&quot;SET&quot;、&quot;msg&quot;、&quot;hello&quot; 三个
	int arity;
    
    // 字符串形式的标识值，这个值记录了命令的属性，，
    // 比如这个命令是写命令还是读命令，这个命令是否允许在载入数据时使用，是否允许在Lua脚本中使用等等
    char *sflags;
    
    // 对sflags标识进行分析得出的二进制标识，由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性
    // 而不是sflags属性，因为对二进制标识的检查可以方便地通过&amp;amp; ^ ~ 等操作来完成
    int flags;
    
    // 服务器总共执行了多少次这个命令
    long long calls;
    
    // 服务器执行这个命令所耗费的总时长
    long long milliseconds;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;serverCron&lt;/h4&gt;
&lt;h5&gt;基本介绍&lt;/h5&gt;
&lt;p&gt;Redis 服务器以周期性事件的方式来运行 serverCron 函数，服务器初始化时读取配置 server.hz 的值，默认为 10，代表每秒钟执行 10 次，即&lt;strong&gt;每隔 100 毫秒执行一次&lt;/strong&gt;，执行指令 info server 可以查看&lt;/p&gt;
&lt;p&gt;serverCron 函数负责定期对自身的资源和状态进行检查和调整，从而确保服务器可以长期、稳定地运行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新服务器的各类统计信息，比如时间、内存占用、 数据库占用情况等&lt;/li&gt;
&lt;li&gt;清理数据库中的过期键值对&lt;/li&gt;
&lt;li&gt;关闭和清理连接失效的客户端&lt;/li&gt;
&lt;li&gt;进行 AOF 或 RDB 持久化操作&lt;/li&gt;
&lt;li&gt;如果服务器是主服务器，那么对从服务器进行定期同步&lt;/li&gt;
&lt;li&gt;如果处于集群模式，对集群进行定期同步和连接测试&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;时间缓存&lt;/h5&gt;
&lt;p&gt;Redis 服务器中有很多功能需要获取系统的当前时间，而每次获取系统的当前时间都需要执行一次系统调用，为了减少系统调用的执行次数，服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 保存了秒级精度的系统当前UNIX时间戳
    time_t unixtime;
	// 保存了毫秒级精度的系统当前UNIX时间戳 
    long long mstime;
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;serverCron 函数默认以每 100 毫秒一次的频率更新两个属性，所以属性记录的时间的精确度并不高&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间（uptime）这类对时间精确度要求不高的功能上&lt;/li&gt;
&lt;li&gt;对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说，服务器还是会再次执行系统调用，从而获得最准确的系统当前时间&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;LRU 时钟&lt;/h5&gt;
&lt;p&gt;服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 默认每10秒更新一次的时钟缓存，用于计算键的空转(idle)时长。 
    unsigned lruclock:22; 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 Redis 对象都会有一个 lru 属性， 这个 lru 属性保存了对象最后一次被命令访问的时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisObiect {
	unsigned lru:22; 
} robj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当服务器要计算一个数据库键的空转时间（即数据库键对应的值对象的空转时间），程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间&lt;/p&gt;
&lt;p&gt;serverCron 函数默认以每 100 毫秒一次的频率更新这个属性，所以得出的空转时间也是模糊的&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;命令次数&lt;/h5&gt;
&lt;p&gt;serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行，函数功能是以&lt;strong&gt;抽样计算&lt;/strong&gt;的方式，估算并记录服务器在最近一秒钟处理的命令请求数量，这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; INFO stats
# Stats 
instantaneous_ops_per_sec:6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间，以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数，计算出两次函数调用期间，服务器平均每毫秒处理了多少个命令请求，该值乘以 1000 得到每秒内的执行命令的估计值，放入 ops_sec_samples 环形数组里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 上一次进行抽样的时间
	long long ops_sec_last_sample_time;
    // 上一次抽样时，服务器已执行命令的数量 
    long long ops_sec_last_sample_ops;
    // REDIS_OPS_SEC_SAMPLES 大小（默认值为16)的环形数组，数组的每一项记录一次的抽样结果
    long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
    // ops_sec_samples数组的索引值，每次抽样后将值自增一，值为16时重置为0，让数组成为一个环形数组
    int ops_sec_idx;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;内存峰值&lt;/h5&gt;
&lt;p&gt;服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小，循环函数每次执行时都会查看服务器当前使用的内存数量，并与 stat_peak_memory 保存的数值进行比较，设置为较大的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 已使用内存峰值
    size_t stat_peak_memory;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; INFO memory 
# Memory 
...
used_memory_peak:501824 
used_memory_peak_human:490.06K
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;SIGTERM&lt;/h5&gt;
&lt;p&gt;服务器启动时，Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数，该信号处理器负责在服务器接到 SIGTERM 信号时，打开服务器状态的 shutdown_asap 标识&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 关闭服务器的标识：值为1时关闭服务器，值为0时不做操作
    int shutdown_asap;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次 serverCron 函数运行时，程序都会对服务器状态的 shutdown_asap 属性进行检查，并根据属性的值决定是否关闭服务器&lt;/p&gt;
&lt;p&gt;服务器在接到 SIGTERM 信号之后，关闭服务器并打印相关日志的过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ... 
[6794] 14 Nov 21:28:10.108 # User requested shutdown ... 
[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting. 
[6794) 14 Nov 21:28:10.161 * DB saved on disk 
[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ... 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;管理资源&lt;/h5&gt;
&lt;p&gt;serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数，进行管理客户端资源和数据库资源&lt;/p&gt;
&lt;p&gt;clientsCron 函数对一定数量的客户端进行以下两个检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果客户端与服务器之间的连接巳经超时（很长一段时间客户端和服务器都没有互动），那么程序释放这个客户端&lt;/li&gt;
&lt;li&gt;如果客户端在上一次执行命令请求之后，输入缓冲区的大小超过了一定的长度，那么程序会释放客户端当前的输入缓冲区，并重新创建一个默认大小的输入缓冲区，从而防止客户端的输入缓冲区耗费了过多的内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;databasesCron 函数会对服务器中的一部分数据库进行检查，删除其中的过期键，并在有需要时对字典进行收缩操作&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;持久状态&lt;/h5&gt;
&lt;p&gt;服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 记录执行BGSAVE命令的子进程的ID，如果服务器没有在执行BGSAVE，那么这个属性的值为-1
    pid_t rdb_child_pid;
    // 记录执行BGREWRITEAOF命令的子进程的ID，如果服务器没有在执行那么这个属性的值为-1
    pid_t aof_child_pid
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;serverCron 函数执行时，会检查两个属性的值，只要其中一个属性的值不为 -1，程序就会执行一次 wait3 函数，检查子进程是否有信号发来服务器进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有信号到达，那么表示新的 RDB 文件已经生成或者 AOF 重写完毕，服务器需要进行相应命令的后续操作，比如用新的 RDB 文件替换现有的 RDB 文件，用重写后的 AOF 文件替换现有的 AOF 文件&lt;/li&gt;
&lt;li&gt;如果没有信号到达，那么表示持久化操作未完成，程序不做动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果两个属性的值都为 -1，表示服务器没有进行持久化操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看是否有 BGREWRITEAOF 被延迟，然后执行 AOF 后台重写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看服务器的自动保存条件是否已经被满足，并且服务器没有在进行持久化，就开始一次新的 BGSAVE 操作&lt;/p&gt;
&lt;p&gt;因为条件 1 可能会引发一次 AOF，所以在这个检查中会再次确认服务器是否已经在执行持久化操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查服务器设置的 AOF 重写条件是否满足，条件满足并且服务器没有进行持久化，就进行一次 AOF 重写&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果服务器开启了 AOF 持久化功能，并且 AOF 缓冲区里还有待写入的数据， 那么 serverCron 函数会调用相应的程序，将 AOF 缓冲区中的内容写入到 AOF 文件里&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;延迟执行&lt;/h5&gt;
&lt;p&gt;在服务器执行 BGSAVE 命令的期间，如果客户端发送 BGREWRITEAOF 命令，那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后，用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 如果值为1，那么表示有 BGREWRITEAOF命令被延迟了
    int aof_rewrite_scheduled;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行，如果这两个命令都没在执行，并且 aof_rewrite_scheduled 属性的值为 1，那么服务器就会执行之前被推延的 BGREWRITEAOF 命令&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;执行次数&lt;/h5&gt;
&lt;p&gt;服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // serverCron 函数每执行一次，这个属性的值就增 1
    int cronloops;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;缓冲限制&lt;/h5&gt;
&lt;p&gt;服务器会关闭那些输入或者输出&lt;strong&gt;缓冲区大小超出限制&lt;/strong&gt;的客户端&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;h5&gt;初始结构&lt;/h5&gt;
&lt;p&gt;一个 Redis 服务器从启动到能够接受客户端的命令请求，需要经过一系列的初始化和设置过程&lt;/p&gt;
&lt;p&gt;第一步：创建一个 redisServer 类型的实例变量 server 作为服务器的状态，并为结构中的各个属性设置默认值，由 initServerConfig 函数进行初始化一般属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件&lt;/li&gt;
&lt;li&gt;初始化服务器的 LRU 时钟，创建命令表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二步：载入配置选项，用户可以通过给定配置参数或者指定配置文件，对 server 变量相关属性的默认值进行修改&lt;/p&gt;
&lt;p&gt;第三步：初始化服务器数据结构（除了命令表之外），因为服务器&lt;strong&gt;必须先载入用户指定的配置选项才能正确地对数据结构进行初始化&lt;/strong&gt;，所以载入配置完成后才进性数据结构的初始化，服务器将调用 initServer 函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;server.clients 链表，记录了的客户端的状态结构；server.db 数组，包含了服务器的所有数据库&lt;/li&gt;
&lt;li&gt;用于保存频道订阅信息的 server.pubsub_channels 字典， 以及保存模式订阅信息的 server.pubsub_patterns 链表&lt;/li&gt;
&lt;li&gt;用于执行 Lua 脚本的 Lua 环境 server.lua&lt;/li&gt;
&lt;li&gt;保存慢查询日志的 server.slowlog 属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;initServer 还进行了非常重要的设置操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为服务器设置进程信号处理器&lt;/li&gt;
&lt;li&gt;创建共享对象，包含 OK、ERR、&lt;strong&gt;整数 1 到 10000 的字符串对象&lt;/strong&gt;等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打开服务器的监听端口&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为 serverCron 函数创建时间事件&lt;/strong&gt;， 等待服务器正式运行时执行 serverCron 函数&lt;/li&gt;
&lt;li&gt;如果 AOF 持久化功能已经打开，那么打开现有的 AOF 文件，如果 AOF 文件不存在，那么创建并打开一个新的 AOF 文件 ，为 AOF 写入做好准备&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化服务器的后台 I/O 模块&lt;/strong&gt;（BIO）, 为将来的 I/O 操作做好准备&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 initServer 函数执行完毕之后， 服务器将用 ASCII 字符在日志中打印出 Redis 的图标， 以及 Redis 的版本号信息&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;还原状态&lt;/h5&gt;
&lt;p&gt;在完成了对服务器状态的初始化之后，服务器需要载入RDB文件或者AOF 文件， 并根据文件记录的内容来还原服务器的数据库状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果服务器启用了 AOF 持久化功能，那么服务器使用 AOF 文件来还原数据库状态&lt;/li&gt;
&lt;li&gt;如果服务器没有启用 AOF 持久化功能，那么服务器使用 RDB 文件来还原数据库状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当服务器完成数据库状态还原工作之后，服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;驱动循环&lt;/h5&gt;
&lt;p&gt;在初始化的最后一步，服务器将打印出以下日志，并开始&lt;strong&gt;执行服务器的事件循环&lt;/strong&gt;（loop）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器现在开始可以接受客户端的连接请求，并处理客户端发来的命令请求了&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;慢日志&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求，通过产生的日志来监视和优化查询速度&lt;/p&gt;
&lt;p&gt;服务器配置有两个和慢查询日志相关的选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上&lt;/li&gt;
&lt;li&gt;slowlog-max-len 选项指定服务器最多保存多少条慢查询日志&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器使用先进先出 FIFO 的方式保存多条慢查询日志，当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时，在添加一条新的慢查询日志之前，会先将最旧的一条慢查询日志删除&lt;/p&gt;
&lt;p&gt;配置选项可以通过 CONFIG SET option value 命令进行设置&lt;/p&gt;
&lt;p&gt;常用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SLOWLOG GET [n]	# 查看 n 条服务器保存的慢日志
SLOWLOG LEN		# 查看日志数量
SLOWLOG RESET	# 清除所有慢查询日志
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;日志保存&lt;/h4&gt;
&lt;p&gt;服务器状态中包含了慢查询日志功能有关的属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
	// 下一条慢查询日志的ID
	long long slowlog_entry_id;
    
	// 保存了所有慢查询日志的链表
	list *slowlog;
    
	// 服务器配置选项的值 
    long long slowlog-log-slower-than;
	// 服务器配置选项的值
	unsigned long slowlog_max_len;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slowlog_entry_id 属性的初始值为 0，每当创建一条新的慢查询日志时，这个属性就会用作新日志的 id 值，之后该属性增一&lt;/p&gt;
&lt;p&gt;slowlog 链表保存了服务器中的所有慢查询日志，链表中的每个节点是一个 slowlogEntry 结构， 代表一条慢查询日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct slowlogEntry {
    // 唯一标识符
    long long id;
   	// 命令执行时的时间，格式为UNIX时间戳
    time_t time;
	// 执行命令消耗的时间，以微秒为单位 
    long long duration;
	// 命令与命令参数
	robj **argv;
	// 命令与命令参数的数量
	int argc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;添加日志&lt;/h4&gt;
&lt;p&gt;在每次执行命令的前后，程序都会记录微秒格式的当前 UNIX 时间戳，两个时间之差就是执行命令所耗费的时长，函数会检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果是的话，就为命令创建一个新的日志，并将新日志添加到 slowlog 链表的表头&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度，如果是将多出来的日志从 slowlog 链表中删除掉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 redisServer. slowlog_entry_id 的值增 1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;数据结构&lt;/h2&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;h4&gt;SDS&lt;/h4&gt;
&lt;p&gt;Redis 构建了简单动态字符串（SDS）的数据类型，作为 Redis 的默认字符串表示，包含字符串的键值对在底层都是由 SDS 实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sdshdr {
    // 记录buf数组中已使用字节的数量，等于 SDS 所保存字符串的长度
    int len;
    
	// 记录buf数组中未使用字节的数量
    int free;
    
    // 【字节】数组，用于保存字符串（不是字符数组）
    char buf[];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SDS 遵循 C 字符串&lt;strong&gt;以空字符结尾&lt;/strong&gt;的惯例，保存空字符的 1 字节不计算在 len 属性，SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾，所以空字符对于 SDS 的使用者来说是完全透明的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;对比&lt;/h4&gt;
&lt;p&gt;常数复杂度获取字符串长度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C 字符串不记录自身的长度，获取时需要遍历整个字符串，遇到空字符串为止，时间复杂度为 O(N)&lt;/li&gt;
&lt;li&gt;SDS 获取字符串长度的时间复杂度为 O(1)，设置和更新 SDS 长度由函数底层自动完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;杜绝缓冲区溢出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C 字符串调用 strcat 函数拼接字符串时，如果字符串内存不够容纳目标字符串，就会造成缓冲区溢出（Buffer Overflow）&lt;/p&gt;
&lt;p&gt;s1 和 s2 是内存中相邻的字符串，执行 &lt;code&gt;strcat(s1, &quot; Cluster&quot;)&lt;/code&gt;（有空格）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E9%97%AE%E9%A2%98.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SDS 空间分配策略：当对 SDS 进行修改时，首先检查 SDS 的空间是否满足修改所需的要求， 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小，然后执行实际的修改操作， 避免了缓冲区溢出的问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;二进制安全：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C 字符串中的字符必须符合某种编码（比如 ASCII）方式，除了字符串末尾以外其他位置不能包含空字符，否则会被误认为是字符串的结尾，所以只能保存文本数据&lt;/li&gt;
&lt;li&gt;SDS 的 API 都是二进制安全的，使用字节数组 buf 保存一系列的二进制数据，&lt;strong&gt;使用 len 属性来判断数据的结尾&lt;/strong&gt;，所以可以保存图片、视频、压缩文件等二进制数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;兼容 C 字符串的函数：SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符，所以可以重用一部分 C 字符串函数库的函数&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存&lt;/h4&gt;
&lt;p&gt;C 字符串&lt;strong&gt;每次&lt;/strong&gt;增长或者缩短都会进行一次内存重分配，拼接操作通过重分配扩展底层数组空间，截断操作通过重分配释放不使用的内存空间，防止出现内存泄露&lt;/p&gt;
&lt;p&gt;SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联，在 SDS 中 buf 数组的长度不一定就是字符数量加一， 数组里面可以包含未使用的字节，字节的数量由 free 属性记录&lt;/p&gt;
&lt;p&gt;内存重分配涉及复杂的算法，需要执行&lt;strong&gt;系统调用&lt;/strong&gt;，是一个比较耗时的操作，SDS 的两种优化策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;空间预分配：当 SDS 需要进行空间扩展时，程序不仅会为 SDS 分配修改所必需的空间， 还会为 SDS 分配额外的未使用空间&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对 SDS 修改之后，SDS 的长度（len 属性）小于 1MB，程序分配和 len 属性同样大小的未使用空间，此时 len 和 free 相等&lt;/p&gt;
&lt;p&gt;s 为 Redis，执行 &lt;code&gt;sdscat(s, &quot; Cluster&quot;)&lt;/code&gt; 后，len 变为 13 字节，所以也分配了 13 字节的 free 空间，总长度变为 27 字节（额外的一字节保存空字符，13 + 13 + 1 = 27）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS%E5%86%85%E5%AD%98%E9%A2%84%E5%88%86%E9%85%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对 SDS 修改之后，SDS 的长度大于等于 1MB，程序会分配 1MB 的未使用空间&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在扩展 SDS 空间前，API 会先检查 free 空间是否足够，如果足够就无需执行内存重分配，所以通过预分配策略，SDS 将连续增长 N 次字符串所需内存的重分配次数从&lt;strong&gt;必定 N 次降低为最多 N 次&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;惰性空间释放：当 SDS 缩短字符串时，程序并不立即使用内存重分配来回收缩短后多出来的字节，而是使用 free 属性将这些字节的数量记录起来，并等待将来复用&lt;/p&gt;
&lt;p&gt;SDS 提供了相应的 API 来真正释放 SDS 的未使用空间，所以不用担心空间惰性释放策略造成的内存浪费问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;链表&lt;/h3&gt;
&lt;p&gt;链表提供了高效的节点重排能力，C 语言并没有内置这种数据结构，所以 Redis 构建了链表数据类型&lt;/p&gt;
&lt;p&gt;链表节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    
    // 后置节点
    struct listNode *next;
    
    // 节点的值
    void *value
} listNode;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多个 listNode 通过 prev 和 next 指针组成&lt;strong&gt;双端链表&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;list 链表结构：提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    
    // 链表所包含的节点数量
    unsigned long len;
    
    // 节点值复制函数，用于复制链表节点所保存的值
    void *(*dup) (void *ptr);
    // 节点值释放函数，用于释放链表节点所保存的值
    void (*free) (void *ptr);
    // 节点值对比函数，用于对比链表节点所保存的值和另一个输入值是否相等
    int (*match) (void *ptr, void *key);
} list;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%93%BE%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redis 链表的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双端：链表节点带有 prev 和 next 指针，获取某个节点的前置节点和后置节点的时间复杂度都是 O(1)&lt;/li&gt;
&lt;li&gt;无环：表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL，对链表的访问以 NULL 为终点&lt;/li&gt;
&lt;li&gt;带表头指针和表尾指针： 通过 list 结构的 head 指针和 tail 指针，获取链表的表头节点和表尾节点的时间复杂度为 O(1)&lt;/li&gt;
&lt;li&gt;带链表长度计数器：使用 len 属性来对 list 持有的链表节点进行计数，获取链表中节点数量的时间复杂度为 O(1)&lt;/li&gt;
&lt;li&gt;多态：链表节点使用 void * 指针来保存节点值， 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数，所以链表可以保存各种&lt;strong&gt;不同类型的值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;字典&lt;/h3&gt;
&lt;h4&gt;哈希表&lt;/h4&gt;
&lt;p&gt;Redis 字典使用的哈希表结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct dictht {
    // 哈希表数组，数组中每个元素指向 dictEntry 结构
	dictEntry **table;
    
	// 哈希表大小，数组的长度
	unsigned long size;
    
	// 哈希表大小掩码，用于计算索引值，总是等于 【size-1】
	unsigned long sizemask;
    
	// 该哈希表已有节点的数量 
	unsigned long used;
} dictht;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈希表节点结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct dictEntry {
    // 键
	void *key;
    
	// 值，可以是一个指针，或者整数
	union {
        void *val;	// 指针
        uint64_t u64;
        int64_t s64;
    }
    
	// 指向下个哈希表节点，形成链表，用来解决冲突问题
    struct dictEntry *next;
} dictEntry;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%93%88%E5%B8%8C%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;字典结构&lt;/h4&gt;
&lt;p&gt;字典，又称为符号表、关联数组、映射（Map），用于保存键值对的数据结构，字典中的每个键都是独一无二的。底层采用哈希表实现，一个哈希表包含多个哈希表节点，每个节点保存一个键值对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct dict {
    // 类型特定函数
    dictType *type;
    
    // 私有数据
    void *privdata;
    
    // 哈希表，数组中的每个项都是一个dictht哈希表，
    // 一般情况下字典只使用 ht[0] 哈希表， ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用
    dictht ht[2];
    
    // rehash 索引，当 rehash 不在进行时，值为 -1
    int rehashidx;
} dict;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;type 属性和 privdata 属性是针对不同类型的键值对， 为创建多态字典而设置的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;type 属性是指向 dictType 结构的指针， 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数， Redis 会为用途不同的字典设置不同的类型特定函数&lt;/li&gt;
&lt;li&gt;privdata 属性保存了需要传给那些类型特定函数的可选参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%AD%97%E5%85%B8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;哈希冲突&lt;/h4&gt;
&lt;p&gt;Redis 使用 MurmurHash 算法来计算键的哈希值，这种算法的优点在于，即使输入的键是有规律的，算法仍能给出一个很好的随机分布性，并且算法的计算速度也非常快&lt;/p&gt;
&lt;p&gt;将一个新的键值对添加到字典里，需要先根据键 key 计算出哈希值，然后进行取模运算（取余）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = hash &amp;amp; dict-&amp;gt;ht[x].sizemask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时，就称这些键发生了哈希冲突（collision）&lt;/p&gt;
&lt;p&gt;Redis 的哈希表使用链地址法（separate chaining）来解决键哈希冲突， 每个哈希表节点都有一个 next 指针，多个节点通过 next 指针构成一个单向链表，被分配到同一个索引上的多个节点可以用这个单向链表连接起来，这就解决了键冲突的问题&lt;/p&gt;
&lt;p&gt;dictEntry 节点组成的链表没有指向链表表尾的指针，为了速度考虑，程序总是将新节点添加到链表的表头位置（&lt;strong&gt;头插法&lt;/strong&gt;），时间复杂度为 O(1)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%AD%97%E5%85%B8%E8%A7%A3%E5%86%B3%E5%93%88%E5%B8%8C%E5%86%B2%E7%AA%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;负载因子&lt;/h4&gt;
&lt;p&gt;负载因子的计算方式：哈希表中的&lt;strong&gt;节点数量&lt;/strong&gt; / 哈希表的大小（&lt;strong&gt;长度&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;load_factor = ht[0].used / ht[0].size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了让哈希表的负载因子（load factor）维持在一个合理的范围之内，当哈希表保存的键值对数量太多或者太少时 ，程序会自动对哈希表的大小进行相应的扩展或者收缩&lt;/p&gt;
&lt;p&gt;哈希表执行扩容的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令，哈希表的负载因子大于等于 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令，哈希表的负载因子大于等于 5&lt;/p&gt;
&lt;p&gt;原因：执行该命令的过程中，Redis 需要创建当前服务器进程的子进程，而大多数操作系统都采用写时复制（copy-on­-write）技术来优化子进程的使用效率，通过提高执行扩展操作的负载因子，尽可能地避免在子进程存在期间进行哈希表扩展操作，可以避免不必要的内存写入操作，最大限度地节约内存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哈希表执行收缩的条件：负载因子小于 0.1（自动执行，servreCron 中检测）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;重新散列&lt;/h4&gt;
&lt;p&gt;扩展和收缩哈希表的操作通过 rehash（重新散列）来完成，步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为字典的 ht[1] 哈希表分配空间，空间大小的分配情况：
&lt;ul&gt;
&lt;li&gt;如果执行的是扩展操作，ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$&lt;/li&gt;
&lt;li&gt;如果执行的是收缩操作，ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值，迁移到 ht[1] 上&lt;/li&gt;
&lt;li&gt;当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后（ht[0] 变为空表），释放 ht[0]，将 ht[1] 设置为 ht[0]，并在 ht[1] 创建一个新的空白哈希表，为下一次 rehash 做准备&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果哈希表里保存的键值对数量很少，rehash 就可以在瞬间完成，但是如果哈希表里数据很多，那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算，可能会导致服务器在一段时间内停止服务&lt;/p&gt;
&lt;p&gt;Redis 对 rehash 做了优化，使 rehash 的动作并不是一次性、集中式的完成，而是分多次，渐进式的完成，又叫&lt;strong&gt;渐进式 rehash&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为 ht[1] 分配空间，此时字典同时持有 ht[0] 和 ht[1] 两个哈希表&lt;/li&gt;
&lt;li&gt;在字典中维护了一个索引计数器变量 rehashidx，并将变量的值设为 0，表示 rehash 正式开始&lt;/li&gt;
&lt;li&gt;在 rehash 进行期间，每次对字典执行增删改查操作时，程序除了执行指定的操作以外，还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1]，rehash 完成之后&lt;strong&gt;将 rehashidx 属性的值增一&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;随着字典操作的不断执行，最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1]，将 rehashidx 属性的值设为 -1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;渐进式 rehash 采用&lt;strong&gt;分而治之&lt;/strong&gt;的方式，将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上，从而避免了集中式 rehash 带来的庞大计算量&lt;/p&gt;
&lt;p&gt;渐进式 rehash 期间的哈希表操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字典的查找、删除、更新操作会在两个哈希表上进行，比如查找一个键会先在 ht[0] 上查找，查找不到就去 ht[1] 继续查找&lt;/li&gt;
&lt;li&gt;字典的添加操作会直接在 ht[1] 上添加，不在 ht[0] 上进行任何添加&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;跳跃表&lt;/h3&gt;
&lt;h4&gt;底层结构&lt;/h4&gt;
&lt;p&gt;跳跃表（skiplist）是一种有序（&lt;strong&gt;默认升序&lt;/strong&gt;）的数据结构，在链表的基础上&lt;strong&gt;增加了多级索引以提升查找的效率&lt;/strong&gt;，索引是占内存的，所以是一个&lt;strong&gt;空间换时间&lt;/strong&gt;的方案，跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找，效率与平衡树相当但是实现更简单&lt;/p&gt;
&lt;p&gt;原始链表中存储的有可能是很大的对象，而索引结点只需要存储关键值和几个指针，并不需要存储对象，因此当节点本身比较大或者元素数量比较多的时候，其优势可以被放大，而缺点（占内存）则可以忽略&lt;/p&gt;
&lt;p&gt;Redis 只在两个地方应用了跳跃表，一个是实现有序集合键，另一个是在集群节点中用作内部数据结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct zskiplist {
    // 表头节点和表尾节点，O(1) 的时间复杂度定位头尾节点
    struct skiplistNode *head, *tail;
    
    // 表的长度，也就是表内的节点数量 (表头节点不计算在内)
    unsigned long length;
    
    // 表中层数最大的节点的层数 (表头节点的层高不计算在内)
    int level
} zskiplist;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;typedef struct zskiplistNode {
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
    
    // 后退指针
    struct zskiplistNode *backward;
    
    // 分值
    double score;
    
    // 成员对象
    robj *obj;
} zskiplistNode;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E8%B7%B3%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;属性分析&lt;/h4&gt;
&lt;p&gt;层：level 数组包含多个元素，每个元素包含指向其他节点的指针。根据幕次定律（power law，越大的数出现的概率越小）&lt;strong&gt;随机&lt;/strong&gt;生成一个介于 1 和 32 之间的值（Redis5 之后最大为 64）作为 level 数组的大小，这个大小就是层的高度，节点的第一层是 level[0] = L1&lt;/p&gt;
&lt;p&gt;前进指针：forward 用于从表头到表尾方向&lt;strong&gt;正序（升序）遍历节点&lt;/strong&gt;，遇到 NULL 停止遍历&lt;/p&gt;
&lt;p&gt;跨度：span 用于记录两个节点之间的距离，用来计算排位（rank）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;两个节点之间的跨度越大相距的就越远，指向 NULL 的所有前进指针的跨度都为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在查找某个节点的过程中，&lt;strong&gt;将沿途访问过的所有层的跨度累计起来，结果就是目标节点在跳跃表中的排位&lt;/strong&gt;，按照上图所示：&lt;/p&gt;
&lt;p&gt;查找分值为 3.0 的节点，沿途经历的层：查找的过程只经过了一个层，并且层的跨度为 3，所以目标节点在跳跃表中的排位为 3&lt;/p&gt;
&lt;p&gt;查找分值为 2.0 的节点，沿途经历的层：经过了两个跨度为 1 的节点，因此可以计算出目标节点在跳跃表中的排位为 2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后退指针：backward 用于从表尾到表头方向&lt;strong&gt;逆序（降序）遍历节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分值：score 属性一个 double 类型的浮点数，跳跃表中的所有节点都按分值从小到大来排序&lt;/p&gt;
&lt;p&gt;成员对象：obj 属性是一个指针，指向一个 SDS 字符串对象。同一个跳跃表中，各个节点保存的&lt;strong&gt;成员对象必须是唯一的&lt;/strong&gt;，但是多个节点保存的分值可以是相同的，分值相同的节点将按照成员对象在字典序中的大小来进行排序（从小到大）&lt;/p&gt;
&lt;p&gt;个人笔记：JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;整数集合&lt;/h3&gt;
&lt;h4&gt;底层结构&lt;/h4&gt;
&lt;p&gt;整数集合（intset）是用于保存整数值的集合数据结构，是 Redis 集合键的底层实现之一&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct intset {
	// 编码方式
	uint32_t encoding;
    
	// 集合包含的元素数量，也就是 contents 数组的长度
	uint32_t length;
    
	// 保存元素的数组
    int8_t contents[];
} intset;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;encoding 取值为三种：INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64&lt;/p&gt;
&lt;p&gt;整数集合的每个元素都是 contents 数组的一个数组项（item），在数组中按值的大小从小到大&lt;strong&gt;有序排列&lt;/strong&gt;，并且数组中&lt;strong&gt;不包含任何重复项&lt;/strong&gt;。虽然 contents 属性声明为 int8_t 类型，但实际上数组并不保存任何 int8_t 类型的值， 真正类型取决于 encoding 属性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%95%B4%E6%95%B0%E9%9B%86%E5%90%88%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明：底层存储结构是数组，所以为了保证有序性和不重复性，每次添加一个元素的时间复杂度是 O(N)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;类型升级&lt;/h4&gt;
&lt;p&gt;整数集合添加的新元素的类型比集合现有所有元素的类型都要长时，需要先进行升级（upgrade），升级流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;根据新元素的类型长度以及集合元素的数量（包括新元素在内），扩展整数集合底层数组的空间大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将底层数组现有的所有元素都转换成与新元素相同的类型，并将转换后的元素放入正确的位置，放置过程保证数组的有序性&lt;/p&gt;
&lt;p&gt;图示 32 * 4 = 128 位，首先将 3 放入索引 2（64 位 - 95 位），然后将 2 放置索引 1，将 1 放置在索引 0，从后向前依次放置在对应的区间，最后放置 65535 元素到索引 3（96 位- 127 位），修改 length 属性为 4&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将新元素添加到底层数组里&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%95%B4%E6%95%B0%E9%9B%86%E5%90%88%E5%8D%87%E7%BA%A7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每次向整数集合添加新元素都可能会引起升级，而每次升级都需要对底层数组中的所有元素进行类型转换，所以向整数集合添加新元素的时间复杂度为 O(N)&lt;/p&gt;
&lt;p&gt;引发升级的新元素的长度总是比整数集合现有所有元素的长度都大，所以这个新元素的值要么就大于所有现有元素，要么就小于所有现有元素，升级之后新元素的摆放位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在新元素小于所有现有元素的情况下，新元素会被放置在底层数组的最开头（索引 0）&lt;/li&gt;
&lt;li&gt;在新元素大于所有现有元素的情况下，新元素会被放置在底层数组的最末尾（索引 length-1）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整数集合升级策略的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;提升整数集合的灵活性：C 语言是静态类型语言，为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面，整数集合可以自动升级底层数组来适应新元素，所以可以随意的添加整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节约内存：要让数组可以同时保存 int16、int32、int64 三种类型的值，可以直接使用 int64_t 类型的数组作为整数集合的底层实现，但是会造成内存浪费，整数集合可以确保升级操作只会在有需要的时候进行，尽量节省内存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整数集合&lt;strong&gt;不支持降级操作&lt;/strong&gt;，一旦对数组进行了升级，编码就会一直保持升级后的状态&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;压缩列表&lt;/h3&gt;
&lt;h4&gt;底层结构&lt;/h4&gt;
&lt;p&gt;压缩列表（ziplist）是 Redis 为了节约内存而开发的，是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型（sequential）数据结构，一个压缩列表可以包含任意多个节点（entry），每个节点可以保存一个字节数组或者一个整数值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zlbytes：uint32_t 类型 4 字节，记录整个压缩列表占用的内存字节数，在对压缩列表进行内存重分配或者计算 zlend 的位置时使用&lt;/li&gt;
&lt;li&gt;zltail：uint32_t 类型 4 字节，记录压缩列表表尾节点距离起始地址有多少字节，通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址&lt;/li&gt;
&lt;li&gt;zllen：uint16_t 类型 2 字节，记录了压缩列表包含的节点数量，当该属性的值小于 UINT16_MAX (65535) 时，该值就是压缩列表中节点的数量；当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出&lt;/li&gt;
&lt;li&gt;entryX：列表节点，压缩列表中的各个节点，&lt;strong&gt;节点的长度由节点保存的内容决定&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;zlend：uint8_t 类型 1 字节，是一个特殊值 0xFF (255)，用于标记压缩列表的末端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E7%A4%BA%E4%BE%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;列表 zlbytes 属性的值为 0x50 (十进制 80)，表示压缩列表的总长为 80 字节，列表 zltail 属性的值为 0x3c (十进制 60)，假设表的起始地址为 p，计算得出表尾节点 entry3 的地址 p + 60&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;列表节点&lt;/h4&gt;
&lt;p&gt;列表节点 entry 的数据结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%8A%82%E7%82%B9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;previous_entry_length：以字节为单位记录了压缩列表中前一个节点的长度，程序可以通过指针运算，根据当前节点的起始地址来计算出前一个节点的起始地址，完成&lt;strong&gt;从表尾向表头遍历&lt;/strong&gt;操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果前一节点的长度小于 254 字节，该属性的长度为 1 字节，前一节点的长度就保存在这一个字节里&lt;/li&gt;
&lt;li&gt;如果前一节点的长度大于等于 254 字节，该属性的长度为 5 字节，其中第一字节会被设置为 0xFE（十进制 254），之后的四个字节则用于保存前一节点的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;encoding：记录了节点的 content 属性所保存的数据类型和长度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;长度为 1 字节、2 字节或者 5 字节&lt;/strong&gt;，值的最高位为 00、01 或者 10 的是字节数组编码，数组的长度由编码除去最高两位之后的其他位记录，下划线 &lt;code&gt;_&lt;/code&gt; 表示留空，而 &lt;code&gt;b&lt;/code&gt;、&lt;code&gt;x&lt;/code&gt; 等变量则代表实际的二进制数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E5%AD%97%E8%8A%82%E6%95%B0%E7%BB%84%E7%BC%96%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;长度为 1 字节，值的最高位为 11 的是整数编码，整数值的类型和长度由编码除去最高两位之后的其他位记录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E6%95%B4%E6%95%B0%E7%BC%96%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;content：每个压缩列表节点可以保存一个字节数组或者一个整数值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字节数组可以是以下三种长度的其中一种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;长度小于等于 $63 (2^6-1)$ 字节的字节数组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;长度小于等于 $16383(2^{14}-1)$ 字节的字节数组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整数值则可以是以下六种长度的其中一种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;4 位长，介于 0 至 12 之间的无符号整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1 字节长的有符号整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3 字节长的有符号整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;int16_t 类型整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;int32_t 类型整数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;int64_t 类型整数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;连锁更新&lt;/h4&gt;
&lt;p&gt;Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新（cascade update）&lt;/p&gt;
&lt;p&gt;假设在一个压缩列表中，有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点，new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节，无法保存新节点 new 的长度，所以要对压缩列表执行空间重分配操作，并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间，所以扩展后 e1 的长度就变成了 254 至 257 字节之间，导致 e2 的  previous_entry_length 属性无法保存 e1 的长度，程序需要不断地对压缩列表执行空间重分配操作，直到 eN 为止&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%BF%9E%E9%94%81%E6%9B%B4%E6%96%B01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;删除节点也可能会引发连锁更新，big.length &amp;gt;= 254，small.length &amp;lt; 254，删除 small 节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%BF%9E%E9%94%81%E6%9B%B4%E6%96%B02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配，每次重分配的最坏复杂度为 O(N)，所以连锁更新的最坏复杂度为 O(N^2)&lt;/p&gt;
&lt;p&gt;说明：尽管连锁更新的复杂度较高，但出现的记录是非常低的，即使出现只要被更新的节点数量不多，就不会对性能造成影响&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;数据类型&lt;/h2&gt;
&lt;h3&gt;redisObj&lt;/h3&gt;
&lt;h4&gt;对象系统&lt;/h4&gt;
&lt;p&gt;Redis 使用对象来表示数据库中的键和值，当在 Redis 数据库中新创建一个键值对时至少会创建两个对象，一个对象用作键值对的键（&lt;strong&gt;键对象&lt;/strong&gt;），另一个对象用作键值对的值（&lt;strong&gt;值对象&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;Redis 中对象由一个 redisObject 结构表示，该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisObiect {
	// 类型
	unsigned type:4;
	// 编码
	unsigned encoding:4;
	// 指向底层数据结构的指针
	void *ptr;
    
    // ....
} robj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 并没有直接使用数据结构来实现键值对数据库，而是基于这些数据结构创建了一个对象系统，包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象，而每种对象又通过不同的编码映射到不同的底层数据结构&lt;/p&gt;
&lt;p&gt;Redis 是一个 Map 类型，其中所有的数据都是采用 key : value 的形式存储，&lt;strong&gt;键对象都是字符串对象&lt;/strong&gt;，而值对象有五种基本类型和三种高级类型对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对一个数据库键执行 TYPE 命令，返回的结果为数据库键对应的值对象的类型，而不是键对象的类型&lt;/li&gt;
&lt;li&gt;对一个数据库键执行 OBJECT ENCODING 命令，查看数据库键对应的值对象的编码&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;命令多态&lt;/h4&gt;
&lt;p&gt;Redis 中用于操作键的命令分为两种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种命令可以对任何类型的键执行，比如说 DEL 、EXPIRE、RENAME、 TYPE 等（基于类型的多态）&lt;/li&gt;
&lt;li&gt;只能对特定类型的键执行，比如 SET 只能对字符串键执行、HSET 对哈希键执行、SADD 对集合键执行，如果类型步匹配会报类型错误： &lt;code&gt;(error) WRONGTYPE Operation against a key holding the wrong kind of value&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 为了确保只有指定类型的键可以执行某些特定的命令，在执行类型特定的命令之前，先通过值对象 redisObject 结构 type 属性检查操作类型是否正确，然后再决定是否执行指定的命令&lt;/p&gt;
&lt;p&gt;对于多态命令，比如列表对象有 ziplist 和 linkedlist 两种实现方式，通过 redisObject 结构 encoding 属性确定具体的编码类型，底层调用对应的 API 实现具体的操作（基于编码的多态）&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存回收&lt;/h4&gt;
&lt;p&gt;对象的整个生命周期可以划分为创建对象、 操作对象、 释放对象三个阶段&lt;/p&gt;
&lt;p&gt;C 语言没有自动回收内存的功能，所以 Redis 在对象系统中构建了引用计数（reference counting）技术实现的内存回收机制，程序可以跟踪对象的引用计数信息，在适当的时候自动释放对象并进行内存回收&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisObiect {
	// 引用计数
	int refcount;
} robj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象的引用计数信息会随着对象的使用状态而不断变化，创建时引用计数 refcount 初始化为 1，每次被一个新程序使用时引用计数加 1，当对象不再被一个程序使用时引用计数值会被减 1，当对象的引用计数值变为 0 时，对象所占用的内存会被释放&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;对象共享&lt;/h4&gt;
&lt;p&gt;对象的引用计数属性带有对象共享的作用，共享对象机制更节约内存，数据库中保存的相同值对象越多，节约的内存就越多&lt;/p&gt;
&lt;p&gt;让多个键共享一个对象的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将数据库键的值指针指向一个现有的值对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将被共享的值对象的引用计数增一&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象共享.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 在初始化服务器时创建一万个（配置文件可以修改）字符串对象，包含了&lt;strong&gt;从 0 到 9999 的所有整数值&lt;/strong&gt;，当服务器需要用到值为 0 到 9999 的字符串对象时，服务器就会使用这些共享对象，而不是新创建对象&lt;/p&gt;
&lt;p&gt;比如创建一个值为 100 的键 A，并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数，会发现值对象的引用计数为 2，引用这个值对象的两个程序分别是持有这个值对象的服务器程序，以及共享这个值对象的键 A&lt;/p&gt;
&lt;p&gt;共享对象在嵌套了字符串对象的对象（linkedlist 编码的列表、hashtable 编码的哈希、zset 编码的有序集合）中也能使用&lt;/p&gt;
&lt;p&gt;Redis 不共享包含字符串对象的原因：验证共享对象和目标对象是否相同的复杂度越高，消耗的 CPU 时间也会越多&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整数值的字符串对象， 验证操作的复杂度为 O(1)&lt;/li&gt;
&lt;li&gt;字符串值的字符串对象， 验证操作的复杂度为 O(N)&lt;/li&gt;
&lt;li&gt;如果共享对象是包含了多个值（或者对象的）对象，比如列表对象或者哈希对象，验证操作的复杂度为 O(N^2)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;空转时长&lt;/h4&gt;
&lt;p&gt;redisObject 结构包含一个 lru 属性，该属性记录了对象最后一次被命令程序访问的时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisObiect {
	unsigned lru:22; 
} robj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OBJECT IDLETIME 命令可以打印出给定键的空转时长，该值就是通过将当前时间减去键的值对象的 lru 时间计算得出的，这个命令在访问键的值对象时，不会修改值对象的 lru 属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; OBJECT IDLETIME msg
(integer) 10
# 等待一分钟
redis&amp;gt; OBJECT IDLETIME msg
(integer) 70
# 访问 msg
redis&amp;gt; GET msg
&quot;hello world&quot;
# 键处于活跃状态，空转时长为 0
redis&amp;gt; OBJECT IDLETIME msg
(integer) 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空转时长的作用：如果服务器开启 maxmemory 选项，并且回收内存的算法为 volatile-lru 或者 allkeys-lru，那么当服务器占用的内存数超过了 maxmemory 所设置的上限值时，空转时长较高的那部分键会优先被服务器释放，从而回收内存（LRU 算法）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;string&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;存储的数据：单个数据，最简单的数据存储类型，也是最常用的数据存储类型，实质上是存一个字符串，string 类型是二进制安全的，可以包含任何数据，比如图片或者序列化的对象&lt;/p&gt;
&lt;p&gt;存储数据的格式：一个存储空间保存一个数据，每一个空间中只能保存一个字符串信息&lt;/p&gt;
&lt;p&gt;存储内容：通常使用字符串，如果字符串以整数的形式展示，可以作为数字操作使用&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-string结构图.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Redis 所有操作都是&lt;strong&gt;原子性&lt;/strong&gt;的，采用&lt;strong&gt;单线程&lt;/strong&gt;机制，命令是单个顺序执行，无需考虑并发带来影响，原子性就是有一个失败则都失败&lt;/p&gt;
&lt;p&gt;字符串对象可以是 int、raw、embstr 三种实现方式&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;p&gt;指令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set key value			#添加/修改数据添加/修改数据
del key					#删除数据
setnx key value			#判定性添加数据，键值为空则设添加
mset k1 v1 k2 v2...		#添加/修改多个数据，m：Multiple
append key value		#追加信息到原始信息后部（如果原始信息存在就追加，否则新建）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;get key					#获取数据，如果不存在，返回空（nil）
mget key1 key2...		#获取多个数据
strlen key				#获取数据字符个数（字符串长度）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置数值数据增加/减少指定范围的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incr key					#key++
incrby key increment		#key+increment
incrbyfloat key increment	#对小数操作
decr key					#key--
decrby key increment		#key-increment
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置数据具有指定的生命周期&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setex key seconds value  		#设置key-value存活时间，seconds单位是秒
psetex key milliseconds value	#毫秒级
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数据操作不成功的反馈与数据正常操作之间的差异&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表示运行结果是否成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(integer) 0  → false ，失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(integer) 1  → true，成功&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表示运行结果值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(integer) 3  → 3 个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(integer) 1  → 1 个&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据未获取到时，对应的数据为（nil），等同于null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据最大存储量&lt;/strong&gt;：512MB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;string 在 Redis 内部存储默认就是一个字符串，当遇到增减类操作 incr，decr 时&lt;strong&gt;会转成数值型&lt;/strong&gt;进行计算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按数值进行操作的数据，如果原始数据不能转成数值，或超越了Redis 数值上限范围，将报错
9223372036854775807（java 中 Long 型数据最大值，Long.MAX_VALUE）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redis 可用于控制数据库表主键 ID，为数据库表主键提供生成策略，保障数据库表的主键唯一性&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;单数据和多数据的选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单数据执行 3 条指令的过程：3 次发送 + 3 次处理 + 3 次返回&lt;/li&gt;
&lt;li&gt;多数据执行 1 条指令的过程：1 次发送 + 3 次处理 + 1 次返回（发送和返回的事件略高于单数据）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/string单数据与多数据操作.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;p&gt;字符串对象的编码可以是 int、raw、embstr 三种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;int：字符串对象保存的是&lt;strong&gt;整数值&lt;/strong&gt;，并且整数值可以用 long 类型来表示，那么对象会将整数值保存在字符串对象结构的 ptr 属性面（将 void * 转换成 long)，并将字符串对象的编码设置为 int（浮点数用另外两种方式）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象int编码.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;raw：字符串对象保存的是一个字符串值，并且值的长度大于 39 字节，那么对象将使用简单动态字符串（SDS）来保存该值，并将对象的编码设置为 raw&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AF%B9%E8%B1%A1raw%E7%BC%96%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;embstr：字符串对象保存的是一个字符串值，并且值的长度小于等于 39 字节，那么对象将使用 embstr 编码的方式来保存这个字符串值，并将对象的编码设置为 embstr&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AF%B9%E8%B1%A1embstr%E7%BC%96%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图所示，embstr 与 raw 都使用了 redisObject 和 sdshdr 来表示字符串对象，但是 raw 需要调用两次内存分配函数分别创建两种结构，embstr 只需要一次内存分配来分配一块&lt;strong&gt;连续的空间&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;embstr 是用于保存短字符串的一种编码方式，对比 raw 的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存分配次数从两次降低为一次，同样释放内存的次数也从两次变为一次&lt;/li&gt;
&lt;li&gt;embstr 编码的字符串对象的数据都保存在同一块连续内存，所以比 raw 编码能够更好地利用缓存优势（局部性原理）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;int 和 embstr 编码的字符串对象在条件满足的情况下，会被转换为 raw 编码的字符串对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int 编码的整数值，执行 APPEND 命令追加一个字符串值，先将整数值转为字符串然后追加，最后得到一个 raw 编码的对象&lt;/li&gt;
&lt;li&gt;Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序，所以 embstr 对象实际上&lt;strong&gt;是只读的&lt;/strong&gt;，执行修改命令会将对象的编码从 embstr 转换成 raw，操作完成后得到一个 raw 编码的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某些情况下，程序会将字符串对象里面的字符串值转换回浮点数值，执行某些操作后再将浮点数值转换回字符串值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SET pi 3.14 
OK 
redis&amp;gt; OBJECT ENCODING pi
&quot;embstr&quot; 
redis&amp;gt; INCRBYFLOAT pi 2.0 # 转为浮点数执行增加的操作
&quot;5. 14&quot; 
redis&amp;gt; OBJECT ENCODING pi 
&quot;embstr&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;p&gt;主页高频访问信息显示控制，例如新浪微博大 V 主页显示粉丝数与微博数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 Redis 中为大 V 用户设定用户信息，以用户主键和属性值作为 key，后台设定定时刷新策略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set user:id:3506728370:fans 12210947
set user:id:3506728370:blogs 6164
set user:id:3506728370:focuses 83
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 JSON 格式保存数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user:id:3506728370 → {&quot;fans&quot;:12210947,&quot;blogs&quot;:6164,&quot;focuses&quot;:83}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;key的设置约定：表名 : 主键名 : 主键值 : 字段名&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;主键名&lt;/th&gt;
&lt;th&gt;主键值&lt;/th&gt;
&lt;th&gt;字段名&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;order&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;29437595&lt;/td&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;equip&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;390472345&lt;/td&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;news&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;202004150&lt;/td&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;hash&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;数据存储需求：对一系列存储的数据进行编组，方便管理，典型应用存储对象信息&lt;/p&gt;
&lt;p&gt;数据存储结构：一个存储空间保存多个键值对数据&lt;/p&gt;
&lt;p&gt;hash 类型：底层使用&lt;strong&gt;哈希表&lt;/strong&gt;结构实现数据存储&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/hash结构图.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Redis 中的 hash 类似于 Java 中的  &lt;code&gt;Map&amp;lt;String, Map&amp;lt;Object,object&amp;gt;&amp;gt;&lt;/code&gt;，左边是 key，右边是值，中间叫 field 字段，本质上 &lt;strong&gt;hash 存了一个 key-value 的存储空间&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;hash 是指的一个数据类型，并不是一个数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 field 数量较少，存储结构优化为&lt;strong&gt;压缩列表结构&lt;/strong&gt;（有序）&lt;/li&gt;
&lt;li&gt;如果 field 数量较多，存储结构使用 HashMap 结构（无序）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;p&gt;指令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hset key field value		#添加/修改数据
hdel key field1 [field2]	#删除数据，[]代表可选
hsetnx key field value		#设置field的值，如果该field存在则不做任何操作
hmset key f1 v1 f2 v2...	#添加/修改多个数据
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hget key field				#获取指定field对应数据
hgetall key					#获取指定key所有数据
hmget key field1 field2...	#获取多个数据
hexists key field			#获取哈希表中是否存在指定的字段
hlen key					#获取哈希表中字段的数量
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取哈希表中所有的字段名或字段值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hkeys key					#获取所有的field	
hvals key					#获取所有的value
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置指定字段的数值数据增加指定范围的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hincrby key field increment		#指定字段的数值数据增加指定的值，increment为负数则减少
hincrbyfloat key field increment#操作小数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;hash 类型中 value 只能存储字符串，不允许存储其他数据类型，不存在嵌套现象，如果数据未获取到，对应的值为（nil）&lt;/li&gt;
&lt;li&gt;每个 hash 可以存储 2^32 - 1 个键值对&lt;/li&gt;
&lt;li&gt;hash 类型和对象的数据存储形式相似，并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的，不可滥用，不可将 hash 作为对象列表使用&lt;/li&gt;
&lt;li&gt;hgetall 操作可以获取全部属性，如果内部 field 过多，遍历整体数据效率就很会低，有可能成为数据访问瓶颈&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;p&gt;哈希对象的内部编码有两种：ziplist（压缩列表）、hashtable（哈希表、字典）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;压缩列表实现哈希对象：同一键值对的节点总是挨在一起，保存键的节点在前，保存值的节点在后&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%93%88%E5%B8%8C%E5%AF%B9%E8%B1%A1ziplist.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字典实现哈希对象：字典的每一个键都是一个字符串对象，每个值也是&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希对象dict.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当存储的数据量比较小的情况下，Redis 才使用压缩列表来实现字典类型，具体需要满足两个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当键值对数量小于 hash-max-ziplist-entries 配置（默认 512 个）&lt;/li&gt;
&lt;li&gt;所有键和值的长度都小于 hash-max-ziplist-value 配置（默认 64 字节）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两个条件的上限值是可以通过配置文件修改的，当两个条件的任意一个不能被满足时，对象的编码转换操作就会被执行&lt;/p&gt;
&lt;p&gt;ziplist 使用更加紧凑的结构实现多个元素的连续存储，所以在节省内存方面比 hashtable 更加优秀，当 ziplist 无法满足哈希类型时，Redis 会使用 hashtable 作为哈希的内部实现，因为此时 ziplist 的读写效率会下降，而 hashtable 的读写时间复杂度为 O(1)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;user:id:3506728370 → {&quot;name&quot;:&quot;春晚&quot;,&quot;fans&quot;:12210862,&quot;blogs&quot;:83}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于以上数据，使用单条去存的话，存的条数会很多。但如果用 json 格式，存一条数据就够了。&lt;/p&gt;
&lt;p&gt;假如现在粉丝数量发生了变化，要把整个值都改变，但是用单条存就不存在这个问题，只需要改其中一个就可以&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/hash应用场景结构图.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以实现购物车的功能，key 对应着每个用户，存储空间存储购物车的信息&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;list&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;数据存储需求：存储多个数据，并对数据进入存储空间的顺序进行区分&lt;/p&gt;
&lt;p&gt;数据存储结构：一个存储空间保存多个数据，且通过数据可以体现进入顺序，允许重复元素&lt;/p&gt;
&lt;p&gt;list 类型：保存多个数据，底层使用&lt;strong&gt;双向链表&lt;/strong&gt;存储结构实现，类似于 LinkedList&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/list结构图.png&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;如果两端都能存取数据的话，这就是双端队列，如果只能从一端进一端出，这个模型叫栈&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;p&gt;指令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lpush key value1 [value2]...#从左边添加/修改数据(表头)
rpush key value1 [value2]...#从右边添加/修改数据(表尾)
lpop key					#从左边获取并移除第一个数据，类似于出栈/出队
rpop key					#从右边获取并移除第一个数据
lrem key count value		#删除指定数据，count=2删除2个，该value可能有多个(重复数据)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lrange key start stop		#从左边遍历数据并指定开始和结束索引，0是第一个索引，-1是终索引
lindex key index			#获取指定索引数据，没有则为nil，没有索引越界
llen key					#list中数据长度/个数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规定时间内获取并移除数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;b							#代表阻塞
blpop key1 [key2] timeout	#在指定时间内获取指定key(可以多个)的数据，超时则为(nil)
							#可以从其他客户端写数据，当前客户端阻塞读取数据
brpop key1 [key2] timeout	#从右边操作
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brpoplpush source destination timeout	#从source获取数据放入destination，假如在指定时间内没有任何元素被弹出，则返回一个nil和等待时长。反之，返回一个含有两个元素的列表，第一个元素是被弹出元素的值，第二个元素是等待时长
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;list 中保存的数据都是 string 类型的，数据总容量是有限的，最多 2^32 - 1 个元素（4294967295）&lt;/li&gt;
&lt;li&gt;list 具有索引的概念，但操作数据时通常以队列的形式进行入队出队，或以栈的形式进行入栈出栈&lt;/li&gt;
&lt;li&gt;获取全部数据操作结束索引设置为 -1&lt;/li&gt;
&lt;li&gt;list 可以对数据进行分页操作，通常第一页的信息来自于 list，第 2 页及更多的信息通过数据库的形式加载&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;p&gt;在 Redis3.2 版本以前列表对象的内部编码有两种：ziplist（压缩列表）和 linkedlist（链表）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;压缩列表实现的列表对象：PUSH 1、three、5 三个元素&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%88%97%E8%A1%A8%E5%AF%B9%E8%B1%A1ziplist.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链表实现的列表对象：为了简化字符串对象的表示，使用了 StringObject 的结构，底层其实是 sdshdr 结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%88%97%E8%A1%A8%E5%AF%B9%E8%B1%A1linkedlist.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;列表中存储的数据量比较小的时候，列表就会使用一块连续的内存存储，采用压缩列表的方式实现的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表对象保存的所有字符串元素的长度都小于 64 字节&lt;/li&gt;
&lt;li&gt;列表对象保存的元素数量小于 512 个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两个条件的上限值是可以通过配置文件修改的，当两个条件的任意一个不能被满足时，对象的编码转换操作就会被执行&lt;/p&gt;
&lt;p&gt;在 Redis3.2 版本 以后对列表数据结构进行了改造，使用 **quicklist（快速列表）**代替了 linkedlist，quicklist 实际上是 ziplist 和 linkedlist 的混合体，将 linkedlist 按段切分，每一段使用 ziplist 来紧凑存储，多个 ziplist 之间使用双向指针串接起来，既满足了快速的插入删除性能，又不会出现太大的空间冗余&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-快速列表数据结构.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;p&gt;企业运营过程中，系统将产生出大量的运营数据，如何保障多台服务器操作日志的统一顺序输出？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;依赖 list 的数据具有顺序的特征对信息进行管理，右进左查或者左近左查&lt;/li&gt;
&lt;li&gt;使用队列模型解决多路信息汇总合并的问题&lt;/li&gt;
&lt;li&gt;使用栈模型解决最新消息的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;微信文章订阅公众号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如订阅了两个公众号，它们发布了两篇文章，文章 ID 分别为 666 和 888，可以通过执行 &lt;code&gt;LPUSH key 666 888&lt;/code&gt; 命令推送给我&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;set&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;数据存储需求：存储大量的数据，在查询方面提供更高的效率&lt;/p&gt;
&lt;p&gt;数据存储结构：能够保存大量的数据，高效的内部存储机制，便于查询&lt;/p&gt;
&lt;p&gt;set 类型：与 hash 存储结构哈希表完全相同，只是仅存储键不存储值（nil），所以添加，删除，查找的复杂度都是 O(1)，并且&lt;strong&gt;值是不允许重复且无序的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/set结构图.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;p&gt;指令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sadd key member1 [member2]	#添加数据
srem key member1 [member2]	#删除数据
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smembers key				#获取全部数据
scard key					#获取集合数据总量
sismember key member		#判断集合中是否包含指定数据
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spop key [count]			#随机获取集中的某个数据并将该数据移除集合
srandmember key [count]		#随机获取集合中指定(数量)的数据

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合的交、并、差&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sinter key1 [key2...]  					#两个集合的交集，不存在为(empty list or set)
sunion key1 [key2...]  					#两个集合的并集
sdiff key1 [key2...]					#两个集合的差集

sinterstore destination key1 [key2...]	#两个集合的交集并存储到指定集合中
sunionstore destination key1 [key2...]	#两个集合的并集并存储到指定集合中
sdiffstore destination key1 [key2...]	#两个集合的差集并存储到指定集合中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smove source destination member			#将指定数据从原始集合中移动到目标集合中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;set 类型不允许数据重复，如果添加的数据在 set 中已经存在，将只保留一份&lt;/li&gt;
&lt;li&gt;set 虽然与 hash 的存储结构相同，但是无法启用 hash 中存储值的空间&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;p&gt;集合对象的内部编码有两种：intset（整数集合）、hashtable（哈希表、字典）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;整数集合实现的集合对象：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集合对象intset.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字典实现的集合对象：键值对的值为 NULL&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集合对象dict.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当集合对象可以同时满足以下两个条件时，对象使用 intset 编码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集合中的元素都是整数值&lt;/li&gt;
&lt;li&gt;集合中的元素数量小于 set-maxintset-entries配置（默认 512 个）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两个条件的上限值是可以通过配置文件修改的&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;黑名单：资讯类信息类网站追求高访问量，但是由于其信息的价值，往往容易被不法分子利用，通过爬虫技术，快速获取信息，个别特种行业网站信息通过爬虫获取分析后，可以转换成商业机密。&lt;/p&gt;
&lt;p&gt;注意：爬虫不一定做摧毁性的工作，有些小型网站需要爬虫为其带来一些流量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;白名单：对于安全性更高的应用访问，仅仅靠黑名单是不能解决安全问题的，此时需要设定可访问的用户群体， 依赖白名单做更为苛刻的访问验证&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机操作可以实现抽奖功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合的交并补可以实现微博共同关注的查看，可以根据共同关注或者共同喜欢推荐相关内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;zset&lt;/h3&gt;
&lt;h4&gt;简介&lt;/h4&gt;
&lt;p&gt;数据存储需求：数据排序有利于数据的有效展示，需要提供一种可以根据自身特征进行排序的方式&lt;/p&gt;
&lt;p&gt;数据存储结构：新的存储模型，可以保存可排序的数据&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;操作&lt;/h4&gt;
&lt;p&gt;指令操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zadd key score1 member1 [score2 member2]	#添加数据
zrem key member [member ...]				#删除数据
zremrangebyrank key start stop 				#删除指定索引范围的数据
zremrangebyscore key min max				#删除指定分数区间内的数据
zscore key member							#获取指定值的分数
zincrby key increment member				#指定值的分数增加increment
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zrange key start stop [WITHSCORES]		#获取指定范围的数据，升序，WITHSCORES 代表显示分数
zrevrange key start stop [WITHSCORES]	#获取指定范围的数据，降序

zrangebyscore key min max [WITHSCORES] [LIMIT offset count]	#按条件获取数据，从小到大
zrevrangebyscore key max min [WITHSCORES] [...]				#从大到小

zcard key										#获取集合数据的总量
zcount key min max								#获取指定分数区间内的数据总量
zrank key member								#获取数据对应的索引（排名）升序
zrevrank key member								#获取数据对应的索引（排名）降序
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;min 与 max 用于限定搜索查询的条件&lt;/li&gt;
&lt;li&gt;start 与 stop 用于限定查询范围，作用于索引，表示开始和结束索引&lt;/li&gt;
&lt;li&gt;offset 与 count 用于限定查询范围，作用于查询结果，表示开始位置和数据总量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合的交、并操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zinterstore destination numkeys key [key ...]	#两个集合的交集并存储到指定集合中
zunionstore destination numkeys key [key ...]	#两个集合的并集并存储到指定集合中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;score 保存的数据存储空间是 64 位，如果是整数范围是 -9007199254740992~9007199254740992&lt;/li&gt;
&lt;li&gt;score 保存的数据也可以是一个双精度的 double 值，基于双精度浮点数的特征可能会丢失精度，慎重使用&lt;/li&gt;
&lt;li&gt;sorted_set 底层存储还是基于 set 结构的，因此数据不能重复，如果重复添加相同的数据，score 值将被反复覆盖，保留最后一次修改的结果&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;p&gt;有序集合对象的内部编码有两种：ziplist（压缩列表）和 skiplist（跳跃表）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;压缩列表实现有序集合对象：ziplist 本身是有序、不可重复的，符合有序集合的特性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%9C%89%E5%BA%8F%E9%9B%86%E5%90%88%E5%AF%B9%E8%B1%A1ziplist.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跳跃表实现有序集合对象：&lt;strong&gt;底层是 zset 结构，zset 同时包含字典和跳跃表的结构&lt;/strong&gt;，图示字典和跳跃表中重复展示了各个元素的成员和分值，但实际上两者会&lt;strong&gt;通过指针来共享相同元素的成员和分值&lt;/strong&gt;，不会产生空间浪费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct zset {
    zskiplist *zsl;
    dict *dict;
} zset;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%9C%89%E5%BA%8F%E9%9B%86%E5%90%88%E5%AF%B9%E8%B1%A1zset.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用字典加跳跃表的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字典为有序集合创建了一个&lt;strong&gt;从成员到分值的映射&lt;/strong&gt;，用 O(1) 复杂度查找给定成员的分值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序操作使用跳跃表完成&lt;/strong&gt;，节省每次重新排序带来的时间成本和空间成本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 ziplist 格式存储需要满足以下两个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有序集合保存的元素个数要小于 128 个；&lt;/li&gt;
&lt;li&gt;有序集合保存的所有元素大小都小于 64 字节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当元素比较多时，此时 ziplist 的读写效率会下降，时间复杂度是 O(n)，跳表的时间复杂度是 O(logn)&lt;/p&gt;
&lt;p&gt;为什么用跳表而不用平衡树？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在做范围查找的时候，跳表操作简单（前进指针或后退指针），平衡树需要回旋查找&lt;/li&gt;
&lt;li&gt;跳表比平衡树实现简单，平衡树的插入和删除操作可能引发子树的旋转调整，而跳表的插入和删除只需要修改相邻节点的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;排行榜&lt;/li&gt;
&lt;li&gt;对于基于时间线限定的任务处理，将处理时间记录为 score 值，利用排序功能区分处理的先后顺序&lt;/li&gt;
&lt;li&gt;当任务或者消息待处理，形成了任务队列或消息队列时，对于高优先级的任务要保障对其优先处理，采用 score 记录权重&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Bitmaps&lt;/h3&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;p&gt;Bitmaps 是二进制位数组（bit array），底层使用 SDS 字符串表示，因为 SDS 是二进制安全的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E4%BD%8D%E6%95%B0%E7%BB%84%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;buf 数组的每个字节用一行表示，buf[1] 是 &lt;code&gt;&apos;\0&apos;&lt;/code&gt;，保存位数组的顺序和书写位数组的顺序是完全相反的，图示的位数组 0100 1101&lt;/p&gt;
&lt;p&gt;数据结构的详解查看 Java → Algorithm → 位图&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;命令实现&lt;/h4&gt;
&lt;h5&gt;GETBIT&lt;/h5&gt;
&lt;p&gt;GETBIT 命令获取位数组 bitarray 在 offset 偏移量上的二进制位的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GETBIT &amp;lt;bitarray&amp;gt; &amp;lt;offset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算 &lt;code&gt;byte = offset/8&lt;/code&gt;（向下取整）, byte 值记录数据保存在位数组中的索引&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;bit = (offset mod 8) + 1&lt;/code&gt;，bit 值记录数据在位数组中的第几个二进制位&lt;/li&gt;
&lt;li&gt;根据 byte 和 bit 值，在位数组 bitarray 中定位 offset 偏移量指定的二进制位，并返回这个位的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GETBIT 命令执行的所有操作都可以在常数时间内完成，所以时间复杂度为 O(1)&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;SETBIT&lt;/h5&gt;
&lt;p&gt;SETBIT 将位数组 bitarray 在 offset 偏移量上的二进制位的值设置为 value，并向客户端返回二进制位的旧值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SETBIT &amp;lt;bitarray&amp;gt; &amp;lt;offset&amp;gt; &amp;lt;value&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算 &lt;code&gt;len = offset/8 + 1&lt;/code&gt;，len 值记录了保存该数据至少需要多少个字节&lt;/li&gt;
&lt;li&gt;检查 bitarray 键保存的位数组的长度是否小于 len，成立就会将 SDS 扩展为 len 字节（注意空间预分配机制），所有新扩展空间的二进制位的值置为 0&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;byte = offset/8&lt;/code&gt;（向下取整）, byte 值记录数据保存在位数组中的索引&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;bit = (offset mod 8) + 1&lt;/code&gt;，bit 值记录数据在位数组中的第几个二进制位&lt;/li&gt;
&lt;li&gt;根据 byte 和 bit 值，在位数组 bitarray 中定位 offset 偏移量指定的二进制位，首先将指定位现存的值保存在 oldvalue 变量，然后将新值 value 设置为这个二进制位的值&lt;/li&gt;
&lt;li&gt;向客户端返回 oldvalue 变量的值&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;BITCOUNT&lt;/h5&gt;
&lt;p&gt;BITCOUNT 命令用于统计给定位数组中，值为 1 的二进制位的数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BITCOUNT &amp;lt;bitarray&amp;gt; [start end]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二进制位统计算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遍历法：遍历位数组中的每个二进制位&lt;/li&gt;
&lt;li&gt;查表算法：读取每个字节（8 位）的数据，查表获取数值对应的二进制中有几个 1&lt;/li&gt;
&lt;li&gt;variable-precision SWAR算法：计算汉明距离&lt;/li&gt;
&lt;li&gt;Redis 实现：
&lt;ul&gt;
&lt;li&gt;如果二进制位的数量大于等于 128 位， 那么使用 variable-precision SWAR 算法来计算二进制位的汉明重量&lt;/li&gt;
&lt;li&gt;如果二进制位的数量小于 128 位，那么使用查表算法来计算二进制位的汉明重量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;BITOP&lt;/h5&gt;
&lt;p&gt;BITOP 命令对指定 key 按位进行交、并、非、异或操作，并将结果保存到指定的键中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BITOP OPTION destKey key1 [key2...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OPTION 有 AND（与）、OR（或）、 XOR（异或）和 NOT（非）四个选项&lt;/p&gt;
&lt;p&gt;AND、OR、XOR 三个命令可以接受多个位数组作为输入，需要遍历输入的每个位数组的每个字节来进行计算，所以命令的复杂度为 O(n^2)；与此相反，NOT 命令只接受一个位数组输入，所以时间复杂度为 O(n)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;应用场景&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解决 Redis 缓存穿透&lt;/strong&gt;，判断给定数据是否存在， 防止缓存穿透&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-Bitmaps应用之缓存穿透.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;垃圾邮件过滤，对每一个发送邮件的地址进行判断是否在布隆的黑名单中，如果在就判断为垃圾邮件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;爬虫去重，爬给定网址的时候对已经爬取过的 URL 去重&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信息状态统计&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Hyper&lt;/h3&gt;
&lt;p&gt;基数是数据集去重后元素个数，HyperLogLog 是用来做基数统计的，运用了 LogLog 的算法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{1, 3, 5, 7, 5, 7, 8} 	基数集： {1, 3, 5 ,7, 8} 	基数：5
{1, 1, 1, 1, 1, 7, 1} 	基数集： {1,7} 				基数：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pfadd key element [element ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统计数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pfcount key [key ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pfmerge destkey sourcekey [sourcekey...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于进行基数统计，不是集合不保存数据，只记录数量而不是具体数据，比如网站的访问量&lt;/li&gt;
&lt;li&gt;核心是基数估算算法，最终数值存在一定误差&lt;/li&gt;
&lt;li&gt;误差范围：基数估计的结果是一个带有 0.81% 标准错误的近似值&lt;/li&gt;
&lt;li&gt;耗空间极小，每个 hyperloglog key 占用了12K的内存用于标记基数&lt;/li&gt;
&lt;li&gt;pfadd 命令不是一次性分配12K内存使用，会随着基数的增加内存逐渐增大&lt;/li&gt;
&lt;li&gt;Pfmerge 命令合并后占用的存储空间为12K，无论合并之前数据量多少&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;GEO&lt;/h3&gt;
&lt;p&gt;GeoHash 是一种地址编码方法，把二维的空间经纬度数据编码成一个字符串&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加坐标点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;geoadd key longitude latitude member [longitude latitude member ...]
georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取坐标点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;geopos key member [member ...]
georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算距离&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;geodist key member1 member2 [unit]	#计算坐标点距离
geohash key member [member ...]		#计算经纬度
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 应用于地理位置计算&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;持久机制&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;持久化：利用永久性存储介质将数据进行保存，在特定的时间将保存的数据进行恢复的工作机制称为持久化&lt;/p&gt;
&lt;p&gt;作用：持久化用于防止数据的意外丢失，确保数据安全性，因为 Redis 是内存级，所以需要持久化到磁盘&lt;/p&gt;
&lt;p&gt;计算机中的数据全部都是二进制，保存一组数据有两种方式
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-持久化的两种方式.png&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;RDB：将当前数据状态进行保存，快照形式，存储数据结果，存储格式简单&lt;/p&gt;
&lt;p&gt;AOF：将数据的操作过程进行保存，日志形式，存储操作过程，存储格式复杂&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;RDB&lt;/h3&gt;
&lt;h4&gt;文件创建&lt;/h4&gt;
&lt;p&gt;RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件，通过该文件可以还原生成 RDB 文件时的数据库状态，有两个 Redis 命令可以生成 RDB 文件，一个是 SAVE，另一个是 BGSAVE&lt;/p&gt;
&lt;h5&gt;SAVE&lt;/h5&gt;
&lt;p&gt;SAVE 指令：手动执行一次保存操作，该指令的执行会阻塞当前 Redis 服务器，客户端发送的所有命令请求都会被拒绝，直到当前 RDB 过程完成为止，有可能会造成长时间阻塞，线上环境不建议使用&lt;/p&gt;
&lt;p&gt;工作原理：Redis 是个&lt;strong&gt;单线程的工作模式&lt;/strong&gt;，会创建一个任务队列，所有的命令都会进到这个队列排队执行。当某个指令在执行的时候，队列后面的指令都要等待，所以这种执行方式会非常耗时&lt;/p&gt;
&lt;p&gt;配置 redis.conf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dir path				#设置存储.rdb文件的路径，通常设置成存储空间较大的目录中，目录名称data
dbfilename &quot;x.rdb&quot;		#设置本地数据库文件名，默认值为dump.rdb，通常设置为dump-端口号.rdb
rdbcompression yes|no	#设置存储至本地数据库时是否压缩数据，默认yes，设置为no节省CPU运行时间
rdbchecksum yes|no		#设置读写文件过程是否进行RDB格式校验，默认yes
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;BGSAVE&lt;/h5&gt;
&lt;p&gt;BGSAVE：bg 是 background，代表后台执行，命令的完成需要两个进程，&lt;strong&gt;进程之间不相互影响&lt;/strong&gt;，所以持久化期间 Redis 正常工作&lt;/p&gt;
&lt;p&gt;工作原理：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-bgsave工作原理.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;流程：客户端发出 BGSAVE 指令，Redis 服务器使用 fork 函数创建一个子进程，然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作，持久化过程是先将数据写入到一个临时文件中，持久化操作结束再用这个临时文件&lt;strong&gt;替换&lt;/strong&gt;上次持久化的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建子进程
pid = fork()
if pid == 0:
    # 子进程负责创建 RDB 文件
    rdbSave()
    # 完成之后向父进程发送信号
    signal_parent()
elif pid &amp;gt; 0:
    # 父进程继续处理命令请求，并通过轮询等待子进程的信号
    handle_request_and_wait_signal()
else:
    # 处理出错恃况
    handle_fork_error() 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置 redis.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stop-writes-on-bgsave-error yes|no	#后台存储过程中如果出现错误，是否停止保存操作，默认yes
dbfilename filename  
dir path  
rdbcompression yes|no  
rdbchecksum yes|no
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：BGSAVE 命令是针对 SAVE 阻塞问题做的优化，Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式，SAVE 命令放弃使用&lt;/p&gt;
&lt;p&gt;在 BGSAVE 命令执行期间，服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SAVE 命令会被服务器拒绝，服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程（服务器进程）和子进程同时执行两个 rdbSave 调用，产生竞争条件&lt;/li&gt;
&lt;li&gt;BGSAVE 命令也会被服务器拒绝，也会产生竞争条件&lt;/li&gt;
&lt;li&gt;BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行
&lt;ul&gt;
&lt;li&gt;如果 BGSAVE 命令正在执行，那么 BGREWRITEAOF 命令会被&lt;strong&gt;延迟&lt;/strong&gt;到 BGSAVE 命令执行完毕之后执行&lt;/li&gt;
&lt;li&gt;如果 BGREWRITEAOF 命令正在执行，那么 BGSAVE 命令会被服务器拒绝&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;特殊指令&lt;/h5&gt;
&lt;p&gt;RDB 特殊启动形式的指令（客户端输入）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器运行过程中重启&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;debug reload
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭服务器时指定保存数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shutdown save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下执行 shutdown 命令时，自动执行 bgsave（如果没有开启 AOF 持久化功能）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全量复制：主从复制部分详解&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;文件载入&lt;/h4&gt;
&lt;p&gt;RDB 文件的载入工作是在服务器启动时自动执行，期间 Redis 会一直处于阻塞状态，直到载入完成&lt;/p&gt;
&lt;p&gt;Redis 并没有专门用于载入 RDB 文件的命令，只要服务器在启动时检测到 RDB 文件存在，就会自动载入 RDB 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds  # 服务器在成功载入 RDB 文件之后打印
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF 文件的更新频率通常比 RDB 文件的更新频率高：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果服务器开启了 AOF 持久化功能，那么会优先使用 AOF 文件来还原数据库状态&lt;/li&gt;
&lt;li&gt;只有在 AOF 持久化功能处于关闭状态时，服务器才会使用 RDB 文件来还原数据库状态&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;自动保存&lt;/h4&gt;
&lt;h5&gt;配置文件&lt;/h5&gt;
&lt;p&gt;Redis 支持通过配置服务器的 save 选项，让服务器每隔一段时间自动执行一次 BGSAVE 命令&lt;/p&gt;
&lt;p&gt;配置 redis.conf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;save second changes #设置自动持久化条件，满足限定时间范围内key的变化数量就进行持久化(bgsave)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;second：监控时间范围&lt;/li&gt;
&lt;li&gt;changes：监控 key 的变化量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认三个条件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;save 900 1		# 900s内1个key发生变化就进行持久化
save 300 10
save 60 10000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判定 key 变化的依据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对数据产生了影响，不包括查询&lt;/li&gt;
&lt;li&gt;不进行数据比对，比如 name 键存在，重新 set name seazean 也算一次变化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;save 配置要根据实际业务情况进行设置，频度过高或过低都会出现性能问题，结果可能是灾难性的&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;自动原理&lt;/h5&gt;
&lt;p&gt;服务器状态相关的属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 记录了保存条件的数组
    struct saveparam *saveparams;
    
    // 修改计数器
    long long dirty;
    
    // 上一次执行保存的时间 
    time_t lastsave;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Redis 服务器启动时，可以通过指定配置文件或者传入启动参数的方式设置 save 选项， 如果没有自定义就设置为三个默认值（上节提及），设置服务器状态 redisServe.saveparams 属性，该数组每一项为一个 saveparam 结构，代表 save 的选项设置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct saveparam {
    // 秒数
    time_t seconds
    // 修改数
    int changes;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后，服务器中的所有数据库进行了多少次修改（包括写入、删除、更新等操作），当服务器成功执行一个修改指令，该命令修改了多少次数据库， dirty 的值就增加多少&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lastsave 属性是一个 UNIX 时间戳，记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次，该函数用于对正在运行的服务器进行维护&lt;/p&gt;
&lt;p&gt;serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足，会遍历 saveparams 数组中的&lt;strong&gt;所有保存条件&lt;/strong&gt;，只要有任意一个条件被满足服务器就会执行 BGSAVE 命令&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-BGSAVE%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;文件结构&lt;/h4&gt;
&lt;p&gt;RDB 的存储结构：图示全大写单词标示常量，用全小写单词标示变量和数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-RDB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REDIS：长度为 5 字节，保存着 &lt;code&gt;REDIS&lt;/code&gt; 五个字符，是 RDB 文件的开头，在载入文件时可以快速检查所载入的文件是否 RDB 文件&lt;/li&gt;
&lt;li&gt;db_version：长度为 4 字节，是一个用字符串表示的整数，记录 RDB 的版本号&lt;/li&gt;
&lt;li&gt;database：包含着零个或任意多个数据库，以及各个数据库中的键值对数据&lt;/li&gt;
&lt;li&gt;EOF：长度为 1 字节的常量，标志着 RDB 文件正文内容的结束，当读入遇到这个值时，代表所有数据库的键值对都已经载入完毕&lt;/li&gt;
&lt;li&gt;check_sum：长度为 8 字节的无符号整数，保存着一个校验和，该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时，会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比，来检查 RDB 文件是否有出错或者损坏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 本身带有 RDB 文件检查工具 redis-check-dump&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;AOF&lt;/h3&gt;
&lt;h4&gt;基本概述&lt;/h4&gt;
&lt;p&gt;AOF（append only file）持久化：以独立日志的方式记录每次写命令（不记录读）来记录数据库状态，&lt;strong&gt;增量保存&lt;/strong&gt;只许追加文件但不可以改写文件，&lt;strong&gt;与 RDB 相比可以理解为由记录数据改为记录数据的变化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AOF 主要作用是解决了&lt;strong&gt;数据持久化的实时性&lt;/strong&gt;，目前已经是 Redis 持久化的主流方式&lt;/p&gt;
&lt;p&gt;AOF 写数据过程：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF工作原理.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Redis 只会将对数据库进行了修改的命令写入到 AOF 文件，并复制到各个从服务器，但是 PUBSUB 和 SCRIPT LOAD 命令例外：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PUBSUB 命令虽然没有修改数据库，但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用，接收到消息的所有客户端的状态都会因为这个命令而改变，所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时，服务器就可以再次执行相同的 PUBSUB 命令，并产生相同的副作用&lt;/li&gt;
&lt;li&gt;SCRIPT LOAD  命令虽然没有修改数据库，但它修改了服务器状态，所以也是一个带有副作用的命令，需要使用 REDIS_FORCE_AOF&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;持久实现&lt;/h4&gt;
&lt;p&gt;AOF 持久化功能的实现可以分为命令追加（append）、文件写入、文件同步（sync）三个步骤&lt;/p&gt;
&lt;h5&gt;命令追加&lt;/h5&gt;
&lt;p&gt;启动 AOF 的基本配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;appendonly yes|no				#开启AOF持久化功能，默认no，即不开启状态
appendfilename filename			#AOF持久化文件名，默认appendonly.aof，建议设置appendonly-端口号.aof
dir								#AOF持久化文件保存路径，与RDB持久化文件路径保持一致即可
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 AOF 持久化功能处于打开状态时，服务器在执行完一个写命令之后，会以协议格式将被执行的写命令&lt;strong&gt;追加&lt;/strong&gt;到服务器状态的 aof_buf 缓冲区的末尾&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // AOF 缓冲区
    sds aof_buf;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;文件写入&lt;/h5&gt;
&lt;p&gt;服务器在处理文件事件时会执行&lt;strong&gt;写命令，追加一些内容到 aof_buf 缓冲区&lt;/strong&gt;里，所以服务器每次结束一个事件循环之前，就会执行 flushAppendOnlyFile 函数，判断是否需要&lt;strong&gt;将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件&lt;/strong&gt;里&lt;/p&gt;
&lt;p&gt;flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;appendfsync always|everysec|no	#AOF写数据策略：默认为everysec
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;always：每次写入操作都将 aof_buf 缓冲区中的所有内容&lt;strong&gt;写入并同步&lt;/strong&gt;到 AOF 文件&lt;/p&gt;
&lt;p&gt;特点：安全性最高，数据零误差，但是性能较低，不建议使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;everysec：先将 aof_buf 缓冲区中的内容写入到操作系统缓存，判断上次同步 AOF 文件的时间距离现在超过一秒钟，再次进行同步 fsync，这个同步操作是由一个（子）线程专门负责执行的&lt;/p&gt;
&lt;p&gt;特点：在系统突然宕机的情况下丢失 1 秒内的数据，准确性较高，性能较高，建议使用，也是默认配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;no：将 aof_buf 缓冲区中的内容写入到操作系统缓存，但并不进行同步，何时同步由操作系统来决定&lt;/p&gt;
&lt;p&gt;特点：&lt;strong&gt;整体不可控&lt;/strong&gt;，服务器宕机会丢失上次同步 AOF 后的所有写指令&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;文件同步&lt;/h5&gt;
&lt;p&gt;在现代操作系统中，当用户调用 write 函数将数据写入文件时，操作系统通常会将写入数据暂时保存在一个内存缓冲区空间，等到缓冲区&lt;strong&gt;写满或者到达特定时间周期&lt;/strong&gt;，才真正地将缓冲区中的数据写入到磁盘里面（刷脏）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：提高文件的写入效率&lt;/li&gt;
&lt;li&gt;缺点：为写入数据带来了安全问题，如果计算机发生停机，那么保存在内存缓冲区里面的写入数据将会丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统提供了 fsync 和 fdatasync 两个同步函数做&lt;strong&gt;强制硬盘同步&lt;/strong&gt;，可以让操作系统立即将缓冲区中的数据写入到硬盘里面，函数会阻塞到写入硬盘完成后返回，保证了数据持久化&lt;/p&gt;
&lt;p&gt;异常恢复：AOF 文件损坏，通过 redis-check-aof--fix appendonly.aof 进行恢复，重启 Redis，然后重新加载&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;文件载入&lt;/h4&gt;
&lt;p&gt;AOF 文件里包含了重建数据库状态所需的所有写命令，所以服务器只要读入并重新执行一遍 AOF 文件里的命令，就还原服务器关闭之前的数据库状态，服务器在启动时，还原数据库状态打印的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的，其他都是通过客户端发送的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* 2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n	# 服务器自动添加
* 3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
* 5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 读取 AOF 文件并还原数据库状态的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个&lt;strong&gt;不带网络连接的伪客户端&lt;/strong&gt;（fake client）执行命令，因为 Redis 的命令只能在客户端上下文中执行， 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接&lt;/li&gt;
&lt;li&gt;从 AOF 文件分析并读取一条写命令&lt;/li&gt;
&lt;li&gt;使用伪客户端执行被读出的写命令，然后重复上述步骤&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;重写实现&lt;/h4&gt;
&lt;h5&gt;重写策略&lt;/h5&gt;
&lt;p&gt;AOF 重写：读取服务器当前的数据库状态，&lt;strong&gt;生成新 AOF 文件来替换旧 AOF 文件&lt;/strong&gt;，不会对现有的 AOF 文件进行任何读取、分析或者写入操作，而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令，所以体积通常会比旧 AOF 文件小得多&lt;/p&gt;
&lt;p&gt;AOF 重写规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进程内具有时效性的数据，并且数据已超时将不再写入文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对同一数据的多条写命令合并为一条命令，因为会读取当前的状态，所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出，对 list、set、hash、zset 等集合类型，&lt;strong&gt;单条指令&lt;/strong&gt;最多写入 64 个元素&lt;/p&gt;
&lt;p&gt;如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为：lpush list1 a b c&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非写入类的无效指令将被忽略，只保留最终数据的写入命令，但是 select 指令虽然不更改数据，但是更改了数据的存储位置，此类命令同样需要记录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AOF 重写作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;降低磁盘占用量，提高磁盘利用率&lt;/li&gt;
&lt;li&gt;提高持久化效率，降低持久化写时间，提高 IO 性能&lt;/li&gt;
&lt;li&gt;降低数据恢复的用时，提高数据恢复效率&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;重写原理&lt;/h5&gt;
&lt;p&gt;AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件， 但是该函数会进行大量的写入操作，调用这个函数的线程将被长时间阻塞，所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行，不会阻塞父进程，重写命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bgrewriteaof
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;子进程进行 AOF 重写期间，服务器进程（父进程）可以继续处理命令请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;子进程带有服务器进程的数据副本，使用子进程而不是线程，可以在避免使用锁的情况下， 保证数据安全性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF%E6%89%8B%E5%8A%A8%E9%87%8D%E5%86%99%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;子进程在进行 AOF 重写期间，服务器进程还需要继续处理命令请求，而新命令可能会对现有的数据库状态进行修改，从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致，所以 Redis 设置了 AOF 重写缓冲区&lt;/p&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 服务器执行完一个写命令，会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区（从创建子进程后才开始写入）&lt;/li&gt;
&lt;li&gt;当子进程完成 AOF 重写工作之后，会向父进程发送一个信号，父进程在接到该信号之后， 会调用一个信号处理函数，该函数执行时会&lt;strong&gt;对服务器进程（父进程）造成阻塞&lt;/strong&gt;（影响很小，类似 JVM STW），主要工作：
&lt;ul&gt;
&lt;li&gt;将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中， 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致&lt;/li&gt;
&lt;li&gt;对新的 AOF 文件进行改名，&lt;strong&gt;原子地（atomic）覆盖&lt;/strong&gt;现有的 AOF 文件，完成新旧两个 AOF 文件的替换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;自动重写&lt;/h5&gt;
&lt;p&gt;触发时机：Redis 会记录上次重写时的 AOF 大小，默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto-aof-rewrite-min-size size		#设置重写的基准值，最小文件 64MB，达到这个值开始重写
auto-aof-rewrite-percentage percent	#触发AOF文件执行重写的增长率，当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写，比如文件达到 100% 时开始重写就是两倍时触发
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动重写触发比对参数（ 运行指令 &lt;code&gt;info Persistence&lt;/code&gt; 获取具体信息 ）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aof_current_size					#AOF文件当前尺寸大小（单位:字节）
aof_base_size						#AOF文件上次启动和重写时的尺寸大小（单位:字节）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动重写触发条件公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;aof_current_size &amp;gt; auto-aof-rewrite-min-size&lt;/li&gt;
&lt;li&gt;(aof_current_size - aof_base_size) / aof_base_size &amp;gt;= auto-aof-rewrite-percentage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;p&gt;RDB 的特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RDB 优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RDB 是一个紧凑压缩的二进制文件，存储效率较高，但存储数据量较大时，存储效率较低&lt;/li&gt;
&lt;li&gt;RDB 内部存储的是 Redis 在某个时间点的数据快照，非常&lt;strong&gt;适合用于数据备份，全量复制、灾难恢复&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;RDB 恢复数据的速度要比 AOF 快很多，因为是快照，直接恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RDB 缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BGSAVE 指令每次运行要执行 fork 操作创建子进程，会牺牲一些性能&lt;/li&gt;
&lt;li&gt;RDB 方式无论是执行指令还是利用配置，无法做到实时持久化，具有丢失数据的可能性，最后一次持久化后的数据可能丢失&lt;/li&gt;
&lt;li&gt;Redis 的众多版本中未进行 RDB 文件格式的版本统一，可能出现各版本之间数据格式无法兼容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AOF 特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AOF 的优点：数据持久化有&lt;strong&gt;较好的实时性&lt;/strong&gt;，通过 AOF 重写可以降低文件的体积&lt;/li&gt;
&lt;li&gt;AOF 的缺点：文件较大时恢复较慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AOF 和 RDB 同时开启，系统默认取 AOF 的数据（数据不会存在丢失）&lt;/p&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对数据&lt;strong&gt;非常敏感&lt;/strong&gt;，建议使用默认的 AOF 持久化方案，AOF 持久化策略使用 everysecond，每秒钟 fsync 一次，该策略 Redis 仍可以保持很好的处理性能&lt;/p&gt;
&lt;p&gt;注意：AOF 文件存储体积较大，恢复速度较慢，因为要执行每条指令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据呈现&lt;strong&gt;阶段有效性&lt;/strong&gt;，建议使用 RDB 持久化方案，可以做到阶段内无丢失，且恢复速度较快&lt;/p&gt;
&lt;p&gt;注意：利用 RDB 实现紧凑的数据持久化，存储数据量较大时，存储效率较低&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RDB 与 AOF 的选择实际上是在做一种权衡，每种都有利有弊&lt;/li&gt;
&lt;li&gt;灾难恢复选用 RDB&lt;/li&gt;
&lt;li&gt;如不能承受数分钟以内的数据丢失，对业务数据非常敏感，选用 AOF；如能承受数分钟以内的数据丢失，且追求大数据集的恢复速度，选用 RDB&lt;/li&gt;
&lt;li&gt;双保险策略，同时开启 RDB 和 AOF，重启后 Redis 优先使用 AOF 来恢复数据，降低丢失数据的量&lt;/li&gt;
&lt;li&gt;不建议单独用 AOF，因为可能会出现 Bug，如果只是做纯内存缓存，可以都不用&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;fork&lt;/h3&gt;
&lt;h4&gt;介绍&lt;/h4&gt;
&lt;p&gt;fork() 函数创建一个子进程，子进程与父进程几乎是完全相同的进程，系统先给子进程分配资源，然后把父进程的所有数据都复制到子进程中，只有少数值与父进程的值不同，相当于克隆了一个进程&lt;/p&gt;
&lt;p&gt;在完成对其调用之后，会产生 2 个进程，且每个进程都会&lt;strong&gt;从 fork() 的返回处开始执行&lt;/strong&gt;，这两个进程将执行相同的程序段，但是拥有各自不同的堆段，栈段，数据段，每个子进程都可修改各自的数据段，堆段，和栈段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;unistd.h&amp;gt;
pid_t fork(void);
// 父进程返回子进程的pid，子进程返回0，错误返回负值，根据返回值的不同进行对应的逻辑处理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fork 调用一次，却能够&lt;strong&gt;返回两次&lt;/strong&gt;，可能有三种不同的返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在父进程中，fork 返回新创建子进程的进程 ID&lt;/li&gt;
&lt;li&gt;在子进程中，fork 返回 0&lt;/li&gt;
&lt;li&gt;如果出现错误，fork 返回一个负值，错误原因：
&lt;ul&gt;
&lt;li&gt;当前的进程数已经达到了系统规定的上限，这时 errno 的值被设置为 EAGAIN&lt;/li&gt;
&lt;li&gt;系统内存不足，这时 errno 的值被设置为 ENOMEM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;fpid 的值在父子进程中不同：进程形成了链表，父进程的 fpid 指向子进程的进程 id，因为子进程没有子进程，所以其 fpid 为0&lt;/p&gt;
&lt;p&gt;创建新进程成功后，系统中出现两个基本完全相同的进程，这两个进程执行没有固定的先后顺序，哪个进程先执行要看系统的调度策略&lt;/p&gt;
&lt;p&gt;每个进程都有一个独特（互不相同）的进程标识符 process ID，可以通过 getpid() 函数获得；还有一个记录父进程 pid 的变量，可以通过 getppid() 函数获得变量的值&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;使用&lt;/h4&gt;
&lt;p&gt;基本使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;  
#include &amp;lt;stdio.h&amp;gt;   
int main ()   
{   
    pid_t fpid; // fpid表示fork函数返回的值  
    int count = 0;  
    fpid = fork();   
    if (fpid &amp;lt; 0)   
        printf(&quot;error in fork!&quot;);   
    else if (fpid == 0) {  
        printf(&quot;i am the child process, my process id is %d/n&quot;, getpid());    
        count++;  
    }  
    else {  
        printf(&quot;i am the parent process, my process id is %d/n&quot;, getpid());   
        count++;  
    }  
    printf(&quot;count: %d/n&quot;,count);// 1  
    return 0;  
}  
/* 输出内容：
    i am the child process, my process id is 5574
    count: 1
    i am the parent process, my process id is 5573
    count: 1
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进阶使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;  
#include &amp;lt;stdio.h&amp;gt;  
int main(void)  
{  
   int i = 0;  
   // ppid 指当前进程的父进程pid  
   // pid 指当前进程的pid,  
   // fpid 指fork返回给当前进程的值，在这可以表示子进程
   for(i = 0; i &amp;lt; 2; i++){  
       pid_t fpid = fork();  
       if(fpid == 0)  
           printf(&quot;%d child  %4d %4d %4d/n&quot;,i, getppid(), getpid(), fpid);  
       else  
           printf(&quot;%d parent %4d %4d %4d/n&quot;,i, getppid(), getpid(),fpid);  
   }  
   return 0;  
} 
/*输出内容：
	i        父id  id  子id
	0 parent 2043 3224 3225
    0 child  3224 3225    0
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227
    1 child     1 3227    0
    1 child     1 3226    0 
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-fork函数使用演示.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;在 p3224 和 p3225 执行完第二个循环后，main 函数退出，进程死亡。所以 p3226，p3227 就没有父进程了，成为孤儿进程，所以 p3226 和 p3227 的父进程就被置为 ID 为 1 的 init 进程（笔记 Tool → Linux → 进程管理详解）&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/love_gaohz/article/details/41727415&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存&lt;/h4&gt;
&lt;p&gt;fork() 调用之后父子进程的内存关系&lt;/p&gt;
&lt;p&gt;早期 Linux 的 fork() 实现时，就是全部复制，这种方法效率太低，而且造成了很大的内存浪费，现在 Linux 实现采用了两种方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;父子进程的代码段是相同的，所以代码段是没必要复制的，只需内核将代码段标记为只读，父子进程就共享此代码段。fork() 之后在进程创建代码段时，子进程的进程级页表项都指向和父进程相同的物理页帧&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-fork以后内存关系1.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于父进程的数据段，堆段，栈段中的各页，由于父子进程相互独立，采用&lt;strong&gt;写时复制 COW&lt;/strong&gt; 的技术，来提高内存以及内核的利用率&lt;/p&gt;
&lt;p&gt;在 fork 之后两个进程用的是相同的物理空间（内存区），子进程的代码段、数据段、堆栈都是指向父进程的物理空间，&lt;strong&gt;两者的虚拟空间不同，但其对应的物理空间是同一个&lt;/strong&gt;，当父子进程中有更改相应段的行为发生时，再为子进程相应的段分配物理空间。如果两者的代码完全相同，代码段继续共享父进程的物理空间；而如果两者执行的代码不同，子进程的代码段也会分配单独的物理空间。&lt;/p&gt;
&lt;p&gt;fork 之后内核会将子进程放在队列的前面，让子进程先执行，以免父进程执行导致写时复制，而后子进程再执行，因无意义的复制而造成效率的下降&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-fork以后内存关系2.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充知识：&lt;/p&gt;
&lt;p&gt;vfork（虚拟内存 fork virtual memory fork）：调用 vfork() 父进程被挂起，子进程使用父进程的地址空间。不采用写时复制，如果子进程修改父地址空间的任何页面，这些修改过的页面对于恢复的父进程是可见的&lt;/p&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/Shreck66/article/details/47039937&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;事务机制&lt;/h2&gt;
&lt;h3&gt;事务特征&lt;/h3&gt;
&lt;p&gt;Redis 事务就是将多个命令请求打包，然后&lt;strong&gt;一次性、按顺序&lt;/strong&gt;地执行多个命令的机制，并且在事务执行期间，服务器不会中断事务去执行其他的命令请求，会将事务中的所有命令都执行完毕，然后才去处理其他客户端的命令请求，Redis 事务的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 事务&lt;strong&gt;没有隔离级别&lt;/strong&gt;的概念，队列中的命令在事务没有提交之前都不会实际被执行&lt;/li&gt;
&lt;li&gt;Redis 单条命令式保存原子性的，但是事务&lt;strong&gt;不保证原子性&lt;/strong&gt;，事务中如果有一条命令执行失败，其后的命令仍然会被执行，没有回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;p&gt;事务的执行流程分为三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事务开始：MULTI 命令的执行标志着事务的开始，通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识，将执行该命令的客户端从非事务状态切换至事务状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MULTI	# 设定事务的开启位置，此指令执行后，后续的所有指令均加入到事务中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令入队：事务队列以先进先出（FIFO）的方式保存入队的命令，每个 Redis 客户端都有事务状态，包含着事务队列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisClient {
	// 事务状态
    multiState mstate;	/* MULTI/EXEC state */ 
}

typedef struct multiState {
    // 事务队列，FIFO顺序
    multiCmd *commands; 
    
   	// 已入队命令计数
    int count；
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果命令为 EXEC、DISCARD、WATCH、MULTI 四个命中的一个，那么服务器立即执行这个命令&lt;/li&gt;
&lt;li&gt;其他命令服务器不执行，而是将命令放入一个事务队列里面，然后向客户端返回 QUEUED 回复&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务执行：EXEC 提交事务给服务器执行，服务器会遍历这个客户端的事务队列，执行队列中的命令并将执行结果返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXEC	# Commit 提交，执行事务，与multi成对出现，成对使用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务取消的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;取消事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DISCARD	# 终止当前事务的定义，发生在multi之后，exec之前
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般用于事务执行过程中输入了错误的指令，直接取消这次事务，类似于回滚&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;WATCH&lt;/h3&gt;
&lt;h4&gt;监视机制&lt;/h4&gt;
&lt;p&gt;WATCH 命令是一个乐观锁（optimistic locking），可以在 EXEC 命令执行之前，监视任意数量的数据库键，并在 EXEC 命令执行时，检查被监视的键是否至少有一个已经被修改过了，如果是服务器将拒绝执行事务，并向客户端返回代表事务执行失败的空回复&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加监控锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WATCH key1 [key2……]	#可以监控一个或者多个key
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消对所有 key 的监视&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UNWATCH
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;p&gt;每个 Redis 数据库都保存着一个 watched_keys 字典，键是某个被 WATCH 监视的数据库键，值则是一个链表，记录了所有监视相应数据库键的客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct redisDb {
	// 正在被 WATCH 命令监视的键
    dict *watched_keys;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有对数据库进行修改的命令，在执行后都会调用 &lt;code&gt;multi.c/touchWatchKey&lt;/code&gt; 函数对 watched_keys 字典进行检查，是否有客户端正在监视刚被命令修改过的数据库键，如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开，表示该客户端的事务安全性已经被破坏&lt;/p&gt;
&lt;p&gt;服务器接收到个客户端 EXEC 命令时，会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识，如果打开了说明客户端提交事务不安全，服务器会拒绝执行&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ACID&lt;/h3&gt;
&lt;h4&gt;原子性&lt;/h4&gt;
&lt;p&gt;事务具有原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability）&lt;/p&gt;
&lt;p&gt;原子性指事务队列中的命令要么就全部都执行，要么一个都不执行，但是在命令执行出错时，不会保证原子性（下一节详解）&lt;/p&gt;
&lt;p&gt;Redis 不支持事务回滚机制（rollback），即使事务队列中的某个命令在执行期间出现了错误，整个事务也会继续执行下去，直到将事务队列中的所有命令都执行完毕为止&lt;/p&gt;
&lt;p&gt;回滚需要程序员在代码中实现，应该尽可能避免：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事务操作之前记录数据的状态&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单数据：string&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多数据：hash、list、set、zset&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置指令恢复所有的被修改的项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单数据：直接 set（注意周边属性，例如时效）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多数据：修改对应值或整体克隆复制&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;一致性&lt;/h4&gt;
&lt;p&gt;事务具有一致性指的是，数据库在执行事务之前是一致的，那么在事务执行之后，无论事务是否执行成功，数据库也应该仍然是一致的&lt;/p&gt;
&lt;p&gt;一致是数据符合数据库的定义和要求，没有包含非法或者无效的错误数据，Redis 通过错误检测和简单的设计来保证事务的一致性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;入队错误：命令格式输入错误，出现语法错误造成，&lt;strong&gt;整体事务中所有命令均不会执行&lt;/strong&gt;，包括那些语法正确的命令&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-命令的语法错误.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行错误：命令执行出现错误，例如对字符串进行 incr 操作，事务中正确的命令会被执行，运行错误的命令不会被执行&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-事务中执行错误.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器停机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果服务器运行在无持久化的内存模式下，那么重启之后的数据库将是空白的，因此数据库是一致的&lt;/li&gt;
&lt;li&gt;如果服务器运行在持久化模式下，重启之后将数据库还原到一致的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;隔离性&lt;/h4&gt;
&lt;p&gt;Redis 是一个单线程的执行原理，所以对于隔离性，分以下两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发操作在 EXEC 命令前执行，隔离性的保证要使用 WATCH 机制来实现，否则隔离性无法保证&lt;/li&gt;
&lt;li&gt;并发操作在 EXEC 命令后执行，隔离性可以保证&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;持久性&lt;/h4&gt;
&lt;p&gt;Redis 并没有为事务提供任何额外的持久化功能，事务的持久性由 Redis 所使用的持久化模式决定&lt;/p&gt;
&lt;p&gt;配置选项 &lt;code&gt;no-appendfsync-on-rewrite&lt;/code&gt; 可以配合 appendfsync 选项在 AOF 持久化模式使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选项打开时在执行 BGSAVE 或者 BGREWRITEAOF 期间，服务器会暂时停止对 AOF 文件进行同步，从而尽可能地减少 I/O 阻塞&lt;/li&gt;
&lt;li&gt;选项打开时运行在 always 模式的 AOF 持久化，事务也不具有持久性，所以该选项默认关闭&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一个事务的最后加上 SAVE 命令总可以保证事务的耐久性&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Lua 脚本&lt;/h2&gt;
&lt;h3&gt;环境创建&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Redis 对 Lua 脚本支持，通过在服务器中嵌入 Lua 环境，客户端可以使用 Lua 脚本直接在服务器端&lt;strong&gt;原子地执行&lt;/strong&gt;多个命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EVAL &amp;lt;script&amp;gt; &amp;lt;numkeys&amp;gt; [key ...] [arg ...]
EVALSHA &amp;lt;sha1&amp;gt; &amp;lt;numkeys&amp;gt; [key ...] [arg ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EVAL 命令可以直接对输入的脚本计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; EVAL &quot;return 1 + 1&quot; 0	# 0代表需要的参数
(integer) 2 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EVALSHA 命令根据脚本的 SHA1 校验和来对脚本计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; EVALSHA &quot;2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3&quot; 0
(integer) 2 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：Redis 只保证单条命令的原子性，所以为了实现原子操作，将多条的对 Redis 的操作整合到一个脚本里，但是避免把不需要做并发控制的操作写入脚本中&lt;/p&gt;
&lt;p&gt;Lua 语法特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明变量的时候无需指定数据类型，而是用 local 来声明变量为局部变量&lt;/li&gt;
&lt;li&gt;数组下标是从 1 开始&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;创建过程&lt;/h4&gt;
&lt;p&gt;Redis 服务器创建并修改 Lua 环境的整个过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个基础的 Lua 环境，调用 Lua 的 API 函数 lua_open&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;载入多个函数库到 Lua 环境里面，让 Lua 脚本可以使用这些函数库来进行数据操作，包括基础核心函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建全局变量 redis 表格，表格包含以下函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行 Redis 命令的 redis.call 和 redis.pcall 函数&lt;/li&gt;
&lt;li&gt;记录 Redis 日志的 redis.log 函数，以及相应的日志级别 (level) 常量 redis.LOG_DEBUG 等&lt;/li&gt;
&lt;li&gt;计算 SHAl 校验和的 redis.shalhex 函数&lt;/li&gt;
&lt;li&gt;返回错误信息的 redis.error_reply 函数和 redis.status_reply 函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Redis 自制的随机函数来替换 Lua 原有的带有副作用的随机函数，从而避免在脚本中引入副作用&lt;/p&gt;
&lt;p&gt;Redis 要求所有传入服务器的 Lua 脚本，以及 Lua 环境中的所有函数，都必须是无副作用（side effect）的纯函数（pure function），所以对有副作用的随机函数 &lt;code&gt;math.random&lt;/code&gt; 和 &lt;code&gt;math.randornseed&lt;/code&gt; 进行替换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建排序辅助函数 &lt;code&gt; _redis_compare_helper&lt;/code&gt;，使用辅助函数来对一部分 Redis 命令的结果进行排序，从而消除命令的不确定性&lt;/p&gt;
&lt;p&gt;比如集合元素的排列是无序的， 所以即使两个集合的元素完全相同，输出结果也不一定相同，Redis 将 SMEMBERS 这类在相同数据集上产生不同输出的命令称为带有不确定性的命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 redis.pcall 函数的错误报告辅助函数 &lt;code&gt;_redis_err_handler &lt;/code&gt;，这个函数可以打印出错代码的来源和发生错误的行数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对 Lua 环境中的全局环境进行保护，确保传入服务器的脚本不会因忘记使用 local 关键字，而将额外的全局变量添加到 Lua 环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将完成修改的 Lua 环境保存到服务器状态的 lua 属性中，等待执行服务器传来的 Lua 脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    Lua *lua;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 使用串行化的方式来执行 Redis 命令，所以在任何时间里最多都只会有一个脚本能够被放进 Lua 环境里面运行，因此整个 Redis 服务器只需要创建一个 Lua 环境即可&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;协作组件&lt;/h3&gt;
&lt;h4&gt;伪客户端&lt;/h4&gt;
&lt;p&gt;Redis 服务器为 Lua 环境创建了一个伪客户端负责处理 Lua 脚本中包含的所有 Redis 命令，工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lua 环境将 redis.call 或者 redis.pcall 函数想要执行的命令传给伪客户端&lt;/li&gt;
&lt;li&gt;伪客户端将命令传给命令执行器&lt;/li&gt;
&lt;li&gt;命令执行器执行命令并将命令的执行结果返回给伪客户端&lt;/li&gt;
&lt;li&gt;伪客户端接收命令执行器返回的命令结果，并将结果返回给 Lua 环境&lt;/li&gt;
&lt;li&gt;Lua 将命令结果返回给 redis.call 函数或者 redis.pcall 函数&lt;/li&gt;
&lt;li&gt;redis.call 函数或者 redis.pcall 函数会将命令结果作为返回值返回给脚本的调用者&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-Lua%E4%BC%AA%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%89%A7%E8%A1%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;脚本字典&lt;/h4&gt;
&lt;p&gt;Redis 服务器为 Lua 环境创建 lua_scripts 字典，键为某个 Lua 脚本的 SHA1 校验和（checksum），值则是校验和对应的 Lua 脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    dict *lua_scripts;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器会将所有被 EVAL 命令执行过的 Lua 脚本，以及所有被 SCRIPT LOAD 命令载入过的 Lua 脚本都保存到 lua_scripts 字典&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SCRIPT LOAD &quot;return &apos;hi&apos;&quot;
&quot;2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3&quot; # 字典的键，SHA1 校验和
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;命令实现&lt;/h3&gt;
&lt;h4&gt;脚本函数&lt;/h4&gt;
&lt;p&gt;EVAL 命令的执行的第一步是为传入的脚本定义一个相对应的 Lua 函数，Lua 函数的名字由 f_ 前缀加上脚本的 SHA1 校验和（四十个字符长）组成，而函数的体（body）则是脚本本身&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EVAL &quot;return &apos;hello world&apos;&quot; 0 
# 命令将会定义以下的函数
function f_533203lc6b470dc5a0dd9b4bf2030dea6d65de91() {
	return &apos;hello world&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用函数来保存客户端传入的脚本有以下优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过函数的局部性来让 Lua 环境保持清洁，减少了垃圾回收的工作最， 并且避免了使用全局变量&lt;/li&gt;
&lt;li&gt;如果某个脚本在 Lua 环境中被定义过至少一次，那么只需要 SHA1 校验和，服务器就可以在不知道脚本本身的情况下，直接通过调用 Lua 函数来执行脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EVAL 命令第二步是将客户端传入的脚本保存到服务器的 lua_scripts 字典里，在字典中新添加一个键值对&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;执行函数&lt;/h4&gt;
&lt;p&gt;EVAL 命令第三步是执行脚本函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将 EVAL 命令中传入的&lt;strong&gt;键名参数和脚本参数&lt;/strong&gt;分别保存到 KEYS 数组和 ARGV 数组，将这两个数组作为&lt;strong&gt;全局变量&lt;/strong&gt;传入到 Lua 环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为 Lua 环境装载超时处理钩子（hook），这个钩子可以在脚本出现超时运行情况时，让客户端通过 &lt;code&gt;SCRIPT KILL&lt;/code&gt; 命令停止脚本，或者通过 SHUTDOWN 命令直接关闭服务器&lt;/p&gt;
&lt;p&gt;因为 Redis 是单线程的执行命令，当 Lua 脚本阻塞时需要兜底策略，可以中断执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行脚本函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移除之前装载的超时钩子&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将执行脚本函数的结果保存到客户端状态的输出缓冲区里，等待服务器将结果返回给客户端&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;EVALSHA&lt;/h4&gt;
&lt;p&gt;EVALSHA 命令的实现原理就是根据脚本的 SHA1 校验和来调用&lt;strong&gt;脚本对应的函数&lt;/strong&gt;，如果函数在 Lua 环境中不存在，找不到 f_ 开头的函数，就会返回 &lt;code&gt;SCRIPT NOT FOUND&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;管理命令&lt;/h3&gt;
&lt;p&gt;Redis 中与 Lua 脚本有关的管理命令有四个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SCRIPT FLUSH：用于清除服务器中所有和 Lua 脚本有关的信息，会释放并重建 lua_scripts 字典，关闭现有的 Lua 环境并重新创建一个新的 Lua 环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SCRIPT EXISTS：根据输入的 SHA1 校验和（允许一次传入多个校验和），检查校验和对应的脚本是否存在于服务器中，通过检查 lua_scripts 字典实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SCRIPT LOAD：在 Lua 环境中为脚本创建相对应的函数，然后将脚本保存到 lua_scripts字典里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; SCRIPT LOAD &quot;return &apos;hi&apos;&quot;
&quot;2f3lba2bb6d6a0f42ccl59d2e2dad55440778de3&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SCRIPT KILL：停止脚本&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果服务器配置了 lua-time-li­mit 选项，那么在每次执行 Lua 脚本之前，都会设置一个超时处理的钩子。钩子会在脚本运行期间会定期检查运行时间是否超过配置时间，如果超时钩子将定期在脚本运行的间隙中，查看是否有 SCRIPT KILL 或者 SHUTDOWN 到达：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果超时运行的脚本没有执行过写入操作，客户端可以通过 SCRIPT KILL 来停止这个脚本&lt;/li&gt;
&lt;li&gt;如果执行过写入操作，客户端只能用 SHUTDOWN nosave 命令来停止服务器，防止不合法的数据被写入数据库中&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;脚本复制&lt;/h3&gt;
&lt;h4&gt;命令复制&lt;/h4&gt;
&lt;p&gt;当服务器运行在复制模式时，具有写性质的脚本命令也会被复制到从服务器，包括 EVAL、EVALSHA、SCRIPT FLUSH，以及 SCRIPT LOAD 命令&lt;/p&gt;
&lt;p&gt;Redis 复制 EVAL、SCRIPT FLUSH、SCRIPT LOAD 三个命令的方法和复制普通 Redis 命令的方法一样，当主服务器执行完以上三个命令的其中一个时，会直接将被执行的命令传播（propagate）给所有从服务器，在从服务器中产生相同的效果&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;EVALSHA&lt;/h4&gt;
&lt;p&gt;EVALSHA 命令的复制操作相对复杂，因为多个从服务器之间载入 Lua 脚本的清况各有不同，一个在主服务器被成功执行的 EVALSHA 命令，在从服务器执行时可能会出现脚本未找到（not found）错误&lt;/p&gt;
&lt;p&gt;Redis 要求主服务器在传播 EVALSHA 命令时，必须确保 EVALSHA 命令要执行的脚本已经被所有从服务器载入过，如果不能确保主服务器会&lt;strong&gt;将 EVALSHA 命令转换成一个等价的 EVAL 命令&lt;/strong&gt;，然后通过传播 EVAL 命令来代替 EVALSHA 命令&lt;/p&gt;
&lt;p&gt;主服务器使用服务器状态的 repl_scriptcache_dict 字典记录已经将哪些脚本传播给了&lt;strong&gt;所有从服务器&lt;/strong&gt;，当一个校验和出现在字典时，说明校验和对应的 Lua 脚本已经传播给了所有从服务器，主服务器可以直接传播 EVALSHA 命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
    // 键是一个个 Lua 脚本的 SHA1 校验和，值则全部都是 NULL
    dict *repl_scriptcache_dict;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：每当主服务器添加一个新的从服务器时，都会清空 repl_scriptcache_dict 字典，因为字典里面记录的脚本已经不再被所有从服务器载入过，所以服务器以清空字典的方式，强制重新向所有从服务器传播脚本&lt;/p&gt;
&lt;p&gt;通过使用 EVALSHA 命令指定的 SHA1 校验和，以及 lua_scripts 字典保存的 Lua 脚本，可以将一个 EVALSHA 命令转化为 EVAL 命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EVALSHA &quot;533203lc6b470dc5a0dd9b4bf2030dea6d65de91&quot; 0 
# -&amp;gt; 转换
EVAL &quot;return&apos;hello world&apos;&quot; 0 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本内容 &lt;code&gt;&quot;return&apos;hello world&apos;&quot;&lt;/code&gt; 来源于 lua_scripts 字典 533203lc6b470dc5a0dd9b4bf2030dea6d65de91 键的值&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;分布式锁&lt;/h2&gt;
&lt;h3&gt;基本操作&lt;/h3&gt;
&lt;p&gt;在分布式场景下，锁变量需要由一个共享存储系统来维护，多个客户端才可以通过访问共享存储系统来访问锁变量，加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值多步操作&lt;/p&gt;
&lt;p&gt;Redis 分布式锁的基本使用，悲观锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 SETNX 设置一个公共锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SETNX lock-key value	# value任意数，返回为1设置成功，返回为0设置失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NX&lt;/code&gt;：只在键不存在时，才对键进行设置操作，&lt;code&gt;SET key value NX&lt;/code&gt; 效果等同于 &lt;code&gt;SETNX key value&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XX&lt;/code&gt;：只在键已经存在时，才对键进行设置操作&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EX&lt;/code&gt;：设置键 key 的过期时间，单位时秒&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PX&lt;/code&gt;：设置键 key 的过期时间，单位时毫秒&lt;/p&gt;
&lt;p&gt;说明：由于 &lt;code&gt;SET&lt;/code&gt; 命令加上选项已经可以完全取代 SETNX、SETEX、PSETEX 的功能，Redis 不推荐使用这几个命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作完毕通过 DEL 操作释放锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEL lock-key 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 EXPIRE 为锁 key 添加存活（持有）时间，过期自动删除（放弃）锁，防止线程出现异常，无法释放锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EXPIRE lock-key second 
PEXPIRE lock-key milliseconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 EXPIRE 设置过期时间缺乏原子性，如果在 SETNX 和 EXPIRE 之间出现异常，锁也无法释放&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 SET 时指定过期时间，保证原子性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET key value NX [EX seconds | PX milliseconds]





&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;防误删&lt;/h3&gt;
&lt;p&gt;场景描述：线程 A 正在执行，但是业务阻塞，在锁的过期时间内未执行完成，过期删除后线程 B 重新获取到锁，此时线程 A 执行完成，删除锁，导致线程 B 的锁被线程 A 误删&lt;/p&gt;
&lt;p&gt;SETNX 获取锁时，设置一个指定的唯一值（UUID），释放前获取这个值，判断是否自己的锁，防止出现线程之间误删了其他线程的锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 加锁, unique_value作为客户端唯一性的标识，
// PX 10000 则表示 lock_key 会在 10s 后过期，以免客户端在这期间发生异常而无法释放锁
SET lock_key unique_value NX PX 10000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lua 脚本（unlock.script）实现的释放锁操作的伪代码：key 类型参数会放入 KEYS 数组，其它参数会放入 ARGV 数组，在脚本中通过 KEYS 和 ARGV 传递参数，&lt;strong&gt;保证判断标识和释放锁这两个操作的原子性&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EVAL &quot;return redis.call(&apos;set&apos;, KEYS[1], ARGV[1])&quot; 1 lock_key unique_value # 1 代表需要一个参数
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 释放锁，KEYS[1] 就是锁的 key，ARGV[1] 就是标识值，避免误释放
// 获取标识值，判断是否与当前线程标示一致
if redis.call(&quot;get&quot;, KEYS[1]) == ARGV[1] then
    return redis.call(&quot;del&quot;, KEYS[1])
else
    return 0
end
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;优化锁&lt;/h3&gt;
&lt;h4&gt;不可重入&lt;/h4&gt;
&lt;p&gt;不可重入：同一个线程无法多次获取同一把锁&lt;/p&gt;
&lt;p&gt;使用 hash 键，filed 是加锁的线程标识， value 是&lt;strong&gt;锁重入次数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|    key    |       value       |
|           |  filed  |  value  |
|-------------------------------|
|  lock_key | thread1 |    1    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;锁重入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加锁时判断锁的 filed 属性是否是当前线程，如果是将 value 加 1&lt;/li&gt;
&lt;li&gt;解锁时判断锁的 filed 属性是否是当前线程，首先将 value 减一，如果 value 为 0 直接释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Lua 脚本保证多条命令的原子性&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;不可重试&lt;/h4&gt;
&lt;p&gt;不可重试：获取锁只尝试一次就返回 false，没有重试机制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用 Lua 脚本尝试获取锁，获取失败获取锁的剩余超时时间 ttl，或者通过参数传入线程抢锁允许等待的时间&lt;/li&gt;
&lt;li&gt;利用订阅功能订阅锁释放的信息，然后线程挂起等待 ttl 时间&lt;/li&gt;
&lt;li&gt;利用 Lua 脚本在释放锁时，发布一条锁释放的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;超时释放&lt;/h4&gt;
&lt;p&gt;超时释放：锁超时释放可以避免死锁，但如果是业务执行耗时较长，需要进行锁续时，防止业务未执行完提前释放锁&lt;/p&gt;
&lt;p&gt;看门狗 Watch Dog 机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取锁成功后，提交周期任务，每隔一段时间（Redisson 中默认为过期时间 / 3），重置一次超时时间&lt;/li&gt;
&lt;li&gt;如果服务宕机，Watch Dog 机制线程就停止，就不会再延长 key 的过期时间&lt;/li&gt;
&lt;li&gt;释放锁后，终止周期任务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;主从一致&lt;/h4&gt;
&lt;p&gt;主从一致性：集群模式下，主从同步存在延迟，当加锁后主服务器宕机时，从服务器还没同步主服务器中的锁数据，此时从服务器升级为主服务器，其他线程又可以获取到锁&lt;/p&gt;
&lt;p&gt;将服务器升级为多主多从：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取锁需要从所有主服务器 SET 成功才算获取成功&lt;/li&gt;
&lt;li&gt;某个 master 宕机，slave 还没有同步锁数据就升级为 master，其他线程尝试加锁会加锁失败，因为其他 master 上已经存在该锁&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;主从复制&lt;/h2&gt;
&lt;h3&gt;基本操作&lt;/h3&gt;
&lt;h4&gt;主从介绍&lt;/h4&gt;
&lt;p&gt;主从复制：一个服务器去复制另一个服务器，被复制的服务器为主服务器 master，复制的服务器为从服务器 slave&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;master 用来&lt;strong&gt;写数据&lt;/strong&gt;，执行写操作时，将出现变化的数据自动同步到 slave，很少会进行读取操作&lt;/li&gt;
&lt;li&gt;slave 用来读数据，禁止在 slave 服务器上进行读操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进行复制中的主从服务器双方的数据库将保存相同的数据，将这种现象称作&lt;strong&gt;数据库状态一致&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主从复制的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;薪火相传&lt;/strong&gt;：一个 slave 可以是下一个 slave 的 master，slave 同样可以接收其他 slave 的连接和同步请求，那么该 slave 作为了链条中下一个的 master，可以有效减轻 master 的写压力，去中心化降低风险&lt;/p&gt;
&lt;p&gt;注意：主机挂了，从机还是从机，无法写数据了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;反客为主&lt;/strong&gt;：当一个 master 宕机后，后面的 slave 可以立刻升为 master，其后面的 slave 不做任何修改&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主从复制的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读写分离&lt;/strong&gt;：master 写、slave 读，提高服务器的读写负载能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载均衡&lt;/strong&gt;：基于主从结构，配合读写分离，由 slave 分担 master 负载，并根据需求的变化，改变 slave 的数量，通过多个从节点分担数据读取负载，大大提高 Redis 服务器并发量与数据吞吐量&lt;/li&gt;
&lt;li&gt;故障恢复：当 master 出现问题时，由 slave 提供服务，实现快速的故障恢复&lt;/li&gt;
&lt;li&gt;数据冗余：实现数据热备份，是持久化之外的一种数据冗余方式&lt;/li&gt;
&lt;li&gt;高可用基石：基于主从复制，构建哨兵模式与集群，实现 Redis 的高可用方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;三高&lt;/strong&gt;架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;高并发：应用提供某一业务要能支持很多客户端同时访问的能力，称为并发&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高性能：性能最直观的感受就是速度快，时间短&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高可用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可用性：应用服务在全年宕机的时间加在一起就是全年应用服务不可用的时间&lt;/li&gt;
&lt;li&gt;业界可用性目标 5 个 9，即 99.999%，即服务器年宕机时长低于 315 秒，约 5.25 分钟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;操作指令&lt;/h4&gt;
&lt;p&gt;系统状态指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INFO replication
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;master 和 slave 互连：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：客户端发送命令，设置 slaveof 选项，产生主从结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slaveof masterip masterport
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：服务器带参启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server --slaveof masterip masterport
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式三：服务器配置（主流方式）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slaveof masterip masterport
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主从断开连接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;slave 断开连接后，不会删除已有数据，只是不再接受 master 发送的数据，可以作&lt;strong&gt;为从服务器升级为主服务器的指令&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slaveof no one	
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;授权访问：master 有服务端和客户端，slave 也有服务端和客户端，不仅服务端之间可以发命令，客户端也可以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;master 客户端发送命令设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;requirepass password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;master 配置文件设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config set requirepass password
config get requirepass
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;slave 客户端发送命令设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auth password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slave 配置文件设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;masterauth password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slave 启动服务器设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server –a password
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;复制流程&lt;/h3&gt;
&lt;h4&gt;旧版复制&lt;/h4&gt;
&lt;p&gt;Redis 的复制功能分为同步（sync）和命令传播（command propagate）两个操作，主从库间的复制是&lt;strong&gt;异步进行的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态，该过程又叫全量复制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从服务器向主服务器发送 SYNC 命令来进行同步&lt;/li&gt;
&lt;li&gt;收到 SYNC 的主服务器执行 BGSAVE 命令，在后台生成一个 RDB 文件，并使用一个&lt;strong&gt;缓冲区&lt;/strong&gt;记录从现在开始执行的所有&lt;strong&gt;写命令&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当 BGSAVE 命令执行完毕时，主服务器会将 RDB 文件发送给从服务器&lt;/li&gt;
&lt;li&gt;从服务接收并载入 RDB 文件（从服务器会&lt;strong&gt;清空原有数据&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;缓冲区记录了 RDB 文件所在状态后的所有写命令，主服务器将在缓冲区的所有命令发送给从服务器，从服务器执行这些写命令&lt;/li&gt;
&lt;li&gt;至此从服务器的数据库状态和主服务器一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;命令传播用于在主服务器的数据库状态被修改，导致主从数据库状态出现不一致时， 让主从服务器的数据库重新回到一致状态&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主服务器会将自己执行的写命令，也即是造成主从服务器不一致的那条写命令，发送给从服务器&lt;/li&gt;
&lt;li&gt;从服务器接受命令并执行，主从服务器将再次回到一致状态&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;功能缺陷&lt;/h4&gt;
&lt;p&gt;SYNC 本身就是一个非常消耗资源的操作，每次执行 SYNC 命令，都需要执行以下动作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成 RDB 文件，耗费主服务器大量 CPU 、内存和磁盘 I/O 资源&lt;/li&gt;
&lt;li&gt;RDB 文件发送给从服务器，耗费主从服务器大量的网络资源（带宽和流量），并对主服务器响应命令请求的时间产生影响&lt;/li&gt;
&lt;li&gt;从服务器载入 RDB 文件，期间会因为阻塞而没办法处理命令请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SYNC 命令下的从服务器对主服务器的复制分为两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初次复制：从服务器没有复制过任何主服务器，或者从服务器当前要复制的主服务器和上一次复制的主服务器不同&lt;/li&gt;
&lt;li&gt;断线后重复制：处于命令传播阶段的主从服务器因为网络原因而中断了复制，自动重连后并继续复制主服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;旧版复制在断线后重复制时，也会创建 RDB 文件进行&lt;strong&gt;全量复制&lt;/strong&gt;，但是从服务器只需要断线时间内的这部分数据，所以旧版复制的实现方式非常浪费资源&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;新版复制&lt;/h4&gt;
&lt;p&gt;Redis 从 2.8 版本开始，使用 PSYNC 命令代替 SYNC 命令来执行复制时的&lt;strong&gt;同步操作&lt;/strong&gt;（命令传播阶段相同），解决了旧版复制在处理断线重复制情况的低效问题&lt;/p&gt;
&lt;p&gt;PSYNC 命令具有完整重同步（full resynchronization）和&lt;strong&gt;部分重同步&lt;/strong&gt;（partial resynchronization）两种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完整重同步：处理初次复制情况，执行步骤和 SYNC命令基本一样&lt;/li&gt;
&lt;li&gt;部分重同步：处理断线后重复制情况，主服务器可以将主从连接断开期间执行的写命令发送给从服务器，从服务器只要接收并执行这些写命令，就可以将数据库更新至主服务器当前所处的状态，该过程又叫&lt;strong&gt;部分复制&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;部分同步&lt;/h3&gt;
&lt;p&gt;部分重同步功能由以下三个部分构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主服务器的复制偏移量（replication offset）和从服务器的复制偏移量&lt;/li&gt;
&lt;li&gt;主服务器的复制积压缓冲区（replication backlog）&lt;/li&gt;
&lt;li&gt;服务器的运行 ID (run ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;偏移量&lt;/h4&gt;
&lt;p&gt;主服务器和从服务器会分别维护一个复制偏移量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主服务器每次向从服务器传播 N 个字节的数据时，就将自己的复制偏移量的值加上 N&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从服务器每次收到主服务器传播来的 N 个字节的数据时，就将自己的复制偏移量的值加上 N&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过对比主从服务器的复制偏移量，可以判断主从服务器是否处于一致状态&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主从服务器的偏移量是相同的，说明主从服务器处于一致状态&lt;/li&gt;
&lt;li&gt;主从服务器的偏移量是不同的，说明主从服务器处于不一致状态&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓冲区&lt;/h4&gt;
&lt;p&gt;复制积压缓冲区是由主服务器维护的一个固定长度（fixed-size）先进先出（FIFO）队列，默认大小为 1MB&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出队规则跟普通的先进先出队列一样&lt;/li&gt;
&lt;li&gt;入队规则是当入队元素的数量大于队列长度时，最先入队的元素会被弹出，然后新元素才会被放入队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当主服务器进行&lt;strong&gt;命令传播时，不仅会将写命令发送给所有从服务器，还会将写命令入队到复制积压缓冲区&lt;/strong&gt;，缓冲区会保存着一部分最近传播的写命令，并且缓冲区会为队列中的每个字节记录相应的复制偏移量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%A4%8D%E5%88%B6%E7%A7%AF%E5%8E%8B%E7%BC%93%E5%86%B2%E5%8C%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从服务器会通过 PSYNC 命令将自己的复制偏移量 offset 发送给主服务器，主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;offset 之后的数据（即 offset+1）仍然存在于复制积压缓冲区里，那么主服务器将对从服务器执行部分重同步操作&lt;/li&gt;
&lt;li&gt;offset 之后的数据已经不在复制积压缓冲区，说明部分数据已经丢失，那么主服务器将对从服务器执行完整重同步操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;复制缓冲区大小设定不合理，会导致&lt;strong&gt;数据溢出&lt;/strong&gt;。比如主服务器需要执行大量写命令，又或者主从服务器断线后重连接所需的时间较长，导致缓冲区中的数据已经丢失，则必须进行完整重同步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repl-backlog-size ?mb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建议设置如下，这样可以保证绝大部分断线情况都能用部分重同步来处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从服务器断线后重新连接上主服务器所需的平均时间 second&lt;/li&gt;
&lt;li&gt;获取 master 平均每秒产生写命令数据总量 write_size_per_second&lt;/li&gt;
&lt;li&gt;最优复制缓冲区空间 = 2 * second * write_size_per_second&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;运行ID&lt;/h4&gt;
&lt;p&gt;服务器运行 ID（run ID）：是每一台服务器每次运行的身份识别码，在服务器启动时自动生成，由 40 位随机的十六进制字符组成，一台服务器多次运行可以生成多个运行 ID&lt;/p&gt;
&lt;p&gt;作用：服务器间进行传输识别身份，如果想两次操作均对同一台服务器进行，&lt;strong&gt;每次必须操作携带对应的运行 ID&lt;/strong&gt;，用于对方识别&lt;/p&gt;
&lt;p&gt;从服务器对主服务器进行初次复制时，主服务器将自己的运行 ID 传送给从服务器，然后从服务器会将该运行 ID 保存。当从服务器断线并重新连上一个主服务器时，会向当前连接的主服务器发送之前保存的运行 ID：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果运行 ID 和当前连接的主服务器的运行 ID 相同，说明从服务器断线之前复制的就是当前连接的这个主服务器，执行部分重同步&lt;/li&gt;
&lt;li&gt;如果不同，需要执行完整重同步操作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;PSYNC&lt;/h4&gt;
&lt;p&gt;PSYNC 命令的调用方法有两种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果从服务器之前没有复制过任何主服务器，或者执行了 &lt;code&gt;SLAVEOF no one&lt;/code&gt;，开始一次新的复制时将向主服务器发送 &lt;code&gt;PSYNC ? -1&lt;/code&gt; 命令，主动请求主服务器进行完整重同步&lt;/li&gt;
&lt;li&gt;如果从服务器已经复制过某个主服务器，那么从服务器在开始一次新的复制时将向主服务器发送 &lt;code&gt;PSYNC &amp;lt;runid&amp;gt; &amp;lt;offset&amp;gt;&lt;/code&gt; 命令，runid 是上一次复制的主服务器的运行 ID，offset 是复制的偏移量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接收到 PSYNC 命令的主服务器会向从服务器返回以下三种回复的其中一种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行完整重同步操作：返回 &lt;code&gt;+FULLRESYNC &amp;lt;runid&amp;gt; &amp;lt;offset&amp;gt;&lt;/code&gt;，runid 是主服务器的运行 ID，offset 是主服务器的复制偏移量&lt;/li&gt;
&lt;li&gt;执行部分重同步操作：返回 &lt;code&gt;+CONTINUE&lt;/code&gt;，从服务器收到该回复说明只需要等待主服务器发送缺失的部分数据即可&lt;/li&gt;
&lt;li&gt;主服务器的版本低于 Redis2.8：返回 &lt;code&gt;-ERR&lt;/code&gt;，版本过低识别不了 PSYNC，从服务器将向主服务器发送 SYNC 命令&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;复制实现&lt;/h3&gt;
&lt;h4&gt;实现流程&lt;/h4&gt;
&lt;p&gt;通过向从服务器发送 SLAVEOF 命令，可以让从服务器去复制一个主服务器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置主服务器的地址和端口：将 SLAVEOF 命令指定的 ip 和 port 保存到服务器状态 redisServer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
	// 主服务器的地址 
    char *masterhost; 
	 //主服务器的端口 
    int masterport; 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SLAVEOF 命令是一个&lt;strong&gt;异步命令&lt;/strong&gt;，在完成属性的设置后服务器直接返回 OK，而实际的复制工作将在 OK 返回之后才真正开始执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建立套接字连接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从服务器 connect 主服务器建立套接字连接，成功后从服务器将为这个套接字关联一个用于复制工作的文件事件处理器，负责执行后续的复制工作，如接收 RDB 文件、接收主服务器传播来的写命令等&lt;/li&gt;
&lt;li&gt;主服务器在接受 accept 从务器的套接字连接后，将为该套接字创建相应的客户端状态，将从服务器看作一个客户端，从服务器将同时具有 server 和 client（可以发命令）两个身份&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送 PING 命令：从服务器向主服务器发送一个 PING 命令，检查主从之间的通信是否正常、主服务器处理命令的能力是否正常&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回错误，表示主服务器无法处理从服务器的命令请求（忙碌），从服务器断开并重新创建连向主服务器的套接字&lt;/li&gt;
&lt;li&gt;返回命令回复，但从服务器不能在规定的时间内读取出命令回复的内容，表示主从之间的网络状态不佳，需要断开重连&lt;/li&gt;
&lt;li&gt;读取到 PONG，表示一切状态正常，可以执行复制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;身份验证：如果从服务器设置了 masterauth 选项就进行身份验证，将向主服务器发送一条 AUTH 命令，命令参数为从服务器 masterauth 选项的值，如果主从设置的密码不相同，那么主将返回一个 invalid password 错误&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送端口信息：身份验证后&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从服务器执行命令 &lt;code&gt;REPLCONF listening-port &amp;lt;port­number&amp;gt;&lt;/code&gt;， 向主服务器发送从服务器的监听端口号&lt;/li&gt;
&lt;li&gt;主服务器在接收到这个命令后，会将端口号记录在对应的客户端状态 redisClient.slave_listening_port 属性中：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同步：从服务器将向主服务器发送 PSYNC 命令，在同步操作执行之后，&lt;strong&gt;主从服务器双方都是对方的客户端&lt;/strong&gt;，可以相互发送命令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;完整重同步：主服务器需要成为从服务器的客户端，才能将保存在缓冲区里面的写命令发送给从服务器执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部分重同步：主服务器需要成为从服务器的客户端，才能向从服务器发送保存在复制积压缓冲区里面的写命令&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令传播：主服务器将写命令发送给从服务器，保持数据库的状态一致&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;复制图示&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E6%B5%81%E7%A8%8B%E6%9B%B4%E6%96%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;心跳检测&lt;/h3&gt;
&lt;h4&gt;心跳机制&lt;/h4&gt;
&lt;p&gt;心跳机制：进入命令传播阶段，&lt;strong&gt;从服务器&lt;/strong&gt;默认会以每秒一次的频率，&lt;strong&gt;向主服务器发送命令&lt;/strong&gt;：&lt;code&gt;REPLCONF ACK &amp;lt;replication_offset&amp;gt;&lt;/code&gt;，replication_offset 是从服务器当前的复制偏移量&lt;/p&gt;
&lt;p&gt;心跳的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检测主从服务器的网络连接状态&lt;/li&gt;
&lt;li&gt;辅助实现 min-slaves 选项&lt;/li&gt;
&lt;li&gt;检测命令丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;网络状态&lt;/h4&gt;
&lt;p&gt;如果主服务器超过一秒钟没有收到从服务器发来的 REPLCONF ACK 命令，主服务就认为主从服务器之间的连接出现问题&lt;/p&gt;
&lt;p&gt;向主服务器发送 &lt;code&gt;INFO replication&lt;/code&gt; 命令，lag 一栏表示从服务器最后一次向主服务器发送 ACK 命令距离现在多少秒：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; INFO replication 
# Replication 
role:master 
connected_slaves:2 
slave0: ip=127.0.0.1,port=11111,state=online,offset=123,lag=0 # 刚刚发送过 REPLCONF ACK 
slavel: ip=127.0.0.1,port=22222,state=online,offset=456,lag=3 # 3秒之前发送过REPLCONF ACK 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一般情况下，lag 的值应该在 0 或者 1 秒之间跳动，如果超过 1 秒说明主从服务器之间的连接出现了故障&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;配置选项&lt;/h4&gt;
&lt;p&gt;Redis 的 min-slaves-to-write 和 min-slaves-max-lag 两个选项可以防止主服务器在&lt;strong&gt;不安全的情况下&lt;/strong&gt;拒绝执行写命令&lt;/p&gt;
&lt;p&gt;比如向主服务器设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;min-slaves-to-write：主库最少有 N 个健康的从库存活才能执行写命令，没有足够的从库直接拒绝写入&lt;/li&gt;
&lt;li&gt;min-slaves-max-lag：从库和主库进行数据复制时的 ACK 消息延迟的最大时间&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;min-slaves-to-write 5
min-slaves-max-lag 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么在从服务器的数少于 5 个，或者 5 个从服务器的延迟（lag）值都大于或等于10 秒时，主服务器将拒绝执行写命令&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;命令丢失&lt;/h4&gt;
&lt;p&gt;检测命令丢失：由于网络或者其他原因，主服务器传播给从服务器的写命令丢失，那么当从服务器向主服务器发送 REPLCONF ACK 命令时，主服务器会检查从服务器的复制偏移量是否小于自己的，然后在复制积压缓冲区里找到从服务器缺少的数据，并将这些数据重新发送给从服务器&lt;/p&gt;
&lt;p&gt;说明：REPLCONF ACK 命令和复制积压缓冲区都是 Redis 2.8 版本新增的，在 Redis 2.8 版本以前，即使命令在传播过程中丢失，主从服务器都不会注意到，也不会向从服务器补发丢失的数据，所以为了保证&lt;strong&gt;主从复制的数据一致性&lt;/strong&gt;，最好使用 2.8 或以上版本的 Redis&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;h4&gt;重启恢复&lt;/h4&gt;
&lt;p&gt;系统不断运行，master 的数据量会越来越大，一旦 &lt;strong&gt;master 重启&lt;/strong&gt;，runid 将发生变化，会导致全部 slave 的全量复制操作&lt;/p&gt;
&lt;p&gt;解决方法：本机保存上次 runid，重启后恢复该值，使所有 slave 认为还是之前的 master&lt;/p&gt;
&lt;p&gt;优化方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;master 内部创建 master_replid 变量，使用 runid 相同的策略生成，并发送给所有 slave&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 master 关闭时执行命令 &lt;code&gt;shutdown save&lt;/code&gt;，进行 RDB 持久化，将 runid 与 offset 保存到 RDB 文件中&lt;/p&gt;
&lt;p&gt;&lt;code&gt;redis-check-rdb dump.rdb&lt;/code&gt; 命令可以查看该信息，保存为 repl-id 和 repl-offset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;master 重启后加载 RDB 文件，恢复数据，将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中，master_repl_id = repl-id，master_repl_offset = repl-offset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 info 命令可以查看该信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;网络中断&lt;/h4&gt;
&lt;p&gt;master 的 CPU 占用过高或 slave 频繁断开连接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;出现的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slave 每 1 秒发送 REPLCONF ACK 命令到 master&lt;/li&gt;
&lt;li&gt;当 slave 接到了慢查询时（keys * ，hgetall 等），会大量占用 CPU 性能&lt;/li&gt;
&lt;li&gt;master 每 1 秒调用复制定时函数 replicationCron()，比对 slave 发现长时间没有进行响应&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终导致 master 各种资源（输出缓冲区、带宽、连接等）被严重占用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方法：通过设置合理的超时时间，确认是否释放 slave&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repl-timeout	# 该参数定义了超时时间的阈值（默认60秒），超过该值，释放slave
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;slave 与 master 连接断开&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;出现的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;master 发送 ping 指令频度较低&lt;/li&gt;
&lt;li&gt;master 设定超时时间较短&lt;/li&gt;
&lt;li&gt;ping 指令在网络中存在丢包&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方法：提高 ping 指令发送的频度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repl-ping-slave-period	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超时时间 repl-time 的时间至少是 ping 指令频度的5到10倍，否则 slave 很容易判定超时&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;一致性&lt;/h4&gt;
&lt;p&gt;网络信息不同步，数据发送有延迟，导致多个 slave 获取相同数据不同步&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优化主从间的网络环境&lt;/strong&gt;，通常放置在同一个机房部署，如使用阿里云等云服务器时要注意此现象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监控主从节点延迟（通过offset）判断，如果 slave 延迟过大，&lt;strong&gt;暂时屏蔽程序对该 slave 的数据访问&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slave-serve-stale-data yes|no
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启后仅响应 info、slaveof 等少数命令（慎用，除非对数据一致性要求很高）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个 slave 同时对 master 请求数据同步，master 发送的 RDB 文件增多，会对带宽造成巨大冲击，造成 master 带宽不足，因此数据同步需要根据业务需求，适量错峰&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;哨兵模式&lt;/h2&gt;
&lt;h3&gt;哨兵概述&lt;/h3&gt;
&lt;p&gt;Sentinel（哨兵）是 Redis 的高可用性（high availability）解决方案，由一个或多个 Sentinel 实例 instance 组成的 Sentinel 系统可以监视任意多个主服务器，以及这些主服务器的所有从服务器，并在被监视的主服务器下线时进行故障转移&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵系统.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双环图案表示主服务器&lt;/li&gt;
&lt;li&gt;单环图案表示三个从服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哨兵的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;监控：监控 master 和 slave，不断的检查 master 和 slave 是否正常运行，master 存活检测、master 与 slave 运行情况检测&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通知：当被监控的服务器出现问题时，向其他哨兵发送通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自动故障转移：断开 master 与 slave 连接，选取一个 slave 作为 master，将其他 slave 连接新的 master，并告知客户端新的服务器地址&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;启用哨兵&lt;/h3&gt;
&lt;h4&gt;配置方式&lt;/h4&gt;
&lt;p&gt;配置三个哨兵 sentinel.conf：一般多个哨兵配置相同、端口不同，特殊需求可以配置不同的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;port 26401
dir &quot;/redis/data&quot;
sentinel monitor mymaster 127.0.0.1 6401 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 20000
sentinel parallel-sync mymaster 1
sentinel deny-scripts-reconfig yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置哨兵监听的主服务器信息，判断主观下线所需要的票数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel monitor &amp;lt;master-name&amp;gt; &amp;lt;master_ip&amp;gt; &amp;lt;master_port&amp;gt; &amp;lt;quorum&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定哨兵在监控 Redis 服务时，设置判定服务器宕机的时长，该设置控制是否进行主从切换&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel down-after-milliseconds &amp;lt;master-name&amp;gt; &amp;lt;million_seconds&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;出现故障后，故障切换的最大超时时间，超过该值，认定切换失败，默认 3 分钟&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel failover-timeout &amp;lt;master_name&amp;gt; &amp;lt;million_seconds&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;故障转移时，同时进行主从同步的 slave 数量，数值越大，要求网络资源越高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel parallel-syncs &amp;lt;master_name&amp;gt; &amp;lt;sync_slave_number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动哨兵：服务端命令（Linux 命令）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-sentinel filename
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;p&gt;Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器，当一个 Sentinel 启动时，首先初始化 Redis 服务器，但是初始化过程和普通 Redis 服务器的初始化过程并不完全相同，哨兵&lt;strong&gt;不提供数据相关服务&lt;/strong&gt;，所以不会载入 RDB、AOF 文件&lt;/p&gt;
&lt;p&gt;整体流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始化服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化 Sentinel 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据给定的配置文件，初始化 Sentinel 的监视主服务器列表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建连向主服务器的网络连接&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;代码替换&lt;/h4&gt;
&lt;p&gt;将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码&lt;/p&gt;
&lt;p&gt;Redis 服务器端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# define REDIS_SERVERPORT 6379 		// 普通服务器端口
# define REDIS_SENTINEL_PORT 26379 	// 哨兵端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器的命令表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 普通 Redis 服务器
struct redisCommand redisCommandTable[] = {
    {&quot;get&quot;, getCommand, 2, &quot;r&quot;, 0, NULL, 1, 1, 1, 0, 0},
    {&quot;set&quot;, setCommand, -3, &quot;wm&quot;, 0, noPreloadGetKeys, 1, 1, 1, 0, 0},
    //....
}
// 哨兵
struct redisCommand sentinelcmds[] = {
    {&quot;ping&quot;, pingCommand, 1, &quot;&quot;, 0, NULL, 0, 0, 0, 0, 0},
    {&quot;sentinel&quot;, sentinelCommand, -2,&quot;&quot;,0,NULL,0,0,0,0,0},
    {&quot;subscribe&quot;,...}, {&quot;unsubscribe&quot;,...O}, {&quot;psubscribe&quot;,...}, {&quot;punsubscribe&quot;,...},
    {&quot;info&quot;,...}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述表是哨兵模式下客户端可以执行的命令，所以对于 GET、SET 等命令，服务器根本就没有载入&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;哨兵状态&lt;/h4&gt;
&lt;p&gt;服务器会初始化一个 sentinelState 结构，又叫 Sentinel 状态，结构保存了服务器中所有和 Sentinel 功能有关的状态（服务器的一般状态仍然由 redisServer 结构保存）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sentinelState {
    // 当前纪元，用于实现故障转移
    uint64_t current_epoch; 
    
    // 【保存了所有被这个sentinel监视的主服务器】
    dict *masters;
    
    // 是否进入了 TILT 模式
    int tilt;
    // 进入 TILT 模式的时间
    mstime_t tilt_start_time;
    
    // 最后一次执行时间处理的事件
    mstime_t previous_time;
    
    // 目前正在执行的脚本数量
    int running_scripts;
    // 一个FIFO队列，包含了所有需要执行的用户脚本
    list *scripts_queue;
    
} sentinel;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;监控列表&lt;/h4&gt;
&lt;p&gt;Sentinel 状态的初始化将 masters 字典的初始化，根据被载入的 Sentinel 配置文件 conf 来进行属性赋值&lt;/p&gt;
&lt;p&gt;Sentinel 状态中的 masters 字典记录了所有被 Sentinel 监视的&lt;strong&gt;主服务器的相关信息&lt;/strong&gt;，字典的键是被监视主服务器的名字，值是主服务器对应的实例结构&lt;/p&gt;
&lt;p&gt;实例结构是一个 sentinelRedisinstance 数据类型，代表被 Sentinel 监视的实例，这个实例可以是主、从服务器，或者其他 Sentinel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct sentinelRedisinstance {
    // 标识值，记录了实例的类型，以及该实例的当前状态
    int flags;
    
    // 实例的名字，主服务器的名字由用户在配置文件中设置，
    // 从服务器和哨兵的名字由 Sentinel 自动设置，格式为 ip:port，例如 127.0.0.1:6379
    char *name;
    
    // 实例运行的 ID
    char *runid;
    
    // 配置纪元，用于实现故障转移
    uint64_t config_epoch;
    
    // 实例地址
    sentinelAddr *addr; 
    
    // 如果当前实例时主服务器，该字段保存从服务器信息，键是名字格式为 ip:port，值是实例结构
    dict *slaves;
    
    // 所有监视当前服务器的 Sentinel 实例，键是名字格式为 ip:port，值是实例结构
    dict *sentinels;
    
    // sentinel down-after-milliseconds 的值，表示实例无响应多少毫秒后会被判断为主观下线(subjectively down) 
    mstime_t down_after_period;
    
    // sentinel monitor 选项中的quorum参数，判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;
    
    // sentinel parallel-syncs 的值，在执行故障转移操作时，可以同时对新的主服务器进行同步的从服务器数量
    int parallel-syncs;
    
    // sentinel failover-timeout的值，刷新故障迁移状态的最大时限
    mstime_t failover_timeout;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;addr 属性是一个指向 sentinelAddr 的指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct sentinelAddr {
    char *ip;
    int port;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;网络连接&lt;/h4&gt;
&lt;p&gt;初始化 Sentinel 的最后一步是创建连向被监视主服务器的网络连接，Sentinel 将成为主服务器的客户端，可以向主服务器发送命令，并从命令回复中获取相关的信息&lt;/p&gt;
&lt;p&gt;每个被 Sentinel 监视的主服务器，Sentinel 会创建两个连向主服务器的&lt;strong&gt;异步网络连接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令连接：用于向主服务器发送命令，并接收命令回复&lt;/li&gt;
&lt;li&gt;订阅连接：用于订阅主服务器的 &lt;code&gt;_sentinel_:hello&lt;/code&gt; 频道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建立两个连接的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 Redis 目前的发布与订阅功能中，被发送的信息都不会保存在 Redis 服务器里， 如果在信息发送时接收信息的客户端离线或断线，那么这个客户端就会丢失这条信息，为了不丢失 hello 频道的任何信息，Sentinel 必须用一个订阅连接来接收该频道的信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sentinel 还必须向主服务器发送命令，以此来与主服务器进行通信，所以 Sentinel 还必须向主服务器创建命令连接&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：断线的意思就是网络连接断开&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%93%A8%E5%85%B5%E7%B3%BB%E7%BB%9F%E5%BB%BA%E7%AB%8B%E8%BF%9E%E6%8E%A5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;信息交互&lt;/h3&gt;
&lt;h4&gt;获取信息&lt;/h4&gt;
&lt;h5&gt;主服务器&lt;/h5&gt;
&lt;p&gt;Sentinel 默认会以每十秒一次的频率，通过命令连接向被监视的主服务器发送 INFO 命令，来获取主服务器的信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一部分是主服务器本身的信息，包括 runid 域记录的服务器运行 ID，以及 role 域记录的服务器角色&lt;/li&gt;
&lt;li&gt;另一部分是服务器属下所有从服务器的信息，每个从服务器都由一个 slave 字符串开头的行记录，根据这些 IP 地址和端口号，Sentinel 无须用户提供从服务器的地址信息，就可以&lt;strong&gt;自动发现从服务器&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Server 
run_id:76llc59dc3a29aa6fa0609f84lbb6al019008a9c
...
# Replication 
role:master 
...
slave0: ip=l27.0.0.1, port=11111, state=online, offset=22, lag=0
slave1: ip=l27.0.0.1, port=22222, state=online, offset=22, lag=0
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据 run_id 和 role 记录的信息 Sentinel 将对主服务器的实例结构进行更新，比如主服务器重启之后，运行 ID 就会和实例结构之前保存的运行 ID 不同，哨兵检测到这一情况之后就会对实例结构的运行 ID 进行更新&lt;/p&gt;
&lt;p&gt;对于主服务器返回的从服务器信息，用实例结构的 slaves 字典记录了从服务器的信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果从服务器对应的实例结构已经存在，那么 Sentinel 对从服务器的实例结构进行更新&lt;/li&gt;
&lt;li&gt;如果不存在，为这个从服务器新创建一个实例结构加入字典，字典键为 &lt;code&gt;ip:port&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;从服务器&lt;/h5&gt;
&lt;p&gt;当 Sentinel 发现主服务器有新的从服务器出现时，会为这个新的从服务器创建相应的实例结构，还会&lt;strong&gt;创建到从服务器的命令连接和订阅连接&lt;/strong&gt;，所以 Sentinel 对所有的从服务器之间都可以进行命令操作&lt;/p&gt;
&lt;p&gt;Sentinel 默认会以每十秒一次的频率，向从服务器发送 INFO 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Server 
run_id:76llc59dc3a29aa6fa0609f84lbb6al019008a9c	#从服务器的运行 id
...
# Replication 
role:slave 				# 从服务器角色
...
master_host:127.0.0.1 	# 主服务器的 ip
master_port:6379 		# 主服务器的 port
master_link_status:up 	# 主从服务器的连接状态
slave_repl_offset:11111	# 从服务器的复制偏移蜇
slave_priority:100 		# 从服务器的优先级
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优先级属性&lt;/strong&gt;在故障转移时会用到&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据这些信息，Sentinel 会对从服务器的实例结构进行更新&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;发送信息&lt;/h4&gt;
&lt;p&gt;Sentinel 在默认情况下，会以每两秒一次的频率，通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUBLISH _sentinel_:hello &quot;&amp;lt;s_ip&amp;gt;, &amp;lt;s_port&amp;gt;, &amp;lt;s_runid&amp;gt;, &amp;lt;s_epoch&amp;gt;, &amp;lt;m_name&amp;gt;, &amp;lt;m_ip&amp;gt;, &amp;lt;m_port&amp;gt;, &amp;lt;m_epoch&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令向服务器的 &lt;code&gt;_sentinel_:hello&lt;/code&gt; 频道发送了一条信息，信息的内容由多个参数组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以 s_ 开头的参数记录的是 Sentinel 本身的信息&lt;/li&gt;
&lt;li&gt;以 m_ 开头的参数记录的则是主服务器的信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：&lt;strong&gt;通过命令连接发送的频道信息&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;接受信息&lt;/h4&gt;
&lt;h5&gt;订阅频道&lt;/h5&gt;
&lt;p&gt;Sentinel 与一个主或从服务器建立起订阅连接之后，就会通过订阅连接向服务器发送订阅命令，频道的订阅会一直持续到 Sentinel 与服务器的连接断开为止&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SUBSCRIBE _sentinel_:hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;订阅成功后，Sentinel 就可以通过订阅连接从服务器的 &lt;code&gt;_sentinel_:hello&lt;/code&gt; 频道接收信息，对消息分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果信息中记录的 Sentinel 运行 ID 与自己的相同，不做进一步处理&lt;/li&gt;
&lt;li&gt;如果不同，将根据信息中的各个参数，对相应主服务器的实例结构进行更新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sentinel 为主服务器创建的实例结构的 sentinels 字典保存所有同样监视这个&lt;strong&gt;主服务器的 Sentinel 信息&lt;/strong&gt;（包括 Sentinel 自己），字典的键是 Sentinel 的名字，格式为 &lt;code&gt;ip:port&lt;/code&gt;，值是键所对应 Sentinel 的实例结构&lt;/p&gt;
&lt;p&gt;监视同一个服务器的 Sentinel 订阅的频道相同，Sentinel 发送的信息会被其他 Sentinel 接收到（发送信息的为源 Sentinel，接收信息的为目标 Sentinel），目标 Sentinel 在自己的 sentinelState.masters 中查找源 Sentinel 服务器的实例结构进行添加或更新&lt;/p&gt;
&lt;p&gt;因为 Sentinel 可以接收到的频道信息来感知其他 Sentinel 的存在，并通过发送频道信息来让其他 Sentinel 知道自己的存在，所以用户在使用 Sentinel 时并不需要提供各个 Sentinel 的地址信息，&lt;strong&gt;监视同一个主服务器的多个 Sentinel 可以相互发现对方&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;哨兵实例之间可以相互发现，要归功于 Redis 提供发布订阅机制&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;命令连接&lt;/h5&gt;
&lt;p&gt;Sentinel 通过频道信息发现新的 Sentinel，除了创建实例结构，还会创建一个连向新 Sentinel 的命令连接，而新 Sentinel 也同样会创建连向这个 Sentinel 的命令连接，最终监视同一主服务器的多个 Sentinel 将形成相互连接的网络&lt;/p&gt;
&lt;p&gt;作用：&lt;strong&gt;通过命令连接相连的各个 Sentinel&lt;/strong&gt; 可以向其他 Sentinel 发送命令请求来进行信息交换&lt;/p&gt;
&lt;p&gt;Sentinel 之间不会创建订阅连接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sentinel 需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新 Sentinel，所以才创建订阅连接&lt;/li&gt;
&lt;li&gt;相互已知的 Sentinel 只要使用命令连接来进行通信就足够了&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;下线检测&lt;/h3&gt;
&lt;h4&gt;主观下线&lt;/h4&gt;
&lt;p&gt;Sentinel 在默认情况下会以每秒一次的频率向所有与它创建了命令连接的实例（包括主从服务器、其他 Sentinel）发送 PING 命令，通过实例返回的 PING 命令回复来判断实例是否在线&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有效回复：实例返回 +PONG、-LOADING、-MASTERDOWN 三种回复的其中一种&lt;/li&gt;
&lt;li&gt;无效回复：实例返回除上述三种以外的任何数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sentinel 配置文件中 down-after-milliseconds 选项指定了判断实例进入主观下线所需的时长，如果主服务器在该时间内一直向 Sentinel 返回无效回复，Sentinel 就会在该服务器对应实例结构的 flags 属性打开 SRI_S_DOWN 标识，表示该主服务器进入主观下线状态&lt;/p&gt;
&lt;p&gt;配置的 down-after-milliseconds 值不仅适用于主服务器，还会被用于当前 Sentinel 判断主服务器属下的所有从服务器，以及所有同样监视这个主服务器的其他 Sentinel 的主观下线状态&lt;/p&gt;
&lt;p&gt;注意：对于监视同一个主服务器的多个 Sentinel 来说，设置的 down-after-milliseconds 选项的值可能不同，所以当一个 Sentinel 将主服务器判断为主观下线时，其他 Sentinel 可能仍然会认为主服务器处于在线状态&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;客观下线&lt;/h4&gt;
&lt;p&gt;当 Sentinel 将一个主服务器判断为主观下线之后，会向同样监视这一主服务器的其他 Sentinel 进行询问&lt;/p&gt;
&lt;p&gt;Sentinel 使用命令询问其他 Sentinel 是否同意主服务器已下线：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SENTINEL is-master-down-by-addr &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; &amp;lt;current_epoch&amp;gt; &amp;lt;runid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;ip：被 Sentinel 判断为主观下线的主服务器的 IP 地址&lt;/li&gt;
&lt;li&gt;port：被 Sentinel 判断为主观下线的主服务器的端口号&lt;/li&gt;
&lt;li&gt;current_epoch：Sentinel 当前的配置纪元，用于选举领头 Sentinel&lt;/li&gt;
&lt;li&gt;runid：取值为 * 符号代表命令仅仅用于检测主服务器的客观下线状态；取值为 Sentinel 的运行 ID 则用于选举领头 Sentinel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目标 Sentinel 接收到源 Sentinel 的命令时，会根据参数的 lP 和端口号，检查主服务器是否已下线，然后返回一条包含三个参数的 Multi Bulk 回复：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;down_state：返回目标 Sentinel 对服务器的检查结果，1 代表主服务器已下线，0 代表未下线&lt;/li&gt;
&lt;li&gt;leader_runid：取值为 * 符号代表命令仅用于检测服务器的下线状态；而局部领头 Sentinel 的运行 ID 则用于选举领头 Sentinel&lt;/li&gt;
&lt;li&gt;leader_epoch：目标 Sentinel 的局部领头 Sentinel 的配置纪元&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源 Sentinel 将统计其他 Sentinel 同意主服务器已下线的数量，当这一数量达到配置指定的判断客观下线所需的数量（quorum）时，Sentinel 会将主服务器对应实例结构 flags 属性的 SRI_O_DOWN 标识打开，代表客观下线，并对主服务器执行故障转移操作&lt;/p&gt;
&lt;p&gt;注意：&lt;strong&gt;不同 Sentinel 判断客观下线的条件可能不同&lt;/strong&gt;，因为载入的配置文件中的属性 quorum 可能不同&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;领头选举&lt;/h3&gt;
&lt;p&gt;主服务器被判断为客观下线时，&lt;strong&gt;监视该主服务器的各个 Sentinel 会进行协商&lt;/strong&gt;，选举出一个领头 Sentinel 对下线服务器执行故障转移&lt;/p&gt;
&lt;p&gt;Redis 选举领头 Sentinel 的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有在线的 Sentinel 都有被选为领头 Sentinel 的资格&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个发现主服务器进入客观下线的 Sentinel 都会要求其他 Sentinel 将自己设置为局部领头 Sentinel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一个配置纪元里，所有 Sentinel 都只有一次将某个 Sentinel 设置为局部领头 Sentinel 的机会，并且局部领头一旦设置，在这个配置纪元里就不能再更改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sentinel 设置局部领头 Sentinel 的规则是先到先得，最先向目标 Sentinel 发送设置要求的源 Sentinel 将成为目标 Sentinel 的局部领头 Sentinel，之后接收到的所有设置要求都会被目标 Sentinel 拒绝&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;领头 Sentinel 的产生&lt;strong&gt;需要半数以上 Sentinel 的支持&lt;/strong&gt;，并且每个 Sentinel 只有一票，所以一个配置纪元只会出现一个领头 Sentinel，比如 10 个 Sentinel 的系统中，至少需要 &lt;code&gt;10/2 + 1 = 6&lt;/code&gt; 票&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选举过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 Sentinel 向目标 Sentinel 发送 &lt;code&gt;SENTINEL is-master-down-by-addr&lt;/code&gt; 命令，命令中的 runid 参数不是＊符号而是源 Sentinel 的运行 ID，表示源 Sentinel 要求目标 Sentinel 将自己设置为它的局部领头 Sentinel&lt;/li&gt;
&lt;li&gt;目标 Sentinel 接受命令处理完成后，将返回一条命令回复，回复中的 leader_runid 和 leader_epoch 参数分别记录了目标 Sentinel 的局部领头 Sentinel 的运行 ID 和配置纪元&lt;/li&gt;
&lt;li&gt;源 Sentinel 接收目标 Sentinel 命令回复之后，会判断 leader_epoch 是否和自己的相同，相同就继续判断 leader_runid 是否和自己的运行 ID 一致，成立表示目标 Sentinel 将源 Sentinel 设置成了局部领头 Sentinel，即获得一票&lt;/li&gt;
&lt;li&gt;如果某个 Sentinel 被半数以上的 Sentinel 设置成了局部领头 Sentinel，那么这个 Sentinel 成为领头 Sentinel&lt;/li&gt;
&lt;li&gt;如果在给定时限内，没有一个 Sentinel 被选举为领头 Sentinel，那么各个 Sentinel 将在一段时间后&lt;strong&gt;再次选举&lt;/strong&gt;，直到选出领头&lt;/li&gt;
&lt;li&gt;每次进行领头 Sentinel 选举之后，不论选举是否成功，所有 Sentinel 的配置纪元（configuration epoch）都要自增一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sentinel 集群至少 3 个节点的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 Sentinel 集群只有 2 个 Sentinel 节点，则领头选举需要 &lt;code&gt;2/2 + 1 = 2&lt;/code&gt; 票，如果一个节点挂了，那就永远选不出领头&lt;/li&gt;
&lt;li&gt;Sentinel 集群允许 1 个 Sentinel 节点故障则需要 3 个节点的集群，允许 2 个节点故障则需要 5 个节点集群&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如何获取哨兵节点的半数数量&lt;/strong&gt;？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客观下线是通过配置文件获取的数量，达到  quorum 就客观下线&lt;/li&gt;
&lt;li&gt;哨兵数量是通过主节点是实例结构中，保存着监视该主节点的所有哨兵信息，从而获取得到&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;故障转移&lt;/h3&gt;
&lt;h4&gt;执行流程&lt;/h4&gt;
&lt;p&gt;领头 Sentinel 将对已下线的主服务器执行故障转移操作，该操作包含以下三个步骤&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从下线主服务器属下的所有从服务器里面，挑选出一个从服务器，执行 &lt;code&gt;SLAVEOF no one&lt;/code&gt;，将从服务器升级为主服务器&lt;/p&gt;
&lt;p&gt;在发送 SLAVEOF no one 命令后，领头 Sentinel 会以&lt;strong&gt;每秒一次的频率&lt;/strong&gt;（一般是 10s/次）向被升级的从服务器发送 INFO 命令，观察命令回复中的角色信息，当被升级服务器的 role 从 slave 变为 master 时，说明从服务器已经顺利升级为主服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将已下线的主服务器的所有从服务器改为复制新的主服务器，通过向从服务器发送 SLAVEOF 命令实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将已经下线的主服务器设置为新的主服务器的从服务器，设置是保存在服务器对应的实例结构中，当旧的主服务器重新上线时，Sentinel 就会向它发送 SLAVEOF 命令，成为新的主服务器的从服务器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：sever1 是主，sever2、sever3、sever4 是从服务器，sever1 故障后选中 sever2 升级&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%93%A8%E5%85%B5%E6%89%A7%E8%A1%8C%E6%95%85%E9%9A%9C%E8%BD%AC%E7%A7%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;选择算法&lt;/h4&gt;
&lt;p&gt;领头 Sentinel 会将已下线主服务器的所有从服务器保存到一个列表里，然后按照以下规则对列表进行过滤，最后挑选出一个&lt;strong&gt;状态良好、数据完整&lt;/strong&gt;的从服务器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;删除列表中所有处于下线或者断线状态的从服务器，保证列表中的从服务器都是正常在线的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除列表中所有最近五秒内没有回复过领头 Sentinel 的 INFO 命令的从服务器，保证列表中的从服务器最近成功进行过通信&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除所有与已下线主服务器连接断开超过 &lt;code&gt;down-after-milliseconds * 10&lt;/code&gt; 毫秒的从服务器，保证列表中剩余的从服务器都没有过早地与主服务器断开连接，保存的数据都是比较新的&lt;/p&gt;
&lt;p&gt;down-after-milliseconds 时间用来判断是否主观下线，其余的时间完全可以完成客观下线和领头选举&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据从服务器的优先级，对列表中剩余的从服务器进行排序，并选出其中&lt;strong&gt;优先级最高&lt;/strong&gt;的从服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果有多个具有相同最高优先级的从服务器，领头 Sentinel 将对这些相同优先级的服务器按照复制偏移量进行排序，选出其中偏移量最大的从服务器，也就是保存着最新数据的从服务器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果还没选出来，就按照运行 ID 对这些从服务器进行排序，并选出其中运行 ID 最小的从服务器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;集群模式&lt;/h2&gt;
&lt;h3&gt;集群节点&lt;/h3&gt;
&lt;h4&gt;节点概述&lt;/h4&gt;
&lt;p&gt;Redis 集群是 Redis 提供的分布式数据库方案，集群通过分片（sharding）来进行数据共享， 并提供复制和故障转移功能，一个 Redis 集群通常由多个节点（node）组成，将各个独立的节点连接起来，构成一个包含多节点的集群&lt;/p&gt;
&lt;p&gt;一个节点就是一个&lt;strong&gt;运行在集群模式下的 Redis 服务器&lt;/strong&gt;，Redis 在启动时会根据配置文件中的 &lt;code&gt;cluster-enabled&lt;/code&gt; 配置选项是否为 yes 来决定是否开启服务器的集群模式&lt;/p&gt;
&lt;p&gt;节点会继续使用所有在单机模式中使用的服务器组件，使用 redisServer 结构来保存服务器的状态，使用 redisClient 结构来保存客户端的状态，也有集群特有的数据结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%9B%86%E7%BE%A4%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;数据结构&lt;/h4&gt;
&lt;p&gt;每个节点都保存着一个集群状态 clusterState 结构，这个结构记录了在当前节点的视角下，集群目前所处的状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterState {
    // 指向当前节点的指针
	clusterNode *myself;
    
	// 集群当前的配置纪元，用于实现故障转移
	uint64_t currentEpoch;
    
	// 集群当前的状态，是在线还是下线
	int state;
    
	// 集群中至少处理着一个槽的（主）节点的数量，为0表示集群目前没有任何节点在处理槽
    // 【选举时投票数量超过半数，从这里获取的】
	int size;

    // 集群节点名单（包括 myself 节点），字典的键为节点的名字，字典的值为节点对应的clusterNode结构 
    dict *nodes;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个节点都会使用 clusterNode 结构记录当前状态，并为集群中的所有其他节点（包括主节点和从节点）都创建一个相应的 clusterNode 结构，以此来记录其他节点的状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct clusterNode {
    // 创建节点的时间
    mstime_t ctime;
    
    // 节点的名字，由 40 个十六进制字符组成
    char name[REDIS_CLUSTER_NAMELEN];
    
    // 节点标识，使用各种不同的标识值记录节点的角色（比如主节点或者从节点）以及节点目前所处的状态（比如在线或者下线）
    int flags;
    
    // 节点当前的配置纪元，用于实现故障转移
    uint64_t configEpoch;
    
    // 节点的IP地址
    char ip[REDIS_IP_STR_LEN];
    
    // 节点的端口号
    int port;
    
    // 保存连接节点所需的有关信息
    clusterLink *link;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;clusterNode 结构的 link 属性是一个 clusterLink 结构，该结构保存了连接节点所需的有关信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterLink {
    // 连接的创建时间 
    mstime_t ctime;
    
	// TCP套接字描述符
	int fd;
    
	// 输出缓冲区，保存着等待发送给其他节点的消息(message)。 
    sds sndbuf;
    
	// 输入缓冲区，保存着从其他节点接收到的消息。
	sds rcvbuf;
    
	// 与这个连接相关联的节点，如果没有的话就为NULL
	struct clusterNode *node; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;redisClient 结构中的套接宇和缓冲区是用于连接客户端的&lt;/li&gt;
&lt;li&gt;clusterLink 结构中的套接宇和缓冲区则是用于连接节点的&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;MEET&lt;/h4&gt;
&lt;p&gt;CLUSTER MEET 命令用来将 ip 和 port 所指定的节点添加到接受命令的节点所在的集群中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLUSTER MEET &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设向节点 A 发送 CLUSTER MEET 命令，让节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里，收到命令的节点 A 将与根据 ip 和 port 向节点 B 进行握手（handshake）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点 A 会为节点 B 创建一个 clusterNode 结构，并将该结构添加到自己的 clusterState.nodes 字典里，然后节点 A 向节点 B &lt;strong&gt;发送 MEET 消息&lt;/strong&gt;（message）&lt;/li&gt;
&lt;li&gt;节点 B 收到 MEET 消息后，节点 B 会为节点 A 创建一个 clusterNode 结构，并将该结构添加到自己的 clusterState.nodes 字典里，之后节点 B 将向节点 A &lt;strong&gt;返回一条 PONG 消息&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;节点 A 收到 PONG 消息后，代表节点 A 可以知道节点 B 已经成功地接收到了自已发送的 MEET 消息，此时节点 A 将向节点 B &lt;strong&gt;返回一条 PING 消息&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;节点 B 收到 PING 消息后， 代表节点 B 可以知道节点 A 已经成功地接收到了自己返回的 PONG 消息，握手完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%9B%86%E7%BE%A4%E8%8A%82%E7%82%B9%E6%8F%A1%E6%89%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点，让其他节点也与节点 B 进行握手，最终经过一段时间之后，节点 B 会被集群中的所有节点认识&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;槽指派&lt;/h3&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;p&gt;Redis 集群通过分片的方式来保存数据库中的键值对，集群的整个数据库被分为 16384 个槽（slot），数据库中的每个键都属于 16384 个槽中的一个，集群中的每个节点可以处理 0 个或最多 16384 个槽（&lt;strong&gt;每个主节点存储的数据并不一样&lt;/strong&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当数据库中的 16384 个槽都有节点在处理时，集群处于上线状态（ok）&lt;/li&gt;
&lt;li&gt;如果数据库中有任何一个槽得到处理，那么集群处于下线状态（fail）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过向节点发送 CLUSTER ADDSLOTS 命令，可以将一个或多个槽指派（assign）给节点负责&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLUSTER ADDSLOTS &amp;lt;slot&amp;gt; [slot ... ] 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:7000&amp;gt; CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 # 将槽0至槽5000指派给节点7000负责
OK 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令执行细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果命令参数中有一个槽已经被指派给了某个节点，那么会向客户端返回错误，并终止命令执行&lt;/li&gt;
&lt;li&gt;将 slots 数组中的索引 i 上的二进制位设置为 1，就代表指派成功&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;节点指派&lt;/h4&gt;
&lt;p&gt;clusterNode 结构的 slots 属性和 numslot 属性记录了节点负责处理哪些槽：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct clusterNode {
    // 处理信息，一字节等于 8 位
    unsigned char slots[l6384/8];
    // 记录节点负责处理的槽的数量，就是 slots 数组中值为 1 的二进制位数量
    int numslots;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slots 是一个二进制位数组（bit array），长度为 &lt;code&gt;16384/8 = 2048&lt;/code&gt; 个字节，包含 16384 个二进制位，Redis 以 0 为起始索引，16383 为终止索引，对 slots 数组的 16384 个二进制位进行编号，并根据索引 i 上的二进制位的值来判断节点是否负责处理槽 i：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在索引 i 上的二进制位的值为 1，那么表示节点负责处理槽 i&lt;/li&gt;
&lt;li&gt;在索引 i 上的二进制位的值为 0，那么表示节点不负责处理槽 i&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%9B%86%E7%BE%A4%E6%A7%BD%E6%8C%87%E6%B4%BE%E4%BF%A1%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;取出和设置 slots 数组中的任意一个二进制位的值的&lt;strong&gt;复杂度仅为 O(1)&lt;/strong&gt;，所以对于一个给定节点的 slots 数组来说，检查节点是否负责处理某个槽或者将某个槽指派给节点负责，这两个动作的复杂度都是 O(1)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传播节点的槽指派信息&lt;/strong&gt;：一个节点除了会将自己负责处理的槽记录在 clusterNode 中，还会将自己的 slots 数组通过消息发送给集群中的其他节点，每个接收到 slots 数组的节点都会将数组保存到相应节点的 clusterNode 结构里面，因此集群中的&lt;strong&gt;每个节点&lt;/strong&gt;都会知道数据库中的 16384 个槽分别被指派给了集群中的哪些节点&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;集群指派&lt;/h4&gt;
&lt;p&gt;集群状态 clusterState 结构中的 slots 数组记录了集群中所有 16384 个槽的指派信息，数组每一项都是一个指向 clusterNode 的指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterState {
    // ...
    clusterNode *slots[16384];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果 slots[i] 指针指向 NULL，那么表示槽 i 尚未指派给任何节点&lt;/li&gt;
&lt;li&gt;如果 slots[i] 指针指向一个 clusterNode 结构，那么表示槽 i 已经指派给该节点所代表的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过该节点，程序检查槽 i 是否已经被指派或者取得负责处理槽 i 的节点，只需要访问 clusterState. slots[i] 即可，时间复杂度仅为 O(1)&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;集群数据&lt;/h4&gt;
&lt;p&gt;集群节点保存键值对以及键值对过期时间的方式，与单机 Redis 服务器保存键值对以及键值对过期时间的方式完全相同，但是&lt;strong&gt;集群节点只能使用 0 号数据库&lt;/strong&gt;，单机服务器可以任意使用&lt;/p&gt;
&lt;p&gt;除了将键值对保存在数据库里面之外，节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来&lt;strong&gt;保存槽和键之间的关系&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterState {
    // ...
    zskiplist *slots_to_keys;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slots_to_keys 跳跃表每个节点的分值（score）都是一个槽号，而每个节点的成员（member）都是一个数据库键（按槽号升序）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当节点往数据库中添加一个新的键值对时，节点就会将这个键以及键的槽号关联到 slots_to_keys 跳跃表&lt;/li&gt;
&lt;li&gt;当节点删除数据库中的某个键值对时，节点就会在 slots_to_keys 跳跃表解除被删除键与槽号的关联&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E6%A7%BD%E5%92%8C%E9%94%AE%E8%B7%B3%E8%B7%83%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过在 slots_to_keys 跳跃表中记录各个数据库键所属的槽，可以很方便地对属于某个或某些槽的所有数据库键进行批量操作，比如 &lt;code&gt;CLUSTER GETKEYSINSLOT &amp;lt;slot&amp;gt; &amp;lt;count&amp;gt;&lt;/code&gt; 命令返回最多 count 个属于槽 slot 的数据库键，就是通过该跳表实现&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;集群命令&lt;/h3&gt;
&lt;h4&gt;执行命令&lt;/h4&gt;
&lt;p&gt;集群处于上线状态，客户端就可以向集群中的节点发送命令（16384 个槽全部指派就进入上线状态）&lt;/p&gt;
&lt;p&gt;当客户端向节点发送与数据库键有关的命令时，接收命令的节点会计算出命令该键属于哪个槽，并检查这个槽是否指派给了自己&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果键所在的槽正好就指派给了当前节点，那么节点直接执行这个命令&lt;/li&gt;
&lt;li&gt;反之，节点会向客户端返回一个 MOVED 错误，指引客户端转向（redirect）至正确的节点，再次发送该命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计算键归属哪个槽的&lt;strong&gt;寻址算法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def slot_number(key): 			// CRC16(key) 语句计算键 key 的 CRC-16 校验和
	return CRC16(key) &amp;amp; 16383;	// 取模，十进制对16384的取余
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;CLUSTER KEYSLOT &amp;lt;key&amp;gt;&lt;/code&gt; 命令可以查看一个给定键属于哪个槽，底层实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def CLUSTER_KEYSLOT(key):
	// 计算槽号
	slot = slot_number(key);
	// 将槽号返回给客户端
	reply_client(slot);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断槽是否由当前节点负责处理：如果 clusterState.slots[i] 不等于 clusterState.myself，那么说明槽 i 并非由当前节点负责，节点会根据 clusterState.slots[i] 指向的 clusterNode 结构所记录的节点 IP 和端口号，向客户端返回 MOVED 错误&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;MOVED&lt;/h4&gt;
&lt;p&gt;MOVED 错误的格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOVED &amp;lt;slot&amp;gt; &amp;lt;ip&amp;gt;:&amp;lt;port＞
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数 slot 为键所在的槽，ip 和 port 是负责处理槽 slot 的节点的 ip 地址和端口号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOVED 12345 127.0.0.1:6380 # 表示槽 12345 正由 IP地址为 127.0.0.1, 端口号为 6380 的节点负责
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当客户端接收到节点返回的 MOVED 错误时，客户端会根据 MOVED 错误中提供的 IP 地址和端口号，转至负责处理槽 slot 的节点重新发送执行的命令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个集群客户端通常会与集群中的多个节点创建套接字连接，节点转向实际上就是换一个套接字来发送命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果客户端尚未与转向的节点创建套接字连接，那么客户端会先根据 IP 地址和端口号来连接节点，然后再进行转向&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集群模式的 redis-cli 在接收到 MOVED 错误时，并不会打印出 MOVED 错误，而是根据错误&lt;strong&gt;自动进行节点转向&lt;/strong&gt;，并打印出转向信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ redis-cli -c -p 6379 	#集群模式
127.0.0.1:6379&amp;gt; SET msg &quot;happy&quot; 
-&amp;gt; Redirected to slot [6257] located at 127.0.0.1:6380
OK 

127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用单机（stand alone）模式的 redis-cli 会打印错误，因为单机模式客户端不清楚 MOVED 错误的作用，不会进行自动转向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ redis-cli -c -p 6379 	#集群模式
127.0.0.1:6379&amp;gt; SET msg &quot;happy&quot; 
(error) MOVED 6257 127.0.0.1:6380

127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;重新分片&lt;/h3&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;p&gt;Redis 集群的重新分片操作可以将任意数量已经指派给某个节点（源节点）的槽改为指派给另一个节点（目标节点），并且相关槽的键值对也会从源节点被移动到目标节点，该操作是可以在线（online）进行，在重新分片的过程中源节点和目标节点都可以处理命令请求&lt;/p&gt;
&lt;p&gt;Redis 的集群管理软件 redis-trib 负责执行重新分片操作，redis-trib 通过向源节点和目标节点发送命令来进行重新分片操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向目标节点发送 &lt;code&gt;CLUSTER SETSLOT &amp;lt;slot&amp;gt; IMPORTING &amp;lt;source_id&amp;gt;&lt;/code&gt; 命令，准备好从源节点导入属于槽 slot 的键值对&lt;/li&gt;
&lt;li&gt;向源节点发送 &lt;code&gt;CLUSTER SETSLOT &amp;lt;slot&amp;gt; MIGRATING &amp;lt;target_id&amp;gt;&lt;/code&gt; 命令，让源节点准备好将属于槽 slot 的键值对迁移&lt;/li&gt;
&lt;li&gt;redis-trib 向源节点发送 &lt;code&gt;CLUSTER GETKEYSINSLOT &amp;lt;slot&amp;gt; &amp;lt;count&amp;gt;&lt;/code&gt; 命令，获得最多 count 个属于槽 slot 的键值对的键名&lt;/li&gt;
&lt;li&gt;对于每个 key，redis-trib 都向源节点发送一个 &lt;code&gt;MIGRATE &amp;lt;target_ip&amp;gt; &amp;lt;target_port&amp;gt; &amp;lt;key_name&amp;gt; 0 &amp;lt;timeout＞&lt;/code&gt; 命令，将被选中的键&lt;strong&gt;原子地&lt;/strong&gt;从源节点迁移至目标节点&lt;/li&gt;
&lt;li&gt;重复上述步骤，直到源节点保存的所有槽 slot 的键值对都被迁移至目标节点为止&lt;/li&gt;
&lt;li&gt;redis-trib 向集群中的任意一个节点发送 &lt;code&gt;CLUSTER SETSLOT &amp;lt;slot&amp;gt; NODE &amp;lt;target _id&amp;gt;&lt;/code&gt; 命令，将槽 slot 指派给目标节点，这一指派信息会通过消息传播至整个集群，最终集群中的所有节点都直到槽 slot 已经指派给了目标节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E9%9B%86%E7%BE%A4%E9%87%8D%E6%96%B0%E5%88%86%E7%89%87.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果重新分片涉及多个槽，那么 redis-trib 将对每个给定的槽分别执行上面给出的步骤&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;命令原理&lt;/h4&gt;
&lt;p&gt;clusterState 结构的 importing_slots_from 数组记录了当前节点正在从其他节点导入的槽，migrating_slots_to 数组记录了当前节点正在迁移至其他节点的槽：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterState {
    // 如果 importing_slots_from[i] 的值不为 NULL，而是指向一个 clusterNode 结构，
    // 那么表示当前节点正在从 clusterNode 所代表的节点导入槽 i
    clusterNode *importing_slots_from[16384];
    
    // 表示当前节点正在将槽 i 迁移至 clusterNode 所代表的节点
    clusterNode *migrating_slots_to[16384];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CLUSTER SETSLOT &amp;lt;slot&amp;gt; IMPORTING &amp;lt;source_id&amp;gt;&lt;/code&gt; 命令：将目标节点 &lt;code&gt;clusterState.importing_slots_from[slot]&lt;/code&gt; 的值设置为  source_id 所代表节点的 clusterNode 结构&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CLUSTER SETSLOT &amp;lt;slot&amp;gt; MIGRATING &amp;lt;target_id&amp;gt;&lt;/code&gt; 命令：将源节点 &lt;code&gt;clusterState.migrating_slots_to[slot]&lt;/code&gt; 的值设置为target_id 所代表节点的 clusterNode 结构&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;ASK 错误&lt;/h4&gt;
&lt;p&gt;重新分片期间，源节点向目标节点迁移一个槽的过程中，可能出现被迁移槽的一部分键值对保存在源节点，另一部分保存在目标节点&lt;/p&gt;
&lt;p&gt;客户端向源节点发送命令请求，并且命令要处理的数据库键属于被迁移的槽：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;源节点会先在数据库里面查找指定键，如果找到的话，就直接执行客户端发送的命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未找到会检查 clusterState.migrating_slots_to[slot]，看键 key 所属的槽 slot 是否正在进行迁移&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;槽 slot 正在迁移则源节点将向客户端返回一个 ASK 错误，指引客户端转向正在导入槽的目标节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ASK &amp;lt;slot&amp;gt; &amp;lt;ip:port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接到 ASK 错误的客户端，会根据错误提供的 IP 地址和端口号转向目标节点，首先向目标节点发送一个 ASKING 命令，再重新发送原本想要执行的命令&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和 MOVED 错误情况类似，集群模式的 redis-cli 在接到 ASK 错误时不会打印错误进行自动转向；单机模式的 redis-cli 会打印错误&lt;/p&gt;
&lt;p&gt;对比 MOVED 错误：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MOVED 错误代表槽的负责权已经从一个节点转移到了另一个节点，转向是一种持久性的转向&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ASK 错误只是两个节点在迁移槽的过程中使用的一种临时措施，ASK 的转向不会对客户端今后发送关于槽 slot 的命令请求产生任何影响，客户端仍然会将槽 slot 的命令请求发送至目前负责处理槽 slot 的节点，除非 ASK 错误再次出现&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;ASKING&lt;/h4&gt;
&lt;p&gt;客户端不发送 ASKING 命令，而是直接发送执行的命令，那么客户端发送的命令将被节点拒绝执行，并返回 MOVED 错误&lt;/p&gt;
&lt;p&gt;ASKING 命令作用是打开发送该命令的客户端的 REDIS_ASKING 标识，该命令的伪代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def ASKING ():
    // 打开标识
    client.flags |= REDIS_ASKING 
    // 向客户端返回OK回复
    reply(&quot;OK&quot;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前节点正在导入槽 slot，并且发送命令的客户端带有 REDIS_ASKING 标识，那么节点将破例执行这个关于槽 slot 的命令一次&lt;/p&gt;
&lt;p&gt;客户端的 REDIS_ASKING 标识是一次性标识，当节点执行了一个带有 REDIS_ASKING 标识的客户端发送的命令之后，该客户端的 REDIS_ASKING 标识就会被移除&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;高可用&lt;/h3&gt;
&lt;h4&gt;节点复制&lt;/h4&gt;
&lt;p&gt;Redis 集群中的节点分为主节点（master）和从节点（slave），其中主节点用于处理槽，而从节点则用于复制主节点，并在被复制的主节点下线时，代替下线主节点继续处理命令请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLUSTER REPLICATE &amp;lt;node_id&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;向一个节点发送命令可以让接收命令的节点成为 node_id 所指定节点的从节点，并开始对主节点进行复制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;接受命令的节点首先会在的 clusterState.nodes 字典中找到 node_id 所对应节点的 clusterNode 结构，并将自己的节点中的 clusterState.myself.slaveof 指针指向这个结构，记录这个节点正在复制的主节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点会修改 clusterState.myself.flags 中的属性，关闭 REDIS_NODE_MASTER 标识，打开 REDIS_NODE_SLAVE 标识&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点会调用复制代码，对主节点进行复制（节点的复制功能和单机 Redis 服务器的使用了相同的代码）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个节点成为从节点，并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点，最终集群中的所有节点都会知道某个从节点正在复制某个主节点&lt;/p&gt;
&lt;p&gt;主节点的 clusterNode 结构的 slaves 属性和 numslaves 属性中记录正在复制这个主节点的从节点名单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct clusterNode {
    // 正在复制这个主节点的从节点数量
    int numslaves;
    
    // 数组项指向一个正在复制这个主节点的从节点的clusterNode结构
    struct clusterNode **slaves; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;故障检测&lt;/h4&gt;
&lt;p&gt;集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息，来检测对方是否在线，如果接收 PING 的节点没有在规定的时间内返回 PONG 消息，那么发送消息节点就会将接收节点标记为&lt;strong&gt;疑似下线&lt;/strong&gt;（probable fail）&lt;/p&gt;
&lt;p&gt;集群中的节点会互相发送消息，来&lt;strong&gt;交换集群中各个节点的状态信息&lt;/strong&gt;，当一个主节点 A 通过消息得知主节点 B 认为主节点 C 进入了疑似下线状态时，主节点 A 会在 clusterState.nodes 字典中找到主节点 C 所对应的节点，并将主节点 B 的下线报告（failure report）添加到 clusterNode.fail_reports 链表里面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct clusterNode {
    // 一个链表，记录了所有其他节点对该节点的下线报告 
    list *fail_reports;
}
// 每个下线报告由一个 clusterNodeFailReport 结构表示
struct clusterNodeFailReport {
    // 报告目标节点巳经下线的节点 
    struct clusterNode *node;
    
    // 最后一次从node节点收到下线报告的时间
    // 程序使用这个时间戳来检查下线报告是否过期，与当前时间相差太久的下线报告会被删除 
    mstime_t time; 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群里&lt;strong&gt;半数以上&lt;/strong&gt;负责处理槽的主节点都将某个主节点 X 报告为疑似下线，那么 X 将被标记为&lt;strong&gt;已下线&lt;/strong&gt;（FAIL），将 X 标记为已下线的节点会向集群广播一条关于主节点 X 的 FAIL 消息，所有收到消息的节点都会将 X 标记为已下线&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;故障转移&lt;/h4&gt;
&lt;p&gt;当一个从节点发现所属的主节点进入了已下线状态，从节点将开始对下线主节点进行故障转移，执行步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下属的从节点通过选举产生一个节点&lt;/li&gt;
&lt;li&gt;被选中的从节点会执行 &lt;code&gt;SLAVEOF no one&lt;/code&gt; 命令，成为新的主节点&lt;/li&gt;
&lt;li&gt;新的主节点会&lt;strong&gt;撤销所有对已下线主节点的槽指派&lt;/strong&gt;，并将这些槽全部指派给自己&lt;/li&gt;
&lt;li&gt;新的主节点向集群广播一条 PONG 消息，让集群中的其他节点知道当前节点变成了主节点，并且接管了下线节点负责处理的槽&lt;/li&gt;
&lt;li&gt;新的主节点开始接收有关的命令请求，故障转移完成&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;选举算法&lt;/h4&gt;
&lt;p&gt;集群选举新的主节点的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集群的配置纪元是一个自增的计数器，初始值为 0&lt;/li&gt;
&lt;li&gt;当集群里某个节点开始一次故障转移，集群的配置纪元就是增加一&lt;/li&gt;
&lt;li&gt;每个配置纪元里，集群中每个主节点都有一次投票的机会，而第一个向主节点要求投票的从节点将获得该主节点的投票&lt;/li&gt;
&lt;li&gt;具有投票权的主节点是必须具有正在处理的槽&lt;/li&gt;
&lt;li&gt;集群里有 N 个具有投票权的主节点，那么当一个从节点收集到大于等于 &lt;code&gt;N/2+1&lt;/code&gt; 张支持票时，从节点就会当选&lt;/li&gt;
&lt;li&gt;每个配置纪元里，具有投票权的主节点只能投一次票，所以获得一半以上票的节点只会有一个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选举流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当某个从节点发现正在复制的主节点进入已下线状态时，会向集群广播一条 &lt;code&gt;CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST&lt;/code&gt; 消息，要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票&lt;/li&gt;
&lt;li&gt;如果主节点尚未投票给其他从节点，将向要求投票的从节点返回一条 &lt;code&gt;CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK&lt;/code&gt; 消息，表示这个主节点支持从节点成为新的主节点&lt;/li&gt;
&lt;li&gt;如果从节点获取到了半数以上的选票，则会当选新的主节点&lt;/li&gt;
&lt;li&gt;如果一个配置纪元里没有从节点能收集到足够多的支待票，那么集群进入一个新的配置纪元，并再次进行选举，直到选出新的主节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选举新主节点的方法和选举领头 Sentinel 的方法非常相似，两者都是基于 Raft 算法的领头选举（leader election）方法实现的&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;消息机制&lt;/h3&gt;
&lt;h4&gt;消息结构&lt;/h4&gt;
&lt;p&gt;集群中的各个节点通过发送和接收消息（message）来进行通信，将发送消息的节点称为发送者（sender），接收消息的节点称为接收者（receiver）&lt;/p&gt;
&lt;p&gt;节点发送的消息主要有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MEET 消息：当发送者接到客户端发送的 CLUSTER MEET 命令时，会向接收者发送 MEET 消息，请求接收者加入到发送者当前所处的集群里&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PING 消息：集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个，然后对这五个节点中最长时间没有发送过 PING 消息的节点发送 PING，以此来&lt;strong&gt;随机检测&lt;/strong&gt;被选中的节点是否在线&lt;/p&gt;
&lt;p&gt;如果节点 A 最后一次收到节点 B 发送的 PONG 消息的时间，距离当前已经超过了节点 A 的 cluster-node­-timeout 设置时长的一半，那么 A 也会向 B 发送 PING 消息，防止 A 因为长时间没有随机选中 B 发送 PING，而导致对节点 B 的信息更新滞后&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PONG 消息：当接收者收到 MEET 消息或者 PING 消息时，为了让发送者确认已经成功接收消息，会向发送者返回一条 PONG；节点也可以通过向集群广播 PONG 消息来让集群中的其他节点立即刷新关于这个节点的认识（从升级为主）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FAIL 消息：当一个主节点 A 判断另一个主节点 B 已经进入 FAIL 状态时，节点 A 会向集群广播一条 B 节点的 FAIL 信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PUBLISH 消息：当节点接收到一个 PUBLISH 命令时，节点会执行这个命令并向集群广播一条 PUBLISH 消息，接收到 PUBLISH 消息的节点都会执行相同的 PUBLISH 命令&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;消息头&lt;/h4&gt;
&lt;p&gt;节点发送的所有消息都由一个消息头包裹，消息头除了包含消息正文之外，还记录了消息发送者自身的一些信息&lt;/p&gt;
&lt;p&gt;消息头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterMsg {
    // 消息的长度（包括这个消息头的长度和消息正文的长度）
	uint32_t totlen;
	// 消息的类型
	uint16_t type;
    // 消息正文包含的节点信息数量，只在发送MEET、PING、PONG这三种Gossip协议消息时使用 
    uint16_t count;
    
    // 发送者所处的配置纪元
    uint64_t currentEpoch;
    // 如果发送者是一个主节点，那么这里记录的是发送者的配置纪元
    // 如果发送者是一个从节点，那么这里记录的是发送者正在复制的主节点的配置纪元
    uint64_t configEpoch;
    
    // 发送者的名字(ID)
	char sender[REDIS CLUSTER NAMELEN];
	// 发送者目前的槽指派信息
	unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
    
    // 如果发送者是一个从节点，那么这里记录的是发送者正在复制的主节点的名字
    // 如果发送者是一个主节点，那么这里记录的是 REDIS_NODE_NULL_NAME，一个 40 宇节长值全为 0 的字节数组
    char slaveof[REDIS_CLUSTER_NAMELEN];
    
	// 发送者的端口号
	uint16_t port;
	// 发送者的标识值
    uint16_t flags; 
	//发送者所处集群的状态
    unsigned char state;
	// 消息的正文（或者说， 内容） 
    union clusterMsgData data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;clusterMsg 结构的 currentEpoch、sender、myslots 等属性记录了发送者的节点信息，接收者会根据这些信息在 clusterState.nodes 字典里找到发送者对应的 clusterNode 结构，并对结构进行更新，比如&lt;strong&gt;传播节点的槽指派信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消息正文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;union clusterMsgData {
    // MEET、PING、PONG 消息的正文
    struct {
        // 每条 MEET、PING、PONG 消息都包含两个 clusterMsgDataGossip 结构
        clusterMsgDataGossip gossip[1];
    } ping;
    
    // FAIL 消息的正文
    struct { 
		clusterMsgDataFail about;
    } fail;
    
    // PUBLISH 消息的正文
    struct {
    	clusterMsgDataPublish msg;
    } publish;
    
    // 其他消息正文...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Gossip&lt;/h4&gt;
&lt;p&gt;Redis 集群中的各个节点通过 Gossip 协议来交换各自关于不同节点的状态信息，其中 Gossip 协议由 MEET、PING、PONG 消息实现，三种消息使用相同的消息正文，所以节点通过消息头的 type 属性来判断消息的具体类型&lt;/p&gt;
&lt;p&gt;发送者发送这三种消息时，会从已知节点列表中&lt;strong&gt;随机选出两个节点&lt;/strong&gt;（主从都可以），将两个被选中节点信息保存到两个 Gossip 结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterMsgDataGossip {
    // 节点的名字
	char nodename[REDIS CLUSTER NAMELEN];
    
	// 最后一次向该节点发送PING消息的时间戳
    uint32_t ping_sent;
	// 最后一次从该节点接收到PONG消息的时间戳
    uint32_t pong_received;
    
	// 节点的IP地址
	char ip[16];
    // 节点的端口号
    uint16_t port;
	// 节点的标识值
    uint16_t flags;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当接收者收到消息时，会访问消息正文中的两个数据结构，来进行相关操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果被选中节点不存在于接收者的已知节点列表，接收者将根据结构中记录的 IP 地址和端口号，与节点进行握手&lt;/li&gt;
&lt;li&gt;如果存在，根据 Gossip 结构记录的信息对节点所对应的 clusterNode 结构进行更新&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;FAIL&lt;/h4&gt;
&lt;p&gt;在集群的节点数量比较大的情况下，使用 Gossip 协议来传播节点的已下线信息会带来一定延迟，因为 Gossip 协议消息通常需要一段时间才能传播至整个集群，所以通过发送 FAIL消息可以让集群里的所有节点立即知道某个主节点已下线，从而尽快进行其他操作&lt;/p&gt;
&lt;p&gt;FAIL 消息的正文由 clusterMsgDataFail 结构表示，该结构只有一个属性，记录了已下线节点的名字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterMsgDataFail {
	char nodename[REDIS_CLUSTER_NAMELEN)];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为传播下线信息不需要其他属性，所以节省了传播的资源&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;PUBLISH&lt;/h4&gt;
&lt;p&gt;当客户端向集群中的某个节点发送命令，接收到 PUBLISH 命令的节点不仅会向 channel 频道发送消息 message，还会向集群广播一条 PUBLISH 消息，所有接收到这条 PUBLISH 消息的节点都会向 channel 频道发送 message 消息，最终集群中所有节点都发了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUBLISH &amp;lt;channel&amp;gt; &amp;lt;message&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PUBLISH 消息的正文由 clusterMsgDataPublish 结构表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct clusterMsgDataPublish {
    // channel参数的长度
    uint32_t channel_len;
    // message参数的长度
    uint32_t message_len;
    
    // 定义为8字节只是为了对齐其他消息结构，实际的长度由保存的内容决定
    // bulk_data 的 0 至 channel_len-1 字节保存的是channel参数
    // bulk_data的 channel_len 字节至 channel_len + message_len-1 字节保存的则是message参数
    unsigned char bulk_data[8];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让集群的所有节点执行相同的 PUBLISH 命令，最简单的方法就是向所有节点广播相同的 PUBLISH 命令，这也是 Redis 复制 PUBLISH 命令时所使用的，但是这种做法并不符合 Redis 集群的各&lt;strong&gt;个节点通过发送和接收消息来进行通信&lt;/strong&gt;的规则&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;脑裂问题&lt;/h3&gt;
&lt;p&gt;脑裂指在主从集群中，同时有两个相同的主节点能接收写请求，导致客户端不知道应该往哪个主节点写入数据，最后 不同客户端往不同的主节点上写入数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原主节点并没有真的发生故障，由于某些原因无法处理请求（CPU 利用率很高、自身阻塞），无法按时响应心跳请求，被哨兵/集群主节点错误的判断为下线&lt;/li&gt;
&lt;li&gt;在被判断下线之后，原主库又重新开始处理请求了，哨兵/集群主节点还没有完成主从切换，客户端仍然可以和原主库通信，客户端发送的写操作就会在原主库上写入数据，造成脑裂问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据丢失问题：从库一旦升级为新主库，哨兵就会让原主库执行 slave of 命令，和新主库重新进行全量同步，原主库需要清空本地的数据，加载新主库发送的 RDB 文件，所以原主库在主从切换期间保存的新写数据就丢失了&lt;/p&gt;
&lt;p&gt;预防脑裂：在主从集群部署时，合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设从库有 K 个，可以将 min-slaves-to-write 设置为 K/2+1（如果 K 等于 1，就设为 1）&lt;/li&gt;
&lt;li&gt;将 min-slaves-max-lag 设置为十几秒（例如 10～20s）&lt;/li&gt;
&lt;li&gt;在假故障期间无法响应哨兵发出的心跳测试，无法和从库进行 ACK 确认，并且没有足够的从库，&lt;strong&gt;拒绝客户端的写入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;结构搭建&lt;/h3&gt;
&lt;p&gt;整体框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置服务器（3 主 3 从）&lt;/li&gt;
&lt;li&gt;建立通信（Meet）&lt;/li&gt;
&lt;li&gt;分槽（Slot）&lt;/li&gt;
&lt;li&gt;搭建主从（master-slave）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建集群 conf 配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;redis-6501.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;port 6501
dir &quot;/redis/data&quot;
dbfilename &quot;dump-6501.rdb&quot;
cluster-enabled yes
cluster-config-file &quot;cluster-6501.conf&quot;
cluster-node-timeout 5000

#其他配置文件参照上面的修改端口即可，内容完全一样
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server config_file_name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -p 6504 -c
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;cluster 配置：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否启用 cluster，加入 cluster 节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster-enabled yes|no
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cluster 配置文件名，该文件属于自动生成，仅用于快速查找文件并查询文件内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster-config-file filename
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点服务响应超时时间，用于判定该节点是否下线或切换为从节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster-node-timeout milliseconds
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;master 连接的 slave 最小数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster-migration-barrier min_slave_number
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客户端启动命令：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cluster 节点操作命令（客户端命令）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看集群节点信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更改 slave 指向新的 master&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster replicate master-id
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发现一个新节点，新增 master&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster meet ip:port
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忽略一个没有 solt 的节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster forget server_id
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;手动故障转移&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster failover
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;集群操作命令（Linux）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建集群&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli –-cluster create masterhost1:masterport1 masterhost2:masterport2  masterhost3:masterport3 [masterhostn:masterportn …] slavehost1:slaveport1  slavehost2:slaveport2 slavehost3:slaveport3 -–cluster-replicas n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：master 与 slave 的数量要匹配，一个 master 对应 n 个 slave，由最后的参数 n 决定。master 与 slave 的匹配顺序为第一个 master 与前 n 个 slave 分为一组，形成主从结构&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 master 到当前集群中，连接时可以指定任意现有节点地址与端口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --cluster add-node new-master-host:new-master-port now-host:now-port
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 slave&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --cluster add-node new-slave-host:new-slave-port master-host:master-port --cluster-slave --cluster-master-id masterid
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除节点，如果删除的节点是 master，必须保障其中没有槽 slot&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --cluster del-node del-slave-host:del-slave-port del-slave-id
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新分槽，分槽是从具有槽的 master 中划分一部分给其他 master，过程中不创建新的槽&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --cluster reshard new-master-host:new-master:port --cluster-from src-  master-id1, src-master-id2, src-master-idn --cluster-to target-master-id --  cluster-slots slots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：将需要参与分槽的所有 masterid 不分先后顺序添加到参数中，使用 &lt;code&gt;,&lt;/code&gt; 分隔，指定目标得到的槽的数量，所有的槽将平均从每个来源的 master 处获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新分配槽，从具有槽的 master 中分配指定数量的槽到另一个 master 中，常用于清空指定 master 中的槽&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli --cluster reshard src-master-host:src-master-port --cluster-from src-  master-id --cluster-to target-master-id --cluster-slots slots --cluster-yes
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;其他操作&lt;/h2&gt;
&lt;h3&gt;发布订阅&lt;/h3&gt;
&lt;h4&gt;基本指令&lt;/h4&gt;
&lt;p&gt;Redis 发布订阅（pub/sub）是一种消息通信模式：发送者（pub）发送消息，订阅者（sub）接收消息&lt;/p&gt;
&lt;p&gt;Redis 客户端可以订阅任意数量的频道，每当有客户端向被订阅的频道发送消息（message）时，频道的&lt;strong&gt;所有订阅者都会收到消息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打开一个客户端订阅 channel1：&lt;code&gt;SUBSCRIBE channel1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开另一个客户端，给 channel1 发布消息 hello：&lt;code&gt;PUBLISH channel1 hello&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一个客户端可以看到发送的消息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅指令操作.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;客户端还可以通过 PSUBSCRIBE 命令订阅一个或多个模式，每当有其他客户端向某个频道发送消息时，消息不仅会被发送给这个频道的所有订阅者，还会被&lt;strong&gt;发送给所有与这个频道相匹配的模式的订阅者&lt;/strong&gt;，比如 &lt;code&gt;PSUBSCRIBE channel*&lt;/code&gt; 订阅模式，与 channel1 匹配&lt;/p&gt;
&lt;p&gt;注意：发布的消息没有持久化，所以订阅的客户端只能收到订阅后发布的消息&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;频道操作&lt;/h4&gt;
&lt;p&gt;Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里，键是某个被订阅的频道，值是一个记录所有订阅这个频道的客户端链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
	// 保存所有频道的订阅关系，
	dict *pubsub_channels;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端执行 SUBSCRIBE 命令订阅某个或某些频道，服务器会将客户端与频道进行关联：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;频道已经存在，直接将客户端添加到链表末尾&lt;/li&gt;
&lt;li&gt;频道还未有任何订阅者，在字典中为频道创建一个键值对，再将客户端添加到链表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UNSUBSCRIBE 命令用来退订某个频道，服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;模式操作&lt;/h4&gt;
&lt;p&gt;Redis 服务器将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 属性里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct redisServer {
	// 保存所有模式订阅关系，链表中每个节点是一个 pubsubPattern
	list *pubsub_patterns;
}

typedef struct pubsubPattern {
    // 订阅的客户端
    redisClient *client;
	// 被订阅的模式，比如  channel*
    robj *pattern; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端执行 PSUBSCRIBE 命令订阅某个模式，服务器会新建一个 pubsubPattern 结构并赋值，放入 pubsub_patterns 链表结尾&lt;/p&gt;
&lt;p&gt;模式的退订命令 PUNSUBSCRIBE 是订阅命令的反操作，服务器在 pubsub_patterns 链表中查找并删除对应的结构&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;发送消息&lt;/h4&gt;
&lt;p&gt;Redis 客户端执行 &lt;code&gt;PUBLISH &amp;lt;channel&amp;gt; &amp;lt;message&amp;gt;&lt;/code&gt; 命令将消息 message发送给频道 channel，服务器会执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 pubsub_channels 字典里找到频道 channel 的订阅者名单，将消息 message 发送给所有订阅者&lt;/li&gt;
&lt;li&gt;遍历整个 pubsub_patterns 链表，查找与 channel 频道相&lt;strong&gt;匹配的模式&lt;/strong&gt;，并将消息发送给所有订阅了这些模式的客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 如果频道和模式相匹配
if match(channel, pubsubPattern.pattern) {
    // 将消息发送给订阅该模式的客户端
    send_message(pubsubPattern.client, message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;查看信息&lt;/h4&gt;
&lt;p&gt;PUBSUB 命令用来查看频道或者模式的相关信息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUBSUB CHANNELS [pattern]&lt;/code&gt; 返回服务器当前被订阅的频道，其中 pattern 参数是可选的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不给定 pattern  参数，那么命令返回服务器当前被订阅的所有频道&lt;/li&gt;
&lt;li&gt;如果给定 pattern 参数，那么命令返回服务器当前被订阅的频道中与 pattern 模式相匹配的频道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]&lt;/code&gt;  命令接受任意多个频道作为输入参数，并返回这些频道的订阅者数量&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUBSUB NUMPAT&lt;/code&gt; 命令用于返回服务器当前被订阅模式的数量&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ACL 指令&lt;/h3&gt;
&lt;p&gt;Redis ACL 是 Access Control List（访问控制列表）的缩写，该功能允许根据可以执行的命令和可以访问的键来限制某些连接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL%E6%8C%87%E4%BB%A4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;acl cat：查看添加权限指令类别&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;acl whoami：查看当前用户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;acl setuser username on &amp;gt;password ~cached:* +get：设置有用户名、密码、ACL 权限（只能 get）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;监视器&lt;/h3&gt;
&lt;p&gt;MONITOR 命令，可以将客户端变为一个监视器，实时地接收并打印出服务器当前处理的命令请求的相关信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 实现原理
def MONITOR():
	// 打开客户端的监视器标志
	client.flags |= REDIS_MONITOR
        
  	// 将客户端添加到服务器状态的 redisServer.monitors链表的末尾
   	server.monitors.append(client)
  	// 向客户端返回 ok
	send_reply(&quot;OK&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器每次处理命令请求都会调用 replicationFeedMonitors 函数，函数将被处理的命令请求的相关信息&lt;strong&gt;发送给各个监视器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-监视器.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; MONITOR 
OK 
1378822099.421623 [0 127.0.0.1:56604] &quot;PING&quot; 
1378822105.089572 [0 127.0.0.1:56604] &quot;SET&quot; &quot;msg&quot; &quot;hello world&quot; 
1378822109.036925 [0 127.0.0.1:56604] &quot;SET&quot; &quot;number&quot; &quot;123&quot; 
1378822140.649496 (0 127.0.0.1:56604] &quot;SADD&quot; &quot;fruits&quot; &quot;Apple&quot; &quot;Banana&quot; &quot;Cherry&quot; 
1378822154.117160 [0 127.0.0.1:56604] &quot;EXPIRE&quot; &quot;msg&quot; &quot;10086&quot; 
1378822257.329412 [0 127.0.0.1:56604] &quot;KEYS&quot; &quot;*&quot; 
1378822258.690131 [0 127.0.0.1:56604] &quot;DBSIZE&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;批处理&lt;/h3&gt;
&lt;p&gt;Redis 的管道 Pipeline 机制可以一次处理多条指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pipeline 中的多条命令非原子性，因为在向管道内添加命令时，其他客户端的发送的命令仍然在执行&lt;/li&gt;
&lt;li&gt;原生批命令（MSET 等）是服务端实现，而 Pipeline 需要服务端与客户端共同完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Pipeline 封装的命令数量不能太多，数据量过大会增加客户端的等待时间，造成网络阻塞，Jedis 中的 Pipeline 使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建管道
Pipeline pipeline = jedis.pipelined();
for (int i = 1; i &amp;lt;= 100000; i++) {
    // 放入命令到管道
    pipeline.set(&quot;key_&quot; + i, &quot;value_&quot; + i);
    if (i % 1000 == 0) {
        // 每放入1000条命令，批量执行
        pipeline.sync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群下模式下，批处理命令的多个 key 必须落在一个插槽中，否则就会导致执行失败，N 条批处理命令的优化方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;串行命令：for 循环遍历，依次执行每个命令&lt;/li&gt;
&lt;li&gt;串行 slot：在客户端计算每个 key 的 slot，将 slot 一致的分为一组，每组都利用 Pipeline 批处理，串行执行各组命令&lt;/li&gt;
&lt;li&gt;并行 slot：在客户端计算每个 key 的 slot，将 slot 一致的分为一组，每组都利用 Pipeline 批处理，&lt;strong&gt;并行执行各组命令&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;hash_tag：将所有 key 设置相同的 hash_tag，则所有 key 的 slot 一定相同&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;耗时&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;串行命令&lt;/td&gt;
&lt;td&gt;N 次网络耗时 + N 次命令耗时&lt;/td&gt;
&lt;td&gt;实现简单&lt;/td&gt;
&lt;td&gt;耗时久&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;串行 slot&lt;/td&gt;
&lt;td&gt;m 次网络耗时 + N 次命令耗时，m = key 的 slot 个数&lt;/td&gt;
&lt;td&gt;耗时较短&lt;/td&gt;
&lt;td&gt;实现稍复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并行 slot&lt;/td&gt;
&lt;td&gt;1 次网络耗时 + N 次命令耗时&lt;/td&gt;
&lt;td&gt;耗时非常短&lt;/td&gt;
&lt;td&gt;实现复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hash_tag&lt;/td&gt;
&lt;td&gt;1 次网络耗时 + N 次命令耗时&lt;/td&gt;
&lt;td&gt;耗时非常短、实现简单&lt;/td&gt;
&lt;td&gt;容易出现&lt;strong&gt;数据倾斜&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;h3&gt;缓存方案&lt;/h3&gt;
&lt;h4&gt;缓存模式&lt;/h4&gt;
&lt;h5&gt;旁路缓存&lt;/h5&gt;
&lt;p&gt;缓存本质：弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟&lt;/p&gt;
&lt;p&gt;旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式，比较适合读请求比较多的场景&lt;/p&gt;
&lt;p&gt;Cache Aside Pattern 中服务端需要同时维系 DB 和 cache，并且是以 DB 的结果为准&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写操作：先更新 DB，然后直接删除 cache&lt;/li&gt;
&lt;li&gt;读操作：从 cache 中读取数据，读取到就直接返回；读取不到就从 DB 中读取数据返回，并放到 cache&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时序导致的不一致问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在写数据的过程中，不能先删除 cache 再更新 DB，因为会造成缓存的不一致。比如请求 1 先写数据 A，请求 2 随后读数据 A，当请求 1 删除 cache 后，请求 2 直接读取了 DB，此时请求 1 还没写入 DB（延迟双删）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在写数据的过程中，先更新 DB 再删除 cache 也会出现问题，但是概率很小，因为缓存的写入速度非常快&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;旁路缓存的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首次请求数据一定不在 cache 的问题，一般采用缓存预热的方法，将热点数据可以提前放入 cache 中&lt;/li&gt;
&lt;li&gt;写操作比较频繁的话导致 cache 中的数据会被频繁被删除，影响缓存命中率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;删除缓存而不是更新缓存的原因&lt;/strong&gt;：每次更新数据库都更新缓存，造成无效写操作较多（懒惰加载，需要的时候再放入缓存）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;读写穿透&lt;/h5&gt;
&lt;p&gt;读写穿透模式 Read/Write Through Pattern：服务端把 cache 视为主要数据存储，从中读取数据并将数据写入其中，cache 负责将此数据同步写入 DB，从而减轻了应用程序的职责&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写操作：先查 cache，cache 中不存在，直接更新 DB；cache 中存在则先更新 cache，然后 cache 服务更新 DB（同步更新 cache 和 DB）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读操作：从 cache 中读取数据，读取到就直接返回 ；读取不到先从 DB 加载，写入到 cache 后返回响应&lt;/p&gt;
&lt;p&gt;Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下，发生读请求的时候，如果 cache 中不存在对应的数据，是由客户端负责把数据写入 cache，而 Read Through Pattern 则是 cache 服务自己来写入缓存的，对客户端是透明的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read-Through Pattern 也存在首次不命中的问题，采用缓存预热解决&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;异步缓存&lt;/h5&gt;
&lt;p&gt;异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写，对比读写穿透不同的是 Write Behind Caching 是只更新缓存，不直接更新 DB，改为&lt;strong&gt;异步批量&lt;/strong&gt;的方式来更新 DB，可以减小写的成本&lt;/p&gt;
&lt;p&gt;缺点：这种模式对数据一致性没有高要求，可能出现 cache 还没异步更新 DB，服务就挂掉了&lt;/p&gt;
&lt;p&gt;应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DB 的写性能非常高，适合一些数据经常变化又对数据一致性要求不高的场景，比如浏览量、点赞量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 的 InnoDB Buffer Pool 机制用到了这种策略&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存一致&lt;/h4&gt;
&lt;p&gt;使用缓存代表不需要强一致性，只需要最终一致性&lt;/p&gt;
&lt;p&gt;缓存不一致的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据库和缓存数据强一致场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步双写：更新 DB 时同样更新 cache，保证在一个事务中，通过加锁来保证更新 cache 时不存在线程安全问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;延迟双删：先淘汰缓存再写数据库，休眠 1 秒再次淘汰缓存，可以将 1 秒内造成的缓存脏数据再次删除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步通知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 MQ 的异步通知：对数据的修改后，代码需要发送一条消息到 MQ 中，缓存服务监听 MQ 消息&lt;/li&gt;
&lt;li&gt;Canal 订阅 MySQL binlog 的变更上报给 Kafka，系统监听 Kafka 消息触发缓存失效，或者直接将变更发送到处理服务，&lt;strong&gt;没有任何代码侵入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;低耦合，可以同时通知多个缓存服务，但是时效性一般，可能存在中间不一致状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;低一致性场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新 DB 的时候同样更新 cache，但是给缓存加一个比较短的过期时间，这样就可以保证即使数据不一致影响也比较小&lt;/li&gt;
&lt;li&gt;使用 Redis 自带的内存淘汰机制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存问题&lt;/h4&gt;
&lt;h5&gt;缓存预热&lt;/h5&gt;
&lt;p&gt;场景：宕机，服务器启动后迅速宕机&lt;/p&gt;
&lt;p&gt;问题排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;请求数量较高，大量的请求过来之后都需要去从缓存中获取数据，但是缓存中又没有，此时从数据库中查找数据然后将数据再存入缓存，造成了短期内对 redis 的高强度操作从而导致问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主从之间数据吞吐量较大，数据同步操作频度较高&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前置准备工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;日常例行统计数据访问记录，统计访问频度较高的热点数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;利用 LRU 数据删除策略，构建数据留存队列例如：storm 与 kafka 配合&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;准备工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将统计结果中的数据分类，根据级别，redis 优先加载级别较高的热点数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;利用分布式多服务器同时进行数据读取，提速数据加载过程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;热点数据主从同时预热&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实施：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用脚本程序固定触发数据预热过程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果条件允许，使用了 CDN（内容分发网络），效果会更好&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说：缓存预热就是系统启动前，提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候，先查询数据库，然后再将数据缓存的问题，用户直接查询事先被预热的缓存数据&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存雪崩&lt;/h5&gt;
&lt;p&gt;场景：数据库服务器崩溃，一连串的问题会随之而来&lt;/p&gt;
&lt;p&gt;问题排查：在一个较短的时间内，&lt;strong&gt;缓存中较多的 key 集中过期&lt;/strong&gt;，此周期内请求访问过期的数据 Redis 未命中，Redis 向数据库获取数据，数据库同时收到大量的请求无法及时处理。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加锁，慎用&lt;/li&gt;
&lt;li&gt;设置热点数据永远不过期，如果缓存数据库是分布式部署，将热点数据均匀分布在不同搞得缓存数据库中&lt;/li&gt;
&lt;li&gt;缓存数据的过期时间设置随机，防止同一时间大量数据过期现象发生&lt;/li&gt;
&lt;li&gt;构建&lt;strong&gt;多级缓存&lt;/strong&gt;架构，Nginx 缓存 + Redis 缓存 + ehcache 缓存&lt;/li&gt;
&lt;li&gt;灾难预警机制，监控 Redis 服务器性能指标，CPU 使用率、内存容量、平均响应时间、线程数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限流、降级&lt;/strong&gt;：短时间范围内牺牲一些客户体验，限制一部分请求访问，降低应用服务器压力，待业务低速运转后再逐步放开访问&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总的来说：缓存雪崩就是瞬间过期数据量太大，导致对数据库服务器造成压力。如能够有效避免过期时间集中，可以有效解决雪崩现象的出现（约 40%），配合其他策略一起使用，并监控服务器的运行数据，根据运行记录做快速调整。&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存击穿&lt;/h5&gt;
&lt;p&gt;缓存击穿也叫热点 Key 问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Redis 中某个 key 过期，该 key 访问量巨大&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个数据请求从服务器直接压到 Redis 后，均未命中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redis 在短时间内发起了大量对数据库中同一数据的访问&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;预先设定：以电商为例，每个商家根据店铺等级，指定若干款主打商品，在购物节期间，加大此类信息 key 的过期时长 注意：购物节不仅仅指当天，以及后续若干天，访问峰值呈现逐渐降低的趋势&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现场调整：监控访问量，对自然流量激增的数据&lt;strong&gt;延长过期时间或设置为永久性 key&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后台刷新数据：启动定时任务，高峰期来临之前，刷新数据有效期，确保不丢失&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;二级缓存&lt;/strong&gt;：设置不同的失效时间，保障不会被同时淘汰就行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加锁：分布式锁，防止被击穿，但是要注意也是性能瓶颈，慎重&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总的来说：缓存击穿就是单个高热数据过期的瞬间，数据访问量较大，未命中 Redis 后，发起了大量对同一数据的数据库访问，导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行，配合运行监控测试与即时调整策略，毕竟单个 key 的过期监控难度较高，配合雪崩处理策略即可&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;缓存穿透&lt;/h5&gt;
&lt;p&gt;场景：系统平稳运行过程中，应用服务器流量随时间增量较大，Redis 服务器命中率随时间逐步降低，Redis 内存平稳，内存无压力，Redis 服务器 CPU 占用激增，数据库服务器压力激增，数据库崩溃&lt;/p&gt;
&lt;p&gt;问题排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Redis 中大面积出现未命中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;出现非正常 URL 访问&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问了不存在的数据，跳过了 Redis 缓存，数据库页查询不到对应数据&lt;/li&gt;
&lt;li&gt;Redis 获取到 null 数据未进行持久化，直接返回&lt;/li&gt;
&lt;li&gt;出现黑客攻击服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;缓存 null：对查询结果为 null 的数据进行缓存，设定短时限，例如 30-60 秒，最高 5 分钟&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;白名单策略：提前预热各种分类&lt;strong&gt;数据 id 对应的 bitmaps&lt;/strong&gt;，id 作为 bitmaps 的 offset，相当于设置了数据白名单。当加载正常数据时放行，加载异常数据时直接拦截（效率偏低），也可以使用布隆过滤器（有关布隆过滤器的命中问题对当前状况可以忽略）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实时监控：实时监控 Redis 命中率（业务正常范围时，通常会有一个波动值）与 null 数据的占比&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非活动时段波动：通常检测 3-5 倍，超过 5 倍纳入重点排查对象&lt;/li&gt;
&lt;li&gt;活动时段波动：通常检测10-50 倍，超过 50 倍纳入重点排查对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据倍数不同，启动不同的排查流程。然后使用黑名单进行防控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;key 加密：临时启动防灾业务 key，对 key 进行业务层传输加密服务，设定校验程序，过来的 key 校验；例如每天随机分配 60 个加密串，挑选 2 到 3 个，混淆到页面数据 id 中，发现访问 key 不满足规则，驳回数据访问&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总的来说：缓存击穿是指访问了不存在的数据，跳过了合法数据的 Redis 数据缓存阶段，&lt;strong&gt;每次访问数据库&lt;/strong&gt;，导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值，当出现此类情况以毒攻毒，并及时报警。无论是黑名单还是白名单，都是对整体系统的压力，警报解除后尽快移除&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV15y4y1r7X3&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Key 设计&lt;/h3&gt;
&lt;p&gt;大 Key：通常以 Key 的大小和 Key 中成员的数量来综合判定，引发的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端执行命令的时长变慢&lt;/li&gt;
&lt;li&gt;Redis 内存达到 maxmemory 定义的上限引发操作阻塞或重要的 Key 被逐出，甚至引发内存溢出（OOM）&lt;/li&gt;
&lt;li&gt;集群架构下，某个数据分片的内存使用率远超其他数据分片，使&lt;strong&gt;数据分片的内存资源不均衡&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;对大 Key 执行读请求，会使 Redis 实例的带宽使用率被占满，导致自身服务变慢，同时易波及相关的服务&lt;/li&gt;
&lt;li&gt;对大 Key 执行删除操作，会造成主库较长时间的阻塞，进而可能引发同步中断或主从切换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;热 Key：通常以其接收到的 Key 被请求频率来判定，引发的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;占用大量的 CPU 资源，影响其他请求并导致整体性能降低&lt;/li&gt;
&lt;li&gt;分布式集群架构下，产生&lt;strong&gt;访问倾斜&lt;/strong&gt;，即某个数据分片被大量访问，而其他数据分片处于空闲状态，可能引起该数据分片的连接数被耗尽，新的连接建立请求被拒绝等问题&lt;/li&gt;
&lt;li&gt;在抢购或秒杀场景下，可能因商品对应库存 Key 的请求量过大，超出 Redis 处理能力造成超卖&lt;/li&gt;
&lt;li&gt;热 Key 的请求压力数量超出 Redis 的承受能力易造成缓存击穿，即大量请求将被直接指向后端的存储层，导致存储访问量激增甚至宕机，从而影响其他业务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;热 Key 分类两种，治理方式如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种是单一数据，比如秒杀场景，假设总量 10000 可以拆为多个 Key 进行访问，每次对请求进行路由到不同的 Key 访问，保证最终一致性，但是会出现访问不同 Key 产生的剩余量是不同的，这时可以通过前端进行 Mock 假数据&lt;/li&gt;
&lt;li&gt;一种是多数据集合，比如进行 ID 过滤，这时可以添加本地 LRU 缓存，减少对热 Key 的访问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文档：https://help.aliyun.com/document_detail/353223.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;慢查询&lt;/h3&gt;
&lt;p&gt;确认服务和 Redis 之间的链路是否正常，排除网络原因后进行 Redis 的排查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用复杂度过高的命令&lt;/li&gt;
&lt;li&gt;操作大 key，分配内存和释放内存会比较耗时&lt;/li&gt;
&lt;li&gt;key 集中过期，导致定时任务需要更长的时间去清理&lt;/li&gt;
&lt;li&gt;实例内存达到上限，每次写入新的数据之前，Redis 必须先从实例中踢出一部分数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/traditional/p/15633919.html（非常好）&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Java&lt;/h1&gt;
&lt;h2&gt;JDBC&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;JDBC（Java DataBase Connectivity，Java 数据库连接）是一种用于执行 SQL 语句的 Java API，可以为多种关系型数据库提供统一访问，是由一组用 Java 语言编写的类和接口组成的。&lt;/p&gt;
&lt;p&gt;JDBC 是 Java 官方提供的一套规范（接口），用于帮助开发人员快速实现不同关系型数据库的连接&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;功能类&lt;/h3&gt;
&lt;h4&gt;DriverManager&lt;/h4&gt;
&lt;p&gt;DriverManager：驱动管理对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注册驱动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注册给定的驱动：&lt;code&gt;public static void registerDriver(Driver driver)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码实现语法：&lt;code&gt;Class.forName(&quot;com.mysql.jdbc.Driver)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;com.mysql.jdbc.Driver 中存在静态代码块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static {
    try {
        DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {
        throw new RuntimeException(&quot;Can&apos;t register driver!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不需要通过 DriverManager 调用静态方法 registerDriver，因为 Driver 类被使用，则自动执行静态代码块完成注册驱动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jar 包中 META-INF 目录下存在一个 java.sql.Driver 配置文件，文件中指定了 com.mysql.jdbc.Driver&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取数据库连接并返回连接对象：&lt;/p&gt;
&lt;p&gt;方法：&lt;code&gt;public static Connection getConnection(String url, String user, String password)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;url：指定连接的路径，语法为 &lt;code&gt;jdbc:mysql://ip地址(域名):端口号/数据库名称&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;user：用户名&lt;/li&gt;
&lt;li&gt;password：密码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Connection&lt;/h4&gt;
&lt;p&gt;Connection：数据库连接对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取执行者对象
&lt;ul&gt;
&lt;li&gt;获取普通执行者对象：&lt;code&gt;Statement createStatement()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取预编译执行者对象：&lt;code&gt;PreparedStatement prepareStatement(String sql)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;管理事务
&lt;ul&gt;
&lt;li&gt;开启事务：&lt;code&gt;setAutoCommit(boolean autoCommit)&lt;/code&gt;，false 开启事务，true 自动提交模式（默认）&lt;/li&gt;
&lt;li&gt;提交事务：&lt;code&gt;void commit()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;回滚事务：&lt;code&gt;void rollback()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;释放资源
&lt;ul&gt;
&lt;li&gt;释放此 Connection 对象的数据库和 JDBC 资源：&lt;code&gt;void close()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Statement&lt;/h4&gt;
&lt;p&gt;Statement：执行 sql 语句的对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行 DML 语句：&lt;code&gt;int executeUpdate(String sql)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;返回值 int：返回影响的行数&lt;/li&gt;
&lt;li&gt;参数 sql：可以执行 insert、update、delete 语句&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行 DQL 语句：&lt;code&gt;ResultSet executeQuery(String sql)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;返回值 ResultSet：封装查询的结果&lt;/li&gt;
&lt;li&gt;参数 sql：可以执行 select 语句&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;释放资源
&lt;ul&gt;
&lt;li&gt;释放此 Statement 对象的数据库和 JDBC 资源：&lt;code&gt;void close()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;ResultSet&lt;/h4&gt;
&lt;p&gt;ResultSet：结果集对象，ResultSet 对象维护了一个游标，指向当前的数据行，初始在第一行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判断结果集中是否有数据：&lt;code&gt;boolean next()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;有数据返回 true，并将索引&lt;strong&gt;向下移动一行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;没有数据返回 false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;获取结果集中&lt;strong&gt;当前行&lt;/strong&gt;的数据：&lt;code&gt;XXX getXxx(&quot;列名&quot;)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;XXX 代表数据类型（要获取某列数据，这一列的数据类型）&lt;/li&gt;
&lt;li&gt;例如：String getString(&quot;name&quot;);   int getInt(&quot;age&quot;);&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;释放资源
&lt;ul&gt;
&lt;li&gt;释放 ResultSet 对象的数据库和 JDBC 资源：&lt;code&gt;void close()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建db14数据库
CREATE DATABASE db14;

-- 使用db14数据库
USE db14;

-- 创建student表
CREATE TABLE student(
	sid INT PRIMARY KEY AUTO_INCREMENT,	-- 学生id
	NAME VARCHAR(20),					-- 学生姓名
	age INT,							-- 学生年龄
	birthday DATE,						-- 学生生日
);

-- 添加数据
INSERT INTO student VALUES (NULL,&apos;张三&apos;,23,&apos;1999-09-23&apos;),(NULL,&apos;李四&apos;,24,&apos;1998-08-10&apos;),
(NULL,&apos;王五&apos;,25,&apos;1996-06-06&apos;),(NULL,&apos;赵六&apos;,26,&apos;1994-10-20&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDBC 连接代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JDBCDemo01 {
    public static void main(String[] args) throws Exception{
        //1.导入jar包
        //2.注册驱动
        Class.forName(&quot;com.mysql.jdbc.Driver&quot;);

        //3.获取连接
        Connection con = DriverManager.getConnection(&quot;jdbc:mysql://192.168.2.184:3306/db2&quot;,&quot;root&quot;,&quot;123456&quot;);

        //4.获取执行者对象
        Statement stat = con.createStatement();

        //5.执行sql语句，并且接收结果
        String sql = &quot;SELECT * FROM user&quot;;
        ResultSet rs = stat.executeQuery(sql);

        //6.处理结果
        while(rs.next()) {
            System.out.println(rs.getInt(&quot;id&quot;) + &quot;\t&quot; + rs.getString(&quot;name&quot;));
        }

        //7.释放资源
        con.close();
        stat.close();
        con.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;注入攻击&lt;/h3&gt;
&lt;h4&gt;攻击演示&lt;/h4&gt;
&lt;p&gt;SQL 注入攻击演示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在登录界面，输入一个错误的用户名或密码，也可以登录成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/SQL%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB%E6%BC%94%E7%A4%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原理：我们在密码处输入的所有内容，都应该认为是密码的组成，但是 Statement 对象在执行 SQL 语句时，将一部分内容当做查询条件来执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM user WHERE loginname=&apos;aaa&apos; AND password=&apos;aaa&apos; OR &apos;1&apos;=&apos;1&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;攻击解决&lt;/h4&gt;
&lt;p&gt;PreparedStatement：预编译 sql 语句的执行者对象，继承 &lt;code&gt;PreparedStatement extends Statement&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在执行 sql 语句之前，将 sql 语句进行提前编译，&lt;strong&gt;明确 sql 语句的格式&lt;/strong&gt;，剩余的内容都会认为是参数&lt;/li&gt;
&lt;li&gt;sql 语句中的参数使用 ? 作为&lt;strong&gt;占位符&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为 ? 占位符赋值的方法：&lt;code&gt;setXxx(int parameterIndex, xxx data)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数1：? 的位置编号（编号从 1 开始）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数2：? 的实际参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String sql = &quot;SELECT * FROM user WHERE loginname=? AND password=?&quot;;
pst = con.prepareStatement(sql);
pst.setString(1,loginName);
pst.setString(2,password);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行 sql 语句的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行 insert、update、delete 语句：&lt;code&gt;int executeUpdate()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行 select 语句：&lt;code&gt;ResultSet executeQuery()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;连接池&lt;/h3&gt;
&lt;h4&gt;概念&lt;/h4&gt;
&lt;p&gt;数据库连接背景：数据库连接是一种关键的、有限的、昂贵的资源，这一点在多用户的网页应用程序中体现得尤为突出。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性，影响到程序的性能指标。&lt;/p&gt;
&lt;p&gt;数据库连接池：&lt;strong&gt;数据库连接池负责分配、管理和释放数据库连接&lt;/strong&gt;，它允许应用程序&lt;strong&gt;重复使用&lt;/strong&gt;一个现有的数据库连接，而不是再重新建立一个，这项技术能明显提高对数据库操作的性能。&lt;/p&gt;
&lt;p&gt;数据库连接池原理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5%E6%B1%A0%E5%8E%9F%E7%90%86%E5%9B%BE%E8%A7%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;归还连接&lt;/h4&gt;
&lt;p&gt;使用动态代理的方式来改进&lt;/p&gt;
&lt;p&gt;自定义数据库连接池类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyDataSource implements DataSource {
    //1.准备一个容器。用于保存多个数据库连接对象
    private static List&amp;lt;Connection&amp;gt; pool = Collections.synchronizedList(new ArrayList&amp;lt;&amp;gt;());

    //2.定义静态代码块,获取多个连接对象保存到容器中
    static{
        for(int i = 1; i &amp;lt;= 10; i++) {
            Connection con = JDBCUtils.getConnection();
            pool.add(con);
        }
    }
    //3.提供一个获取连接池大小的方法
    public int getSize() {
        return pool.size();
    }

   	//动态代理方式
    @Override
    public Connection getConnection() throws SQLException {
        if(pool.size() &amp;gt; 0) {
            Connection con = pool.remove(0);

            Connection proxyCon = (Connection) Proxy.newProxyInstance(
                con.getClass().getClassLoader(), new Class[]{Connection.class}, 
                new InvocationHandler() {
                /*
                    执行Connection实现类连接对象所有的方法都会经过invoke
                    如果是close方法，归还连接
                    如果不是，直接执行连接对象原有的功能即可
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if(method.getName().equals(&quot;close&quot;)) {
                        //归还连接
                        pool.add(con);
                        return null;
                    }else {
                        return method.invoke(con,args);
                    }
                }
            });
            return proxyCon;
        }else {
            throw new RuntimeException(&quot;连接数量已用尽&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;开源项目&lt;/h4&gt;
&lt;h5&gt;C3P0&lt;/h5&gt;
&lt;p&gt;使用 C3P0 连接池：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置文件名称：c3p0-config.xml，必须放在 src 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;c3p0-config&amp;gt;
  &amp;lt;!-- 使用默认的配置读取连接池对象 --&amp;gt;
  &amp;lt;default-config&amp;gt;
  	&amp;lt;!--  连接参数 --&amp;gt;
    &amp;lt;property name=&quot;driverClass&quot;&amp;gt;com.mysql.jdbc.Driver&amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;jdbcUrl&quot;&amp;gt;jdbc:mysql://192.168.2.184:3306/db14&amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;user&quot;&amp;gt;root&amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;password&quot;&amp;gt;123456&amp;lt;/property&amp;gt;
    
    &amp;lt;!-- 连接池参数 --&amp;gt;
    &amp;lt;!--初始化数量--&amp;gt;
    &amp;lt;property name=&quot;initialPoolSize&quot;&amp;gt;5&amp;lt;/property&amp;gt;
    &amp;lt;!--最大连接数量--&amp;gt;
    &amp;lt;property name=&quot;maxPoolSize&quot;&amp;gt;10&amp;lt;/property&amp;gt;
    &amp;lt;!--超时时间 3000ms--&amp;gt;
    &amp;lt;property name=&quot;checkoutTimeout&quot;&amp;gt;3000&amp;lt;/property&amp;gt;
  &amp;lt;/default-config&amp;gt;

  &amp;lt;named-config name=&quot;otherc3p0&quot;&amp;gt; 
    &amp;lt;!--  连接参数 --&amp;gt;
    &amp;lt;!-- 连接池参数 --&amp;gt;
  &amp;lt;/named-config&amp;gt;
&amp;lt;/c3p0-config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码演示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class C3P0Test1 {
    public static void main(String[] args) throws Exception{
        //1.创建c3p0的数据库连接池对象
        DataSource dataSource = new ComboPooledDataSource();

        //2.通过连接池对象获取数据库连接
        Connection con = dataSource.getConnection();

        //3.执行操作
        String sql = &quot;SELECT * FROM student&quot;;
        PreparedStatement pst = con.prepareStatement(sql);

        //4.执行sql语句，接收结果集
        ResultSet rs = pst.executeQuery();

        //5.处理结果集
        while(rs.next()) {
            System.out.println(rs.getInt(&quot;sid&quot;) + &quot;\t&quot; + rs.getString(&quot;name&quot;) + &quot;\t&quot; + rs.getInt(&quot;age&quot;) + &quot;\t&quot; + rs.getDate(&quot;birthday&quot;));
        }

        //6.释放资源
        rs.close();   pst.close();   con.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Druid&lt;/h5&gt;
&lt;p&gt;Druid 连接池：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置文件：druid.properties，必须放在 src 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.2.184:3306/db14
username=root
password=123456
initialSize=5
maxActive=10
maxWait=3000
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码演示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DruidTest1 {
    public static void main(String[] args) throws Exception{
        //获取配置文件的流对象
        InputStream is = DruidTest1.class.getClassLoader().getResourceAsStream(&quot;druid.properties&quot;);

        //1.通过Properties集合，加载配置文件
        Properties prop = new Properties();
        prop.load(is);

        //2.通过Druid连接池工厂类获取数据库连接池对象
        DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);

        //3.通过连接池对象获取数据库连接进行使用
        Connection con = dataSource.getConnection();
        
		//4.执行sql语句，接收结果集
        String sql = &quot;SELECT * FROM student&quot;;
        PreparedStatement pst = con.prepareStatement(sql);
        ResultSet rs = pst.executeQuery();

        //5.处理结果集
        while(rs.next()) {
            System.out.println(rs.getInt(&quot;sid&quot;) + &quot;\t&quot; + rs.getString(&quot;name&quot;) + &quot;\t&quot; + rs.getInt(&quot;age&quot;) + &quot;\t&quot; + rs.getDate(&quot;birthday&quot;));
        }

        //6.释放资源
        rs.close();   pst.close();   con.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Jedis&lt;/h2&gt;
&lt;h3&gt;基本使用&lt;/h3&gt;
&lt;p&gt;Jedis 用于 Java 语言连接 Redis 服务，并提供对应的操作 API&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;jar 包导入&lt;/p&gt;
&lt;p&gt;下载地址：https://mvnrepository.com/artifact/redis.clients/jedis&lt;/p&gt;
&lt;p&gt;基于 maven：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;2.9.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端连接 Redis：API 文档 http://xetorthio.github.io/jedis/&lt;/p&gt;
&lt;p&gt;连接 redis：&lt;code&gt;Jedis jedis = new Jedis(&quot;192.168.0.185&quot;, 6379)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;操作 redis：&lt;code&gt;jedis.set(&quot;name&quot;, &quot;seazean&quot;);  jedis.get(&quot;name&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;关闭 redis：&lt;code&gt;jedis.close()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JedisTest {
    public static void main(String[] args) {
        //1.获取连接对象
        Jedis jedis = new Jedis(&quot;192.168.2.185&quot;,6379);
        //2.执行操作
        jedis.set(&quot;age&quot;,&quot;39&quot;);
        String hello = jedis.get(&quot;hello&quot;);
        System.out.println(hello);
        jedis.lpush(&quot;list1&quot;,&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;);
        List&amp;lt;String&amp;gt; list1 = jedis.lrange(&quot;list1&quot;, 0, -1);
        for (String s:list1 ) {
            System.out.println(s);
        }
        jedis.sadd(&quot;set1&quot;,&quot;abc&quot;,&quot;abc&quot;,&quot;def&quot;,&quot;poi&quot;,&quot;cba&quot;);
        Long len = jedis.scard(&quot;set1&quot;);
        System.out.println(len);
        //3.关闭连接
        jedis.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;工具类&lt;/h3&gt;
&lt;p&gt;连接池对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JedisPool：Jedis 提供的连接池技术&lt;/li&gt;
&lt;li&gt;poolConfig：连接池配置对象&lt;/li&gt;
&lt;li&gt;host：Redis 服务地址&lt;/li&gt;
&lt;li&gt;port：Redis 服务端口号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JedisPool 的构造器如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) {
	this(poolConfig, host, port, 2000, (String)null, 0, (String)null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建配置文件 redis.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis.maxTotal=50
redis.maxIdel=10
redis.host=192.168.2.185
redis.port=6379
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工具类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JedisUtils {
    private static int maxTotal;
    private static int maxIdel;
    private static String host;
    private static int port;
    private static JedisPoolConfig jpc;
    private static JedisPool jp;

    static {
        ResourceBundle bundle = ResourceBundle.getBundle(&quot;redis&quot;);
        //最大连接数
        maxTotal = Integer.parseInt(bundle.getString(&quot;redis.maxTotal&quot;));
        //活动连接数
        maxIdel = Integer.parseInt(bundle.getString(&quot;redis.maxIdel&quot;));
        host = bundle.getString(&quot;redis.host&quot;);
        port = Integer.parseInt(bundle.getString(&quot;redis.port&quot;));

        //Jedis连接配置
        jpc = new JedisPoolConfig();
        jpc.setMaxTotal(maxTotal);
        jpc.setMaxIdle(maxIdel);
        //连接池对象
        jp = new JedisPool(jpc, host, port);
    }

    //对外访问接口，提供jedis连接对象，连接从连接池获取
    public static Jedis getJedis() {
        return jp.getResource();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SSM</title><link>https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/ssm/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%90%88%E9%9B%86/ssm/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MyBatis&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;p&gt;ORM（Object Relational Mapping）： 对象关系映射，指的是持久化数据和实体对象的映射模式，解决面向对象与关系型数据库存在的互不匹配的现象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-ORM%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MyBatis&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 是一个优秀的基于 Java 的持久层框架，它内部封装了 JDBC，使开发者只需关注 SQL 语句本身，而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 通过 XML 或注解的方式将要执行的各种 Statement 配置起来，并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题，对 JDBC 进行了封装，屏蔽了 JDBC 底层 API 的调用细节，使我们不用操作 JDBC API，就可以完成对数据库的持久化操作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MyBatis 官网地址：http://www.mybatis.org/mybatis-3/&lt;/p&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/37974444/&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;h3&gt;相关API&lt;/h3&gt;
&lt;p&gt;Resources：加载资源的工具类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InputStream getResourceAsStream(String fileName)&lt;/code&gt;：通过类加载器返回指定资源的字节流
&lt;ul&gt;
&lt;li&gt;参数 fileName 是放在 src 的核心配置文件名：MyBatisConfig.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SqlSessionFactoryBuilder：构建器，用来获取 SqlSessionFactory 工厂对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SqlSessionFactory build(InputStream is)&lt;/code&gt;：通过指定资源的字节输入流获取 SqlSession 工厂对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SqlSessionFactory：获取 SqlSession 构建者对象的工厂接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SqlSession openSession()&lt;/code&gt;：获取 SqlSession 构建者对象，并开启手动提交事务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SqlSession openSession(boolean)&lt;/code&gt;：获取 SqlSession 构建者对象，参数为 true 开启自动提交事务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SqlSession：构建者对象接口，用于执行 SQL、管理事务、接口代理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SqlSession &lt;strong&gt;代表和数据库的一次会话&lt;/strong&gt;，用完必须关闭&lt;/li&gt;
&lt;li&gt;SqlSession 和 Connection 一样都是非线程安全，每次使用都应该去获取新的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：&lt;strong&gt;update 数据需要提交事务，或开启默认提交&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SqlSession 常用 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;E&amp;gt; selectList(String statement,Object parameter)&lt;/td&gt;
&lt;td&gt;执行查询语句，返回List集合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T selectOne(String statement,Object parameter)&lt;/td&gt;
&lt;td&gt;执行查询语句，返回一个结果对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int insert(String statement,Object parameter)&lt;/td&gt;
&lt;td&gt;执行新增语句，返回影响行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int update(String statement,Object parameter)&lt;/td&gt;
&lt;td&gt;执行删除语句，返回影响行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int delete(String statement,Object parameter)&lt;/td&gt;
&lt;td&gt;执行修改语句，返回影响行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void commit()&lt;/td&gt;
&lt;td&gt;提交事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void rollback()&lt;/td&gt;
&lt;td&gt;回滚事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T getMapper(Class&amp;lt;T&amp;gt; cls)&lt;/td&gt;
&lt;td&gt;获取指定接口的代理实现类对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void close()&lt;/td&gt;
&lt;td&gt;释放资源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;映射配置&lt;/h3&gt;
&lt;p&gt;映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句，放在 src 目录下&lt;/p&gt;
&lt;p&gt;命名：StudentMapper.xml&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;映射配置文件的文件头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;mapper&amp;gt;：核心根标签&lt;/li&gt;
&lt;li&gt;namespace：属性，名称空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;功能标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt; select &amp;gt;：查询功能标签&lt;/li&gt;
&lt;li&gt;&amp;lt;insert&amp;gt;：新增功能标签&lt;/li&gt;
&lt;li&gt;&amp;lt;update&amp;gt;：修改功能标签&lt;/li&gt;
&lt;li&gt;&amp;lt;delete&amp;gt;：删除功能标签
&lt;ul&gt;
&lt;li&gt;id：属性，唯一标识，配合名称空间使用&lt;/li&gt;
&lt;li&gt;resultType：指定结果映射对象类型，和对应的方法的返回值类型（全限定名）保持一致，但是如果返回值是 List 则和其泛型保持一致&lt;/li&gt;
&lt;li&gt;parameterType：指定参数映射对象类型，必须和对应的方法的参数类型（全限定名）保持一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;statementType&lt;/strong&gt;：可选 STATEMENT，PREPARED 或 CALLABLE，默认值：PREPARED
&lt;ul&gt;
&lt;li&gt;STATEMENT：直接操作 SQL，使用 Statement 不进行预编译，获取数据：$&lt;/li&gt;
&lt;li&gt;PREPARED：预处理参数，使用 PreparedStatement 进行预编译，获取数据：#&lt;/li&gt;
&lt;li&gt;CALLABLE：执行存储过程，CallableStatement&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数获取方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL 获取参数：&lt;code&gt;#{属性名}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;StudentMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;selectById&quot; resultType=&quot;student&quot; parameterType=&quot;int&quot;&amp;gt;
		SELECT * FROM student WHERE id = #{id}
    &amp;lt;/select&amp;gt;
&amp;lt;mapper/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;强烈推荐官方文档：https://mybatis.org/mybatis-3/zh/sqlmap-xml.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;核心配置&lt;/h3&gt;
&lt;p&gt;核心配置文件包含了 MyBatis 最核心的设置和属性信息，如数据库的连接、事务、连接池信息等&lt;/p&gt;
&lt;p&gt;命名：MyBatisConfig.xml&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心配置文件的文件头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;configuration&amp;gt;：核心根标签&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入连接配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;properties&amp;gt;： 引入数据库连接配置文件标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;resource：属性，指定配置文件名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;properties resource=&quot;jdbc.properties&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调整设置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;settings&amp;gt;：可以改变 Mybatis 运行时行为&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;起别名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;typeAliases&amp;gt;：为全类名起别名的父标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;typeAlias&amp;gt;：为全类名起别名的子标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;type：指定全类名&lt;/li&gt;
&lt;li&gt;alias：指定别名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;package&amp;gt;：为指定包下所有类起别名的子标签，别名就是类名，首字母小写&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--起别名--&amp;gt;
&amp;lt;typeAliases&amp;gt;
	&amp;lt;typeAlias type=&quot;bean.Student&quot; alias=&quot;student&quot;/&amp;gt;
	&amp;lt;package name=&quot;com.seazean.bean&quot;/&amp;gt;
		&amp;lt;!--二选一--&amp;gt;
&amp;lt;/typeAliase&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自带别名：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;别名&lt;/th&gt;
&lt;th&gt;数据类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;java.lang.String&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long&lt;/td&gt;
&lt;td&gt;java.lang.Lang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;java.lang.Integer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;java.lang.Double&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;java.lang.Boolean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;....&lt;/td&gt;
&lt;td&gt;......&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置环境，可以配置多个标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;environments&amp;gt;：配置数据库环境标签，default 属性指定哪个 environment&lt;/li&gt;
&lt;li&gt;&amp;lt;environment&amp;gt;：配置数据库环境子标签，id 属性是唯一标识，与 default 对应&lt;/li&gt;
&lt;li&gt;&amp;lt;transactionManager&amp;gt;：事务管理标签，type 属性默认 JDBC 事务&lt;/li&gt;
&lt;li&gt;&amp;lt;dataSoure&amp;gt;：数据源标签
&lt;ul&gt;
&lt;li&gt;type 属性：POOLED 使用连接池（MyBatis 内置），UNPOOLED 不使用连接池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;property&amp;gt;：数据库连接信息标签。
&lt;ul&gt;
&lt;li&gt;name 属性取值：driver，url，username，password&lt;/li&gt;
&lt;li&gt;value 属性取值：与 name 对应&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入映射配置文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;mappers&amp;gt;：引入映射配置文件标签&lt;/li&gt;
&lt;li&gt;&amp;lt;mapper&amp;gt;：引入映射配置文件子标签
&lt;ul&gt;
&lt;li&gt;resource：属性指定映射配置文件的名称&lt;/li&gt;
&lt;li&gt;url：引用网路路径或者磁盘路径下的 sql 映射文件&lt;/li&gt;
&lt;li&gt;class：指定映射配置类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;package&amp;gt;：批量注册&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考官方文档：https://mybatis.org/mybatis-3/zh/configuration.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;#{}和${}&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;#{}：&lt;strong&gt;占位符，传入的内容会作为字符串&lt;/strong&gt;加上引号&lt;/strong&gt;，以&lt;strong&gt;预编译&lt;/strong&gt;的方式传入，将 sql 中的 #{} 替换为 ? 号，调用 PreparedStatement 的 set 方法来赋值，有效的防止 SQL 注入，提高系统安全性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;${}：&lt;strong&gt;拼接符，传入的内容会&lt;/strong&gt;直接替换&lt;/strong&gt;拼接，不会加上引号，可能存在 sql 注入的安全隐患&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;能用 #{} 的地方就用 #{}，不用或少用 ${}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;必须使用 ${} 的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表名作参数时，如：&lt;code&gt;SELECT * FROM ${tableName}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;order by 时，如：&lt;code&gt;SELECT * FROM t_user ORDER BY ${columnName}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sql 语句使用 #{}，properties 文件内容获取使用 ${}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;日志文件&lt;/h3&gt;
&lt;p&gt;在日常开发过程中，排查问题时需要输出 MyBatis 真正执行的 SQL 语句、参数、结果等信息，就可以借助 log4j 的功能来实现执行信息的输出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在核心配置文件根标签内配置 log4j&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置LOG4J--&amp;gt;
&amp;lt;settings&amp;gt;
	&amp;lt;setting name=&quot;logImpl&quot; value=&quot;log4j&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 src 目录下创建 log4j.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

#输出到日志文件
  #log4j.appender.file=org.apache.log4j.FileAppender
  #log4j.appender.file.File=../logs/iask.log
  #log4j.appender.file.layout=org.apache.log4j.PatternLayout
  #log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss}  %l  %m%n
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;slf4j-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.7.21&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;slf4j-log4j12&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.7.21&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实体类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private Integer id;
    private String name;
    private Integer age;
    .....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StudentMapper&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface StudentMapper {
    //查询全部
    public abstract List&amp;lt;Student&amp;gt; selectAll();

    //根据id查询
    public abstract Student selectById(Integer id);

    //新增数据
    public abstract Integer insert(Student stu);

    //修改数据
    public abstract Integer update(Student stu);

    //删除数据
    public abstract Integer delete(Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;config.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;driver=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.2.184:3306/db1
username=root
password=123456
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatisConfig.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;

&amp;lt;!--核心根标签--&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;!--引入数据库连接的配置文件--&amp;gt;
    &amp;lt;properties resource=&quot;jdbc.properties&quot;/&amp;gt;
    
    &amp;lt;!--配置LOG4J--&amp;gt;
    &amp;lt;settings&amp;gt;
        &amp;lt;setting name=&quot;logImpl&quot; value=&quot;log4j&quot;/&amp;gt;
    &amp;lt;/settings&amp;gt;
    
    &amp;lt;!--起别名--&amp;gt;
    &amp;lt;typeAliases&amp;gt;
        &amp;lt;typeAlias type=&quot;bean.Student&quot; alias=&quot;student&quot;/&amp;gt;
        &amp;lt;!--&amp;lt;package name=&quot;bean&quot;/&amp;gt;--&amp;gt;
    &amp;lt;/typeAliases&amp;gt;

    &amp;lt;!--配置数据库环境，可以多个环境，default指定哪个--&amp;gt;
    &amp;lt;environments default=&quot;mysql&quot;&amp;gt;
        &amp;lt;!--id属性唯一标识--&amp;gt;
        &amp;lt;environment id=&quot;mysql&quot;&amp;gt;
            &amp;lt;!--事务管理，type属性，默认JDBC事务--&amp;gt;
            &amp;lt;transactionManager type=&quot;JDBC&quot;&amp;gt;&amp;lt;/transactionManager&amp;gt;
            &amp;lt;!--数据源信息   type属性连接池--&amp;gt;
            &amp;lt;dataSource type=&quot;POOLED&quot;&amp;gt;
                &amp;lt;!--property获取数据库连接的配置信息--&amp;gt;
                &amp;lt;property name=&quot;driver&quot; value=&quot;${driver}&quot;/&amp;gt;
                &amp;lt;property name=&quot;url&quot; value=&quot;${url}&quot;/&amp;gt;
                &amp;lt;property name=&quot;username&quot; value=&quot;${username}&quot;/&amp;gt;
                &amp;lt;property name=&quot;password&quot; value=&quot;${password}&quot;/&amp;gt;
            &amp;lt;/dataSource&amp;gt;
        &amp;lt;/environment&amp;gt;
    &amp;lt;/environments&amp;gt;

    &amp;lt;!--引入映射配置文件--&amp;gt;
    &amp;lt;mappers&amp;gt;
        &amp;lt;!--mapper引入指定的映射配置 resource属性执行的映射配置文件的名称--&amp;gt;
        &amp;lt;mapper resource=&quot;StudentMapper.xml&quot;/&amp;gt;
    &amp;lt;/mappers&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StudentMapper.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;StudentMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;selectAll&quot; resultType=&quot;student&quot;&amp;gt;
        SELECT * FROM student
    &amp;lt;/select&amp;gt;

    &amp;lt;select id=&quot;selectById&quot; resultType=&quot;student&quot; parameterType=&quot;int&quot;&amp;gt;
        SELECT * FROM student WHERE id = #{id}
    &amp;lt;/select&amp;gt;

    &amp;lt;insert id=&quot;insert&quot; parameterType=&quot;student&quot;&amp;gt;
        INSERT INTO student VALUES (#{id},#{name},#{age})
    &amp;lt;/insert&amp;gt;

    &amp;lt;update id=&quot;update&quot; parameterType=&quot;student&quot;&amp;gt;
        UPDATE student SET name = #{name}, age = #{age} WHERE id = #{id}
    &amp;lt;/update&amp;gt;

    &amp;lt;delete id=&quot;delete&quot; parameterType=&quot;student&quot;&amp;gt;
        DELETE FROM student WHERE id = #{id}
    &amp;lt;/delete&amp;gt;

&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制层测试代码：根据 id 查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void selectById() throws Exception{
    //1.加载核心配置文件
    InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

    //2.获取SqlSession工厂对象
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);

    //3.通过工厂对象获取SqlSession对象
    SqlSession sqlSession = ssf.openSession();

    //4.执行映射配置文件中的sql语句，并接收结果
    Student stu = sqlSession.selectOne(&quot;StudentMapper.selectById&quot;, 3);

    //5.处理结果
    System.out.println(stu);

    //6.释放资源
    sqlSession.close();
    is.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制层测试代码：新增功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void insert() throws Exception{
    //1.加载核心配置文件
    //2.获取SqlSession工厂对象
    //3.通过工厂对象获取SqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    //4.执行映射配置文件中的sql语句，并接收结果
    Student stu = new Student(5, &quot;周七&quot;, 27);
    int result = sqlSession.insert(&quot;StudentMapper.insert&quot;, stu);

    //5.提交事务
    //sqlSession.commit();

    //6.处理结果
    System.out.println(result);

    //7.释放资源
    sqlSession.close();
    is.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;批量操作&lt;/h3&gt;
&lt;p&gt;三种方式实现批量操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;settings&amp;gt; 标签属性：这种方式属于&lt;strong&gt;全局批量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
    &amp;lt;setting name=&quot;defaultExecutorType&quot; value=&quot;BATCH&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;defaultExecutorType：配置默认的执行器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SIMPLE 就是普通的执行器（默认，每次执行都要重新设置参数）&lt;/li&gt;
&lt;li&gt;REUSE 执行器会重用预处理语句（只预设置一次参数，多次执行）&lt;/li&gt;
&lt;li&gt;BATCH 执行器不仅重用语句还会执行批量更新（只针对&lt;strong&gt;修改操作&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SqlSession &lt;strong&gt;会话内批量&lt;/strong&gt;操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void testBatch() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();

    // 可以执行批量操作的sqlSession
    SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    long start = System.currentTimeMillis();
    try{
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        for (int i = 0; i &amp;lt; 10000; i++) {
            mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), &quot;b&quot;, &quot;1&quot;));
        }
        openSession.commit();
        long end = System.currentTimeMillis();
        // 批量：（预编译sql一次==&amp;gt;设置参数===&amp;gt;10000次===&amp;gt;执行1次（类似管道））
        // 非批量：（预编译sql=设置参数=执行）==》10000   耗时更多
        System.out.println(&quot;执行时长：&quot; + (end - start));
    }finally{
        openSession.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring 配置文件方式（applicationContext.xml）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置一个可以进行批量执行的sqlSession  --&amp;gt;
&amp;lt;bean id=&quot;sqlSession&quot; class=&quot;org.mybatis.spring.SqlSessionTemplate&quot;&amp;gt;
    &amp;lt;constructor-arg name=&quot;sqlSessionFactory&quot; ref=&quot;sqlSessionFactoryBean&quot;/&amp;gt;
    &amp;lt;constructor-arg name=&quot;executorType&quot; value=&quot;BATCH&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private SqlSession sqlSession;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;代理开发&lt;/h2&gt;
&lt;h3&gt;代理规则&lt;/h3&gt;
&lt;p&gt;分层思想：控制层（controller）、业务层（service）、持久层（dao）&lt;/p&gt;
&lt;p&gt;调用流程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/%E5%88%86%E5%B1%82%E6%80%9D%E6%83%B3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传统方式实现 DAO 层，需要写接口和实现类。采用 Mybatis 的代理开发方式实现 DAO 层的开发，只需要编写 Mapper 接口（相当于 Dao 接口），由 Mybatis 框架根据接口定义创建接口的&lt;strong&gt;动态代理对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接口开发方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义接口&lt;/li&gt;
&lt;li&gt;操作数据库，MyBatis 框架根据接口，通过动态代理的方式生成代理对象，负责数据库的操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Mapper 接口开发需要遵循以下规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mapper.xml 文件中的 namespace 与 DAO 层 mapper 接口的全类名相同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mapper.xml 文件中的增删改查标签的id属性和 DAO 层 Mapper 接口方法名相同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mapper.xml 文件中的增删改查标签的 parameterType 属性和 DAO 层 Mapper 接口方法的参数相同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mapper.xml 文件中的增删改查标签的 resultType 属性和 DAO 层 Mapper 接口方法的返回值相同&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/%E6%8E%A5%E5%8F%A3%E4%BB%A3%E7%90%86%E6%96%B9%E5%BC%8F%E5%AE%9E%E7%8E%B0DAO%E5%B1%82.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;实现原理&lt;/h3&gt;
&lt;p&gt;通过动态代理开发模式，只编写一个接口不写实现类，通过 &lt;strong&gt;getMapper()&lt;/strong&gt; 方法最终获取到 MapperProxy 代理对象，而这个代理对象是 MyBatis 使用了 JDK 的动态代理技术生成的&lt;/p&gt;
&lt;p&gt;动态代理实现类对象在执行方法时最终调用了 &lt;strong&gt;MapperMethod.execute()&lt;/strong&gt; 方法，这个方法中通过 switch case 语句根据操作类型来判断是新增、修改、删除、查询操作，最后一步回到了 MyBatis 最原生的 &lt;strong&gt;SqlSession 方式来执行增删改查&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Student selectById(Integer id) {
    Student stu = null;
    SqlSession sqlSession = null;
    InputStream is = null;
    try{
        //1.加载核心配置文件
        is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

        //2.获取SqlSession工厂对象
        SqlSessionFactory s = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        sqlSession = s.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 

        //5.通过实现类对象调用方法，接收结果
        stu = mapper.selectById(id);
    } catch (Exception e) {
        e.getMessage();
    } finally {
        //6.释放资源
        if(sqlSession != null) {
            sqlSession.close();
        }
        if(is != null) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //7.返回结果
    return stu;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;结果映射&lt;/h2&gt;
&lt;h3&gt;相关标签&lt;/h3&gt;
&lt;p&gt;&amp;lt;resultType&amp;gt;：返回结果映射对象类型，和对应方法的返回值类型保持一致，但是如果返回值是 List 则和其泛型保持一致&lt;/p&gt;
&lt;p&gt;&amp;lt;resultMap&amp;gt;：返回一条记录的 Map，key 是列名，value 是对应的值，用来配置&lt;strong&gt;字段和对象属性&lt;/strong&gt;的映射关系标签，结果映射（和 resultType 二选一）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id 属性：唯一标识&lt;/li&gt;
&lt;li&gt;type 属性：实体对象类型&lt;/li&gt;
&lt;li&gt;autoMapping 属性：结果自动映射&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;resultMap&amp;gt; 内的核心配置文件标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;id&amp;gt;：配置主键映射关系标签&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;result&amp;gt;：配置非主键映射关系标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;column 属性：表中字段名称&lt;/li&gt;
&lt;li&gt;property 属性： 实体对象变量名称&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;association&amp;gt;：配置被包含&lt;strong&gt;单个对象&lt;/strong&gt;的映射关系标签，嵌套封装结果集（多对一、一对一）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;property 属性：被包含对象的变量名，要进行映射的属性名&lt;/li&gt;
&lt;li&gt;javaType 属性：被包含对象的数据类型，要进行映射的属性的类型（Java 中的 Bean 类）&lt;/li&gt;
&lt;li&gt;select 属性：加载复杂类型属性的映射语句的 ID，会从 column 属性指定的列中检索数据，作为参数传递给目标 select 语句&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;collection&amp;gt;：配置被包含&lt;strong&gt;集合对象&lt;/strong&gt;的映射关系标签，嵌套封装结果集（一对多、多对多）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;property 属性：被包含集合对象的变量名&lt;/li&gt;
&lt;li&gt;ofType 属性：集合中保存的对象数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;discriminator&amp;gt;：鉴别器，用来判断某列的值，根据得到某列的不同值做出不同自定义的封装行为&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自定义封装规则可以将数据库中比较复杂的数据类型映射为 JavaBean 中的属性&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;嵌套查询&lt;/h3&gt;
&lt;p&gt;子查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Blog {
    private int id;
    private String msg;
    private Author author;
    // set + get
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;blogResult&quot; type=&quot;Blog&quot; autoMapping = &quot;true&quot;&amp;gt;
    &amp;lt;association property=&quot;author&quot; column=&quot;author_id&quot; javaType=&quot;Author&quot; select=&quot;selectAuthor&quot;/&amp;gt;
&amp;lt;/resultMap&amp;gt;

&amp;lt;select id=&quot;selectBlog&quot; resultMap=&quot;blogResult&quot;&amp;gt;
    SELECT * FROM BLOG WHERE ID = #{id}
&amp;lt;/select&amp;gt;

&amp;lt;select id=&quot;selectAuthor&quot; resultType=&quot;Author&quot;&amp;gt;
    SELECT * FROM AUTHOR WHERE ID = #{id}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环引用：通过缓存解决&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;blogResult&quot; type=&quot;Blog&quot; autoMapping = &quot;true&quot;&amp;gt;
    &amp;lt;id column=&quot;id&quot; property=&quot;id&quot;/&amp;gt;
    &amp;lt;collection property=&quot;comment&quot; ofType=&quot;Comment&quot;&amp;gt;
        &amp;lt;association property=&quot;blog&quot; javaType=&quot;Blog&quot; resultMap=&quot;blogResult&quot;/&amp;gt;&amp;lt;!--y--&amp;gt;
    &amp;lt;/collection&amp;gt;
&amp;lt;/resultMap
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;多表查询&lt;/h3&gt;
&lt;h4&gt;一对一&lt;/h4&gt;
&lt;p&gt;一对一实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE person(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20),
	age INT
);
INSERT INTO person VALUES (NULL,&apos;张三&apos;,23),(NULL,&apos;李四&apos;,24),(NULL,&apos;王五&apos;,25);

CREATE TABLE card(
	id INT PRIMARY KEY AUTO_INCREMENT,
	number VARCHAR(30),
	pid INT,
	CONSTRAINT cp_fk FOREIGN KEY (pid) REFERENCES person(id)
);
INSERT INTO card VALUES (NULL,&apos;12345&apos;,1),(NULL,&apos;23456&apos;,2),(NULL,&apos;34567&apos;,3);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bean 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Card {
    private Integer id;     //主键id
    private String number;  //身份证号
    private Person p;       //所属人的对象
    ......
}

public class Person {
    private Integer id;     //主键id
    private String name;    //人的姓名
    private Integer age;    //人的年龄
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件 OneToOneMapper.xml，MyBatisConfig.xml 需要引入（可以把 bean 包下起别名）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;OneToOneMapper&quot;&amp;gt;

    &amp;lt;!--配置字段和实体对象属性的映射关系--&amp;gt;
    &amp;lt;resultMap id=&quot;oneToOne&quot; type=&quot;card&quot;&amp;gt;
       	&amp;lt;!--column 表中字段名称，property 实体对象变量名称--&amp;gt;
        &amp;lt;id column=&quot;cid&quot; property=&quot;id&quot; /&amp;gt;
        &amp;lt;result column=&quot;number&quot; property=&quot;number&quot; /&amp;gt;
        &amp;lt;!--
            association：配置被包含对象的映射关系
            property：被包含对象的变量名
            javaType：被包含对象的数据类型
        --&amp;gt;
        &amp;lt;association property=&quot;p&quot; javaType=&quot;bean.Person&quot;&amp;gt;
            &amp;lt;id column=&quot;pid&quot; property=&quot;id&quot; /&amp;gt;
            &amp;lt;result column=&quot;name&quot; property=&quot;name&quot; /&amp;gt;
            &amp;lt;result column=&quot;age&quot; property=&quot;age&quot; /&amp;gt;
        &amp;lt;/association&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;select id=&quot;selectAll&quot; resultMap=&quot;oneToOne&quot;&amp;gt; &amp;lt;!--SQL--&amp;gt;
        SELECT c.id cid,number,pid,NAME,age FROM card c,person p WHERE c.pid=p.id
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心配置文件 MyBatisConfig.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- mappers引入映射配置文件 --&amp;gt;
&amp;lt;mappers&amp;gt;
    &amp;lt;mapper resource=&quot;one_to_one/OneToOneMapper.xml&quot;/&amp;gt;
    &amp;lt;mapper resource=&quot;one_to_many/OneToManyMapper.xml&quot;/&amp;gt;
    &amp;lt;mapper resource=&quot;many_to_many/ManyToManyMapper.xml&quot;/&amp;gt;
&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test01 {
    @Test
    public void selectAll() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

        //2.获取SqlSession工厂对象
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = ssf.openSession(true);

        //4.获取OneToOneMapper接口的实现类对象
        OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class);

        //5.调用实现类的方法，接收结果
        List&amp;lt;Card&amp;gt; list = mapper.selectAll();

        //6.处理结果
        for (Card c : list) {
            System.out.println(c);
        }

        //7.释放资源
        sqlSession.close();
        is.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;一对多&lt;/h4&gt;
&lt;p&gt;一对多实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE classes(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20)
);
INSERT INTO classes VALUES (NULL,&apos;程序一班&apos;),(NULL,&apos;程序二班&apos;)

CREATE TABLE student(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(30),
	age INT,
	cid INT,
	CONSTRAINT cs_fk FOREIGN KEY (cid) REFERENCES classes(id)
);
INSERT INTO student VALUES (NULL,&apos;张三&apos;,23,1),(NULL,&apos;李四&apos;,24,1),(NULL,&apos;王五&apos;,25,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bean 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Classes {
    private Integer id;     //主键id
    private String name;    //班级名称
    private List&amp;lt;Student&amp;gt; students; //班级中所有学生对象
    ........
}
public class Student {
    private Integer id;     //主键id
    private String name;    //学生姓名
    private Integer age;    //学生年龄
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;映射配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;OneToManyMapper&quot;&amp;gt;
    &amp;lt;resultMap id=&quot;oneToMany&quot; type=&quot;bean.Classes&quot;&amp;gt;
        &amp;lt;id column=&quot;cid&quot; property=&quot;id&quot;/&amp;gt;
        &amp;lt;result column=&quot;cname&quot; property=&quot;name&quot;/&amp;gt;

        &amp;lt;!--collection：配置被包含的集合对象映射关系--&amp;gt;
        &amp;lt;collection property=&quot;students&quot; ofType=&quot;bean.Student&quot;&amp;gt;
            &amp;lt;id column=&quot;sid&quot; property=&quot;id&quot;/&amp;gt;
            &amp;lt;result column=&quot;sname&quot; property=&quot;name&quot;/&amp;gt;
            &amp;lt;result column=&quot;sage&quot; property=&quot;age&quot;/&amp;gt;
        &amp;lt;/collection&amp;gt;
    &amp;lt;/resultMap&amp;gt;
    &amp;lt;select id=&quot;selectAll&quot; resultMap=&quot;oneToMany&quot;&amp;gt; &amp;lt;!--SQL--&amp;gt;
        SELECT c.id cid,c.name cname,s.id sid,s.name sname,s.age sage FROM classes c,student s WHERE c.id=s.cid
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码实现片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//4.获取OneToManyMapper接口的实现类对象
OneToManyMapper mapper = sqlSession.getMapper(OneToManyMapper.class);

//5.调用实现类的方法，接收结果
List&amp;lt;Classes&amp;gt; classes = mapper.selectAll();

//6.处理结果
for (Classes cls : classes) {
    System.out.println(cls.getId() + &quot;,&quot; + cls.getName());
    List&amp;lt;Student&amp;gt; students = cls.getStudents();
    for (Student student : students) {
        System.out.println(&quot;\t&quot; + student);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;多对多&lt;/h4&gt;
&lt;p&gt;学生课程例子，中间表不需要 bean 实体类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据准备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE course(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20)
);
INSERT INTO course VALUES (NULL,&apos;语文&apos;),(NULL,&apos;数学&apos;);

CREATE TABLE stu_cr(
	id INT PRIMARY KEY AUTO_INCREMENT,
	sid INT,
	cid INT,
	CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id),
	CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id)
);
INSERT INTO stu_cr VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bean类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private Integer id;     //主键id
    private String name;    //学生姓名
    private Integer age;    //学生年龄
    private List&amp;lt;Course&amp;gt; courses;   // 学生所选择的课程集合
}
public class Course {
    private Integer id;     //主键id
    private String name;    //课程名称
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;ManyToManyMapper&quot;&amp;gt;
    &amp;lt;resultMap id=&quot;manyToMany&quot; type=&quot;Bean.Student&quot;&amp;gt;
        &amp;lt;id column=&quot;sid&quot; property=&quot;id&quot;/&amp;gt;
        &amp;lt;result column=&quot;sname&quot; property=&quot;name&quot;/&amp;gt;
        &amp;lt;result column=&quot;sage&quot; property=&quot;age&quot;/&amp;gt;

        &amp;lt;collection property=&quot;courses&quot; ofType=&quot;Bean.Course&quot;&amp;gt;
            &amp;lt;id column=&quot;cid&quot; property=&quot;id&quot;/&amp;gt;
            &amp;lt;result column=&quot;cname&quot; property=&quot;name&quot;/&amp;gt;
        &amp;lt;/collection&amp;gt;
    &amp;lt;/resultMap&amp;gt;
    &amp;lt;select id=&quot;selectAll&quot; resultMap=&quot;manyToMany&quot;&amp;gt; &amp;lt;!--SQL--&amp;gt;
        SELECT sc.sid,s.name sname,s.age sage,sc.cid,c.name cname FROM student s,course c,stu_cr sc WHERE sc.sid=s.id AND sc.cid=c.id
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;鉴别器&lt;/h3&gt;
&lt;p&gt;需求：如果查询结果是女性，则把部门信息查询出来，否则不查询 ；如果是男性，把 last_name 这一列的值赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 
    column：指定要判断的列名 
    javaType：列值对应的java类型
   --&amp;gt;
&amp;lt;discriminator javaType=&quot;string&quot; column=&quot;gender&quot;&amp;gt;
    &amp;lt;!-- 女生 --&amp;gt;
    &amp;lt;!-- resultType不可缺少，也可以使用resutlMap --&amp;gt;
    &amp;lt;case value=&quot;0&quot; resultType=&quot;com.bean.Employee&quot;&amp;gt;
        &amp;lt;association property=&quot;dept&quot;
                     select=&quot;com.dao.DepartmentMapper.getDeptById&quot;
                     column=&quot;d_id&quot;&amp;gt;
        &amp;lt;/association&amp;gt;
    &amp;lt;/case&amp;gt;
    &amp;lt;!-- 男生 --&amp;gt;
    &amp;lt;case value=&quot;1&quot; resultType=&quot;com.bean.Employee&quot;&amp;gt;
        &amp;lt;id column=&quot;id&quot; property=&quot;id&quot;/&amp;gt;
        &amp;lt;result column=&quot;last_name&quot; property=&quot;lastName&quot;/&amp;gt;
        &amp;lt;result column=&quot;gender&quot; property=&quot;gender&quot;/&amp;gt;
    &amp;lt;/case&amp;gt;
&amp;lt;/discriminator&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;延迟加载&lt;/h3&gt;
&lt;h4&gt;两种加载&lt;/h4&gt;
&lt;p&gt;立即加载：只要调用方法，马上发起查询&lt;/p&gt;
&lt;p&gt;延迟加载：在需要用到数据时才进行加载，不需要用到数据时就不加载数据，延迟加载也称懒加载&lt;/p&gt;
&lt;p&gt;优点： 先从单表查询，需要时再从关联表去关联查询，提高数据库性能，因为查询单表要比关联查询多张表速度要快，节省资源&lt;/p&gt;
&lt;p&gt;坏处：只有当需要用到数据时，才会进行数据库查询，这样在大批量数据查询时，查询工作也要消耗时间，所以可能造成用户等待时间变长，造成用户体验下降&lt;/p&gt;
&lt;p&gt;核心配置文件：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lazyLoadingEnabled&lt;/td&gt;
&lt;td&gt;延迟加载的全局开关。当开启时，所有关联对象都会延迟加载，特定关联关系中可通过设置 &lt;code&gt;fetchType&lt;/code&gt; 属性来覆盖该项的开关状态。&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggressiveLazyLoading&lt;/td&gt;
&lt;td&gt;开启时，任一方法的调用都会加载该对象的所有延迟加载属性。否则每个延迟加载属性会按需加载（参考 lazyLoadTriggerMethods）&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;settings&amp;gt; 
	&amp;lt;setting name=&quot;lazyLoadingEnabled&quot; value=&quot;true&quot;/&amp;gt; 
    &amp;lt;setting name=&quot;aggressiveLazyLoading&quot; value=&quot;false&quot;/&amp;gt; 
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;assocation&lt;/h4&gt;
&lt;p&gt;分布查询：先按照身份 id 查询所属人的 id、然后根据所属人的 id 去查询人的全部信息，这就是分步查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;映射配置文件 OneToOneMapper.xml&lt;/p&gt;
&lt;p&gt;一对一映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;column 属性表示给要调用的其它的 select 标签传入的参数&lt;/li&gt;
&lt;li&gt;select 属性表示调用其它的 select 标签&lt;/li&gt;
&lt;li&gt;fetchType=&quot;lazy&quot; 表示延迟加载（局部配置，只有配置了这个的地方才会延迟加载）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;OneToOneMapper&quot;&amp;gt;
    &amp;lt;!--配置字段和实体对象属性的映射关系--&amp;gt;
    &amp;lt;resultMap id=&quot;oneToOne&quot; type=&quot;card&quot;&amp;gt;
        &amp;lt;id column=&quot;id&quot; property=&quot;id&quot; /&amp;gt;
        &amp;lt;result column=&quot;number&quot; property=&quot;number&quot; /&amp;gt;
        &amp;lt;association property=&quot;p&quot; javaType=&quot;bean.Person&quot;
                     column=&quot;pid&quot; 
                     select=&quot;one_to_one.PersonMapper.findPersonByid&quot;
                     fetchType=&quot;lazy&quot;&amp;gt;
            		&amp;lt;!--需要配置新的映射文件--&amp;gt;
        &amp;lt;/association&amp;gt;
    &amp;lt;/resultMap&amp;gt;

    &amp;lt;select id=&quot;selectAll&quot; resultMap=&quot;oneToOne&quot;&amp;gt; 
        SELECT * FROM card &amp;lt;!--查询全部，负责根据条件直接全部加载--&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PersonMapper.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;one_to_one.PersonMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;findPersonByid&quot; parameterType=&quot;int&quot; resultType=&quot;person&quot;&amp;gt;
        SELECT * FROM person WHERE id=#{pid}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PersonMapper.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface PersonMapper {
    User findPersonByid(int id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test01 {
    @Test
    public void selectAll() throws Exception{
        InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);
        SqlSession sqlSession = ssf.openSession(true);
        OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class);
        // 调用实现类的方法，接收结果
        List&amp;lt;Card&amp;gt; list = mapper.selectAll();
        
      	// 不能遍历，遍历就是相当于使用了该数据，需要加载，不遍历就是没有使用。
        
        // 释放资源
        sqlSession.close();
        is.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;collection&lt;/h4&gt;
&lt;p&gt;同样在一对多关系配置的 &amp;lt;collection&amp;gt; 结点中配置延迟加载策略，&amp;lt;collection&amp;gt; 结点中也有 select 属性和 column 属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;映射配置文件 OneToManyMapper.xml&lt;/p&gt;
&lt;p&gt;一对多映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;column 是用于指定使用哪个字段的值作为条件查询&lt;/li&gt;
&lt;li&gt;select 是用于指定查询账户的唯一标识（账户的 dao 全限定类名加上方法名称）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;OneToManyMapper&quot;&amp;gt;
    &amp;lt;resultMap id=&quot;oneToMany&quot; type=&quot;bean.Classes&quot;&amp;gt;
        &amp;lt;id column=&quot;id&quot; property=&quot;id&quot;/&amp;gt;
        &amp;lt;result column=&quot;name&quot; property=&quot;name&quot;/&amp;gt;

        &amp;lt;!--collection：配置被包含的集合对象映射关系--&amp;gt;
        &amp;lt;collection property=&quot;students&quot; ofType=&quot;bean.Student&quot;
                    column=&quot;id&quot; 
                    select=&quot;one_to_one.StudentMapper.findStudentByCid&quot;&amp;gt;
        &amp;lt;/collection&amp;gt;
    &amp;lt;/resultMap&amp;gt;
    &amp;lt;select id=&quot;selectAll&quot; resultMap=&quot;oneToMany&quot;&amp;gt;
      SELECT * FROM classes
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StudentMapper.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;one_to_one.StudentMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;findPersonByCid&quot; parameterType=&quot;int&quot; resultType=&quot;student&quot;&amp;gt;
        SELECT * FROM person WHERE cid=#{id}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;注解开发&lt;/h2&gt;
&lt;h3&gt;单表操作&lt;/h3&gt;
&lt;p&gt;注解可以简化开发操作，省略映射配置文件的编写&lt;/p&gt;
&lt;p&gt;常用注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Select(“查询的 SQL 语句”)：执行查询操作注解&lt;/li&gt;
&lt;li&gt;@Insert(“插入的 SQL 语句”)：执行新增操作注解&lt;/li&gt;
&lt;li&gt;@Update(“修改的 SQL 语句”)：执行修改操作注解&lt;/li&gt;
&lt;li&gt;@Delete(“删除的 SQL 语句”)：执行删除操作注解&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参数注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Param：当 SQL 语句需要&lt;strong&gt;多个（大于1）参数&lt;/strong&gt;时，用来指定参数的对应规则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心配置文件配置映射关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mappers&amp;gt;
	&amp;lt;package name=&quot;使用了注解的Mapper接口所在包&quot;/&amp;gt;
&amp;lt;/mappers&amp;gt;
&amp;lt;!--或者--&amp;gt;
&amp;lt;mappers&amp;gt;
 	&amp;lt;mapper class=&quot;包名.Mapper名&quot;&amp;gt;&amp;lt;/mapper&amp;gt;
&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本增删改查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建 Mapper 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package mapper;
public interface StudentMapper {
    //查询全部
    @Select(&quot;SELECT * FROM student&quot;)
    public abstract List&amp;lt;Student&amp;gt; selectAll();

    //新增数据
    @Insert(&quot;INSERT INTO student VALUES (#{id},#{name},#{age})&quot;)
    public abstract Integer insert(Student student);

    //修改操作
    @Update(&quot;UPDATE student SET name=#{name},age=#{age} WHERE id=#{id}&quot;)
    public abstract Integer update(Student student);

    //删除操作
    @Delete(&quot;DELETE FROM student WHERE id=#{id}&quot;)
    public abstract Integer delete(Integer id);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 MyBatis 的核心配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mappers&amp;gt;
	&amp;lt;package name=&quot;mapper&quot;/&amp;gt;
&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bean类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Student {
    private Integer id;
    private String name;
    private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void selectAll() throws Exception{
    //1.加载核心配置文件
    InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

    //2.获取SqlSession工厂对象
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);

    //3.通过工厂对象获取SqlSession对象
    SqlSession sqlSession = ssf.openSession(true);

    //4.获取StudentMapper接口的实现类对象
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

    //5.调用实现类对象中的方法，接收结果
    List&amp;lt;Student&amp;gt; list = mapper.selectAll();

    //6.处理结果
    for (Student student : list) {
        System.out.println(student);
    }
    
    //7.释放资源
    sqlSession.close();
    is.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;多表操作&lt;/h3&gt;
&lt;h4&gt;相关注解&lt;/h4&gt;
&lt;p&gt;实现复杂关系映射之前我们可以在映射文件中通过配置 &amp;lt;resultMap&amp;gt; 来实现，使用注解开发后，可以使用 @Results 注解，@Result 注解，@One 注解，@Many 注解组合完成复杂关系的配置&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@Results&lt;/td&gt;
&lt;td&gt;代替 &amp;lt;resultMap&amp;gt; 标签，注解中使用单个 @Result 注解或者 @Result 集合&amp;lt;br/&amp;gt;使用格式：@Results({ @Result(), @Result() })或@Results({ @Result() })&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Result&lt;/td&gt;
&lt;td&gt;代替&amp;lt; id&amp;gt; 和 &amp;lt;result&amp;gt; 标签，@Result 中属性介绍：&amp;lt;br /&amp;gt;column：数据库的列名      property：封装类的变量名&amp;lt;br /&amp;gt;one：需要使用 @One 注解（@Result(one = @One)）&amp;lt;br /&amp;gt;Many：需要使用 @Many 注解（@Result(many= @Many)）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@One(一对一)&lt;/td&gt;
&lt;td&gt;代替 &amp;lt;association&amp;gt; 标签，多表查询的关键，用来指定子查询返回单一对象&amp;lt;br/&amp;gt;select：指定调用 Mapper 接口中的某个方法&amp;lt;br /&amp;gt;使用格式：@Result(column=&quot;&quot;, property=&quot;&quot;, one=@One(select=&quot;&quot;))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Many(多对一)&lt;/td&gt;
&lt;td&gt;代替 &amp;lt;collection&amp;gt; 标签，多表查询的关键，用来指定子查询返回对象集合&amp;lt;br /&amp;gt;select：指定调用 Mapper 接口中的某个方法&amp;lt;br /&amp;gt;使用格式：@Result(column=&quot;&quot;, property=&quot;&quot;, many=@Many(select=&quot;&quot;))&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;一对一&lt;/h4&gt;
&lt;p&gt;身份证对人&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PersonMapper 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface PersonMapper {
    //根据id查询
    @Select(&quot;SELECT * FROM person WHERE id=#{id}&quot;)
    public abstract Person selectById(Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CardMapper接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface CardMapper {
    //查询全部
    @Select(&quot;SELECT * FROM card&quot;)
    @Results({
            @Result(column = &quot;id&quot;,property = &quot;id&quot;),
            @Result(column = &quot;number&quot;,property = &quot;number&quot;),
            @Result(
                    property = &quot;p&quot;,             // 被包含对象的变量名
                    javaType = Person.class,    // 被包含对象的实际数据类型
                    column = &quot;pid&quot;,  // 根据查询出的card表中的pid字段来查询person表
                     /* 
                     	one、@One 一对一固定写法
                        select属性：指定调用哪个接口中的哪个方法
                     */
                    one = @One(select = &quot;one_to_one.PersonMapper.selectById&quot;)
            )
    })
    public abstract List&amp;lt;Card&amp;gt; selectAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类（详细代码参考单表操作）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1.加载核心配置文件
//2.获取SqlSession工厂对象
//3.通过工厂对象获取SqlSession对象

//4.获取StudentMapper接口的实现类对象
CardMapper mapper = sqlSession.getMapper(CardMapper.class);
//5.调用实现类对象中的方法，接收结果
List&amp;lt;Card&amp;gt; list = mapper.selectAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;一对多&lt;/h4&gt;
&lt;p&gt;班级和学生&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;StudentMapper接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface StudentMapper {
    //根据cid查询student表  cid是外键约束列
    @Select(&quot;SELECT * FROM student WHERE cid=#{cid}&quot;)
    public abstract List&amp;lt;Student&amp;gt; selectByCid(Integer cid);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ClassesMapper接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface ClassesMapper {
    //查询全部
    @Select(&quot;SELECT * FROM classes&quot;)
    @Results({
            @Result(column = &quot;id&quot;, property = &quot;id&quot;),
            @Result(column = &quot;name&quot;, property = &quot;name&quot;),
            @Result(
                    property = &quot;students&quot;,  //被包含对象的变量名
                    javaType = List.class,  //被包含对象的实际数据类型
                    column = &quot;id&quot;,          //根据id字段查询student表
                    many = @Many(select = &quot;one_to_many.StudentMapper.selectByCid&quot;)
            )
    })
    public abstract List&amp;lt;Classes&amp;gt; selectAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//4.获取StudentMapper接口的实现类对象
ClassesMapper mapper = sqlSession.getMapper(ClassesMapper.class);
//5.调用实现类对象中的方法，接收结果
List&amp;lt;Classes&amp;gt; classes = mapper.selectAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;多对多&lt;/h4&gt;
&lt;p&gt;学生和课程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL 查询语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id
SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CourseMapper 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface CourseMapper {
    //根据学生id查询所选课程
    @Select(&quot;SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id}&quot;)
    public abstract List&amp;lt;Course&amp;gt; selectBySid(Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;StudentMapper 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface StudentMapper {
    //查询全部
    @Select(&quot;SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id&quot;)
    @Results({
            @Result(column = &quot;id&quot;,property = &quot;id&quot;),
            @Result(column = &quot;name&quot;,property = &quot;name&quot;),
            @Result(column = &quot;age&quot;,property = &quot;age&quot;),
            @Result(
                    property = &quot;courses&quot;,    //被包含对象的变量名
                    javaType = List.class,  //被包含对象的实际数据类型
                    column = &quot;id&quot;, //根据查询出的student表中的id字段查询中间表和课程表
                    many = @Many(select = &quot;many_to_many.CourseMapper.selectBySid&quot;)
            )
    })
    public abstract List&amp;lt;Student&amp;gt; selectAll();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//4.获取StudentMapper接口的实现类对象
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
//5.调用实现类对象中的方法，接收结果
List&amp;lt;Student&amp;gt; students = mapper.selectAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;缓存机制&lt;/h2&gt;
&lt;h3&gt;缓存概述&lt;/h3&gt;
&lt;p&gt;缓存：缓存就是一块内存空间，保存临时数据&lt;/p&gt;
&lt;p&gt;作用：将数据源（数据库或者文件）中的数据读取出来存放到缓存中，再次获取时直接从缓存中获取，可以减少和数据库交互的次数，提升程序的性能&lt;/p&gt;
&lt;p&gt;缓存适用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适用于缓存的：经常查询但不经常修改的，数据的正确与否对最终结果影响不大的&lt;/li&gt;
&lt;li&gt;不适用缓存的：经常改变的数据 , 敏感数据（例如：股市的牌价，银行的汇率，银行卡里面的钱）等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缓存类别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一级缓存：SqlSession 级别的缓存，又叫本地会话缓存，自带的（不需要配置），一级缓存的生命周期与 SqlSession 一致。在操作数据库时需要构造 SqlSession 对象，&lt;strong&gt;在对象中有一个数据结构（HashMap）用于存储缓存数据&lt;/strong&gt;，不同的 SqlSession 之间的缓存数据区域是互相不影响的&lt;/li&gt;
&lt;li&gt;二级缓存：mapper（namespace）级别的缓存，二级缓存的使用，需要手动开启（需要配置）。多个 SqlSession 去操作同一个 Mapper 的 SQL 可以共用二级缓存，二级缓存是跨 SqlSession 的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开启缓存：配置核心配置文件中 &amp;lt;settings&amp;gt; 标签&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cacheEnabled：true 表示全局性地开启所有映射器配置文件中已配置的任何缓存，默认 true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/ysocean/p/7342498.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;一级缓存&lt;/h3&gt;
&lt;p&gt;一级缓存是 SqlSession 级别的缓存&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-一级缓存.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;工作流程：第一次发起查询用户 id 为 1 的用户信息，先去找缓存中是否有 id 为 1 的用户信息，如果没有，从数据库查询用户信息，得到用户信息，将用户信息存储到一级缓存中；第二次发起查询用户 id 为 1 的用户信息，先去找缓存中是否有 id 为 1 的用户信息，缓存中有，直接从缓存中获取用户信息。&lt;/p&gt;
&lt;p&gt;一级缓存的失效：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SqlSession 不同&lt;/li&gt;
&lt;li&gt;SqlSession 相同，查询条件不同时（还未缓存该数据）&lt;/li&gt;
&lt;li&gt;SqlSession 相同，手动清除了一级缓存，调用 &lt;code&gt;sqlSession.clearCache()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SqlSession 相同，执行 commit 操作或者执行插入、更新、删除，清空 SqlSession 中的一级缓存，这样做的目的为了让缓存中存储的是最新的信息，&lt;strong&gt;避免脏读&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring 整合 MyBatis 后，一级缓存作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未开启事务的情况，每次查询 Spring 都会创建新的 SqlSession，因此一级缓存失效&lt;/li&gt;
&lt;li&gt;开启事务的情况，Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession，因此此时一级缓存是有效的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试一级缓存存在&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void testFirstLevelCache(){
    //1. 获取sqlSession对象
    SqlSession sqlSession = SqlSessionFactoryUtils.openSession();
    //2. 通过sqlSession对象获取UserDao接口的代理对象
    UserDao userDao1 = sqlSession.getMapper(UserDao.class);
    //3. 调用UserDao接口的代理对象的findById方法获取信息
	User user1 = userDao1.findById(1);
	System.out.println(user1);
    
    //sqlSession.clearCache() 清空缓存
    
   	UserDao userDao2 = sqlSession.getMapper(UserDao.class);
    User user = userDao.findById(1);
    System.out.println(user2);
    
    //4.测试两次结果是否一样
    System.out.println(user1 == user2);//true
    
    //5. 提交事务关闭资源
    SqlSessionFactoryUtils.commitAndClose(sqlSession);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;二级缓存&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;二级缓存是 mapper 的缓存，只要是同一个命名空间（namespace）的 SqlSession 就共享二级缓存的内容，并且可以操作二级缓存&lt;/p&gt;
&lt;p&gt;作用：作用范围是整个应用，可以跨线程使用，适合缓存一些修改较少的数据&lt;/p&gt;
&lt;p&gt;工作流程：一个会话查询数据，这个数据就会被放在当前会话的一级缓存中，如果&lt;strong&gt;会话关闭或提交&lt;/strong&gt;一级缓存中的数据会保存到二级缓存&lt;/p&gt;
&lt;p&gt;二级缓存的基本使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 MyBatisConfig.xml 文件开启二级缓存，&lt;strong&gt;cacheEnabled 默认值为 true&lt;/strong&gt;，所以这一步可以省略不配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置开启二级缓存--&amp;gt;
&amp;lt;settings&amp;gt;
    &amp;lt;setting name=&quot;cacheEnabled&quot; value=&quot;true&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 Mapper 映射文件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;cache&amp;gt;&lt;/code&gt; 标签表示当前这个 mapper 映射将使用二级缓存，区分的标准就看 mapper 的 namespace 值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;dao.UserDao&quot;&amp;gt;
    &amp;lt;!--开启user支持二级缓存--&amp;gt;
   	&amp;lt;cache eviction=&quot;FIFO&quot; flushInterval=&quot;6000&quot; readOnly=&quot;&quot; size=&quot;1024&quot;/&amp;gt;
	&amp;lt;cache&amp;gt;&amp;lt;/cache&amp;gt; &amp;lt;!--则表示所有属性使用默认值--&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eviction（清除策略）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LRU&lt;/code&gt; – 最近最少使用：移除最长时间不被使用的对象，默认&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIFO&lt;/code&gt; – 先进先出：按对象进入缓存的顺序来移除它们&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SOFT&lt;/code&gt; – 软引用：基于垃圾回收器状态和软引用规则移除对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEAK&lt;/code&gt; – 弱引用：更积极地基于垃圾收集器状态和弱引用规则移除对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;flushInterval（刷新间隔）：可以设置为任意的正整数， 默认情况是不设置，也就是没有刷新间隔，缓存仅仅会在调用语句时刷新&lt;/p&gt;
&lt;p&gt;size（引用数目）：缓存存放多少元素，默认值是 1024&lt;/p&gt;
&lt;p&gt;readOnly（只读）：可以被设置为 true 或 false&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只读的缓存会给所有调用者返回缓存对象的相同实例，因此这些对象不能被修改，促进了性能提升&lt;/li&gt;
&lt;li&gt;可读写的缓存会（通过序列化）返回缓存对象的拷贝， 速度上会慢一些，但是更安全，因此默认值是 false&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;type：指定自定义缓存的全类名，实现 Cache 接口即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要进行二级缓存的类必须实现 java.io.Serializable 接口，可以使用序列化方式来保存对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User implements Serializable{}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;相关属性&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;select 标签的 useCache 属性&lt;/p&gt;
&lt;p&gt;映射文件中的 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 标签中设置 &lt;code&gt;useCache=&quot;true&quot;&lt;/code&gt; 代表当前 statement 要使用二级缓存（默认）&lt;/p&gt;
&lt;p&gt;注意：如果每次查询都需要最新的数据 sql，要设置成 useCache=false，禁用二级缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;findAll&quot; resultType=&quot;user&quot; useCache=&quot;true&quot;&amp;gt;
    select * from user
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个增删改标签都有 flushCache 属性，默认为 true，代表在&lt;strong&gt;执行增删改之后就会清除一、二级缓存&lt;/strong&gt;，保证缓存的一致性；而查询标签默认值为 false，所以查询不会清空缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;localCacheScope：本地缓存作用域，&amp;lt;settings&amp;gt; 中的配置项，默认值为 SESSION，当前会话的所有数据保存在会话缓存中，设置为 STATEMENT 禁用一级缓存&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;源码解析&lt;/h4&gt;
&lt;p&gt;事务提交二级缓存才生效：DefaultSqlSession 调用 commit() 时会回调 &lt;code&gt;executor.commit()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CachingExecutor#query()：执行查询方法，查询出的数据会先放入 entriesToAddOnCommit 集合暂存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 从二缓存中获取数据，获取不到去一级缓存获取
List&amp;lt;E&amp;gt; list = (List&amp;lt;E&amp;gt;) tcm.getObject(cache, key);
if (list == null) {
    // 回调 BaseExecutor#query
    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    // 将数据放入 entriesToAddOnCommit 集合暂存，此时还没放入二级缓存
    tcm.putObject(cache, key, list);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;commit()：事务提交，&lt;strong&gt;清空一级缓存，放入二级缓存&lt;/strong&gt;，二级缓存使用 TransactionalCacheManager（tcm）管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void commit(boolean required) throws SQLException {
    // 首先调用 BaseExecutor#commit 方法，【清空一级缓存】
    delegate.commit(required);
    tcm.commit();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TransactionalCacheManager#commit：查询出的数据放入二级缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void commit() {
    // 获取所有的缓存事务，挨着进行提交
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    // 将 entriesToAddOnCommit 中的数据放入二级缓存
    flushPendingEntries();
    // 清空相关集合
    reset();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void flushPendingEntries() {
    for (Map.Entry&amp;lt;Object, Object&amp;gt; entry : entriesToAddOnCommit.entrySet()) {
        // 将数据放入二级缓存
        delegate.putObject(entry.getKey(), entry.getValue());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;增删改操作会清空缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;update()：CachingExecutor 的更新操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    // 回调 BaseExecutor#update 方法，也会清空一级缓存
    return delegate.update(ms, parameterObject);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flushCacheIfRequired()：判断是否需要清空二级缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // 判断二级缓存是否存在，然后判断标签的 flushCache 的值，增删改操作的 flushCache 属性默认为 true
    if (cache != null &amp;amp;&amp;amp; ms.isFlushCacheRequired()) {
        // 清空二级缓存
        tcm.clear(cache);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;自定义&lt;/h3&gt;
&lt;p&gt;自定义缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;cache type=&quot;com.domain.something.MyCustomCache&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口，且提供一个接受 String 参数作为 id 的构造器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缓存的配置，只需要在缓存实现中添加公有的 JavaBean 属性，然后通过 cache 元素传递属性值，例如在缓存实现上调用一个名为 &lt;code&gt;setCacheFile(String file)&lt;/code&gt; 的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;cache type=&quot;com.domain.something.MyCustomCache&quot;&amp;gt;
  &amp;lt;property name=&quot;cacheFile&quot; value=&quot;/tmp/my-custom-cache.tmp&quot;/&amp;gt;
&amp;lt;/cache&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可以使用所有简单类型作为 JavaBean 属性的类型，MyBatis 会进行转换。&lt;/li&gt;
&lt;li&gt;可以使用占位符（如 &lt;code&gt;${cache.file}&lt;/code&gt;），以便替换成在配置文件属性中定义的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MyBatis 支持在所有属性设置完毕之后，调用一个初始化方法， 如果想要使用这个特性，可以在自定义缓存类里实现 &lt;code&gt;org.apache.ibatis.builder.InitializingObject&lt;/code&gt; 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface InitializingObject {
  void initialize() throws Exception;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：对缓存的配置（如清除策略、可读或可读写等），不能应用于自定义缓存&lt;/p&gt;
&lt;p&gt;对某一命名空间的语句，只会使用该命名空间的缓存进行缓存或刷新，在多个命名空间中共享相同的缓存配置和实例，可以使用 cache-ref 元素来引用另一个缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;cache-ref namespace=&quot;com.someone.application.data.SomeMapper&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;构造语句&lt;/h2&gt;
&lt;h3&gt;动态 SQL&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;动态 SQL 是 MyBatis 强大特性之一，逻辑复杂时，MyBatis 映射配置文件中，SQL 是动态变化的，所以引入动态 SQL 简化拼装 SQL 的操作&lt;/p&gt;
&lt;p&gt;DynamicSQL 包含的标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if&lt;/li&gt;
&lt;li&gt;where&lt;/li&gt;
&lt;li&gt;set&lt;/li&gt;
&lt;li&gt;choose (when、otherwise)&lt;/li&gt;
&lt;li&gt;trim&lt;/li&gt;
&lt;li&gt;foreach&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各个标签都可以进行灵活嵌套和组合&lt;/p&gt;
&lt;p&gt;OGNL：Object Graphic Navigation Language（对象图导航语言），用于对数据进行访问&lt;/p&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/ysocean/p/7289529.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;where&lt;/h4&gt;
&lt;p&gt;&amp;lt;where&amp;gt;：条件标签，有动态条件则使用该标签代替 WHERE 关键字，封装查询条件&lt;/p&gt;
&lt;p&gt;作用：如果标签返回的内容是以 AND 或 OR 开头的，标签内会剔除掉&lt;/p&gt;
&lt;p&gt;表结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E5%8A%A8%E6%80%81sql%E7%94%A8%E6%88%B7%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;if&lt;/h4&gt;
&lt;p&gt;基本格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;if test=“条件判断”&amp;gt;
	查询条件拼接
&amp;lt;/if&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们根据实体类的不同取值，使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询，如果username 不同空时还要加入用户名作为条件，这种情况在我们的多条件组合查询中经常会碰到。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UserMapper.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;mapper.UserMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;selectCondition&quot; resultType=&quot;user&quot; parameterType=&quot;user&quot;&amp;gt;
        SELECT * FROM user
        &amp;lt;where&amp;gt;
            &amp;lt;if test=&quot;id != null &quot;&amp;gt;
                id = #{id}
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;username != null &quot;&amp;gt;
                AND username = #{username}
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;sex != null &quot;&amp;gt;
                AND sex = #{sex}
            &amp;lt;/if&amp;gt;
        &amp;lt;/where&amp;gt;
    &amp;lt;/select&amp;gt;	

&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatisConfig.xml，引入映射配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mappers&amp;gt;
    &amp;lt;!--mapper引入指定的映射配置 resource属性执行的映射配置文件的名称--&amp;gt;
    &amp;lt;mapper resource=&quot;UserMapper.xml&quot;/&amp;gt;
&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DAO 层 Mapper 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserMapper {
    //多条件查询
    public abstract List&amp;lt;User&amp;gt; selectCondition(Student stu);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DynamicTest {
    @Test
    public void selectCondition() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

        //2.获取SqlSession工厂对象
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = ssf.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = new User();
        user.setId(2);
        user.setUsername(&quot;李四&quot;);
        //user.setSex(男); AND 后会自动剔除

        //5.调用实现类的方法，接收结果
        List&amp;lt;Student&amp;gt; list = mapper.selectCondition(user);

        //6.处理结果
        for (User user : list) {
            System.out.println(user);
        }
        
        //7.释放资源
        sqlSession.close();
        is.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;set&lt;/h4&gt;
&lt;p&gt;&amp;lt;set&amp;gt;：进行更新操作的时候，含有 set 关键词，使用该标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 根据 id 更新 user 表的数据 --&amp;gt;
&amp;lt;update id=&quot;updateUserById&quot; parameterType=&quot;com.ys.po.User&quot;&amp;gt;
    UPDATE user u
        &amp;lt;set&amp;gt;
            &amp;lt;if test=&quot;username != null and username != &apos;&apos;&quot;&amp;gt;
                u.username = #{username},
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;
                u.sex = #{sex}
            &amp;lt;/if&amp;gt;
        &amp;lt;/set&amp;gt;
     WHERE id=#{id}
&amp;lt;/update&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果第一个条件 username 为空，那么 sql 语句为：update user u set u.sex=? where id=?&lt;/li&gt;
&lt;li&gt;如果第一个条件不为空，那么 sql 语句为：update user u set u.username = ? ,u.sex = ? where id=?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;choose&lt;/h4&gt;
&lt;p&gt;假如不想用到所有的查询条件，只要查询条件有一个满足即可，使用 choose 标签可以解决此类问题，类似于 Java 的 switch 语句&lt;/p&gt;
&lt;p&gt;标签：&amp;lt;when&amp;gt;，&amp;lt;otherwise&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;selectUserByChoose&quot; resultType=&quot;user&quot; parameterType=&quot;user&quot;&amp;gt;
    SELECT * FROM user
    &amp;lt;where&amp;gt;
        &amp;lt;choose&amp;gt;
            &amp;lt;when test=&quot;id !=&apos;&apos; and id != null&quot;&amp;gt;
                id=#{id}
            &amp;lt;/when&amp;gt;
            &amp;lt;when test=&quot;username !=&apos;&apos; and username != null&quot;&amp;gt;
                AND username=#{username}
            &amp;lt;/when&amp;gt;
            &amp;lt;otherwise&amp;gt;
                AND sex=#{sex}
            &amp;lt;/otherwise&amp;gt;
        &amp;lt;/choose&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有三个条件，id、username、sex，只能选择一个作为查询条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 id 不为空，那么查询语句为：select * from user where  id=?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 id 为空，那么看 username 是否为空&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不为空，那么语句为：select * from user where username=?&lt;/li&gt;
&lt;li&gt;如果 username 为空，那么查询语句为 select * from user where sex=?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;trim&lt;/h4&gt;
&lt;p&gt;trim 标记是一个格式化的标记，可以完成 set 或者是 where 标记的功能，自定义字符串截取&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prefix：给拼串后的整个字符串加一个前缀，trim 标签体中是整个字符串拼串后的结果&lt;/li&gt;
&lt;li&gt;prefixOverrides：去掉整个字符串前面多余的字符&lt;/li&gt;
&lt;li&gt;suffix：给拼串后的整个字符串加一个后缀&lt;/li&gt;
&lt;li&gt;suffixOverrides：去掉整个字符串后面多余的字符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改写 if + where 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;selectUserByUsernameAndSex&quot; resultType=&quot;user&quot; parameterType=&quot;com.ys.po.User&quot;&amp;gt;
    SELECT * FROM user
    &amp;lt;trim prefix=&quot;where&quot; prefixOverrides=&quot;and | or&quot;&amp;gt;
        &amp;lt;if test=&quot;username != null&quot;&amp;gt;
            AND username=#{username}
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;sex != null&quot;&amp;gt;
            AND sex=#{sex}
        &amp;lt;/if&amp;gt;
    &amp;lt;/trim&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改写 if + set 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 根据 id 更新 user 表的数据 --&amp;gt;
&amp;lt;update id=&quot;updateUserById&quot; parameterType=&quot;com.ys.po.User&quot;&amp;gt;
    UPDATE user u
    &amp;lt;trim prefix=&quot;set&quot; suffixOverrides=&quot;,&quot;&amp;gt;
        &amp;lt;if test=&quot;username != null and username != &apos;&apos;&quot;&amp;gt;
            u.username = #{username},
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;
            u.sex = #{sex},
        &amp;lt;/if&amp;gt;
    &amp;lt;/trim&amp;gt;
    WHERE id=#{id}
&amp;lt;/update&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;foreach&lt;/h4&gt;
&lt;p&gt;基本格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;foreach&amp;gt;：循环遍历标签。适用于多个参数或者的关系。
    &amp;lt;foreach collection=“”open=“”close=“”item=“”separator=“”&amp;gt;
		获取参数
&amp;lt;/foreach&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;collection：参数容器类型， (list-集合， array-数组)&lt;/li&gt;
&lt;li&gt;open：开始的 SQL 语句&lt;/li&gt;
&lt;li&gt;close：结束的 SQL 语句&lt;/li&gt;
&lt;li&gt;item：参数变量名&lt;/li&gt;
&lt;li&gt;separator：分隔符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需求：循环执行 sql 的拼接操作，&lt;code&gt;SELECT * FROM user WHERE id IN (1,2,5)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UserMapper.xml片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;selectByIds&quot; resultType=&quot;user&quot; parameterType=&quot;list&quot;&amp;gt;
    SELECT * FROM student
    &amp;lt;where&amp;gt;
        &amp;lt;foreach collection=&quot;list&quot; open=&quot;id IN(&quot; close=&quot;)&quot; item=&quot;id&quot; separator=&quot;,&quot;&amp;gt;
            #{id}
        &amp;lt;/foreach&amp;gt;
    &amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试代码片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//4.获取StudentMapper接口的实现类对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

List&amp;lt;Integer&amp;gt; ids = new ArrayList&amp;lt;&amp;gt;();
Collections.addAll(list, 1, 2);
//5.调用实现类的方法，接收结果
List&amp;lt;User&amp;gt; list = mapper.selectByIds(ids);

for (User user : list) {
    System.out.println(user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;SQL片段&lt;/h4&gt;
&lt;p&gt;将一些重复性的 SQL 语句进行抽取，以达到复用的效果&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;sql id=“片段唯一标识”&amp;gt;抽取的SQL语句&amp;lt;/sql&amp;gt;		&amp;lt;!--抽取标签--&amp;gt;
&amp;lt;include refid=“片段唯一标识”/&amp;gt;				&amp;lt;!--引入标签--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;sql id=&quot;select&quot;&amp;gt;SELECT * FROM user&amp;lt;/sql&amp;gt;

&amp;lt;select id=&quot;selectByIds&quot; resultType=&quot;user&quot; parameterType=&quot;list&quot;&amp;gt;
    &amp;lt;include refid=&quot;select&quot;/&amp;gt;
    &amp;lt;where&amp;gt;
        &amp;lt;foreach collection=&quot;list&quot; open=&quot;id IN(&quot; close=&quot;)&quot; item=&quot;id&quot; separator=&quot;,&quot;&amp;gt;
            #{id}
        &amp;lt;/foreach&amp;gt;
    &amp;lt;/where&amp;gt;
 &amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;逆向工程&lt;/h3&gt;
&lt;p&gt;MyBatis 逆向工程，可以针对&lt;strong&gt;单表&lt;/strong&gt;自动生成 MyBatis 执行所需要的代码（mapper.java、mapper.xml、pojo…）&lt;/p&gt;
&lt;p&gt;generatorConfig.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE generatorConfiguration
  PUBLIC &quot;-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN&quot;
  &quot;http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd&quot;&amp;gt;
 
&amp;lt;generatorConfiguration&amp;gt;
    &amp;lt;context id=&quot;testTables&quot; targetRuntime=&quot;MyBatis3&quot;&amp;gt;
        &amp;lt;commentGenerator&amp;gt;
            &amp;lt;!-- 是否去除自动生成的注释 true：是 ： false:否 --&amp;gt;
            &amp;lt;property name=&quot;suppressAllComments&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;/commentGenerator&amp;gt;
        &amp;lt;!--数据库连接的信息：驱动类、连接地址、用户名、密码 --&amp;gt;
        &amp;lt;jdbcConnection driverClass=&quot;com.mysql.jdbc.Driver&quot;
            connectionURL=&quot;jdbc:mysql://localhost:3306/mybatisrelation&quot; userId=&quot;root&quot;
            password=&quot;root&quot;&amp;gt;
        &amp;lt;/jdbcConnection&amp;gt;
 
        &amp;lt;!-- 默认false，把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer，为 true时把JDBC DECIMAL和NUMERIC类型解析为java.math.BigDecimal --&amp;gt;
        &amp;lt;javaTypeResolver&amp;gt;
            &amp;lt;property name=&quot;forceBigDecimals&quot; value=&quot;false&quot; /&amp;gt;
        &amp;lt;/javaTypeResolver&amp;gt;
 
        &amp;lt;!-- targetProject:生成PO类的位置！！ --&amp;gt;
        &amp;lt;javaModelGenerator targetPackage=&quot;com.ys.po&quot;
            targetProject=&quot;.\src&quot;&amp;gt;
            &amp;lt;!-- enableSubPackages:是否让schema作为包的后缀 --&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;false&quot; /&amp;gt;
            &amp;lt;!-- 从数据库返回的值被清理前后的空格 --&amp;gt;
            &amp;lt;property name=&quot;trimStrings&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;/javaModelGenerator&amp;gt;
        &amp;lt;!-- targetProject:mapper映射文件生成的位置！！ --&amp;gt;
        &amp;lt;sqlMapGenerator targetPackage=&quot;com.ys.mapper&quot;
            targetProject=&quot;.\src&quot;&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;false&quot; /&amp;gt;
        &amp;lt;/sqlMapGenerator&amp;gt;
        &amp;lt;!-- targetPackage：mapper接口生成的位置，重要！！ --&amp;gt;
        &amp;lt;javaClientGenerator type=&quot;XMLMAPPER&quot;
            targetPackage=&quot;com.ys.mapper&quot;
            targetProject=&quot;.\src&quot;&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;false&quot; /&amp;gt;
        &amp;lt;/javaClientGenerator&amp;gt;
        &amp;lt;!-- 指定数据库表，要生成哪些表，就写哪些表，要和数据库中对应，不能写错！ --&amp;gt;
        &amp;lt;table tableName=&quot;items&quot;&amp;gt;&amp;lt;/table&amp;gt;
        &amp;lt;table tableName=&quot;orders&quot;&amp;gt;&amp;lt;/table&amp;gt;
        &amp;lt;table tableName=&quot;orderdetail&quot;&amp;gt;&amp;lt;/table&amp;gt;
        &amp;lt;table tableName=&quot;user&quot;&amp;gt;&amp;lt;/table&amp;gt;       
    &amp;lt;/context&amp;gt;
&amp;lt;/generatorConfiguration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void testGenerator() throws Exception{
    List&amp;lt;String&amp;gt; warnings = new ArrayList&amp;lt;String&amp;gt;();
    boolean overwrite = true;
    //指向逆向工程配置文件
    File configFile = new File(GeneratorTest.class.
                               getResource(&quot;/generatorConfig.xml&quot;).getFile());
    ConfigurationParser cp = new ConfigurationParser(warnings);
    Configuration config = cp.parseConfiguration(configFile);
    DefaultShellCallback callback = new DefaultShellCallback(overwrite);
    MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                                                             callback, warnings);
    myBatisGenerator.generate(null);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考文章：https://www.cnblogs.com/ysocean/p/7360409.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;构建 SQL&lt;/h3&gt;
&lt;h4&gt;基础语法&lt;/h4&gt;
&lt;p&gt;MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类，专门用于构建 SQL 语句&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SELECT(String... columns)&lt;/td&gt;
&lt;td&gt;根据字段拼接查询语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROM(String... tables)&lt;/td&gt;
&lt;td&gt;根据表名拼接语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WHERE(String... conditions)&lt;/td&gt;
&lt;td&gt;根据条件拼接语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT_INTO(String tableName)&lt;/td&gt;
&lt;td&gt;根据表名拼接新增语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INTO_VALUES(String... values)&lt;/td&gt;
&lt;td&gt;根据值拼接新增语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE(String table)&lt;/td&gt;
&lt;td&gt;根据表名拼接修改语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE_FROM(String table)&lt;/td&gt;
&lt;td&gt;根据表名拼接删除语句&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;增删改查注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@SelectProvider：生成查询用的 SQL 语句&lt;/li&gt;
&lt;li&gt;@InsertProvider：生成新增用的 SQL 语句&lt;/li&gt;
&lt;li&gt;@UpdateProvider：生成修改用的 SQL 语句注解&lt;/li&gt;
&lt;li&gt;@DeleteProvider：生成删除用的 SQL 语句注解。
&lt;ul&gt;
&lt;li&gt;type 属性：生成 SQL 语句功能类对象&lt;/li&gt;
&lt;li&gt;method 属性：指定调用方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基本操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MyBatisConfig.xml 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;!-- mappers引入映射配置文件 --&amp;gt;
&amp;lt;mappers&amp;gt;
    &amp;lt;package name=&quot;mapper&quot;/&amp;gt;
&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mapper 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface StudentMapper {
    //查询全部
    @SelectProvider(type = ReturnSql.class, method = &quot;getSelectAll&quot;)
    public abstract List&amp;lt;Student&amp;gt; selectAll();

    //新增数据
    @InsertProvider(type = ReturnSql.class, method = &quot;getInsert&quot;)
    public abstract Integer insert(Student student);

    //修改操作
    @UpdateProvider(type = ReturnSql.class, method = &quot;getUpdate&quot;)
    public abstract Integer update(Student student);

    //删除操作
    @DeleteProvider(type = ReturnSql.class, method = &quot;getDelete&quot;)
    public abstract Integer delete(Integer id);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ReturnSQL 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ReturnSql {
    //定义方法，返回查询的sql语句
    public String getSelectAll() {
        return new SQL() {
            {
                SELECT(&quot;*&quot;);
                FROM(&quot;student&quot;);
            }
        }.toString();
    }

    //定义方法，返回新增的sql语句
    public String getInsert(Student stu) {
        return new SQL() {
            {
                INSERT_INTO(&quot;student&quot;);
                INTO_VALUES(&quot;#{id},#{name},#{age}&quot;);
            }
        }.toString();
    }

    //定义方法，返回修改的sql语句
    public String getUpdate(Student stu) {
        return new SQL() {
            {
                UPDATE(&quot;student&quot;);
                SET(&quot;name=#{name}&quot;,&quot;age=#{age}&quot;);
                WHERE(&quot;id=#{id}&quot;);
            }
        }.toString();
    }

    //定义方法，返回删除的sql语句
    public String getDelete(Integer id) {
        return new SQL() {
            {
                DELETE_FROM(&quot;student&quot;);
                WHERE(&quot;id=#{id}&quot;);
            }
        }.toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;功能实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SqlTest {	
	@Test  //查询全部
    public void selectAll() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream(&quot;MyBatisConfig.xml&quot;);

        //2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法，接收结果
        List&amp;lt;Student&amp;gt; list = mapper.selectAll();

        //6.处理结果
        for (Student student : list) {
            System.out.println(student);
        }

        //7.释放资源
        sqlSession.close();
        is.close();
    }
    
    @Test  //新增
    public void insert() throws Exception{
        //1 2 3 4获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法，接收结果 -&amp;gt;6 7
        Student stu = new Student(4,&quot;赵六&quot;,26);
        Integer result = mapper.insert(stu);
    }
    
    @Test //修改
    public void update() throws Exception{
        //1 2 3 4 5调用实现类对象中的方法，接收结果 -&amp;gt;6 7 
		Student stu = new Student(4,&quot;赵六wq&quot;,36);
        Integer result = mapper.update(stu);
    }
    @Test //删除
    public void delete() throws Exception{
        //1 2 3 4 5 6 7
        Integer result = mapper.delete(4);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;运行原理&lt;/h2&gt;
&lt;h3&gt;运行机制&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MyBatis 运行过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;加载 MyBatis 全局配置文件，通过 XPath 方式解析 XML 配置文件，首先解析核心配置文件，&amp;lt;settings&amp;gt; 标签中配置属性项有 defaultExecutorType，用来配置指定 Executor 类型，将配置文件的信息填充到 Configuration对象。最后解析映射器配置的映射文件，并&lt;strong&gt;构建 MappedStatement 对象填充至 Configuration&lt;/strong&gt;，将解析后的映射器添加到 mapperRegistry 中，用于获取代理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建一个 DefaultSqlSession 对象，&lt;strong&gt;根据参数创建指定类型的 Executor&lt;/strong&gt;，二级缓存默认开启，把 Executor 包装成缓存执行器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DefaulSqlSession 调用 getMapper()，通过 JDK 动态代理获取 Mapper 接口的代理对象 MapperProxy&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 SQL 语句：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MapperProxy.invoke() 执行代理方法，通过 MapperMethod#execute 判断执行的是增删改查中的哪个方法&lt;/li&gt;
&lt;li&gt;查询方法调用 sqlSession.selectOne()，从 Configuration 中获取执行者对象 MappedStatement，然后 Executor 调用 executor.query 开始执行查询方法&lt;/li&gt;
&lt;li&gt;首先通过 CachingExecutor 去二级缓存查询，查询不到去一级缓存查询，&lt;strong&gt;最后去数据库查询并放入一级缓存&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Configuration 对象根据 &amp;lt;select&amp;gt; 标签的 statementType 属性创建 StatementHandler 对象，在 StatementHandler 的构造方法中，创建了 ParameterHandler 和 ResultSetHandler 对象&lt;/li&gt;
&lt;li&gt;最后获取 &lt;strong&gt;JDBC 原生的&lt;/strong&gt; Connection 数据库连接对象，创建 Statement 执行者对象，然后通过 ParameterHandler 设置预编译参数，底层是 TypeHandler#setParameter 方法，然后通过 StatementHandler 回调执行者对象执行增删改查，最后调用 ResultsetHandler 处理查询结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;四大对象&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;StatementHandler：执行 SQL 语句的对象&lt;/li&gt;
&lt;li&gt;ParameterHandler：设置预编译参数用的&lt;/li&gt;
&lt;li&gt;ResultHandler：处理结果集&lt;/li&gt;
&lt;li&gt;Executor：执行器，真正进行 Java 与数据库交互的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1mW411M737?p=71&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;获取工厂&lt;/h3&gt;
&lt;p&gt;SqlSessionFactoryBuilder.build(InputStream, String,  Properties)：构建工厂&lt;/p&gt;
&lt;p&gt;XMLConfigBuilder.parse()：解析核心配置文件每个标签的信息（&lt;strong&gt;XPath&lt;/strong&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parseConfiguration(parser.evalNode(&quot;/configuration&quot;))&lt;/code&gt;：读取节点内数据，&amp;lt;configuration&amp;gt; 是 MyBatis 配置文件中的顶层标签&lt;/p&gt;
&lt;p&gt;&lt;code&gt;settings = settingsAsProperties(root.evalNode(&quot;settings&quot;))&lt;/code&gt;：读取核心配置文件中的 &amp;lt;settings&amp;gt; 标签&lt;/p&gt;
&lt;p&gt;&lt;code&gt;settingsElement(settings)&lt;/code&gt;：设置框架相关的属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;configuration.setCacheEnabled()&lt;/code&gt;：&lt;strong&gt;设置缓存属性，默认是 true&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configuration.setDefaultExecutorType()&lt;/code&gt;：&lt;strong&gt;设置 Executor 类型到 configuration，默认是 SIMPLE&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;mapperElement(root.evalNode(&quot;mappers&quot;))&lt;/code&gt;：解析 mappers 信息，分为 package 和 单个注册两种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if...else...&lt;/code&gt;：根据映射方法选择合适的读取方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;XMLMapperBuilder.parse()&lt;/code&gt;：解析 mapper 的标签的信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configurationElement(parser.evalNode(&quot;/mapper&quot;))&lt;/code&gt;：解析 mapper 文件，顶层节点 &amp;lt;mapper&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;buildStatementFromContext(context.evalNodes(&quot;select...&quot;))&lt;/code&gt;：解析&lt;strong&gt;每个操作标签&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XMLStatementBuilder.parseStatementNode()&lt;/code&gt;：解析&lt;strong&gt;操作标签&lt;/strong&gt;的所有的属性&lt;/p&gt;
&lt;p&gt;&lt;code&gt;builderAssistant.addMappedStatement(...)&lt;/code&gt;：&lt;strong&gt;封装成 MappedStatement 对象加入 Configuration 对象&lt;/strong&gt;，代表一个增删改查的标签&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Class&amp;lt;?&amp;gt; mapperInterface = Resources.classForName(mapperClass)&lt;/code&gt;：加载 Mapper 接口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Configuration.addMappers()&lt;/code&gt;：将核心配置文件配置的映射器添加到 mapperRegistry 中，用来&lt;strong&gt;获取代理对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type)&lt;/code&gt;：创建&lt;strong&gt;注解&lt;/strong&gt;解析器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parser.parse()&lt;/code&gt;：解析 Mapper 接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SqlSource sqlSource = getSqlSourceFromAnnotations()&lt;/code&gt;：获取 SQL 的资源对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-SQL%E8%B5%84%E6%BA%90%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;builderAssistant.addMappedStatement(...)&lt;/code&gt;：封装成 MappedStatement 对象加入 Configuration 对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return configuration&lt;/code&gt;：返回配置完成的 configuration 对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;return new DefaultSqlSessionFactory(config)：返回工厂对象，包含 Configuration 对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E8%8E%B7%E5%8F%96%E5%B7%A5%E5%8E%82%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;总结：解析 XML 是对 Configuration 中的属性进行填充，那么可以在一个类中创建 Configuration 对象，自定义其中属性的值来达到配置的效果&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;获取会话&lt;/h3&gt;
&lt;p&gt;DefaultSqlSessionFactory.openSession()：获取 Session 对象，并且创建 Executor 对象&lt;/p&gt;
&lt;p&gt;DefaultSqlSessionFactory.openSessionFromDataSource(...)：ExecutorType 为 Executor 的类型，TransactionIsolationLevel 为事务隔离级别，autoCommit 是否开启事务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;transactionFactory.newTransaction(DataSource, IsolationLevel, boolean&lt;/code&gt;：事务对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configuration.newExecutor(tx, execType)&lt;/code&gt;：&lt;strong&gt;根据参数创建指定类型的 Executor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;批量操作笔记的部分有讲解到 &amp;lt;setting&amp;gt; 的属性 defaultExecutorType，根据配置创建对象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二级缓存默认开启&lt;/strong&gt;，会包装 Executor 对象 &lt;code&gt;new CachingExecutor(executor)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;return new DefaultSqlSession(configuration, executor, autoCommit)：返回 DefaultSqlSession 对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E8%8E%B7%E5%8F%96%E4%BC%9A%E8%AF%9D%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;获取代理&lt;/h3&gt;
&lt;p&gt;Configuration.getMapper(Class, SqlSession)：获取代理的 mapper 对象&lt;/p&gt;
&lt;p&gt;MapperRegistry.getMapper(Class, SqlSession)：MapperRegistry 是 Configuration 属性，在获取工厂对象时初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(MapperProxyFactory&amp;lt;T&amp;gt;) knownMappers.get(type)&lt;/code&gt;：获取接口信息封装为 MapperProxyFactory 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapperProxyFactory.newInstance(sqlSession)&lt;/code&gt;：&lt;strong&gt;创建代理对象&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new MapperProxy&amp;lt;&amp;gt;(sqlSession, mapperInterface, methodCache)&lt;/code&gt;：包装对象
&lt;ul&gt;
&lt;li&gt;methodCache 是并发安全的 ConcurrentHashMap 集合，存放要执行的方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapperProxy&amp;lt;T&amp;gt; implements InvocationHandler&lt;/code&gt; 说明 MapperProxy 默认是一个 InvocationHandler 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Proxy.newProxyInstance()&lt;/code&gt;：&lt;strong&gt;JDK 动态代理&lt;/strong&gt;创建 MapperProxy 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E8%8E%B7%E5%8F%96%E4%BB%A3%E7%90%86%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;执行SQL&lt;/h3&gt;
&lt;p&gt;MapperProxy.invoke()：执行 SQL 语句，Object 类的方法直接执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 当前方法是否是属于 Object 类中的方法
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
            // 当前方法是否是默认方法
        } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    // 包装成一个 MapperMethod 对象并初始化该对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 【根据 switch-case 判断使用的什么类型的 SQL 进行逻辑处理】，此处分析查询语句的查询操作
    return mapperMethod.execute(sqlSession, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sqlSession.selectOne(String, Object)：查询数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object execute(SqlSession sqlSession, Object[] args) {
    //.....
    // 解析传入的参数
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
}
// DefaultSqlSession.selectList(String, Object)
public &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; selectList(String statement, Object parameter, RowBounds rowBounds) {
    // 获取执行者对象
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 开始执行查询语句，参数通过 wrapCollection() 包装成集合类
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Executor#query()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CachingExecutor.query()&lt;/code&gt;：先执行 CachingExecutor 去二级缓存获取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CachingExecutor implements Executor {
  private final Executor delegate;		// 包装了 BaseExecutor，二级缓存不存在数据调用 BaseExecutor 查询
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MappedStatement.getBoundSql(parameterObject)&lt;/code&gt;：&lt;strong&gt;把 parameterObject 封装成 BoundSql&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构造函数中有：&lt;code&gt;this.parameterObject = parameterObject&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-boundSql%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CachingExecutor.createCacheKey()&lt;/code&gt;：创建缓存对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ms.getCache()&lt;/code&gt;：获取二级缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tcm.getObject(cache, key)&lt;/code&gt;：尝试从&lt;strong&gt;二级缓存&lt;/strong&gt;中获取数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BaseExecutor.query()&lt;/code&gt;：二级缓存不存在该数据，调用该方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localCache.getObject(key) &lt;/code&gt;：尝试从&lt;strong&gt;本地缓存（一级缓存&lt;/strong&gt;）获取数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BaseExecutor.queryFromDatabase()&lt;/code&gt;：缓存获取数据失败，&lt;strong&gt;开始从数据库获取数据，并放入本地缓存&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SimpleExecutor.doQuery()&lt;/code&gt;：执行 query&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configuration.newStatementHandler()&lt;/code&gt;：创建 StatementHandler 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据 &amp;lt;select&amp;gt; 标签的 statementType 属性，根据属性选择创建哪种对象&lt;/li&gt;
&lt;li&gt;判断 BoundSql 是否被创建，没有创建会重新封装参数信息到 BoundSql&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;StatementHandler 的构造方法中，创建了 ParameterHandler 和 ResultSetHandler 对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interceptorChain.pluginAll(statementHandler)&lt;/code&gt;：拦截器链&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;prepareStatement()&lt;/code&gt;：通过 StatementHandler 创建 JDBC 原生的 Statement 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getConnection()&lt;/code&gt;：&lt;strong&gt;获取 JDBC 的 Connection 对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler.prepare()&lt;/code&gt;：初始化 Statement 对象
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;instantiateStatement(Connection connection)&lt;/code&gt;：Connection  中的方法实例化对象
&lt;ul&gt;
&lt;li&gt;获取普通执行者对象：&lt;code&gt;Connection.createStatement()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取预编译执行者对象&lt;/strong&gt;：&lt;code&gt;Connection.prepareStatement()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler.parameterize()&lt;/code&gt;：进行参数的设置
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ParameterHandler.setParameters()&lt;/code&gt;：&lt;strong&gt;通过 ParameterHandler 设置参数&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typeHandler.setParameter()&lt;/code&gt;：底层通过 TypeHandler 实现，回调 JDBC 的接口进行设置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StatementHandler.query()&lt;/code&gt;：&lt;strong&gt;调用 JDBC 原生的 PreparedStatement 执行 SQL&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public &amp;lt;E&amp;gt; List&amp;lt;E&amp;gt; query(Statement statement, ResultHandler resultHandler) {
    // 获取 SQL 语句
    String sql = boundSql.getSql();
    statement.execute(sql);
    // 通过 ResultSetHandler 对象封装结果集，映射成 JavaBean
    return resultSetHandler.handleResultSets(statement);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;resultSetHandler.handleResultSets(statement)&lt;/code&gt;：处理结果集&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;handleResultSet(rsw, resultMap, multipleResults, null)&lt;/code&gt;：底层回调&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;handleRowValues()&lt;/code&gt;：逐行处理数据，根据是否配置了 &amp;lt;resultMap&amp;gt; 属性选择是否使用简单结果集映射&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先判断数据是否被限制行数，然后进行结果集的映射&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后将数据存入 ResultHandler 对象，底层就是 List 集合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultResultHandler implements ResultHandler&amp;lt;Object&amp;gt; {
	private final List&amp;lt;Object&amp;gt; list;
  	public void handleResult(ResultContext&amp;lt;?&amp;gt; context) {
    	list.add(context.getResultObject());
  	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return collapseSingleResultList(multipleResults)&lt;/code&gt;：可能存在多个结果集的情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;localCache.putObject(key, list)&lt;/code&gt;：&lt;strong&gt;放入一级（本地）缓存&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;return list.get(0)&lt;/code&gt;：返回结果集的第一个数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-%E6%89%A7%E8%A1%8CSQL%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;插件使用&lt;/h2&gt;
&lt;h3&gt;插件原理&lt;/h3&gt;
&lt;p&gt;实现原理：插件是按照插件配置顺序创建层层包装对象，执行目标方法的之后，按照逆向顺序执行（栈）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-插件原理.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;在四大对象创建时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个创建出来的对象不是直接返回的，而是 &lt;code&gt;interceptorChain.pluginAll(parameterHandler)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取到所有 Interceptor（插件需要实现的接口），调用 &lt;code&gt;interceptor.plugin(target)&lt;/code&gt;返回 target 包装后的对象&lt;/li&gt;
&lt;li&gt;插件机制可以使用插件为目标对象创建一个代理对象，代理对象可以&lt;strong&gt;拦截到四大对象的每一个执行&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Intercepts(
		{
		@Signature(type=StatementHandler.class,method=&quot;parameterize&quot;,args=java.sql.Statement.class)
		})
public class MyFirstPlugin implements Interceptor{

	//intercept：拦截目标对象的目标方法的执行
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		System.out.println(&quot;MyFirstPlugin...intercept:&quot; + invocation.getMethod());
		//动态的改变一下sql运行的参数：以前1号员工，实际从数据库查询11号员工
		Object target = invocation.getTarget();
		System.out.println(&quot;当前拦截到的对象：&quot; + target);
		//拿到：StatementHandler==&amp;gt;ParameterHandler===&amp;gt;parameterObject
		//拿到target的元数据
		MetaObject metaObject = SystemMetaObject.forObject(target);
		Object value = metaObject.getValue(&quot;parameterHandler.parameterObject&quot;);
		System.out.println(&quot;sql语句用的参数是：&quot; + value);
		//修改完sql语句要用的参数
		metaObject.setValue(&quot;parameterHandler.parameterObject&quot;, 11);
		//执行目标方法
		Object proceed = invocation.proceed();
		//返回执行后的返回值
		return proceed;
	}

	// plugin：包装目标对象的，为目标对象创建一个代理对象
	@Override
	public Object plugin(Object target) {
		//可以借助 Plugin 的 wrap 方法来使用当前 Interceptor 包装我们目标对象
		System.out.println(&quot;MyFirstPlugin...plugin:mybatis将要包装的对象&quot; + target);
		Object wrap = Plugin.wrap(target, this);
		//返回为当前target创建的动态代理
		return wrap;
	}

	// setProperties：将插件注册时的property属性设置进来
	@Override
	public void setProperties(Properties properties) {
		System.out.println(&quot;插件配置的信息：&quot; + properties);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--plugins：注册插件  --&amp;gt;
&amp;lt;plugins&amp;gt;
    &amp;lt;plugin interceptor=&quot;mybatis.dao.MyFirstPlugin&quot;&amp;gt;
        &amp;lt;property name=&quot;username&quot; value=&quot;root&quot;/&amp;gt;
        &amp;lt;property name=&quot;password&quot; value=&quot;123456&quot;/&amp;gt;
    &amp;lt;/plugin&amp;gt;
&amp;lt;/plugins&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;分页插件&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/%E5%88%86%E9%A1%B5%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分页可以将很多条结果进行分页显示。如果当前在第一页，则没有上一页。如果当前在最后一页，则没有下一页，需要明确当前是第几页，这一页中显示多少条结果。&lt;/li&gt;
&lt;li&gt;MyBatis 是不带分页功能的，如果想实现分页功能，需要手动编写 LIMIT 语句，不同的数据库实现分页的 SQL 语句也是不同，手写分页 成本较高。&lt;/li&gt;
&lt;li&gt;PageHelper：第三方分页助手，将复杂的分页操作进行封装，从而让分页功能变得非常简单&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;分页操作&lt;/h3&gt;
&lt;p&gt;开发步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;导入 PageHelper 的 Maven 坐标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 MyBatis 核心配置文件中配置 PageHelper 插件&lt;/p&gt;
&lt;p&gt;注意：分页助手的插件配置在通用 Mapper 之前&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;plugins&amp;gt;
    &amp;lt;plugin interceptor=&quot;com.github.pagehelper.PageInterceptor&quot;&amp;gt;
        &amp;lt;!-- 指定方言 --&amp;gt;
    	&amp;lt;property name=&quot;dialect&quot; value=&quot;mysql&quot;/&amp;gt;
    &amp;lt;/plugin&amp;gt; 
&amp;lt;/plugins&amp;gt;
&amp;lt;mappers&amp;gt;.........&amp;lt;/mappers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与 MySQL 分页查询页数计算公式不同&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static &amp;lt;E&amp;gt; Page&amp;lt;E&amp;gt; startPage(int pageNum, int pageSize)&lt;/code&gt;：pageNum第几页，pageSize页面大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void selectAll() {
    //第一页：显示2条数据
    PageHelper.startPage(1,2);
    List&amp;lt;Student&amp;gt; students = sqlSession.selectList(&quot;StudentMapper.selectAll&quot;);
    for (Student student : students) {
        System.out.println(student);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;参数获取&lt;/h3&gt;
&lt;p&gt;PageInfo构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PageInfo&amp;lt;Student&amp;gt; info = new PageInfo&amp;lt;&amp;gt;(list)&lt;/code&gt; : list 是 SQL 执行返回的结果集合，参考上一节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PageInfo相关API：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;startPage()：设置分页参数&lt;/li&gt;
&lt;li&gt;PageInfo：分页相关参数功能类。&lt;/li&gt;
&lt;li&gt;getTotal()：获取总条数&lt;/li&gt;
&lt;li&gt;getPages()：获取总页数&lt;/li&gt;
&lt;li&gt;getPageNum()：获取当前页&lt;/li&gt;
&lt;li&gt;getPageSize()：获取每页显示条数&lt;/li&gt;
&lt;li&gt;getPrePage()：获取上一页&lt;/li&gt;
&lt;li&gt;getNextPage()：获取下一页&lt;/li&gt;
&lt;li&gt;isIsFirstPage()：获取是否是第一页&lt;/li&gt;
&lt;li&gt;isIsLastPage()：获取是否是最后一页&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;Spring&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Spring 是分层的 JavaSE/EE 应用 full-stack 轻量级开源框架&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-%E6%A1%86%E6%9E%B6%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring 优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方便解耦，简化开发&lt;/li&gt;
&lt;li&gt;方便集成各种框架&lt;/li&gt;
&lt;li&gt;方便程序测试&lt;/li&gt;
&lt;li&gt;AOP 编程难过的支持&lt;/li&gt;
&lt;li&gt;声明式事务的支持&lt;/li&gt;
&lt;li&gt;降低 JavaEE API 的使用难度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;体系结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/37974444&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;IoC&lt;/h2&gt;
&lt;h3&gt;基本概述&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;IoC（Inversion Of Control）控制反转，Spring 反向控制应用程序所需要使用的外部资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spring 控制的资源全部放置在 Spring 容器中，该容器称为 IoC 容器&lt;/strong&gt;（存放实例对象）&lt;/li&gt;
&lt;li&gt;官方网站：https://spring.io/ → Projects → spring-framework → LEARN → Reference Doc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-IOC%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;耦合（Coupling）：代码编写过程中所使用技术的结合紧密度，用于衡量软件中各个模块之间的互联程度&lt;/li&gt;
&lt;li&gt;内聚（Cohesion）：代码编写过程中单个模块内部各组成部分间的联系，用于衡量软件中各个功能模块内部的功能联系&lt;/li&gt;
&lt;li&gt;代码编写的目标：高内聚，低耦合。同一个模块内的各个元素之间要高度紧密，各个模块之间的相互依存度不紧密&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;入门项目&lt;/h3&gt;
&lt;p&gt;模拟三层架构中表现层调用业务层功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表现层：UserApp 模拟 UserServlet（使用 main 方法模拟）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层：UserService&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;导入 spring 坐标（5.1.9.release）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写业务层与表现层（模拟）接口与实现类 service.UserService，service.impl.UserServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
	//业务方法  
	void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    public void save() {
        System.out.println(&quot;user service running...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建立 Spring 配置文件：resources.&lt;strong&gt;applicationContext&lt;/strong&gt;.xml (名字一般使用该格式)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置所需资源（Service）为 Spring 控制的资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd&quot;&amp;gt;
    &amp;lt;!-- 1.创建spring控制的资源--&amp;gt;
    &amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;/&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表现层（App）通过 Spring 获取资源（Service 实例）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserApp {
    public static void main(String[] args) {
        //2.加载配置文件
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        //3.获取资源
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
        userService.save();//user service running...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-IOC%E5%AE%9E%E7%8E%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;XML开发&lt;/h3&gt;
&lt;h4&gt;bean&lt;/h4&gt;
&lt;h5&gt;基本属性&lt;/h5&gt;
&lt;p&gt;标签：&amp;lt;bean&amp;gt; 标签，&amp;lt;beans&amp;gt; 的子标签&lt;/p&gt;
&lt;p&gt;作用：定义 Spring 中的资源，受此标签定义的资源将受到 Spring 控制&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans&amp;gt;
	&amp;lt;bean /&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：bean 的名称，通过 id 值获取 bean (首字母小写)&lt;/li&gt;
&lt;li&gt;class：bean 的类型，使用完全限定类名&lt;/li&gt;
&lt;li&gt;name：bean 的名称，可以通过 name 值获取 bean，用于多人配合时给 bean 起别名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;beanId&quot; name=&quot;beanName1,beanName2&quot; class=&quot;ClassName&quot;&amp;gt;&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ctx.getBean(&quot;beanId&quot;) == ctx.getBean(&quot;beanName1&quot;) == ctx.getBean(&quot;beanName2&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;作用范围&lt;/h5&gt;
&lt;p&gt;作用：定义 bean 的作用范围&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean scope=&quot;singleton&quot;&amp;gt;&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;singleton：设定创建出的对象保存在 Spring 容器中，是一个单例的对象&lt;/li&gt;
&lt;li&gt;prototype：设定创建出的对象保存在 Spring 容器中，是一个非单例（原型）的对象&lt;/li&gt;
&lt;li&gt;request、session、application、 websocket ：设定创建出的对象放置在 web 容器对应的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring 容器中 Bean 的&lt;strong&gt;线程安全&lt;/strong&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原型 Bean，每次创建一个新对象，线程之间并不存在 Bean 共享，所以不会有线程安全的问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单例 Bean，所有线程共享一个单例实例 Bean，因此是存在资源的竞争，如果单例 Bean是一个&lt;strong&gt;无状态 Bean&lt;/strong&gt;，也就是线程中的操作不会对 Bean 的成员执行查询以外的操作，那么这个单例 Bean 是线程安全的&lt;/p&gt;
&lt;p&gt;解决方法：开发人员来进行线程安全的保证，最简单的办法就是把 Bean 的作用域 singleton 改为 protopyte&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;生命周期&lt;/h5&gt;
&lt;p&gt;作用：定义 bean 对象在初始化或销毁时完成的工作&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean init-method=&quot;init&quot; destroy-method=&quot;destroy&amp;gt;&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取值：bean 对应的类中对应的具体方法名&lt;/p&gt;
&lt;p&gt;实现接口的方式实现初始化，无需配置文件配置 init-method：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现 InitializingBean，定义初始化逻辑&lt;/li&gt;
&lt;li&gt;实现 DisposableBean，定义销毁逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 scope=“singleton” 时，Spring 容器中有且仅有一个对象，init 方法在创建容器时仅执行一次&lt;/li&gt;
&lt;li&gt;当 scope=“prototype” 时，Spring 容器要创建同一类型的多个对象，init 方法在每个对象创建时均执行一次&lt;/li&gt;
&lt;li&gt;当 scope=“singleton” 时，关闭容器（&lt;code&gt;.close()&lt;/code&gt;）会导致 bean 实例的销毁，调用 destroy 方法一次&lt;/li&gt;
&lt;li&gt;当 scope=“prototype” 时，对象的销毁由垃圾回收机制 GC 控制，destroy 方法将不会被执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;bean 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--init-method和destroy-method用于控制bean的生命周期--&amp;gt;
&amp;lt;bean id=&quot;userService3&quot; scope=&quot;prototype&quot; init-method=&quot;init&quot; destroy-method=&quot;destroy&quot; class=&quot;service.impl.UserServiceImpl&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;业务层实现类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService{
    public UserServiceImpl(){
        System.out.println(&quot; constructor is running...&quot;);
    }

    public void init(){
        System.out.println(&quot;init....&quot;);
    }

    public void destroy(){
        System.out.println(&quot;destroy....&quot;);
    }

    public void save() {
        System.out.println(&quot;user service running...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserService userService = (UserService)ctx.getBean(&quot;userService3&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;创建方式&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态工厂&lt;/p&gt;
&lt;p&gt;作用：定义 bean 对象创建方式，使用静态工厂的形式创建 bean，兼容早期遗留系统的升级工作&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean class=&quot;FactoryClassName&quot; factory-method=&quot;factoryMethodName&quot;&amp;gt;&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取值：工厂 bean 中用于获取对象的静态方法名&lt;/p&gt;
&lt;p&gt;注意事项：class 属性必须配置成静态工厂的类名&lt;/p&gt;
&lt;p&gt;bean 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--静态工厂创建 bean--&amp;gt;
&amp;lt;bean id=&quot;userService4&quot; class=&quot;service.UserServiceFactory&quot; factory-method=&quot;getService&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工厂类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceFactory {
    public static UserService getService(){
        System.out.println(&quot;factory create object...&quot;);
        return new UserServiceImpl();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserService userService = (UserService)ctx.getBean(&quot;userService4&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例工厂&lt;/p&gt;
&lt;p&gt;作用：定义 bean 对象创建方式，使用实例工厂的形式创建 bean，兼容早期遗留系统的升级工作&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean factory-bean=&quot;factoryBeanId&quot; factory-method=&quot;factoryMethodName&quot;&amp;gt;&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用实例工厂创建 bean 首先需要将实例工厂配置 bean，交由 Spring 进行管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;factory-bean 是实例工厂的 Id&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;bean 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--实例工厂创建 bean，依赖工厂对象对应的 bean--&amp;gt;
&amp;lt;bean id=&quot;factoryBean&quot; class=&quot;service.UserServiceFactory2&quot;/&amp;gt;
&amp;lt;bean id=&quot;userService5&quot; factory-bean=&quot;factoryBean&quot; factory-method=&quot;getService&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工厂类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceFactory2 {
    public UserService getService(){
        System.out.println(&quot; instance factory create object...&quot;);
        return new UserServiceImpl();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;获取Bean&lt;/h5&gt;
&lt;p&gt;ApplicationContext 子类相关API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String[] getBeanDefinitionNames()&lt;/td&gt;
&lt;td&gt;获取 Spring 容器中定义的所有 JavaBean 的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BeanDefinition getBeanDefinition(String beanName)&lt;/td&gt;
&lt;td&gt;返回给定 bean 名称的 BeanDefinition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String[] getBeanNamesForType(Class&amp;lt;?&amp;gt; type)&lt;/td&gt;
&lt;td&gt;获取 Spring 容器中指定类型的所有 JavaBean 的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment getEnvironment()&lt;/td&gt;
&lt;td&gt;获取与此组件关联的环境&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;DI&lt;/h4&gt;
&lt;h5&gt;依赖注入&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;IoC（Inversion Of Control）控制翻转，Spring 反向控制应用程序所需要使用的外部资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DI（Dependency Injection）依赖注入，应用程序运行依赖的资源由 Spring 为其提供，资源进入应用程序的方式称为注入，简单说就是利用反射机制为类的属性赋值的操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-DI%E4%BB%8B%E7%BB%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IoC 和 DI 的关系：IoC 与 DI 是同一件事站在不同角度看待问题&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;set 注入&lt;/h5&gt;
&lt;p&gt;标签：&amp;lt;property&amp;gt; 标签，&amp;lt;bean&amp;gt; 的子标签&lt;/p&gt;
&lt;p&gt;作用：使用 set 方法的形式为 bean 提供资源&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean&amp;gt;
	&amp;lt;property /&amp;gt;
    &amp;lt;property /&amp;gt;
    .....
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name：对应 bean 中的属性名，要注入的变量名，要求该属性必须提供可访问的 set 方法（严格规范此名称是 set 方法对应名称，首字母必须小写）&lt;/li&gt;
&lt;li&gt;value：设定非引用类型属性对应的值，&lt;strong&gt;不能与 ref 同时使用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;ref：设定引用类型属性对应 bean 的 id ，不能与 value 同时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property name=&quot;propertyName&quot; value=&quot;propertyValue&quot; ref=&quot;beanId&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DAO 层：要注入的资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDao {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserDaoImpl implements UserDao{
    public void save(){
        System.out.println(&quot;user dao running...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service 业务层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
	private UserDao userDao;
    private int num;
    
    //1.对需要进行注入的变量添加set方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
	public void setNum(int num) {
        this.num = num;
    }
    
    public void save() {
        System.out.println(&quot;user service running...&quot; + num);
        userDao.save();
        bookDao.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--2.将要注入的资源声明为bean--&amp;gt;
&amp;lt;bean id=&quot;userDao&quot; class=&quot;dao.impl.UserDaoImpl&quot;/&amp;gt;

&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
	&amp;lt;!--3.将要注入的引用类型的变量通过property属性进行注入，--&amp;gt;
    &amp;lt;property name=&quot;userDao&quot; ref=&quot;userDao&quot;/&amp;gt;
    &amp;lt;property name=&quot;num&quot; value=&quot;666&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
        userService.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;构造注入&lt;/h5&gt;
&lt;p&gt;标签：&amp;lt;constructor-arg&amp;gt; 标签，&amp;lt;bean&amp;gt; 的子标签&lt;/p&gt;
&lt;p&gt;作用：使用构造方法的形式为 bean 提供资源，兼容早期遗留系统的升级工作&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean&amp;gt;
	&amp;lt;constructor-arg /&amp;gt;
    .....&amp;lt;!--一个bean可以有多个constructor-arg标签--&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name：对应bean中的构造方法所携带的参数名&lt;/li&gt;
&lt;li&gt;value：设定非引用类型构造方法参数对应的值，不能与 ref 同时使用&lt;/li&gt;
&lt;li&gt;ref：设定引用类型构造方法参数对应 bean 的 id ，不能与 value 同时使用&lt;/li&gt;
&lt;li&gt;type：设定构造方法参数的类型，用于按类型匹配参数或进行类型校验&lt;/li&gt;
&lt;li&gt;index：设定构造方法参数的位置，用于按位置匹配参数，参数 index 值从 0 开始计数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;constructor-arg name=&quot;argsName&quot; value=&quot;argsValue&quot; /&amp;gt;
&amp;lt;constructor-arg index=&quot;arg-index&quot; type=&quot;arg-type&quot; ref=&quot;beanId&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DAO 层：要注入的资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserDaoImpl implements UserDao{
    private String username;
    private String pwd;
    private String driver;

    public UserDaoImpl(String driver,String username, String pwd) {
        this.driver = driver;
        this.username = username;
        this.pwd = pwd;
    }
    public void save(){
        System.out.println(&quot;user dao running...&quot;+username+&quot; &quot;+pwd+&quot; &quot;+driver);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service 业务层：参考 set 注入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;userDao&quot; class=&quot;dao.impl.UserDaoImpl&quot;&amp;gt;
    &amp;lt;!--使用构造方法进行注入，需要保障注入的属性与bean中定义的属性一致--&amp;gt;
	&amp;lt;!--一致指顺序一致或类型一致或使用index解决该问题--&amp;gt;
    &amp;lt;constructor-arg index=&quot;2&quot; value=&quot;123&quot;/&amp;gt;
    &amp;lt;constructor-arg index=&quot;1&quot; value=&quot;root&quot;/&amp;gt;
    &amp;lt;constructor-arg index=&quot;0&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;userDao&quot; ref=&quot;userDao&quot;/&amp;gt;
    &amp;lt;property name=&quot;num&quot; value=&quot;666&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方式二：使用 UserServiceImpl 的构造方法注入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
	&amp;lt;constructor-arg name=&quot;userDao&quot; ref=&quot;userDao&quot;/&amp;gt;
	&amp;lt;constructor-arg name=&quot;num&quot; value=&quot;666666&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类：参考 set 注入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;集合注入&lt;/h5&gt;
&lt;p&gt;标签：&amp;lt;array&amp;gt; &amp;lt;list&amp;gt; &amp;lt;set&amp;gt; &amp;lt;map&amp;gt; &amp;lt;props&amp;gt;，&amp;lt;property&amp;gt; 或 &amp;lt;constructor-arg&amp;gt; 标签的子标签&lt;/p&gt;
&lt;p&gt;作用：注入集合数据类型属性&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property&amp;gt;
	&amp;lt;list&amp;gt;&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DAO 层：要注入的资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface BookDao {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class BookDaoImpl implements BookDao {
    private ArrayList al;
    private Properties properties;
    private int[] arr;
    private HashSet hs;
    private HashMap hm ;

    public void setAl(ArrayList al) {
        this.al = al;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public void setArr(int[] arr) {
        this.arr = arr;
    }

    public void setHs(HashSet hs) {
        this.hs = hs;
    }

    public void setHm(HashMap hm) {
        this.hm = hm;
    }

    public void save() {
        System.out.println(&quot;book dao running...&quot;);
        System.out.println(&quot;ArrayList:&quot; + al);
        System.out.println(&quot;Properties:&quot; + properties);
        for (int i = 0; i &amp;lt; arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println(&quot;HashSet:&quot; + hs);
        System.out.println(&quot;HashMap:&quot; + hm);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service 业务层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    private BookDao bookDao;
    
    public UserServiceImpl() {}
    
	public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public void save() {
        System.out.println(&quot;user service running...&quot; + num);
        bookDao.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;bookDao&quot; ref=&quot;bookDao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean id=&quot;bookDao&quot; class=&quot;dao.impl.BookDaoImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;al&quot;&amp;gt;
        &amp;lt;list&amp;gt;
            &amp;lt;value&amp;gt;seazean&amp;lt;/value&amp;gt;
            &amp;lt;value&amp;gt;66666&amp;lt;/value&amp;gt;
        &amp;lt;/list&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;properties&quot;&amp;gt;
        &amp;lt;props&amp;gt;
            &amp;lt;prop key=&quot;name&quot;&amp;gt;seazean666&amp;lt;/prop&amp;gt;
            &amp;lt;prop key=&quot;value&quot;&amp;gt;666666&amp;lt;/prop&amp;gt;
        &amp;lt;/props&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;arr&quot;&amp;gt;
        &amp;lt;array&amp;gt;
            &amp;lt;value&amp;gt;123456&amp;lt;/value&amp;gt;
            &amp;lt;value&amp;gt;66666&amp;lt;/value&amp;gt;
        &amp;lt;/array&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;hs&quot;&amp;gt;
        &amp;lt;set&amp;gt;
            &amp;lt;value&amp;gt;seazean&amp;lt;/value&amp;gt;
            &amp;lt;value&amp;gt;66666&amp;lt;/value&amp;gt;
        &amp;lt;/set&amp;gt;
    &amp;lt;/property&amp;gt;
    &amp;lt;property name=&quot;hm&quot;&amp;gt;
        &amp;lt;map&amp;gt;
            &amp;lt;entry key=&quot;name&quot; value=&quot;seazean66666&quot;/&amp;gt;
            &amp;lt;entry key=&quot;value&quot; value=&quot;6666666666&quot;/&amp;gt;
        &amp;lt;/map&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;P&lt;/h4&gt;
&lt;p&gt;标签：&amp;lt;p:propertyName&amp;gt;，&amp;lt;p:propertyName-ref&amp;gt;&lt;/p&gt;
&lt;p&gt;作用：为 bean 注入属性值&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean p:propertyName=&quot;propertyValue&quot; p:propertyName-ref=&quot;beanId&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启 p 命令空间：开启 Spring 对 p 命令空间的的支持，在 beans 标签中添加对应空间支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;   		
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;    
       xmlns:p=&quot;http://www.springframework.org/schema/p&quot;       
       xsi:schemaLocation=&quot;
		http://www.springframework.org/schema/beans     
		https://www.springframework.org/schema/beans/spring-beans.xsd&quot;&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean 
      id=&quot;userService&quot;
      class=&quot;service.impl.UserServiceImpl&quot;
      p:userDao-ref=&quot;userDao&quot;
      p:bookDao-ref=&quot;bookDao&quot;
      p:num=&quot;10&quot;
	/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;SpEL&lt;/h4&gt;
&lt;p&gt;Spring 提供了对 EL 表达式的支持，统一属性注入格式&lt;/p&gt;
&lt;p&gt;作用：为 bean 注入属性值&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property value=&quot;EL&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：所有属性值不区分是否引用类型，统一使用value赋值&lt;/p&gt;
&lt;p&gt;所有格式统一使用  value=“#{}”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常量  #{10}  #{3.14}  #{2e5}  #{‘it’}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用 bean  #{beanId}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用 bean 属性  #{beanId.propertyName}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用 bean 方法  beanId.methodName().method2()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用静态方法  T(java.lang.Math).PI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运算符支持  #{3 lt 4 == 4 ge 3}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正则表达式支持  #{user.name matches‘[a-z]{6,}’}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合支持  #{likes[3]}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
        &amp;lt;property name=&quot;userDao&quot; value=&quot;#{userDao}&quot;/&amp;gt;
        &amp;lt;property name=&quot;bookDao&quot; value=&quot;#{bookDao}&quot;/&amp;gt;
        &amp;lt;property name=&quot;num&quot; value=&quot;#{666666666}&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;prop&lt;/h4&gt;
&lt;p&gt;Spring 提供了读取外部 properties 文件的机制，使用读取到的数据为 bean 的属性赋值&lt;/p&gt;
&lt;p&gt;操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;准备外部 properties 文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开启 context 命名空间支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xsi:schemaLocation=&quot;
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        &quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加载指定的 properties 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:property-placeholder location=&quot;classpath:data.properties&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用加载的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property name=&quot;propertyName&quot; value=&quot;${propertiesName}&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意：如果需要加载所有的 properties 文件，可以使用 &lt;code&gt;*.properties&lt;/code&gt; 表示加载所有的 properties 文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意：读取数据使用 &lt;strong&gt;${propertiesName}&lt;/strong&gt; 格式进行，其中 propertiesName 指 properties 文件中的属性名&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;data.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;username=root
pwd=123456
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DAO 层：注入的资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDao {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserDaoImpl implements UserDao{
    private String userName;
    private String password;

    public void setUserName(String userName) {
        this.userName = userName;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    public void save(){
        System.out.println(&quot;user dao running...&quot;+userName+&quot; &quot;+password);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service 业务层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    public void save() {
        System.out.println(&quot;user service running...&quot;);
        userDao.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:property-placeholder location=&quot;classpath:*.properties&quot;/&amp;gt;

&amp;lt;bean id=&quot;userDao&quot; class=&quot;dao.impl.UserDaoImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;userName&quot; value=&quot;${username}&quot;/&amp;gt;
    &amp;lt;property name=&quot;password&quot; value=&quot;${pwd}&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;userDao&quot; ref=&quot;userDao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
        userService.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;import&lt;/h4&gt;
&lt;p&gt;标签：&amp;lt;import&amp;gt;，&amp;lt;beans&amp;gt;标签的子标签&lt;/p&gt;
&lt;p&gt;作用：在当前配置文件中导入其他配置文件中的项&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans&amp;gt;
    &amp;lt;import /&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;resource：加载的配置文件名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;import resource=“config2.xml&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring 容器加载多个配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;applicationContext-book.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;bookDao&quot; class=&quot;dao.impl.BookDaoImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;num&quot; value=&quot;1&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext-user.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;userDao&quot; class=&quot;dao.impl.UserDaoImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;userName&quot; value=&quot;${username}&quot;/&amp;gt;
    &amp;lt;property name=&quot;password&quot; value=&quot;${pwd}&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;userDao&quot; ref=&quot;userDao&quot;/&amp;gt;
    &amp;lt;property name=&quot;bookDao&quot; ref=&quot;bookDao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;import resource=&quot;applicationContext-user.xml&quot;/&amp;gt;
&amp;lt;import resource=&quot;applicationContext-book.xml&quot;/&amp;gt;

&amp;lt;bean id=&quot;bookDao&quot; class=&quot;com.seazean.dao.impl.BookDaoImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;num&quot; value=&quot;2&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new ClassPathXmlApplicationContext(&quot;applicationContext-user.xml&quot;,&quot;applicationContext-book.xml&quot;);
new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring 容器中的 bean 定义冲突问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同 id 的 bean，后定义的覆盖先定义的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入配置文件可以理解为将导入的配置文件复制粘贴到对应位置，程序执行选择最下面的配置使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入配置文件的顺序与位置不同可能会导致最终程序运行结果不同&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;三方资源&lt;/h4&gt;
&lt;h5&gt;Druid&lt;/h5&gt;
&lt;p&gt;第三方资源配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.1.16&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--加载druid资源--&amp;gt;
&amp;lt;bean id=&quot;dataSource&quot; class=&quot;com.alibaba.druid.pool.DruidDataSource&quot;&amp;gt;
    &amp;lt;property name=&quot;driverClassName&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&amp;gt;
    &amp;lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://192.168.2.185:3306/spring_db&quot;/&amp;gt;
    &amp;lt;property name=&quot;username&quot; value=&quot;root&quot;/&amp;gt;
    &amp;lt;property name=&quot;password&quot; value=&quot;123456&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;App.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
DruidDataSource datasource = (DruidDataSource) ctx.getBean(&quot;datasource&quot;);
System.out.println(datasource);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Mybatis&lt;/h5&gt;
&lt;p&gt;Mybatis 核心配置文件消失&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环境 environment 转换成数据源对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;映射 Mapper 扫描工作交由 Spring 处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类型别名交由 Spring 处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DAO 接口不需要创建实现类，MyBatis-Spring 提供了一个动态代理的实现 &lt;strong&gt;MapperFactoryBean&lt;/strong&gt;，这个类可以让直接注入数据映射器接口到 service 层 bean 中，底层将会动态代理创建类&lt;/p&gt;
&lt;p&gt;整合原理：利用 Spring 框架的 SPI 机制，在 META-INF 目录的 spring.handlers 中给 Spring 容器中导入 NamespaceHandler 类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NamespaceHandler 的 init 方法注册 bean 信息的解析器 MapperScannerBeanDefinitionParser&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解析器在 Spring 容器创建过程中去&lt;strong&gt;解析 mapperScanner 标签&lt;/strong&gt;，解析出的属性填充到 MapperScannerConfigurer 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口，重写 postProcessBeanDefinitionRegistry() 方法，可以扫描到 MyBatis 的 Mapper&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解开发&lt;/h3&gt;
&lt;h4&gt;注解驱动&lt;/h4&gt;
&lt;h5&gt;XML&lt;/h5&gt;
&lt;p&gt;启动注解扫描，加载类中配置的注解项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:component-scan base-package=&quot;packageName1,packageName2&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在进行包扫描时，会对配置的包及其子包中所有文件进行扫描，多个包采用&lt;code&gt;,&lt;/code&gt;隔开&lt;/li&gt;
&lt;li&gt;扫描过程是以文件夹递归迭代的形式进行的&lt;/li&gt;
&lt;li&gt;扫描过程仅读取合法的 Java 文件&lt;/li&gt;
&lt;li&gt;扫描时仅读取 Spring 可识别的注解&lt;/li&gt;
&lt;li&gt;扫描结束后会将可识别的有效注解转化为 Spring 对应的资源加入 IoC 容器&lt;/li&gt;
&lt;li&gt;从加载效率上来说注解优于 XML 配置文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注解：启动时使用注解的形式替代 xml 配置，将 Spring 配置文件从工程中消除，简化书写&lt;/p&gt;
&lt;p&gt;缺点：为了达成注解驱动的目的，可能会将原先很简单的书写，变的更加复杂。XML 中配置第三方开发的资源是很方便的，但使用注解驱动无法在第三方开发的资源中进行编辑，因此会增大开发工作量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/%E6%B3%A8%E8%A7%A3%E9%A9%B1%E5%8A%A8%E7%A4%BA%E4%BE%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;纯注解&lt;/h5&gt;
&lt;p&gt;注解配置类&lt;/p&gt;
&lt;p&gt;名称：@Configuration、@ComponentScan&lt;/p&gt;
&lt;p&gt;类型：类注解&lt;/p&gt;
&lt;p&gt;作用：&lt;strong&gt;设置当前类为 Spring 核心配置加载类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan({&quot;scanPackageName1&quot;,&quot;scanPackageName2&quot;})
public class SpringConfigClassName{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心配合类用于替换 Spring 核心配置文件，此类可以设置空的，不设置变量与属性&lt;/li&gt;
&lt;li&gt;bean 扫描工作使用注解 @ComponentScan 替代，多个包用 &lt;code&gt;{} 和 ,&lt;/code&gt; 隔开&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加载纯注解格式上下文对象，需要使用 &lt;strong&gt;AnnotationConfigApplicationContext&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SpringConfig {
    @Bean
    public Person person() {
        return new Person1(&quot;lisi&quot;, 20);
    }
}

public class MainTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new 
            		AnnotationConfigApplicationContext(SpringConfig.class);
        //方式一：名称对应类名
        Person bean = applicationContext.getBean(Person.class);
        System.out.println(bean);
		
        //方式二：名称对应方法名 
        Person bean1 = (Person) applicationContext.getBean(&quot;person1&quot;);	
        
        //方法三：指定名称@Bean(&quot;person2&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;扫描器&lt;/h5&gt;
&lt;p&gt;组件扫描过滤器&lt;/p&gt;
&lt;p&gt;开发过程中，需要根据需求加载必要的 bean，排除指定 bean&lt;/p&gt;
&lt;p&gt;名称：@ComponentScan&lt;/p&gt;
&lt;p&gt;类型：&lt;strong&gt;类注解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：设置 Spring 配置加载类扫描规则&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ComponentScan(
    value = {&quot;dao&quot;,&quot;service&quot;},			//设置基础扫描路径
    excludeFilters =					//设置过滤规则，当前为排除过滤
	@ComponentScan.Filter(				//设置过滤器
	    type= FilterType.ANNOTATION,  	//设置过滤方式为按照注解进行过滤
	    classes = Service.class)     	//设置具体的过滤项，过滤所有@Service修饰的bean
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;includeFilters：设置包含性过滤器&lt;/li&gt;
&lt;li&gt;excludeFilters：设置排除性过滤器&lt;/li&gt;
&lt;li&gt;type：设置过滤器类型&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基本注解&lt;/h4&gt;
&lt;h5&gt;设置 bean&lt;/h5&gt;
&lt;p&gt;名称：@Component、@Controller、@Service、@Repository&lt;/p&gt;
&lt;p&gt;类型：类注解，写在类定义上方&lt;/p&gt;
&lt;p&gt;作用：设置该类为 Spring 管理的 bean&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ClassName{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：@Controller、@Service 、@Repository 是 @Component 的衍生注解，功能同 @Component&lt;/p&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value（默认）：定义 bean 的访问 id&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;作用范围&lt;/h5&gt;
&lt;p&gt;名称：@Scope&lt;/p&gt;
&lt;p&gt;类型：类注解，写在类定义上方&lt;/p&gt;
&lt;p&gt;作用：设置该类作为 bean 对应的 scope 属性&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Scope
public class ClassName{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value（默认）：定义 bean 的作用域，默认为 singleton，非单例取值 prototype&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;生命周期&lt;/h5&gt;
&lt;p&gt;名称：@PostConstruct、@PreDestroy&lt;/p&gt;
&lt;p&gt;类型：方法注解，写在方法定义上方&lt;/p&gt;
&lt;p&gt;作用：设置该类作为 bean 对应的生命周期方法&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//定义bean，后面添加bean的id
@Component(&quot;userService&quot;)
//定义bean的作用域
@Scope(&quot;singleton&quot;)
public class UserServiceImpl implements UserService {
    //初始化
    @PostConstruct
    public void init(){
        System.out.println(&quot;user service init...&quot;);
    }
	//销毁
    @PreDestroy
    public void destroy(){
        System.out.println(&quot;user service destroy...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个对象的执行顺序：Constructor &amp;gt;&amp;gt; @Autowired（注入属性） &amp;gt;&amp;gt; @PostConstruct（初始化逻辑）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;加载资源&lt;/h5&gt;
&lt;p&gt;名称：@Bean&lt;/p&gt;
&lt;p&gt;类型：方法注解&lt;/p&gt;
&lt;p&gt;作用：设置该方法的返回值作为 Spring 管理的 bean&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean(&quot;dataSource&quot;)
public DruidDataSource createDataSource() {    return ……;    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为第三方 bean 无法在其源码上进行修改，使用 @Bean 解决第三方 bean 的引入问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该注解用于替代 XML 配置中的静态工厂与实例工厂创建 bean，不区分方法是否为静态或非静态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Bean 所在的类必须被 Spring 扫描加载，否则该注解无法生效&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value（默认）：定义 bean 的访问 id&lt;/li&gt;
&lt;li&gt;initMethod：声明初始化方法&lt;/li&gt;
&lt;li&gt;destroyMethod：声明销毁方法&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;属性注入&lt;/h4&gt;
&lt;h5&gt;基本类型&lt;/h5&gt;
&lt;p&gt;名称：@Value&lt;/p&gt;
&lt;p&gt;类型：属性注解、方法注解&lt;/p&gt;
&lt;p&gt;作用：设置对应属性的值或对方法进行传参&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//@Value(&quot;${jdbc.username}&quot;)
@Value(&quot;root&quot;)
private String username;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;value 值&lt;strong&gt;仅支持非引用类型数据&lt;/strong&gt;，赋值时对方法的所有参数全部赋值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;value 值支持读取 properties 文件中的属性值，通过类属性将 properties 中数据传入类中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;value 值支持 SpEL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@value 注解如果添加在属性上方，可以省略 set 方法（set 方法的目的是为属性赋值）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value（默认）：定义对应的属性值或参数值&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;自动装配&lt;/h5&gt;
&lt;h6&gt;属性注入&lt;/h6&gt;
&lt;p&gt;名称：@Autowired、@Qualifier&lt;/p&gt;
&lt;p&gt;类型：属性注解、方法注解&lt;/p&gt;
&lt;p&gt;作用：设置对应属性的对象、对方法进行引用类型传参&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired(required = false)
@Qualifier(&quot;userDao&quot;)
private UserDao userDao;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Autowired 默认按类型装配，指定 @Qualifier 后可以指定自动装配的 bean 的 id&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;required：&lt;strong&gt;为 true （默认）表示注入 bean 时该 bean 必须存在&lt;/strong&gt;，不然就会注入失败抛出异常；为 false  表示注入时该 bean 存在就注入，不存在就忽略跳过&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：在使用 @Autowired 时，首先在容器中查询对应类型的 bean，如果查询结果刚好为一个，就将该 bean 装配给 @Autowired 指定的数据，如果查询的结果不止一个，那么 @Autowired 会根据名称来查找，如果查询的结果为空，那么会抛出异常&lt;/p&gt;
&lt;p&gt;解决方法：使用 required = false&lt;/p&gt;
&lt;hr /&gt;
&lt;h6&gt;优先注入&lt;/h6&gt;
&lt;p&gt;名称：@Primary&lt;/p&gt;
&lt;p&gt;类型：类注解&lt;/p&gt;
&lt;p&gt;作用：设置类对应的 bean 按类型装配时优先装配&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Primary
public class ClassName{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Autowired 默认按类型装配，当出现相同类型的 bean，使用 @Primary 提高按类型自动装配的优先级，多个 @Primary 会导致优先级设置无效&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;注解对比&lt;/h6&gt;
&lt;p&gt;名称：@Inject、@Named、@Resource&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Inject 与 @Named 是 JSR330 规范中的注解，功能与 @Autowired 和 @Qualifier 完全相同，适用于不同架构场景&lt;/li&gt;
&lt;li&gt;@Resource 是 JSR250 规范中的注解，可以简化书写格式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Resource 相关属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;name：设置注入的 bean 的 id&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;type：设置注入的 bean 的类型，接收的参数为 Class 类型&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Autowired 和 @Resource之间的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@Autowired 默认是&lt;strong&gt;按照类型装配&lt;/strong&gt;注入，默认情况下它要求依赖对象必须存在（可以设置 required 属性为 false）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Resource 默认&lt;strong&gt;按照名称装配&lt;/strong&gt;注入，只有当找不到与名称匹配的 bean 才会按照类型来装配注入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;静态注入&lt;/h5&gt;
&lt;p&gt;Spring 容器管理的都是实例对象，&lt;strong&gt;@Autowired 依赖注入的都是容器内的对象实例&lt;/strong&gt;，在 Java 中 static 修饰的静态属性（变量和方法）是属于类的，而非属于实例对象&lt;/p&gt;
&lt;p&gt;当类加载器加载静态变量时，Spring 上下文尚未加载，所以类加载器不会在 Bean 中正确注入静态类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class TestClass {
    @Autowired
    private static Component component;

    // 调用静态组件的方法
    public static void testMethod() {
        component.callTestMethod()；
    }  
}
// 编译正常，但运行时报java.lang.NullPointerException，所以在调用testMethod()方法时，component变量还没被初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@Autowired 注解到&lt;strong&gt;类的构造函数&lt;/strong&gt;上，Spring 扫描到 Component 的 Bean，然后赋给静态变量 component&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class TestClass {
    private static Component component;

    @Autowired
    public TestClass(Component component) {
        TestClass.component = component;
    }

    public static void testMethod() {
        component.callTestMethod()；
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Autowired 注解到&lt;strong&gt;静态属性的 setter 方法&lt;/strong&gt;上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 @PostConstruct 注解一个方法，在方法内为 static 静态成员赋值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Spring 框架工具类获取 bean，定义成局部变量使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestClass {
    // 调用静态组件的方法
   public static void testMethod() {
      Component component = SpringApplicationContextUtil.getBean(&quot;component&quot;);
      component.callTestMethod();
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：http://jessehzx.top/2018/03/18/spring-autowired-static-field/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;文件读取&lt;/h4&gt;
&lt;p&gt;名称：@PropertySource&lt;/p&gt;
&lt;p&gt;类型：类注解&lt;/p&gt;
&lt;p&gt;作用：加载 properties 文件中的属性值&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@PropertySource(value = &quot;classpath:filename.properties&quot;)
public class ClassName {
    @Value(&quot;${propertiesAttributeName}&quot;)
    private String attributeName;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不支持 * 通配符，加载后，所有 Spring 控制的 bean 中均可使用对应属性值，加载多个需要用 &lt;code&gt;{} 和 ,&lt;/code&gt; 隔开&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;value（默认）：设置加载的 properties 文件名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ignoreResourceNotFound：如果资源未找到，是否忽略，默认为 false&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加载控制&lt;/h4&gt;
&lt;h5&gt;依赖加载&lt;/h5&gt;
&lt;p&gt;@DependsOn&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;名称：@DependsOn&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类型：类注解、方法注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：控制 bean 的加载顺序，使其在指定 bean 加载完毕后再加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@DependsOn(&quot;beanId&quot;)
public class ClassName {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置在方法上，使 @DependsOn 指定的 bean 优先于 @Bean 配置的 bean 进行加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置在类上，使 @DependsOn 指定的 bean 优先于当前类中所有 @Bean 配置的 bean 进行加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置在类上，使 @DependsOn 指定的 bean 优先于 @Component 等配置的 bean 进行加载&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相关属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value（默认）：设置当前 bean 所依赖的 bean 的 id&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Order&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;名称：@Order&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类型：&lt;strong&gt;配置类注解&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：控制配置类的加载顺序，值越小越先加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Order(1)
public class SpringConfigClassName {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Lazy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;名称：@Lazy&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类型：类注解、方法注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：控制 bean 的加载时机，使其延迟加载，获取的时候加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Lazy
public class ClassName {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;应用场景&lt;/h5&gt;
&lt;p&gt;@DependsOn&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;微信订阅号，发布消息和订阅消息的 bean 的加载顺序控制（先开订阅，再发布）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双 11 活动，零点前是结算策略 A，零点后是结算策略 B，策略 B 操作的数据为促销数据，策略 B 加载顺序与促销数据的加载顺序&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Lazy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序灾难出现后对应的应急预案处理是启动容器时加载时机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@Order&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个种类的配置出现后，优先加载系统级的，然后加载业务级的，避免细粒度的加载控制&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;整合资源&lt;/h4&gt;
&lt;h5&gt;导入&lt;/h5&gt;
&lt;p&gt;名称：@Import&lt;/p&gt;
&lt;p&gt;类型：类注解&lt;/p&gt;
&lt;p&gt;作用：导入第三方 bean 作为 Spring 控制的资源，这些类都会被 Spring 创建并放入 ioc 容器&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@Import(OtherClassName.class)
public class ClassName {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Import 注解在同一个类上，仅允许添加一次，如果需要导入多个，使用数组的形式进行设定&lt;/li&gt;
&lt;li&gt;在被导入的类中可以继续使用 @Import 导入其他资源&lt;/li&gt;
&lt;li&gt;@Bean 所在的类可以使用导入的形式进入 Spring 容器，无需声明为 bean&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Druid&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加载资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class JDBCConfig {
    @Bean(&quot;dataSource&quot;)
    public static DruidDataSource getDataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(&quot;com.mysql.jdbc.Driver&quot;);
        ds.setUrl(&quot;jdbc:mysql://192.168.2.185:3306/spring_db&quot;);
        ds.setUsername(&quot;root&quot;);
        ds.setPassword(&quot;123456&quot;);
        return ds;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan(value = {&quot;service&quot;,&quot;dao&quot;})
@Import(JDBCConfig.class)
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DruidDataSource dataSource = (DruidDataSource) ctx.getBean(&quot;dataSource&quot;);
System.out.println(dataSource);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;Junit&lt;/h5&gt;
&lt;p&gt;Spring 接管 Junit 的运行权，使用 Spring 专用的 Junit 类加载器，为 Junit 测试用例设定对应的 Spring 容器&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从 Spring5.0 以后，要求 Junit 的版本必须是4.12及以上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Junit 仅用于单元测试，不能将 Junit 的测试类配置成 Spring 的 bean，否则该配置将会被打包进入工程中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;test / java / service / UserServiceTest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//设定spring专用的类加载器
@RunWith(SpringJUnit4ClassRunner.class)
//设定加载的spring上下文对应的配置
@ContextConfiguration(classes = SpringConfig.class)
public class UserServiceTest {
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById() {
        Account account = accountService.findById(1);
        Assert.assertEquals(&quot;Mike&quot;, account.getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-test&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;IoC原理&lt;/h3&gt;
&lt;h4&gt;核心类&lt;/h4&gt;
&lt;h5&gt;BeanFactory&lt;/h5&gt;
&lt;p&gt;ApplicationContext：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;ApplicationContext 是一个接口，提供了访问 Spring 容器的 API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ClassPathXmlApplicationContext 是一个类，实现了上述功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ApplicationContext 的顶层接口是 BeanFactory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanFactory 定义了 bean 相关的最基本操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ApplicationContext 在 BeanFactory 基础上追加了若干新功能&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;ApplicationContext 和 BeanFactory对比：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BeanFactory 和 ApplicationContext 是 Spring 的两大核心接口，都可以当做 Spring 的容器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanFactory 是 Spring 里面最底层的接口，是 IoC 的核心，定义了 IoC 的基本功能，包含了各种 Bean 的定义、加载、实例化，依赖注入和生命周期管理。ApplicationContext 接口作为 BeanFactory 的子类，除了提供 BeanFactory 所具有的功能外，还提供了更完整的框架功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;继承 MessageSource，因此支持国际化&lt;/li&gt;
&lt;li&gt;资源文件访问，如 URL 和文件（ResourceLoader）。&lt;/li&gt;
&lt;li&gt;载入多个（有继承关系）上下文（即加载多个配置文件） ，使得每一个上下文都专注于一个特定的层次，比如应用的 web 层&lt;/li&gt;
&lt;li&gt;提供在监听器中注册 bean 的事件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanFactory 创建的 bean 采用延迟加载形式，只有在使用到某个 Bean 时（调用 getBean），才对该 Bean 进行加载实例化（Spring 早期使用该方法获取 bean），这样就不能提前发现一些存在的 Spring 的配置问题；ApplicationContext 是在容器启动时，一次性创建了所有的 Bean，容器启动时，就可以发现 Spring 中存在的配置错误，这样有利于检查所依赖属性是否注入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ApplicationContext 启动后预载入所有的单实例 Bean，所以程序启动慢，运行时速度快&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两者都支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用，但两者之间的区别是：BeanFactory 需要手动注册，而 ApplicationContext 则是自动注册&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FileSystemXmlApplicationContext：加载文件系统中任意位置的配置文件，而 ClassPathXmlAC 只能加载类路径下的配置文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-ApplicationContext%E5%B1%82%E7%BA%A7%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BeanFactory 的成员属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String FACTORY_BEAN_PREFIX = &quot;&amp;amp;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;区分是 FactoryBean 还是创建的 Bean，加上 &amp;amp; 代表是工厂，getBean 将会返回工厂&lt;/li&gt;
&lt;li&gt;FactoryBean：如果某个 bean 的配置非常复杂，或者想要使用编码的形式去构建它，可以提供一个构建该 bean 实例的工厂，这个工厂就是 FactoryBean 接口实现类，FactoryBean 接口实现类也是需要 Spring 管理
&lt;ul&gt;
&lt;li&gt;这里产生两种对象，一种是 FactoryBean 接口实现类（IOC 管理），另一种是 FactoryBean 接口内部管理的对象&lt;/li&gt;
&lt;li&gt;获取 FactoryBean 接口实现类，使用 getBean 时传的 beanName 需要带 &amp;amp; 开头&lt;/li&gt;
&lt;li&gt;获取 FactoryBean 内部管理的对象，不需要带 &amp;amp; 开头&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BeanFactory 的基本使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Resource res = new ClassPathResource(&quot;applicationContext.xml&quot;);
BeanFactory bf = new XmlBeanFactory(res);
UserService userService = (UserService)bf.getBean(&quot;userService&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;FactoryBean&lt;/h5&gt;
&lt;p&gt;FactoryBean：对单一的 bean 的初始化过程进行封装，达到简化配置的目的&lt;/p&gt;
&lt;p&gt;FactoryBean与 BeanFactory 区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;FactoryBean：封装单个 bean 的创建过程，就是工厂的 Bean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanFactory：Spring 容器顶层接口，定义了 bean 相关的获取操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;FactoryBean，实现类一般是 MapperFactoryBean，创建 DAO 层接口的实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EquipmentDaoImplFactoryBean implements FactoryBean {
    @Override	//获取Bean
    public Object getObject() throws Exception {
        return new EquipmentDaoImpl();
    }
    
    @Override	//获取bean的类型
    public Class&amp;lt;?&amp;gt; getObjectType() {
        return null;
    }
    
    @Override	//是否单例
    public boolean isSingleton() {
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MapperFactoryBean 继承 SqlSessionDaoSupport，可以获取 SqlSessionTemplate，完成 MyBatis 的整合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SqlSessionDaoSupport extends DaoSupport {
  	private SqlSessionTemplate sqlSessionTemplate;
	// 获取 SqlSessionTemplate 对象
	public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    	if (this.sqlSessionTemplate == null || 
        	sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      		this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    	}
  	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;过滤器&lt;/h4&gt;
&lt;h5&gt;数据准备&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DAO 层 UserDao、AccountDao、BookDao、EquipmentDao&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDao {
	public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component(&quot;userDao&quot;)
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println(&quot;user dao running...&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service 业务层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Service(&quot;userService&quot;)
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;//...........BookDao等

    public void save() {
        System.out.println(&quot;user service running...&quot;);
        userDao.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;过滤器&lt;/h5&gt;
&lt;p&gt;名称：TypeFilter&lt;/p&gt;
&lt;p&gt;类型：&lt;strong&gt;接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：自定义类型过滤器&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;config / filter / MyTypeFilter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyTypeFilter implements TypeFilter {
    @Override
    /**
    * metadataReader:读取到的当前正在扫描的类的信息
    * metadataReaderFactory:可以获取到任何其他类的信息
    */
    //加载的类满足要求，匹配成功
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //获取当前类注解的信息
		AnnotationMetadata am = metadataReader.getAnnotationMetadata();
		//获取当前正在扫描的类的类信息
		ClassMetadata classMetadata = metadataReader.getClassMetadata();
		//获取当前类资源（类的路径）
		Resource resource = metadataReader.getResource();
        
        
        //通过类的元数据获取类的名称
        String className = classMetadata.getClassName();
        //如果加载的类名满足过滤器要求，返回匹配成功
        if(className.equals(&quot;service.impl.UserServiceImpl&quot;)){
       	//返回true表示匹配成功，返回false表示匹配失败。此处仅确认匹配结果，不会确认是排除还是加入，排除/加入由配置项决定，与此处无关
            return true;
        }
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SpringConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
//设置排除bean，排除的规则是自定义规则（FilterType.CUSTOM），具体的规则定义为MyTypeFilter
@ComponentScan(
        value = {&quot;dao&quot;,&quot;service&quot;},
        excludeFilters = @ComponentScan.Filter(
                type= FilterType.CUSTOM,
                classes = MyTypeFilter.class
        )
)
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;导入器&lt;/h4&gt;
&lt;p&gt;bean 只有通过配置才可以进入 Spring 容器，被 Spring 加载并控制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置 bean 的方式如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XML 文件中使用 &amp;lt;bean/&amp;gt; 标签配置&lt;/li&gt;
&lt;li&gt;使用 @Component 及衍生注解配置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;导入器可以快速高效导入大量 bean，替代 @Import({a.class,b.class})，无需在每个类上添加 @Bean&lt;/p&gt;
&lt;p&gt;名称： ImportSelector&lt;/p&gt;
&lt;p&gt;类型：&lt;strong&gt;接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：自定义bean导入器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;selector / MyImportSelector&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyImportSelector implements ImportSelector{
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//      1.编程形式加载一个类
//      return new String[]{&quot;dao.impl.BookDaoImpl&quot;};

//      2.加载import.properties文件中的单个类名
//      ResourceBundle bundle = ResourceBundle.getBundle(&quot;import&quot;);
//      String className = bundle.getString(&quot;className&quot;);

//      3.加载import.properties文件中的多个类名
        ResourceBundle bundle = ResourceBundle.getBundle(&quot;import&quot;);
        String className = bundle.getString(&quot;className&quot;);
        return className.split(&quot;,&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;import.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#2.加载import.properties文件中的单个类名
#className=dao.impl.BookDaoImpl

#3.加载import.properties文件中的多个类名
#className=dao.impl.BookDaoImpl,dao.impl.AccountDaoImpl

#4.导入包中的所有类
path=dao.impl.*
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SpringConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan({&quot;dao&quot;,&quot;service&quot;})
@Import(MyImportSelector.class)
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;注册器&lt;/h4&gt;
&lt;p&gt;可以取代 ComponentScan 扫描器&lt;/p&gt;
&lt;p&gt;名称：ImportBeanDefinitionRegistrar&lt;/p&gt;
&lt;p&gt;类型：&lt;strong&gt;接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用：自定义 bean 定义注册器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;registrar / MyImportBeanDefinitionRegistrar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
 * AnnotationMetadata:当前类的注解信息
 * BeanDefinitionRegistry:BeanDefinition注册类，把所有需要添加到容器中的bean调用registerBeanDefinition手工注册进来
 */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //自定义注册器
        //1.开启类路径bean定义扫描器，需要参数bean定义注册器BeanDefinitionRegistry，需要制定是否使用默认类型过滤器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry,false);
        //2.添加包含性加载类型过滤器（可选，也可以设置为排除性加载类型过滤器）
        scanner.addIncludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                //所有匹配全部成功，此处应该添加实际的业务判定条件
                return true;
            }
        });
        //设置扫描路径
        scanner.addExcludeFilter(tf);//排除
        scanner.scan(&quot;dao&quot;,&quot;service&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SpringConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;处理器&lt;/h4&gt;
&lt;p&gt;通过创建类&lt;strong&gt;继承相应的处理器的接口&lt;/strong&gt;，重写后置处理的方法，来实现&lt;strong&gt;拦截 Bean 的生命周期&lt;/strong&gt;来实现自己自定义的逻辑&lt;/p&gt;
&lt;p&gt;BeanPostProcessor：bean 后置处理器，bean 创建对象初始化前后进行拦截工作的&lt;/p&gt;
&lt;p&gt;BeanFactoryPostProcessor：beanFactory 的后置处理器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 加载时机：在 BeanFactory 初始化之后调用，来定制和修改 BeanFactory 的内容；所有的 bean 定义已经保存加载到 beanFactory，但是 bean 的实例还未创建
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  执行流程：
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;ioc 容器创建对象&lt;/li&gt;
&lt;li&gt;invokeBeanFactoryPostProcessors(beanFactory)：执行 BeanFactoryPostProcessor
&lt;ul&gt;
&lt;li&gt;在 BeanFactory 中找到所有类型是 BeanFactoryPostProcessor 的组件，并执行它们的方法&lt;/li&gt;
&lt;li&gt;在初始化创建其他组件前面执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BeanDefinitionRegistryPostProcessor：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加载时机：在所有 bean 定义信息将要被加载，但是 bean 实例还未创建，优先于 BeanFactoryPostProcessor 执行；利用 BeanDefinitionRegistryPostProcessor 给容器中再额外添加一些组件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ioc 容器创建对象&lt;/li&gt;
&lt;li&gt;refresh() → invokeBeanFactoryPostProcessors(beanFactory)&lt;/li&gt;
&lt;li&gt;从容器中获取到所有的 BeanDefinitionRegistryPostProcessor 组件
&lt;ul&gt;
&lt;li&gt;依次触发所有的 postProcessBeanDefinitionRegistry() 方法&lt;/li&gt;
&lt;li&gt;再来触发 postProcessBeanFactory() 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;监听器&lt;/h4&gt;
&lt;h5&gt;基本概述&lt;/h5&gt;
&lt;p&gt;ApplicationListener：监听容器中发布的事件，完成事件驱动模型开发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface ApplicationListener&amp;lt;E extends ApplicationEvent&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以监听 ApplicationEvent 及其下面的子事件&lt;/p&gt;
&lt;p&gt;应用监听器步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写一个监听器（ApplicationListener实现类）来监听某个事件（ApplicationEvent及其子类）&lt;/li&gt;
&lt;li&gt;把监听器加入到容器 @Component&lt;/li&gt;
&lt;li&gt;只要容器中有相关事件的发布，就能监听到这个事件；
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; ContextRefreshedEvent：容器刷新完成（所有 bean 都完全创建）会发布这个事件
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; ContextClosedEvent：关闭容器会发布这个事件
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;发布一个事件：&lt;code&gt;applicationContext.publishEvent()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class MyApplicationListener implements ApplicationListener&amp;lt;ApplicationEvent&amp;gt; {
	//当容器中发布此事件以后，方法触发
	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		System.out.println(&quot;收到事件：&quot; + event);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;p&gt;ContextRefreshedEvent 事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;容器初始化过程中执行 &lt;code&gt;initApplicationEventMulticaster()&lt;/code&gt;：初始化事件多播器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先去容器中查询 &lt;code&gt;id = applicationEventMulticaster&lt;/code&gt; 的组件，有直接返回&lt;/li&gt;
&lt;li&gt;没有就执行 &lt;code&gt;this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory)&lt;/code&gt; 并且加入到容器中&lt;/li&gt;
&lt;li&gt;以后在其他组件要派发事件，自动注入这个 applicationEventMulticaster&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器初始化过程执行 &lt;strong&gt;registerListeners()&lt;/strong&gt; 注册监听器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从容器中获取所有监听器：&lt;code&gt;getBeanNamesForType(ApplicationListener.class, true, false)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将 listener 注册到 ApplicationEventMulticaster&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器刷新完成：finishRefresh() → publishEvent(new ContextRefreshedEvent(this))&lt;/p&gt;
&lt;p&gt;发布 ContextRefreshedEvent 事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取事件的多播器（派发器）：getApplicationEventMulticaster()&lt;/li&gt;
&lt;li&gt;multicastEvent 派发事件
&lt;ul&gt;
&lt;li&gt;获取到所有的 ApplicationListener&lt;/li&gt;
&lt;li&gt;遍历 ApplicationListener
&lt;ul&gt;
&lt;li&gt;如果有 Executor，可以使用 Executor 异步派发 &lt;code&gt;Executor executor = getTaskExecutor()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;没有就同步执行 listener 方法 &lt;code&gt;invokeListener(listener, event)&lt;/code&gt;，拿到 listener 回调 onApplicationEvent&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器关闭会发布 ContextClosedEvent&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;注解实现&lt;/h5&gt;
&lt;p&gt;注解：@EventListener&lt;/p&gt;
&lt;p&gt;基本使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService{
    @EventListener(classes={ApplicationEvent.class})
	public void listen(ApplicationEvent event){
		System.out.println(&quot;UserService。。监听到的事件：&quot; + event);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理：使用 EventListenerMethodProcessor 处理器来解析方法上的 @EventListener，Spring 扫描使用注解的方法，并为之创建一个监听对象&lt;/p&gt;
&lt;p&gt;SmartInitializingSingleton 原理：afterSingletonsInstantiated()&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 	IOC 容器创建对象并 refresh()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 	finishBeanFactoryInitialization(beanFactory)：初始化剩下的单实例 bean
 * 先创建所有的单实例 bean：getBean()
 * 获取所有创建好的单实例 bean，判断是否是 SmartInitializingSingleton 类型的，如果是就调用 afterSingletonsInstantiated()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;AOP&lt;/h2&gt;
&lt;h3&gt;基本概述&lt;/h3&gt;
&lt;p&gt;AOP（Aspect Oriented Programing）：面向切面编程，一种编程&lt;strong&gt;范式&lt;/strong&gt;，指导开发者如何组织程序结构&lt;/p&gt;
&lt;p&gt;AOP 弥补了 OOP 的不足，基于 OOP 基础之上进行横向开发：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;uOOP 规定程序开发以类为主体模型，一切围绕对象进行，完成某个任务先构建模型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;uAOP 程序开发主要关注基于 OOP 开发中的共性功能，一切围绕共性功能进行，完成某个任务先构建可能遇到的所有共性功能（当所有功能都开发出来也就没有共性与非共性之分），将软件开发由手工制作走向半自动化/全自动化阶段，实现“插拔式组件体系结构”搭建&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AOP 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;提高代码的可重用性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务代码编码更简洁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务代码维护更高效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务功能扩展更便捷&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;核心概念&lt;/h3&gt;
&lt;h4&gt;概念详解&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Joinpoint（连接点）：就是方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pointcut（切入点）：就是挖掉共性功能的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Advice（通知）：就是共性功能，最终以一个方法的形式呈现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Aspect（切面）：就是共性功能与挖的位置的对应关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Target（目标对象）：就是挖掉功能的方法对应的类产生的对象，这种对象是无法直接完成最终工作的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Weaving（织入）：就是将挖掉的功能回填的动态过程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Proxy（代理）：目标对象无法直接完成工作，需要对其进行功能回填，通过创建原始对象的代理对象实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Introduction（引入/引介）：就是对原始对象无中生有的添加成员变量或成员方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E8%BF%9E%E6%8E%A5%E7%82%B9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E5%88%87%E5%85%A5%E7%82%B9%E5%88%87%E9%9D%A2%E9%80%9A%E7%9F%A5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E7%BB%87%E5%85%A5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;入门项目&lt;/h4&gt;
&lt;p&gt;开发步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开发阶段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;制作程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将非共性功能开发到对应的目标对象类中，并制作成切入点方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将共性功能独立开发出来，制作成通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在配置文件中，声明切入点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在配置文件中，声明切入点与通知间的关系（含通知类型），即切面&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行阶段（AOP 完成）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 容器加载配置文件，监控所有配置的&lt;strong&gt;切入点&lt;/strong&gt;方法的执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当监控到切入点方法被运行，&lt;strong&gt;使用代理机制，动态创建目标对象的代理对象，根据通知类别，在代理对象的对应位置将通知对应的功能织入&lt;/strong&gt;，完成完整的代码逻辑并运行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;导入坐标 pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;aspectjweaver&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.9.4&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层抽取通用代码  service / UserServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
    public void save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
    public void save() {
        //System.out.println(&quot;共性功能&quot;);
        System.out.println(&quot;user service running...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;aop.AOPAdvice&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1.制作通知类，在类中定义一个方法用于完成共性功能
public class AOPAdvice {
    //共性功能抽取后职称独立的方法
    public void function(){
        System.out.println(&quot;共性功能&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把通知加入spring容器管理，配置aop  applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;
       xsi:schemaLocation=&quot;
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        &quot;&amp;gt;
    &amp;lt;!--原始Spring控制资源--&amp;gt;
    &amp;lt;bean id=&quot;userService&quot; class= &quot;service.impl.UserServiceImpl&quot;/&amp;gt;
    &amp;lt;!--2.配置共性功能成功spring控制的资源--&amp;gt;
    &amp;lt;bean id=&quot;myAdvice&quot; class=&quot;aop.AOPAdvice&quot;/&amp;gt;
    &amp;lt;!--3.开启AOP命名空间: beans标签内--&amp;gt;
    &amp;lt;!--4.配置AOP--&amp;gt;
    &amp;lt;aop:config&amp;gt;
        &amp;lt;!--5.配置切入点--&amp;gt;
        &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *..*(..))&quot;/&amp;gt;
        &amp;lt;!--6.配置切面（切入点与通知的关系）--&amp;gt;
        &amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
            &amp;lt;!--7.配置具体的切入点对应通知中那个操作方法--&amp;gt;
            &amp;lt;aop:before method=&quot;function&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
        &amp;lt;/aop:aspect&amp;gt;
    &amp;lt;/aop:config&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
        userService.save();//先输出共性功能，然后 user service running...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;XML开发&lt;/h3&gt;
&lt;h4&gt;AspectJ&lt;/h4&gt;
&lt;p&gt;Aspect（切面）用于描述切入点与通知间的关系，是 AOP 编程中的一个概念&lt;/p&gt;
&lt;p&gt;AspectJ 是基于 java 语言对 Aspect 的实现&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;AOP&lt;/h4&gt;
&lt;h5&gt;config&lt;/h5&gt;
&lt;p&gt;标签：&lt;a&gt;aop:config&lt;/a&gt;，&amp;lt;beans&amp;gt; 的子标签&lt;/p&gt;
&lt;p&gt;作用：设置 AOP&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans&amp;gt;
    &amp;lt;aop:config&amp;gt;……&amp;lt;/aop:config&amp;gt;
    &amp;lt;aop:config&amp;gt;……&amp;lt;/aop:config&amp;gt;
    &amp;lt;!--一个beans标签中可以配置多个aop:config标签--&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;pointcut&lt;/h5&gt;
&lt;p&gt;标签：&lt;a&gt;aop:pointcut&lt;/a&gt;，归属于 aop:config 标签和 aop:aspect 标签&lt;/p&gt;
&lt;p&gt;作用：设置切入点&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:config&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pointcutId&quot; expression=&quot;……&quot;/&amp;gt;
    &amp;lt;aop:aspect&amp;gt;
        &amp;lt;aop:pointcut id=&quot;pointcutId&quot; expression=&quot;……&quot;/&amp;gt;
    &amp;lt;/aop:aspect&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 aop:config 标签中可以配置多个 aop:pointcut 标签，且该标签可以配置在 aop:aspect 标签内&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;id ：识别切入点的名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;expression ：切入点表达式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;aspect&lt;/h5&gt;
&lt;p&gt;标签：&lt;a&gt;aop:aspect&lt;/a&gt;，aop:config 的子标签&lt;/p&gt;
&lt;p&gt;作用：设置具体的 AOP 通知对应的切入点（切面）&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:config&amp;gt;
    &amp;lt;aop:aspect ref=&quot;beanId&quot;&amp;gt;……&amp;lt;/aop:aspect&amp;gt;
    &amp;lt;aop:aspect ref=&quot;beanId&quot;&amp;gt;……&amp;lt;/aop:aspect&amp;gt;
    &amp;lt;!--一个aop:config标签中可以配置多个aop:aspect标签--&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ref ：通知所在的 bean 的 id&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Pointcut&lt;/h4&gt;
&lt;h5&gt;切入点&lt;/h5&gt;
&lt;p&gt;切入点描述的是某个方法&lt;/p&gt;
&lt;p&gt;切入点表达式是一个快速匹配方法描述的通配格式，类似于正则表达式&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;表达式&lt;/h5&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;关键字(访问修饰符  返回值  包名.类名.方法名(参数)异常名)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//匹配UserService中只含有一个参数的findById方法
execution(public User service.UserService.findById(int))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键字：描述表达式的匹配模式（参看关键字列表）&lt;/li&gt;
&lt;li&gt;访问修饰符：方法的访问控制权限修饰符&lt;/li&gt;
&lt;li&gt;类名：方法所在的类（此处可以配置接口名称）&lt;/li&gt;
&lt;li&gt;异常：方法定义中指定抛出的异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;execution ：匹配执行指定方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;args ：匹配带有指定参数类型的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;within、this、target、@within、@target、@args、@annotation、bean、reference pointcut等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通配符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;*：单个独立的任意符号，可以独立出现，也可以作为前缀或者后缀的匹配符出现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//匹配com.seazean包下的任意包中的UserService类或接口中所有find开头的带有一个任意参数的方法
execution(public * com.seazean.*.UserService.find*(*)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;.. ：多个连续的任意符号，可以独立出现，常用于简化包名与参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//匹配com包下的任意包中的UserService类或接口中所有名称为findById参数任意数量和类型的方法
execution(public User com..UserService.findById(..))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;+：专用于匹配子类类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//匹配任意包下的Service结尾的类或者接口的子类或者实现类
execution(* *..*Service+.*(..))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;逻辑运算符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;amp;&amp;amp;：连接两个切入点表达式，表示两个切入点表达式同时成立的匹配&lt;/li&gt;
&lt;li&gt;||：连接两个切入点表达式，表示两个切入点表达式成立任意一个的匹配&lt;/li&gt;
&lt;li&gt;! ：连接单个切入点表达式，表示该切入点表达式不成立的匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;execution(* *(..))		//前三个都是匹配全部
execution(* *..*(..))
execution(* *..*.*(..))
execution(public * *..*.*(..))
execution(public int *..*.*(..))
execution(public void *..*.*(..))
execution(public void com..*.*(..)) 
execution(public void com..service.*.*(..))
execution(public void com.seazean.service.*.*(..))
execution(public void com.seazean.service.User*.*(..))
execution(public void com.seazean.service.*Service.*(..))
execution(public void com.seazean.service.UserService.*(..))
execution(public User com.seazean.service.UserService.find*(..))	//find开头
execution(public User com.seazean.service.UserService.*Id(..))		//I
execution(public User com.seazean.service.UserService.findById(..))
execution(public User com.seazean.service.UserService.findById(int))
execution(public User com.seazean.service.UserService.findById(int,int))
execution(public User com.seazean.service.UserService.findById(int,*))
execution(public User com.seazean.service.UserService.findById())
execution(List com.seazean.service.*Service+.findAll(..))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;配置方式&lt;/h5&gt;
&lt;p&gt;XML 配置规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;企业开发命名规范严格遵循规范文档进行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;先为方法配置局部切入点，再抽取类中公共切入点，最后抽取全局切入点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码走查过程中检测切入点是否存在越界性包含&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码走查过程中检测切入点是否存在非包含性进驻&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设定 AOP 执行检测程序，在单元测试中监控通知被执行次数与预计次数是否匹配（不绝对正确：加进一个不该加的，删去一个不该删的相当于结果不变）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设定完毕的切入点如果发生调整务必进行回归测试&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:config&amp;gt;
    &amp;lt;!--1.配置公共切入点--&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt1&quot; expression=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
        &amp;lt;!--2.配置局部切入点--&amp;gt;
        &amp;lt;aop:pointcut id=&quot;pt2&quot; expression=&quot;execution(* *(..))&quot;/&amp;gt;
        &amp;lt;!--引用公共切入点--&amp;gt;
        &amp;lt;aop:before method=&quot;logAdvice&quot; pointcut-ref=&quot;pt1&quot;/&amp;gt;
        &amp;lt;!--引用局部切入点--&amp;gt;
        &amp;lt;aop:before method=&quot;logAdvice&quot; pointcut-ref=&quot;pt2&quot;/&amp;gt;
        &amp;lt;!--3.直接配置切入点--&amp;gt;
        &amp;lt;aop:before method=&quot;logAdvice&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;/aop:aspect&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Advice&lt;/h4&gt;
&lt;h5&gt;通知类型&lt;/h5&gt;
&lt;p&gt;AOP 的通知类型共5种：前置通知，后置通知、返回后通知、抛出异常后通知、环绕通知&lt;/p&gt;
&lt;h6&gt;before&lt;/h6&gt;
&lt;p&gt;标签：&lt;a&gt;aop:before&lt;/a&gt;，aop:aspect的子标签&lt;/p&gt;
&lt;p&gt;作用：设置前置通知&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前置通知&lt;/strong&gt;：原始方法执行前执行，如果通知中抛出异常，阻止原始方法运行&lt;/li&gt;
&lt;li&gt;应用：数据校验&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;adviceId&quot;&amp;gt;
    &amp;lt;aop:before method=&quot;methodName&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;!--一个aop:aspect标签中可以配置多个aop:before标签--&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;method：在通知类中设置当前通知类别对应的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut：设置当前通知对应的切入点表达式，与pointcut-ref属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut-ref：设置当前通知对应的切入点id，与pointcut属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;after&lt;/h6&gt;
&lt;p&gt;标签：&lt;a&gt;aop:after&lt;/a&gt;，aop:aspect的子标签&lt;/p&gt;
&lt;p&gt;作用：设置后置通知&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;后置通知&lt;/strong&gt;：原始方法执行后执行，无论原始方法中是否出现异常，都将执行通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用：现场清理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;adviceId&quot;&amp;gt;
    &amp;lt;aop:after method=&quot;methodName&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;!--一个aop:aspect标签中可以配置多个aop:after标签--&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;method：在通知类中设置当前通知类别对应的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut：设置当前通知对应的切入点表达式，与pointcut-ref属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut-ref：设置当前通知对应的切入点id，与pointcut属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;after-r&lt;/h6&gt;
&lt;p&gt;标签：&lt;a&gt;aop:after-returning&lt;/a&gt;，aop:aspect的子标签&lt;/p&gt;
&lt;p&gt;作用：设置返回后通知&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;返回后通知&lt;/strong&gt;：原始方法正常执行完毕并返回结果后执行，如果原始方法中抛出异常，无法执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用：返回值相关数据处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;adviceId&quot;&amp;gt;
    &amp;lt;aop:after-returning method=&quot;methodName&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;!--一个aop:aspect标签中可以配置多个aop:after-returning标签--&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;method：在通知类中设置当前通知类别对应的方法&lt;/li&gt;
&lt;li&gt;pointcut：设置当前通知对应的切入点表达式，与pointcut-ref属性冲突&lt;/li&gt;
&lt;li&gt;pointcut-ref：设置当前通知对应的切入点id，与pointcut属性冲突&lt;/li&gt;
&lt;li&gt;returning：设置接受返回值的参数，与通知类中对应方法的参数一致&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;after-t&lt;/h6&gt;
&lt;p&gt;标签：&lt;a&gt;aop:after-throwing&lt;/a&gt;，aop:aspect的子标签&lt;/p&gt;
&lt;p&gt;作用：设置抛出异常后通知&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;抛出异常后通知&lt;/strong&gt;：原始方法抛出异常后执行，如果原始方法没有抛出异常，无法执行&lt;/li&gt;
&lt;li&gt;应用：对原始方法中出现的异常信息进行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;adviceId&quot;&amp;gt;
    &amp;lt;aop:after-throwing method=&quot;methodName&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;!--一个aop:aspect标签中可以配置多个aop:after-throwing标签--&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;method：在通知类中设置当前通知类别对应的方法&lt;/li&gt;
&lt;li&gt;pointcut：设置当前通知对应的切入点表达式，与pointcut-ref属性冲突&lt;/li&gt;
&lt;li&gt;pointcut-ref：设置当前通知对应的切入点id，与pointcut属性冲突&lt;/li&gt;
&lt;li&gt;throwing：设置接受异常对象的参数，与通知类中对应方法的参数一致&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;around&lt;/h6&gt;
&lt;p&gt;标签：&lt;a&gt;aop:around&lt;/a&gt;，aop:aspect的子标签&lt;/p&gt;
&lt;p&gt;作用：设置环绕通知&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环绕通知&lt;/strong&gt;：在原始方法执行前后均有对应执行执行，还可以阻止原始方法的执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用：功能强大，可以做任何事情&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;adviceId&quot;&amp;gt;
    &amp;lt;aop:around method=&quot;methodName&quot; pointcut=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;!--一个aop:aspect标签中可以配置多个aop:around标签--&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;method ：在通知类中设置当前通知类别对应的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut ：设置当前通知对应的切入点表达式，与pointcut-ref属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pointcut-ref ：设置当前通知对应的切入点id，与pointcut属性冲突&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;环绕通知的开发方式（参考通知顺序章节）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环绕通知是&lt;strong&gt;在原始方法的前后添加功能&lt;/strong&gt;，在环绕通知中，存在对原始方法的显式调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环绕通知方法相关说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法须设定 Object 类型的返回值，否则会&lt;strong&gt;拦截&lt;/strong&gt;原始方法的返回。如果原始方法返回值类型为 void，通知方法也可以设定返回值类型为 void，最终返回 null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法需在第一个参数位置设定 ProceedingJoinPoint 对象，通过该对象调用 proceed() 方法，实现&lt;strong&gt;对原始方法的调用&lt;/strong&gt;。如省略该参数，原始方法将无法执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 proceed() 方法调用原始方法时，因无法预知原始方法运行过程中是否会出现异常，强制抛出 Throwable 对象，封装原始方法中可能出现的异常信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;通知顺序&lt;/h5&gt;
&lt;p&gt;当同一个切入点配置了多个通知时，通知会存在运行的先后顺序，该顺序以通知配置的顺序为准。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AOPAdvice&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AOPAdvice {
    public void before(){
        System.out.println(&quot;before...);
    }
    public void after(){
        System.out.println(&quot;after...&quot;);
    }
    public void afterReturing(){
        System.out.println(&quot;afterReturing...&quot;);
    }
    public void afterThrowing(){
        System.out.println(&quot;afterThrowing...&quot;);
    }
    public Object around(ProceedingJoinPoint pjp) {
        System.out.println(&quot;around before...&quot;);
       	//对原始方法的调用
        Object ret = pjp.proceed();
        System.out.println(&quot;around after...&quot;+ret);
   	    return ret;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext.xml  &lt;strong&gt;顺序执行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:config&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *..*(..))&quot;/&amp;gt;
    &amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
		&amp;lt;aop:before method=&quot;before&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
        &amp;lt;aop:after method=&quot;after&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
        &amp;lt;aop:after-returning method=&quot;afterReturing&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
        &amp;lt;aop:after-throwing method=&quot;afterThrowing&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
        &amp;lt;aop:around method=&quot;around&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
    &amp;lt;/aop:aspect&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;获取数据&lt;/h5&gt;
&lt;h6&gt;参数&lt;/h6&gt;
&lt;p&gt;第一种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设定通知方法第一个参数为 JoinPoint，通过该对象调用 getArgs() 方法，获取原始方法运行的参数数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void before(JoinPoint jp) throws Throwable {
    Object[] args = jp.getArgs();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有的通知均可以获取参数，环绕通知使用ProceedingJoinPoint.getArgs()方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设定切入点表达式为通知方法传递参数（锁定通知变量名）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;流程图：&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E9%80%9A%E7%9F%A5%E8%8E%B7%E5%8F%96%E5%8F%82%E6%95%B0%E6%96%B9%E5%BC%8F%E4%BA%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;amp&lt;/code&gt; 代表并且 &amp;amp;&lt;/li&gt;
&lt;li&gt;输出结果：a = param1   b = param2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设定切入点表达式为通知方法传递参数（改变通知变量名的定义顺序）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;流程图：&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E9%80%9A%E7%9F%A5%E8%8E%B7%E5%8F%96%E5%8F%82%E6%95%B0%E6%96%B9%E5%BC%8F%E4%B8%89.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解释：输出结果 a = param2   b = param1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;返回值&lt;/h6&gt;
&lt;p&gt;环绕通知和返回后通知可以获取返回值，后置通知不一定，其他类型获取不到&lt;/p&gt;
&lt;p&gt;第一种方式：适用于返回后通知（after-returning）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设定返回值变量名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原始方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
    public int save() {
        System.out.println(&quot;user service running...&quot;);
        return 100;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOP 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *(..))&quot;/&amp;gt;
    &amp;lt;aop:after-returning method=&quot;afterReturning&quot; pointcut-ref=&quot;pt&quot; returning=&quot;ret&quot;/&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通知类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AOPAdvice {
    public void afterReturning(Object ret) {
        System.out.println(&quot;return:&quot; + ret);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二种：适用于环绕通知（around）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在通知类的方法中调用原始方法获取返回值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原始方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
    public int save() {
        System.out.println(&quot;user service running...&quot;);
        return 100;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOP 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *(..))  &quot;/&amp;gt;
    &amp;lt;aop:around method=&quot;around&quot; pointcut-ref=&quot;pt&quot; /&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通知类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AOPAdvice {    
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        return ret;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
		int ret = userService.save();
       	System.out.println(&quot;app.....&quot; + ret);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;异常&lt;/h6&gt;
&lt;p&gt;环绕通知和抛出异常后通知可以获取异常，后置通知不一定，其他类型获取不到&lt;/p&gt;
&lt;p&gt;第一种：适用于返回后通知（after-throwing）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设定异常对象变量名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原始方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
	public void save() {
        System.out.println(&quot;user service running...&quot;);
        int i = 1/0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOP 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
	&amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *(..))  &quot;/&amp;gt;
    &amp;lt;aop:after-throwing method=&quot;afterThrowing&quot; pointcut-ref=&quot;pt&quot; throwing=&quot;t&quot;/&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通知类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void afterThrowing(Throwable t){
    System.out.println(t.getMessage());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二种：适用于环绕通知（around）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在通知类的方法中调用原始方法捕获异常&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原始方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
	public void save() {
        System.out.println(&quot;user service running...&quot;);
        int i = 1/0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOP 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *(..))  &quot;/&amp;gt;
    &amp;lt;aop:around method=&quot;around&quot; pointcut-ref=&quot;pt&quot; /&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通知类：try……catch……捕获异常后，ret为null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();	//对此处调用进行try……catch……捕获异常，或抛出异常
    /* try {
            ret = pjp.proceed();
        } catch (Throwable throwable) {
            System.out.println(&quot;around exception...&quot; + throwable.getMessage());
        }*/
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userService.delete();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h6&gt;获取全部&lt;/h6&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UserService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
    public void save(int i, int m);

    public int update();

    public void delete();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImpl implements UserService {
    @Override
    public void save(int i, int m) {
        System.out.println(&quot;user service running...&quot; + i + &quot;,&quot; + m);
    }

    @Override
    public int update() {
        System.out.println(&quot;user service update running...&quot;);
        return 100;
    }

    @Override
    public void delete() {
        System.out.println(&quot;user service delete running...&quot;);
        int i = 1 / 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOPAdvice&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AOPAdvice {
    public void before(JoinPoint jp){
        //通过JoinPoint参数获取调用原始方法所携带的参数
        Object[] args = jp.getArgs();
        System.out.println(&quot;before...&quot;+args[0]);
    }

    public void after(JoinPoint jp){
        Object[] args = jp.getArgs();
        System.out.println(&quot;after...&quot;+args[0]);
    }

    public void afterReturing(Object ret){
        System.out.println(&quot;afterReturing...&quot;+ret);
    }

    public void afterThrowing(Throwable t){
        System.out.println(&quot;afterThrowing...&quot;+t.getMessage());
    }

    public Object around(ProceedingJoinPoint pjp) {
        System.out.println(&quot;around before...&quot;);
        Object ret = null;
        try {
            //对原始方法的调用
            ret = pjp.proceed();
        } catch (Throwable throwable) {
            System.out.println(&quot;around...exception....&quot;+throwable.getMessage());
        }
        System.out.println(&quot;around after...&quot;+ret);
        return ret;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;
       xsi:schemaLocation=&quot;
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        &quot;&amp;gt;
    &amp;lt;bean id=&quot;userService&quot; class=&quot;service.impl.UserServiceImpl&quot;/&amp;gt;
    &amp;lt;bean id=&quot;myAdvice&quot; class=&quot;aop.AOPAdvice&quot;/&amp;gt;

    &amp;lt;aop:config&amp;gt;
        &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *..*(..))&quot;/&amp;gt;
        &amp;lt;aop:aspect ref=&quot;myAdvice&quot;&amp;gt;
            &amp;lt;aop:before method=&quot;before&quot; pointcut=&quot;pt&quot;/&amp;gt;
            &amp;lt;aop:around method=&quot;around&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
            &amp;lt;aop:after method=&quot;after&quot; pointcut=&quot;pt&quot;/&amp;gt;
            &amp;lt;aop:after-returning method=&quot;afterReturning&quot; pointcut-ref=&quot;pt&quot; returning=&quot;ret&quot;/&amp;gt;
            &amp;lt;aop:after-throwing method=&quot;afterThrowing&quot; pointcut-ref=&quot;pt&quot; throwing=&quot;t&quot;/&amp;gt;
        &amp;lt;/aop:aspect&amp;gt;
    &amp;lt;/aop:config&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
        UserService userService = (UserService) ctx.getBean(&quot;userService&quot;);
//        userService.save(666, 888);
//        int ret = userService.update();
//        System.out.println(&quot;app.....&quot; + ret);
        userService.delete();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解开发&lt;/h3&gt;
&lt;h4&gt;AOP注解&lt;/h4&gt;
&lt;p&gt;AOP 注解简化 XML：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E6%B3%A8%E8%A7%A3%E5%BC%80%E5%8F%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;切入点最终体现为一个方法，无参无返回值，无实际方法体内容，但不能是抽象方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用切入点时必须使用方法调用名称，方法后面的 () 不能省略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切面类中定义的切入点只能在当前类中使用，如果想引用其他类中定义的切入点使用“类名.方法名()”引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在通知类型注解后添加参数，实现 XML 配置中的属性，例如 after-returning 后的 returning 性&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4&gt;启动注解&lt;/h4&gt;
&lt;h5&gt;XML&lt;/h5&gt;
&lt;p&gt;开启 AOP 注解支持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aop:aspectj-autoproxy/&amp;gt;
&amp;lt;context:component-scan base-package=&quot;aop,config,service&quot;/&amp;gt;&amp;lt;!--启动Spring扫描--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;导入坐标（伴随 spring-context 坐标导入已经依赖导入完成）&lt;/li&gt;
&lt;li&gt;开启 AOP 注解支持&lt;/li&gt;
&lt;li&gt;配置切面 @Aspect&lt;/li&gt;
&lt;li&gt;定义专用的切入点方法，并配置切入点 @Pointcut&lt;/li&gt;
&lt;li&gt;为通知方法配置通知类型及对应切入点 @Before&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;纯注解&lt;/h5&gt;
&lt;p&gt;注解：@EnableAspectJAutoProxy&lt;/p&gt;
&lt;p&gt;位置：Spring 注解配置类定义上方&lt;/p&gt;
&lt;p&gt;作用：设置当前类开启 AOP 注解驱动的支持，加载 AOP 注解&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan(&quot;com.seazean&quot;)
@EnableAspectJAutoProxy
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;基本注解&lt;/h4&gt;
&lt;h5&gt;Aspect&lt;/h5&gt;
&lt;p&gt;注解：@Aspect&lt;/p&gt;
&lt;p&gt;位置：类定义上方&lt;/p&gt;
&lt;p&gt;作用：设置当前类为切面类&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Aspect
public class AopAdvice {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Pointcut&lt;/h5&gt;
&lt;p&gt;注解：@Pointcut&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：使用当前方法名作为切入点引用名称&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Pointcut(&quot;execution(* *(..))&quot;)
public void pt() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：被修饰的方法忽略其业务功能，格式设定为无参无返回值的方法，方法体内空实现（非抽象）&lt;/p&gt;
&lt;h5&gt;Before&lt;/h5&gt;
&lt;p&gt;注解：@Before&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：标注当前方法作为前置通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Before(&quot;pt()&quot;)
public void before(JoinPoint joinPoint){
    //joinPoint.getArgs();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;strong&gt;多个参数时，JoinPoint参数一定要在第一位&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;After&lt;/h5&gt;
&lt;p&gt;注解：@After&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：标注当前方法作为后置通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@After(&quot;pt()&quot;)
public void after(){
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;AfterR&lt;/h5&gt;
&lt;p&gt;注解：@AfterReturning&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：标注当前方法作为返回后通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AfterReturning(value=&quot;pt()&quot;, returning = &quot;result&quot;)
public void afterReturning(Object result) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特殊参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;returning ：设定使用通知方法参数&lt;strong&gt;接收&lt;/strong&gt;返回值的变量名&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;AfterT&lt;/h5&gt;
&lt;p&gt;注解：@AfterThrowing&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：标注当前方法作为异常后通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AfterThrowing(value=&quot;pt()&quot;, throwing = &quot;t&quot;)
public void afterThrowing(Throwable t){
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特殊参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;throwing ：设定使用通知方法参数接收原始方法中抛出的异常对象名&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Around&lt;/h5&gt;
&lt;p&gt;注解：@Around&lt;/p&gt;
&lt;p&gt;位置：方法定义上方&lt;/p&gt;
&lt;p&gt;作用：标注当前方法作为环绕通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Around(&quot;pt()&quot;)
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;执行顺序&lt;/h4&gt;
&lt;p&gt;AOP 使用 XML 配置情况下，通知的执行顺序由配置顺序决定，在注解情况下由于不存在配置顺序的概念，参照通知所配置的&lt;strong&gt;方法名字符串对应的编码值顺序&lt;/strong&gt;，可以简单理解为字母排序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同一个通知类中，相同通知类型以方法名排序为准&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Before(&quot;aop.AOPPointcut.pt()&quot;)
public void aop001Log(){}

@Before(&quot;aop.AOPPointcut.pt()&quot;)
public void aop002Exception(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不同通知类中，以类名排序为准&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 @Order 注解通过变更 bean 的加载顺序改变通知的加载顺序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Aspect
@Order(1)  //先执行
public class AOPAdvice2 {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Aspect
@Order(2) 
public class AOPAdvice1 {//默认执行此通知
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;AOP 原理&lt;/h3&gt;
&lt;h4&gt;静态代理&lt;/h4&gt;
&lt;p&gt;装饰者模式（Decorator Pattern）：在不惊动原始设计的基础上，为其添加功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceDecorator implements UserService{
    private UserService userService;
    
    public UserServiceDecorator(UserService userService) {
        this.userService = userService;
    }
    
    public void save() {
        //原始调用
        userService.save();
        //增强功能（后置）
        System.out.println(&quot;后置增强功能&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Proxy&lt;/h4&gt;
&lt;p&gt;JDKProxy 动态代理是针对对象做代理，要求原始对象具有接口实现，并对接口方法进行增强，因为&lt;strong&gt;代理类继承Proxy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;静态代理和动态代理的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来&lt;/li&gt;
&lt;li&gt;动态代理是程序在运行后通过反射创建字节码文件交由 JVM 加载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceJDKProxy {
    public static UserService createUserServiceJDKProxy(UserService userService) {
        UserService service = (UserService) Proxy.newProxyInstance(
            userService.getClass().getClassLoader(),//获取被代理对象的类加载器
            userService.getClass().getInterfaces(),	//获取被代理对象实现的接口
            new InvocationHandler() {				//对原始方法执行进行拦截并增强
				@Override
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getName().equals(&quot;save&quot;)) {
                        System.out.println(&quot;前置增强&quot;);
                        Object ret = method.invoke(userService, args);
                        System.out.println(&quot;后置增强&quot;);
                        return ret;
                    }
                    return null;
				}
 			});
        return service;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;CGLIB&lt;/h4&gt;
&lt;p&gt;CGLIB（Code Generation Library）：Code 生成类库&lt;/p&gt;
&lt;p&gt;CGLIB 特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CGLIB 动态代理&lt;strong&gt;不限定&lt;/strong&gt;是否具有接口，可以对任意操作进行增强&lt;/li&gt;
&lt;li&gt;CGLIB 动态代理无需要原始被代理对象，动态创建出新的代理对象&lt;/li&gt;
&lt;li&gt;CGLIB &lt;strong&gt;继承被代理类&lt;/strong&gt;，如果代理类是 final 则不能实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86-cglib.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CGLIB 类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDKProxy 仅对接口方法做增强，CGLIB 对所有方法做增强，包括 Object 类中的方法（toString、hashCode）&lt;/li&gt;
&lt;li&gt;返回值类型采用多态向下转型，所以需要设置父类类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要对方法进行判断是否是 save，来选择性增强&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceImplCglibProxy {
    public static UserService createUserServiceCglibProxy(Class cls){
        //1.创建Enhancer对象（可以理解为内存中动态创建了一个类的字节码）
        Enhancer enhancer = new Enhancer();
        
        //2.设置Enhancer对象的父类是指定类型UserServerImpl
        enhancer.setSuperclass(cls);
        
        //3.设置回调方法
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method m, Object[] args, MethodProxy mp) throws Throwable {
                //o是被代理出的类创建的对象，所以使用MethodProxy调用，并且是调用父类
                //通过调用父类的方法实现对原始方法的调用
                Object ret = methodProxy.invokeSuper(o, args);
                //后置增强内容,需要判断是都是save方法
                if (method.getName().equals(&quot;save&quot;)) {
                    System.out.println(&quot;I love Java&quot;);
                }
                return ret;
            }
        });
        //使用Enhancer对象创建对应的对象
        return (UserService)enhancer.create();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        UserService userService = UserServiceCglibProxy.createUserServiceCglibProxy(UserServiceImpl.class);
        userService.save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;代理选择&lt;/h4&gt;
&lt;p&gt;Spirng 可以通过配置的形式控制使用的代理形式，Spring 会先判断是否实现了接口，如果实现了接口就使用 JDK 动态代理，如果没有实现接口则使用 CGLIB 动态代理，通过配置可以修改为使用 CGLIB&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;XML 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--XML配置AOP--&amp;gt;
&amp;lt;aop:config proxy-target-class=&quot;false&quot;&amp;gt;&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;XML 注解支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--注解配置AOP--&amp;gt;
&amp;lt;aop:aspectj-autoproxy proxy-target-class=&quot;false&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注解驱动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//修改为使用 cglib 创建代理对象
@EnableAspectJAutoProxy(proxyTargetClass = true)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 动态代理和 CGLIB 动态代理的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK 动态代理只能对实现了接口的类生成代理，没有实现接口的类不能使用。&lt;/li&gt;
&lt;li&gt;CGLIB 动态代理即使被代理的类没有实现接口也可以使用，因为 CGLIB 动态代理是使用继承被代理类的方式进行扩展&lt;/li&gt;
&lt;li&gt;CGLIB 动态代理是通过继承的方式，覆盖被代理类的方法来进行代理，所以如果方法是被 final 修饰的话，就不能进行代理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;织入时机&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP%E7%BB%87%E5%85%A5%E6%97%B6%E6%9C%BA.png&quot; alt=&quot;AOP织入时机&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;事务&lt;/h2&gt;
&lt;h3&gt;事务机制&lt;/h3&gt;
&lt;h4&gt;事务介绍&lt;/h4&gt;
&lt;p&gt;事务：数据库中多个操作合并在一起形成的操作序列，事务特征（ACID）&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当数据库操作序列中个别操作失败时，提供一种方式使数据库状态恢复到正常状态（&lt;strong&gt;A&lt;/strong&gt;），保障数据库即使在异常状态下仍能保持数据一致性（&lt;strong&gt;C&lt;/strong&gt;）（要么操作前状态，要么操作后状态）&lt;/li&gt;
&lt;li&gt;当出现并发访问数据库时，在多个访问间进行相互隔离，防止并发访问操作结果互相干扰（&lt;strong&gt;I&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring 事务一般加到业务层，对应着业务的操作，Spring 事务的本质其实就是数据库对事务的支持，没有数据库的事务支持，Spring 是无法提供事务功能的，Spring 只提供统一事务管理接口&lt;/p&gt;
&lt;p&gt;Spring 在事务开始时，根据当前环境中设置的隔离级别，调整数据库隔离级别，由此保持一致。程序是否支持事务首先取决于数据库 ，比如 MySQL ，如果是 &lt;strong&gt;Innodb 引擎&lt;/strong&gt;，是支持事务的；如果 MySQL 使用 MyISAM 引擎，那从根上就是不支持事务的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;保证原子性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要保证事务的原子性，就需要在异常发生时，对已经执行的操作进行&lt;strong&gt;回滚&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在 MySQL 中，恢复机制是通过&lt;strong&gt;回滚日志（undo log）&lt;/strong&gt; 实现，所有事务进行的修改都会先先记录到这个回滚日志中，然后再执行相关的操作。如果执行过程中遇到异常的话，直接利用回滚日志中的信息将数据回滚到修改之前的样子即可&lt;/li&gt;
&lt;li&gt;回滚日志会先于数据持久化到磁盘上，这样保证了即使遇到数据库突然宕机等情况，当用户再次启动数据库的时候，数据库还能够通过查询回滚日志来回滚将之前未完成的事务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;隔离级别&lt;/h4&gt;
&lt;p&gt;TransactionDefinition 接口中定义了五个表示隔离级别的常量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TransactionDefinition.ISOLATION_DEFAULT：使用后端数据库默认的隔离级别，MySQL 默认采用的 REPEATABLE_READ 隔离级别，Oracle 默认采用的 READ_COMMITTED隔离级别.&lt;/li&gt;
&lt;li&gt;TransactionDefinition.ISOLATION_READ_UNCOMMITTED：最低的隔离级别，允许读取尚未提交的数据变更，可能会导致脏读、幻读或不可重复读&lt;/li&gt;
&lt;li&gt;TransactionDefinition.ISOLATION_READ_COMMITTED：允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生&lt;/li&gt;
&lt;li&gt;TransactionDefinition.ISOLATION_REPEATABLE_READ：对同一字段的多次读取结果都是一致的，除非数据是被本身事务自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生。&lt;/li&gt;
&lt;li&gt;TransactionDefinition.ISOLATION_SERIALIZABLE：最高的隔离级别，完全服从 ACID 的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MySQL InnoDB 存储引擎的默认支持的隔离级别是 &lt;strong&gt;REPEATABLE-READ（可重读）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分布式事务&lt;/strong&gt;：允许多个独立的事务资源（transactional resources）参与到一个全局的事务中。事务资源通常是关系型数据库系统，但也可以是其他类型的资源，全局事务要求在其中的所有参与的事务要么都提交，要么都回滚，这对于事务原有的 ACID 要求又有了提高&lt;/p&gt;
&lt;p&gt;在使用分布式事务时，InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;传播行为&lt;/h4&gt;
&lt;p&gt;事务传播行为是为了解决业务层方法之间互相调用的事务问题，也就是方法嵌套：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当事务方法被另一个事务方法调用时，必须指定事务应该如何传播。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：方法可能继续在现有事务中运行，也可能开启一个新事务，并在自己的事务中运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//外层事务 Service A 的 aMethod 调用内层 Service B 的 bMethod
class A {
    @Transactional(propagation=propagation.xxx)
    public void aMethod {
        B b = new B();
        b.bMethod();
    }
}
class B {
    @Transactional(propagation=propagation.xxx)
    public void bMethod {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;支持当前事务&lt;/strong&gt;的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_REQUIRED： 如果当前存在事务则&lt;strong&gt;加入该事务&lt;/strong&gt;；如果当前没有事务则创建一个新的事务
&lt;ul&gt;
&lt;li&gt;内外层是相同的事务，在 aMethod 或者在 bMethod 内的任何地方出现异常，事务都会被回滚&lt;/li&gt;
&lt;li&gt;工作流程：
&lt;ul&gt;
&lt;li&gt;线程执行到 serviceA.aMethod() 时，其实是执行的代理 serviceA 对象的 aMethod&lt;/li&gt;
&lt;li&gt;首先执行事务增强器逻辑（环绕增强），提取事务标签属性，检查当前线程是否绑定 connection 数据库连接资源，没有就调用 datasource.getConnection()，设置事务提交为手动提交 autocommit(false)&lt;/li&gt;
&lt;li&gt;执行其他增强器的逻辑，然后调用 target 的目标方法 aMethod() 方法，进入 serviceB 的逻辑&lt;/li&gt;
&lt;li&gt;serviceB 也是先执行事务增强器的逻辑，提取事务标签属性，但此时会检查到线程绑定了 connection，检查注解的传播属性，所以调用 DataSourceUtils.getConnection(datasource) 共享该连接资源，执行完相关的增强和 SQL 后，发现事务并不是当前方法开启的，可以直接返回上层&lt;/li&gt;
&lt;li&gt;serviceA.aMethod() 继续执行，执行完增强后进行提交事务或回滚事务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_SUPPORTS： 如果当前存在事务，则&lt;strong&gt;加入该事务&lt;/strong&gt;；如果当前没有事务，则以非事务的方式继续运行&lt;/li&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_MANDATORY： 如果当前存在事务，则&lt;strong&gt;加入该事务&lt;/strong&gt;；如果当前没有事务，则抛出异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不支持当前事务&lt;/strong&gt;的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_REQUIRES_NEW： 创建一个新的事务，如果当前存在事务，则把当前事务挂起
&lt;ul&gt;
&lt;li&gt;内外层是不同的事务，如果 bMethod 已经提交，如果 aMethod 失败回滚 ，bMethod 不会回滚&lt;/li&gt;
&lt;li&gt;如果 bMethod 失败回滚，ServiceB 抛出的异常被 ServiceA 捕获，如果 B 抛出的异常是 A 会回滚的异常，aMethod 事务需要回滚，否则仍然可以提交&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_NOT_SUPPORTED： &lt;strong&gt;以非事务方式运行&lt;/strong&gt;，如果当前存在事务，则把当前事务挂起&lt;/li&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_NEVER： &lt;strong&gt;以非事务方式运行&lt;/strong&gt;，如果当前存在事务，则抛出异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TransactionDefinition.PROPAGATION_NESTED： 如果当前存在事务，则创建一个事务作为当前事务的嵌套事务（两个事务没有关系）来运行
&lt;ul&gt;
&lt;li&gt;如果 ServiceB 异常回滚，可以通过 try-catch 机制执行 ServiceC&lt;/li&gt;
&lt;li&gt;如果 ServiceB 提交， ServiceA 可以根据具体的配置决定是 commit 还是 rollback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：在查询数据的时候要向数据库中存储一些日志，系统不希望存日志的行为影响到主逻辑，可以使用该传播&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;requied：必须的、supports：支持的、mandatory：强制的、nested：嵌套的&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;超时属性&lt;/h4&gt;
&lt;p&gt;事务超时，指一个事务所允许执行的最长时间，如果超过该时间限制事务还没有完成，则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间，其单位是秒，默认值为 -1&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;只读属性&lt;/h4&gt;
&lt;p&gt;对于只有读取数据查询的事务，可以指定事务类型为 readonly，即只读事务；只读事务不涉及数据的修改，数据库会提供一些优化手段，适合用在有多条数据库查询操作的方法中&lt;/p&gt;
&lt;p&gt;读操作为什么需要启用事务支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL  默认对每一个新建立的连接都启用了 &lt;code&gt;autocommit&lt;/code&gt; 模式，在该模式下，每一个发送到 MySQL 服务器的 SQL 语句都会在一个&lt;strong&gt;单独&lt;/strong&gt;的事务中进行处理，执行结束后会自动提交事务，并开启一个新的事务&lt;/li&gt;
&lt;li&gt;执行多条查询语句，如果方法加上了 &lt;code&gt;@Transactional&lt;/code&gt; 注解，这个方法执行的所有 SQL 会被放在一个事务中，如果声明了只读事务的话，数据库就会去优化它的执行，并不会带来其他的收益。如果不加 &lt;code&gt;@Transactional&lt;/code&gt;，每条 SQL 会开启一个单独的事务，中间被其它事务修改了数据，比如在前条 SQL 查询之后，后条 SQL 查询之前，数据被其他用户改变，则这次整体的统计查询将会出&lt;strong&gt;现读数据不一致的状态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;核心对象&lt;/h3&gt;
&lt;h4&gt;事务对象&lt;/h4&gt;
&lt;p&gt;J2EE 开发使用分层设计的思想进行，对于简单的业务层转调数据层的单一操作，事务开启在业务层或者数据层并无太大差别，当业务中包含多个数据层的调用时，需要在业务层开启事务，对数据层中多个操作进行组合并归属于同一个事务进行处理&lt;/p&gt;
&lt;p&gt;Spring 为业务层提供了整套的事务解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PlatformTransactionManager&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TransactionDefinition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TransactionStatus&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;PTM&lt;/h4&gt;
&lt;p&gt;PlatformTransactionManager，平台事务管理器实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DataSourceTransactionManager  适用于 Spring JDBC 或 MyBatis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HibernateTransactionManager  适用于 Hibernate3.0 及以上版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JpaTransactionManager  适用于 JPA&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JdoTransactionManager  适用于 JDO&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JtaTransactionManager  适用于 JTA&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;管理器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JPA（Java Persistence API）Java EE 标准之一，为 POJO 提供持久化标准规范，并规范了持久化开发的统一 API，符合 JPA 规范的开发可以在不同的 JPA 框架下运行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;非持久化一个字段&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static String transient1; // not persistent because of static
final String transient2 = “Satish”; // not persistent because of final
transient String transient3; // not persistent because of transient
@Transient
String transient4; // not persistent because of @Transient
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDO（Java Data Object）是 Java 对象持久化规范，用于存取某种数据库中的对象，并提供标准化 API。JDBC 仅针对关系数据库进行操作，JDO 可以扩展到关系数据库、XML、对象数据库等，可移植性更强&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JTA（Java Transaction API）Java EE 标准之一，允许应用程序执行分布式事务处理。与 JDBC 相比，JDBC 事务则被限定在一个单一的数据库连接，而一个 JTA 事务可以有多个参与者，比如 JDBC 连接、JDO 都可以参与到一个 JTA 事务中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此接口定义了事务的基本操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TransactionStatus getTransaction(TransactionDefinition definition)&lt;/td&gt;
&lt;td&gt;获取事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void commit(TransactionStatus status)&lt;/td&gt;
&lt;td&gt;提交事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void rollback(TransactionStatus status)&lt;/td&gt;
&lt;td&gt;回滚事务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;Definition&lt;/h4&gt;
&lt;p&gt;TransactionDefinition 此接口定义了事务的基本信息：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String getName()&lt;/td&gt;
&lt;td&gt;获取事务定义名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isReadOnly()&lt;/td&gt;
&lt;td&gt;获取事务的读写属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getIsolationLevel()&lt;/td&gt;
&lt;td&gt;获取事务隔离级别&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getTimeout()&lt;/td&gt;
&lt;td&gt;获取事务超时时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int getPropagationBehavior()&lt;/td&gt;
&lt;td&gt;获取事务传播行为特征&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;Status&lt;/h4&gt;
&lt;p&gt;TransactionStatus 此接口定义了事务在执行过程中某个时间点上的状态信息及对应的状态操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;boolean isNewTransaction()&lt;/td&gt;
&lt;td&gt;获取事务是否处于新开始事务状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;voin flush()&lt;/td&gt;
&lt;td&gt;刷新事务状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isCompleted()&lt;/td&gt;
&lt;td&gt;获取事务是否处于已完成状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean hasSavepoint()&lt;/td&gt;
&lt;td&gt;获取事务是否具有回滚储存点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isRollbackOnly()&lt;/td&gt;
&lt;td&gt;获取事务是否处于回滚状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;void setRollbackOnly()&lt;/td&gt;
&lt;td&gt;设置事务处于回滚状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;编程式&lt;/h3&gt;
&lt;h4&gt;控制方式&lt;/h4&gt;
&lt;p&gt;编程式、声明式（XML）、声明式（注解）&lt;/p&gt;
&lt;h4&gt;环境准备&lt;/h4&gt;
&lt;p&gt;银行转账业务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;包装类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;
    .....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DAO层接口：AccountDao&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AccountDao {
    //入账操作	name:入账用户名	money:入账金额
    void inMoney(@Param(&quot;name&quot;) String name, @Param(&quot;money&quot;) Double money);

    //出账操作	name:出账用户名	money:出账金额
    void outMoney(@Param(&quot;name&quot;) String name, @Param(&quot;money&quot;) Double money);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层接口提供转账操作：AccountService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AccountService {
	//转账操作	outName:出账用户名	inName:入账用户名	money:转账金额
	public void transfer(String outName,String inName,Double money);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层实现提供转账操作：AccountServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String outName,String inName,Double money){
		accountDao.inMoney(outName,money);
        accountDao.outMoney(inName,money);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;映射配置文件：dao / AccountDao.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;dao.AccountDao&quot;&amp;gt;
    &amp;lt;update id=&quot;inMoney&quot;&amp;gt;
        UPDATE account SET money = money + #{money} WHERE name = #{name}
    &amp;lt;/update&amp;gt;

    &amp;lt;update id=&quot;outMoney&quot;&amp;gt;
        UPDATE account SET money = money - #{money} WHERE name = #{name}
    &amp;lt;/update&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jdbc.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.2.185:3306/spring_db
jdbc.username=root
jdbc.password=1234
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心配置文件：applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:property-placeholder location=&quot;classpath:*.properties&quot;/&amp;gt;

&amp;lt;bean id=&quot;dataSource&quot; class=&quot;com.alibaba.druid.pool.DruidDataSource&quot;&amp;gt;
    &amp;lt;property name=&quot;driverClassName&quot; value=&quot;${jdbc.driver}&quot;/&amp;gt;
    &amp;lt;property name=&quot;url&quot; value=&quot;${jdbc.url}&quot;/&amp;gt;
    &amp;lt;property name=&quot;username&quot; value=&quot;${jdbc.username}&quot;/&amp;gt;
    &amp;lt;property name=&quot;password&quot; value=&quot;${jdbc.password}&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean id=&quot;accountService&quot; class=&quot;service.impl.AccountServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;accountDao&quot; ref=&quot;accountDao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;bean class=&quot;org.mybatis.spring.SqlSessionFactoryBean&quot;&amp;gt;
    &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;/&amp;gt;
    &amp;lt;property name=&quot;typeAliasesPackage&quot; value=&quot;domain&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!--扫描映射配置和Dao--&amp;gt;
&amp;lt;bean class=&quot;org.mybatis.spring.mapper.MapperScannerConfigurer&quot;&amp;gt;
    &amp;lt;property name=&quot;basePackage&quot; value=&quot;dao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;ap...xml&quot;);
AccountService accountService = (AccountService) ctx.getBean(&quot;accountService&quot;);
accountService.transfer(&quot;Jock1&quot;, &quot;Jock2&quot;, 100d);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;编程式&lt;/h4&gt;
&lt;p&gt;编程式事务就是代码显式的给出事务的开启和提交&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改业务层实现提供转账操作：AccountServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void transfer(String outName,String inName,Double money){
    //1.创建事务管理器，
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    //2.为事务管理器设置与数据层相同的数据源
    dstm.setDataSource(dataSource);
    //3.创建事务定义对象
    TransactionDefinition td = new DefaultTransactionDefinition();
    //4.创建事务状态对象，用于控制事务执行，【开启事务】
    TransactionStatus ts = dstm.getTransaction(td);
    accountDao.inMoney(inName,money);
    int i = 1/0;    //模拟业务层事务过程中出现错误
    accountDao.outMoney(outName,money);
    //5.提交事务
    dstm.commit(ts);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--添加属性注入--&amp;gt;
&amp;lt;bean id=&quot;accountService&quot; class=&quot;service.impl.AccountServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;accountDao&quot; ref=&quot;accountDao&quot;/&amp;gt;
    &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;AOP改造&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将业务层的事务处理功能抽取出来制作成 AOP 通知，利用环绕通知运行期动态织入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TxAdvice {
    private DataSource dataSource;
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Object tx(ProceedingJoinPoint pjp) throws Throwable {
        //开启事务
        PlatformTransactionManager ptm = new DataSourceTransactionManager(dataSource);
        //事务定义
        TransactionDefinition td = new DefaultTransactionDefinition();
        //事务状态
        TransactionStatus ts =  ptm.getTransaction(td);
        //pjp.getArgs()标准写法，也可以不加，同样可以传递参数
        Object ret = pjp.proceed(pjp.getArgs());
        
        //提交事务
        ptm.commit(ts);

        return ret;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 applicationContext.xml，要开启 AOP 空间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--修改bean的属性注入--&amp;gt;
&amp;lt;bean id=&quot;accountService&quot; class=&quot;service.impl.AccountServiceImpl&quot;&amp;gt;
    &amp;lt;property name=&quot;accountDao&quot; ref=&quot;accountDao&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;!--配置AOP通知类，并注入dataSource--&amp;gt;
&amp;lt;bean id=&quot;txAdvice&quot; class=&quot;aop.TxAdvice&quot;&amp;gt;
    &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;!--使用环绕通知将通知类织入到原始业务对象执行过程中--&amp;gt;
&amp;lt;aop:config&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* *..transfer(..))&quot;/&amp;gt;
    &amp;lt;aop:aspect ref=&quot;txAdvice&quot;&amp;gt;
        &amp;lt;aop:around method=&quot;tx&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
    &amp;lt;/aop:aspect&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改业务层实现提供转账操作：AccountServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String outName,String inName,Double money){
		accountDao.inMoney(outName,money);
        //int i = 1 / 0;
        accountDao.outMoney(inName,money);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;声明式&lt;/h3&gt;
&lt;h4&gt;XML&lt;/h4&gt;
&lt;h5&gt;tx使用&lt;/h5&gt;
&lt;p&gt;删除 TxAdvice 通知类，开启 tx 命名空间，配置 applicationContext.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置平台事务管理器--&amp;gt;
&amp;lt;bean id=&quot;txManager&quot; class=&quot;org.springframework.jdbc.datasource.DataSourceTransactionManager&quot;&amp;gt;
    &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;!--定义事务管理的通知类--&amp;gt;
&amp;lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;txManager&quot;&amp;gt;
    &amp;lt;!--定义控制的事务--&amp;gt;
    &amp;lt;tx:attributes&amp;gt;
        &amp;lt;tx:method name=&quot;transfer&quot; read-only=&quot;false&quot;/&amp;gt;
    &amp;lt;/tx:attributes&amp;gt;
&amp;lt;/tx:advice&amp;gt;

&amp;lt;!--使用aop:advisor在AOP配置中引用事务专属通知类，底层invoke调用--&amp;gt;
&amp;lt;aop:config&amp;gt;
    &amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* service.*Service.*(..))&quot;/&amp;gt;
    &amp;lt;aop:advisor advice-ref=&quot;txAdvice&quot; pointcut-ref=&quot;pt&quot;/&amp;gt;
&amp;lt;/aop:config&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;aop:advice 与 aop:advisor 区别
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;aop:advice 配置的通知类可以是普通 Java 对象，不实现接口，也不使用继承关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;aop:advisor 配置的通知类必须实现通知接口，底层 invoke 调用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MethodBeforeAdvice&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AfterReturningAdvice&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ThrowsAdvice&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pom.xml 文件引入依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-tx&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;tx配置&lt;/h5&gt;
&lt;h6&gt;advice&lt;/h6&gt;
&lt;p&gt;标签：tx:advice，beans 的子标签&lt;/p&gt;
&lt;p&gt;作用：专用于声明事务通知&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans&amp;gt;
    &amp;lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;txManager&quot;&amp;gt;
    &amp;lt;/tx:advice&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：用于配置 aop 时指定通知器的 id&lt;/li&gt;
&lt;li&gt;transaction-manager：指定事务管理器 bean&lt;/li&gt;
&lt;/ul&gt;
&lt;h6&gt;attributes&lt;/h6&gt;
&lt;p&gt;类型：tx:attributes，tx:advice 的子标签&lt;/p&gt;
&lt;p&gt;作用：定义通知属性&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;txManager&quot;&amp;gt;
    &amp;lt;tx:attributes&amp;gt;
    &amp;lt;/tx:attributes&amp;gt;
&amp;lt;/tx:advice&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;method&lt;/h6&gt;
&lt;p&gt;标签：tx:method，tx:attribute 的子标签&lt;/p&gt;
&lt;p&gt;作用：设置具体的事务属性&lt;/p&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;tx:attributes&amp;gt;
    &amp;lt;!--标准格式--&amp;gt;
    &amp;lt;tx:method name=&quot;*&quot; read-only=&quot;false&quot;/&amp;gt;
    &amp;lt;tx:method name=&quot;get*&quot; read-only=&quot;true&quot;/&amp;gt;
    &amp;lt;tx:method name=&quot;find*&quot; read-only=&quot;true&quot;/&amp;gt;
&amp;lt;/tx:attributes&amp;gt;
&amp;lt;aop:pointcut id=&quot;pt&quot; expression=&quot;execution(* service.*Service.*(..))&quot;/&amp;gt;&amp;lt;!--标准--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：通常事务属性会配置多个，包含 1 个读写的全事务属性，1 个只读的查询类事务属性&lt;/p&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name：待添加事务的方法名表达式（支持 * 通配符）&lt;/li&gt;
&lt;li&gt;read-only：设置事务的读写属性，true 为只读，false 为读写&lt;/li&gt;
&lt;li&gt;timeout：设置事务的超时时长，单位秒，-1 为无限长&lt;/li&gt;
&lt;li&gt;isolation：设置事务的隔离界别，该隔离级设定是基于 Spring 的设定，非数据库端&lt;/li&gt;
&lt;li&gt;no-rollback-for：设置事务中不回滚的异常，多个异常使用 &lt;code&gt;,&lt;/code&gt; 分隔&lt;/li&gt;
&lt;li&gt;rollback-for：设置事务中必回滚的异常，多个异常使用 &lt;code&gt;,&lt;/code&gt; 分隔&lt;/li&gt;
&lt;li&gt;propagation：设置事务的传播行为&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;注解&lt;/h4&gt;
&lt;h5&gt;开启注解&lt;/h5&gt;
&lt;h6&gt;XML&lt;/h6&gt;
&lt;p&gt;标签：tx:annotation-driven&lt;/p&gt;
&lt;p&gt;归属：beans 标签&lt;/p&gt;
&lt;p&gt;作用：开启事务注解驱动，并指定对应的事务管理器&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;tx:annotation-driven transaction-manager=&quot;txManager&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h6&gt;纯注解&lt;/h6&gt;
&lt;p&gt;名称：@EnableTransactionManagement&lt;/p&gt;
&lt;p&gt;类型：类注解，Spring 注解配置类上方&lt;/p&gt;
&lt;p&gt;作用：开启注解驱动，等同 XML 格式中的注解驱动&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan(&quot;com.seazean&quot;)
@PropertySource(&quot;classpath:jdbc.properties&quot;)
@Import({JDBCConfig.class,MyBatisConfig.class,TransactionManagerConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class TransactionManagerConfig {
    @Bean												//自动装配
    public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;配置注解&lt;/h5&gt;
&lt;p&gt;名称：@Transactional&lt;/p&gt;
&lt;p&gt;类型：方法注解，类注解，接口注解&lt;/p&gt;
&lt;p&gt;作用：设置当前类/接口中所有方法或具体方法开启事务，并指定相关事务属性&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional(
    readOnly = false,
    timeout = -1,
    isolation = Isolation.DEFAULT,
    rollbackFor = {ArithmeticException.class, IOException.class},
    noRollbackFor = {},
    propagation = Propagation.REQUIRES_NEW
)
public void addAccount{} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; 注解只有作用到 public 方法上事务才生效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不推荐在接口上使用 &lt;code&gt;@Transactional&lt;/code&gt; 注解&lt;/p&gt;
&lt;p&gt;原因：在接口上使用注解，&lt;strong&gt;只有在使用基于接口的代理（JDK）时才会生效，因为注解是不能继承的&lt;/strong&gt;，这就意味着如果正在使用基于类的代理（CGLIB）时，那么事务的设置将不能被基于类的代理所识别&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正确的设置 &lt;code&gt;@Transactional&lt;/code&gt; 的 rollbackFor 和 propagation 属性，否则事务可能会回滚失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认情况下，事务只有遇到运行期异常 和 Error 会导致事务回滚，但是在遇到检查型（Checked）异常时不会回滚&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;继承自 RuntimeException 或 error 的是非检查型异常，比如空指针和索引越界，而继承自 Exception 的则是检查型异常，比如 IOException、ClassNotFoundException，RuntimeException 本身继承 Exception&lt;/li&gt;
&lt;li&gt;非检查型类异常可以不用捕获，而检查型异常则必须用 try 语句块把异常交给上级方法，这样事务才能有效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;事务不生效的问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;情况 1：确认创建的 MySQL 数据库表引擎是 InnoDB，MyISAM 不支持事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 2：注解到 protected，private 方法上事务不生效，但不会报错&lt;/p&gt;
&lt;p&gt;原因：理论上而言，不用 public 修饰，也可以用 aop 实现事务的功能，但是方法私有化让其他业务无法调用&lt;/p&gt;
&lt;p&gt;AopUtils.canApply：&lt;code&gt;methodMatcher.matches(method, targetClass) --true--&amp;gt; return true&lt;/code&gt;
&lt;code&gt;TransactionAttributeSourcePointcut.matches()&lt;/code&gt; ，AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的 computeTransactionAttribute 方法，当加了事务注解的方法不是 public 时，该方法直接返回 null，所以造成增强不匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private TransactionAttribute computeTransactionAttribute(Method method, Class&amp;lt;?&amp;gt; targetClass) {
    // Don&apos;t allow no-public methods as required.
    if (allowPublicMethodsOnly() &amp;amp;&amp;amp; !Modifier.isPublic(method.getModifiers())) {
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 3：注解所在的类没有被加载成 Bean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 4：在业务层捕捉异常后未向上抛出，事务不生效&lt;/p&gt;
&lt;p&gt;原因：在业务层捕捉并处理了异常（try..catch）等于把异常处理掉了，Spring 就不知道这里有错，也不会主动去回滚数据，推荐做法是在业务层统一抛出异常，然后在控制层统一处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 5：遇到检测异常时，也无法回滚&lt;/p&gt;
&lt;p&gt;原因：Spring 的默认的事务规则是遇到运行异常（RuntimeException）和程序错误（Error）才会回滚。想针对检测异常进行事务回滚，可以在 @Transactional 注解里使用 rollbackFor 属性明确指定异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 6：Spring 的事务传播策略在&lt;strong&gt;内部方法&lt;/strong&gt;调用时将不起作用，在一个 Service 内部，事务方法之间的嵌套调用，普通方法和事务方法之间的嵌套调用，都不会开启新的事务，事务注解要加到调用方法上才生效&lt;/p&gt;
&lt;p&gt;原因：Spring 的事务都是使用 AOP 代理的模式，动态代理 invoke 后会调用原始对象，而原始对象在去调用方法时是不会触发拦截器，就是&lt;strong&gt;一个方法调用本对象的另一个方法&lt;/strong&gt;，所以事务也就无法生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional
public int add(){
    update();
}
//注解添加在update方法上无效，需要添加到add()方法上
public int update(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况 7：注解在接口上，代理对象是 CGLIB&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;使用注解&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Dao 层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AccountDao {
    @Update(&quot;update account set money = money + #{money} where name = #{name}&quot;)
    void inMoney(@Param(&quot;name&quot;) String name, @Param(&quot;money&quot;) Double money);

    @Update(&quot;update account set money = money - #{money} where name = #{name}&quot;)
    void outMoney(@Param(&quot;name&quot;) String name, @Param(&quot;money&quot;) Double money);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AccountService {
    //对当前方法添加事务，该配置将替换接口的配置
    @Transactional(
        readOnly = false,
        timeout = -1,
        isolation = Isolation.DEFAULT,
        rollbackFor = {},//java.lang.ArithmeticException.class, IOException.class
        noRollbackFor = {},
        propagation = Propagation.REQUIRED
        )
    public void transfer(String outName, String inName, Double money);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    public void transfer(String outName, String inName, Double money) {
        accountDao.inMoney(outName,money);
        //int i = 1/0;
        accountDao.outMoney(inName,money);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加文件 Spring.config、Mybatis.config、JDBCConfig (参考ioc_Mybatis)、TransactionManagerConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan({&quot;&quot;,&quot;&quot;,&quot;&quot;})
@PropertySource(&quot;classpath:jdbc.properties&quot;)
@Import({JDBCConfig.class,MyBatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;模板对象&lt;/h3&gt;
&lt;p&gt;Spring 模板对象：TransactionTemplate、JdbcTemplate、RedisTemplate、RabbitTemplate、JmsTemplate、HibernateTemplate、RestTemplate&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JdbcTemplate：提供标准的 sql 语句操作API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NamedParameterJdbcTemplate：提供标准的具名 sql 语句操作API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RedisTemplate：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void changeMoney(Integer id, Double money) {
    redisTemplate.opsForValue().set(&quot;account:id:&quot;+id,money);
}
public Double findMondyById(Integer id) {
    Object money = redisTemplate.opsForValue().get(&quot;account:id:&quot; + id);
    return new Double(money.toString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-RedisTemplate.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;h3&gt;XML&lt;/h3&gt;
&lt;p&gt;三大对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BeanDefinition&lt;/strong&gt;：是 Spring 中极其重要的一个概念，存储了 bean 对象的所有特征信息，如是否单例、是否懒加载、factoryBeanName 等，和 bean 的关系就是类与对象的关系，一个不同的 bean 对应一个 BeanDefinition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BeanDefinationRegistry&lt;/strong&gt;：存放 BeanDefination 的容器，是一种键值对的形式，通过特定的 Bean 定义的 id，映射到相应的 BeanDefination，&lt;strong&gt;BeanFactory 的实现类同样继承 BeanDefinationRegistry 接口&lt;/strong&gt;，拥有保存 BD 的能力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BeanDefinitionReader&lt;/strong&gt;：读取配置文件，&lt;strong&gt;XML 用 Dom4j 解析&lt;/strong&gt;，&lt;strong&gt;注解用 IO 流加载解析&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BeanFactory bf = new XmlBeanFactory(new ClassPathResource(&quot;applicationContext.xml&quot;));
UserService userService1 = (UserService)bf.getBean(&quot;userService&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) {
    super(parentBeanFactory);
    this.reader.loadBeanDefinitions(resource);
}
public int loadBeanDefinitions(Resource resource) {
    //将 resource 包装成带编码格式的 EncodedResource
    //EncodedResource 中 getReader()方法，调用java.io包下的 转换流 创建指定编码的输入流对象
    return loadBeanDefinitions(new EncodedResource(resource));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;XmlBeanDefinitionReader.loadBeanDefinitions()&lt;/code&gt;：&lt;strong&gt;把 Resource 解析成 BeanDefinition 对象&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;currentResources = this.resourcesCurrentlyBeingLoaded.get()&lt;/code&gt;：拿到当前线程已经加载过的所有 EncodedResoure 资源，用 ThreadLocal 保证线程安全&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (currentResources == null)&lt;/code&gt;：判断 currentResources 是否为空，为空则进行初始化&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!currentResources.add(encodedResource))&lt;/code&gt;：如果已经加载过该资源会报错，防止重复加载&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inputSource = new InputSource(inputStream)&lt;/code&gt;：资源对象包装成 InputSource，InputSource 是 &lt;strong&gt;SAX&lt;/strong&gt; 中的资源对象，用来进行 XML 文件的解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return doLoadBeanDefinitions()&lt;/code&gt;：&lt;strong&gt;加载返回&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;currentResources.remove(encodedResource)&lt;/code&gt;：加载完成移除当前 encodedResource&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resourcesCurrentlyBeingLoaded.remove()&lt;/code&gt;：ThreadLocal 为空时移除元素，防止内存泄露&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;XmlBeanDefinitionReader.doLoadBeanDefinitions(inputSource, resource)&lt;/code&gt;：真正的加载函数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Document doc = doLoadDocument(inputSource, resource)&lt;/code&gt;：转换成有&lt;strong&gt;层次结构&lt;/strong&gt;的 Document 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getEntityResolver()&lt;/code&gt;&lt;strong&gt;：获取用来解析 DTD、XSD 约束的解析器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getValidationModeForResource(resource)&lt;/code&gt;：获取验证模式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;int count = registerBeanDefinitions(doc, resource)&lt;/code&gt;：&lt;strong&gt;将 Document 解析成 BD 对象，注册（添加）到  BeanDefinationRegistry 中&lt;/strong&gt;，返回新注册的数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createBeanDefinitionDocumentReader()&lt;/code&gt;：创建 DefaultBeanDefinitionDocumentReader 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getRegistry().getBeanDefinitionCount()&lt;/code&gt;：获取解析前 BeanDefinationRegistry 中的 bd 数量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;registerBeanDefinitions(doc, readerContext)&lt;/code&gt;：注册 BD
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.readerContext = readerContext&lt;/code&gt;：保存上下文对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;doRegisterBeanDefinitions(doc.getDocumentElement())&lt;/code&gt;：真正的注册 BD 函数
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc.getDocumentElement()&lt;/code&gt;：拿出顶层标签 &amp;lt;beans&amp;gt;&amp;lt;/beans&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return getRegistry().getBeanDefinitionCount() - countBefore&lt;/code&gt;：返回新加入的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions()&lt;/code&gt;：注册 BD 到 BR&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createDelegate(getReaderContext(), root, parent)&lt;/code&gt;：beans 是标签的解析器对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delegate.isDefaultNamespace(root)&lt;/code&gt;：判断 beans 标签是否是默认的属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root.getAttribute(PROFILE_ATTRIBUTE)&lt;/code&gt;：解析 profile 属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preProcessXml(root)&lt;/code&gt;：解析前置处理，自定义实现&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parseBeanDefinitions(root, this.delegate)&lt;/code&gt;：&lt;strong&gt;解析 beans 标签中的子标签&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parseDefaultElement(ele, delegate)&lt;/code&gt;：如果是默认的标签，用该方法解析子标签
&lt;ul&gt;
&lt;li&gt;判断标签名称，进行相应的解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processBeanDefinition(ele, delegate)&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delegate.parseCustomElement(ele)&lt;/code&gt;：解析自定义的标签&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postProcessXml(root)&lt;/code&gt;：解析后置处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultBeanDefinitionDocumentReader.processBeanDefinition()&lt;/code&gt;：&lt;strong&gt;解析 bean 标签并注册到注册中心&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;delegate.parseBeanDefinitionElement(ele)&lt;/code&gt;：解析 bean 标签封装为 BeanDefinitionHolder&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!StringUtils.hasText(beanName) &amp;amp;&amp;amp; !aliases.isEmpty())&lt;/code&gt;：条件一成立说明 name 没有值，条件二成立说明别名有值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;beanName = aliases.remove(0)&lt;/code&gt;：拿别名列表的第一个元素当作 beanName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parseBeanDefinitionElement(ele, beanName, containingBean)&lt;/code&gt;：&lt;strong&gt;解析 bean 标签&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parseState.push(new BeanEntry(beanName))&lt;/code&gt;：当前解析器的状态设置为 BeanEntry&lt;/li&gt;
&lt;li&gt;class 和 parent 属性存在一个，parent 是作为父标签为了被继承&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createBeanDefinition(className, parent)&lt;/code&gt;：设置了class 的 GenericBeanDefinition对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parseBeanDefinitionAttributes()&lt;/code&gt;：解析 bean 标签的属性&lt;/li&gt;
&lt;li&gt;接下来解析子标签&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanName = this.readerContext.generateBeanName(beanDefinition)&lt;/code&gt;：生成 className + # + 序号的名称赋值给 beanName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return new BeanDefinitionHolder(beanDefinition, beanName, aliases)&lt;/code&gt;：&lt;strong&gt;包装成 BeanDefinitionHolder&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;registerBeanDefinition(bdHolder, getReaderContext().getRegistry())&lt;/code&gt;：&lt;strong&gt;注册到容器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;beanName = definitionHolder.getBeanName()&lt;/code&gt;：获取beanName&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.beanDefinitionMap.put(beanName, beanDefinition)&lt;/code&gt;：添加到注册中心&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getReaderContext().fireComponentRegistered()&lt;/code&gt;：发送注册完成事件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;说明：源码部分的笔记不一定适合所有人阅读，作者采用流水线式去解析重要的代码，解析的结构类似于树状，如果视觉疲劳可以去网上参考一些博客和流程图学习源码。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;IOC&lt;/h3&gt;
&lt;h4&gt;容器启动&lt;/h4&gt;
&lt;p&gt;Spring IOC 容器是 ApplicationContext 或者 BeanFactory，使用多个 Map 集合保存单实例 Bean，环境信息等资源，不同层级有不同的容器，比如整合 SpringMVC 的父子容器（先看 Bean 部分的源码解析再回看容器）&lt;/p&gt;
&lt;p&gt;ClassPathXmlApplicationContext 与 AnnotationConfigApplicationContext 差不多：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public AnnotationConfigApplicationContext(Class&amp;lt;?&amp;gt;... annotatedClasses) {
    this();
    register(annotatedClasses);// 解析配置类，封装成一个 BeanDefinitionHolder，并注册到容器
    refresh();// 加载刷新容器中的 Bean
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public AnnotationConfigApplicationContext() {
    // 注册 Spring 的注解解析器到容器
    this.reader = new AnnotatedBeanDefinitionReader(this);
    // 实例化路径扫描器，用于对指定的包目录进行扫描查找 bean 对象
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractApplicationContext.refresh()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;prepareRefresh()：刷新前的&lt;strong&gt;预处理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.startupDate = System.currentTimeMillis()&lt;/code&gt;：设置容器的启动时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initPropertySources()&lt;/code&gt;：初始化一些属性设置，可以自定义个性化的属性设置方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getEnvironment().validateRequiredProperties()&lt;/code&gt;：检查环境变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;earlyApplicationEvents= new LinkedHashSet&amp;lt;ApplicationEvent&amp;gt;()&lt;/code&gt;：保存容器中早期的事件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;obtainFreshBeanFactory()：获取一个&lt;strong&gt;全新的 BeanFactory 接口实例&lt;/strong&gt;，如果容器中存在工厂实例直接销毁&lt;/p&gt;
&lt;p&gt;&lt;code&gt;refreshBeanFactory()&lt;/code&gt;：创建 BeanFactory，设置序列化 ID、读取 BeanDefinition 并加载到工厂&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (hasBeanFactory())&lt;/code&gt;：applicationContext 内部拥有一个 beanFactory 实例，需要将该实例完全释放销毁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroyBeans()&lt;/code&gt;：销毁原 beanFactory 实例，将 beanFactory 内部维护的单实例 bean 全部清掉，如果哪个 bean 实现了 Disposablejie接口，还会进行 bean distroy 方法的调用处理
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.singletonsCurrentlyInDestruction = true&lt;/code&gt;：设置当前 beanFactory 状态为销毁状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String[] disposableBeanNames&lt;/code&gt;：获取销毁集合中的 bean，如果当前 bean 有&lt;strong&gt;析构函数&lt;/strong&gt;就会在销毁集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroySingleton(disposableBeanNames[i])&lt;/code&gt;：遍历所有的 disposableBeans，执行销毁方法
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;removeSingleton(beanName)&lt;/code&gt;：清除三级缓存和 registeredSingletons 中的当前 beanName 的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.disposableBeans.remove(beanName)&lt;/code&gt;：从销毁集合中清除，每个 bean 只能 destroy 一次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroyBean(beanName, disposableBean)&lt;/code&gt;：销毁 bean
&lt;ul&gt;
&lt;li&gt;dependentBeanMap 记录了依赖当前 bean 的其他 bean 信息，因为依赖的对象要被回收了，所以依赖当前 bean 的其他对象都要执行 destroySingleton，遍历 dependentBeanMap 执行销毁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bean.destroy()&lt;/code&gt;：解决完成依赖后，执行 DisposableBean 的 destroy 方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt; this.dependenciesForBeanMap.remove(beanName)&lt;/code&gt;：保存当前 bean 依赖了谁，直接清除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;进行一些集合和缓存的清理工作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;closeBeanFactory()&lt;/code&gt;：将容器内部的 beanFactory 设置为空，重新创建&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beanFactory = createBeanFactory()&lt;/code&gt;：创建新的 DefaultListableBeanFactory 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beanFactory.setSerializationId(getId())&lt;/code&gt;：进行 ID 的设置，可以根据 ID 获取 BeanFactory 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;customizeBeanFactory(beanFactory)&lt;/code&gt;：设置是否允许覆盖和循环引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loadBeanDefinitions(beanFactory)&lt;/code&gt;：&lt;strong&gt;加载 BeanDefinition 信息，注册 BD注册到 BeanFactory 中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.beanFactory = beanFactory&lt;/code&gt;：把 beanFactory 填充至容器中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;getBeanFactory()&lt;/code&gt;：返回创建的 DefaultListableBeanFactory 对象，该对象继承 BeanDefinitionRegistry&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;prepareBeanFactory(beanFactory)：&lt;strong&gt;BeanFactory 的预准备&lt;/strong&gt;工作，向容器中添加一些组件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setBeanClassLoader(getClassLoader())&lt;/code&gt;：给当前 bf 设置一个&lt;strong&gt;类加载器&lt;/strong&gt;，加载 bd 的 class 信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setBeanExpressionResolver()&lt;/code&gt;：设置 EL 表达式解析器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addPropertyEditorRegistrar&lt;/code&gt;：添加一个属性编辑器，解决属性注入时的格式转换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addBeanPostProcessor()&lt;/code&gt;：添加后处理器，主要用于向 bean 内部注入一些框架级别的实例&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ignoreDependencyInterface()&lt;/code&gt;：设置忽略自动装配的接口，bean 内部的这些类型的字段   不参与依赖注入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;registerResolvableDependency()&lt;/code&gt;：注册一些类型依赖关系&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addBeanPostProcessor()&lt;/code&gt;：将配置的监听者注册到容器中，当前 bean 实现 ApplicationListener 接口就是监听器事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beanFactory.registerSingleton()&lt;/code&gt;：添加一些系统信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;postProcessBeanFactory(beanFactory)：BeanFactory 准备工作完成后进行的后置处理工作，扩展方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invokeBeanFactoryPostProcessors(beanFactory)：&lt;strong&gt;执行 BeanFactoryPostProcessor 的方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processedBeans = new HashSet&amp;lt;&amp;gt;()&lt;/code&gt;：存储已经执行过的 BeanFactoryPostProcessor 的 beanName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (beanFactory instanceof BeanDefinitionRegistry)&lt;/code&gt;：&lt;strong&gt;当前 BeanFactory 是 bd 的注册中心，bd 全部注册到 bf&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors)&lt;/code&gt;：遍历所有的 bf 后置处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (postProcessor instanceof BeanDefinitionRegistryPostProcessor)&lt;/code&gt;：是 Registry 类的后置处理器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;registryProcessor.postProcessBeanDefinitionRegistry(registry)&lt;/code&gt;：向 bf 中注册一些 bd&lt;/p&gt;
&lt;p&gt;&lt;code&gt;registryProcessors.add(registryProcessor)&lt;/code&gt;：添加到 BeanDefinitionRegistryPostProcessor 集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;regularPostProcessors.add(postProcessor)&lt;/code&gt;：添加到 BeanFactoryPostProcessor 集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑到这里已经获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor  接口类型的后置处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;首先回调 BeanDefinitionRegistryPostProcessor 类的后置处理方法 postProcessBeanDefinitionRegistry()&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取实现了 PriorityOrdered（主排序接口）接口的 bdrpp，进行 sort 排序，然后全部执行并放入已经处理过的集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再执行实现了 Ordered（次排序接口）接口的 bdrpp&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后执行没有实现任何优先级或者是顺序接口 bdrpp，&lt;code&gt;boolean reiterate = true&lt;/code&gt; 控制 while 是否需要再次循环，循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法，接口方法执行以后会向 bf 内注册 bd，注册的 bd 也有可能是 bdrpp 类型，所以需要该变量控制循环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processedBeans.add(ppName)&lt;/code&gt;：已经执行过的后置处理器存储到该集合中，防止重复执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; invokeBeanFactoryPostProcessors()&lt;/code&gt;：bdrpp 继承了 BeanFactoryPostProcessor，有 postProcessBeanFactory 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行普通 BeanFactoryPostProcessor 的相关 postProcessBeanFactory 方法，按照主次无次序执行&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (processedBeans.contains(ppName))&lt;/code&gt;：会过滤掉已经执行过的后置处理器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.clearMetadataCache()&lt;/code&gt;：清除缓存中合并的 Bean 定义，因为后置处理器可能更改了元数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;以上是 BeanFactory 的创建及预准备工作，接下来进入 Bean 的流程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;registerBeanPostProcessors(beanFactory)：&lt;strong&gt;注册 Bean 的后置处理器&lt;/strong&gt;，为了干预 Spring 初始化 bean 的流程，这里仅仅是向容器中&lt;strong&gt;注入而非使用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.getBeanNamesForType(BeanPostProcessor.class)&lt;/code&gt;：&lt;strong&gt;获取配置中实现了 BeanPostProcessor 接口类型&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int beanProcessorTargetCount&lt;/code&gt;：后置处理器的数量，已经注册的 + 未注册的 + 即将要添加的一个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker())&lt;/code&gt;：添加一个检查器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BeanPostProcessorChecker.postProcessAfterInitialization()&lt;/code&gt;：初始化后的后处理器方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;!(bean instanceof BeanPostProcessor) &lt;/code&gt;：当前 bean 类型是普通 bean，不是后置处理器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!isInfrastructureBean(beanName)&lt;/code&gt;：成立说明当前 beanName 是用户级别的 bean  不是 Spring 框架的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.beanFactory.getBeanPostProcessorCount() &amp;lt; this.beanPostProcessorTargetCount&lt;/code&gt;：BeanFactory 上面注册后处理器数量 &amp;lt; 后处理器数量，说明后处理框架尚未初始化完成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (String ppName : postProcessorNames)&lt;/code&gt;：遍历 PostProcessor 集合，&lt;strong&gt;根据实现不同的顺序接口添加到不同集合&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sortPostProcessors(priorityOrderedPostProcessors, beanFactory)&lt;/code&gt;：实现 PriorityOrdered 接口的后处理器排序&lt;/p&gt;
&lt;p&gt;&lt;code&gt;registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors)&lt;/code&gt;：&lt;strong&gt;注册到 beanFactory 中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接着排序注册实现 Ordered 接口的后置处理器，然后注册普通的（ 没有实现任何优先级接口）后置处理器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后排序 MergedBeanDefinitionPostProcessor 类型的处理器，根据实现的排序接口，排序完注册到 beanFactory 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext))&lt;/code&gt;：重新注册 ApplicationListenerDetector 后处理器，用于在 Bean 创建完成后检查是否属于 ApplicationListener 类型，如果是就把 Bean 放到&lt;strong&gt;监听器容器&lt;/strong&gt;中保存起来&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;initMessageSource()：初始化 MessageSource 组件，主要用于做国际化功能，消息绑定与消息解析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME))&lt;/code&gt;：容器是否含有名称为 messageSource 的 bean&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class)&lt;/code&gt;：如果有证明用户自定义了该类型的 bean，获取后直接赋值给 this.messageSource&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dms = new DelegatingMessageSource()&lt;/code&gt;：容器中没有就新建一个赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;initApplicationEventMulticaster()：&lt;strong&gt;初始化事件传播器&lt;/strong&gt;，在注册监听器时会用到&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME))&lt;/code&gt;：&lt;strong&gt;条件成立说明用户自定义了事件传播器&lt;/strong&gt;，可以实现 ApplicationEventMulticaster 接口编写自己的事件传播器，通过 bean 的方式提供给 Spring&lt;/li&gt;
&lt;li&gt;如果有就直接从容器中获取；如果没有则创建一个 SimpleApplicationEventMulticaster 注册&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onRefresh()：留给用户去实现，可以硬编码提供一些组件，比如提供一些监听器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;registerListeners()：注册通过配置提供的 Listener，这些&lt;strong&gt;监听器&lt;/strong&gt;最终注册到 ApplicationEventMulticaster 内&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (ApplicationListener&amp;lt;?&amp;gt; listener : getApplicationListeners()) &lt;/code&gt;：注册编码实现的监听器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getBeanNamesForType(ApplicationListener.class, true, false)&lt;/code&gt;：注册通过配置提供的 Listener&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;multicastEvent(earlyEvent)&lt;/code&gt;：&lt;strong&gt;发布前面步骤产生的事件 applicationEvents&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Executor executor = getTaskExecutor()&lt;/code&gt;：获取线程池，有线程池就异步执行，没有就同步执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;finishBeanFactoryInitialization()：&lt;strong&gt;实例化非懒加载状态的单实例&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.freezeConfiguration()&lt;/code&gt;：&lt;strong&gt;冻结配置信息&lt;/strong&gt;，就是冻结 BD 信息，冻结后无法再向 bf 内注册 bd&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.preInstantiateSingletons()&lt;/code&gt;：实例化 non-lazy-init singletons&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (String beanName : beanNames)&lt;/code&gt;：遍历容器内所有的 beanDefinitionNames&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getMergedLocalBeanDefinition(beanName)&lt;/code&gt;：获取与父类合并后的对象（Bean → 获取流程部分详解此函数）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!bd.isAbstract() &amp;amp;&amp;amp; bd.isSingleton() &amp;amp;&amp;amp; !bd.isLazyInit())&lt;/code&gt;：BD 对应的 Class 满足非抽象、单实例，非懒加载，需要预先实例化&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (isFactoryBean(beanName))&lt;/code&gt;：BD 对应的 Class 是 factoryBean 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getBean(FACTORY_BEAN_PREFIX + beanName)&lt;/code&gt;：获取工厂 FactoryBean 实例本身&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isEagerInit&lt;/code&gt;：控制 FactoryBean 内部管理的 Bean 是否也初始化&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getBean(beanName)&lt;/code&gt;：&lt;strong&gt;初始化 Bean，获取 Bean 详解此函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;getBean(beanName)&lt;/code&gt;：不是工厂 bean 直接获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (String beanName : beanNames)&lt;/code&gt;：检查所有的 Bean 是否实现 SmartInitializingSingleton 接口，实现了就执行 afterSingletonsInstantiated()，进行一些创建后的操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;finishRefresh()&lt;/code&gt;：完成刷新后做的一些事情，主要是启动生命周期&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clearResourceCaches()&lt;/code&gt;：清空上下文缓存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initLifecycleProcessor()&lt;/code&gt;：&lt;strong&gt;初始化和生命周期有关的后置处理器&lt;/strong&gt;，容器的生命周期
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME))&lt;/code&gt;：成立说明自定义了生命周期处理器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultProcessor = new DefaultLifecycleProcessor()&lt;/code&gt;：Spring 默认提供的生命周期处理器&lt;/li&gt;
&lt;li&gt;&lt;code&gt; beanFactory.registerSingleton()&lt;/code&gt;：将生命周期处理器注册到 bf 的一级缓存和注册单例集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getLifecycleProcessor().onRefresh()&lt;/code&gt;：获取该&lt;strong&gt;生命周期后置处理器回调 onRefresh()&lt;/strong&gt;，调用 &lt;code&gt;startBeans(true)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lifecycleBeans = getLifecycleBeans()&lt;/code&gt;：获取到所有实现了 Lifecycle 接口的对象包装到 Map 内，key 是beanName， value 是 Lifecycle 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int phase = getPhase(bean)&lt;/code&gt;：获取当前 Lifecycle 的 phase 值，当前生命周期对象可能依赖其他生命周期对象的执行结果，所以需要 phase 决定执行顺序，数值越低的优先执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LifecycleGroup group = phases.get(phase)&lt;/code&gt;：把 phsae 相同的 Lifecycle 存入 LifecycleGroup&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (group == null)&lt;/code&gt;：group 为空则创建，初始情况下是空的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group.add(beanName, bean)&lt;/code&gt;：将当前 Lifecycle 添加到当前 phase 值一样的 group 内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Collections.sort(keys)&lt;/code&gt;：&lt;strong&gt;从小到大排序，按优先级启动&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;phases.get(key).start()&lt;/code&gt;：遍历所有的 Lifecycle 对象开始启动&lt;/li&gt;
&lt;li&gt;&lt;code&gt;doStart(this.lifecycleBeans, member.name, this.autoStartupOnly)&lt;/code&gt;：底层调用该方法启动
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bean = lifecycleBeans.remove(beanName)&lt;/code&gt;： 确保 Lifecycle 只被启动一次，在一个分组内被启动了在其他分组内就看不到 Lifecycle 了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName)&lt;/code&gt;：获取当前即将被启动的 Lifecycle 所依赖的其他 beanName，需要&lt;strong&gt;先启动所依赖的 bean&lt;/strong&gt;，才能启动自身&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if ()&lt;/code&gt;：传入的参数 autoStartupOnly 为 true 表示启动 isAutoStartUp 为 true 的 SmartLifecycle 对象，不会启动普通的生命周期的对象；false 代表全部启动&lt;/li&gt;
&lt;li&gt;bean.start()：&lt;strong&gt;调用启动方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publishEvent(new ContextRefreshedEvent(this))&lt;/code&gt;：&lt;strong&gt;发布容器刷新完成事件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;liveBeansView.registerApplicationContext(this)&lt;/code&gt;：暴露 Mbean&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充生命周期 stop() 方法的调用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DefaultLifecycleProcessor.stop()：调用 DefaultLifecycleProcessor.stopBeans()&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取到所有实现了 Lifecycle 接口的对象并按 phase 数值分组的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;keys.sort(Collections.reverseOrder())&lt;/code&gt;：按 phase 降序排序 Lifecycle 接口，最先启动的最晚关闭（责任链？）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;phases.get(key).stop()&lt;/code&gt;：遍历所有的 Lifecycle 对象开始停止&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;latch = new CountDownLatch(this.smartMemberCount)&lt;/code&gt;：创建 CountDownLatch，设置 latch 内部的值为当前分组内的  smartMemberCount 的数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet&amp;lt;&amp;gt;())&lt;/code&gt;：保存当前正在处理关闭的smartLifecycle 的 BeanName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (LifecycleGroupMember member : this.members)&lt;/code&gt;：处理本分组内需要关闭的 Lifecycle&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames)&lt;/code&gt;：真正的停止方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getBeanFactory().getDependentBeans(beanName)&lt;/code&gt;：&lt;strong&gt;获取依赖当前 Lifecycle 的其他对象的 beanName&lt;/strong&gt;，因为当前的 Lifecycle 即将要关闭了，所有的依赖了当前 Lifecycle 的 bean 也要关闭&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;countDownBeanNames.add(beanName)&lt;/code&gt;：将当前 SmartLifecycle beanName 添加到 countDownBeanNames 集合内，该集合表示正在关闭的 SmartLifecycle&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bean.stop()&lt;/code&gt;：调用停止的方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;获取Bean&lt;/h4&gt;
&lt;p&gt;单实例：在容器启动时创建对象&lt;/p&gt;
&lt;p&gt;多实例：在每次获取的时候创建对象&lt;/p&gt;
&lt;p&gt;获取流程：&lt;strong&gt;获取 Bean 时先从单例池获取，如果没有则进行第二次获取，并带上工厂类去创建并添加至单例池&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java 启动 Spring 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ApplicationContext ctx = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);
UserService userService = (UserService) context.getBean(&quot;userService&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractBeanFactory.doGetBean()：获取 Bean，context.getBean() 追踪到此&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanName = transformedBeanName(name)&lt;/code&gt;：name 可能是一个别名，重定向出来真实 beanName；也可能是一个 &amp;amp; 开头的 name，说明要获取的 bean 实例对象，是一个 FactoryBean 对象（IOC 原理 → 核心类）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BeanFactoryUtils.transformedBeanName(name)&lt;/code&gt;：判断是哪种 name，返回截取 &amp;amp; 以后的 name 并放入缓存
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;transformedBeanNameCache.computeIfAbsent&lt;/code&gt;：缓存是并发安全集合，key == null || value == null 时 put 成功&lt;/li&gt;
&lt;li&gt;do while 循环一直去除 &amp;amp; 直到不再含有 &amp;amp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canonicalName(name)&lt;/code&gt;：aliasMap 保存别名信息，其中的 do while 逻辑是迭代查找，比如 A 别名叫做 B，但是 B 又有别名叫 C， aliasMap 为 {&quot;C&quot;:&quot;B&quot;, &quot;B&quot;:&quot;A&quot;}，get(C) 最后返回的是  A&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultSingletonBeanRegistry.getSingleton()&lt;/code&gt;：&lt;strong&gt;第一次获取从缓存池获取&lt;/strong&gt;（循环依赖详解此代码）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存中有数据进行 getObjectForBeanInstance() 获取可使用的 Bean（本节结束部分详解此函数）&lt;/li&gt;
&lt;li&gt;缓存中没有数据进行下面的逻辑进行创建&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(isPrototypeCurrentlyInCreation(beanName))&lt;/code&gt;：检查 bean 是否在原型（Prototype）正在被创建的集合中，如果是就报错，说明产生了循环依赖，&lt;strong&gt;原型模式解决不了循环依赖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因：先加载 A，把 A 加入集合，A 依赖 B 去加载 B，B 又依赖 A，去加载 A，发现 A 在正在创建集合中，产生循环依赖&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;markBeanAsCreated(beanName)&lt;/code&gt;：把 bean 标记为已经创建，&lt;strong&gt;防止其他线程重新创建 Bean&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd = getMergedLocalBeanDefinition(beanName)&lt;/code&gt;：&lt;strong&gt;获取合并父 BD 后的 BD 对象&lt;/strong&gt;，BD 是直接继承的，合并后的 BD 信息是包含父类的 BD 信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mergedBeanDefinitions.get(beanName)&lt;/code&gt;：从缓存中获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(bd.getParentName()==null)&lt;/code&gt;：beanName 对应 BD 没有父 BD 就不用处理继承，封装为 RootBeanDefinition 返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parentBeanName = transformedBeanName(bd.getParentName())&lt;/code&gt;：处理父 BD 的 name 信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(!beanName.equals(parentBeanName))&lt;/code&gt;：一般情况父子 BD 的名称不同&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pbd = getMergedBeanDefinition(parentBeanName)&lt;/code&gt;：递归调用，最终返回父 BD 的父 BD 信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd = new RootBeanDefinition(pbd)&lt;/code&gt;：按照父 BD 信息创建 RootBeanDefinition 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd.overrideFrom(bd)&lt;/code&gt;：&lt;strong&gt;子 BD 信息覆盖 mbd&lt;/strong&gt;，因为是要以子 BD 为基准，不存在的才去父 BD 寻找（&lt;strong&gt;类似 Java 继承&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mergedBeanDefinitions.put(beanName, mbd)&lt;/code&gt;：放入缓存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;checkMergedBeanDefinition()&lt;/code&gt;：判断当前 BD 是否为&lt;strong&gt;抽象 BD&lt;/strong&gt;，抽象 BD 不能创建实例，只能作为父 BD 被继承&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd.getDependsOn()&lt;/code&gt;：获取 bean 标签 depends-on&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(dependsOn != null)&lt;/code&gt;：&lt;strong&gt;遍历所有的依赖加载，解决不了循环依赖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isDependent(beanName, dep)&lt;/code&gt;：判断循环依赖，出现循环依赖问题报错&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;两个 Map：&lt;code&gt;&amp;lt;bean name=&quot;A&quot; depends-on=&quot;B&quot; ...&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependentBeanMap：记录依赖了当前 beanName 的其他 beanName（谁依赖我，我记录谁）&lt;/li&gt;
&lt;li&gt;dependenciesForBeanMap：记录当前 beanName 依赖的其它 beanName&lt;/li&gt;
&lt;li&gt;以 B 为视角 dependentBeanMap {&quot;B&quot;：{&quot;A&quot;}}，以 A 为视角 dependenciesForBeanMap {&quot;A&quot; :{&quot;B&quot;}}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;canonicalName(beanName)&lt;/code&gt;：处理 bean 的 name&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dependentBeans = this.dependentBeanMap.get(canonicalName)&lt;/code&gt;：获取依赖了当前 bean 的 name&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (dependentBeans.contains(dependentBeanName))&lt;/code&gt;：依赖了当前 bean 的集合中是否有该 name，有就产生循环依赖&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行递归处理所有的引用：假如 &lt;code&gt;&amp;lt;bean name=&quot;A&quot; dp=&quot;B&quot;&amp;gt; &amp;lt;bean name=&quot;B&quot; dp=&quot;C&quot;&amp;gt; &amp;lt;bean name=&quot;C&quot; dp=&quot;A&quot;&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependentBeanMap={A:{C}, B:{A}, C:{B}} 
// C 依赖 A     		判断谁依赖了C				递归判断				谁依赖了B
isDependent(C, A)  → C#dependentBeans={B} → isDependent(B, A); → B#dependentBeans={A} //返回true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;registerDependentBean(dep, beanName)&lt;/code&gt;：把 bean 和依赖注册到两个 Map 中，注意参数的位置，被依赖的在前&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getBean(dep)&lt;/code&gt;：&lt;strong&gt;先加载依赖的 Bean&lt;/strong&gt;，又进入 doGetBean() 的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mbd.isSingleton())&lt;/code&gt;：&lt;strong&gt;判断 bean 是否是单例的 bean&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getSingleton(String, ObjectFactory&amp;lt;?&amp;gt;)&lt;/code&gt;：&lt;strong&gt;第二次获取，传入一个工厂对象&lt;/strong&gt;，这个方法更倾向于创建实例并返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sharedInstance = getSingleton(beanName, () -&amp;gt; {
    return createBean(beanName, mbd, args);//创建，跳转生命周期
    //lambda表达式，调用了ObjectFactory的getObject()方法，实际回调接口实现的是 createBean()方法进行创建对象
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;singletonObjects.get(beanName)&lt;/code&gt;：从一级缓存检查是否已经被加载，单例模式复用已经创建的 bean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.singletonsCurrentlyInDestruction&lt;/code&gt;：容器销毁时会设置这个属性为 true，这时就不能再创建 bean 实例了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beforeSingletonCreation(beanName)&lt;/code&gt;：检查构造注入的依赖，&lt;strong&gt;构造参数注入产生的循环依赖无法解决&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!this.singletonsCurrentlyInCreation.add(beanName)&lt;/code&gt;：将当前 beanName 放入到正在创建中单实例集合，放入成功说明没有产生循环依赖，失败则产生循环依赖，进入判断条件内的逻辑抛出异常&lt;/p&gt;
&lt;p&gt;原因：加载 A，向正在创建集合中添加了 {A}，根据 A 的构造方法实例化 A 对象，发现 A 的构造方法依赖 B，然后加载 B，B 构造方法的参数依赖于 A，又去加载 A 时来到当前方法，因为创建中集合已经存在 A，所以添加失败&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;singletonObject = singletonFactory.getObject()&lt;/code&gt;：&lt;strong&gt;创建 bean&lt;/strong&gt;（生命周期部分详解）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建完成以后，Bean 已经初始化好，是一个完整的可使用的 Bean&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;afterSingletonCreation(beanName)&lt;/code&gt;：从正在创建中的集合中移出&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addSingleton(beanName, singletonObject)&lt;/code&gt;：&lt;strong&gt;添加一级缓存单例池中，从二级三级缓存移除&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;bean = getObjectForBeanInstance&lt;/code&gt;：&lt;strong&gt;单实例可能是普通单实例或者 FactoryBean&lt;/strong&gt;，如果是 FactoryBean 实例，需要判断 name 是带 &amp;amp; 还是不带 &amp;amp;，带 &amp;amp; 说明 getBean 获取 FactoryBean 对象，否则是获取 FactoryBean 内部管理的实例&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数 name 是未处理 &amp;amp; 的 name，beanName 是处理过 &amp;amp; 和别名后的 name&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(BeanFactoryUtils.isFactoryDereference(name))&lt;/code&gt;：判断 doGetBean 中参数 name 前是否带 &amp;amp;，不是处理后的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name))&lt;/code&gt;：Bean 是普通单实例或者是 FactoryBean 就可以直接返回，否则进入下面的获取 &lt;strong&gt;FactoryBean 内部管理的实例&lt;/strong&gt;的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getCachedObjectForFactoryBean(beanName)&lt;/code&gt;：尝试到缓存获取，获取到直接返回，获取不到进行下面逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mbd == null &amp;amp;&amp;amp; containsBeanDefinition(beanName))&lt;/code&gt;：Spring 中有当前 beanName 的 BeanDefinition 信息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mbd = getMergedLocalBeanDefinition(beanName)&lt;/code&gt;：获取合并后的 BeanDefinition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd.isSynthetic()&lt;/code&gt;：默认值是 false 表示这是一个用户对象，如果是 true 表示是系统对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;object = getObjectFromFactoryBean(factory, beanName, !synthetic)&lt;/code&gt;：从工厂内获取实例&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;factory.isSingleton() &amp;amp;&amp;amp; containsSingleton(beanName)&lt;/code&gt;：工厂内部维护的对象是单实例并且一级缓存存在该 bean&lt;/li&gt;
&lt;li&gt;首先去缓存中获取，获取不到就&lt;strong&gt;使用工厂获取&lt;/strong&gt;然后放入缓存，进行循环依赖判断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (mbd.isPrototype())&lt;/code&gt;：&lt;strong&gt;bean 是原型的 bean&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;beforePrototypeCreation(beanName)&lt;/code&gt;：当前线程正在创建的原型对象 beanName 存入 prototypesCurrentlyInCreation&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;curVal = this.prototypesCurrentlyInCreation.get()&lt;/code&gt;：获取当前线程的正在创建的原型类集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.prototypesCurrentlyInCreation.set(beanName)&lt;/code&gt;：集合为空就把当前 beanName 加入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (curVal instanceof String)&lt;/code&gt;：已经有线程相关原型类创建了，把当前的创建的加进去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;createBean(beanName, mbd, args)&lt;/code&gt;：创建原型类对象，不需要三级缓存&lt;/p&gt;
&lt;p&gt;&lt;code&gt;afterPrototypeCreation(beanName)&lt;/code&gt;：从正在创建中的集合中移除该 beanName， &lt;strong&gt;与 beforePrototypeCreation逻辑相反&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;convertIfNecessary()&lt;/code&gt;：&lt;strong&gt;依赖检查&lt;/strong&gt;，检查所需的类型是否与实际 bean 实例的类型匹配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return (T) bean&lt;/code&gt;：返回创建完成的 bean&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;h5&gt;四个阶段&lt;/h5&gt;
&lt;p&gt;Bean 的生命周期：实例化 instantiation，填充属性 populate，初始化 initialization，销毁 destruction&lt;/p&gt;
&lt;p&gt;AbstractAutowireCapableBeanFactory.createBean()：进入 Bean 生命周期的流程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resolvedClass = resolveBeanClass(mbd, beanName)&lt;/code&gt;：判断 mdb 中的 class 是否已经&lt;strong&gt;加载到 JVM&lt;/strong&gt;，如果未加载则使用类加载器将 beanName 加载到 JVM中并返回 class 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (resolvedClass != null &amp;amp;&amp;amp; !mbd.hasBeanClass() &amp;amp;&amp;amp; mbd.getBeanClassName() != null)&lt;/code&gt;：条件成立封装 mbd 并把 resolveBeanClass 设置到 bd 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件二：mbd 在 resolveBeanClass 之前是否有 class&lt;/li&gt;
&lt;li&gt;条件三：mbd 有 className&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bean = resolveBeforeInstantiation(beanName, mbdToUse)&lt;/code&gt;：实例化前的后置处理器返回一个代理实例对象（不是 AOP）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义类继承 InstantiationAwareBeanPostProcessor，重写 postProcessBeforeInstantiation 方法，&lt;strong&gt;方法逻辑为创建对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;并配置文件 &lt;code&gt;&amp;lt;bean class=&quot;intefacePackage.MyInstantiationAwareBeanPostProcessor&quot;&amp;gt;&lt;/code&gt; 导入为 bean&lt;/li&gt;
&lt;li&gt;条件成立，&lt;strong&gt;短路操作&lt;/strong&gt;，直接 return bean&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object beanInstance = doCreateBean(beanName, mbdToUse, args)&lt;/code&gt;：Do it&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AbstractAutowireCapableBeanFactory.&lt;strong&gt;doCreateBean&lt;/strong&gt;(beanName, RootBeanDefinition, Object[] args)：创建 Bean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BeanWrapper instanceWrapper = null&lt;/code&gt;：&lt;strong&gt;Spring 给所有创建的 Bean 实例包装成 BeanWrapper&lt;/strong&gt;，内部最核心的方法是获取实例，提供了一些额外的接口方法，比如属性访问器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;instanceWrapper = this.factoryBeanInstanceCache.remove(beanName)&lt;/code&gt;：单例对象尝试从缓存中获取，会移除缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;createBeanInstance()&lt;/code&gt;：&lt;strong&gt;缓存中没有实例就进行创建实例&lt;/strong&gt;（逻辑复杂，下一小节详解）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!mbd.postProcessed)&lt;/code&gt;：每个 bean 只进行一次该逻辑&lt;/p&gt;
&lt;p&gt;&lt;code&gt;applyMergedBeanDefinitionPostProcessors()&lt;/code&gt;：&lt;strong&gt;后置处理器，合并 bd 信息&lt;/strong&gt;，接下来要属性填充了&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()&lt;/code&gt;：&lt;strong&gt;后置处理逻辑（@Autowired）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;metadata = findAutowiringMetadata(beanName, beanType, null)&lt;/code&gt;：提取当前 bean 整个继承体系内的 &lt;strong&gt;@Autowired、@Value、@Inject&lt;/strong&gt; 信息，存入一个 InjectionMetadata 对象，保存着当前 bean 信息和要自动注入的字段信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Class&amp;lt;?&amp;gt; targetClass;							//当前 bean 
private final Collection&amp;lt;InjectedElement&amp;gt; injectedElements;	//要注入的信息集合
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;metadata = buildAutowiringMetadata(clazz)&lt;/code&gt;：查询当前 clazz 感兴趣的注解信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ReflectionUtils.doWithLocalFields()&lt;/code&gt;：提取&lt;strong&gt;字段&lt;/strong&gt;的注解的信息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;findAutowiredAnnotation(field)&lt;/code&gt;：代表感兴趣的注解就是那三种注解，获取这三种注解的元数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ReflectionUtils.doWithLocalMethods()&lt;/code&gt;：提取&lt;strong&gt;方法&lt;/strong&gt;的注解的信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;do{} while (targetClass != null &amp;amp;&amp;amp; targetClass != Object.class)&lt;/code&gt;：循环从父类中解析，直到 Object 类&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.injectionMetadataCache.put(cacheKey, metadata)&lt;/code&gt;：存入缓存&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;mbd.postProcessed = true&lt;/code&gt;：设置为 true，下次访问该逻辑不会再进入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;earlySingletonExposure = (mbd.isSingleton() &amp;amp;&amp;amp; this.allowCircularReferences &amp;amp;&amp;amp; isSingletonCurrentlyInCreation(beanName)&lt;/code&gt;：单例、解决循环引用、是否在单例正在创建集合中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (earlySingletonExposure) {
    // 【放入三级缓存一个工厂对象，用来获取提前引用】
    addSingletonFactory(beanName, () -&amp;gt; getEarlyBeanReference(beanName, mbd, bean));
    // lamda 表达式，用来获取提前引用，循环依赖部分详解该逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; populateBean(beanName, mbd, instanceWrapper)&lt;/code&gt;：**属性填充，依赖注入，整体逻辑是先处理标签再处理注解，填充至 pvs 中，最后通过 apply 方法最后完成属性依赖注入到 BeanWrapper **&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))&lt;/code&gt;：实例化后的后置处理器，默认返回 true，可以自定义类继承 InstantiationAwareBeanPostProcessor 修改后置处理方法的返回值为 false，使 continueWithPropertyPopulation 为 false，&lt;strong&gt;会导致直接返回，不进行属性的注入&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!continueWithPropertyPopulation)&lt;/code&gt;：自定义方法返回值会造成该条件成立，逻辑为直接返回，&lt;strong&gt;不进行依赖注入&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null)&lt;/code&gt;：处理依赖注入逻辑开始&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mbd.getResolvedAutowireMode() == ?&lt;/code&gt;：&lt;strong&gt;根据 bean 标签配置的 autowire&lt;/strong&gt; 判断是 BY_NAME 或者 BY_TYPE&lt;/p&gt;
&lt;p&gt;&lt;code&gt;autowireByName(beanName, mbd, bw, newPvs)&lt;/code&gt;：根据字段名称去获取依赖的 bean，还没注入，只是添加到 pvs&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;propertyNames = unsatisfiedNonSimpleProperties(mbd, bw)&lt;/code&gt;：bean 实例中有该字段和该字段的 setter 方法，但是在 bd 中没有 property 属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拿到配置的 property 信息和 bean 的所有字段信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pd.getWriteMethod() != null&lt;/code&gt;：&lt;strong&gt;当前字段是否有 set 方法，配置类注入的方式需要 set 方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!isExcludedFromDependencyCheck(pd)&lt;/code&gt;：当前字段类型是否在忽略自动注入的列表中&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!pvs.contains(pd.getName()&lt;/code&gt;：当前字段不在 xml 或者其他方式的配置中，也就是 bd 中不存在对应的 property&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!BeanUtils.isSimpleProperty(pd.getPropertyType()&lt;/code&gt;：是否是基本数据类型和内置的几种数据类型，基本数据类型不允许自动注入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (containsBean(propertyName))&lt;/code&gt;：BeanFactory 中存在当前 property 的 bean 实例，说明找到对应的依赖数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;getBean(propertyName)&lt;/code&gt;：&lt;strong&gt;拿到 propertyName 对应的 bean 实例&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pvs.add(propertyName, bean)&lt;/code&gt;：填充到 pvs 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;registerDependentBean(propertyName, beanName))&lt;/code&gt;：添加到两个依赖 Map（dependsOn）中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;autowireByType(beanName, mbd, bw, newPvs)&lt;/code&gt;：根据字段类型去查找依赖的 bean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;desc = new AutowireByTypeDependencyDescriptor(methodParam, eager)&lt;/code&gt;：依赖描述信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolveDependency(desc, beanName, autowiredBeanNames, converter)&lt;/code&gt;：根据描述信息，查找依赖对象，容器中没有对应的实例但是有对应的 BD，会调用 getBean(Type) 获取对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pvs = newPvs&lt;/code&gt;：newPvs 是处理了依赖数据后的 pvs，所以赋值给 pvs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hasInstAwareBpps&lt;/code&gt;：表示当前是否有 InstantiationAwareBeanPostProcessors 的后置处理器（Autowired）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)&lt;/code&gt;：&lt;strong&gt;@Autowired 注解的注入&lt;/strong&gt;，这个传入的 pvs 对象，最后原封不动的返回，不会添加东西&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;findAutowiringMetadata()&lt;/code&gt;：包装着当前 bd 需要注入的注解信息集合，&lt;strong&gt;三种注解的元数据&lt;/strong&gt;，直接缓存获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;InjectionMetadata.InjectedElement.inject()&lt;/code&gt;：遍历注解信息解析后注入到 Bean，方法和字段的注入实现不同&lt;/p&gt;
&lt;p&gt;以字段注入为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;value = resolveFieldValue(field, bean, beanName)&lt;/code&gt;：处理字段属性值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;value = beanFactory.resolveDependency()&lt;/code&gt;：解决依赖&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result = doResolveDependency()&lt;/code&gt;：&lt;strong&gt;真正处理自动注入依赖的逻辑&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object shortcut = descriptor.resolveShortcut(this)&lt;/code&gt;：默认返回 null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor)&lt;/code&gt;：&lt;strong&gt;获取 @Value 的值&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor())&lt;/code&gt;：如果 value 不是 null，就直接进行类型转换返回数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;matchingBeans = findAutowireCandidates(beanName, type, descriptor)&lt;/code&gt;：如果 value 是空说明字段是引用类型，&lt;strong&gt;获取 @Autowired 的 Bean&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// addCandidateEntry() → Object beanInstance = descriptor.resolveCandidate()
public Object resolveCandidate(String beanName, Class&amp;lt;?&amp;gt; requiredType, BeanFactory beanFactory) throws BeansException {
	// 获取 bean
    return beanFactory.getBean(beanName);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ReflectionUtils.makeAccessible(field)&lt;/code&gt;：修改访问权限&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;field.set(bean, value)&lt;/code&gt;：获取属性访问器为此 field 对象赋值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;applyPropertyValues()&lt;/code&gt;：&lt;strong&gt;将所有解析的 PropertyValues 的注入至 BeanWrapper 实例中&lt;/strong&gt;（深拷贝）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (pvs.isEmpty())&lt;/code&gt;：注解 @Autowired 和 @Value 标注的信息在后置处理的逻辑注入完成，此处为空直接返回&lt;/li&gt;
&lt;li&gt;下面的逻辑进行 XML 配置的属性的注入，首先获取转换器进行数据转换，然后&lt;strong&gt;获取 WriteMethod (set) 方法进行反射调用&lt;/strong&gt;，完成属性的注入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;initializeBean(String,Object,RootBeanDefinition)&lt;/code&gt;：&lt;strong&gt;初始化，分为配置文件和实现接口两种方式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;invokeAwareMethods(beanName, bean)&lt;/code&gt;：根据 bean 是否实现 Aware 接口执行初始化的方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;wrappedBean = applyBeanPostProcessorsBeforeInitialization&lt;/code&gt;：初始化前的后置处理器，可以继承接口重写方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;processor.postProcessBeforeInitialization()&lt;/code&gt;：执行后置处理的方法，默认返回 bean 本身&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (current == null) return result&lt;/code&gt;：重写方法返回 null，会造成后置处理的短路，直接返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;invokeInitMethods(beanName, wrappedBean, mbd)&lt;/code&gt;：&lt;strong&gt;反射执行初始化方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;isInitializingBean = (bean instanceof InitializingBean)&lt;/code&gt;：初始化方法的定义有两种方式，一种是自定义类实现 InitializingBean 接口，另一种是配置文件配置 &amp;lt;bean id=&quot;...&quot; class=&quot;...&quot; init-method=&quot;init&quot;/ &amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;isInitializingBean &amp;amp;&amp;amp; (mbd == null || !mbd.isExternallyManagedInitMethod(&quot;afterPropertiesSet&quot;))&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;条件一：当前 bean 是不是实现了 InitializingBean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件二：InitializingBean 接口中的方法 afterPropertiesSet，判断该方法是否是容器外管理的方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mbd != null &amp;amp;&amp;amp; bean.getClass() != NullBean.class)&lt;/code&gt;：成立说明是配置文件的方式&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if(!(接口条件))&lt;/code&gt;表示&lt;strong&gt;如果通过接口实现了初始化方法的话，就不会在调用配置类中 init-method 定义的方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;((InitializingBean) bean).afterPropertiesSet()&lt;/code&gt;：调用方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;invokeCustomInitMethod&lt;/code&gt;：执行自定义的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;initMethodName = mbd.getInitMethodName()&lt;/code&gt;：获取方法名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Method initMethod = ()&lt;/code&gt;：根据方法名获取到 init-method 方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt; methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod)&lt;/code&gt;：将方法转成从接口层面获取&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReflectionUtils.makeAccessible(methodToInvoke)&lt;/code&gt;：访问权限设置成可访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt; methodToInvoke.invoke(bean)&lt;/code&gt;：&lt;strong&gt;反射调用初始化方法&lt;/strong&gt;，以当前 bean 为角度去调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;wrappedBean = applyBeanPostProcessorsAfterInitialization&lt;/code&gt;：初始化后的后置处理器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AbstractAutoProxyCreator.postProcessAfterInitialization()&lt;/code&gt;：如果 Bean 被子类标识为要代理的 bean，则使用配置的拦截器&lt;strong&gt;创建代理对象&lt;/strong&gt;，AOP 部分详解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不存在循环依赖，创建动态代理 bean 在此处完成；否则真正的创建阶段是在属性填充时获取提前引用的阶段，&lt;strong&gt;循环依赖&lt;/strong&gt;详解，源码分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 该集合用来避免重复将某个 bean 生成代理对象，
private final Map&amp;lt;Object, Object&amp;gt; earlyProxyReferences = new ConcurrentHashMap&amp;lt;&amp;gt;(16);

public Object postProcessAfterInitialization(@Nullable Object bean,String bN){
    if (bean != null) {
        // cacheKey 是 beanName 或者加上 &amp;amp;
        Object cacheKey = getCacheKey(bean.getClass(), beanName);y
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                // 去提前代理引用池中寻找该key，不存在则创建代理
                // 如果存在则证明被代理过，则判断是否是当前的 bean，不是则创建代理
                return wrapIfNecessary(bean, bN, cacheKey);
            }
    }
    return bean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (earlySingletonExposure)&lt;/code&gt;：是否允许提前引用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;earlySingletonReference = getSingleton(beanName, false)&lt;/code&gt;：&lt;strong&gt;从二级缓存获取实例&lt;/strong&gt;，放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 逻辑中，此时在 createBean 的逻辑还没有返回，所以一级缓存没有&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (earlySingletonReference != null)&lt;/code&gt;：当前 bean 实例从二级缓存中获取到了，说明&lt;strong&gt;产生了循环依赖&lt;/strong&gt;，在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 的代理对象（或原始实例），放入二级缓存中，然后使用原始 bean 继续执行初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; if (exposedObject == bean)&lt;/code&gt;：&lt;strong&gt;初始化后的 bean == 创建的原始实例&lt;/strong&gt;，条件成立的两种情况：当前的真实实例不需要被代理；当前实例存在循环依赖已经被提前代理过了，初始化时的后置处理器直接返回 bean 原实例&lt;/p&gt;
&lt;p&gt;&lt;code&gt;exposedObject = earlySingletonReference&lt;/code&gt;：&lt;strong&gt;把代理后的 Bean 传给 exposedObject 用来返回，因为只有代理对象才封装了拦截器链，main 方法中用代理对象调用方法时会进行增强，代理是对原始对象的包装，所以这里返回的代理对象中含有完整的原实例（属性填充和初始化后的），是一个完整的代理对象，返回后外层方法会将当前 Bean 放入一级缓存&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (!this.allowRawInjectionDespiteWrapping &amp;amp;&amp;amp; hasDependentBean(beanName))&lt;/code&gt;：是否有其他 bean 依赖当前 bean，执行到这里说明是不存在循环依赖、存在增强代理的逻辑，也就是正常的逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dependentBeans = getDependentBeans(beanName)&lt;/code&gt;：取到依赖当前 bean 的其他 beanName&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean))&lt;/code&gt;：判断 dependentBean 是否创建完成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (!this.alreadyCreated.contains(beanName))&lt;/code&gt;：成立当前 bean 尚未创建完成，当前 bean 是依赖exposedObject 的 bean，返回 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return false&lt;/code&gt;：创建完成返回 false&lt;/p&gt;
&lt;p&gt;&lt;code&gt;actualDependentBeans.add(dependentBean)&lt;/code&gt;：创建完成的 dependentBean 加入该集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!actualDependentBeans.isEmpty())&lt;/code&gt;：条件成立说明有依赖于当前 bean 的 bean 实例创建完成，但是当前的 bean 还没创建完成返回，依赖当前 bean 的外部 bean 持有的是不完整的 bean，所以需要报错&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;registerDisposableBeanIfNecessary&lt;/code&gt;：判断当前 bean 是否需要&lt;strong&gt;注册析构函数回调&lt;/strong&gt;，当容器销毁时进行回调&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!mbd.isPrototype() &amp;amp;&amp;amp; requiresDestruction(bean, mbd))&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果是原型 prototype 不会注册析构回调，不会回调该函数，对象的回收由 JVM 的 GC 机制完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;requiresDestruction()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DisposableBeanAdapter.hasDestroyMethod(bean, mbd)&lt;/code&gt;：bd 中定义了 DestroyMethod 返回 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hasDestructionAwareBeanPostProcessors()&lt;/code&gt;：后处理器框架决定是否进行析构回调&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;registerDisposableBean()&lt;/code&gt;：条件成立进入该方法，给当前单实例注册回调适配器，适配器内根据当前 bean 实例是继承接口（DisposableBean）还是自定义标签来判定具体调用哪个方法实现&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.disposableBeans.put(beanName, bean)&lt;/code&gt;：向销毁集合添加实例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;创建实例&lt;/h5&gt;
&lt;p&gt;AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefinition, Object[] args)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resolveBeanClass(mbd, beanName)&lt;/code&gt;：确保 Bean 的 Class 真正的被加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判断类的访问权限是不是 public，不是进入下一个判断，是否允许访问类的 non-public 的构造方法，不允许则报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Supplier&amp;lt;?&amp;gt; instanceSupplier = mbd.getInstanceSupplier()&lt;/code&gt;：获取创建实例的函数，可以自定义，没有进入下面的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mbd.getFactoryMethodName() != null)&lt;/code&gt;：&lt;strong&gt;判断 bean 是否设置了 factory-method 属性，优先使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;bean class=&quot;&quot; factory-method=&quot;&quot;&amp;gt;，设置了该属性进入 factory-method 方法创建实例&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resolved = false&lt;/code&gt;：代表 bd 对应的构造信息是否已经解析成可以反射调用的构造方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;autowireNecessary = false&lt;/code&gt;：是否自动匹配构造方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(mbd.resolvedConstructorOrFactoryMethod != null)&lt;/code&gt;：获取 bd 的构造信息转化成反射调用的 method 信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;method 为 null 则 resolved 和 autowireNecessary 都为默认值 false&lt;/li&gt;
&lt;li&gt;&lt;code&gt;autowireNecessary = mbd.constructorArgumentsResolved&lt;/code&gt;：构造方法有参数，设置为 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;bd 对应的构造信息解析完成，可以直接反射调用构造方法了&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return autowireConstructor(beanName, mbd, null, null)&lt;/code&gt;：&lt;strong&gt;有参构造&lt;/strong&gt;，根据参数匹配最优的构造器创建实例&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return instantiateBean(beanName, mbd)&lt;/code&gt;：&lt;strong&gt;无参构造方法通过反射创建实例&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SimpleInstantiationStrategy.instantiate()&lt;/code&gt;：&lt;strong&gt;真正用来实例化的函数&lt;/strong&gt;（无论如何都会走到这一步）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!bd.hasMethodOverrides())&lt;/code&gt;：没有方法重写覆盖&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BeanUtils.instantiateClass(constructorToUse)&lt;/code&gt;：调用 &lt;code&gt;Constructor.newInstance()&lt;/code&gt; 实例化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;instantiateWithMethodInjection(bd, beanName, owner)&lt;/code&gt;：&lt;strong&gt;有方法重写采用 CGLIB  实例化&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BeanWrapper bw = new BeanWrapperImpl(beanInstance)&lt;/code&gt;：包装成 BeanWrapper 类型的对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return bw&lt;/code&gt;：返回实例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)&lt;/code&gt;：&lt;strong&gt;@Autowired 注解&lt;/strong&gt;，对应的后置处理器 AutowiredAnnotationBeanPostProcessor 逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置了 lookup 的相关逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.candidateConstructorsCache.get(beanClass)&lt;/code&gt;：从缓存中获取构造方法，第一次获取为 null，进入下面逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rawCandidates = beanClass.getDeclaredConstructors()&lt;/code&gt;：获取所有的构造器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Constructor&amp;lt;?&amp;gt; requiredConstructor = null&lt;/code&gt;：唯一的选项构造器，&lt;strong&gt;@Autowired(required = &quot;true&quot;)&lt;/strong&gt; 时有值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Constructor&amp;lt;?&amp;gt; candidate : rawCandidates)&lt;/code&gt;：遍历所有的构造器：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ann = findAutowiredAnnotation(candidate)&lt;/code&gt;：有三种注解中的一个会返回注解的属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;遍历 this.autowiredAnnotationTypes 中的三种注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.autowiredAnnotationTypes.add(Autowired.class);//！！！！！！！！！！！！！！
this.autowiredAnnotationTypes.add(Value.class);
this.autowiredAnnotationTypes.add(...ClassUtils.forName(&quot;javax.inject.Inject&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; AnnotatedElementUtils.getMergedAnnotationAttributes(ao, type)&lt;/code&gt;：获取注解的属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (attributes != null) return attributes&lt;/code&gt;：任意一个注解属性不为空就注解返回&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;if (ann == null)&lt;/code&gt;：注解属性为空&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;userClass = ClassUtils.getUserClass(beanClass)&lt;/code&gt;：如果当前 beanClass 是代理对象，方法上就已经没有注解了，所以&lt;strong&gt;获取原始的用户类型重新获取该构造器上的注解属性&lt;/strong&gt;（&lt;strong&gt;事务注解失效&lt;/strong&gt;也是这个原理）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;if (ann != null)&lt;/code&gt;：注解属性不为空了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;required = determineRequiredStatus(ann)&lt;/code&gt;：获取 required 属性的值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;!ann.containsKey(this.requiredParameterName) || &lt;/code&gt;：判断属性是否包含 required，不包含进入后面逻辑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.requiredParameterValue == ann.getBoolean(this.requiredParameterName)&lt;/code&gt;：获取属性值返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (required)&lt;/code&gt;：代表注解 @Autowired(required = true)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (!candidates.isEmpty())&lt;/code&gt;：true 代表只能有一个构造方法，构造集合不是空代表可选的构造器不唯一，报错&lt;/p&gt;
&lt;p&gt;&lt;code&gt;requiredConstructor = candidate&lt;/code&gt;：把构造器赋值给 requiredConstructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;candidates.add(candidate)&lt;/code&gt;：把当前构造方法添加至 candidates 集合&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt; if(candidate.getParameterCount() == 0)&lt;/code&gt;：当前遍历的构造器的参数为 0 代表没有参数，是&lt;strong&gt;默认构造器&lt;/strong&gt;，赋值给 defaultConstructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;candidateConstructors = candidates.toArray(new Constructor&amp;lt;?&amp;gt;[0])&lt;/code&gt;：&lt;strong&gt;将构造器转成数组返回&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if(ctors != null)&lt;/code&gt;：条件成立代表指定了&lt;strong&gt;构造方法数组&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR&lt;/code&gt;：&amp;lt;bean autowire=&quot;&quot;&amp;gt; 标签内 autowiremode 的属性值，默认是 no，AUTOWIRE_CONSTRUCTOR 代表选择最优的构造方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mbd.hasConstructorArgumentValues()&lt;/code&gt;：bean 信息中是否配置了构造参数的值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!ObjectUtils.isEmpty(args)&lt;/code&gt;：getBean 时，指定了参数 arg&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return autowireConstructor(beanName, mbd, ctors, args)&lt;/code&gt;：&lt;strong&gt;选择最优的构造器进行创建实例&lt;/strong&gt;（复杂，不建议研究）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;beanFactory.initBeanWrapper(bw)&lt;/code&gt;：向 BeanWrapper 中注册转换器，向工厂中注册属性编辑器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Constructor&amp;lt;?&amp;gt; constructorToUse = null&lt;/code&gt;：实例化反射构造器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ArgumentsHolder argsHolderToUse&lt;/code&gt;：实例化时真正去用的参数，并持有对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rawArguments 是转换前的参数，arguments 是类型转换完成的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Object[] argsToUse&lt;/code&gt;：参数实例化时使用的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object[] argsToResolve&lt;/code&gt;：表示构造器参数做转换后的参数引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (constructorToUse != null &amp;amp;&amp;amp; mbd.constructorArgumentsResolved)&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件一成立说明当前 bd 生成的实例不是第一次，缓存中有解析好的构造器方法可以直接拿来反射调用&lt;/li&gt;
&lt;li&gt;条件二成立说明构造器参数已经解析过了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;argsToUse = resolvePreparedArguments()&lt;/code&gt;：argsToResolve 不是完全解析好的，还需要继续解析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (constructorToUse == null || argsToUse == null)&lt;/code&gt;：条件成立说明缓存机制失败，进入构造器匹配逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Constructor&amp;lt;?&amp;gt;[] candidates = chosenCtors&lt;/code&gt;：chosenCtors  只有在构造方法上有 autowaire 三种注解时才有数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (candidates == null)&lt;/code&gt;：candidates 为空就根据 beanClass 是否允许访问非公开的方法来获取构造方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (candidates.length == 1 &amp;amp;&amp;amp; explicitArgs == null &amp;amp;&amp;amp; !mbd.hasConstructorArgumentValues())&lt;/code&gt;：默认无参&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bw.setBeanInstance(instantiate())&lt;/code&gt;：&lt;strong&gt;使用无参构造器反射调用，创建出实例对象，设置到 BeanWrapper 中去&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean autowiring&lt;/code&gt;：&lt;strong&gt;需要选择最优的构造器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargs = mbd.getConstructorArgumentValues()&lt;/code&gt;：获取参数值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;resolvedValues = new ConstructorArgumentValues()&lt;/code&gt;：获取已经解析后的构造器参数值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final Map&amp;lt;Integer, ValueHolder&amp;gt; indexedArgumentValues&lt;/code&gt;：key 是 index， value 是值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final List&amp;lt;ValueHolder&amp;gt; genericArgumentValues&lt;/code&gt;：没有 index 的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;minNrOfArgs = resolveConstructorArguments(..,resolvedValues)&lt;/code&gt;：从 bd 中解析并获取构造器参数的个数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;valueResolver.resolveValueIfNecessary()&lt;/code&gt;：将引用转换成真实的对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolvedValueHolder.setSource(valueHolder)&lt;/code&gt;：将对象填充至 ValueHolder 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt; resolvedValues.addIndexedArgumentValue()&lt;/code&gt;：将参数值封装至 resolvedValues 中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AutowireUtils.sortConstructors(candidates)&lt;/code&gt;：排序规则 public &amp;gt; 非公开的 &amp;gt; 参数多的 &amp;gt; 参数少的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; int minTypeDiffWeight = Integer.MAX_VALUE&lt;/code&gt;：值越低说明构造器&lt;strong&gt;参数列表类型&lt;/strong&gt;和构造参数的匹配度越高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Set&amp;lt;Constructor&amp;lt;?&amp;gt;&amp;gt; ambiguousConstructors&lt;/code&gt;：模棱两可的构造器，两个构造器匹配度相等时放入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Constructor&amp;lt;?&amp;gt; candidate : candidates)&lt;/code&gt;：遍历筛选出 minTypeDiffWeight 最低的构造器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Class&amp;lt;?&amp;gt;[] paramTypes = candidate.getParameterTypes()&lt;/code&gt;：获取当前处理的构造器的参数类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if()&lt;/code&gt;：candidates 是排过序的，当前筛选出来的构造器的优先级一定是优先于后面的 constructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (paramTypes.length &amp;lt; minNrOfArgs)&lt;/code&gt;：需求的小于给的，不匹配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int typeDiffWeight&lt;/code&gt;：获取匹配度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mbd.isLenientConstructorResolution()&lt;/code&gt;：true 表示 ambiguousConstructors 允许有数据，false 代表不允许有数据，有数据就报错（LenientConstructorResolution：宽松的构造函数解析）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argsHolder.getTypeDifferenceWeight(paramTypes)&lt;/code&gt;：选择参数转换前和转换后匹配度最低的，循环向父类中寻找该方法，直到寻找到 Obejct 类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; if (typeDiffWeight &amp;lt; minTypeDiffWeight)&lt;/code&gt;：条件成立说明当前循环处理的构造器更优&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else if (constructorToUse != null &amp;amp;&amp;amp; typeDiffWeight == minTypeDiffWeight)&lt;/code&gt;：当前处理的构造器的计算出来的 DiffWeight 与上一次筛选出来的最优构造器的值一致，说明有模棱两可的情况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (constructorToUse == null)&lt;/code&gt;：未找到可以使用的构造器，报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; else if (ambiguousConstructors != null &amp;amp;&amp;amp; !mbd.isLenientConstructorResolution())&lt;/code&gt;：模棱两可有数据，LenientConstructorResolution == false，所以报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;argsHolderToUse.storeCache(mbd, constructorToUse)&lt;/code&gt;：匹配成功，进行缓存，方便后来者使用该 bd 实例化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse))&lt;/code&gt;：匹配成功调用 instantiate 创建出实例对象，设置到 BeanWrapper 中去&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return instantiateBean(beanName, mbd)&lt;/code&gt;：默认走到这里&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;循环依赖&lt;/h4&gt;
&lt;h5&gt;循环引用&lt;/h5&gt;
&lt;p&gt;循环依赖：是一个或多个对象实例之间存在直接或间接的依赖关系，这种依赖关系构成一个环形调用&lt;/p&gt;
&lt;p&gt;Spring 循环依赖有四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DependsOn 依赖加载【无法解决】（两种 Map）&lt;/li&gt;
&lt;li&gt;原型模式 Prototype 循环依赖【无法解决】（正在创建集合）&lt;/li&gt;
&lt;li&gt;单例 Bean 循环依赖：构造参数产生依赖【无法解决】（正在创建集合，getSingleton() 逻辑中）&lt;/li&gt;
&lt;li&gt;单例 Bean 循环依赖：setter 产生依赖【可以解决】&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决循环依赖：提前引用，提前暴露创建中的 Bean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring 先实例化 A，拿到 A 的构造方法反射创建出来 A 的早期实例对象，这个对象被包装成 ObjectFactory 对象，放入三级缓存&lt;/li&gt;
&lt;li&gt;处理 A 的依赖数据，检查发现 A 依赖 B 对象，所以 Spring 就会去根据 B 类型到容器中去 getBean(B)，这里产生递归&lt;/li&gt;
&lt;li&gt;拿到 B 的构造方法，进行反射创建出来 B 的早期实例对象，也会把 B 包装成 ObjectFactory 对象，放到三级缓存，处理 B 的依赖数据，检查发现 B 依赖了 A 对象，然后 Spring 就会去根据 A 类型到容器中去 getBean(A.class)&lt;/li&gt;
&lt;li&gt;这时从三级缓存中获取到 A 的早期对象进入属性填充&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;循环依赖的三级缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//一级缓存：存放所有初始化完成单实例 bean，单例池，key是beanName，value是对应的单实例对象引用
private final Map&amp;lt;String, Object&amp;gt; singletonObjects = new ConcurrentHashMap&amp;lt;&amp;gt;(256);

//二级缓存：存放实例化未进行初始化的 Bean，提前引用池
private final Map&amp;lt;String, Object&amp;gt; earlySingletonObjects = new HashMap&amp;lt;&amp;gt;(16);

/** Cache of singleton factories: bean name to ObjectFactory. 3*/
private final Map&amp;lt;String, ObjectFactory&amp;lt;?&amp;gt;&amp;gt; singletonFactories = new HashMap&amp;lt;&amp;gt;(16);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么需要三级缓存？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环依赖解决需要提前引用动态代理对象，AOP 动态代理是在 Bean 初始化后的后置处理中进行，这时的 bean 已经是成品对象。因为需要提前进行动态代理，三级缓存的 ObjectFactory 提前产生需要代理的对象，把提前引用放入二级缓存&lt;/li&gt;
&lt;li&gt;如果只有二级缓存，提前引用就直接放入了一级缓存，然后 Bean 初始化完成后又会放入一级缓存，产生数据覆盖，&lt;strong&gt;导致提前引用的对象和一级缓存中的并不是同一个对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一级缓存只能存放完整的单实例，&lt;strong&gt;为了保证 Bean 的生命周期不被破坏&lt;/strong&gt;，不能将未初始化的 Bean 暴露到一级缓存&lt;/li&gt;
&lt;li&gt;若存在循环依赖，&lt;strong&gt;后置处理不创建代理对象，真正创建代理对象的过程是在 getBean(B) 的阶段中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三级缓存一定会创建提前引用吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出现循环依赖就会去三级缓存获取提前引用，不出现就不会，走正常的逻辑，创建完成直接放入一级缓存&lt;/li&gt;
&lt;li&gt;存在循环依赖，就创建代理对象放入二级缓存，如果没有增强方法就返回 createBeanInstance 创建的实例，因为 addSingletonFactory 参数中传入了实例化的 Bean，在 singletonFactory.getObject() 中返回给 singletonObject，所以&lt;strong&gt;存在循环依赖就一定会使用工厂&lt;/strong&gt;，但是不一定创建的是代理对象，不需要增强就是原始对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wrapIfNecessary 一定创建代理对象吗？（AOP 动态代理部分有源码解析）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存在增强器会创建动态代理，不需要增强就不需要创建动态代理对象&lt;/li&gt;
&lt;li&gt;存在循环依赖会提前增强，初始化后不需要增强&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么时候将 Bean 的引用提前暴露给第三级缓存的 ObjectFactory 持有？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例化之后，依赖注入之前&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;createBeanInstance -&amp;gt; addSingletonFactory -&amp;gt; populateBean
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;源码解析&lt;/h5&gt;
&lt;p&gt;假如 A 依赖 B，B 依赖 A&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当 A 创建实例后填充属性前，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addSingletonFactory(beanName, () -&amp;gt; getEarlyBeanReference(beanName, mbd, bean))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 添加给定的单例工厂以构建指定的单例
protected void addSingletonFactory(String beanName, ObjectFactory&amp;lt;?&amp;gt; singletonFactory) {
    Assert.notNull(singletonFactory, &quot;Singleton factory must not be null&quot;);
    synchronized (this.singletonObjects) {
        // 单例池包含该Bean说明已经创建完成，不需要循环依赖
        if (!this.singletonObjects.containsKey(beanName)) {
            //加入三级缓存
            this.singletonFactories.put(beanName,singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            // 从二级缓存移除，因为三个Map中都是一个对象，不能同时存在！
            this.registeredSingletons.add(beanName);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;填充属性时 A 依赖 B，这时需要 getBean(B)，也会把 B 的工厂放入三级缓存，接着 B 填充属性时发现依赖 A，去进行**第一次 ** getSingleton(A)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);//为true代表允许拿到早期引用。
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 在一级缓存中获取 beanName 对应的单实例对象。
    Object singletonObject = this.singletonObjects.get(beanName);
    // 单实例确实尚未创建；单实例正在创建，发生了循环依赖
    if (singletonObject == null &amp;amp;&amp;amp; isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 从二级缓存获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 二级缓存不存在，并且允许获取早期实例对象，去三级缓存查看
            if (singletonObject == null &amp;amp;&amp;amp; allowEarlyReference) {
                ObjectFactory&amp;lt;?&amp;gt; singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 从三级缓存获取工厂对象，并得到 bean 的提前引用
                    singletonObject = singletonFactory.getObject();
                    // 【缓存升级】，放入二级缓存，提前引用池
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 从三级缓存移除该对象
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从三级缓存获取 A 的 Bean：&lt;code&gt;singletonFactory.getObject()&lt;/code&gt;，调用了 lambda 表达式的 getEarlyBeanReference 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 【向提前引用代理池 earlyProxyReferences 中添加该 Bean，防止对象被重新代理】
    this.earlyProxyReferences.put(cacheKey, bean);
    // 创建代理对象，createProxy
    return wrapIfNecessary(bean, beanName, cacheKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B 填充了 A 的提前引用后会继续初始化直到完成，&lt;strong&gt;返回原始 A 的逻辑继续执行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;AOP&lt;/h3&gt;
&lt;h4&gt;注解原理&lt;/h4&gt;
&lt;p&gt;@EnableAspectJAutoProxy：AOP 注解驱动，给容器中导入 AspectJAutoProxyRegistrar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    // 是否强制使用 CGLIB 创建代理对象 
    // 配置文件方式：&amp;lt;aop:aspectj-autoproxy proxy-target-class=&quot;true&quot;/&amp;gt;
	boolean proxyTargetClass() default false;
	
    // 将当前代理对象暴露到上下文内，方便代理对象内部的真实对象拿到代理对象
    // 配置文件方式：&amp;lt;aop:aspectj-autoproxy expose-proxy=&quot;true&quot;/&amp;gt;
	boolean exposeProxy() default false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AspectJAutoProxyRegistrar 在用来向容器中注册 &lt;strong&gt;AnnotationAwareAspectJAutoProxyCreator&lt;/strong&gt;，以 BeanDefiantion 形式存在，在容器初始化时加载。AnnotationAwareAspectJAutoProxyCreator 间接实现了 InstantiationAwareBeanPostProcessor，Order 接口，该类会在 Bean 的实例化和初始化的前后起作用&lt;/p&gt;
&lt;p&gt;工作流程：创建 IOC 容器，调用 refresh() 刷新容器，&lt;code&gt;registerBeanPostProcessors(beanFactory)&lt;/code&gt; 阶段，通过 getBean() 创建 AnnotationAwareAspectJAutoProxyCreator 对象，在生命周期的初始化方法中执行回调 initBeanFactory() 方法初始化注册三个工具类：BeanFactoryAdvisorRetrievalHelperAdapter、ReflectiveAspectJAdvisorFactory、BeanFactoryAspectJAdvisorsBuilderAdapter&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;后置处理&lt;/h4&gt;
&lt;p&gt;Bean 初始化完成的执行后置处理器的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object postProcessAfterInitialization(@Nullable Object bean,String bN){
    if (bean != null) {
        // cacheKey 是 【beanName 或者加上 &amp;amp; 的 beanName】
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                // 去提前代理引用池中寻找该 key，不存在则创建代理
                // 如果存在则证明被代理过，则判断是否是当前的 bean，不是则创建代理
                return wrapIfNecessary(bean, bN, cacheKey);
            }
    }
    return bean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractAutoProxyCreator.wrapIfNecessary()：根据通知创建动态代理，没有通知直接返回原实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 条件一般不成立，很少使用 TargetSourceCreator 去创建对象 BeforeInstantiation 阶段，doCreateBean 之前的阶段
    if (StringUtils.hasLength(beanName) &amp;amp;&amp;amp; this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    // advisedBeans 集合保存的是 bean 是否被增强过了
    // 条件成立说明当前 beanName 对应的实例不需要被增强处理，判断是在 BeforeInstantiation 阶段做的
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    // 条件一：判断当前 bean 类型是否是基础框架类型，这个类的实例不能被增强
    // 条件二：shouldSkip 判断当前 beanName 是否是 .ORIGINAL 结尾，如果是就跳过增强逻辑，直接返回
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // 【查找适合当前 bean 实例的增强方法】（下一节详解）
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    // 条件成立说明上面方法查询到适合当前class的通知
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 根据查询到的增强创建代理对象（下一节详解）
        // 参数一：目标对象
        // 参数二：beanName
        // 参数三：匹配当前目标对象 clazz 的 Advisor 数据
        Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        // 保存代理对象类型
        this.proxyTypes.put(cacheKey, proxy.getClass());
        // 返回代理对象
        return proxy;
    }
	// 执行到这里说明没有查到通知，当前 bean 不需要增强
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    // 【返回原始的 bean 实例】
    return bean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;获取通知&lt;/h4&gt;
&lt;p&gt;AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean()：查找适合当前类实例的增强，并进行排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected Object[] getAdvicesAndAdvisorsForBean(Class&amp;lt;?&amp;gt; beanClass, String beanName, @Nullable TargetSource targetSource) {
	// 查询适合当前类型的增强通知
    List&amp;lt;Advisor&amp;gt; advisors = findEligibleAdvisors(beanClass, beanName);
    if (advisors.isEmpty()) {
        // 增强为空直接返回 null，不需要创建代理
        return DO_NOT_PROXY;
    }
    // 不是空，转成数组返回
    return advisors.toArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractAdvisorAutoProxyCreator.findEligibleAdvisors()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;candidateAdvisors = findCandidateAdvisors()&lt;/code&gt;：&lt;strong&gt;获取当前容器内可以使用（所有）的 advisor&lt;/strong&gt;，调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法，每个方法对应一个 Advisor&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;advisors = super.findCandidateAdvisors()&lt;/code&gt;：&lt;strong&gt;查询出 XML 配置的所有 Advisor 类型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors()&lt;/code&gt;：通过 BF 查询出来 BD 配置的 class 中 是 Advisor 子类的 BeanName&lt;/li&gt;
&lt;li&gt;&lt;code&gt;advisors.add()&lt;/code&gt;：使用 Spring 容器获取当前这个 Advisor 类型的实例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;advisors.addAll(....buildAspectJAdvisors())&lt;/code&gt;：&lt;strong&gt;获取所有添加 @Aspect 注解类中的 Advisor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;buildAspectJAdvisors()&lt;/code&gt;：构建的方法，&lt;strong&gt;把 Advice 封装成 Advisor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false)&lt;/code&gt;：获取出容器内 Object 所有的 beanName，就是全部的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; for (String beanName : beanNames)&lt;/code&gt;：遍历所有的 beanName，判断每个 beanName 对应的 Class 是否是 Aspect 类型，就是加了 @Aspect 注解的类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName)&lt;/code&gt;：使用工厂模式管理 Aspect 的元数据，关联的真实 @Aspect 注解的实例对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;classAdvisors = this.advisorFactory.getAdvisors(factory)&lt;/code&gt;：添加了 @Aspect 注解的类的通知信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;aspectClass：@Aspect 标签的类的 class&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Method method : getAdvisorMethods(aspectClass))&lt;/code&gt;：遍历&lt;strong&gt;不包括 @Pointcut 注解的方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName)&lt;/code&gt;：&lt;strong&gt;将当前 method 包装成 Advisor 数据&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AspectJExpressionPointcut expressionPointcut = getPointcut()&lt;/code&gt;：获取切点表达式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return new InstantiationModelAwarePointcutAdvisorImpl()&lt;/code&gt;：把 method 中 Advice 包装成 Advisor，Spring 中每个 Advisor 内部一定是持有一个 Advice 的，Advice 内部最重要的数据是当前 method 和aspectInstanceFactory，工厂用来获取实例&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut)&lt;/code&gt;：实例化 Advice 对象，逻辑是获取注解信息，根据注解的不同生成对应的 Advice 对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;advisors.addAll(classAdvisors)&lt;/code&gt;：保存通过 @Aspect 注解定义的 Advisor 数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.aspectBeanNames = aspectNames&lt;/code&gt;：将所有 @Aspect 注解 beanName 缓存起来，表示提取 Advisor 工作完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return advisors&lt;/code&gt;：返回 Advisor 列表&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, ...)&lt;/code&gt;：&lt;strong&gt;选出匹配当前类的增强&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (candidateAdvisors.isEmpty())&lt;/code&gt;：条件成立说明当前 Spring 没有可以操作的 Advisor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;Advisor&amp;gt; eligibleAdvisors = new ArrayList&amp;lt;&amp;gt;()&lt;/code&gt;：存放匹配当前 beanClass 的 Advisors 信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Advisor candidate : candidateAdvisors)&lt;/code&gt;：&lt;strong&gt;遍历所有的 Advisor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; if (canApply(candidate, clazz, hasIntroductions))&lt;/code&gt;：判断遍历的 advisor 是否匹配当前的 class，匹配就加入集合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (advisor instanceof PointcutAdvisor)&lt;/code&gt;：创建的 advisor 是 InstantiationModelAwarePointcutAdvisorImpl 类型&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PointcutAdvisor pca = (PointcutAdvisor) advisor&lt;/code&gt;：封装当前 Advisor&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return canApply(pca.getPointcut(), targetClass, hasIntroductions)&lt;/code&gt;：重载该方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (!pc.getClassFilter().matches(targetClass))&lt;/code&gt;：&lt;strong&gt;类不匹配 Pointcut 表达式，直接返回 false&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;methodMatcher = pc.getMethodMatcher()&lt;/code&gt;：&lt;strong&gt;获取 Pointcut 方法匹配器&lt;/strong&gt;，类匹配进行类中方法的匹配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes&lt;/code&gt;：保存目标对象 class 和目标对象父类超类的接口和自身实现的接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!Proxy.isProxyClass(targetClass))&lt;/code&gt;：判断当前实例是不是代理类，确保 class 内存储的数据包括目标对象的class  而不是代理类的 class&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for (Class&amp;lt;?&amp;gt; clazz : classes)&lt;/code&gt;：&lt;strong&gt;检查目标 class 和上级接口的所有方法，查看是否会被方法匹配器匹配&lt;/strong&gt;，如果有一个方法匹配成功，就说明目标对象 AOP 代理需要增强
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;specificMethod = AopUtils.getMostSpecificMethod(method, targetClass)&lt;/code&gt;：方法可能是接口的，判断当前类有没有该方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return (specificMethod != method &amp;amp;&amp;amp; matchesMethod(specificMethod))&lt;/code&gt;：&lt;strong&gt;类和方法的匹配&lt;/strong&gt;，不包括参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;extendAdvisors(eligibleAdvisors)&lt;/code&gt;：在 eligibleAdvisors 列表的索引 0 的位置添加 DefaultPointcutAdvisor，&lt;strong&gt;封装了 ExposeInvocationInterceptor 拦截器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; eligibleAdvisors = sortAdvisors(eligibleAdvisors)&lt;/code&gt;：&lt;strong&gt;对拦截器进行排序&lt;/strong&gt;，数值越小优先级越高，高的排在前面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现 Ordered 或 PriorityOrdered 接口，PriorityOrdered 的级别要优先于 Ordered，使用 OrderComparator 比较器&lt;/li&gt;
&lt;li&gt;使用 @Order（Spring 规范）或 @Priority（JDK 规范）注解，使用 AnnotationAwareOrderComparator 比较器&lt;/li&gt;
&lt;li&gt;ExposeInvocationInterceptor 实现了 PriorityOrdered ，所以总是排在第一位，MethodBeforeAdviceInterceptor 没实现任何接口，所以优先级最低，排在最后&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return eligibleAdvisors&lt;/code&gt;：返回拦截器链&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;创建代理&lt;/h4&gt;
&lt;p&gt;AbstractAutoProxyCreator.createProxy()：根据增强方法创建代理对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ProxyFactory proxyFactory = new ProxyFactory()&lt;/code&gt;：&lt;strong&gt;无参构造 ProxyFactory&lt;/strong&gt;，此处讲解一下两种有参构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;public ProxyFactory(Object target)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ProxyFactory(Object target) {
	// 将目标对象封装成 SingletonTargetSource 保存到父类的字段中
   	setTarget(target);
    // 获取目标对象 class 所有接口保存到 AdvisedSupport 中的 interfaces 集合中
   	setInterfaces(ClassUtils.getAllInterfaces(target));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ClassUtils.getAllInterfaces(target) 底层调用 getAllInterfacesForClassAsSet(java.lang.Class&amp;lt;?&amp;gt;, java.lang.ClassLoader)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (clazz.isInterface() &amp;amp;&amp;amp; isVisible(clazz, classLoader))&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;条件一：判断当前目标对象是接口&lt;/li&gt;
&lt;li&gt;条件二：检查给定的类在给定的 ClassLoader 中是否可见&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Class&amp;lt;?&amp;gt;[] ifcs = current.getInterfaces()&lt;/code&gt;：拿到自己实现的接口，拿不到接口实现的接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current = current.getSuperclass()&lt;/code&gt;：递归寻找父类的接口，去获取父类实现的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;public ProxyFactory(Class&amp;lt;?&amp;gt; proxyInterface, Interceptor interceptor)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ProxyFactory(Class&amp;lt;?&amp;gt; proxyInterface, Interceptor interceptor) {
    // 添加一个代理的接口
    addInterface(proxyInterface);
    // 添加通知，底层调用 addAdvisor
    addAdvice(interceptor);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;addAdvisor(pos, new DefaultPointcutAdvisor(advice))&lt;/code&gt;：Spring 中 Advice 对应的接口就是 Advisor，Spring 使用 Advisor 包装 Advice 实例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;proxyFactory.copyFrom(this)&lt;/code&gt;：填充一些信息到 proxyFactory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!proxyFactory.isProxyTargetClass())&lt;/code&gt;：条件成立说明 proxyTargetClass 为 false（默认），两种配置方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;aop:aspectj-autoproxy proxy-target-class=&quot;true&quot;/&amp;gt; &lt;/code&gt;：强制使用 CGLIB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableAspectJAutoProxy(proxyTargetClass = true)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;if (shouldProxyTargetClass(beanClass, beanName))&lt;/code&gt;：如果 bd 内有 preserveTargetClass = true ，那么这个 bd 对应的 class &lt;strong&gt;创建代理时必须使用 CGLIB&lt;/strong&gt;，条件成立设置 proxyTargetClass 为 true&lt;/p&gt;
&lt;p&gt;&lt;code&gt;evaluateProxyInterfaces(beanClass, proxyFactory)&lt;/code&gt;：&lt;strong&gt;根据目标类判定是否可以使用 JDK 动态代理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;targetInterfaces = ClassUtils.getAllInterfacesForClass()&lt;/code&gt;：获取当前目标对象 class 和父类的全部实现接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean hasReasonableProxyInterface = false&lt;/code&gt;：实现的接口中是否有一个合理的接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (!isConfigurationCallbackInterface(ifc) &amp;amp;&amp;amp; !isInternalLanguageInterface(ifc) &amp;amp;&amp;amp; ifc.getMethods().length &amp;gt; 0)&lt;/code&gt;：遍历所有的接口，如果有任意一个接口满足条件，设置 hRPI 变量为 true
&lt;ul&gt;
&lt;li&gt;条件一：判断当前接口是否是 Spring 生命周期内会回调的接口&lt;/li&gt;
&lt;li&gt;条件二：接口不能是 GroovyObject、Factory、MockAccess 类型的&lt;/li&gt;
&lt;li&gt;条件三：找到一个可以使用的被代理的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (hasReasonableProxyInterface)&lt;/code&gt;：&lt;strong&gt;有合理的接口，将这些接口设置到 proxyFactory 内&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxyFactory.setProxyTargetClass(true)&lt;/code&gt;：&lt;strong&gt;没有合理的代理接口，强制使用 CGLIB 创建对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;advisors = buildAdvisors(beanName, specificInterceptors)&lt;/code&gt;：匹配目标对象 clazz 的 Advisors，填充至 ProxyFactory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;proxyFactory.setPreFiltered(true)&lt;/code&gt;：设置为 true 表示传递给 proxyFactory 的 Advisors 信息做过基础类和方法的匹配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return proxyFactory.getProxy(getProxyClassLoader())&lt;/code&gt;：创建代理对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object getProxy() {
    return createAopProxy().getProxy();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DefaultAopProxyFactory.createAopProxy(AdvisedSupport config)：参数是一个配置对象，保存着创建代理需要的生产资料，会加锁创建，保证线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    // 条件二为 true 代表强制使用 CGLIB 动态代理
    if (config.isOptimize() || config.isProxyTargetClass() || 
        // 条件三：被代理对象没有实现任何接口或者只实现了 SpringProxy 接口，只能使用 CGLIB 动态代理
        hasNoUserSuppliedProxyInterfaces(config)) {
        Class&amp;lt;?&amp;gt; targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException(&quot;&quot;);
        }
        // 条件成立说明 target 【是接口或者是已经被代理过的类型】，只能使用 JDK 动态代理
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);	// 使用 JDK 动态代理
        }
        return new ObjenesisCglibAopProxy(config);	// 使用 CGLIB 动态代理
    }
    else {
        return new JdkDynamicAopProxy(config);		// 【有接口的情况下只能使用 JDK 动态代理】
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JdkDynamicAopProxy.getProxy(java.lang.ClassLoader)：获取 JDK 的代理对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
      // 配置类封装到 JdkDynamicAopProxy.advised 属性中
      this.advised = config;
  }
  public Object getProxy(@Nullable ClassLoader classLoader) {
      // 获取需要代理的接口数组
      Class&amp;lt;?&amp;gt;[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
      
      // 查找当前所有的需要代理的接口，看是否有 equals 方法和 hashcode 方法，如果有就做一个标记
      findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
      
      // 该方法最终返回一个代理类对象
      return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
      // classLoader：类加载器  proxiedInterfaces：生成的代理类，需要实现的接口集合
      // this JdkDynamicAopProxy 实现了 InvocationHandler
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AopProxyUtils.completeProxiedInterfaces(this.advised, true)：获取代理的接口数组，并添加 SpringProxy 接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;specifiedInterfaces = advised.getProxiedInterfaces()&lt;/code&gt;：从 ProxyFactory 中拿到所有的 target 提取出来的接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (specifiedInterfaces.length == 0)&lt;/code&gt;：如果没有实现接口，检查当前 target 是不是接口或者已经是代理类，封装到 ProxyFactory 的 interfaces 集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)&lt;/code&gt;：判断目标对象所有接口中是否有 SpringProxy 接口，没有的话需要添加，这个接口&lt;strong&gt;标识这个代理类型是 Spring 管理的&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;addAdvised = !advised.isOpaque() &amp;amp;&amp;amp; !advised.isInterfaceProxied(Advised.class)&lt;/code&gt;：判断目标对象的所有接口，是否已经有 Advised 接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt; addDecoratingProxy = (decoratingProxy &amp;amp;&amp;amp; !advised.isInterfaceProxied(DecoratingProxy.class))&lt;/code&gt;：判断目标对象的所有接口，是否已经有 DecoratingProxy 接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int nonUserIfcCount = 0&lt;/code&gt;：非用户自定义的接口数量，接下来要添加上面的三个接口了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxiedInterfaces = new Class&amp;lt;?&amp;gt;[specifiedInterfaces.length + nonUserIfcCount]&lt;/code&gt;：创建一个新的 class 数组，长度是原目标对象提取出来的接口数量和 Spring 追加的数量，然后进行 &lt;strong&gt;System.arraycopy 拷贝到新数组中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int index = specifiedInterfaces.length&lt;/code&gt;：获取原目标对象提取出来的接口数量，当作 index&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if(addSpringProxy)&lt;/code&gt;：根据上面三个布尔值把接口添加到新数组中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return proxiedInterfaces&lt;/code&gt;：返回追加后的接口集合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JdkDynamicAopProxy.findDefinedEqualsAndHashCodeMethods()：查找在任何定义在接口中的 equals 和 hashCode 方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (Class&amp;lt;?&amp;gt; proxiedInterface : proxiedInterfaces)&lt;/code&gt;：遍历所有的接口
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; Method[] methods = proxiedInterface.getDeclaredMethods()&lt;/code&gt;：获取接口中的所有方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Method method : methods)&lt;/code&gt;：遍历所有的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (AopUtils.isEqualsMethod(method))&lt;/code&gt;：当前方法是 equals 方法，把 equalsDefined 置为 true&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (AopUtils.isHashCodeMethod(method))&lt;/code&gt;：当前方法是 hashCode 方法，把 hashCodeDefined 置为 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.equalsDefined &amp;amp;&amp;amp; this.hashCodeDefined)&lt;/code&gt;：如果有一个接口中有这两种方法，直接返回&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;方法增强&lt;/h4&gt;
&lt;p&gt;main() 函数中调用用户方法，会进入代理对象的 invoke 方法&lt;/p&gt;
&lt;p&gt;JdkDynamicAopProxy 类中的 invoke 方法是真正执行代理方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// proxy：代理对象，method：目标对象的方法，args：目标对象方法对应的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;

    // advised 就是初始化 JdkDynamicAopProxy 对象时传入的变量
    TargetSource targetSource = this.advised.targetSource;
    Object target = null;

    try {
        // 条件成立说明代理类实现的接口没有定义 equals 方法，并且当前 method 调用 equals 方法，
        // 就调用 JdkDynamicAopProxy 提供的 equals 方法
        if (!this.equalsDefined &amp;amp;&amp;amp; AopUtils.isEqualsMethod(method)) {
            return equals(args[0]);
        } //.....

        Object retVal;
		// 需不需要暴露当前代理对象到 AOP 上下文内
        if (this.advised.exposeProxy) {
            // 【把代理对象设置到上下文环境】
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

        // 根据 targetSource 获取真正的代理对象
        target = targetSource.getTarget();
        Class&amp;lt;?&amp;gt; targetClass = (target != null ? target.getClass() : null);

        // 查找【适合该方法的增强】，首先从缓存中查找，查找不到进入主方法【下文详解】
        List&amp;lt;Object&amp;gt; chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

		// 拦截器链是空，说明当前 method 不需要被增强
        if (chain.isEmpty()) {
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
            // 有匹配当前 method 的方法拦截器，要做增强处理，把方法信息封装到方法调用器里
            MethodInvocation invocation =
                new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // 【拦截器链驱动方法，核心】
            retVal = invocation.proceed();
        }

        Class&amp;lt;?&amp;gt; returnType = method.getReturnType();
        if (retVal != null &amp;amp;&amp;amp; retVal == target &amp;amp;&amp;amp;
            returnType != Object.class &amp;amp;&amp;amp; returnType.isInstance(proxy) &amp;amp;&amp;amp;
            !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
          	// 如果目标方法返回目标对象，这里做个普通替换返回代理对象
            retVal = proxy;
        }
        
        // 返回执行的结果
        return retVal;
    }
    finally {
        if (target != null &amp;amp;&amp;amp; !targetSource.isStatic()) {
            targetSource.releaseTarget(target);
        }
        // 如果允许了提前暴露，这里需要设置为初始状态
        if (setProxyContext) {
            // 当前代理对象已经完成工作，【把原始对象设置回上下文】
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)：查找适合该方法的增强，首先从缓存中查找，获取通知时是从全部增强中获取适合当前类的，这里是&lt;strong&gt;从当前类的中获取适合当前方法的增强&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance()&lt;/code&gt;：向容器注册适配器，&lt;strong&gt;可以将非 Advisor 类型的增强，包装成为 Advisor，将 Advisor 类型的增强提取出来对应的 MethodInterceptor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;instance = new DefaultAdvisorAdapterRegistry()&lt;/code&gt;：该对象向容器中注册了 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter 三个适配器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Advisor 中持有 Advice 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Advisor {
	Advice getAdvice();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;advisors = config.getAdvisors()&lt;/code&gt;：获取 ProxyFactory 内部持有的增强信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;interceptorList = new ArrayList&amp;lt;&amp;gt;(advisors.length)&lt;/code&gt;：拦截器列表有 5 个，1 个 ExposeInvocation和 4 个增强器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())&lt;/code&gt;：真实的目标对象类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Boolean hasIntroductions = null&lt;/code&gt;：引介增强，不关心&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Advisor advisor : advisors)&lt;/code&gt;：&lt;strong&gt;遍历所有的 advisor 增强&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (advisor instanceof PointcutAdvisor)&lt;/code&gt;：条件成立说明当前 Advisor 是包含切点信息的，进入匹配逻辑&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pointcutAdvisor = (PointcutAdvisor) advisor&lt;/code&gt;：转成可以获取到切点信息的接口&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))&lt;/code&gt;：当前代理被预处理，或者当前被代理的 class 对象匹配当前 Advisor 成功，只是 class 匹配成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mm = pointcutAdvisor.getPointcut().getMethodMatcher()&lt;/code&gt;：获取切点的方法匹配器，不考虑引介增强&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;match = mm.matches(method, actualClass)&lt;/code&gt;：&lt;strong&gt;静态匹配成功返回 true，只关注于处理类及其方法，不考虑参数&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (match)&lt;/code&gt;：如果静态切点检查是匹配的，在运行的时候才进行&lt;strong&gt;动态切点检查，会考虑参数匹配&lt;/strong&gt;（代表传入了参数）。如果静态匹配失败，直接不需要进行参数匹配，提高了工作效率&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interceptors = registry.getInterceptors(advisor)&lt;/code&gt;：提取出当前 advisor 内持有的 advice 信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Advice advice = advisor.getAdvice()&lt;/code&gt;：获取增强方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (advice instanceof MethodInterceptor)&lt;/code&gt;：当前 advice 是 MethodInterceptor 直接加入集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (AdvisorAdapter adapter : this.adapters)&lt;/code&gt;：&lt;strong&gt;遍历三个适配器进行匹配&lt;/strong&gt;（初始化时创建的），匹配成功创建对应的拦截器返回，以 MethodBeforeAdviceAdapter 为例&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (adapter.supportsAdvice(advice))&lt;/code&gt;：判断当前 advice 是否是对应的 MethodBeforeAdvice&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interceptors.add(adapter.getInterceptor(advisor))&lt;/code&gt;：条件成立就往拦截器链中添加 advisor&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;advice = (MethodBeforeAdvice) advisor.getAdvice()&lt;/code&gt;：&lt;strong&gt;获取增强方法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return new MethodBeforeAdviceInterceptor(advice)&lt;/code&gt;：&lt;strong&gt;封装成 MethodBeforeAdviceInterceptor 返回&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))&lt;/code&gt;：向拦截器链添加动态匹配器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interceptorList.addAll(Arrays.asList(interceptors))&lt;/code&gt;：将当前 advisor 内部的方法拦截器追加到 interceptorList&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;interceptors = registry.getInterceptors(advisor)&lt;/code&gt;：进入 else 的逻辑，说明当前 Advisor 匹配全部 class 的全部 method，全部加入到 interceptorList&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return interceptorList&lt;/code&gt;：返回 method 方法的拦截器链&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;retVal = invocation.proceed()：&lt;strong&gt;拦截器链驱动方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)&lt;/code&gt;：条件成立说明方法拦截器全部都已经调用过了（index 从 - 1 开始累加），接下来需要执行目标对象的目标方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return invokeJoinpoint()&lt;/code&gt;：&lt;strong&gt;调用连接点（目标）方法&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)&lt;/code&gt;：&lt;strong&gt;获取下一个方法拦截器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)&lt;/code&gt;：需要运行时匹配&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (dm.methodMatcher.matches(this.method, targetClass, this.arguments))&lt;/code&gt;：判断是否匹配成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;return dm.interceptor.invoke(this)&lt;/code&gt;：匹配成功，执行方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return proceed()&lt;/code&gt;：匹配失败跳过当前拦截器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)&lt;/code&gt;：&lt;strong&gt;一般方法拦截器都会执行到该方法，此方法内继续执行 proceed() 完成责任链的驱动，直到最后一个  MethodBeforeAdviceInterceptor 调用前置通知，然后调用 mi.proceed()，发现是最后一个拦截器就直接执行连接点（目标方法），return 到上一个拦截器的 mi.proceed() 处，依次返回到责任链的上一个拦截器执行通知方法&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图示先从上往下建立链，然后从下往上依次执行，责任链模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正常执行：（环绕通知）→ 前置通知 → 目标方法 → 后置通知 → 返回通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;出现异常：（环绕通知）→ 前置通知 → 目标方法 → 后置通知 → 异常通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MethodBeforeAdviceInterceptor 源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(MethodInvocation mi) throws Throwable {
    // 先执行通知方法，再驱动责任链
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    // 开始驱动目标方法执行，执行完后返回到这，然后继续向上层返回
    return mi.proceed();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AfterReturningAdviceInterceptor 源码：没有任何异常处理机制，直接抛给上层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(MethodInvocation mi) throws Throwable {
    // 先驱动责任链，再执行通知方法
    Object retVal = mi.proceed();
    this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
    return retVal;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AspectJAfterThrowingAdvice 执行异常处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        // 默认直接驱动责任链
        return mi.proceed();
    }
    catch (Throwable ex) {
        // 出现错误才执行该方法
        if (shouldInvokeOnThrowing(ex)) {
            invokeAdviceMethod(getJoinPointMatch(), null, ex);
        }
        throw ex;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-AOP%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%89%A7%E8%A1%8C%E6%96%B9%E6%B3%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV1gW411W7wy&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;事务&lt;/h3&gt;
&lt;h4&gt;解析方法&lt;/h4&gt;
&lt;h5&gt;标签解析&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;tx:annotation-driven transaction-manager=&quot;txManager&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;容器启动时会根据注解注册对应的解析器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TxNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
		registerBeanDefinitionParser(&quot;advice&quot;, new TxAdviceBeanDefinitionParser());
        // 注册解析器
		registerBeanDefinitionParser(&quot;annotation-driven&quot;, new AnnotationDrivenBeanDefinitionParser());
		registerBeanDefinitionParser(&quot;jta-transaction-manager&quot;, new JtaTransactionManagerBeanDefinitionParser());
	}
}
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取对应的解析器 NamespaceHandlerSupport#findParserForElement：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    String localName = parserContext.getDelegate().getLocalName(element);
    // 获取对应的解析器
    BeanDefinitionParser parser = this.parsers.get(localName);
	// ...
    return parser;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用解析器的方法对 XML 文件进行解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public BeanDefinition parse(Element element, ParserContext parserContext) {
	// 向Spring容器注册了一个 BD -&amp;gt; TransactionalEventListenerFactory.class
    registerTransactionalEventListenerFactory(parserContext);
    String mode = element.getAttribute(&quot;mode&quot;);
    if (&quot;aspectj&quot;.equals(mode)) {
        // mode=&quot;aspectj&quot;
        registerTransactionAspect(element, parserContext);
        if (ClassUtils.isPresent(&quot;javax.transaction.Transactional&quot;, getClass().getClassLoader())) {
            registerJtaTransactionAspect(element, parserContext);
        }
    }
    else {
        // mode=&quot;proxy&quot;，默认逻辑，不配置 mode 时
        // 用来向容器中注入一些 BeanDefinition，包括事务增强器、事务拦截器、注解解析器
        AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;注解解析&lt;/h5&gt;
&lt;p&gt;@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector，该类给 Spring 容器中两个组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected String[] selectImports(AdviceMode adviceMode) {
    switch (adviceMode) {
        // 导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration（默认）
        case PROXY:
            return new String[] {AutoProxyRegistrar.class.getName(),
                                 ProxyTransactionManagementConfiguration.class.getName()};
        // 导入 AspectJTransactionManagementConfiguration（与声明式事务无关）
        case ASPECTJ:
            return new String[] {determineTransactionAspectClass()};
        default:
            return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AutoProxyRegistrar：给容器中注册 InfrastructureAdvisorAutoProxyCreator，&lt;strong&gt;利用后置处理器机制拦截 bean 以后包装并返回一个代理对象&lt;/strong&gt;，代理对象中保存所有的拦截器，利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行（就是 AOP 原理）&lt;/p&gt;
&lt;p&gt;ProxyTransactionManagementConfiguration：是一个 Spring 的事务配置类，注册了三个 Bean：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BeanFactoryTransactionAttributeSourceAdvisor：事务驱动，利用注解 @Bean 把该类注入到容器中，该增强器有两个字段：&lt;/li&gt;
&lt;li&gt;TransactionAttributeSource：解析事务注解的相关信息，真实类型是 AnnotationTransactionAttributeSource，构造方法中注册了三个&lt;strong&gt;注解解析器&lt;/strong&gt;，解析 Spring、JTA、Ejb3 三种类型的事务注解&lt;/li&gt;
&lt;li&gt;TransactionInterceptor：&lt;strong&gt;事务拦截器&lt;/strong&gt;，代理对象执行拦截器方法时，调用 TransactionInterceptor 的 invoke 方法，底层调用TransactionAspectSupport.invokeWithinTransaction()，通过 PlatformTransactionManager 控制着事务的提交和回滚，所以事务的底层原理就是通过 AOP 动态织入，进行事务开启和提交&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注解解析器 SpringTransactionAnnotationParser &lt;strong&gt;解析 @Transactional 注解&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
    RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
	// 从注解信息中获取传播行为
    Propagation propagation = attributes.getEnum(&quot;propagation&quot;);
    rbta.setPropagationBehavior(propagation.value());
    // 获取隔离界别
    Isolation isolation = attributes.getEnum(&quot;isolation&quot;);
    rbta.setIsolationLevel(isolation.value());
    rbta.setTimeout(attributes.getNumber(&quot;timeout&quot;).intValue());
    // 从注解信息中获取 readOnly 参数
    rbta.setReadOnly(attributes.getBoolean(&quot;readOnly&quot;));
    // 从注解信息中获取 value 信息并且设置 qualifier，表示当前事务指定使用的【事务管理器】
    rbta.setQualifier(attributes.getString(&quot;value&quot;));
	// 【存放的是 rollback 条件】，回滚规则放在这个集合
    List&amp;lt;RollbackRuleAttribute&amp;gt; rollbackRules = new ArrayList&amp;lt;&amp;gt;();
    // 表示事务碰到哪些指定的异常才进行回滚，不指定的话默认是 RuntimeException/Error 非检查型异常菜回滚
    for (Class&amp;lt;?&amp;gt; rbRule : attributes.getClassArray(&quot;rollbackFor&quot;)) {
        rollbackRules.add(new RollbackRuleAttribute(rbRule));
    }
    // 与 rollbackFor 功能相同
    for (String rbRule : attributes.getStringArray(&quot;rollbackForClassName&quot;)) {
        rollbackRules.add(new RollbackRuleAttribute(rbRule));
    }
    // 表示事务碰到指定的 exception 实现对象不进行回滚，否则碰到其他的class就进行回滚
    for (Class&amp;lt;?&amp;gt; rbRule : attributes.getClassArray(&quot;noRollbackFor&quot;)) {
        rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
    }
    for (String rbRule : attributes.getStringArray(&quot;noRollbackForClassName&quot;)) {
        rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
    }
    // 设置回滚规则
    rbta.setRollbackRules(rollbackRules);

    return rbta;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;驱动方法&lt;/h4&gt;
&lt;p&gt;TransactionInterceptor 事务拦截器的核心驱动方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object invoke(MethodInvocation invocation) throws Throwable {
    // targetClass 是需要被事务增强器增强的目标类，invocation.getThis() → 目标对象 → 目标类
    Class&amp;lt;?&amp;gt; targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
	// 参数一是目标方法，参数二是目标类，参数三是方法引用，用来触发驱动方法
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

protected Object invokeWithinTransaction(Method method, @Nullable Class&amp;lt;?&amp;gt; targetClass,
                                         final InvocationCallback invocation) throws Throwable {

    // 事务属性源信息
    TransactionAttributeSource tas = getTransactionAttributeSource();
    //  提取 @Transactional 注解信息，txAttr 是注解信息的承载对象
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    // 获取 Spring 配置的事务管理器
    // 首先会检查是否通过XML或注解配置 qualifier，没有就尝试去容器获取，一般情况下为 DatasourceTransactionManager
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    // 权限定类名.方法名，该值用来当做事务名称使用
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
	// 条件成立说明是【声明式事务】
    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
    	// 用来【开启事务】
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // This is an 【around advice】: Invoke the next interceptor in the chain.
            // 环绕通知，执行目标方法（方法引用方式，invocation::proceed，还是调用 proceed）
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            //  执行业务代码时抛出异常，执行回滚逻辑
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            // 清理事务的信息
            cleanupTransactionInfo(txInfo);
        }
        // 提交事务的入口
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
    else {
       // 编程式事务，省略
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;开启事务&lt;/h4&gt;
&lt;h5&gt;事务绑定&lt;/h5&gt;
&lt;p&gt;创建事务的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
                                                       @Nullable TransactionAttribute txAttr, 
                                                       final String joinpointIdentification) {

    // If no name specified, apply method identification as transaction name.
    if (txAttr != null &amp;amp;&amp;amp; txAttr.getName() == null) {
        // 事务的名称： 类的权限定名.方法名
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }
    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            // 通过事务管理器根据事务属性创建事务状态对象，事务状态对象一般情况下包装着 事务对象，当然也有可能是null
            // 方法上的注解为 @Transactional(propagation = NOT_SUPPORTED || propagation = NEVER) 时
            // 【下一小节详解】
            status = tm.getTransaction(txAttr);
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Skipping transactional joinpoint [&quot; + joinpointIdentification +
                             &quot;] because no transaction manager has been configured&quot;);
            }
        }
    }
    // 包装成一个上层的事务上下文对象
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TransactionAspectSupport#prepareTransactionInfo：为事务的属性和状态准备一个事务信息对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification)&lt;/code&gt;：创建事务信息对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;txInfo.newTransactionStatus(status)&lt;/code&gt;：填充事务的状态信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;txInfo.bindToThread()&lt;/code&gt;：利用 ThreadLocal &lt;strong&gt;把当前事务信息绑定到当前线程&lt;/strong&gt;，不同的事务信息会形成一个栈的结构
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.oldTransactionInfo = transactionInfoHolder.get()&lt;/code&gt;：获取其他事务的信息存入 oldTransactionInfo&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transactionInfoHolder.set(this)&lt;/code&gt;：将当前的事务信息设置到 ThreadLocalMap 中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;事务创建&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
    // 获取事务的对象
    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();

    if (definition == null) {
        // Use defaults if no transaction definition given.
        definition = new DefaultTransactionDefinition();
    }
	// 条件成立说明当前是事务重入的情况，事务中有 ConnectionHolder 对象
    if (isExistingTransaction(transaction)) {
        // a方法开启事务，a方法内调用b方法，b方法仍然加了 @Transactional 注解，需要检查传播行为
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }
    
	// 逻辑到这说明当前线程没有连接资源，一个连接对应一个事务，没有连接就相当于没有开启事务
    // 检查事务的延迟属性
    if (definition.getTimeout() &amp;lt; TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException(&quot;Invalid transaction timeout&quot;, definition.getTimeout());
    }

    // 传播行为是 MANDATORY，没有事务就抛出异常
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException();
    }
    // 需要开启事务的传播行为
    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
             definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
             definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // 什么也没挂起，因为线程并没有绑定事务
        SuspendedResourcesHolder suspendedResources = suspend(null);
        try {
            // 是否支持同步线程事务，一般是 true
            boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            // 新建一个事务状态信息
            DefaultTransactionStatus status = newTransactionStatus(
                definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
            // 【启动事务】
            doBegin(transaction, definition);
            // 设置线程上下文变量，方便程序运行期间获取当前事务的一些核心的属性，initSynchronization() 启动同步
            prepareSynchronization(status, definition);
            return status;
        }
        catch (RuntimeException | Error ex) {
            // 恢复现场
            resume(null, suspendedResources);
            throw ex;
        }
    }
    // 不支持事务的传播行为
    else {
        // Create &quot;empty&quot; transaction: no actual transaction, but potentially synchronization.
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        // 创建事务状态对象
        // 参数2 transaction 是 null 说明当前事务状态是未手动开启事，线程上未绑定任何的连接资源，业务程序执行时需要先去 datasource 获取的 conn，是自动提交事务的，不需要 Spring 再提交事务
        // 参数6 suspendedResources 是 null 说明当前事务状态未挂起任何事务，当前事务执行到后置处理时不需要恢复现场
        return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DataSourceTransactionManager#doGetTransaction：真正获取事务的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DataSourceTransactionObject txObject = new DataSourceTransactionObject()&lt;/code&gt;：&lt;strong&gt;创建事务对象&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject.setSavepointAllowed(isNestedAllowed())&lt;/code&gt;：设置事务对象是否支持保存点，由事务管理器控制（默认不支持）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(obtainDataSource())&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从 ThreadLocal 中获取 conHolder 资源，可能拿到 null 或者不是 null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是 null：举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transaction
public void a() {...b.b()....}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不是 null：执行 b 方法事务增强的前置逻辑时，可以拿到 a 放进去的 conHolder 资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transaction
public void b() {....}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject.setConnectionHolder(conHolder, false)&lt;/code&gt;：将 ConnectionHolder 保存到事务对象内，参数二是 false 代表连接资源是上层事务共享的，不是新建的连接资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return txObject&lt;/code&gt;：返回事务的对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DataSourceTransactionManager#doBegin：事务开启的逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject = (DataSourceTransactionObject) transaction&lt;/code&gt;：强转为事务对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务中没有数据库连接资源就要分配：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Connection newCon = obtainDataSource().getConnection()&lt;/code&gt;：&lt;strong&gt;获取 JDBC 原生的数据库连接对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;txObject.setConnectionHolder(new ConnectionHolder(newCon), true)&lt;/code&gt;：代表是新开启的事务，新建的连接对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition)&lt;/code&gt;：修改连接属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (definition != null &amp;amp;&amp;amp; definition.isReadOnly())&lt;/code&gt;：注解（或 XML）配置了只读属性，需要设置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (..definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT)&lt;/code&gt;：注解配置了隔离级别&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int currentIsolation = con.getTransactionIsolation()&lt;/code&gt;：获取连接的隔离界别&lt;/p&gt;
&lt;p&gt;&lt;code&gt;previousIsolationLevel = currentIsolation&lt;/code&gt;：保存之前的隔离界别，返回该值&lt;/p&gt;
&lt;p&gt;&lt;code&gt; con.setTransactionIsolation(definition.getIsolationLevel())&lt;/code&gt;：&lt;strong&gt;将当前连接设置为配置的隔离界别&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject.setPreviousIsolationLevel(previousIsolationLevel)&lt;/code&gt;：将 Conn 原来的隔离级别保存到事务对象，为了释放 Conn 时重置回原状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (con.getAutoCommit())&lt;/code&gt;：默认会成立，说明还没开启事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;txObject.setMustRestoreAutoCommit(true)&lt;/code&gt;：保存 Conn 原来的事务状态&lt;/p&gt;
&lt;p&gt;&lt;code&gt;con.setAutoCommit(false)&lt;/code&gt;：&lt;strong&gt;开启事务，JDBC 原生的方式&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject.getConnectionHolder().setTransactionActive(true)&lt;/code&gt;：表示 Holder 持有的 Conn 已经手动开启事务了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())&lt;/code&gt;：将 ConnectionHolder 对象绑定到 ThreadLocal 内，数据源为 key，为了方便获取手动开启事务的连接对象去执行 SQL&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;事务重入&lt;/h5&gt;
&lt;p&gt;事务重入的核心处理逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private TransactionStatus handleExistingTransaction( TransactionDefinition definition, 
                                                    Object transaction, boolean debugEnabled){
	// 传播行为是 PROPAGATION_NEVER，需要以非事务方式执行操作，如果当前事务存在则【抛出异常】
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
        throw new IllegalTransactionStateException();
    }
	// 传播行为是 PROPAGATION_NOT_SUPPORTED，以非事务方式运行，如果当前存在事务，则【把当前事务挂起】
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
        // 挂起事务
        Object suspendedResources = suspend(transaction);
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        // 创建一个非事务的事务状态对象返回
        return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources);
    }
	// 开启新事物的逻辑
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        // 【挂起当前事务】
        SuspendedResourcesHolder suspendedResources = suspend(transaction);
       	// 【开启新事物】
    }
	// 传播行为是 PROPAGATION_NESTED，嵌套事务
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // Spring 默认不支持内嵌事务
        // 【开启方式】：&amp;lt;property name=&quot;nestedTransactionAllowed&quot; value=&quot;true&quot;&amp;gt;
        if (!isNestedTransactionAllowed()) {
            throw new NestedTransactionNotSupportedException();
        }
        
        if (useSavepointForNestedTransaction()) {
            //  为当前方法创建一个 TransactionStatus 对象，
            DefaultTransactionStatus status =
                prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
            // 创建一个 JDBC 的保存点
            status.createAndHoldSavepoint();
            // 不需要使用同步，直接返回
            return status;
        }
        else {
            // Usually only for JTA transaction，开启一个新事务
        }
    }

    // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED，【使用当前的事务】
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;挂起恢复&lt;/h5&gt;
&lt;p&gt;AbstractPlatformTransactionManager#suspend：&lt;strong&gt;挂起事务&lt;/strong&gt;，并获得一个上下文信息对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) {
    // 事务是同步状态的
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        List&amp;lt;TransactionSynchronization&amp;gt; suspendedSynchronizations = doSuspendSynchronization();
        try {
            Object suspendedResources = null;
            if (transaction != null) {
                // do it
                suspendedResources = doSuspend(transaction);
            }
            //将上层事务绑定在线程上下文的变量全部取出来
            //...
            // 通过被挂起的资源和上层事务的上下文变量，创建一个【SuspendedResourcesHolder】返回
            return new SuspendedResourcesHolder(suspendedResources, suspendedSynchronizations, 
                                                name, readOnly, isolationLevel, wasActive);
        } //...
}
protected Object doSuspend(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    // 将当前方法的事务对象 connectionHolder 属性置为 null，不和上层共享资源
    // 当前方法有可能是不开启事务或者要开启一个独立的事务
    txObject.setConnectionHolder(null);
    // 【解绑在线程上的事务】
    return TransactionSynchronizationManager.unbindResource(obtainDataSource());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractPlatformTransactionManager#resume：&lt;strong&gt;恢复现场&lt;/strong&gt;，根据挂起资源去恢复线程上下文信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) {
    if (resourcesHolder != null) {
        // 获取被挂起的事务资源
        Object suspendedResources = resourcesHolder.suspendedResources;
        if (suspendedResources != null) {
            //绑定上一个事务的 ConnectionHolder 到线程上下文
            doResume(transaction, suspendedResources);
        }
        List&amp;lt;TransactionSynchronization&amp;gt; suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;
        if (suspendedSynchronizations != null) {
            //....
            // 将线程上下文变量恢复为上一个事务的挂起现场
            doResumeSynchronization(suspendedSynchronizations);
        }
    }
}
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
    // doSuspend 的逆动作，【绑定资源】
    TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;提交回滚&lt;/h4&gt;
&lt;h5&gt;回滚方式&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    // 事务状态信息不为空进入逻辑
    if (txInfo != null &amp;amp;&amp;amp; txInfo.getTransactionStatus() != null) {
        // 条件二成立 说明目标方法抛出的异常需要回滚事务
        if (txInfo.transactionAttribute != null &amp;amp;&amp;amp; txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                // 事务管理器的回滚方法
                txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {}
        }
        else {
            // 执行到这里，说明当前事务虽然抛出了异常，但是该异常并不会导致整个事务回滚
            try {
                // 提交事务
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
            }
            catch (TransactionSystemException ex2) {}
        }
    }
}
public boolean rollbackOn(Throwable ex) {
    // 继承自 RuntimeException 或 error 的是【非检查型异常】，才会归滚事务
    // 如果配置了其他回滚错误，会获取到回滚规则 rollbackRules 进行判断
    return (ex instanceof RuntimeException || ex instanceof Error);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public final void rollback(TransactionStatus status) throws TransactionException {
    // 事务已经完成不需要回滚
    if (status.isCompleted()) {
        throw new IllegalTransactionStateException();
    }
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    // 开始回滚事务
    processRollback(defStatus, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractPlatformTransactionManager#processRollback：事务回滚&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;triggerBeforeCompletion(status)&lt;/code&gt;：用来做扩展逻辑，回滚前的前置处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (status.hasSavepoint())&lt;/code&gt;：条件成立说明当前事务是一个&lt;strong&gt;内嵌事务&lt;/strong&gt;，当前方法只是复用了上层事务的一个内嵌事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status.rollbackToHeldSavepoint()&lt;/code&gt;：内嵌事务加入事务时会创建一个保存点，此时恢复至保存点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (status.isNewTransaction())&lt;/code&gt;：说明事务是当前连接开启的，需要去回滚事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doRollback(status)&lt;/code&gt;：真正的的回滚函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DataSourceTransactionObject txObject = status.getTransaction()&lt;/code&gt;：获取事务对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection con = txObject.getConnectionHolder().getConnection()&lt;/code&gt;：获取连接对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;con.rollback()&lt;/code&gt;：&lt;strong&gt;JDBC 的方式回滚事务&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt;：当前方法是共享的上层的事务，和上层使用同一个 Conn 资源，&lt;strong&gt;共享的事务不能直接回滚，应该交给上层处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doSetRollbackOnly(status)&lt;/code&gt;：设置 con.rollbackOnly = true，线程回到上层事务 commit 时会检查该字段，然后执行回滚操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK)&lt;/code&gt;：回滚的后置处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cleanupAfterCompletion(status)&lt;/code&gt;：清理和恢复现场&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;提交方式&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null &amp;amp;&amp;amp; txInfo.getTransactionStatus() != null) {
        // 事务管理器的提交方法
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public final void commit(TransactionStatus status) throws TransactionException {
    // 已经完成的事务不需要提交了
    if (status.isCompleted()) {
        throw new IllegalTransactionStateException();
    }
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    // 条件成立说明是当前的业务强制回滚
    if (defStatus.isLocalRollbackOnly()) {
        // 回滚逻辑，
        processRollback(defStatus, false);
        return;
    }
	// 成立说明共享当前事务的【下层事务逻辑出错，需要回滚】
    if (!shouldCommitOnGlobalRollbackOnly() &amp;amp;&amp;amp; defStatus.isGlobalRollbackOnly()) {
        // 如果当前事务还是事务重入，会继续抛给上层，最上层事务会进行真实的事务回滚操作
        processRollback(defStatus, true);
        return;
    }
	// 执行提交
    processCommit(defStatus);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AbstractPlatformTransactionManager#processCommit：事务提交&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;prepareForCommit(status)&lt;/code&gt;：前置处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (status.hasSavepoint())&lt;/code&gt;：条件成立说明当前事务是一个&lt;strong&gt;内嵌事务&lt;/strong&gt;，只是复用了上层事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status.releaseHeldSavepoint()&lt;/code&gt;：清理保存点，因为没有发生任何异常，所以保存点没有存在的意义了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (status.isNewTransaction())&lt;/code&gt;：说明事务是归属于当前连接的，需要去提交事务&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doCommit(status)&lt;/code&gt;：真正的提交函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Connection con = txObject.getConnectionHolder().getConnection()&lt;/code&gt;：获取连接对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;con.commit()&lt;/code&gt;：&lt;strong&gt;JDBC 的方式提交事务&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;doRollbackOnCommitException(status, ex)&lt;/code&gt;：&lt;strong&gt;提交事务出错后进行回滚&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; cleanupAfterCompletion(status)&lt;/code&gt;：清理和恢复现场&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;清理现场&lt;/h5&gt;
&lt;p&gt;恢复上层事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) {
    if (txInfo != null) {
        // 从当前线程的 ThreadLocal 获取上层的事务信息，将当前事务出栈，继续执行上层事务
        txInfo.restoreThreadLocalStatus();
    }
}
private void restoreThreadLocalStatus() {
    // Use stack to restore old transaction TransactionInfo.
    transactionInfoHolder.set(this.oldTransactionInfo);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前层级事务结束时的清理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    // 设置当前方法的事务状态为完成状态
    status.setCompleted();
    if (status.isNewSynchronization()) {
        // 清理线程上下文变量以及扩展点注册的 sync
        TransactionSynchronizationManager.clear();
    }
    // 事务是当前线程开启的
    if (status.isNewTransaction()) {
        // 解绑资源
        doCleanupAfterCompletion(status.getTransaction());
    }
    // 条件成立说明当前事务执行的时候，【挂起了一个上层的事务】
    if (status.getSuspendedResources() != null) {
        Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
        // 恢复上层事务现场
        resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DataSourceTransactionManager#doCleanupAfterCompletion：清理工作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TransactionSynchronizationManager.unbindResource(obtainDataSource())&lt;/code&gt;：解绑数据库资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (txObject.isMustRestoreAutoCommit())&lt;/code&gt;：是否恢复连接，Conn 归还到 DataSource**，归还前需要恢复到申请时的状态**&lt;/p&gt;
&lt;p&gt;&lt;code&gt;con.setAutoCommit(true)&lt;/code&gt;：恢复连接为自动提交&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel())&lt;/code&gt;：恢复隔离级别&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DataSourceUtils.releaseConnection(con, this.dataSource)&lt;/code&gt;：&lt;strong&gt;将连接归还给数据库连接池&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;txObject.getConnectionHolder().clear()&lt;/code&gt;：清理 ConnectionHolder 资源&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解&lt;/h3&gt;
&lt;h4&gt;Component&lt;/h4&gt;
&lt;p&gt;@Component 解析流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注解类启动容器的时，注册 ClassPathBeanDefinitionScanner 到容器，用来扫描 Bean 的相关信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected Set&amp;lt;BeanDefinitionHolder&amp;gt; doScan(String... basePackages) {
    Set&amp;lt;BeanDefinitionHolder&amp;gt; beanDefinitions = new LinkedHashSet&amp;lt;&amp;gt;();
    // 遍历指定的所有的包，【这就相当于扫描了】
    for (String basePackage : basePackages) {
        // 读取当前包下的资源装换为 BeanDefinition，字节流的方式
        Set&amp;lt;BeanDefinition&amp;gt; candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {
            // 遍历，封装，类似于 XML 的解析方式，注册到容器中
            registerBeanDefinition(definitionHolder, this.registry)
        }
    return beanDefinitions;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ClassPathScanningCandidateComponentProvider.findCandidateComponents()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Set&amp;lt;BeanDefinition&amp;gt; findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null &amp;amp;&amp;amp; indexSupportsIncludeFilters()) {
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
        return scanCandidateComponents(basePackage);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private Set&amp;lt;BeanDefinition&amp;gt; scanCandidateComponents(String basePackage) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + &apos;/&apos; + this.resourcePattern&lt;/code&gt; ：将 package 转化为 ClassLoader 类资源搜索路径 packageSearchPath，例如：&lt;code&gt;com.sea.spring.boot&lt;/code&gt; 转化为 &lt;code&gt;classpath*:com/sea/spring/boot/**/*.class&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resources = getResourcePatternResolver().getResources(packageSearchPath)&lt;/code&gt;：加载路径下的资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (Resource resource : resources) &lt;/code&gt;：遍历所有的资源&lt;/p&gt;
&lt;p&gt;&lt;code&gt;metadataReader = getMetadataReaderFactory().getMetadataReader(resource)&lt;/code&gt;：获取元数据阅读器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (isCandidateComponent(metadataReader))&lt;/code&gt;：&lt;strong&gt;当前类不匹配任何排除过滤器，并且匹配一个包含过滤器&lt;/strong&gt;，返回 true&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;includeFilters 由 &lt;code&gt;registerDefaultFilters()&lt;/code&gt; 设置初始值，方法有 @Component，没有 @Service，因为 @Component 是 @Service 的元注解，Spring 在读取 @Service 时也读取了元注解，并将 @Service 作为 @Component 处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.includeFilters.add(new AnnotationTypeFilter(Component.class))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component	// 拥有了 Component 功能
public @interface Service {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;candidates.add(sbd)&lt;/code&gt;：添加到返回结果的 list&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：https://my.oschina.net/floor/blog/4325651&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Autowired&lt;/h4&gt;
&lt;p&gt;打开 @Autowired 源码，注释上写 Please consult the javadoc for the AutowiredAnnotationBeanPostProcessor&lt;/p&gt;
&lt;p&gt;AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProcessor，就具备了实例化前后（而不是初始化前后）管理对象的能力，实现了 BeanPostProcessor，具有初始化前后管理对象的能力，实现 BeanFactoryAware，具备随时拿到 BeanFactory 的能力，所以这个类&lt;strong&gt;具备一切后置处理器的能力&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在容器启动，为对象赋值的时候，遇到 @Autowired 注解，会用后置处理器机制，来创建属性的实例，然后再利用反射机制，将实例化好的属性，赋值给对象上，这就是 Autowired 的原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作用时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring 在每个 Bean 实例化之后，调用 AutowiredAnnotationBeanPostProcessor 的 &lt;code&gt;postProcessMergedBeanDefinition()&lt;/code&gt; 方法，查找该 Bean 是否有 @Autowired 注解，进行相关元数据的获取&lt;/li&gt;
&lt;li&gt;Spring 在每个 Bean 调用 &lt;code&gt;populateBean()&lt;/code&gt; 进行属性注入的时候，即调用 &lt;code&gt;postProcessProperties()&lt;/code&gt; 方法，查找该 Bean 属性是否有 @Autowired 注解，进行相关数据的填充&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;MVC&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;p&gt;SpringMVC：是一种基于 Java 实现 MVC 模型的轻量级 Web 框架&lt;/p&gt;
&lt;p&gt;SpringMVC 优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用简单&lt;/li&gt;
&lt;li&gt;性能突出（对比现有的框架技术）&lt;/li&gt;
&lt;li&gt;灵活性强&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件开发三层架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;表现层：负责数据展示&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务层：负责业务处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据层：负责数据操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-MVC%E4%B8%89%E5%B1%82%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MVC（Model View Controller），一种用于设计创建Web应用程序表现层的模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Model（模型）：数据模型，用于封装数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View（视图）：页面视图，用于展示数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jsp&lt;/li&gt;
&lt;li&gt;html&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Controller（控制器）：处理用户交互的调度器，用于根据用户需求处理程序逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Servlet&lt;/li&gt;
&lt;li&gt;SpringMVC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-MVC%E5%8A%9F%E8%83%BD%E5%9B%BE%E7%A4%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://space.bilibili.com/37974444/&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;基本配置&lt;/h2&gt;
&lt;h3&gt;入门项目&lt;/h3&gt;
&lt;p&gt;流程分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器启动
&lt;ol&gt;
&lt;li&gt;加载 web.xml 中 DispatcherServlet&lt;/li&gt;
&lt;li&gt;读取 spring-mvc.xml 中的配置，加载所有 controller 包中所有标记为 bean 的类&lt;/li&gt;
&lt;li&gt;读取 bean 中方法上方标注 @RequestMapping 的内容&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;处理请求
&lt;ol&gt;
&lt;li&gt;DispatcherServlet 配置拦截所有请求 /&lt;/li&gt;
&lt;li&gt;使用请求路径与所有加载的 @RequestMapping 的内容进行比对&lt;/li&gt;
&lt;li&gt;执行对应的方法&lt;/li&gt;
&lt;li&gt;根据方法的返回值在 webapp 目录中查找对应的页面并展示&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pom.xml 导入坐标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

&amp;lt;groupId&amp;gt;demo&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring_base_config&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
&amp;lt;packaging&amp;gt;war&amp;lt;/packaging&amp;gt;

&amp;lt;properties&amp;gt;
    &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;maven.compiler.source&amp;gt;1.8&amp;lt;/maven.compiler.source&amp;gt;
    &amp;lt;maven.compiler.target&amp;gt;1.8&amp;lt;/maven.compiler.target&amp;gt;
&amp;lt;/properties&amp;gt;

&amp;lt;dependencies&amp;gt;
    &amp;lt;!-- servlet3.0规范的坐标 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;javax.servlet&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;javax.servlet-api&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--jsp坐标--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;javax.servlet.jsp&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jsp-api&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--spring的坐标--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;!--springmvc的坐标--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-webmvc&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;5.1.9.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;

&amp;lt;!--构建--&amp;gt;
&amp;lt;build&amp;gt;
    &amp;lt;!--设置插件--&amp;gt;
    &amp;lt;plugins&amp;gt;
        &amp;lt;!--具体的插件配置--&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.tomcat.maven&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;tomcat7-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.1&amp;lt;/version&amp;gt;
            &amp;lt;configuration&amp;gt;
                &amp;lt;port&amp;gt;80&amp;lt;/port&amp;gt;
                &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
            &amp;lt;/configuration&amp;gt;
        &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设定具体 Controller，控制层 java / controller / UserController&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller  //@Component衍生注解
public class UserController {
    //设定当前方法的访问映射地址，等同于Servlet在web.xml中的配置
    @RequestMapping(&quot;/save&quot;)
    //设置当前方法返回值类型为String，用于指定请求完成后跳转的页面
    public String save(){
        System.out.println(&quot;user mvc controller is running ...&quot;);
        //设定具体跳转的页面
    	return &quot;success.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;webapp / WEB-INF / web.xml，配置SpringMVC核心控制器，请求转发到对应的具体业务处理器Controller中（等同于Servlet配置）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd&quot;
         version=&quot;3.1&quot;&amp;gt;
    &amp;lt;!--配置Servlet--&amp;gt;
    &amp;lt;servlet&amp;gt;
        &amp;lt;servlet-name&amp;gt;DispatcherServlet&amp;lt;/servlet-name&amp;gt;
        &amp;lt;servlet-class&amp;gt;org.springframework.web.servlet.DispatcherServlet&amp;lt;/servlet-class&amp;gt;
        &amp;lt;!--加载Spring控制文件--&amp;gt;
        &amp;lt;init-param&amp;gt;
            &amp;lt;param-name&amp;gt;contextConfigLocation&amp;lt;/param-name&amp;gt;
            &amp;lt;param-value&amp;gt;classpath*:spring-mvc.xml&amp;lt;/param-value&amp;gt;
        &amp;lt;/init-param&amp;gt;
    &amp;lt;/servlet&amp;gt;
    &amp;lt;servlet-mapping&amp;gt;
        &amp;lt;servlet-name&amp;gt;DispatcherServlet&amp;lt;/servlet-name&amp;gt;
        &amp;lt;url-pattern&amp;gt;/&amp;lt;/url-pattern&amp;gt;
    &amp;lt;/servlet-mapping&amp;gt;
&amp;lt;/web-app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resouces / spring-mvc.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc.xsd&quot;&amp;gt;
    &amp;lt;!--扫描加载所有的控制类--&amp;gt;
    &amp;lt;context:component-scan base-package=&quot;controller&quot;/&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;加载控制&lt;/h3&gt;
&lt;p&gt;Controller 加载控制：SpringMVC 的处理器对应的 bean 必须按照规范格式开发，未避免加入无效的 bean 可通过 bean 加载过滤器进行包含设定或排除设定，表现层 bean 标注通常设定为 @Controller&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;resources / spring-mvc.xml 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:component-scan base-package=&quot;com.seazean&quot;&amp;gt;
    &amp;lt;context:include-filter 
						type=&quot;annotation&quot; 
						expression=&quot;org.springframework.stereotype.Controller&quot;/&amp;gt;
&amp;lt;/context:component-scan&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态资源加载（webapp 目录下的相关资源），spring-mvc.xml 配置，开启 mvc 命名空间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--放行指定类型静态资源配置方式--&amp;gt;
&amp;lt;mvc:resources mapping=&quot;/img/**&quot; location=&quot;/img/&quot;/&amp;gt; &amp;lt;!--webapp/img/资源--&amp;gt;
&amp;lt;mvc:resources mapping=&quot;/js/**&quot; location=&quot;/js/&quot;/&amp;gt;
&amp;lt;mvc:resources mapping=&quot;/css/**&quot; location=&quot;/css/&quot;/&amp;gt;

&amp;lt;!--SpringMVC 提供的通用资源放行方式，建议选择--&amp;gt;
&amp;lt;mvc:default-servlet-handler/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中文乱码处理 SpringMVC 提供专用的中文字符过滤器，用于处理乱码问题。配置在 web.xml 里面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--乱码处理过滤器，与Servlet中使用的完全相同，差异之处在于处理器的类由Spring提供--&amp;gt;
&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;CharacterEncodingFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;org.springframework.web.filter.CharacterEncodingFilter&amp;lt;/filter-class&amp;gt;
    &amp;lt;init-param&amp;gt;
        &amp;lt;param-name&amp;gt;encoding&amp;lt;/param-name&amp;gt;
        &amp;lt;param-value&amp;gt;UTF-8&amp;lt;/param-value&amp;gt;
    &amp;lt;/init-param&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;CharacterEncodingFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;注解驱动&lt;/h3&gt;
&lt;p&gt;WebApplicationContext，生成 Spring 核心容器（主容器/父容器/根容器）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父容器：Spring 环境加载后形成的容器，包含 Spring 环境下的所有的 bean&lt;/li&gt;
&lt;li&gt;子容器：当前 mvc 环境加载后形成的容器，不包含 Spring 环境下的 bean&lt;/li&gt;
&lt;li&gt;子容器可以访问父容器中的资源，父容器不可以访问子容器的资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EnableWebMvc 注解作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持 ConversionService 的配置，可以方便配置自定义类型转换器&lt;/li&gt;
&lt;li&gt;支持 @NumberFormat 注解格式化数字类型&lt;/li&gt;
&lt;li&gt;支持 @DateTimeFormat 注解格式化日期数据，日期包括 Date、Calendar&lt;/li&gt;
&lt;li&gt;支持 @Valid 的参数校验（需要导入 JSR-303 规范）&lt;/li&gt;
&lt;li&gt;配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;纯注解开发：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config /  SpringMVCConfiguration.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ComponentScan(value = &quot;com.seazean&quot;, includeFilters = @ComponentScan.Filter(
    								type=FilterType.ANNOTATION,
    								classes = {Controller.class} )
    )
//等同于&amp;lt;mvc:annotation-driven/&amp;gt;，还不完全相同
@EnableWebMvc
public class SpringMVCConfiguration implements WebMvcConfigurer{
    //注解配置通用放行资源的格式 建议使用
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基于 servlet3.0 规范，自定义 Servlet 容器初始化配置类，加载 SpringMVC 核心配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
    //创建Servlet容器时，使用注解方式加载SPRINGMVC配置类中的信息，
    //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围，
    //在整个WEB容器中可以随时获取调用
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        A.C.W.A ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMVCConfiguration.class);
        return ctx;
    }

    //注解配置映射地址方式，服务于SpringMVC的核心控制器DispatcherServlet
    @Override
    protected String[] getServletMappings() {
        return new String[]{&quot;/&quot;};
    }

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    //乱码处理作为过滤器，在servlet容器启动时进行配置
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        CharacterEncodingFilter cef = new CharacterEncodingFilter();
        cef.setEncoding(&quot;UTF-8&quot;);
        FilterRegistration.Dynamic registration = servletContext.addFilter(&quot;characterEncodingFilter&quot;, cef);
        registration.addMappingForUrlPatterns(EnumSet.of(
           			DispatcherType.REQUEST,
            		DispatcherType.FORWARD,
            		DispatcherType.INCLUDE), false,&quot;/*&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;请求映射&lt;/h3&gt;
&lt;p&gt;名称：@RequestMapping&lt;/p&gt;
&lt;p&gt;类型：方法注解、类注解&lt;/p&gt;
&lt;p&gt;位置：处理器类中的方法定义上方、处理器类定义上方&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法注解&lt;/p&gt;
&lt;p&gt;作用：绑定请求地址与对应处理方法间的关系&lt;/p&gt;
&lt;p&gt;无类映射地址访问格式： http://localhost/requestURL2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class UserController {
    @RequestMapping(&quot;/requestURL2&quot;)
    public String requestURL2() {
        return &quot;page.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类注解&lt;/p&gt;
&lt;p&gt;作用：为当前处理器中所有方法设定公共的访问路径前缀&lt;/p&gt;
&lt;p&gt;带有类映射地址访问格式，将类映射地址作为前缀添加在实际映射地址前面：&lt;strong&gt;/user/requestURL1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最终返回的页面如果未设定绝对访问路径，将从类映射地址所在目录中查找 &lt;strong&gt;webapp/user/page.jsp&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/user&quot;)
public class UserController {
    @RequestMapping(&quot;/requestURL2&quot;)
    public String requestURL2() {
        return &quot;page.jsp&quot;;
    }
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(
    value=&quot;/requestURL3&quot;, //设定请求路径，与path属性、 value属性相同
    method = RequestMethod.GET, //设定请求方式
    params = &quot;name&quot;, //设定请求参数条件
    headers = &quot;content-type=text/*&quot;, //设定请求消息头条件
    consumes = &quot;text/*&quot;, //用于指定可以接收的请求正文类型（MIME类型）
    produces = &quot;text/*&quot; //用于指定可以生成的响应正文类型（MIME类型）
)
public String requestURL3() {
    return &quot;/page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;h3&gt;请求处理&lt;/h3&gt;
&lt;h4&gt;普通类型&lt;/h4&gt;
&lt;p&gt;SpringMVC 将传递的参数封装到处理器方法的形参中，达到快速访问参数的目的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL：http://localhost/requestParam1?name=seazean&amp;amp;age=14&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class UserController {
    @RequestMapping(&quot;/requestParam1&quot;)
    public String requestParam1(String name ,int age){
        System.out.println(&quot;name=&quot; + name + &quot;,age=&quot; + age);
        return &quot;page.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@page pageEncoding=&quot;UTF-8&quot; language=&quot;java&quot; contentType=&quot;text/html;UTF-8&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
	&amp;lt;h1&amp;gt;请求参数测试页面&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@RequestParam 的使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类型：形参注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置：处理器类中的方法形参前方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：绑定请求参数与对应处理方法形参间的关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问 URL：http://localhost/requestParam2?userName=Jock&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam2&quot;)
public String requestParam2(@RequestParam(
                            name = &quot;userName&quot;,
                            required = true,	//为true代表必须有参数
                            defaultValue = &quot;s&quot;) String name){
    System.out.println(&quot;name=&quot; + name);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;POJO类型&lt;/h4&gt;
&lt;h5&gt;简单类型&lt;/h5&gt;
&lt;p&gt;当 POJO 中使用简单类型属性时， 参数名称与 POJO 类属性名保持一致&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL： http://localhost/requestParam3?name=seazean&amp;amp;age=14&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam3&quot;)
public String requestParam3(User user){
    System.out.println(&quot;name=&quot; + user.getName());
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private Integer age;
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;参数冲突&lt;/h5&gt;
&lt;p&gt;当 POJO 类型属性与其他形参出现同名问题时，将被&lt;strong&gt;同时赋值&lt;/strong&gt;，建议使用 @RequestParam 注解进行区分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL： http://localhost/requestParam4?name=seazean&amp;amp;age=14&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam4&quot;)
public String requestParam4(User user, String age){
    System.out.println(&quot;user.age=&quot; + user.getAge() + &quot;,age=&quot; + age);//14 14 
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;复杂类型&lt;/h5&gt;
&lt;p&gt;当 POJO 中出现对象属性时，参数名称与对象层次结构名称保持一致&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL： http://localhost/requestParam5?address.province=beijing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam5&quot;)
public String requestParam5(User user){
    System.out.println(&quot;user.address=&quot; + user.getAddress().getProvince());
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private Integer age;
    private Address address; //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Address {
    private String province;
    private String city;
    private String address;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;容器类型&lt;/h5&gt;
&lt;p&gt;POJO 中出现集合类型的处理方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过 URL 地址中同名参数，可以为 POJO 中的集合属性进行赋值，集合属性要求保存简单数据&lt;/p&gt;
&lt;p&gt;访问 URL：http://localhost/requestParam6?nick=Jock1&amp;amp;nick=Jockme&amp;amp;nick=zahc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam6&quot;)
public String requestParam6(User user){
    System.out.println(&quot;user=&quot; + user);
    //user = User{name=&apos;null&apos;,age=null,nick={Jock1,Jockme,zahc}}
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private Integer age;
    private List&amp;lt;String&amp;gt; nick;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POJO 中出现 List 保存对象数据，参数名称与对象层次结构名称保持一致，使用数组格式描述集合中对象的位置访问 URL：http://localhost/requestParam7?addresses[0].province=bj&amp;amp;addresses[1].province=tj&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam7&quot;)
public String requestParam7(User user){
    System.out.println(&quot;user.addresses=&quot; + user.getAddress());
    //{Address{provice=bj,city=&apos;null&apos;,address=&apos;null&apos;}},{Address{....}}
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private String name;
    private Integer age;
    private List&amp;lt;Address&amp;gt; addresses;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POJO 中出现 Map 保存对象数据，参数名称与对象层次结构名称保持一致，使用映射格式描述集合中对象位置&lt;/p&gt;
&lt;p&gt;URL: http://localhost/requestParam8?addressMap[’home’].province=bj&amp;amp;addressMap[’job’].province=tj&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam8&quot;)
public String requestParam8(User user){
    System.out.println(&quot;user.addressMap=&quot; + user.getAddressMap());
    //user.addressMap={home=Address{p=,c=,a=},job=Address{....}}
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private Map&amp;lt;String,Address&amp;gt; addressMap;
    //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数组集合&lt;/h4&gt;
&lt;h5&gt;数组类型&lt;/h5&gt;
&lt;p&gt;请求参数名与处理器方法形参名保持一致，且请求参数数量＞ 1个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL： http://localhost/requestParam9?nick=Jockme&amp;amp;nick=zahc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam9&quot;)
public String requestParam9(String[] nick){
    System.out.println(nick[0] + &quot;,&quot; + nick[1]);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;集合类型&lt;/h5&gt;
&lt;p&gt;保存简单类型数据，请求参数名与处理器方法形参名保持一致，且请求参数数量＞ 1个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问 URL： http://localhost/requestParam10?nick=Jockme&amp;amp;nick=zahc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam10&quot;)
public String requestParam10(@RequestParam(&quot;nick&quot;) List&amp;lt;String&amp;gt; nick){
    System.out.println(nick);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意： SpringMVC 默认将 List 作为对象处理，赋值前先创建对象，然后将 nick &lt;strong&gt;作为对象的属性&lt;/strong&gt;进行处理。List 是接口无法创建对象，报无法找到构造方法异常；修复类型为可创建对象的 ArrayList 类型后，对象可以创建但没有 nick 属性，因此数据为空
解决方法：需要告知 SpringMVC 的处理器 nick 是一组数据，而不是一个单一属性。通过 @RequestParam 注解，将数量大于 1 个 names 参数打包成参数数组后， SpringMVC 才能识别该数据格式，并判定形参类型是否为数组或集合，并按数组或集合对象的形式操作数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;转换器&lt;/h4&gt;
&lt;h5&gt;类型&lt;/h5&gt;
&lt;p&gt;开启转换配置：&lt;code&gt;&amp;lt;mvc:annotation-driven /&amp;gt;  &lt;/code&gt;
作用：提供 Controller 请求转发，Json 自动转换等功能&lt;/p&gt;
&lt;p&gt;如果访问 URL：http://localhost/requestParam1?name=seazean&amp;amp;age=seazean，会出现报错，类型转化异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam1&quot;)
public String requestParam1(String name ,int age){
    System.out.println(&quot;name=&quot; + name + &quot;,age=&quot; + age);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringMVC 对接收的数据进行自动类型转换，该工作通过 Converter 接口实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标量转换器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;集合、数组相关转换器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认转换器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;日期&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-date%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果访问 URL：http://localhost/requestParam11?date=1999-09-09 会报错，所以需要日期类型转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;声明自定义的转换格式并覆盖系统转换格式，配置 resources / spring-mvc.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--5.启用自定义Converter--&amp;gt;
&amp;lt;mvc:annotation-driven conversion-service=&quot;conversionService&quot;/&amp;gt;
&amp;lt;!--1.设定格式类型Converter，注册为Bean，受SpringMVC管理--&amp;gt;
&amp;lt;bean id=&quot;conversionService&quot; class=&quot;org.springframework.format.support.FormattingConversionServiceFactoryBean&quot;&amp;gt;
    &amp;lt;!--2.自定义Converter格式类型设定，该设定使用的是同类型覆盖的思想--&amp;gt;
    &amp;lt;property name=&quot;formatters&quot;&amp;gt;
        &amp;lt;!--3.使用set保障相同类型的转换器仅保留一个，避免冲突--&amp;gt;
        &amp;lt;set&amp;gt;
            &amp;lt;!--4.设置具体的格式类型--&amp;gt;
            &amp;lt;bean class=&quot;org.springframework.format.datetime.DateFormatter&quot;&amp;gt;
                &amp;lt;!--5.类型规则--&amp;gt;
                &amp;lt;property name=&quot;pattern&quot; value=&quot;yyyy-MM-dd&quot;/&amp;gt;
            &amp;lt;/bean&amp;gt;
        &amp;lt;/set&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@DateTimeFormat
类型：形参注解、成员变量注解
位置：形参前面 或 成员变量上方
作用：为当前参数或变量指定类型转换规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public String requestParam12(@DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;) Date date){
    System.out.println(&quot;date=&quot; + date);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)
private Date date;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖注解驱动支持，xml 开启配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mvc:annotation-driven /&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;自定义&lt;/h5&gt;
&lt;p&gt;自定义类型转换器，实现 Converter 接口或者直接容器中注入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebConfig implements WebMvcConfigurer {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter&amp;lt;String, Date&amp;gt;() {
                    @Override
                    public Pet convert(String source) {
                    	DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);
                        Date date = null;
                        //类型转换器无法预计使用过程中出现的异常，因此必须在类型转换器内部捕获，
                        //不允许抛出，框架无法预计此类异常如何处理
                        try {
                            date = df.parse(source);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        return date;
                    }
                });
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//本例中的泛型填写的是String，Date，最终出现字符串转日期时，该类型转换器生效
public class MyDateConverter implements Converter&amp;lt;String, Date&amp;gt; {
    //重写接口的抽象方法，参数由泛型决定
    public Date convert(String source) {
        DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);
        Date date = null;
        //类型转换器无法预计使用过程中出现的异常，因此必须在类型转换器内部捕获，
        //不允许抛出，框架无法预计此类异常如何处理
        try {
            date = df.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置 resources / spring-mvc.xml，注册自定义转换器，将功能加入到 SpringMVC 转换服务 ConverterService 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--1.将自定义Converter注册为Bean，受SpringMVC管理--&amp;gt;
&amp;lt;bean id=&quot;myDateConverter&quot; class=&quot;converter.MyDateConverter&quot;/&amp;gt;
&amp;lt;!--2.设定自定义Converter服务bean--&amp;gt;
&amp;lt;bean id=&quot;conversionService&quot;
      class=&quot;org.springframework.context.support.ConversionServiceFactoryBean&quot;&amp;gt;
    &amp;lt;!--3.注入所有的自定义Converter，该设定使用的是同类型覆盖的思想--&amp;gt;
    &amp;lt;property name=&quot;converters&quot;&amp;gt;
        &amp;lt;!--4.set保障同类型转换器仅保留一个，去重规则以Converter&amp;lt;S,T&amp;gt;的泛型为准--&amp;gt;
        &amp;lt;set&amp;gt;
            &amp;lt;!--5.具体的类型转换器--&amp;gt;
            &amp;lt;ref bean=&quot;myDateConverter&quot;/&amp;gt;
        &amp;lt;/set&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;

&amp;lt;!--开启注解驱动，加载自定义格式化转换器对应的类型转换服务--&amp;gt;
&amp;lt;mvc:annotation-driven conversion-service=&quot;conversionService&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用转换器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/requestParam12&quot;)
public String requestParam12(Date date){
    System.out.println(date);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;响应处理&lt;/h3&gt;
&lt;h4&gt;页面跳转&lt;/h4&gt;
&lt;p&gt;请求转发和重定向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求转发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class UserController {
    @RequestMapping(&quot;/showPage1&quot;)
	public String showPage1() {
   	 	System.out.println(&quot;user mvc controller is running ...&quot;);
    	return &quot;forward:/WEB-INF/page/page.jsp;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showPage2&quot;)
public String showPage2() {
    System.out.println(&quot;user mvc controller is running ...&quot;);
    return &quot;redirect:/WEB-INF/page/page.jsp&quot;;//不能访问WEB-INF下的资源
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;页面访问快捷设定（InternalResourceViewResolver）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;展示页面的保存位置通常固定且结构相似，可以设定通用的访问路径简化页面配置，配置 spring-mvc.xml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&amp;gt;
    &amp;lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/pages/&quot;/&amp;gt;
    &amp;lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showPage3&quot;)
public String showPage3() {
    System.out.println(&quot;user mvc controller is running...&quot;);
    return &quot;page&quot;;
}
@RequestMapping(&quot;/showPage4&quot;)
public String showPage4() {
    System.out.println(&quot;user mvc controller is running...&quot;);
    return &quot;forward:page&quot;;
}

@RequestMapping(&quot;/showPage5&quot;)
public String showPage5() {
    System.out.println(&quot;user mvc controller is running...&quot;);
    return &quot;redirect:page&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果未设定了返回值，使用 void 类型，则默认使用访问路径作页面地址的前缀后缀&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//最简页面配置方式，使用访问路径作为页面名称，省略返回值
@RequestMapping(&quot;/showPage6&quot;)
public void showPage6() {
    System.out.println(&quot;user mvc controller is running ...&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;数据跳转&lt;/h4&gt;
&lt;p&gt;ModelAndView 是 SpringMVC 提供的一个对象，该对象可以用作控制器方法的返回值（Model 同），实现携带数据跳转&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置数据，向请求域对象中存储数据&lt;/li&gt;
&lt;li&gt;设置视图，逻辑视图&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 HttpServletRequest 类型形参进行数据传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class BookController {
    @RequestMapping(&quot;/showPageAndData1&quot;)
    public String showPageAndData1(HttpServletRequest request) {
        request.setAttribute(&quot;name&quot;,&quot;seazean&quot;);
        return &quot;page&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Model 类型形参进行数据传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showPageAndData2&quot;)
public String showPageAndData2(Model model) {
    model.addAttribute(&quot;name&quot;,&quot;seazean&quot;);
    Book book = new Book();
    book.setName(&quot;SpringMVC入门实战&quot;);
    book.setPrice(66.6d);
    //添加数据的方式，key对value
    model.addAttribute(&quot;book&quot;,book);
    return &quot;page&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Book {
    private String name;
    private Double price;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 ModelAndView 类型形参进行数据传递，将该对象作为返回值传递给调用者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showPageAndData3&quot;)
public ModelAndView showPageAndData3(ModelAndView modelAndView) {
    //ModelAndView mav = new ModelAndView(); 替换形参中的参数
    Book book  = new Book();
    book.setName(&quot;SpringMVC入门案例&quot;);
    book.setPrice(66.66d);

    //添加数据的方式，key对value
    modelAndView.addObject(&quot;book&quot;,book);
    modelAndView.addObject(&quot;name&quot;,&quot;Jockme&quot;);
    //设置页面的方式，该方法最后一次执行的结果生效
    modelAndView.setViewName(&quot;page&quot;);
    //返回值设定成ModelAndView对象
    return modelAndView;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ModelAndView 扩展&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ModelAndView对象支持转发的手工设定，该设定不会启用前缀后缀的页面拼接格式
@RequestMapping(&quot;/showPageAndData4&quot;)
public ModelAndView showPageAndData4(ModelAndView modelAndView) {
    modelAndView.setViewName(&quot;forward:/WEB-INF/page/page.jsp&quot;);
    return modelAndView;
}

//ModelAndView对象支持重定向的手工设定，该设定不会启用前缀后缀的页面拼接格式
@RequestMapping(&quot;/showPageAndData5&quot;)
public ModelAndView showPageAndData6(ModelAndView modelAndView) {
    modelAndView.setViewName(&quot;redirect:page.jsp&quot;);
    return modelAndView;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;JSON&lt;/h4&gt;
&lt;p&gt;注解：@ResponseBody&lt;/p&gt;
&lt;p&gt;作用：将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后，写入到 Response 的 body 区。如果返回值是字符串，那么直接将字符串返回客户端；如果是一个对象，会&lt;strong&gt;将对象转化为 JSON&lt;/strong&gt;，返回客户端&lt;/p&gt;
&lt;p&gt;注意：当方法上面没有写 ResponseBody，底层会将方法的返回值封装为 ModelAndView 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 HttpServletResponse 对象响应数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class AccountController {
    @RequestMapping(&quot;/showData1&quot;)
    public void showData1(HttpServletResponse response) throws IOException {
        response.getWriter().write(&quot;message&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;strong&gt;@ResponseBody 将返回的结果作为响应内容&lt;/strong&gt;（页面显示），而非响应的页面名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showData2&quot;)
@ResponseBody
public String showData2(){
    return &quot;{&apos;name&apos;:&apos;Jock&apos;}&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 jackson 进行 json 数据格式转化&lt;/p&gt;
&lt;p&gt;导入坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--json相关坐标3个--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jackson-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.9.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jackson-databind&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.9.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jackson-annotations&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.9.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showData3&quot;)
@ResponseBody
public String showData3() throws JsonProcessingException {
    Book book  = new Book();
    book.setName(&quot;SpringMVC入门案例&quot;);
    book.setPrice(66.66d);

    ObjectMapper om = new ObjectMapper();
    return om.writeValueAsString(book);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为 JSON 数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//使用SpringMVC注解驱动，对标注@ResponseBody注解的控制器方法进行结果转换，由于返回值为引用类型，自动调用jackson提供的类型转换器进行格式转换
@RequestMapping(&quot;/showData4&quot;)
@ResponseBody
public Book showData4() {
    Book book  = new Book();
    book.setName(&quot;SpringMVC入门案例&quot;);
    book.setPrice(66.66d);
    return book;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;手工添加信息类型转换器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean class=&quot;org.springframework.web.servlet.mvc.method.
             annotation.RequestMappingHandlerAdapter&quot;&amp;gt;
    &amp;lt;property name=&quot;messageConverters&quot;&amp;gt;
        &amp;lt;list&amp;gt;
            &amp;lt;bean class=&quot;org.springframework.http.converter.
                      json.MappingJackson2HttpMessageConverter&quot;/&amp;gt;
        &amp;lt;/list&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 SpringMVC 注解驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--开启springmvc注解驱动，对@ResponseBody的注解进行格式增强，追加其类型转换的功能，具体实现由MappingJackson2HttpMessageConverter进行--&amp;gt;
&amp;lt;mvc:annotation-driven/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换集合类型数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/showData5&quot;)
@ResponseBody
public List showData5() {
    Book book1  = new Book();
    book1.setName(&quot;SpringMVC入门案例&quot;);
    book1.setPrice(66.66d);

    Book book2  = new Book();
    book2.setName(&quot;SpringMVC入门案例&quot;);
    book2.setPrice(66.66d);

    ArrayList al = new ArrayList();
    al.add(book1);
    al.add(book2);
    return al;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Restful&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Rest（REpresentational State Transfer）：表现层状态转化，定义了&lt;strong&gt;资源在网络传输中以某种表现形式进行状态转移&lt;/strong&gt;，即网络资源的访问方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源：把真实的对象数据称为资源，一个资源既可以是一个集合，也可以是单个个体；每一种资源都有特定的 URI（统一资源标识符）与之对应，如果获取这个资源，访问这个 URI 就可以，比如获取特定的班级 &lt;code&gt;/class/12&lt;/code&gt;；资源也可以包含子资源，比如 &lt;code&gt;/classes/classId/teachers&lt;/code&gt; 某个指定班级的所有老师&lt;/li&gt;
&lt;li&gt;表现形式：资源是一种信息实体，它可以有多种外在表现形式，把资源具体呈现出来的形式比如 json、xml、image、txt 等等叫做它的&quot;表现层/表现形式&quot;&lt;/li&gt;
&lt;li&gt;状态转移：描述的服务器端资源的状态，比如增删改查（通过 HTTP 动词实现）引起资源状态的改变，互联网通信协议 HTTP 协议，是一个&lt;strong&gt;无状态协议&lt;/strong&gt;，所有的资源状态都保存在服务器端&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;访问方式&lt;/h4&gt;
&lt;p&gt;Restful 是按照 Rest 风格访问网络资源&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传统风格访问路径：http://localhost/user/get?id=1&lt;/li&gt;
&lt;li&gt;Rest 风格访问路径：http://localhost/user/1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：隐藏资源的访问行为，通过地址无法得知做的是何种操作，书写简化&lt;/p&gt;
&lt;p&gt;Restful 请求路径简化配置方式：&lt;code&gt;@RestController = @Controller + @ResponseBody&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;相关注解：@GetMapping 注解是 @RequestMapping 注解的衍生，所以效果是一样的，建议使用 @GetMapping&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@GetMapping(&quot;/poll&quot;)&lt;/code&gt; = &lt;code&gt;@RequestMapping(value = &quot;/poll&quot;,method = RequestMethod.GET)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(method = RequestMethod.GET)			// @GetMapping 就拥有了 @RequestMapping 的功能
public @interface GetMapping {
    @AliasFor(annotation = RequestMapping.class)	// 与 RequestMapping 相通
	String name() default &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@PostMapping(&quot;/push&quot;)&lt;/code&gt; = &lt;code&gt;@RequestMapping(value = &quot;/push&quot;,method = RequestMethod.POST)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;过滤器：HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;restful.jsp：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面表单&lt;strong&gt;使用隐藏域提交请求类型&lt;/strong&gt;，参数名称固定为 _method，必须配合提交类型 method=post 使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GET 请求通过地址栏可以发送，也可以通过设置 form 的请求方式提交&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POST 请求必须通过 form 的请求方式提交&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;restful风格请求表单&amp;lt;/h1&amp;gt;
&amp;lt;!--切换请求路径为restful风格--&amp;gt;
&amp;lt;form action=&quot;/user&quot; method=&quot;post&quot;&amp;gt;
	&amp;lt;!--一隐藏域，切换为PUT请求或DELETE请求，但是form表单的提交方式method属性必须填写post--&amp;gt;
	&amp;lt;input name=&quot;_method&quot; type=&quot;hidden&quot; value=&quot;PUT&quot;/&amp;gt;
	&amp;lt;input value=&quot;REST-PUT 提交&quot; type=&quot;submit&quot;/&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java / controller / UserController&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController				//设置rest风格的控制器
@RequestMapping(&quot;/user/&quot;)	//设置公共访问路径，配合下方访问路径使用
public class UserController {
    @GetMapping(&quot;/user&quot;)
    //@RequestMapping(value = &quot;/user&quot;,method = RequestMethod.GET)
    public String getUser(){
        return &quot;GET-张三&quot;;
    }

    @PostMapping(&quot;/user&quot;)
    //@RequestMapping(value = &quot;/user&quot;,method = RequestMethod.POST)
    public String saveUser(){
        return &quot;POST-张三&quot;;
    }

    @PutMapping(&quot;/user&quot;)
    //@RequestMapping(value = &quot;/user&quot;,method = RequestMethod.PUT)
    public String putUser(){
        return &quot;PUT-张三&quot;;
    }

    @DeleteMapping(&quot;/user&quot;)
    //@RequestMapping(value = &quot;/user&quot;,method = RequestMethod.DELETE)
    public String deleteUser(){
        return &quot;DELETE-张三&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置拦截器 web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--配置拦截器，解析请求中的参数_method，否则无法发起PUT请求与DELETE请求，配合页面表单使用--&amp;gt;
&amp;lt;filter&amp;gt;
    &amp;lt;filter-name&amp;gt;HiddenHttpMethodFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;filter-class&amp;gt;org.springframework.web.filter.HiddenHttpMethodFilter&amp;lt;/filter-class&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter-mapping&amp;gt;
    &amp;lt;filter-name&amp;gt;HiddenHttpMethodFilter&amp;lt;/filter-name&amp;gt;
    &amp;lt;servlet-name&amp;gt;DispatcherServlet&amp;lt;/servlet-name&amp;gt;
&amp;lt;/filter-mapping&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;参数注解&lt;/h4&gt;
&lt;p&gt;Restful 开发中的参数注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;{id}&quot;)
public String getMessage(@PathVariable(&quot;id&quot;) Integer id){
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 @PathVariable 注解获取路径上配置的具名变量，一般在有多个参数的时候添加&lt;/p&gt;
&lt;p&gt;其他注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@RequestHeader：获取请求头&lt;/li&gt;
&lt;li&gt;@RequestParam：获取请求参数（指问号后的参数，url?a=1&amp;amp;b=2）&lt;/li&gt;
&lt;li&gt;@CookieValue：获取 Cookie 值&lt;/li&gt;
&lt;li&gt;@RequestAttribute：获取 request 域属性&lt;/li&gt;
&lt;li&gt;@RequestBody：获取请求体 [POST]&lt;/li&gt;
&lt;li&gt;@MatrixVariable：矩阵变量&lt;/li&gt;
&lt;li&gt;@ModelAttribute：自定义类型变量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RestController	
@RequestMapping(&quot;/user/&quot;)
public class UserController {
    //rest风格访问路径简化书写方式，配合类注解@RequestMapping使用
    @RequestMapping(&quot;{id}&quot;)
    public String restLocation2(@PathVariable Integer id){
        System.out.println(&quot;restful is running ....get:&quot; + id);
        return &quot;success.jsp&quot;;
    }

    //@RequestMapping(value = &quot;{id}&quot;,method = RequestMethod.GET)
    @GetMapping(&quot;{id}&quot;)
    public String get(@PathVariable Integer id){
        System.out.println(&quot;restful is running ....get:&quot; + id);
        return &quot;success.jsp&quot;;
    }

    @PostMapping(&quot;{id}&quot;)
    public String post(@PathVariable Integer id){
        System.out.println(&quot;restful is running ....post:&quot; + id);
        return &quot;success.jsp&quot;;
    }

    @PutMapping(&quot;{id}&quot;)
    public String put(@PathVariable Integer id){
        System.out.println(&quot;restful is running ....put:&quot; + id);
        return &quot;success.jsp&quot;;
    }

    @DeleteMapping(&quot;{id}&quot;)
    public String delete(@PathVariable Integer id){
        System.out.println(&quot;restful is running ....delete:&quot; + id);
        return &quot;success.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;识别原理&lt;/h4&gt;
&lt;p&gt;表单提交要使用 REST 时，会带上 &lt;code&gt;_method=PUT&lt;/code&gt;，请求过来被 &lt;code&gt;HiddenHttpMethodFilter&lt;/code&gt; 拦截，进行过滤操作&lt;/p&gt;
&lt;p&gt;org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal()：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    // 兼容的请求 PUT、DELETE、PATCH
    private static final List&amp;lt;String&amp;gt; ALLOWED_METHODS =
			Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
					HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    // 隐藏域的名字
	public static final String DEFAULT_METHOD_PARAM = &quot;_method&quot;;

	private String methodParam = DEFAULT_METHOD_PARAM;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        HttpServletRequest requestToUse = request;
        // 请求必须是 POST，
        if (&quot;POST&quot;.equals(request.getMethod()) &amp;amp;&amp;amp; request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
            // 获取标签中 name=&quot;_method&quot; 的 value 值
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                // 转成大写
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                // 兼容的请求方式
                if (ALLOWED_METHODS.contains(method)) {
                    // 包装请求
                    requestToUse = new HttpMethodRequestWrapper(request, method);
                }
            }
        }
        // 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的
        filterChain.doFilter(requestToUse, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rest 使用客户端工具，如 Postman 可直接发送 put、delete 等方式请求不被过滤&lt;/p&gt;
&lt;p&gt;改变默认的 &lt;code&gt;_method&lt;/code&gt; 的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
public class WebConfig{
    //自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        //通过set 方法自定义
        methodFilter.setMethodParam(&quot;_m&quot;);
        return methodFilter;
    }    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Servlet&lt;/h3&gt;
&lt;p&gt;SpringMVC 提供访问原始 Servlet 接口的功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpringMVC 提供访问原始 Servlet 接口 API 的功能，通过形参声明即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/servletApi&quot;)
public String servletApi(HttpServletRequest request,
                         HttpServletResponse response, HttpSession session){
    System.out.println(request);
    System.out.println(response);
    System.out.println(session);
    request.setAttribute(&quot;name&quot;,&quot;seazean&quot;);
    System.out.println(request.getAttribute(&quot;name&quot;));
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Head 数据获取快捷操作方式
名称：@RequestHeader
类型：形参注解
位置：处理器类中的方法形参前方
作用：绑定请求头数据与对应处理方法形参间的关系
范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;快捷操作方式@RequestMapping(&quot;/headApi&quot;)
public String headApi(@RequestHeader(&quot;Accept-Language&quot;) String headMsg){
    System.out.println(headMsg);
    return &quot;page&quot;;
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie 数据获取快捷操作方式
名称：@CookieValue
类型：形参注解
位置：处理器类中的方法形参前方
作用：绑定请求 Cookie 数据与对应处理方法形参间的关系
范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/cookieApi&quot;)
public String cookieApi(@CookieValue(&quot;JSESSIONID&quot;) String jsessionid){
    System.out.println(jsessionid);
    return &quot;page&quot;;
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session 数据获取
名称：@SessionAttribute
类型：形参注解
位置：处理器类中的方法形参前方
作用：绑定请求Session数据与对应处理方法形参间的关系
范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/sessionApi&quot;)
public String sessionApi(@SessionAttribute(&quot;name&quot;) String name){
    System.out.println(name);
    return &quot;page.jsp&quot;;
}
//用于在session中放入数据
@RequestMapping(&quot;/setSessionData&quot;)
public String setSessionData(HttpSession session){
    session.setAttribute(&quot;name&quot;,&quot;seazean&quot;);
    return &quot;page&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session 数据设置
名称：@SessionAttributes
类型：类注解
位置：处理器类上方
作用：声明放入session范围的变量名称，适用于Model类型数据传参
范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
//设定当前类中名称为age和gender的变量放入session范围，不常用
@SessionAttributes(names = {&quot;age&quot;,&quot;gender&quot;})
public class ServletController {
	//将数据放入session存储范围，Model对象实现数据set，@SessionAttributes注解实现范围设定
    @RequestMapping(&quot;/setSessionData2&quot;)
    public String setSessionDate2(Model model) {
        model.addAttribute(&quot;age&quot;,39);
        model.addAttribute(&quot;gender&quot;,&quot;男&quot;);
        return &quot;page&quot;;
    }
    
    @RequestMapping(&quot;/sessionApi&quot;)
    public String sessionApi(@SessionAttribute(&quot;age&quot;) int age,
                             @SessionAttribute(&quot;gender&quot;) String gender){
        System.out.println(name);
        System.out.println(age);
        return &quot;page&quot;;
    }
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;spring-mvc.xml 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd&quot;&amp;gt;

    &amp;lt;context:component-scan base-package=&quot;com.seazean&quot;/&amp;gt;
    &amp;lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&amp;gt;
        &amp;lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/page/&quot;/&amp;gt;
        &amp;lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;
    &amp;lt;mvc:annotation-driven/&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;运行原理&lt;/h2&gt;
&lt;h3&gt;技术架构&lt;/h3&gt;
&lt;h4&gt;组件介绍&lt;/h4&gt;
&lt;p&gt;核心组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DispatcherServlet：核心控制器， 是 SpringMVC 的核心，整体流程控制的中心，所有的请求第一步都先到达这里，由其调用其它组件处理用户的请求，它就是在 web.xml 配置的核心 Servlet，有效的降低了组件间的耦合性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HandlerMapping：处理器映射器， 负责根据请求找到对应具体的 Handler 处理器，SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handler：处理器，其实就是 Controller，业务处理的核心类，通常由开发者编写，并且必须遵守 Controller 开发的规则，这样适配器才能正确的执行。例如实现 Controller 接口，将 Controller 注册到 IOC 容器中等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HandlAdapter：处理器适配器，根据映射器中找到的 Handler，通过 HandlerAdapter 去执行 Handler，这是适配器模式的应用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View Resolver：视图解析器， 将 Handler 中返回的逻辑视图（ModelAndView）解析为一个具体的视图（View）对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View：视图， View 最后对页面进行渲染将结果返回给用户，SpringMVC 框架提供了很多的 View 视图类型，包括：jstlView、freemarkerView、pdfView 等&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与 Spring 集成，更好的管理资源&lt;/li&gt;
&lt;li&gt;有很多参数解析器和视图解析器，支持的数据类型丰富&lt;/li&gt;
&lt;li&gt;将映射器、处理器、视图解析器进行解耦，分工明确&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;工作原理&lt;/h4&gt;
&lt;p&gt;在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系，保存到 Map&amp;lt;URL, Controller&amp;gt; 中，这样 request 就能快速根据 URL 定位到 Controller：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Spring IOC 容器初始化完所有单例 bean 后&lt;/li&gt;
&lt;li&gt;SpringMVC 会遍历所有的 bean，获取 Controller 中对应的 URL（这里获取 URL 的实现类有多个，用于处理不同形式配置的 Controller）&lt;/li&gt;
&lt;li&gt;将每一个 URL 对应一个 Controller 存入 Map&amp;lt;URL, Controller&amp;gt; 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：将 @Controller 注解换成 @Component，启动时不会报错，但是在浏览器中输入路径时会出现 404，说明 Spring 没有对所有的 bean 进行 URL 映射&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个 Request 来了：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监听端口，获得请求：Tomcat 监听 8080 端口的请求处理，根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet，&lt;code&gt;DispatcherServlet#doDispatch&lt;/code&gt; 是&lt;strong&gt;核心调度方法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首先根据 URI 获取 HandlerMapping 处理器映射器&lt;/strong&gt;，RequestMappingHandlerMapping 用来处理 @RequestMapping 注解的映射规则，其中保存了所有 handler 的映射规则，最后包装成一个拦截器链返回，拦截器链对象持有 HandlerMapping。如果没有合适的处理请求的 HandlerMapping，说明请求处理失败，设置响应码 404 返回&lt;/li&gt;
&lt;li&gt;根据映射器获取当前 handler，&lt;strong&gt;处理器适配器执行处理方法&lt;/strong&gt;，适配器根据请求的 URL 去 handler 中寻找对应的处理方法：
&lt;ul&gt;
&lt;li&gt;创建 ModelAndViewContainer (mav) 对象，用来填充数据，然后通过不同的&lt;strong&gt;参数解析器&lt;/strong&gt;去解析 URL 中的参数，完成数据解析绑定，然后执行真正的 Controller 方法，完成 handle 处理&lt;/li&gt;
&lt;li&gt;方法执行完对&lt;strong&gt;返回值&lt;/strong&gt;进行处理，没添加 @ResponseBody 注解的返回值使用视图处理器处理，把视图名称设置进入 mav 中&lt;/li&gt;
&lt;li&gt;对添加了 @ResponseBody 注解的 Controller 的按照普通的返回值进行处理，首先进行内容协商，找到一种浏览器可以接受（请求头 Accept）的并且服务器可以生成的数据类型，选择合适数据转换器，设置响应头中的数据类型，然后写出数据&lt;/li&gt;
&lt;li&gt;最后把 ModelAndViewContainer 和 ModelMap 中的数据&lt;strong&gt;封装到 ModelAndView 对象&lt;/strong&gt;返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视图解析&lt;/strong&gt;，根据返回值创建视图，请求转发 View 实例为 InternalResourceView，重定向 View 实例为 RedirectView。最后调用 view.render 进行页面渲染，结果派发
&lt;ul&gt;
&lt;li&gt;请求转发时请求域中的数据不丢失，会把 ModelAndView 的数据设置到请求域中，获取 Servlet 原生的 RequestDispatcher，调用 &lt;code&gt;RequestDispatcher#forward&lt;/code&gt; 实现转发&lt;/li&gt;
&lt;li&gt;重定向会造成请求域中的数据丢失，使用 Servlet 原生方式实现重定向 &lt;code&gt;HttpServletResponse#sendRedirect&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;调度函数&lt;/h3&gt;
&lt;p&gt;请求进入原生的 HttpServlet 的 doGet() 方法处理，调用子类 FrameworkServlet 的 doGet() 方法，最终调用 DispatcherServlet 的 doService() 方法，为请求设置相关属性后调用 doDispatch()，请求和响应的以参数的形式传入&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E8%AF%B7%E6%B1%82%E7%9B%B8%E5%BA%94%E7%9A%84%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// request 和 response 为 Java 原生的类
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    // 文件上传请求
    boolean multipartRequestParsed = false;
    // 异步管理器
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            // 文件上传相关请求
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 找到当前请求使用哪个 HandlerMapping （Controller 的方法）处理，返回执行链
            mappedHandler = getHandler(processedRequest);
            // 没有合适的处理请求的方式 HandlerMapping，请求失败，直接返回 404
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 根据映射器获取当前 handler 处理器适配器，用来【处理当前的请求】
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            // 获取发出此次请求的方式
            String method = request.getMethod();
            // 判断请求是不是 GET 方法
            boolean isGet = HttpMethod.GET.matches(method);
            if (isGet || HttpMethod.HEAD.matches(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) &amp;amp;&amp;amp; isGet) {
                    return;
                }
            }
			// 拦截器链的前置处理
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            // 执行处理方法，返回的是 ModelAndView 对象，封装了所有的返回值数据
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
			// 设置视图名字
            applyDefaultViewName(processedRequest, mv);
            // 执行拦截器链中的后置处理方法
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception ex) {
            dispatchException = ex;
        }
        
        // 处理程序调用的结果，进行结果派发
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;笔记参考视频：https://www.bilibili.com/video/BV19K4y1L7MT&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;请求映射&lt;/h3&gt;
&lt;h4&gt;映射器&lt;/h4&gt;
&lt;p&gt;doDispatch() 中调用 getHandler 方法获取所有的映射器&lt;/p&gt;
&lt;p&gt;总体流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有的请求映射都在 HandlerMapping 中，&lt;strong&gt;RequestMappingHandlerMapping 处理 @RequestMapping 注解的映射规则&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历所有的 HandlerMapping 看是否可以匹配当前请求，匹配成功后返回，匹配失败设置 HTTP 404 响应码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户可以自定义的映射处理，也可以给容器中放入自定义 HandlerMapping&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;访问 URL：http://localhost:8080/user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/user&quot;)
public String getUser(){
    return &quot;GET&quot;;
}
@PostMapping(&quot;/user&quot;)
public String postUser(){
    return &quot;POST&quot;;
}
//。。。。。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HandlerMapping 处理器映射器，&lt;strong&gt;保存了所有 &lt;code&gt;@RequestMapping&lt;/code&gt;  和 &lt;code&gt;handler&lt;/code&gt; 的映射规则&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        // 遍历所有的 HandlerMapping
        for (HandlerMapping mapping : this.handlerMappings) {
            // 尝试去每个 HandlerMapping 中匹配当前请求的处理
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E8%8E%B7%E5%8F%96Controller%E5%A4%84%E7%90%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mapping.getHandler(request)&lt;/code&gt;：调用 AbstractHandlerMapping#getHandler&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object handler = getHandlerInternal(request)&lt;/code&gt;：&lt;strong&gt;获取映射器&lt;/strong&gt;，底层调用 RequestMappingInfoHandlerMapping 类的方法，又调用 AbstractHandlerMethodMapping#getHandlerInternal&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String lookupPath = initLookupPath(request)&lt;/code&gt;：地址栏的 URI，这里的 lookupPath 为 /user&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mappingRegistry.acquireReadLock()&lt;/code&gt;：加读锁防止其他线程并发修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;handlerMethod = lookupHandlerMethod(lookupPath, request)&lt;/code&gt;：获取当前 HandlerMapping 中的映射规则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)&lt;/code&gt;：获取当前的映射器与当前&lt;strong&gt;请求的 URI 有关的所有映射规则&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-HandlerMapping%E7%9A%84%E6%98%A0%E5%B0%84%E8%A7%84%E5%88%99.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addMatchingMappings(directPathMatches, matches, request)&lt;/code&gt;：&lt;strong&gt;匹配某个映射规则&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (T mapping : mappings)&lt;/code&gt;：遍历所有的映射规则&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match = getMatchingMapping(mapping, request)&lt;/code&gt;：去匹配每一个映射规则，匹配失败返回 null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matches.add(new Match())&lt;/code&gt;：匹配成功后封装成匹配器添加到匹配集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;matches.sort(comparator)&lt;/code&gt;：匹配集合排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Match bestMatch = matches.get(0)&lt;/code&gt;：匹配完成只剩一个，直接获取返回对应的处理方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (matches.size() &amp;gt; 1)&lt;/code&gt;：当有多个映射规则符合请求时，报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return bestMatch.getHandlerMethod()&lt;/code&gt;：返回匹配器中的处理方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;executionChain = getHandlerExecutionChain(handler, request)&lt;/code&gt;：&lt;strong&gt;为当前请求和映射器的构建一个拦截器链&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for (HandlerInterceptor interceptor : this.adaptedInterceptors)&lt;/code&gt;：遍历所有的拦截器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chain.addInterceptor(interceptor)&lt;/code&gt;：把所有的拦截器添加到 HandlerExecutionChain 中，形成拦截器链&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return executionChain&lt;/code&gt;：&lt;strong&gt;返回拦截器链，HandlerMapping 是链的 handler 成员属性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;适配器&lt;/h4&gt;
&lt;p&gt;doDispatch() 中调用 &lt;code&gt;HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        // 遍历所有的 HandlerAdapter
        for (HandlerAdapter adapter : this.handlerAdapters) {
            // 判断当前适配器是否支持当前 handle
            if (adapter.supports(handler)) {
                // 返回的是 【RequestMappingHandlerAdapter】
                // AbstractHandlerMethodAdapter#supports -&amp;gt; RequestMappingHandlerAdapter
                return adapter;
            }
        }
    }
    throw new ServletException();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;方法执行&lt;/h4&gt;
&lt;p&gt;实例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/params&quot;)
public String param(Map&amp;lt;String, Object&amp;gt; map, Model model, HttpServletRequest request) {
    map.put(&quot;k1&quot;, &quot;v1&quot;);			// 都可以向请求域中添加数据
    model.addAttribute(&quot;k2&quot;, &quot;v2&quot;);	// 它们两个都在数据封装在 【BindingAwareModelMap】，继承自 LinkedHashMap
    request.setAttribute(&quot;m&quot;, &quot;HelloWorld&quot;);
    return &quot;forward:/success&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-Model%E5%92%8CMap%E7%9A%84%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;doDispatch() 中调用 &lt;code&gt;mv = ha.handle(processedRequest, response, mappedHandler.getHandler())&lt;/code&gt; &lt;strong&gt;使用适配器执行方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractHandlerMethodAdapter#handle&lt;/code&gt; → &lt;code&gt;RequestMappingHandlerAdapter#handleInternal&lt;/code&gt; → &lt;code&gt;invokeHandlerMethod&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
                                           HttpServletResponse response, 
                                           HandlerMethod handlerMethod) throws Exception {
	// 封装成 SpringMVC 的接口，用于通用 Web 请求拦截器，使能够访问通用请求元数据，而不是用于实际处理请求
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        // WebDataBinder 用于【从 Web 请求参数到 JavaBean 对象的数据绑定】，获取创建该实例的工厂
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        // 创建 Model 实例，用于向模型添加属性
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
		// 方法执行器
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        
        // 参数解析器，有很多
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        // 返回值处理器，也有很多
        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }
        // 设置数据绑定器
        invocableMethod.setDataBinderFactory(binderFactory);
        // 设置参数检查器
		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
   
        // 新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
            
        // 设置一些属性
        
        // 【执行目标方法】
        invocableMethod.invokeAndHandle(webRequest, mavContainer);
        // 异步请求
        if (asyncManager.isConcurrentHandlingStarted()) {
            return null;
        }
		// 【获取 ModelAndView 对象，封装了 ModelAndViewContainer】
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally {
        webRequest.requestCompleted();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ServletInvocableHandlerMethod#invokeAndHandle：执行目标方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)&lt;/code&gt;：&lt;strong&gt;执行自己写的 controller 方法，返回的就是自定义方法中 return 的值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs)&lt;/code&gt;：&lt;strong&gt;参数处理的逻辑&lt;/strong&gt;，遍历所有的参数解析器解析参数或者将 URI 中的参数进行绑定，绑定完成后开始执行目标方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parameters = getMethodParameters()&lt;/code&gt;：获取此处理程序方法的方法参数的详细信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Object[] args = new Object[parameters.length]&lt;/code&gt;：存放所有的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (int i = 0; i &amp;lt; parameters.length; i++)&lt;/code&gt;：遍历所有的参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;args[i] = findProvidedArgument(parameter, providedArgs)&lt;/code&gt;：获取调用方法时提供的参数，一般是空&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!this.resolvers.supportsParameter(parameter))&lt;/code&gt;：&lt;strong&gt;获取可以解析当前参数的参数解析器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return getArgumentResolver(parameter) != null&lt;/code&gt;：获取参数的解析是否为空&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)&lt;/code&gt;：遍历容器内所有的解析器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if (resolver.supportsParameter(parameter))&lt;/code&gt;：是否支持当前参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PathVariableMethodArgumentResolver#supportsParameter&lt;/code&gt;：&lt;strong&gt;解析标注 @PathVariable 注解的参数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModelMethodProcessor#supportsParameter&lt;/code&gt;：解析 Map 和 Model 类型的参数，Model 和 Map 的作用一样&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExpressionValueMethodArgumentResolver#supportsParameter&lt;/code&gt;：解析标注 @Value 注解的参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RequestParamMapMethodArgumentResolver#supportsParameter&lt;/code&gt;：&lt;strong&gt;解析标注 @RequestParam 注解&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RequestPartMethodArgumentResolver#supportsParameter&lt;/code&gt;：解析文件上传的信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModelAttributeMethodProcessor#supportsParameter&lt;/code&gt;：解析标注 @ModelAttribute 注解或者不是简单类型
&lt;ul&gt;
&lt;li&gt;子类 ServletModelAttributeMethodProcessor 是解析自定义类型 JavaBean 的解析器&lt;/li&gt;
&lt;li&gt;简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;args[i] = this.resolvers.resolveArgument()&lt;/code&gt;：&lt;strong&gt;开始解析参数，每个参数使用的解析器不同&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;resolver = getArgumentResolver(parameter)&lt;/code&gt;：获取参数解析器&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return resolver.resolveArgument()&lt;/code&gt;：开始解析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PathVariableMapMethodArgumentResolver#resolveArgument&lt;/code&gt;：@PathVariable，包装 URI 中的参数为 Map&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapMethodProcessor#resolveArgument&lt;/code&gt;：调用 &lt;code&gt;mavContainer.getModel()&lt;/code&gt; 返回默认 BindingAwareModelMap 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModelAttributeMethodProcessor#resolveArgument&lt;/code&gt;：&lt;strong&gt;自定义的 JavaBean 的绑定封装&lt;/strong&gt;，下一小节详解&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;return doInvoke(args)&lt;/code&gt;：&lt;strong&gt;真正的执行 Controller 方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Method method = getBridgedMethod()&lt;/code&gt;：从 HandlerMethod 获取要反射执行的方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReflectionUtils.makeAccessible(method)&lt;/code&gt;：破解权限&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method.invoke(getBean(), args)&lt;/code&gt;：执行方法，getBean 获取的是标记 @Controller 的 Bean 类，其中包含执行方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进行返回值的处理，响应部分详解&lt;/strong&gt;，处理完成进入下面的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RequestMappingHandlerAdapter#getModelAndView：获取 ModelAndView 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;modelFactory.updateModel(webRequest, mavContainer)&lt;/code&gt;：Model 数据升级到会话域（&lt;strong&gt;请求域中的数据在重定向时丢失&lt;/strong&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;updateBindingResult(request, defaultModel)&lt;/code&gt;：把绑定的数据添加到 BindingAwareModelMap 中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mavContainer.isRequestHandled())&lt;/code&gt;：判断请求是否已经处理完成了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ModelMap model = mavContainer.getModel()&lt;/code&gt;：获取&lt;strong&gt;包含 Controller 方法参数&lt;/strong&gt;的 BindingAwareModelMap（本节开头）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mav = new ModelAndView()&lt;/code&gt;：&lt;strong&gt;把 ModelAndViewContainer 和 ModelMap 中的数据封装到 ModelAndView&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!mavContainer.isViewReference())&lt;/code&gt;：是否是通过名称指定视图引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (model instanceof RedirectAttributes)&lt;/code&gt;：判断 model 是否是重定向数据，如果是进行重定向逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return mav&lt;/code&gt;：&lt;strong&gt;任何方法执行都会返回 ModelAndView 对象&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;参数解析&lt;/h4&gt;
&lt;p&gt;解析自定义的 JavaBean 为例，调用 ModelAttributeMethodProcessor#resolveArgument 处理参数的方法，通过合适的类型转换器把 URL 中的参数转换以后，利用反射获取 set 方法，注入到 JavaBean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Person.java：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Component	//加入到容器中
public class Person {
    private String userName;
    private Integer age;
    private Date birth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController	//返回的数据不是页面
public class ParameterController {
    // 数据绑定：页面提交的请求数据（GET、POST）都可以和对象属性进行绑定
    @GetMapping(&quot;/saveuser&quot;)
    public Person saveuser(Person person){
        return person;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问 URL：http://localhost:8080/saveuser?userName=zhangsan&amp;amp;age=20&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进入源码：ModelAttributeMethodProcessor#resolveArgument&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;name = ModelFactory.getNameForParameter(parameter)&lt;/code&gt;：获取名字，此例就是 person&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ann = parameter.getParameterAnnotation(ModelAttribute.class)&lt;/code&gt;：是否有 ModelAttribute 注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (mavContainer.containsAttribute(name))&lt;/code&gt;：ModelAndViewContainer 中是否包含 person 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;attribute = createAttribute()&lt;/code&gt;：&lt;strong&gt;创建一个实例，空的 Person 对象&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;binder = binderFactory.createBinder(webRequest, attribute, name)&lt;/code&gt;：Web 数据绑定器，可以利用 Converters 将请求数据转成指定的数据类型，绑定到 JavaBean 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bindRequestParameters(binder, webRequest)&lt;/code&gt;：&lt;strong&gt;利用反射向目标对象填充数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;servletBinder = (ServletRequestDataBinder) binder&lt;/code&gt;：类型强转&lt;/p&gt;
&lt;p&gt;&lt;code&gt;servletBinder.bind(servletRequest)&lt;/code&gt;：绑定数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mpvs = new MutablePropertyValues(request.getParameterMap())&lt;/code&gt;：获取请求 URI 参数中的 k-v 键值对&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addBindValues(mpvs, request)&lt;/code&gt;：子类可以用来为请求添加额外绑定值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;doBind(mpvs)&lt;/code&gt;：真正的绑定的方法，调用 &lt;code&gt;applyPropertyValues&lt;/code&gt; 应用参数值，然后调用 &lt;code&gt;setPropertyValues&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractPropertyAccessor#setPropertyValues()&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;PropertyValue&amp;gt; propertyValues&lt;/code&gt;：获取到所有的参数的值，就是 URI 上的所有的参数值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (PropertyValue pv : propertyValues)&lt;/code&gt;：遍历所有的参数值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setPropertyValue(pv)&lt;/code&gt;：&lt;strong&gt;填充到空的 Person 实例中&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nestedPa = getPropertyAccessorForPropertyPath(propertyName)&lt;/code&gt;：获取属性访问器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tokens = getPropertyNameTokens()&lt;/code&gt;：获取元数据的信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nestedPa.setPropertyValue(tokens, pv)&lt;/code&gt;：填充数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;processLocalProperty(tokens, pv)&lt;/code&gt;：处理属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!Boolean.FALSE.equals(pv.conversionNecessary))&lt;/code&gt;：数据是否需要转换了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (pv.isConverted())&lt;/code&gt;：数据已经转换过了，转换了直接赋值，没转换进行转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;oldValue = ph.getValue()&lt;/code&gt;：获取未转换的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;valueToApply = convertForProperty()&lt;/code&gt;：进行数据转换&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TypeConverterDelegate#convertIfNecessary&lt;/code&gt;：进入该方法的逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (conversionService.canConvert(sourceTypeDesc, typeDescriptor))&lt;/code&gt;：判断能不能转换&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GenericConverter converter = getConverter(sourceType, targetType)&lt;/code&gt;：&lt;strong&gt;获取类型转换器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;converter = this.converters.find(sourceType, targetType)&lt;/code&gt;：寻找合适的转换器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sourceCandidates = getClassHierarchy(sourceType.getType())&lt;/code&gt;：原数据类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;targetCandidates = getClassHierarchy(targetType.getType())&lt;/code&gt;：目标数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (Class&amp;lt;?&amp;gt; sourceCandidate : sourceCandidates) {
    //双重循环遍历，寻找合适的转换器
 	for (Class&amp;lt;?&amp;gt; targetCandidate : targetCandidates) {
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GenericConverter converter = getRegisteredConverter(..)&lt;/code&gt;：匹配类型转换器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return converter&lt;/code&gt;：返回转换器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;conversionService.convert(newValue, sourceTypeDesc, typeDescriptor)&lt;/code&gt;：开始转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;converter = getConverter(sourceType, targetType)&lt;/code&gt;：&lt;strong&gt;获取可用的转换器&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result = ConversionUtils.invokeConverter()&lt;/code&gt;：执行转换方法
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;converter.convert()&lt;/code&gt;：&lt;strong&gt;调用转换器的转换方法&lt;/strong&gt;（GenericConverter#convert）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return handleResult(sourceType, targetType, result)&lt;/code&gt;：返回结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ph.setValue(valueToApply)&lt;/code&gt;：&lt;strong&gt;设置 JavaBean 属性&lt;/strong&gt;（BeanWrapperImpl.BeanPropertyHandler）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Method writeMethod&lt;/code&gt;：获取写数据方法
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Class&amp;lt;?&amp;gt; cls = getClass0()&lt;/code&gt;：获取 Class 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeMethodName = Introspector.SET_PREFIX + getBaseName()&lt;/code&gt;：&lt;strong&gt;set 前缀 + 属性名&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args)&lt;/code&gt;：获取只包含一个参数的 set 方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setWriteMethod(writeMethod)&lt;/code&gt;：加入缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReflectionUtils.makeAccessible(writeMethod)&lt;/code&gt;：设置访问权限&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeMethod.invoke(getWrappedInstance(), value)&lt;/code&gt;：执行方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bindingResult = binder.getBindingResult()&lt;/code&gt;：获取绑定的结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mavContainer.addAllAttributes(bindingResultModel)&lt;/code&gt;：&lt;strong&gt;把所有填充的参数放入 ModelAndViewContainer&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return attribute&lt;/code&gt;：返回填充后的 Person 对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;响应处理&lt;/h3&gt;
&lt;h4&gt;响应数据&lt;/h4&gt;
&lt;p&gt;以 Person 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ResponseBody  		// 利用返回值处理器里面的消息转换器进行处理，而不是视图
@GetMapping(value = &quot;/person&quot;)
public Person getPerson(){
    Person person = new Person();
    person.setAge(28);
    person.setBirth(new Date());
    person.setUserName(&quot;zhangsan&quot;);
    return person;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接进入方法执行完后的逻辑 ServletInvocableHandlerMethod#invokeAndHandle：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
                            Object... providedArgs) throws Exception {
	// 【执行目标方法】，return person 对象
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    // 设置状态码
    setResponseStatus(webRequest);

    // 判断方法是否有返回值
    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    }	// 返回值是字符串
    else if (StringUtils.hasText(getResponseStatusReason())) {
        // 设置请求处理完成
        mavContainer.setRequestHandled(true);
        return;
	// 设置请求没有处理完成，还需要进行返回值的逻辑
    mavContainer.setRequestHandled(false);
    Assert.state(this.returnValueHandlers != null, &quot;No return value handlers&quot;);
    try {
        // 【返回值的处理】
        this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    catch (Exception ex) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没有加 @ResponseBody 注解的返回数据按照视图处理的逻辑&lt;/strong&gt;，ViewNameMethodReturnValueHandler（视图详解）&lt;/li&gt;
&lt;li&gt;此例是加了注解的，返回的数据不是视图，HandlerMethodReturnValueHandlerComposite#handleReturnValue：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest)  {
	// 获取合适的返回值处理器
    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
    if (handler == null) {
        throw new IllegalArgumentException();
    }
    // 使用处理器处理返回值（详解源码中的这两个函数）
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HandlerMethodReturnValueHandlerComposite#selectHandler：获取合适的返回值处理器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;boolean isAsyncValue = isAsyncReturnValue(value, returnType)&lt;/code&gt;：是否是异步请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers)&lt;/code&gt;：遍历所有的返回值处理器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RequestResponseBodyMethodProcessor#supportsReturnType&lt;/code&gt;：&lt;strong&gt;处理标注 @ResponseBody 注解的返回值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModelAndViewMethodReturnValueHandler#supportsReturnType&lt;/code&gt;：处理返回值类型是 ModelAndView 的处理器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModelAndViewResolverMethodReturnValueHandler#supportsReturnType&lt;/code&gt;：直接返回 true，处理所有数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RequestResponseBodyMethodProcessor#handleReturnValue：处理返回值，要进行&lt;strong&gt;内容协商&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mavContainer.setRequestHandled(true)&lt;/code&gt;：设置请求处理完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;inputMessage = createInputMessage(webRequest)&lt;/code&gt;：获取输入的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;outputMessage = createOutputMessage(webRequest)&lt;/code&gt;：获取输出的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)&lt;/code&gt;：使用消息转换器进行写出&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (value instanceof CharSequence)&lt;/code&gt;：判断返回的数据是不是字符类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;body = value&lt;/code&gt;：把 value 赋值给 body，此时 body 中就是自定义方法执行完后的 Person 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (isResourceType(value, returnType))&lt;/code&gt;：当前数据是不是流数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MediaType selectedMediaType&lt;/code&gt;：&lt;strong&gt;内容协商后选择使用的类型，浏览器和服务器都支持的媒体（数据）类型&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MediaType contentType = outputMessage.getHeaders().getContentType()&lt;/code&gt;：获取响应头的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (contentType != null &amp;amp;&amp;amp; contentType.isConcrete())&lt;/code&gt;：判断当前响应头中是否已经有确定的媒体类型&lt;/p&gt;
&lt;p&gt;&lt;code&gt;selectedMediaType = contentType&lt;/code&gt;：前置处理已经使用了媒体类型，直接继续使用该类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acceptableTypes = getAcceptableMediaTypes(request)&lt;/code&gt;：&lt;strong&gt;获取浏览器支持的媒体类型，请求头字段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.contentNegotiationManager.resolveMediaTypes()&lt;/code&gt;：调用该方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for(ContentNegotiationStrategy strategy:this.strategies)&lt;/code&gt;：&lt;strong&gt;默认策略是提取请求头的字段的内容&lt;/strong&gt;，策略类为HeaderContentNegotiationStrategy，可以配置添加其他类型的策略
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;MediaType&amp;gt; mediaTypes = strategy.resolveMediaTypes(request)&lt;/code&gt;：解析 Accept 字段存储为 List
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)&lt;/code&gt;：获取请求头中 Accept 字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;MediaType&amp;gt; mediaTypes = MediaType.parseMediaTypes(headerValues)&lt;/code&gt;：解析成 List 集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MediaType.sortBySpecificityAndQuality(mediaTypes)&lt;/code&gt;：按照相对品质因数 q 降序排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E6%B5%8F%E8%A7%88%E5%99%A8%E6%94%AF%E6%8C%81%E6%8E%A5%E6%94%B6%E7%9A%84%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;producibleTypes = getProducibleMediaTypes(request, valueType, targetType)&lt;/code&gt;：&lt;strong&gt;服务器能生成的媒体类型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)&lt;/code&gt;：从请求域获取默认的媒体类型
&lt;ul&gt;
&lt;li&gt;&lt;code&gt; for (HttpMessageConverter&amp;lt;?&amp;gt; converter : this.messageConverters)&lt;/code&gt;：遍历所有的消息转换器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;converter.canWrite(valueClass, null)&lt;/code&gt;：是否支持当前的类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt; result.addAll(converter.getSupportedMediaTypes())&lt;/code&gt;：把当前 MessageConverter 支持的所有类型放入 result&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;MediaType&amp;gt; mediaTypesToUse = new ArrayList&amp;lt;&amp;gt;()&lt;/code&gt;：存储最佳匹配的集合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内容协商：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  for (MediaType requestedType : acceptableTypes) {				// 遍历所有浏览器能接受的媒体类型
      for (MediaType producibleType : producibleTypes) {		// 遍历所有服务器能产出的
          if (requestedType.isCompatibleWith(producibleType)) {	// 判断类型是否匹配，最佳匹配
              // 数据协商匹配成功，一般有多种
              mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
          }
      }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MediaType.sortBySpecificityAndQuality(mediaTypesToUse)&lt;/code&gt;：按照相对品质因数 q 排序，降序排序，越大的越好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (MediaType mediaType : mediaTypesToUse)&lt;/code&gt;：&lt;strong&gt;遍历所有的最佳匹配&lt;/strong&gt;，选择一种赋值给选择的类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;selectedMediaType = selectedMediaType.removeQualityValue()&lt;/code&gt;：媒体类型去除相对品质因数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (HttpMessageConverter&amp;lt;?&amp;gt; converter : this.messageConverters)&lt;/code&gt;：&lt;strong&gt;遍历所有的 HTTP 数据转换器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GenericHttpMessageConverter genericConverter&lt;/code&gt;：&lt;strong&gt;MappingJackson2HttpMessageConverter 可以将对象写为 JSON&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;((GenericHttpMessageConverter) converter).canWrite()&lt;/code&gt;：判断转换器是否可以写出给定的类型&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractJackson2HttpMessageConverter#canWrit&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (!canWrite(mediaType))&lt;/code&gt;：是否可以写出指定类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MediaType.ALL.equalsTypeAndSubtype(mediaType)&lt;/code&gt;：是不是 &lt;code&gt;*/*&lt;/code&gt; 类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSupportedMediaTypes()&lt;/code&gt;：支持 &lt;code&gt;application/json&lt;/code&gt; 和 &lt;code&gt;application/*+json&lt;/code&gt; 两种类型
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;return true&lt;/code&gt;：返回 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objectMapper = selectObjectMapper(clazz, mediaType)&lt;/code&gt;：选择可以使用的 objectMapper&lt;/li&gt;
&lt;li&gt;&lt;code&gt;causeRef = new AtomicReference&amp;lt;&amp;gt;()&lt;/code&gt;：获取并发安全的引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if (objectMapper.canSerialize(clazz, causeRef))&lt;/code&gt;：objectMapper 可以序列化当前类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return true&lt;/code&gt;：返回 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt; body = getAdvice().beforeBodyWrite()&lt;/code&gt;：&lt;strong&gt;获取要响应的所有数据，就是 Person 对象&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addContentDispositionHeader(inputMessage, outputMessage)&lt;/code&gt;：检查路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;genericConverter.write(body, targetType, selectedMediaType, outputMessage)&lt;/code&gt;：调用消息转换器的 write 方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractGenericHttpMessageConverter#write&lt;/code&gt;：该类的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addDefaultHeaders(headers, t, contentType)&lt;/code&gt;：&lt;strong&gt;设置响应头中的数据类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%AE%BE%E7%BD%AE%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;writeInternal(t, type, outputMessage)&lt;/code&gt;：&lt;strong&gt;数据写出为 JSON 格式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object value = object&lt;/code&gt;：value 引用 Person 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ObjectWriter objectWriter = objectMapper.writer()&lt;/code&gt;：获取 ObjectWriter 对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objectWriter.writeValue(generator, value)&lt;/code&gt;：使用 ObjectWriter 写出数据为 JSON&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;协商策略&lt;/h4&gt;
&lt;p&gt;开启基于请求参数的内容协商模式：（SpringBoot 方式）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.mvc.contentnegotiation:favor-parameter: true  # 开启请求参数内容协商模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发请求： http://localhost:8080/person?format=json，解析 format&lt;/p&gt;
&lt;p&gt;策略类为 ParameterContentNegotiationStrategy，运行流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acceptableTypes = getAcceptableMediaTypes(request)&lt;/code&gt;：获取浏览器支持的媒体类型&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mediaTypes = strategy.resolveMediaTypes(request)&lt;/code&gt;：解析请求 URL 参数中的数据&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest))&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getMediaTypeKey(webRequest)&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request.getParameter(getParameterName())&lt;/code&gt;：获取 URL 中指定的需求的数据类型
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getParameterName()&lt;/code&gt;：获取参数的属性名 format&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getParameter()&lt;/code&gt;：&lt;strong&gt;获取 URL 中 format 对应的数据&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;resolveMediaTypeKey()&lt;/code&gt;：解析媒体类型，封装成集合&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自定义内容协商策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebConfig implements WebMvcConfigurer {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override	//自定义内容协商策略
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                Map&amp;lt;String, MediaType&amp;gt; mediaTypes = new HashMap&amp;lt;&amp;gt;();
                mediaTypes.put(&quot;json&quot;, MediaType.APPLICATION_JSON);
                mediaTypes.put(&quot;xml&quot;,MediaType.APPLICATION_XML);
                mediaTypes.put(&quot;person&quot;,MediaType.parseMediaType(&quot;application/x-person&quot;));
                // 指定支持解析哪些参数对应的哪些媒体类型
                ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);

                // 请求头解析
                HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy();

                // 添加到容器中，即可以解析请求头 又可以解析请求参数
                configurer.strategies(Arrays.asList(parameterStrategy,headStrategy));
            }
            
            @Override 	// 自定义消息转换器
            public void extendMessageConverters(List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converters) {
                converters.add(new GuiguMessageConverter());
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以自定义 HttpMessageConverter，实现 HttpMessageConverter&amp;lt;T&amp;gt; 接口重写方法即可&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;视图解析&lt;/h3&gt;
&lt;h4&gt;返回解析&lt;/h4&gt;
&lt;p&gt;请求处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/params&quot;)
public String param(){
	return &quot;forward:/success&quot;;
    //return &quot;redirect:/success&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入执行方法逻辑 ServletInvocableHandlerMethod#invokeAndHandle，进入 &lt;code&gt;this.returnValueHandlers.handleReturnValue&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest)  {
	// 获取合适的返回值处理器：调用 if (handler.supportsReturnType(returnType))判断是否支持
    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
    if (handler == null) {
        throw new IllegalArgumentException();
    }
    // 使用处理器处理返回值
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ViewNameMethodReturnValueHandler#supportsReturnType：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean supportsReturnType(MethodParameter returnType) {
    Class&amp;lt;?&amp;gt; paramType = returnType.getParameterType();
    // 返回值是否是 void 或者是 CharSequence 字符序列，这里是字符序列
    return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ViewNameMethodReturnValueHandler#handleReturnValue：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, 
                              NativeWebRequest webRequest) throws Exception {
	// 返回值是字符串，是 return &quot;forward:/success&quot;
    if (returnValue instanceof CharSequence) {
        String viewName = returnValue.toString();
        // 【把视图名称设置进入 ModelAndViewContainer 中】
        mavContainer.setViewName(viewName);
        // 判断是否是重定向数据 `viewName.startsWith(&quot;redirect:&quot;)`
        if (isRedirectViewName(viewName)) {
            // 如果是重定向，设置是重定向指令
            mavContainer.setRedirectModelScenario(true);
        }
    }
    else if (returnValue != null) {
        // should not happen
        throw new UnsupportedOperationException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;结果派发&lt;/h4&gt;
&lt;p&gt;doDispatch() 中的 processDispatchResult：处理派发结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler, 
                                   @Nullable ModelAndView mv,
                                   @Nullable Exception exception) throws Exception {
    boolean errorView = false;
    if (exception != null) {
    }
    // mv 是 ModelAndValue
    if (mv != null &amp;amp;&amp;amp; !mv.wasCleared()) {
        // 渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {}  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DispatcherServlet#render：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Locale locale = this.localeResolver.resolveLocale(request)&lt;/code&gt;：国际化相关&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;String viewName = mv.getViewName()&lt;/code&gt;：视图名字，是请求转发 forward:/success（响应数据解析并存入 ModelAndView）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;view = resolveViewName(viewName, mv.getModelInternal(), locale, request)&lt;/code&gt;：解析视图&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (ViewResolver viewResolver : this.viewResolvers)&lt;/code&gt;：&lt;strong&gt;遍历所有的视图解析器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;view = viewResolver.resolveViewName(viewName, locale)&lt;/code&gt;：根据视图名字解析视图，调用内容协商视图处理器 ContentNegotiatingViewResolver 的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;attrs = RequestContextHolder.getRequestAttributes()&lt;/code&gt;：获取请求的相关属性信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())&lt;/code&gt;：获取最佳匹配的媒体类型，函数内进行了匹配的逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)&lt;/code&gt;：获取候选的视图对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (ViewResolver viewResolver : this.viewResolvers)&lt;/code&gt;：遍历所有的视图解析器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;View view = viewResolver.resolveViewName(viewName, locale)&lt;/code&gt;：&lt;strong&gt;解析视图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractCachingViewResolver#resolveViewName&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;returnview = createView(viewName, locale)&lt;/code&gt;：UrlBasedViewResolver#createView&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;请求转发&lt;/strong&gt;：实例为 InternalResourceView&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if (viewName.startsWith(FORWARD_URL_PREFIX))&lt;/code&gt;：视图名字是否是 &lt;strong&gt;&lt;code&gt;forward:&lt;/code&gt;&lt;/strong&gt; 的前缀&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())&lt;/code&gt;：名字截取前缀&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;view = new InternalResourceView(forwardUrl)&lt;/code&gt;：新建 InternalResourceView  对象并返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return applyLifecycleMethods(FORWARD_URL_PREFIX, view)&lt;/code&gt;：Spring 中的初始化操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重定向&lt;/strong&gt;：实例为 RedirectView&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (viewName.startsWith(REDIRECT_URL_PREFIX))&lt;/code&gt;：视图名字是否是 &lt;strong&gt;&lt;code&gt;redirect:&lt;/code&gt;&lt;/strong&gt; 的前缀&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length())&lt;/code&gt;：名字截取前缀&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RedirectView view = new RedirectView()&lt;/code&gt;：新建 RedirectView 对象并返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bestView = getBestView(candidateViews, requestedMediaTypes, attrs)&lt;/code&gt;：选出最佳匹配的视图对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;view.render(mv.getModelInternal(), request, response)&lt;/code&gt;：&lt;strong&gt;页面渲染&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mergedModel = createMergedOutputModel(model, request, response)&lt;/code&gt;：把请求域中的数据封装到 model&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;prepareResponse(request, response)&lt;/code&gt;：响应前的准备工作，设置一些响应头&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)&lt;/code&gt;：渲染输出的数据&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getRequestToExpose(request)&lt;/code&gt;：获取 Servlet 原生的方式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;请求转发 InternalResourceView 的逻辑：请求域中的数据不丢失&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exposeModelAsRequestAttributes(model, request)&lt;/code&gt;：暴露 model 作为请求域的属性
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model.forEach()&lt;/code&gt;：遍历 Model 中的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;request.setAttribute(name, value)&lt;/code&gt;：&lt;strong&gt;设置到请求域中&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exposeHelpers(request)&lt;/code&gt;：自定义接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dispatcherPath = prepareForRendering(request, response)&lt;/code&gt;：确定调度分派的路径，此例是 /success&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rd = getRequestDispatcher(request, dispatcherPath)&lt;/code&gt;：&lt;strong&gt;获取 Servlet 原生的 RequestDispatcher 实现转发&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rd.forward(request, response)&lt;/code&gt;：实现请求转发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重定向 RedirectView 的逻辑：请求域中的数据会丢失&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;targetUrl = createTargetUrl(model, request)&lt;/code&gt;：获取目标 URL
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enc = request.getCharacterEncoding()&lt;/code&gt;：设置编码 UTF-8&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appendQueryProperties(targetUrl, model, enc)&lt;/code&gt;：添加一些属性，比如 &lt;code&gt;url + ?name=123&amp;amp;&amp;amp;age=324&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sendRedirect(request, response, targetUrl, this.http10Compatible)&lt;/code&gt;：重定向
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;response.sendRedirect(encodedURL)&lt;/code&gt;：&lt;strong&gt;使用 Servlet 原生方法实现重定向&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;异步调用&lt;/h2&gt;
&lt;h3&gt;请求参数&lt;/h3&gt;
&lt;p&gt;名称：@RequestBody&lt;/p&gt;
&lt;p&gt;类型：形参注解&lt;/p&gt;
&lt;p&gt;位置：处理器类中的方法形参前方&lt;/p&gt;
&lt;p&gt;作用：将异步提交数据&lt;strong&gt;转换&lt;/strong&gt;成标准请求参数格式，并赋值给形参
范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller //控制层
public class AjaxController {
    @RequestMapping(&quot;/ajaxController&quot;)
    public String ajaxController(@RequestBody String message){
        System.out.println(message);
        return &quot;page.jsp&quot;;
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注解添加到 POJO  参数前方时，封装的异步提交数据按照 POJO  的属性格式进行关系映射
&lt;ul&gt;
&lt;li&gt;POJO 中的属性如果请求数据中没有，属性值为 null&lt;/li&gt;
&lt;li&gt;POJO 中没有的属性如果请求数据中有，不进行映射&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注解添加到集合参数前方时，封装的异步提交数据按照集合的存储结构进行关系映射&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/ajaxPojoToController&quot;)
//如果处理参数是POJO，且页面发送的请求数据格式与POJO中的属性对应，@RequestBody注解可以自动映射对应请求数据到POJO中
public String  ajaxPojoToController(@RequestBody User user){
    System.out.println(&quot;controller pojo :&quot;+user);
    return &quot;page.jsp&quot;;
}

@RequestMapping(&quot;/ajaxListToController&quot;)
//如果处理参数是List集合且封装了POJO，且页面发送的数据是JSON格式，数据将自动映射到集合参数
public String  ajaxListToController(@RequestBody List&amp;lt;User&amp;gt; userList){
    System.out.println(&quot;controller list :&quot;+userList);
    return &quot;page.jsp&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ajax.jsp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@page pageEncoding=&quot;UTF-8&quot; language=&quot;java&quot; contentType=&quot;text/html;UTF-8&quot; %&amp;gt;

&amp;lt;a href=&quot;javascript:void(0);&quot; id=&quot;testAjax&quot;&amp;gt;访问springmvc后台controller&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;a href=&quot;javascript:void(0);&quot; id=&quot;testAjaxPojo&quot;&amp;gt;传递Json格式POJO&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;a href=&quot;javascript:void(0);&quot; id=&quot;testAjaxList&quot;&amp;gt;传递Json格式List&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
    
&amp;lt;script type=&quot;text/javascript&quot; src=&quot;${pageContext.request.contextPath}/js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    $(function () {
        //为id=&quot;testAjax&quot;的组件绑定点击事件
        $(&quot;#testAjax&quot;).click(function(){
            //发送异步调用
            $.ajax({
               //请求方式：POST请求
               type:&quot;POST&quot;,
               //请求的地址
               url:&quot;ajaxController&quot;,
               //请求参数（也就是请求内容）
               data:&apos;ajax message&apos;,
               //响应正文类型
               dataType:&quot;text&quot;,
               //请求正文的MIME类型
               contentType:&quot;application/text&quot;,
            });
        });
        
         //为id=&quot;testAjaxPojo&quot;的组件绑定点击事件
        $(&quot;#testAjaxPojo&quot;).click(function(){
            $.ajax({
               type:&quot;POST&quot;,
               url:&quot;ajaxPojoToController&quot;,
               data:&apos;{&quot;name&quot;:&quot;Jock&quot;,&quot;age&quot;:39}&apos;,
               dataType:&quot;text&quot;,
               contentType:&quot;application/json&quot;,
            });
        });
        
        //为id=&quot;testAjaxList&quot;的组件绑定点击事件
        $(&quot;#testAjaxList&quot;).click(function(){
            $.ajax({//.....
               data:&apos;[{&quot;name&quot;:&quot;Jock&quot;,&quot;age&quot;:39},{&quot;name&quot;:&quot;Jockme&quot;,&quot;age&quot;:40}]&apos;})}
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;web.xml配置：请求响应章节请求中的web.xml配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CharacterEncodingFilter + DispatcherServlet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring-mvc.xml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;context:component-scan base-package=&quot;controller,domain&quot;/&amp;gt;
&amp;lt;mvc:resources mapping=&quot;/js/**&quot; location=&quot;/js/&quot;/&amp;gt;
&amp;lt;mvc:annotation-driven/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;响应数据&lt;/h3&gt;
&lt;p&gt;注解：@ResponseBody&lt;/p&gt;
&lt;p&gt;作用：将 Java 对象转为 json 格式的数据&lt;/p&gt;
&lt;p&gt;方法返回值为 POJO 时，自动封装数据成 Json 对象数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/ajaxReturnJson&quot;)
@ResponseBody
public User ajaxReturnJson(){
    System.out.println(&quot;controller return json pojo...&quot;);
    User user = new User(&quot;Jockme&quot;,40);
    return user;
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法返回值为 List 时，自动封装数据成 json 对象数组数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/ajaxReturnJsonList&quot;)
@ResponseBody
//基于jackon技术，使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据
public List ajaxReturnJsonList(){
    System.out.println(&quot;controller return json list...&quot;);
    User user1 = new User(&quot;Tom&quot;,3);
    User user2 = new User(&quot;Jerry&quot;,5);

    ArrayList al = new ArrayList();
    al.add(user1);
    al.add(user2);
    return al;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AJAX 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//为id=&quot;testAjaxReturnString&quot;的组件绑定点击事件
$(&quot;#testAjaxReturnString&quot;).click(function(){
    //发送异步调用
    $.ajax({
        type:&quot;POST&quot;,
        url:&quot;ajaxReturnString&quot;,
        //回调函数
        success:function(data){
            //打印返回结果
            alert(data);
        }
    });
});

//为id=&quot;testAjaxReturnJson&quot;的组件绑定点击事件
$(&quot;#testAjaxReturnJson&quot;).click(function(){
    $.ajax({
        type:&quot;POST&quot;,
        url:&quot;ajaxReturnJson&quot;,
        success:function(data){
            alert(data[&apos;name&apos;]+&quot; ,  &quot;+data[&apos;age&apos;]);
        }
    });
});

//为id=&quot;testAjaxReturnJsonList&quot;的组件绑定点击事件
$(&quot;#testAjaxReturnJsonList&quot;).click(function(){
    $.ajax({
        type:&quot;POST&quot;,
        url:&quot;ajaxReturnJsonList&quot;,
        success:function(data){
            alert(data);
            alert(data[0][&quot;name&quot;]);
            alert(data[1][&quot;age&quot;]);
        }
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;跨域访问&lt;/h3&gt;
&lt;p&gt;跨域访问：当通过域名 A 下的操作访问域名 B 下的资源时，称为跨域访问，跨域访问时，会出现无法访问的现象&lt;/p&gt;
&lt;p&gt;环境搭建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为当前主机添加备用域名
&lt;ul&gt;
&lt;li&gt;修改 windows 安装目录中的 host 文件&lt;/li&gt;
&lt;li&gt;格式： ip 域名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态刷新 DNS
&lt;ul&gt;
&lt;li&gt;命令： ipconfig /displaydns&lt;/li&gt;
&lt;li&gt;命令： ipconfig /flushdns&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跨域访问支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名称：@CrossOrigin&lt;/li&gt;
&lt;li&gt;类型：方法注解 、 类注解&lt;/li&gt;
&lt;li&gt;位置：处理器类中的方法上方或类上方&lt;/li&gt;
&lt;li&gt;作用：设置当前处理器方法 / 处理器类中所有方法支持跨域访问&lt;/li&gt;
&lt;li&gt;范例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/cross&quot;)
@ResponseBody
//使用@CrossOrigin开启跨域访问
//标注在处理器方法上方表示该方法支持跨域访问
//标注在处理器类上方表示该处理器类中的所有处理器方法均支持跨域访问
@CrossOrigin
public User cross(HttpServletRequest request){
    System.out.println(&quot;controller cross...&quot; + request.getRequestURL());
    User user = new User(&quot;Jockme&quot;,36);
    return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;jsp 文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;javascript:void(0);&quot; id=&quot;testCross&quot;&amp;gt;跨域访问&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot; src=&quot;${pageContext.request.contextPath}/js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    $(function () {
        //为id=&quot;testCross&quot;的组件绑定点击事件
        $(&quot;#testCross&quot;).click(function(){
            //发送异步调用
            $.ajax({
               type:&quot;POST&quot;,
               url:&quot;http://127.0.0.1/cross&quot;,
               //回调函数
               success:function(data){
                   alert(&quot;跨域调用信息反馈:&quot; + data[&apos;name&apos;] + &quot;,&quot; + data[&apos;age&apos;]);
               }
            });
        });
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;拦截器&lt;/h2&gt;
&lt;h3&gt;基本介绍&lt;/h3&gt;
&lt;p&gt;拦截器（Interceptor）是一种动态拦截方法调用的机制&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在指定的方法调用前后执行预先设定后的的代码&lt;/li&gt;
&lt;li&gt;阻止原始方法的执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心原理：AOP 思想&lt;/p&gt;
&lt;p&gt;拦截器链：多个拦截器按照一定的顺序，对原始被调用功能进行增强&lt;/p&gt;
&lt;p&gt;拦截器和过滤器对比：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;归属不同： Filter 属于 Servlet 技术， Interceptor 属于 SpringMVC 技术&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拦截内容不同： Filter 对所有访问进行增强， Interceptor 仅针对 SpringMVC 的访问进行增强&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-过滤器和拦截器的运行机制.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;处理方法&lt;/h3&gt;
&lt;h4&gt;前置处理&lt;/h4&gt;
&lt;p&gt;原始方法之前运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {
    System.out.println(&quot;preHandle&quot;);
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;参数：
&lt;ul&gt;
&lt;li&gt;request：请求对象&lt;/li&gt;
&lt;li&gt;response：响应对象&lt;/li&gt;
&lt;li&gt;handler：被调用的处理器对象，本质上是一个方法对象，对反射中的Method对象进行了再包装
&lt;ul&gt;
&lt;li&gt;handler：public String controller.InterceptorController.handleRun&lt;/li&gt;
&lt;li&gt;handler.getClass()：org.springframework.web.method.HandlerMethod&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;返回值：
&lt;ul&gt;
&lt;li&gt;返回值为 false，被拦截的处理器将不执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;后置处理&lt;/h4&gt;
&lt;p&gt;原始方法运行后运行，如果原始方法被拦截，则不执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void postHandle(HttpServletRequest request,
                       HttpServletResponse response,
                       Object handler,
                       ModelAndView modelAndView) throws Exception {
    System.out.println(&quot;postHandle&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;modelAndView：如果处理器执行完成具有返回结果，可以读取到对应数据与页面信息，并进行调整&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;异常处理&lt;/h4&gt;
&lt;p&gt;拦截器最后执行的方法，无论原始方法是否执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void afterCompletion(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            Exception ex) throws Exception {
    System.out.println(&quot;afterCompletion&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ex：如果处理器执行过程中出现异常对象，可以针对异常情况进行单独处理&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;拦截配置&lt;/h3&gt;
&lt;p&gt;拦截路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/**&lt;/code&gt;：表示拦截所有映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/* &lt;/code&gt;：表示拦截所有/开头的映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/user/*&lt;/code&gt;：表示拦截所有 /user/ 开头的映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/user/add*&lt;/code&gt;：表示拦截所有 /user/ 开头，且具体映射名称以 add 开头的映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/user/*All&lt;/code&gt;：表示拦截所有 /user/ 开头，且具体映射名称以 All 结尾的映射&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mvc:interceptors&amp;gt;
    &amp;lt;!--开启具体的拦截器的使用，可以配置多个--&amp;gt;
    &amp;lt;mvc:interceptor&amp;gt;
        &amp;lt;!--设置拦截器的拦截路径，支持*通配--&amp;gt;       
        &amp;lt;mvc:mapping path=&quot;/handleRun*&quot;/&amp;gt;
        &amp;lt;!--设置拦截排除的路径，配置/**或/*，达到快速配置的目的--&amp;gt;
        &amp;lt;mvc:exclude-mapping path=&quot;/b*&quot;/&amp;gt;
        &amp;lt;!--指定具体的拦截器类--&amp;gt;
        &amp;lt;bean class=&quot;MyInterceptor&quot;/&amp;gt;
    &amp;lt;/mvc:interceptor&amp;gt;
&amp;lt;/mvc:interceptors&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;拦截器链&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;责任链模式&lt;/strong&gt;：责任链模式是一种行为模式&lt;/p&gt;
&lt;p&gt;特点：沿着一条预先设定的任务链顺序执行，每个节点具有独立的工作任务
优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;独立性：只关注当前节点的任务，对其他任务直接放行到下一节点&lt;/li&gt;
&lt;li&gt;隔离性：具备链式传递特征，无需知晓整体链路结构，只需等待请求到达后进行处理即可&lt;/li&gt;
&lt;li&gt;灵活性：可以任意修改链路结构动态新增或删减整体链路责任&lt;/li&gt;
&lt;li&gt;解耦：将动态任务与原始任务解耦&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链路过长时，处理效率低下&lt;/li&gt;
&lt;li&gt;可能存在节点上的循环引用现象，造成死循环，导致系统崩溃&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-多拦截器配置.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;源码解析&lt;/h3&gt;
&lt;p&gt;DispatcherServlet#doDispatch 方法中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	try {
        // 获取映射器以及映射器的所有拦截器（运行原理部分详解了源码）
        mappedHandler = getHandler(processedRequest);
        // 前置处理，返回 false 代表条件成立
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            //请求从这里直接结束
            return;
        }
        //所有拦截器都返回 true，执行目标方法
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
        // 倒序执行所有拦截器的后置处理方法
        mappedHandler.applyPostHandle(processedRequest, response, mv);
    } catch (Exception ex) {
        //异常处理机制
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HandlerExecutionChain#applyPreHandle：前置处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //遍历所有的拦截器
    for (int i = 0; i &amp;lt; this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        //执行前置处理，如果拦截器返回 false，则条件成立，不在执行其他的拦截器，直接返回 false，请求直接结束
        if (!interceptor.preHandle(request, response, this.handler)) {
            triggerAfterCompletion(request, response, null);
            return false;
        }
        this.interceptorIndex = i;
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HandlerExecutionChain#applyPostHandle：后置处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
    throws Exception {
	//倒序遍历
    for (int i = this.interceptorList.size() - 1; i &amp;gt;= 0; i--) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        interceptor.postHandle(request, response, this.handler, mv);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DispatcherServlet#triggerAfterCompletion 底层调用 HandlerExecutionChain#triggerAfterCompletion：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前面的步骤有任何异常都会直接倒序触发 afterCompletion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面成功渲染有异常，也会倒序触发 afterCompletion&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
    //倒序遍历
    for (int i = this.interceptorIndex; i &amp;gt;= 0; i--) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        try {
            //执行异常处理的方法
            interceptor.afterCompletion(request, response, this.handler, ex);
        }
        catch (Throwable ex2) {
            logger.error(&quot;HandlerInterceptor.afterCompletion threw exception&quot;, ex2);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拦截器的执行流程：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-拦截器工作流程.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参考文章：https://www.yuque.com/atguigu/springboot/vgzmgh#wtPLU&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;自定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Contoller层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class InterceptorController {
    @RequestMapping(&quot;/handleRun&quot;)
    public String handleRun() {
        System.out.println(&quot;业务处理器运行------------main&quot;);
        return &quot;page.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义拦截器需要实现 HandleInterceptor 接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//自定义拦截器需要实现HandleInterceptor接口
public class MyInterceptor implements HandlerInterceptor {
    //处理器运行之前执行
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        System.out.println(&quot;前置运行----a1&quot;);
        //返回值为false将拦截原始处理器的运行
        //如果配置多拦截器，返回值为false将终止当前拦截器后面配置的拦截器的运行
        return true;
    }

    //处理器运行之后执行
    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println(&quot;后置运行----b1&quot;);
    }

    //所有拦截器的后置执行全部结束后，执行该操作
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        System.out.println(&quot;完成运行----c1&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：三个方法的运行顺序为    preHandle → postHandle → afterCompletion，如果 preHandle 返回值为 false，三个方法仅运行preHandle&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;web.xml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CharacterEncodingFilter + DispatcherServlet
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置拦截器：spring-mvc.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mvc:annotation-driven/&amp;gt;
&amp;lt;context:component-scan base-package=&quot;interceptor,controller&quot;/&amp;gt;
&amp;lt;mvc:interceptors&amp;gt;
    &amp;lt;mvc:interceptor&amp;gt;
        &amp;lt;mvc:mapping path=&quot;/handleRun&quot;/&amp;gt;
        &amp;lt;bean class=&quot;interceptor.MyInterceptor&quot;/&amp;gt;
    &amp;lt;/mvc:interceptor&amp;gt;
&amp;lt;/mvc:interceptors&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：配置顺序为&lt;strong&gt;先配置执行位置，后配置执行类&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;异常处理&lt;/h2&gt;
&lt;h3&gt;处理器&lt;/h3&gt;
&lt;p&gt;异常处理器： &lt;strong&gt;HandlerExceptionResolver&lt;/strong&gt; 接口&lt;/p&gt;
&lt;p&gt;类继承该接口的以后，当开发出现异常后会执行指定的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        System.out.println(&quot;异常处理器正在执行中&quot;);
        ModelAndView modelAndView = new ModelAndView();
        //定义异常现象出现后，反馈给用户查看的信息
        modelAndView.addObject(&quot;msg&quot;,&quot;出错啦！ &quot;);
        //定义异常现象出现后，反馈给用户查看的页面
        modelAndView.setViewName(&quot;error.jsp&quot;);
        return modelAndView;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据异常的种类不同，进行分门别类的管理，返回不同的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        System.out.println(&quot;my exception is running ....&quot; + ex);
        ModelAndView modelAndView = new ModelAndView();
        if( ex instanceof NullPointerException){
            modelAndView.addObject(&quot;msg&quot;,&quot;空指针异常&quot;);
        }else if ( ex instanceof  ArithmeticException){
            modelAndView.addObject(&quot;msg&quot;,&quot;算数运算异常&quot;);
        }else{
            modelAndView.addObject(&quot;msg&quot;,&quot;未知的异常&quot;);
        }
        modelAndView.setViewName(&quot;error.jsp&quot;);
        return modelAndView;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class UserController {
    @RequestMapping(&quot;/save&quot;)
    @ResponseBody
    public String save(@RequestBody String name) {
        //模拟业务层发起调用产生了异常
//        int i = 1/0;
//        String str = null;
//        str.length();

        return &quot;error.jsp&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;注解开发&lt;/h3&gt;
&lt;p&gt;使用注解实现异常分类管理，开发异常处理器&lt;/p&gt;
&lt;p&gt;@ControllerAdvice 注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类型：类注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置：异常处理器类上方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：设置当前类为异常处理器类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
//声明该类是一个Controller的通知类，声明后该类就会被加载成异常处理器
@ControllerAdvice
public class ExceptionAdvice {
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@ExceptionHandler 注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类型：方法注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置：异常处理器类中针对指定异常进行处理的方法上方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用：设置指定异常的处理方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;说明：处理器方法可以设定多个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@ControllerAdvice
public class ExceptionAdvice {
    //类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器，后面添加实际处理的异常类型
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public String doNullException(Exception ex){
        return &quot;空指针异常&quot;;
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String doException(Exception ex){
        return &quot;all Exception&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@ResponseStatus 注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类型：类注解、方法注解&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置：异常处理器类、方法上方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;p&gt;value：出现错误指定返回状态码&lt;/p&gt;
&lt;p&gt;reason：出现错误返回的错误信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DispatcherServlet + CharacterEncodingFilter
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ajax.jsp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;%@page pageEncoding=&quot;UTF-8&quot; language=&quot;java&quot; contentType=&quot;text/html;UTF-8&quot; %&amp;gt;

&amp;lt;a href=&quot;javascript:void(0);&quot; id=&quot;testException&quot;&amp;gt;点击&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;

&amp;lt;script type=&quot;text/javascript&quot; src=&quot;${pageContext.request.contextPath}/js/jquery-3.3.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    $(function () {
        $(&quot;#testException&quot;).click(function(){
            $.ajax({
                contentType:&quot;application/json&quot;,
                type:&quot;POST&quot;,
                url:&quot;save&quot;,
                /*通过修改参数，激活自定义异常的出现*/
                // name长度低于8位出现业务异常
                // age小于0出现业务异常
                // age大于100出现系统异常
                // age类型如果无法匹配将转入其他类别异常
                data:&apos;{&quot;name&quot;:&quot;JockSuperMan&quot;,&quot;age&quot;:&quot;-1&quot;}&apos;,
                dataType:&quot;text&quot;,
                //回调函数
                success:function(data){
                    alert(data);
                }
            });
        });
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;spring-mvc.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;mvc:annotation-driven/&amp;gt;
&amp;lt;context:component-scan base-package=&quot;com.seazean&quot;/&amp;gt;
&amp;lt;mvc:resources mapping=&quot;/js/**&quot; location=&quot;/js/&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java / controller / UserController&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class UserController {
    @RequestMapping(&quot;/save&quot;)
    @ResponseBody
    public List&amp;lt;User&amp;gt; save(@RequestBody User user) {
        System.out.println(&quot;user controller save is running ...&quot;);
        //对用户的非法操作进行判定，并包装成异常对象进行处理，便于统一管理
        if(user.getName().trim().length() &amp;lt; 8){
            throw new BusinessException(&quot;对不起，用户名长度不满足要求，请重新输入！&quot;);
        }
        if(user.getAge() &amp;lt; 0){
            throw new BusinessException(&quot;对不起，年龄必须是0到100之间的数字！&quot;);
        }
        if(user.getAge() &amp;gt; 100){
            throw new SystemException(&quot;服务器连接失败，请尽快检查处理！&quot;);
        }

        User u1 = new User(&quot;Tom&quot;,3);
        User u2 = new User(&quot;Jerry&quot;,5);
        ArrayList&amp;lt;User&amp;gt; al = new ArrayList&amp;lt;User&amp;gt;();
        al.add(u1);al.add(u2);
        return al;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//自定义异常继承RuntimeException，覆盖父类所有的构造方法
public class BusinessException extends RuntimeException {覆盖父类所有的构造方法}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class SystemException extends RuntimeException {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过自定义异常将所有的异常现象进行分类管理，以统一的格式对外呈现异常消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@ControllerAdvice
public class ProjectExceptionAdvice {
    @ExceptionHandler(BusinessException.class)
    public String doBusinessException(Exception ex, Model m){
        //使用参数Model将要保存的数据传递到页面上，功能等同于ModelAndView
        //业务异常出现的消息要发送给用户查看
        m.addAttribute(&quot;msg&quot;,ex.getMessage());
        return &quot;error.jsp&quot;;
    }

    @ExceptionHandler(SystemException.class)
    public String doSystemException(Exception ex, Model m){
        //系统异常出现的消息不要发送给用户查看，发送统一的信息给用户看
        m.addAttribute(&quot;msg&quot;,&quot;服务器出现问题，请联系管理员！&quot;);
        return &quot;error.jsp&quot;;
    }

    @ExceptionHandler(Exception.class)
    public String doException(Exception ex, Model m){
        m.addAttribute(&quot;msg&quot;,ex.getMessage());
        //将ex对象保存起来
        return &quot;error.jsp&quot;;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;文件传输&lt;/h2&gt;
&lt;h3&gt;上传下载&lt;/h3&gt;
&lt;p&gt;上传文件过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MultipartResolver接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MultipartResolver 接口定义了文件上传过程中的相关操作，并对通用性操作进行了封装&lt;/li&gt;
&lt;li&gt;MultipartResolver 接口底层实现类 CommonsMultipartResovler&lt;/li&gt;
&lt;li&gt;CommonsMultipartResovler 并未自主实现文件上传下载对应的功能，而是调用了 apache 文件上传下载组件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件上传下载实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;导入坐标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;commons-fileupload&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;commons-fileupload&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.4&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面表单 fileupload.jsp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form method=&quot;post&quot; action=&quot;/upload&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;file&quot;&amp;gt;&amp;lt;br&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;提交&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DispatcherServlet + CharacterEncodingFilter
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@PostMapping(&quot;/upload&quot;)
public String upload(@RequestParam(&quot;email&quot;) String email,
                     @RequestParam(&quot;username&quot;) String username,
                     @RequestPart(&quot;headerImg&quot;) MultipartFile headerImg) throws IOException {

    if(!headerImg.isEmpty()){
        //保存到文件服务器，OSS服务器
        String originalFilename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File(&quot;H:\\cache\\&quot; + originalFilename));
    }
    return &quot;main&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;名称问题&lt;/h3&gt;
&lt;p&gt;MultipartFile 参数中封装了上传的文件的相关信息。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;文件命名问题， 获取上传文件名，并解析文件名与扩展名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file.getOriginalFilename();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件名过长问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件保存路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ServletContext context = request.getServletContext();
String realPath = context.getRealPath(&quot;/uploads&quot;);
File file = new File(realPath + &quot;/&quot;);
if(!file.exists()) file.mkdirs();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重名问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String uuid = UUID.randomUUID.toString().replace(&quot;-&quot;, &quot;&quot;).toUpperCase();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class FileUploadController {
    @RequestMapping(value = &quot;/fileupload&quot;)
	//参数中定义MultipartFile参数，用于接收页面提交的type=file类型的表单，表单名称与参数名相同
    public String fileupload(MultipartFile file,MultipartFile file1,MultipartFile file2, HttpServletRequest request) throws IOException {
        System.out.println(&quot;file upload is running ...&quot;+file);
//        MultipartFile参数中封装了上传的文件的相关信息
//        System.out.println(file.getSize());
//        System.out.println(file.getBytes().length);
//        System.out.println(file.getContentType());
//        System.out.println(file.getName());
//        System.out.println(file.getOriginalFilename());
//        System.out.println(file.isEmpty());
        //首先判断是否是空文件，也就是存储空间占用为0的文件
        if(!file.isEmpty()){
            //如果大小在范围要求内正常处理，否则抛出自定义异常告知用户（未实现）
            //获取原始上传的文件名，可以作为当前文件的真实名称保存到数据库中备用
            String fileName = file.getOriginalFilename();
            //设置保存的路径
            String realPath = request.getServletContext().getRealPath(&quot;/images&quot;);
            //保存文件的方法，通常文件名使用随机生成策略产生，避免文件名冲突问题
            file.transferTo(new File(realPath,file.getOriginalFilename()));
        }
        //测试一次性上传多个文件
        if(!file1.isEmpty()){
            String fileName = file1.getOriginalFilename();
            //可以根据需要，对不同种类的文件做不同的存储路径的区分，修改对应的保存位置即可
            String realPath = request.getServletContext().getRealPath(&quot;/images&quot;);
            file1.transferTo(new File(realPath,file1.getOriginalFilename()));
        }
        if(!file2.isEmpty()){
            String fileName = file2.getOriginalFilename();
            String realPath = request.getServletContext().getRealPath(&quot;/images&quot;);
            file2.transferTo(new File(realPath,file2.getOriginalFilename()));
        }
        return &quot;page.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;源码解析&lt;/h3&gt;
&lt;p&gt;StandardServletMultipartResolver 是文件上传解析器&lt;/p&gt;
&lt;p&gt;DispatcherServlet#doDispatch：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 判断当前请求是不是文件上传请求
    processedRequest = checkMultipart(request);
    // 文件上传请求会对 request 进行包装，导致两者不相等，此处赋值为 true，代表已经被解析
    multipartRequestParsed = (processedRequest != request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DispatcherServlet#checkMultipart：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if (this.multipartResolver != null &amp;amp;&amp;amp; this.multipartResolver.isMultipart(request))&lt;/code&gt;：判断是否是文件请求
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StandardServletMultipartResolver#isMultipart&lt;/code&gt;：根据开头是否符合 multipart/form-data 或者 multipart/&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return this.multipartResolver.resolveMultipart(request)&lt;/code&gt;：把请求封装成 StandardMultipartHttpServletRequest 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开始执行 ha.handle() 目标方法进行数据的解析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RequestPartMethodArgumentResolver#supportsParameter：支持解析文件上传数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean supportsParameter(MethodParameter parameter) {
    // 参数上有 @RequestPart 注解
    if (parameter.hasParameterAnnotation(RequestPart.class)) {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RequestPartMethodArgumentResolver#resolveArgument：解析参数数据，封装成 MultipartFile 对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class)&lt;/code&gt;：获取注解的相关信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String name = getPartName(parameter, requestPart)&lt;/code&gt;：获取上传文件的名字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument()&lt;/code&gt;：解析参数
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;MultipartFile&amp;gt; files = multipartRequest.getFiles(name)&lt;/code&gt;：获取文件的所有数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return doInvoke(args)&lt;/code&gt;：解析完成执行自定义的方法，完成上传功能&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;实用技术&lt;/h2&gt;
&lt;h3&gt;校验框架&lt;/h3&gt;
&lt;h4&gt;校验概述&lt;/h4&gt;
&lt;p&gt;表单校验保障了数据有效性、安全性&lt;/p&gt;
&lt;p&gt;校验分类：客户端校验和服务端校验&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式校验
&lt;ul&gt;
&lt;li&gt;客户端：使用 js 技术，利用正则表达式校验&lt;/li&gt;
&lt;li&gt;服务端：使用校验框架&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;逻辑校验
&lt;ul&gt;
&lt;li&gt;客户端：使用ajax发送要校验的数据，在服务端完成逻辑校验，返回校验结果&lt;/li&gt;
&lt;li&gt;服务端：接收到完整的请求后，在执行业务操作前，完成逻辑校验&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表单校验框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSR（Java Specification Requests）：Java 规范提案&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;303：提供bean属性相关校验规则&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JCP（Java Community Process）：Java社区&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hibernate框架中包含一套独立的校验框架hibernate-validator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--导入校验的jsr303规范--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;javax.validation&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;validation-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.0.1.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!--导入校验框架实现技术--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.hibernate&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;hibernate-validator&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;6.1.0.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tomcat7：搭配 hibernate-validator 版本 5.&lt;em&gt;.&lt;/em&gt;.Final&lt;/li&gt;
&lt;li&gt;tomcat8.5：搭配 hibernate-validator 版本 6.&lt;em&gt;.&lt;/em&gt;.Final&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;h5&gt;开启校验&lt;/h5&gt;
&lt;p&gt;名称：@Valid、@Validated&lt;/p&gt;
&lt;p&gt;类型：形参注解&lt;/p&gt;
&lt;p&gt;位置：处理器类中的实体类类型的方法形参前方&lt;/p&gt;
&lt;p&gt;作用：设定对当前实体类类型参数进行校验&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(value = &quot;/addemployee&quot;)
public String addEmployee(@Valid Employee employee) {
    System.out.println(employee);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;校验规则&lt;/h5&gt;
&lt;p&gt;名称：@NotNull&lt;/p&gt;
&lt;p&gt;类型：属性注解等&lt;/p&gt;
&lt;p&gt;位置：实体类属性上方&lt;/p&gt;
&lt;p&gt;作用：设定当前属性校验规则&lt;/p&gt;
&lt;p&gt;范例：每个校验规则所携带的参数不同，根据校验规则进行相应的调整，具体的校验规则查看对应的校验框架进行获取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Employee{
    @NotNull(message = &quot;姓名不能为空&quot;)
    private String name;//员工姓名
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;错误信息&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(value = &quot;/addemployee&quot;)
//Errors对象用于封装校验结果，如果不满足校验规则，对应的校验结果封装到该对象中，包含校验的属性名和校验不通过返回的消息
public String addEmployee(@Valid Employee employee, Errors errors, Model model){
    System.out.println(employee);
    //判定Errors对象中是否存在未通过校验的字段
    if(errors.hasErrors()){
        for(FieldError error : errors.getFieldErrors()){
        	//将校验结果添加到Model对象中，用于页面显示，返回json数据即可
            model.addAttribute(error.getField(),error.getDefaultMessage());
        }
        //当出现未通过校验的字段时，跳转页面到原始页面，进行数据回显
        return &quot;addemployee.jsp&quot;;
    }
    return &quot;success.jsp&quot;;
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过形参Errors获取校验结果数据，通过Model接口将数据封装后传递到页面显示，页面获取后台封装的校验结果信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;/addemployee&quot; method=&quot;post&quot;&amp;gt;
    员工姓名：&amp;lt;input type=&quot;text&quot; name=&quot;name&quot;&amp;gt;&amp;lt;span style=&quot;color:red&quot;&amp;gt;${name}&amp;lt;/span&amp;gt;&amp;lt;br/&amp;gt;
    员工年龄：&amp;lt;input type=&quot;text&quot; name=&quot;age&quot;&amp;gt;&amp;lt;span style=&quot;color:red&quot;&amp;gt;${age}&amp;lt;/span&amp;gt;&amp;lt;br/&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;提交&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;多规则校验&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同一个属性可以添加多个校验器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Employee{
    @NotBlank(message = &quot;姓名不能为空&quot;)
    private String name;//员工姓名

    @NotNull(message = &quot;请输入年龄&quot;)
    @Max(value = 60,message = &quot;年龄最大值60&quot;)
    @Min(value = 18,message = &quot;年龄最小值18&quot;)
    private Integer age;//员工年龄
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三种判定空校验器的区别
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-%E4%B8%89%E7%A7%8D%E5%88%A4%E5%AE%9A%E7%A9%BA%E6%A3%80%E9%AA%8C%E5%99%A8%E7%9A%84%E5%8C%BA%E5%88%AB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;嵌套校验&lt;/h4&gt;
&lt;p&gt;名称：@Valid&lt;/p&gt;
&lt;p&gt;类型：属性注解&lt;/p&gt;
&lt;p&gt;位置：实体类中的引用类型属性上方&lt;/p&gt;
&lt;p&gt;作用：设定当前应用类型属性中的属性开启校验&lt;/p&gt;
&lt;p&gt;范例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Employee {
    //实体类中的引用类型通过标注@Valid注解，设定开启当前引用类型字段中的属性参与校验
    @Valid
    private Address address;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：开启嵌套校验后，被校验对象内部需要添加对应的校验规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//嵌套校验的实体中，对每个属性正常添加校验规则即可
public class Address implements Serializable {
    @NotBlank(message = &quot;请输入省份名称&quot;)
    private String provinceName;//省份名称

    @NotBlank(message = &quot;请输入邮政编码&quot;)
    @Size(max = 6,min = 6,message = &quot;邮政编码由6位组成&quot;)
    private String zipCode;//邮政编码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;分组校验&lt;/h4&gt;
&lt;p&gt;分组校验的介绍&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个模块，根据执行的业务不同，需要校验的属性会有不同
&lt;ul&gt;
&lt;li&gt;新增用户&lt;/li&gt;
&lt;li&gt;修改用户&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对不同种类的属性进行分组，在校验时可以指定参与校验的字段所属的组类别
&lt;ul&gt;
&lt;li&gt;定义组（通用）&lt;/li&gt;
&lt;li&gt;为属性设置所属组，可以设置多个&lt;/li&gt;
&lt;li&gt;开启组校验&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;domain：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//用于设定分组校验中的组名，当前接口仅提供字节码，用于识别
public interface GroupOne {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Employee{
    @NotBlank(message = &quot;姓名不能为空&quot;,groups = {GroupA.class})
    private String name;//员工姓名

    @NotNull(message = &quot;请输入年龄&quot;,groups = {GroupA.class})
    @Max(value = 60,message = &quot;年龄最大值60&quot;)//不加Group的校验不生效
    @Min(value = 18,message = &quot;年龄最小值18&quot;)
    private Integer age;//员工年龄

    @Valid
    private Address address;
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class EmployeeController {
    @RequestMapping(value = &quot;/addemployee&quot;)
    public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model m){
        if(errors.hasErrors()){
            List&amp;lt;FieldError&amp;gt; fieldErrors = errors.getFieldErrors();
            System.out.println(fieldErrors.size());
            for(FieldError error : fieldErrors){
                m.addAttribute(error.getField(),error.getDefaultMessage());
            }
            return &quot;addemployee.jsp&quot;;
        }
        return &quot;success.jsp&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jsp：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;/addemployee&quot; method=&quot;post&quot;&amp;gt;&amp;lt;%--页面使用${}获取后台传递的校验信息--%&amp;gt;
    员工姓名：&amp;lt;input type=&quot;text&quot; name=&quot;name&quot;&amp;gt;&amp;lt;span style=&quot;color:red&quot;&amp;gt;${name}&amp;lt;/span&amp;gt;&amp;lt;br/&amp;gt;
    员工年龄：&amp;lt;input type=&quot;text&quot; name=&quot;age&quot;&amp;gt;&amp;lt;span style=&quot;color:red&quot;&amp;gt;${age}&amp;lt;/span&amp;gt;&amp;lt;br/&amp;gt;
    &amp;lt;%--注意，引用类型的校验未通过信息不是通过对象进行封装的，直接使用对象名.属性名的格式作为整体属性字符串进行保存的，和使用者的属性传递方式有关，不具有通用性，仅适用于本案例--%&amp;gt;
    省：&amp;lt;input type=&quot;text&quot; name=&quot;address.provinceName&quot;&amp;gt;&amp;lt;span style=&quot;color:red&quot;&amp;gt;${requestScope[&apos;address.provinceName&apos;]}&amp;lt;/span&amp;gt;&amp;lt;br/&amp;gt;
        &amp;lt;input type=&quot;submit&quot; value=&quot;提交&quot;&amp;gt;
/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Lombok&lt;/h3&gt;
&lt;p&gt;Lombok 用标签方式代替构造器、getter/setter、toString() 等方法&lt;/p&gt;
&lt;p&gt;引入依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载插件：IDEA 中 File → Settings → Plugins，搜索安装 Lombok 插件&lt;/p&gt;
&lt;p&gt;常用注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NoArgsConstructor		// 无参构造
@AllArgsConstructor		// 全参构造
@Data					// set + get
@ToString				// toString
@EqualsAndHashCode		// hashConde + equals
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简化日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@RestController
public class HelloController {
    @RequestMapping(&quot;/hello&quot;)
    public String handle01(@RequestParam(&quot;name&quot;) String name){
        log.info(&quot;请求进来了....&quot;);
        return &quot;Hello, Spring!&quot; + &quot;你好：&quot; + name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Boot&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;h3&gt;Boot介绍&lt;/h3&gt;
&lt;p&gt;SpringBoot 提供了一种快速使用 Spring 的方式，基于约定优于配置的思想，可以让开发人员不必在配置与逻辑业务之间进行思维的切换，全身心的投入到逻辑业务的代码编写中，从而大大提高了开发的效率&lt;/p&gt;
&lt;p&gt;SpringBoot 功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自动配置，自动配置是一个运行时（更准确地说，是应用程序启动时）的过程，考虑了众多因素选择使用哪个配置，该过程是SpringBoot 自动完成的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;起步依赖，起步依赖本质上是一个 Maven 项目对象模型（Project Object Model，POM），定义了对其他库的传递依赖，这些东西加在一起即支持某项功能。简单的说，起步依赖就是将具备某种功能的坐标打包到一起，并提供一些默认的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;辅助功能，提供了一些大型项目中常见的非功能性特性，如内嵌 web 服务器、安全、指标，健康检测、外部配置等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：https://www.bilibili.com/video/BV19K4y1L7MT&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;构建工程&lt;/h3&gt;
&lt;p&gt;普通构建：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建 Maven 项目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入 SpringBoot 起步依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--springboot 工程需要继承的父工程--&amp;gt;
&amp;lt;parent&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.1.8.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/parent&amp;gt;

&amp;lt;dependencies&amp;gt;
    &amp;lt;!--web 开发的起步依赖--&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义 Controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class HelloController {
    @RequestMapping(&quot;/hello&quot;)
    public String hello(){
        return &quot; hello Spring Boot !&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写引导类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 引导类，SpringBoot项目的入口
@SpringBootApplication
public class HelloApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;快速构建：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA%E6%9E%84%E5%BB%BA%E5%B7%A5%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;自动装配&lt;/h2&gt;
&lt;h3&gt;依赖管理&lt;/h3&gt;
&lt;p&gt;在 spring-boot-starter-parent 中定义了各种技术的版本信息，组合了一套最优搭配的技术版本。在各种 starter 中，定义了完成该功能需要的坐标合集，其中大部分版本信息来自于父工程。工程继承 parent，引入 starter 后，通过依赖传递，就可以简单方便获得需要的 jar 包，并且不会存在版本冲突，自动版本仲裁机制&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;底层注解&lt;/h3&gt;
&lt;h4&gt;SpringBoot&lt;/h4&gt;
&lt;p&gt;@SpringBootApplication：启动注解，实现 SpringBoot 的自动部署&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数 scanBasePackages：可以指定扫描范围&lt;/li&gt;
&lt;li&gt;默认扫描当前引导类所在包及其子包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假如所在包为 com.example.springbootenable，扫描配置包 com.example.config 的信息，三种解决办法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用 @ComponentScan 扫描 com.example.config 包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 @Import 注解加载类，这些类都会被 Spring 创建并放入 ioc 容器，默认组件的名字就是&lt;strong&gt;全类名&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对 @Import 注解进行封装&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;//1.@ComponentScan(&quot;com.example.config&quot;)
//2.@Import(UserConfig.class)
@EnableUser
@SpringBootApplication
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
    	//获取Bean
        Object user = context.getBean(&quot;user&quot;);
        System.out.println(user);

	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserConfig：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class UserConfig {
    @Bean
    public User user() {
        return new User();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EnableUser 注解类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)//@Import注解实现Bean的动态加载
public @interface EnableUser {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Configuration&lt;/h4&gt;
&lt;p&gt;@Configuration：设置当前类为 SpringBoot 的配置类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;proxyBeanMethods = true：Full 全模式，每个 @Bean 方法被调用多少次返回的组件都是单实例的，默认值，类组件之间&lt;strong&gt;有依赖关系&lt;/strong&gt;，方法会被调用得到之前单实例组件&lt;/li&gt;
&lt;li&gt;proxyBeanMethods = false：Lite 轻量级模式，每个 @Bean 方法被调用多少次返回的组件都是新创建的，类组件之间&lt;strong&gt;无依赖关系&lt;/strong&gt;用 Lite 模式加速容器启动过程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Configuration(proxyBeanMethods = true)
public class MyConfig {
    @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值，就是组件在容器中的实例
    public User user(){
        User user = new User(&quot;zhangsan&quot;, 18);
        return user;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Condition&lt;/h4&gt;
&lt;h5&gt;条件注解&lt;/h5&gt;
&lt;p&gt;Condition 是 Spring4.0 后引入的条件化配置接口，通过实现 Condition 接口可以完成有条件的加载相应的 Bean&lt;/p&gt;
&lt;p&gt;注解：@Conditional&lt;/p&gt;
&lt;p&gt;作用：条件装配，满足 Conditional 指定的条件则进行组件注入，加上方法或者类上，作用范围不同&lt;/p&gt;
&lt;p&gt;使用：@Conditional 配合 Condition 的实现类（ClassCondition）进行使用&lt;/p&gt;
&lt;p&gt;ConditionContext 类API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ConfigurableListableBeanFactory  getBeanFactory()&lt;/td&gt;
&lt;td&gt;获取到 IOC 使用的 beanfactory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClassLoader getClassLoader()&lt;/td&gt;
&lt;td&gt;获取类加载器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment getEnvironment()&lt;/td&gt;
&lt;td&gt;获取当前环境信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BeanDefinitionRegistry getRegistry()&lt;/td&gt;
&lt;td&gt;获取到 bean 定义的注册类&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ClassCondition：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClassCondition implements Condition {
    /**
     * context 上下文对象。用于获取环境，IOC容器，ClassLoader对象
     * metadata 注解元对象。 可以用于获取注解定义的属性值
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
      
        //1.需求： 导入Jedis坐标后创建Bean
        //思路：判断redis.clients.jedis.Jedis.class文件是否存在
        boolean flag = true;
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;redis.clients.jedis.Jedis&quot;);
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UserConfig：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class UserConfig {
    @Bean
    @Conditional(ClassCondition.class)
    public User user(){
        return new User();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class SpringbootConditionApplication {
    public static void main(String[] args) {
        //启动SpringBoot应用，返回Spring的IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);

        Object user = context.getBean(&quot;user&quot;);
        System.out.println(user);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;自定义注解&lt;/h5&gt;
&lt;p&gt;将类的判断定义为动态的，判断哪个字节码文件存在可以动态指定&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自定义条件注解类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
    String[] value();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ClassCondition&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClassCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {

        //需求：通过注解属性值value指定坐标后创建bean
        Map&amp;lt;String, Object&amp;gt; map = metadata.getAnnotationAttributes
            					(ConditionOnClass.class.getName());
        //map = {value={属性值}}
        //获取所有的
        String[] value = (String[]) map.get(&quot;value&quot;);

        boolean flag = true;
        try {
            for (String className : value) {
                Class&amp;lt;?&amp;gt; cls = Class.forName(className);
            }
        } catch (Exception e) {
            flag = false;
        }
        return flag;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UserConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class UserConfig {
    @Bean
    @ConditionOnClass(&quot;com.alibaba.fastjson.JSON&quot;)//JSON加载了才注册 User 到容器
    public User user(){
        return new User();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试 User 对象的创建&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;常用注解&lt;/h5&gt;
&lt;p&gt;SpringBoot 提供的常用条件注解：&lt;/p&gt;
&lt;p&gt;@ConditionalOnProperty：判断&lt;strong&gt;配置文件&lt;/strong&gt;中是否有对应属性和值才初始化 Bean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class UserConfig {
    @Bean
    @ConditionalOnProperty(name = &quot;it&quot;, havingValue = &quot;seazean&quot;)
    public User user() {
        return new User();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;it=seazean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@ConditionalOnClass：判断环境中是否有对应类文件才初始化 Bean&lt;/p&gt;
&lt;p&gt;@ConditionalOnMissingClass：判断环境中是否有对应类文件才初始化 Bean&lt;/p&gt;
&lt;p&gt;@ConditionalOnMissingBean：判断环境中没有对应Bean才初始化 Bean&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;ImportRes&lt;/h4&gt;
&lt;p&gt;使用 bean.xml 文件生成配置 bean，如果需要继续复用 bean.xml，@ImportResource 导入配置文件即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ImportResource(&quot;classpath:beans.xml&quot;)
public class MyConfig {
	//...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans ...&amp;gt;
    &amp;lt;bean id=&quot;haha&quot; class=&quot;com.lun.boot.bean.User&quot;&amp;gt;
        &amp;lt;property name=&quot;name&quot; value=&quot;zhangsan&quot;&amp;gt;&amp;lt;/property&amp;gt;
        &amp;lt;property name=&quot;age&quot; value=&quot;18&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;

    &amp;lt;bean id=&quot;hehe&quot; class=&quot;com.lun.boot.bean.Pet&quot;&amp;gt;
        &amp;lt;property name=&quot;name&quot; value=&quot;tomcat&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Properties&lt;/h4&gt;
&lt;p&gt;@ConfigurationProperties：读取到 properties 文件中的内容，并且封装到 JavaBean 中&lt;/p&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mycar.brand=BYD
mycar.price=100000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaBean 类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component	//导入到容器内
@ConfigurationProperties(prefix = &quot;mycar&quot;)//代表配置文件的前缀
public class Car {
    private String brand;
    private Integer price;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;装配原理&lt;/h3&gt;
&lt;h4&gt;启动流程&lt;/h4&gt;
&lt;p&gt;应用启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class BootApplication {
    public static void main(String[] args) {
        // 启动代码
        SpringApplication.run(BootApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringApplication 构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.resourceLoader = resourceLoader&lt;/code&gt;：资源加载器，初始为 null&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.webApplicationType = WebApplicationType.deduceFromClasspath()&lt;/code&gt;：判断当前应用的类型，是响应式还是 Web 类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories()&lt;/code&gt;：&lt;strong&gt;获取引导器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去 &lt;strong&gt;&lt;code&gt;META-INF/spring.factories&lt;/code&gt;&lt;/strong&gt; 文件中找 org.springframework.boot.Bootstrapper&lt;/li&gt;
&lt;li&gt;寻找的顺序：classpath → spring-beans → boot-devtools → springboot → boot-autoconfigure&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class))&lt;/code&gt;：&lt;strong&gt;获取初始化器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去 &lt;code&gt;META-INF/spring.factories&lt;/code&gt; 文件中找 org.springframework.context.ApplicationContextInitializer&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))&lt;/code&gt;：&lt;strong&gt;获取监听器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去 &lt;code&gt;META-INF/spring.factories&lt;/code&gt; 文件中找 org.springframework.context.ApplicationListener&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this.mainApplicationClass = deduceMainApplicationClass()&lt;/code&gt;：获取出 main 程序类&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SpringApplication#run(String... args)：创建 IOC 容器并实现了自动装配&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StopWatch stopWatch = new StopWatch()&lt;/code&gt;：停止监听器，&lt;strong&gt;监控整个应用的启停&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;stopWatch.start()&lt;/code&gt;：记录应用的启动时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bootstrapContext = createBootstrapContext()&lt;/code&gt;：&lt;strong&gt;创建引导上下文环境&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bootstrapContext = new DefaultBootstrapContext()&lt;/code&gt;：创建默认的引导类环境&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.bootstrapRegistryInitializers.forEach()&lt;/code&gt;：遍历所有的引导器调用 initialize 方法完成初始化设置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configureHeadlessProperty()&lt;/code&gt;：让当前应用进入 headless 模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners = getRunListeners(args)&lt;/code&gt;：&lt;strong&gt;获取所有 RunListener（运行监听器）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去 &lt;code&gt;META-INF/spring.factories&lt;/code&gt; 文件中找 org.springframework.boot.SpringApplicationRunListener&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.starting(bootstrapContext, this.mainApplicationClass)&lt;/code&gt;：遍历所有的运行监听器调用 starting 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;applicationArguments = new DefaultApplicationArguments(args)&lt;/code&gt;：获取所有的命令行参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments)&lt;/code&gt;：&lt;strong&gt;准备环境&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;environment = getOrCreateEnvironment()&lt;/code&gt;：返回或创建基础环境信息对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;switch (this.webApplicationType)&lt;/code&gt;：&lt;strong&gt;根据当前应用的类型创建环境&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;case SERVLET&lt;/code&gt;：Web 应用环境对应 ApplicationServletEnvironment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;case REACTIVE&lt;/code&gt;：响应式编程对应 ApplicationReactiveWebEnvironment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt;：默认为 Spring 环境 ApplicationEnvironment&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configureEnvironment(environment, applicationArguments.getSourceArgs())&lt;/code&gt;：读取所有配置源的属性值配置环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConfigurationPropertySources.attach(environment)&lt;/code&gt;：属性值绑定环境信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)&lt;/code&gt;：把 configurationProperties 放入环境的属性信息头部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.environmentPrepared(bootstrapContext, environment)&lt;/code&gt;：运行监听器调用 environmentPrepared()，EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DefaultPropertiesPropertySource.moveToEnd(environment)&lt;/code&gt;：移动 defaultProperties 属性源到环境中的最后一个源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bindToSpringApplication(environment)&lt;/code&gt;：与容器绑定当前环境&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConfigurationPropertySources.attach(environment)&lt;/code&gt;：重新将属性值绑定环境信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sources.remove(ATTACHED_PROPERTY_SOURCE_NAME)&lt;/code&gt;：从环境信息中移除 configurationProperties&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)&lt;/code&gt;：把 configurationProperties 重新放入环境信息&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;configureIgnoreBeanInfo(environment)&lt;/code&gt;：&lt;strong&gt;配置忽略的 bean&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;printedBanner = printBanner(environment)&lt;/code&gt;：打印 SpringBoot 标志&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;context = createApplicationContext()&lt;/code&gt;：&lt;strong&gt;创建 IOC 容器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;switch (this.webApplicationType)&lt;/code&gt;：根据当前应用的类型创建 IOC 容器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;case SERVLET&lt;/code&gt;：Web 应用环境对应 AnnotationConfigServletWebServerApplicationContext&lt;/li&gt;
&lt;li&gt;&lt;code&gt;case REACTIVE&lt;/code&gt;：响应式编程对应 AnnotationConfigReactiveWebServerApplicationContext&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt;：默认为 Spring 环境 AnnotationConfigApplicationContext&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;context.setApplicationStartup(this.applicationStartup)&lt;/code&gt;：设置一个启动器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;prepareContext()&lt;/code&gt;：配置 IOC 容器的基本信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;postProcessApplicationContext(context)&lt;/code&gt;：后置处理流程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;applyInitializers(context)&lt;/code&gt;：获取所有的&lt;strong&gt;初始化器调用 initialize() 方法&lt;/strong&gt;进行初始化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.contextPrepared(context)&lt;/code&gt;：所有的运行监听器调用 environmentPrepared() 方法，EventPublishingRunListener 发布事件通知 IOC 容器准备完成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.contextLoaded(context)&lt;/code&gt;：所有的运行监听器调用 contextLoaded() 方法，通知 IOC 加载完成&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;refreshContext(context)&lt;/code&gt;：&lt;strong&gt;刷新 IOC 容器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring 的容器启动流程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invokeBeanFactoryPostProcessors(beanFactory)&lt;/code&gt;：&lt;strong&gt;实现了自动装配&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onRefresh()&lt;/code&gt;：&lt;strong&gt;创建 WebServer&lt;/strong&gt; 使用该接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;afterRefresh(context, applicationArguments)&lt;/code&gt;：留给用户自定义容器刷新完成后的处理逻辑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;stopWatch.stop()&lt;/code&gt;：记录应用启动完成的时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;callRunners(context, applicationArguments)&lt;/code&gt;：调用所有 runners&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.started(context)&lt;/code&gt;：所有的运行监听器调用 started() 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;listeners.running(context)&lt;/code&gt;：所有的运行监听器调用 running() 方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取容器中的 ApplicationRunner、CommandLineRunner&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AnnotationAwareOrderComparator.sort(runners)&lt;/code&gt;：合并所有 runner 并且按照 @Order 进行排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;callRunner()&lt;/code&gt;：遍历所有的 runner，调用 run 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;handleRunFailure(context, ex, listeners)&lt;/code&gt;：&lt;strong&gt;处理异常&lt;/strong&gt;，出现异常进入该逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;handleExitCode(context, exception)&lt;/code&gt;：处理错误代码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listeners.failed(context, exception)&lt;/code&gt;：运行监听器调用 failed() 方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reportFailure(getExceptionReporters(context), exception)&lt;/code&gt;：通知异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;注解分析&lt;/h4&gt;
&lt;p&gt;SpringBoot 定义了一套接口规范，这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 &lt;code&gt;META-INF/spring.factories&lt;/code&gt; 文件，将文件中配置的类型信息加载到 Spring 容器，并执行类中定义的各种操作，对于外部的 jar 包，直接引入一个 starter 即可&lt;/p&gt;
&lt;p&gt;@SpringBootApplication 注解是 &lt;code&gt;@SpringBootConfiguration&lt;/code&gt;、&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;、&lt;code&gt;@ComponentScan&lt;/code&gt; 注解的集合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@SpringBootApplication 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Inherited
@SpringBootConfiguration	//代表 @SpringBootApplication 拥有了该注解的功能
@EnableAutoConfiguration	//同理
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
// 扫描被 @Component (@Service,@Controller)注解的 bean，容器中将排除TypeExcludeFilter 和 AutoConfigurationExcludeFilter
public @interface SpringBootApplication { }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@SpringBootConfiguration 注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration	// 代表是配置类
@Indexed
public @interface SpringBootConfiguration {
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@AliasFor 注解：表示别名，可以注解到自定义注解的两个属性上表示这两个互为别名，两个属性其实是同一个含义相互替代&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@ComponentScan 注解：默认扫描当前类所在包及其子级包下的所有文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;@EnableAutoConfiguration 注解：启用 SpringBoot 的自动配置机制&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfigurationPackage	
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { 
	String ENABLED_OVERRIDE_PROPERTY = &quot;spring.boot.enableautoconfiguration&quot;;
    Class&amp;lt;?&amp;gt;[] exclude() default {}; 
    String[] excludeName() default {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@AutoConfigurationPackage：&lt;strong&gt;将添加该注解的类所在的 package 作为自动配置 package 进行管理&lt;/strong&gt;，把启动类所在的包设置一次，为了给各种自动配置的第三方库扫描用，比如带 @Mapper 注解的类，Spring 自身是不能识别的，但自动配置的 Mybatis 需要扫描用到，而 ComponentScan 只是用来扫描注解类，并没有提供接口给三方使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Import(AutoConfigurationPackages.Registrar.class)	// 利用 Registrar 给容器中导入组件
public @interface AutoConfigurationPackage { 
	String[] basePackages() default {};	//自动配置包，指定了配置类的包
    Class&amp;lt;?&amp;gt;[] basePackageClasses() default {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))&lt;/code&gt;：注册 BD&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new PackageImports(metadata).getPackageNames()&lt;/code&gt;：获取添加当前注解的类的所在包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))&lt;/code&gt;：存放到容器中
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new BasePackagesBeanDefinition(packageNames)&lt;/code&gt;：把当前主类所在的包名封装到该对象中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Import(AutoConfigurationImportSelector.class)：&lt;strong&gt;自动装配的核心类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;容器刷新时执行：&lt;strong&gt;invokeBeanFactoryPostProcessors()&lt;/strong&gt; → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → &lt;strong&gt;AutoConfigurationImportSelector#getAutoConfigurationEntry()&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // 获取注解属性，@SpringBootApplication 注解的 exclude 属性和 excludeName 属性
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 获取所有需要自动装配的候选项
    List&amp;lt;String&amp;gt; configurations = getCandidateConfigurations(annotationMetadata, attributes);
    // 去除重复的选项
    configurations = removeDuplicates(configurations);
    // 获取注解配置的排除的自动装配类
    Set&amp;lt;String&amp;gt; exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    // 移除所有的配置的不需要自动装配的类
    configurations.removeAll(exclusions);
    // 过滤，条件装配
    configurations = getConfigurationClassFilter().filter(configurations);
    // 获取 AutoConfigurationImportListener 类的监听器调用 onAutoConfigurationImportEvent 方法
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 包装成 AutoConfigurationEntry 返回
    return new AutoConfigurationEntry(configurations, exclusions);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AutoConfigurationImportSelector#getCandidateConfigurations：&lt;strong&gt;获取自动配置的候选项&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;String&amp;gt; configurations = SpringFactoriesLoader.loadFactoryNames()&lt;/code&gt;：加载自动配置类&lt;/p&gt;
&lt;p&gt;参数一：&lt;code&gt;getSpringFactoriesLoaderFactoryClass()&lt;/code&gt;：获取 @EnableAutoConfiguration 注解类&lt;/p&gt;
&lt;p&gt;参数二：&lt;code&gt;getBeanClassLoader()&lt;/code&gt;：获取类加载器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;factoryTypeName = factoryType.getName()&lt;/code&gt;：@EnableAutoConfiguration 注解的全类名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return loadSpringFactories(classLoaderToUse).getOrDefault()&lt;/code&gt;：加载资源
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)&lt;/code&gt;：获取资源类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FACTORIES_RESOURCE_LOCATION = &quot;META-INF/spring.factories&quot;&lt;/code&gt;：&lt;strong&gt;加载的资源的位置&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;return configurations&lt;/code&gt;：返回所有自动装配类的候选项&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段，获取自动装配类，&lt;strong&gt;进行条件装配，按需装配&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;装配流程&lt;/h4&gt;
&lt;p&gt;Spring Boot 通过 &lt;code&gt;@EnableAutoConfiguration&lt;/code&gt; 开启自动装配，通过 SpringFactoriesLoader 加载 &lt;code&gt;META-INF/spring.factories&lt;/code&gt; 中的自动配置类实现自动装配，自动配置类其实就是通过 &lt;code&gt;@Conditional&lt;/code&gt; 注解按需加载的配置类，想要其生效必须引入 &lt;code&gt;spring-boot-starter-xxx&lt;/code&gt; 包实现起步依赖&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration&lt;/li&gt;
&lt;li&gt;每个自动配置类进行&lt;strong&gt;条件装配&lt;/strong&gt;，默认都会绑定配置文件指定的值（xxxProperties 和配置文件进行了绑定）&lt;/li&gt;
&lt;li&gt;SpringBoot 默认会在底层配好所有的组件，如果用户自己配置了&lt;strong&gt;以用户的优先&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定制化配置：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用户可以使用 @Bean 新建自己的组件来替换底层的组件&lt;/li&gt;
&lt;li&gt;用户可以去看这个组件是获取的配置文件前缀值，在配置文件中修改&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 DispatcherServletAutoConfiguration 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// 类中的 Bean 默认不是单例
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
// 条件装配，环境中有 DispatcherServlet 类才进行自动装配
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
	// 注册的 DispatcherServlet 的 BeanName
	public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = &quot;dispatcherServlet&quot;;

	@Configuration(proxyBeanMethods = false)
	@Conditional(DefaultDispatcherServletCondition.class)
	@ConditionalOnClass(ServletRegistration.class)
    // 绑定配置文件的属性，从配置文件中获取配置项
	@EnableConfigurationProperties(WebMvcProperties.class)
	protected static class DispatcherServletConfiguration {
		
        // 给容器注册一个 DispatcherServlet，起名字为 dispatcherServlet
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
            // 新建一个 DispatcherServlet 设置相关属性
			DispatcherServlet dispatcherServlet = new DispatcherServlet();
            // spring.mvc 中的配置项获取注入，没有就填充默认值
			dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
			// ......
            // 返回该对象注册到容器内
			return dispatcherServlet;
		}

		@Bean
        // 容器中有这个类型组件才进行装配
		@ConditionalOnBean(MultipartResolver.class)
        // 容器中没有这个名字 multipartResolver 的组件
		@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
        // 方法名就是 BeanName
		public MultipartResolver multipartResolver(MultipartResolver resolver) {
			// 给 @Bean 标注的方法传入了对象参数，这个参数就会从容器中找，因为用户自定义了该类型，以用户配置的优先
            // 但是名字不符合规范，所以获取到该 Bean 并返回到容器一个规范的名称：multipartResolver
			return resolver;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 将配置文件中的 spring.mvc 前缀的属性与该类绑定
@ConfigurationProperties(prefix = &quot;spring.mvc&quot;)	
public class WebMvcProperties { }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;事件监听&lt;/h3&gt;
&lt;p&gt;SpringBoot 在项目启动时，会对几个监听器进行回调，可以实现监听器接口，在项目启动时完成一些操作&lt;/p&gt;
&lt;p&gt;ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner、ApplicationRunner&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MyApplicationRunner&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自定义监听器的启动时机&lt;/strong&gt;：MyApplicationRunner 和 MyCommandLineRunner 都是当项目启动后执行，使用 @Component 放入容器即可使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//当项目启动后执行run方法
@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(&quot;ApplicationRunner...run&quot;);
        System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息
    }
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyCommandLineRunner&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println(&quot;CommandLineRunner...run&quot;);
        System.out.println(Arrays.asList(args));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyApplicationContextInitializer 的启用要&lt;strong&gt;在 resource 文件夹下添加 META-INF/spring.factories&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.context.ApplicationContextInitializer=\
com.example.springbootlistener.listener.MyApplicationContextInitializer
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println(&quot;ApplicationContextInitializer....initialize&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySpringApplicationRunListener 的使用要添加&lt;strong&gt;构造器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MySpringApplicationRunListener implements SpringApplicationRunListener {
	//构造器
    public MySpringApplicationRunListener(SpringApplication sa, String[] args) {
    }

    @Override
    public void starting() {
        System.out.println(&quot;starting...项目启动中&quot;);//输出SPRING之前
    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        System.out.println(&quot;environmentPrepared...环境对象开始准备&quot;);
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println(&quot;contextPrepared...上下文对象开始准备&quot;);
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println(&quot;contextLoaded...上下文对象开始加载&quot;);
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
        System.out.println(&quot;started...上下文对象加载完成&quot;);
    }

    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println(&quot;running...项目启动完成，开始运行&quot;);
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println(&quot;failed...项目启动失败&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;h3&gt;配置方式&lt;/h3&gt;
&lt;h4&gt;文件类型&lt;/h4&gt;
&lt;p&gt;SpringBoot 是基于约定的，很多配置都有默认值，如果想使用自己的配置替换默认配置，可以使用 application.properties 或者 application.yml（application.yaml）进行配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认配置文件名称：application&lt;/li&gt;
&lt;li&gt;在同一级目录下优先级为：properties &amp;gt; yml &amp;gt; yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如配置内置 Tomcat 的端口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;properties：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.port=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server: port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;yaml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server: port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;加载顺序&lt;/h4&gt;
&lt;p&gt;所有位置的配置文件都会被加载，互补配置，&lt;strong&gt;高优先级配置内容会覆盖低优先级配置内容&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;扫描配置文件的位置按优先级&lt;strong&gt;从高到底&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;file:./config/&lt;/code&gt;：&lt;strong&gt;当前项目&lt;/strong&gt;下的 /config 目录下&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;file:./&lt;/code&gt;：当前项目的根目录，Project工程目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;classpath:/config/&lt;/code&gt;：classpath 的 /config 目录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;classpath:/&lt;/code&gt;：classpath 的根目录，就是 resoureces 目录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;项目外部配置文件加载顺序：外部配置文件的使用是为了对内部文件的配合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;命令行：在 package 打包后的 target 目录下，使用该命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar myproject.jar --server.port=9000
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定配置文件位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar myproject.jar --spring.config.location=e://application.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按优先级从高到底选择配置文件的加载命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar myproject.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;yaml语法&lt;/h3&gt;
&lt;p&gt;基本语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;大小写敏感&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据值前边必须有空格，作为分隔符&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用缩进表示层级关系&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缩进时不允许使用Tab键，只允许使用空格（各个系统 Tab对应空格数目可能不同，导致层次混乱）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缩进的空格数目不重要，只要相同层级的元素左侧对齐即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&apos;&apos;#&quot; 表示注释，从这个字符一直到行尾，都会被解析器忽略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server: 
	port: 8080  
    address: 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;纯量：单个的、不可再分的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;msg1: &apos;hello \n world&apos;  # 单引忽略转义字符
msg2: &quot;hello \n world&quot;  # 双引识别转义字符
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象：键值对集合，Map、Hash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person:  
   name: zhangsan
   age: 20
# 行内写法
person: {name: zhangsan}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：不建议使用 JSON，应该使用 yaml 语法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组：一组按次序排列的值，List、Array&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;address:
  - beijing
  - shanghai
# 行内写法
address: [beijing,shanghai]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;allPerson	#List&amp;lt;Person&amp;gt;
  - {name:lisi, age:18}
  - {name:wangwu, age:20}
# 行内写法
allPerson: [{name:lisi, age:18}, {name:wangwu, age:20}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: lisi 
person:
  name: ${name} # 引用上边定义的name值
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;获取配置&lt;/h3&gt;
&lt;p&gt;三种获取配置文件的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注解 @Value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class HelloController {
    @Value(&quot;${name}&quot;)
    private String name;

    @Value(&quot;${person.name}&quot;)
    private String name2;

    @Value(&quot;${address[0]}&quot;)
    private String address1;

    @Value(&quot;${msg1}&quot;)
    private String msg1;

    @Value(&quot;${msg2}&quot;)
    private String msg2;
    
    @RequestMapping(&quot;/hello&quot;)
    public String hello(){
        System.out.println(&quot;所有的数据&quot;);
        return &quot; hello Spring Boot !&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Evironment 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private Environment env;

@RequestMapping(&quot;/hello&quot;)
public String hello() {
    System.out.println(env.getProperty(&quot;person.name&quot;));
    System.out.println(env.getProperty(&quot;address[0]&quot;));
    return &quot; hello Spring Boot !&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注解 @ConfigurationProperties 配合 @Component 使用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：参数 prefix 一定要指定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component	//不扫描该组件到容器内，无法完成自动装配
@ConfigurationProperties(prefix = &quot;person&quot;)
public class Person {
    private String name;
    private int age;
    private String[] address;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private Person person;

@RequestMapping(&quot;/hello&quot;)
public String hello() {
    System.out.println(person);
    //Person{name=&apos;zhangsan&apos;, age=20, address=[beijing, shanghai]}
    return &quot; hello Spring Boot !&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;配置提示&lt;/h3&gt;
&lt;p&gt;自定义的类和配置文件绑定一般没有提示，添加如下依赖可以使用提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!-- 下面插件作用是工程打包时，不将spring-boot-configuration-processor打进包内，让其只在编码的时候有用 --&amp;gt;
&amp;lt;build&amp;gt;
    &amp;lt;plugins&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;configuration&amp;gt;
                &amp;lt;excludes&amp;gt;
                    &amp;lt;exclude&amp;gt;
                        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
                    &amp;lt;/exclude&amp;gt;
                &amp;lt;/excludes&amp;gt;
            &amp;lt;/configuration&amp;gt;
        &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Profile&lt;/h3&gt;
&lt;p&gt;@Profile：指定组件在哪个环境的情况下才能被注册到容器中，不指定，任何环境下都能注册这个组件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加了环境标识的 bean，只有这个环境被激活的时候才能注册到容器中，默认是 default 环境&lt;/li&gt;
&lt;li&gt;写在配置类上，只有是指定的环境的时候，整个配置类里面的所有配置才能开始生效&lt;/li&gt;
&lt;li&gt;没有标注环境标识的 bean 在，任何环境下都是加载的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Profile 的配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;profile 是用来完成不同环境下，配置动态切换功能&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;profile 配置方式&lt;/strong&gt;：多 profile 文件方式，提供多个配置文件，每个代表一种环境&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;application-dev.properties/yml 开发环境&lt;/li&gt;
&lt;li&gt;application-test.properties/yml 测试环境&lt;/li&gt;
&lt;li&gt;sapplication-pro.properties/yml 生产环境&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;yml 多文档方式：在 yml 中使用  --- 分隔不同配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
server:
  port: 8081
spring:
  profiles:dev
---
server:
  port: 8082
spring:
  profiles:test
---
server:
  port: 8083
spring:
  profiles:pro
---
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;profile 激活方式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置文件：在配置文件中配置：spring.profiles.active=dev&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.profiles.active=dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟机参数：在VM options 指定：&lt;code&gt;-Dspring.profiles.active=dev&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-profile%E6%BF%80%E6%B4%BB%E6%96%B9%E5%BC%8F%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%8F%82%E6%95%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令行参数：&lt;code&gt;java –jar xxx.jar  --spring.profiles.active=dev&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在 Program arguments 里输入，也可以先 package&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Web开发&lt;/h2&gt;
&lt;h3&gt;功能支持&lt;/h3&gt;
&lt;p&gt;SpringBoot 自动配置了很多约定，大多场景都无需自定义配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver&lt;/li&gt;
&lt;li&gt;支持静态资源（包括 webjars）和静态 index.html 页支持&lt;/li&gt;
&lt;li&gt;自动注册相关类：Converter、GenericConverter、Formatter&lt;/li&gt;
&lt;li&gt;内容协商处理器：HttpMessageConverters&lt;/li&gt;
&lt;li&gt;国际化：MessageCodesResolver&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开发规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;@Configuration&lt;/code&gt; + &lt;code&gt;WebMvcConfigurer&lt;/code&gt; 自定义规则，不使用 &lt;code&gt;@EnableWebMvc&lt;/code&gt; 注解&lt;/li&gt;
&lt;li&gt;声明 &lt;code&gt;WebMvcRegistrations&lt;/code&gt; 的实现类改变默认底层组件&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;@EnableWebMvc&lt;/code&gt; + &lt;code&gt;@Configuration&lt;/code&gt; + &lt;code&gt;DelegatingWebMvcConfiguration&lt;/code&gt; 全面接管 SpringMVC&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;静态资源&lt;/h3&gt;
&lt;h4&gt;访问规则&lt;/h4&gt;
&lt;p&gt;默认的静态资源路径是 classpath 下的，优先级由高到低为：/META-INF/resources、/resources、 /static、/public  的包内，&lt;code&gt;/&lt;/code&gt; 表示当前项目的根路径&lt;/p&gt;
&lt;p&gt;静态映射 &lt;code&gt;/**&lt;/code&gt; ，表示请求 &lt;code&gt;/ + 静态资源名&lt;/code&gt; 就直接去默认的资源路径寻找请求的资源&lt;/p&gt;
&lt;p&gt;处理原理：静请求去寻找 Controller 处理，不能处理的请求就会交给静态资源处理器，静态资源也找不到就响应 404 页面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改默认资源路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  web:
    resources:
      static-locations:: [classpath:/haha/]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改静态资源访问前缀，默认是 &lt;code&gt;/**&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  mvc:
    static-path-pattern: /resources/**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 URL：http://localhost:8080/resources/ + 静态资源名，将所有资源&lt;strong&gt;重定位&lt;/strong&gt;到 &lt;code&gt;/resources/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;webjar 访问资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.webjars&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jquery&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.5.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问地址：http://localhost:8080/webjars/jquery/3.5.1/jquery.js，后面地址要按照依赖里面的包路径&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;欢迎页面&lt;/h4&gt;
&lt;p&gt;静态资源路径下 index.html 默认作为欢迎页面，访问 http://localhost:8080 出现该页面，使用 welcome page 功能不能修改前缀&lt;/p&gt;
&lt;p&gt;网页标签上的小图标可以自定义规则，把资源重命名为 favicon.ico 放在静态资源目录下即可&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;源码分析&lt;/h4&gt;
&lt;p&gt;SpringMVC 功能的自动配置类 WebMvcAutoConfiguration：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebMvcAutoConfiguration {
    //当前项目的根路径
    private static final String SERVLET_LOCATION = &quot;/&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内部类 WebMvcAutoConfigurationAdapter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Import(EnableWebMvcConfiguration.class)
// 绑定 spring.mvc、spring.web、spring.resources 相关的配置属性
@EnableConfigurationProperties({ WebMvcProperties.class,ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
	//有参构造器所有参数的值都会从容器中确定
    public WebMvcAutoConfigurationAdapter(/*参数*/) {
			this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
					: webProperties.getResources();
			this.mvcProperties = mvcProperties;
			this.beanFactory = beanFactory;
			this.messageConvertersProvider = messageConvertersProvider;
			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
			this.dispatcherServletPath = dispatcherServletPath;
			this.servletRegistrations = servletRegistrations;
			this.mvcProperties.checkConfiguration();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;ResourceProperties resourceProperties：获取和 spring.resources 绑定的所有的值的对象&lt;/li&gt;
&lt;li&gt;WebMvcProperties mvcProperties：获取和 spring.mvc 绑定的所有的值的对象&lt;/li&gt;
&lt;li&gt;ListableBeanFactory beanFactory：Spring 的 beanFactory&lt;/li&gt;
&lt;li&gt;HttpMessageConverters：找到所有的 HttpMessageConverters&lt;/li&gt;
&lt;li&gt;ResourceHandlerRegistrationCustomizer：找到 资源处理器的自定义器。&lt;/li&gt;
&lt;li&gt;DispatcherServletPath：项目路径&lt;/li&gt;
&lt;li&gt;ServletRegistrationBean：给应用注册 Servlet、Filter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandler()：两种静态资源映射规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //配置文件设置 spring.resources.add-mappings: false，禁用所有静态资源
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug(&quot;Default resource handling disabled&quot;);//被禁用
        return;
    }
    //注册webjars静态资源的映射规则	映射			路径
    addResourceHandler(registry, &quot;/webjars/**&quot;, &quot;classpath:/META-INF/resources/webjars/&quot;);
    //注册静态资源路径的映射规则		 默认映射 staticPathPattern = &quot;/**&quot; 
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -&amp;gt; {
        //staticLocations = CLASSPATH_RESOURCE_LOCATIONS
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
            registration.addResourceLocations(resource);
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@ConfigurationProperties(&quot;spring.web&quot;)
public class WebProperties {
    public static class Resources {
    	//默认资源路径，优先级从高到低
    	static final String[] CLASSPATH_RESOURCE_LOCATIONS = { &quot;classpath:/META-INF/resources/&quot;,
                                                 &quot;classpath:/resources/&quot;, 
                                                 &quot;classpath:/static/&quot;, &quot;classpath:/public/&quot; }
        private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
        //可以进行规则重写
        public void setStaticLocations(String[] staticLocations) {
			this.staticLocations = appendSlashIfNecessary(staticLocations);
			this.customized = true;
		}
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WebMvcAutoConfiguration.EnableWebMvcConfiguration.welcomePageHandlerMapping()：欢迎页&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//spring.web 属性
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration {
    @Bean
    public WelcomePageHandlerMapping welcomePageHandlerMapping(/*参数*/) {
        WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
            new TemplateAvailabilityProviders(applicationContext), 
            applicationContext, getWelcomePage(),
            //staticPathPattern = &quot;/**&quot;
            this.mvcProperties.getStaticPathPattern());
        return welcomePageHandlerMapping;
    }
}
WelcomePageHandlerMapping(/*参数*/) {
    //所以限制 staticPathPattern 必须为 /** 才能启用该功能
    if (welcomePage != null &amp;amp;&amp;amp; &quot;/**&quot;.equals(staticPathPattern)) {
        logger.info(&quot;Adding welcome page: &quot; + welcomePage);
        //重定向
        setRootViewName(&quot;forward:index.html&quot;);
    }
    else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
        logger.info(&quot;Adding welcome page template: index&quot;);
        setRootViewName(&quot;index&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WelcomePageHandlerMapping，访问 / 能访问到 index.html&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Rest映射&lt;/h3&gt;
&lt;p&gt;开启 Rest 功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码分析，注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebMvcAutoConfiguration {
    @Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
	@ConditionalOnProperty(prefix = &quot;spring.mvc.hiddenmethod.filter&quot;, name = &quot;enabled&quot;)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细源码解析：SpringMVC → 基本操作 → Restful → 识别原理&lt;/p&gt;
&lt;p&gt;Web 部分源码详解：SpringMVC → 运行原理&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;内嵌容器&lt;/h3&gt;
&lt;p&gt;SpringBoot 嵌入式 Servlet 容器，默认支持的 WebServe：Tomcat、Jetty、Undertow&lt;/p&gt;
&lt;p&gt;配置方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;exclusions&amp;gt;
        &amp;lt;exclusion&amp;gt; &amp;lt;!--必须要把内嵌的 Tomcat 容器--&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
    &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-jetty&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web 应用启动，SpringBoot 导入 Web 场景包 tomcat，创建一个 Web 版的 IOC 容器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SpringApplication.run(BootApplication.class, args)&lt;/code&gt;：应用启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConfigurableApplicationContext.run()&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;context = createApplicationContext()&lt;/code&gt;：&lt;strong&gt;创建容器&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;applicationContextFactory = ApplicationContextFactory.DEFAULT&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ApplicationContextFactory DEFAULT = (webApplicationType) -&amp;gt; {
    try {
        switch (webApplicationType) {
            case SERVLET:
                // Servlet 容器，继承自 ServletWebServerApplicationContext
                return new AnnotationConfigServletWebServerApplicationContext();
            case REACTIVE:
                // 响应式编程
                return new AnnotationConfigReactiveWebServerApplicationContext();
            default:
                // 普通 Spring 容器
                return new AnnotationConfigApplicationContext();
        }
    } catch (Exception ex) {
        throw new IllegalStateException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;applicationContextFactory.create(this.webApplicationType)&lt;/code&gt;：根据应用类型创建容器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;refreshContext(context)&lt;/code&gt;：容器启动刷新&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内嵌容器工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 容器启动逻辑中，在实例化非懒加载的单例 Bean 之前有一个方法 &lt;strong&gt;onRefresh()&lt;/strong&gt;，留给子类去扩展，Web 容器就是重写这个方法创建 WebServer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void onRefresh() {
    //省略....
	createWebServer();
}
private void createWebServer() {
    ServletWebServerFactory factory = getWebServerFactory();
    this.webServer = factory.getWebServer(getSelfInitializer());
    createWebServer.end();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取 WebServer 工厂 ServletWebServerFactory，并且获取的数量不等于 1 会报错，Spring 底层有三种：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TomcatServletWebServerFactory&lt;/code&gt;、&lt;code&gt;JettyServletWebServerFactory&lt;/code&gt;、&lt;code&gt;UndertowServletWebServerFactory&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;自动配置类 ServletWebServerFactoryAutoConfiguration&lt;/strong&gt; 导入了 ServletWebServerFactoryConfiguration（配置类），根据条件装配判断系统中到底导入了哪个 Web 服务器的包，创建出服务器并启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认是 web-starter 导入 tomcat 包，容器中就有 TomcatServletWebServerFactory，创建出 Tomcat 服务器并启动，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
	// 初始化
   	initialize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化方法 initialize 中有启动方法：&lt;code&gt;this.tomcat.start()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;自定义&lt;/h3&gt;
&lt;h4&gt;定制规则&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            //进行一些方法重写，来实现自定义的规则
            //比如添加一些解析器和拦截器，就是对原始容器功能的增加
        }
    }
    //也可以不加 @Bean，直接从这里重写方法进行功能增加
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;定制容器&lt;/h4&gt;
&lt;p&gt;@EnableWebMvc：全面接管 SpringMVC，所有规则全部自己重新配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@EnableWebMvc + WebMvcConfigurer + @Bean  全面接管SpringMVC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Import(DelegatingWebMvcConfiguration.&lt;strong&gt;class&lt;/strong&gt;)，该类继承 WebMvcConfigurationSupport，自动配置了一些非常底层的组件，只能保证 SpringMVC 最基本的使用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：自动配置类 &lt;strong&gt;WebMvcAutoConfiguration&lt;/strong&gt; 里面的配置要能生效，WebMvcConfigurationSupport 类不能被加载，所以 @EnableWebMvc 导致配置类失效，从而接管了 SpringMVC&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
public class WebMvcAutoConfiguration {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：一般不适用此注解&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;数据访问&lt;/h2&gt;
&lt;h3&gt;JDBC&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;导入 starter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--导入 JDBC 场景--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-jdbc&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!--导入 MySQL 驱动--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;!--版本对应你的 MySQL 版本&amp;lt;version&amp;gt;5.1.49&amp;lt;/version&amp;gt;--&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单独导入 MySQL 驱动是因为不确定用户使用的什么数据库&lt;/p&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://192.168.0.107:3306/db1?useSSL=false	# 不加 useSSL 会警告
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void contextLoads() {
        Long res = jdbcTemplate.queryForObject(&quot;select count(*) from account_tbl&quot;, Long.class);
        log.info(&quot;记录总数：{}&quot;, res);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;自动配置&lt;/h4&gt;
&lt;p&gt;DataSourceAutoConfiguration：数据源的自动配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    
	@Conditional(PooledDataSourceCondition.class) 
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class})
	protected static class PooledDataSourceConfiguration {}
}
// 配置项
@ConfigurationProperties(prefix = &quot;spring.datasource&quot;)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;底层默认配置好的连接池是：&lt;strong&gt;HikariDataSource&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据库连接池的配置，是容器中没有 DataSource 才自动配置的&lt;/li&gt;
&lt;li&gt;修改数据源相关的配置：spring.datasource&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DataSourceTransactionManagerAutoConfiguration： 事务管理器的自动配置&lt;/li&gt;
&lt;li&gt;JdbcTemplateAutoConfiguration： JdbcTemplate 的自动配置
&lt;ul&gt;
&lt;li&gt;可以修改这个配置项 @ConfigurationProperties(prefix = &lt;strong&gt;&quot;spring.jdbc&quot;&lt;/strong&gt;) 来修改JdbcTemplate&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AutoConfigureAfter(DataSourceAutoConfiguration.class)&lt;/code&gt;：在 DataSource 装配后装配&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JndiDataSourceAutoConfiguration： jndi 的自动配置&lt;/li&gt;
&lt;li&gt;XADataSourceAutoConfiguration： 分布式事务相关&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Druid&lt;/h3&gt;
&lt;p&gt;导入坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;druid-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.1.17&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,
    DruidStatViewServletConfiguration.class,
    DruidWebStatFilterConfiguration.class,
    DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;扩展配置项 &lt;strong&gt;spring.datasource.druid&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DruidSpringAopConfiguration： 监控 SpringBean，配置项为 &lt;code&gt;spring.datasource.druid.aop-patterns&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DruidStatViewServletConfiguration：监控页的配置项为 &lt;code&gt;spring.datasource.druid.stat-view-servlet&lt;/code&gt;，默认开启&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DruidWebStatFilterConfiguration：Web 监控配置项为 &lt;code&gt;spring.datasource.druid.web-stat-filter&lt;/code&gt;，默认开启&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DruidFilterConfiguration：所有 Druid 自己 filter 的配置&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

    druid:
      aop-patterns: com.atguigu.admin.*  #监控SpringBean
      filters: stat,wall     # 底层开启功能，stat（sql监控），wall（防火墙）

      stat-view-servlet:   # 配置监控页功能
        enabled: true
        login-username: admin	#项目启动访问：http://localhost:8080/druid ，账号和密码是 admin
        login-password: admin
        resetEnable: false

      web-stat-filter:  # 监控web
        enabled: true
        urlPattern: /*
        exclusions: &apos;*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*&apos;


      filter:
        stat:    # 对上面filters里面的stat的详细配置
          slow-sql-millis: 1000
          logSlowSql: true
          enabled: true
        wall:
          enabled: true
          config:
            drop-table-allow: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置示例：https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter&lt;/p&gt;
&lt;p&gt;配置项列表：https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;MyBatis&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;导入坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.1.4&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;编写 MyBatis 相关配置：application.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置mybatis规则
mybatis:
#  config-location: classpath:mybatis/mybatis-config.xml  建议不写
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    
 #可以不写全局配置文件，所有全局配置文件的配置都放在 configuration 配置项中即可
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定义表和实体类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User {
    private int id;
    private String username;
    private String password;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写 dao 和 mapper 文件/纯注解开发&lt;/p&gt;
&lt;p&gt;dao：&lt;strong&gt;@Mapper 注解必须加，使用自动装配的 package，否则在启动类指定 @MapperScan() 扫描路径（不建议）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Mapper  //必须加Mapper
@Repository
public interface UserXmlMapper {
    public List&amp;lt;User&amp;gt; findAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mapper.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;com.seazean.springbootmybatis.mapper.UserXmlMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;findAll&quot; resultType=&quot;user&quot;&amp;gt;
        select * from t_user
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;纯注解开发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Mapper
@Repository
public interface UserMapper {
    @Select(&quot;select * from t_user&quot;)
    public List&amp;lt;User&amp;gt; findAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;自动配置&lt;/h4&gt;
&lt;p&gt;MybatisAutoConfiguration：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableConfigurationProperties(MybatisProperties.class)	//MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration {
    @Bean
  	@ConditionalOnMissingBean
  	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    	SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        return factory.getObject();
    }
    
    @org.springframework.context.annotation.Configuration
   	@Import(AutoConfiguredMapperScannerRegistrar.class)
   	@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
   	public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {}
}

@ConfigurationProperties(prefix = &quot;mybatis&quot;)
public class MybatisProperties {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置文件：&lt;code&gt;mybatis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自动配置了 SqlSessionFactory&lt;/li&gt;
&lt;li&gt;导入 &lt;code&gt;AutoConfiguredMapperScannerRegistra&lt;/code&gt; 实现 @Mapper 的扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;MyBatis-Plus&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.4.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动配置类：MybatisPlusAutoConfiguration&lt;/p&gt;
&lt;p&gt;只需要 Mapper 继承 &lt;strong&gt;BaseMapper&lt;/strong&gt; 就可以拥有 CRUD 功能&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Redis&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置redis相关属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  redis:
    host: 127.0.0.1 # redis的主机ip
    port: 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注入 RedisTemplate 模板&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootRedisApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testSet() {
        //存入数据
        redisTemplate.boundValueOps(&quot;name&quot;).set(&quot;zhangsan&quot;);
    }
    @Test
    public void testGet() {
        //获取数据
        Object name = redisTemplate.boundValueOps(&quot;name&quot;).get();
        System.out.println(name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;自动配置&lt;/h4&gt;
&lt;p&gt;RedisAutoConfiguration 自动配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = &quot;redisTemplate&quot;)
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate&amp;lt;Object, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate&amp;lt;Object, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置项：&lt;code&gt;spring.redis&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自动导入了连接工厂配置类：LettuceConnectionConfiguration、JedisConnectionConfiguration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自动注入了模板类：RedisTemplate&amp;lt;Object, Object&amp;gt; 、StringRedisTemplate，k v 都是 String 类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 @Autowired 注入模板类就可以操作 redis&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;单元测试&lt;/h2&gt;
&lt;h3&gt;Junit5&lt;/h3&gt;
&lt;p&gt;Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库，由三个不同的子模块组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JUnit Platform：在 JVM 上启动测试框架的基础，不仅支持 Junit 自制的测试引擎，其他测试引擎也可以接入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JUnit Jupiter：提供了 JUnit5 的新的编程模型，是 JUnit5 新特性的核心，内部包含了一个测试引擎，用于在 Junit Platform 上运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JUnit Vintage：JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎&lt;/p&gt;
&lt;p&gt;注意：SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖，如果需要兼容 Junit4 需要自行引入&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class Boot05WebAdminApplicationTests {
    @Test
    void contextLoads() { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;常用注解&lt;/h3&gt;
&lt;p&gt;JUnit5 的注解如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@Test：表示方法是测试方法，但是与 JUnit4 的 @Test 不同，它的职责非常单一不能声明任何属性，拓展的测试将会由 Jupiter 提供额外测试，包是 &lt;code&gt;org.junit.jupiter.api.Test&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@ParameterizedTest：表示方法是参数化测试&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@RepeatedTest：表示方法可重复执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@DisplayName：为测试类或者测试方法设置展示名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@BeforeEach：表示在每个单元测试之前执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@AfterEach：表示在每个单元测试之后执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@BeforeAll：表示在所有单元测试之前执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@AfterAll：表示在所有单元测试之后执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Tag：表示单元测试类别，类似于 JUnit4 中的 @Categories&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Disabled：表示测试类或测试方法不执行，类似于 JUnit4 中的 @Ignore&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Timeout：表示测试方法运行如果超过了指定时间将会返回错误&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@ExtendWith：为测试类或测试方法提供扩展类引用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;断言机制&lt;/h3&gt;
&lt;h4&gt;简单断言&lt;/h4&gt;
&lt;p&gt;断言（assertions）是测试方法中的核心，用来对测试需要满足的条件进行验证，断言方法都是 org.junit.jupiter.api.Assertions 的静态方法&lt;/p&gt;
&lt;p&gt;用来对单个值进行简单的验证：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assertEquals&lt;/td&gt;
&lt;td&gt;判断两个对象或两个原始类型是否相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNotEquals&lt;/td&gt;
&lt;td&gt;判断两个对象或两个原始类型是否不相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertSame&lt;/td&gt;
&lt;td&gt;判断两个对象引用是否指向同一个对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNotSame&lt;/td&gt;
&lt;td&gt;判断两个对象引用是否指向不同的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertTrue&lt;/td&gt;
&lt;td&gt;判断给定的布尔值是否为 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertFalse&lt;/td&gt;
&lt;td&gt;判断给定的布尔值是否为 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNull&lt;/td&gt;
&lt;td&gt;判断给定的对象引用是否为 null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNotNull&lt;/td&gt;
&lt;td&gt;判断给定的对象引用是否不为 null&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;simple assertion&quot;)
public void simple() {
     assertEquals(3, 1 + 2, &quot;simple math&quot;);
     assertNull(null);
     assertNotNull(new Object());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;数组断言&lt;/h4&gt;
&lt;p&gt;通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;array assertion&quot;)
public void array() {
 	assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;组合断言&lt;/h4&gt;
&lt;p&gt;assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言，可以通过 lambda 表达式提供这些断言&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;assert all&quot;)
public void all() {
	assertAll(&quot;Math&quot;,
              () -&amp;gt; assertEquals(2, 1 + 1),
              () -&amp;gt; assertTrue(1 &amp;gt; 0)
   	);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;异常断言&lt;/h4&gt;
&lt;p&gt;Assertions.assertThrows()，配合函数式编程就可以进行使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;异常测试&quot;)
public void exceptionTest() {
    ArithmeticException exception = Assertions.assertThrows(
        //扔出断言异常
		ArithmeticException.class, () -&amp;gt; System.out.println(1 / 0)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;超时断言&lt;/h4&gt;
&lt;p&gt;Assertions.assertTimeout() 为测试方法设置了超时时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;超时测试&quot;)
public void timeoutTest() {
    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -&amp;gt; Thread.sleep(500));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;快速失败&lt;/h4&gt;
&lt;p&gt;通过 fail 方法直接使得测试失败&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
@DisplayName(&quot;fail&quot;)
public void shouldFail() {
	fail(&quot;This should fail&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;p&gt;JUnit 5 中的前置条件（assumptions）类似于断言，不同之处在于&lt;strong&gt;不满足的断言会使得测试方法失败&lt;/strong&gt;，而不满足的&lt;strong&gt;前置条件只会使得测试方法的执行终止&lt;/strong&gt;，前置条件可以看成是测试方法执行的前提，当该前提不满足时，就没有继续执行的必要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@DisplayName(&quot;测试前置条件&quot;)
@Test
void testassumptions(){
    Assumptions.assumeTrue(false,&quot;结果不是true&quot;);
    System.out.println(&quot;111111&quot;);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;嵌套测试&lt;/h3&gt;
&lt;p&gt;JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试，从而可以更好的把相关的测试方法组织在一起，在内部类中可以使用 @BeforeEach 和 @AfterEach 注解，而且嵌套的层次没有限制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@DisplayName(&quot;A stack&quot;)
class TestingAStackDemo {

    Stack&amp;lt;Object&amp;gt; stack;

    @Test
    @DisplayName(&quot;is instantiated with new Stack()&quot;)
    void isInstantiatedWithNew() {
        assertNull(stack)
    }

    @Nested
    @DisplayName(&quot;when new&quot;)
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack&amp;lt;&amp;gt;();
        }

        @Test
        @DisplayName(&quot;is empty&quot;)
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName(&quot;throws EmptyStackException when popped&quot;)
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;参数测试&lt;/h3&gt;
&lt;p&gt;参数化测试是 JUnit5 很重要的一个新特性，它使得用不同的参数多次运行测试成为了可能&lt;/p&gt;
&lt;p&gt;利用**@ValueSource**等注解，指定入参，我们将可以使用不同的参数进行多次单元测试，而不需要每新增一个参数就新增一个单元测试，省去了很多冗余代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@ValueSource：为参数化测试指定入参来源，支持八大基础类以及 String 类型、Class 类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@NullSource：表示为参数化测试提供一个 null 的入参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@EnumSource：表示为参数化测试提供一个枚举入参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@CsvFileSource：表示读取指定 CSV 文件内容作为参数化测试入参&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@MethodSource：表示读取指定方法的返回值作为参数化测试入参（注意方法返回需要是一个流）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;指标监控&lt;/h2&gt;
&lt;h3&gt;Actuator&lt;/h3&gt;
&lt;p&gt;每一个微服务在云上部署以后，都需要对其进行监控、追踪、审计、控制等，SpringBoot 抽取了 Actuator 场景，使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;暴露所有监控信息为 HTTP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;management:
  endpoints:
    enabled-by-default: true #暴露所有端点信息
    web:
      exposure:
        include: &apos;*&apos;  #以web方式暴露
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 http://localhost:8080/actuator/[beans/health/metrics/]&lt;/p&gt;
&lt;p&gt;可视化界面：https://github.com/codecentric/spring-boot-admin&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Endpoint&lt;/h3&gt;
&lt;p&gt;默认所有的 Endpoint 除过 shutdown 都是开启的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;management:
  endpoints:
    enabled-by-default: false	#禁用所有的
  endpoint:						#手动开启一部分
    beans:
      enabled: true
    health:
      enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;端点：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auditevents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;暴露当前应用程序的审核事件信息。需要一个 &lt;code&gt;AuditEventRepository&lt;/code&gt; 组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示应用程序中所有 Spring Bean 的完整列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;caches&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;暴露可用的缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conditions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示自动配置的所有条件信息，包括匹配或不匹配的原因&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;configprops&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示所有 &lt;code&gt;@ConfigurationProperties&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;暴露 Spring 的属性 &lt;code&gt;ConfigurableEnvironment&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flyway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示已应用的所有 Flyway 数据库迁移。 需要一个或多个 Flyway 组件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示应用程序运行状况信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;httptrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示 HTTP 跟踪信息，默认情况下 100 个 HTTP 请求-响应需要一个 &lt;code&gt;HttpTraceRepository&lt;/code&gt; 组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示应用程序信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;integrationgraph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示 Spring integrationgraph，需要依赖 &lt;code&gt;spring-integration-core&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loggers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示和修改应用程序中日志的配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;liquibase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示已应用的所有 Liquibase 数据库迁移，需要一个或多个 Liquibase 组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示当前应用程序的指标信息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mappings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示所有 &lt;code&gt;@RequestMapping&lt;/code&gt; 路径列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduledtasks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示应用程序中的计划任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许从 Spring Session 支持的会话存储中检索和删除用户会话，需要使用 Spring Session 的基于 Servlet 的 Web 应用程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使应用程序正常关闭，默认禁用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示由 &lt;code&gt;ApplicationStartup&lt;/code&gt; 收集的启动步骤数据。需要使用 &lt;code&gt;SpringApplication&lt;/code&gt; 进行配置 &lt;code&gt;BufferingApplicationStartup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;threaddump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;执行线程转储&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;应用程序是 Web 应用程序（Spring MVC，Spring WebFlux 或 Jersey），则可以使用以下附加端点：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回 &lt;code&gt;hprof&lt;/code&gt; 堆转储文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jolokia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过 HTTP 暴露 JMX bean（需要引入 Jolokia，不适用于 WebFlux），需要引入依赖 &lt;code&gt;jolokia-core&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回日志文件的内容（如果已设置 &lt;code&gt;logging.file.name&lt;/code&gt; 或 &lt;code&gt;logging.file.path&lt;/code&gt; 属性），支持使用 HTTP Range标头来检索部分日志文件的内容。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以 Prometheus 服务器可以抓取的格式公开指标，需要依赖 &lt;code&gt;micrometer-registry-prometheus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常用 Endpoint：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Health：监控状况&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Metrics：运行时指标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Loggers：日志记录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;项目部署&lt;/h2&gt;
&lt;p&gt;SpringBoot 项目开发完毕后，支持两种方式部署到服务器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jar 包 (官方推荐，默认)&lt;/li&gt;
&lt;li&gt;war 包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;更改 pom 文件中的打包方式为 war&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改启动类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class SpringbootDeployApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootDeployApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder b) {
        return b.sources(SpringbootDeployApplication.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定打包的名称&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;packaging&amp;gt;war&amp;lt;/packaging&amp;gt;
&amp;lt;build&amp;gt;
     &amp;lt;finalName&amp;gt;springboot&amp;lt;/finalName&amp;gt;
     &amp;lt;plugins&amp;gt;
         &amp;lt;plugin&amp;gt;
             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
             &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
         &amp;lt;/plugin&amp;gt;
     &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Cloud&lt;/h1&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;p&gt;SpringCloud 是分布式微服务的一站式解决方案，是多种微服务落地技术的集合体，俗称微服务全家桶&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-%E7%BB%84%E4%BB%B6%E6%A6%82%E8%A7%88.png&quot; alt=&quot;Cloud-组件概览&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考文档：https://www.yuque.com/mrlinxi/pxvr4g/wcwd39&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;服务注册&lt;/h2&gt;
&lt;h3&gt;Eureka&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。Eureka 采用了 CS(Client-Server) 的设计架构，Eureka Server 是服务注册中心，系统中的其他微服务使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka%E5%92%8CDubbo%E5%AF%B9%E6%AF%94.png&quot; alt=&quot;Cloud-Eureka和Dubbo对比&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Eureka Server 提供服务注册服务：各个微服务节点通过配置启动后，会在 EurekaServer 中进行注册，EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息，并且具有可视化界面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eureka Client 通过注册中心进行访问：用于简化 Eureka Server的交互，客户端也具备一个内置的、使用轮询 (round-robin) 负载算法的负载均衡器。在应用启动后将会向 Eureka Server 发送心跳（默认周期为30秒），如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳，将会从服务注册表中把这个服务节点移除（默认 90 秒）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;服务端&lt;/h4&gt;
&lt;p&gt;服务器端主启动类增加 @EnableEurekaServer 注解，指定该模块作为 Eureka 注册中心的服务器&lt;/p&gt;
&lt;p&gt;构建流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主启动类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaServer  // 表示当前是Eureka的服务注册中心
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 pom 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.x:    server跟client合在一起
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-eureka&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
2.x： server跟client分开
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-server&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改  application.yml 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 7001

eureka:
  instance:
    hostname: localhost # eureka服务端的实例名称
  client:
    # false表示不向注册中心注册自己。
    register-with-eureka: false
    # false表示自己端就是注册中心，职责就是维护服务实例，并不需要去检索服务
    fetch-registry: false
  service-url:
    # 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址。
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;游览器访问 http://localhost:7001&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;客户端&lt;/h4&gt;
&lt;h5&gt;生产者&lt;/h5&gt;
&lt;p&gt;服务器端主启动类需要增加 @EnableEurekaClient 注解，表示这是一个 Eureka 客户端，要注册进 EurekaServer 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主启动类：PaymentMain8001&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 pom 文件：添加一个 Eureka-Client 依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写 yml 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8001
 
eureka:
  client:
    # 表示将自己注册进EurekaServer默认为true
    register-with-eureka: true
    # 表示可以从Eureka抓取已有的注册信息，默认为true。单节点无所谓，集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: 
      defaultZone: http://localhost:7001/eureka
  instance:
    instance-id: payment8001 # 只暴露服务名，不带有主机名
    prefer-ip-address: true  # 访问信息有 IP 信息提示(鼠标停留在服务名称上时)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;游览器访问 http://localhost:7001&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;消费者&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主启动类：PaymentMain8001&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pom 文件同生产者&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写 yml 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 80

# 微服务名称
spring:
 application:
  name: cloud-order-service
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url: 
      defaultZone: http://localhost:7001/eureka
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器访问 http://localhost:7001&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka%E5%8F%AF%E8%A7%86%E5%8C%96%E7%95%8C%E9%9D%A2.png&quot; alt=&quot;Cloud-Eureka可视化界面&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;集群构建&lt;/h4&gt;
&lt;h5&gt;服务端&lt;/h5&gt;
&lt;p&gt;Server 端高可用集群原理：实现负载均衡和故障容错，互相注册，相互守望&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka%E9%9B%86%E7%BE%A4%E5%8E%9F%E7%90%86.png&quot; alt=&quot;Cloud-Eureka集群原理&quot; /&gt;&lt;/p&gt;
&lt;p&gt;多台 Eureka 服务器，每一台 Eureka 服务器需要有自己的主机名，同时各服务器需要相互注册&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Eureka1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      # 单机就是自己
      # defaultZone: http://eureka7001.com:7001/eureka/
      # 集群指向其他eureka
      #defaultZone: http://eureka7002.com:7002/eureka/
      # 写成这样可以直接通过可视化页面跳转到7002
      defaultZone: http://eureka7002.com:7002/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eureka2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      #写成这样可以直接通过可视化页面跳转到7001
      defaultZone: http://eureka7001.com:7001/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7002 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7002.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问 http://eureka7001.com:7001 和 http://eureka7002.com:7002：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-EurekaServer%E9%9B%86%E7%BE%A4%E6%9E%84%E5%BB%BA%E6%88%90%E5%8A%9F.png&quot; alt=&quot;Cloud-EurekaServer集群构建成功&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RPC 调用：controller.OrderController&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class OrderController {
    public static final String PAYMENT_URL = &quot;http://localhost:8001&quot;;

    @Autowired
    private RestTemplate restTemplate;
	
    // CommonResult 是一个公共的返回类型
    @GetMapping(&quot;/consumer/payment/get/{id}&quot;)
    public CommonResult&amp;lt;Payment&amp;gt; getPayment(@PathVariable(&quot;id&quot;) long id) {
        // 返回对象为响应体中数据转化成的对象，基本上可以理解为JSON
        return restTemplate.getForObject(PAYMENT_URL + &quot;/payment/get/&quot; + id, CommonResult.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;生产者&lt;/h5&gt;
&lt;p&gt;构建 PaymentMain8001 的服务集群&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主启动类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8002 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8002.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写 yml 文件：端口修改，并且 spring.application.name 均为 cloud-payment-service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8002

spring:
  application:
    name: cloud-payment-service
    
eureka:
  client:
    # 表示将自己注册进EurekaServer默认为true
    register-with-eureka: true
    # 表示可以从Eureka抓取已有的注册信息，默认为true。单节点无所谓，集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: 
      defaultZone: http://localhost:7001/eureka
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;负载均衡&lt;/h5&gt;
&lt;p&gt;消费者端的 Controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// public static final String PAYMENT_URL = &quot;http://localhost:8001&quot;;
public static final String PAYMENT_URL = &quot;http://localhost:8002&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于已经建立了生产者集群，所以可以进行负载均衡的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Controller：只修改 PAYMENT_URL 会报错，因为 CLOUD-PAYMENT-SERVICE 对应多个微服务，需要规则来判断调用哪个端口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static final String PAYMENT_URL = &quot;http://CLOUD-PAYMENT-SERVICE&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 @LoadBlanced 注解赋予 RestTemplate 负载均衡的能力，增加 config.ApplicationContextConfig 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;服务发现&lt;/h4&gt;
&lt;p&gt;服务发现：对于注册进 Eureka 里面的微服务，可以通过服务发现来获得该服务的信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主启动类增加注解 @EnableDiscoveryClient：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改生产者的 Controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class PaymentController {
    @Autowired
    private DiscoveryClient discoveryClient;
    
    @GetMapping(value = &quot;/payment/discovery&quot;)
    public Object discovery() {
        List&amp;lt;String&amp;gt; services = discoveryClient.getServices();
        for (String service : services) {
            log.info(&quot;**** element:&quot; + service);
        }

        List&amp;lt;ServiceInstance&amp;gt; instances = discoveryClient.getInstances(&quot;PAYMENT-SERVICE&quot;);
        for (ServiceInstance instance : instances) {
            log.info(instance.getServiceId() + &quot;\t&quot; + instance.getHost() + &quot;\t&quot; + instance.getPort());
        }
        return this.discoveryClient;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;自我保护&lt;/h4&gt;
&lt;p&gt;保护模式用于客户端和 EurekaServer 之间存在网络分区场景下的保护，一旦进入保护模式 EurekaServer 将会尝试保护其服务注册表中的信息，不在删除服务注册表中的数据，属于 CAP 里面的 AP 思想（可用性和分区容错性）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka%E8%87%AA%E6%88%91%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;Cloud-Eureka自我保护机制&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果一定时间内丢失大量该微服务的实例，这时 Eureka 就会开启自我保护机制，不会剔除该服务。 因为这个现象可能是因为网络暂时不通，出现了 Eureka 的假死、拥堵、卡顿，客户端恢复后还能正常发送心跳&lt;/p&gt;
&lt;p&gt;禁止自我保护：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Server：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  server:
    # 关闭自我保护机制，不可用的服务直接删除
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 2000
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Client：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  instance:
    # Eureka客户端向服务端发送心跳的时间间隔默认30秒 
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳后，90s没有收到心跳，剔除服务
    lease-expiration-duration-in-seconds: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Consul&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Consul 是开源的分布式服务发现和配置管理系统，采用 Go 语言开发，官网：https://developer.hashicorp.com/consul&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供了微服务系统中心的服务治理，配置中心，控制总线等功能&lt;/li&gt;
&lt;li&gt;基于 Raft 协议，支持健康检查，同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群&lt;/li&gt;
&lt;li&gt;提供图形界面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下载 Consul 后，运行指令：&lt;code&gt;consul -version&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\Program Files\Java&amp;gt;consul -version
Consul v1.15.1
Revision 7c04b6a0
Build Date 2023-03-07T20:35:33Z
Protocol 2 spoken by default, understands 2 to 3 (.....)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;consul agent -dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问浏览器：http://localhost:8500/&lt;/p&gt;
&lt;p&gt;中文文档：https://www.springcloud.cc/spring-cloud-consul.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;无需 Server 端代码的编写&lt;/p&gt;
&lt;p&gt;生产者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-consul-discovery&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;###consul 服务端口号
server:
  port: 8006

spring:
  application:
    name: consul-provider-payment
  ####consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8006.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;###consul服务端口号
server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  ####consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：同生产者&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类 Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class OrderConsulController {
    public static final String INVOKE_URL = &quot;http://cloud-provider-pament&quot;;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(&quot;/consumer/payment/consul&quot;)
    public String paymentInfo() {
        return restTemplate.getForObject(INVOKE_URL, String.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;服务调用&lt;/h2&gt;
&lt;h3&gt;Ribbon&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套负载均衡工具，提供客户端的软件负载均衡算法和服务调用，Ribbon 客户端组件提供一系列完善的配置项如连接超时，重试等&lt;/p&gt;
&lt;p&gt;官网： https://github.com/Netflix/ribbon/wiki/Getting-Started （已进入维护模式，未来替换为 Load Banlancer）&lt;/p&gt;
&lt;p&gt;负载均衡 Load Balance (LB) 就是将用户的请求平摊的分配到多个服务上，从而达到系统的 HA（高可用）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常见的负载均衡算法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;轮询：为请求选择健康池中的第一个后端服务器，然后按顺序往后依次选择&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最小连接：优先选择连接数最少，即压力最小的后端服务器，在会话较长的情况下可以采取这种方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;散列：根据请求源的 IP 的散列（hash）来选择要转发的服务器，可以一定程度上保证特定用户能连接到相同的服务器，如果应用需要处理状态而要求用户能连接到和之前相同的服务器，可以采取这种方式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ribbon 本地负载均衡客户端与 Nginx 服务端负载均衡区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nginx 是服务器负载均衡，客户端所有请求都会交给 Nginx，然后由 Nginx 实现转发请求，即负载均衡是由服务端实现的&lt;/li&gt;
&lt;li&gt;Ribbon 本地负载均衡，在调用微服务接口时会在注册中心上获取注册信息服务列表，然后缓存到 JVM 本地，从而在本地实现 RPC 远程服务调用技术&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集中式 LB 和进程内 LB 的对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集中式 LB：在服务的消费方和提供方之间使用独立的 LB 设施（如 Nginx），由该设施把访问请求通过某种策略转发至服务的提供方&lt;/li&gt;
&lt;li&gt;进程内 LB：将 LB 逻辑集成到消费方，消费方从服务注册中心获知有哪些服务可用，然后从中选择出一个服务器，Ribbon 属于该类&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;工作流程&lt;/h4&gt;
&lt;p&gt;Ribbon 是一个软负载均衡的客户端组件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Ribbon%E6%9E%B6%E6%9E%84%E5%8E%9F%E7%90%86.png&quot; alt=&quot;Cloud-Ribbon架构原理&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步先选择 EurekaServer，优先选择在同一个区域内负载较少的 Server&lt;/li&gt;
&lt;li&gt;第二步根据用户指定的策略，再从 Server 取到的服务注册列表中选择一个地址&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;核心组件&lt;/h4&gt;
&lt;p&gt;Ribbon 核心组件 IRule 接口，主要实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RoundRobinRule：轮询&lt;/li&gt;
&lt;li&gt;RandomRule：随机&lt;/li&gt;
&lt;li&gt;RetryRule：先按照 RoundRobinRule 的策略获取服务，如果获取服务失败则在指定时间内会进行重试&lt;/li&gt;
&lt;li&gt;WeightedResponseTimeRule：对 RoundRobinRule 的扩展，响应速度越快的实例选择权重越大，越容易被选择&lt;/li&gt;
&lt;li&gt;BestAvailableRule：会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务，然后选择一个并发量最小的服务&lt;/li&gt;
&lt;li&gt;AvailabilityFilteringRule：先过滤掉故障实例，再选择并发较小的实例&lt;/li&gt;
&lt;li&gt;ZoneAvoidanceRule：默认规则，复合判断 Server 所在区域的性能和 Server 的可用性选择服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-IRule%E7%B1%BB%E5%9B%BE.png&quot; alt=&quot;Cloud-IRule类图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：官方文档明确给出了警告，自定义负载均衡配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下&lt;/p&gt;
&lt;p&gt;更换负载均衡算法方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自定义负载均衡配置类 MySelfRule：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class MySelfRule {
    @Bean
    public IRule myRule() {
        return new RandomRule();//定义为随机负载均衡算法
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类添加 @RibbonCilent 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
// 指明访问的服务CLOUD-PAYMENT-SERVICE，以及指定负载均衡策略
@RibbonClient(name = &quot;CLOUD-PAYMENT-SERVICE&quot;, configuration= MySelfRule.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;OpenFeign&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Feign 是一个声明式 WebService 客户端，能让编写 Web 客户端更加简单，只要创建一个接口并添加注解 @Feign 即可，可以与 Eureka 和 Ribbon 组合使用支持负载均衡，所以一般&lt;strong&gt;用在消费者端&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenFeign 在 Feign 的基础上支持了 SpringMVC 注解，并且 @FeignClient 注解可以解析 @RequestMapping 注解下的接口，并通过动态代理的方式产生实现类，在实现类中做负载均衡和服务调用&lt;/p&gt;
&lt;p&gt;优点：利用 RestTemplate 对 HTTP 请求的封装处理，形成了一套模版化的调用方法。但是对服务依赖的调用可能不止一处，往往一个接口会被多处调用，所以一个微服务接口上面标注一个 @Feign 注解，就可以完成包装依赖服务的调用&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;@FeignClient(&quot;provider name&quot;) 注解使用规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明的方法签名必须和 provider 微服务中的 controller 中的方法签名一致&lt;/li&gt;
&lt;li&gt;如果需要传递参数，那么 &lt;code&gt;@RequestParam&lt;/code&gt; 、&lt;code&gt;@RequestBody&lt;/code&gt; 、&lt;code&gt;@PathVariable&lt;/code&gt; 也需要加上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改造消费者服务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：OpenFeign 整合了 Ribbon，具有负载均衡的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：不将其注册到 Eureka 作为微服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 80

eureka:
  client:
    # 表示不将其注入Eureka作为微服务，不作为Eureak客户端了，而是作为Feign客户端
    register-with-eureka: false
    service-url:
      # 集群版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：开启 Feign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableFeignClients //不作为Eureak客户端了，而是作为Feign客户端
public class OrderOpenFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderOpenFeignMain80.class, args);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新建 Service 接口：PaymentFeignService 接口和 @FeignClient 注解，完成 Feign 的包装调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@FeignClient(value = &quot;CLOUD-PAYMENT-SERVICE&quot;) // 作为一个Feign功能绑定的的接口
public interface PaymentFeignService {
    @GetMapping(value = &quot;/payment/get/{id}&quot;)
    public CommonResult&amp;lt;Payment&amp;gt; getPaymentById(@PathVariable(&quot;id&quot;) long id);
    
    @GetMapping(&quot;/payment/feign/timeout&quot;)
    public String paymentFeignTimeout();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class OrderFeignController {
    @Autowired
    private PaymentFeignService paymentFeignService;

    @GetMapping(&quot;/consumer/payment/get/{id}&quot;)
    public CommonResult&amp;lt;Payment&amp;gt; getPayment(@PathVariable(&quot;id&quot;) long id) {
        // 返回对象为响应体中数据转化成的对象，基本上可以理解为JSON
        return paymentFeignService.getPaymentById(id);
    }
    
    @GetMapping(&quot;/consumer/payment/feign/timeout&quot;)
    public String paymentFeignTimeout() {
        // openfeign-ribbon，客户端一般默认等待1s
        return paymentFeignService.paymentFeignTimeout();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;超时问题&lt;/h4&gt;
&lt;p&gt;Feign 默认是支持 Ribbon，Feign 客户端的负载均衡和超时控制都由 Ribbon 控制&lt;/p&gt;
&lt;p&gt;设置 Feign 客户端的超时等待时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ribbon:
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ReadTimeout: 5000
  #指的是建立连接所用的时间，适用于网络状况正常的情况下,两端连接所用的时间
  ConnectTimeout: 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演示超时现象：OpenFeign 默认等待时间为 1 秒钟，超过后会报错&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务提供方 Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/payment/feign/timeout&quot;)
public String paymentFeignTimeout() {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return serverPort;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费者 PaymentFeignService 和 OrderFeignController 参考上一小节代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-OpenFeign%E8%B6%85%E6%97%B6%E9%94%99%E8%AF%AF.png&quot; alt=&quot;Cloud-OpenFeign超时错误&quot; /&gt;!](C:\Users\Seazean\Desktop\123\Cloud-OpenFeign超时错误.png)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;日志级别&lt;/h4&gt;
&lt;p&gt;Feign 提供了日志打印功能，可以通过配置来调整日志级别，从而了解 Feign 中 HTTP 请求的细节&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NONE&lt;/th&gt;
&lt;th&gt;默认的，不显示任何日志&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BASIC&lt;/td&gt;
&lt;td&gt;仅记录请求方法、URL、响应状态码及执行时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEADERS&lt;/td&gt;
&lt;td&gt;除了 BASIC 中定义的信息之外，还有请求和响应的头信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FULL&lt;/td&gt;
&lt;td&gt;除了 HEADERS 中定义的信息外，还有请求和响应的正文及元数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;配置在消费者端&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建 config.FeignConfig 文件：配置日志 Bean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    # feign 日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentFeignService: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Debug 后查看后台日志&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;服务熔断&lt;/h2&gt;
&lt;h3&gt;Hystrix&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Hystrix 是一个用于处理分布式系统的延迟和容错的开源库，在分布式系统里，许多依赖会出现调用失败，比如超时、异常等，Hystrix 能够保证在一个依赖出问题的情况下，不会导致整体服务失败，避免级联故障，以提高分布式系统的弹性&lt;/p&gt;
&lt;p&gt;断路器本身是一种开关装置，当某个服务单元发生故障之后，通过断路器的故障监控（类似熔断保险丝），向调用方返回一个符合预期的、可处理的备选响应（FallBack），而不是长时间的等待或者抛出调用方无法处理的异常，这样就保证了服务调用方的线程不会被长时间地占用，避免了故障在分布式系统中的蔓延，乃至雪崩&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务降级 Fallback：系统不可用时需要一个兜底的解决方案或备选响应，向调用方返回一个可处理的响应&lt;/li&gt;
&lt;li&gt;服务熔断 Break：达到最大服务访问后，直接拒绝访问&lt;/li&gt;
&lt;li&gt;服务限流 Flowlimit：高并发操作时严禁所有请求一次性过来拥挤，一秒钟 N 个，有序排队进行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档：https://github.com/Netflix/Hystrix/wiki/How-To-Use&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;服务降级&lt;/h4&gt;
&lt;h5&gt;案例构建&lt;/h5&gt;
&lt;p&gt;生产者模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-hystrix&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：开启 Feign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 降级使用
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;
    @Value(&quot;${server.port}&quot;)
    private String serverPort;

    // 正常访问
    @GetMapping(&quot;/payment/hystrix/ok/{id}&quot;)
    private String paymentInfo_Ok(@PathVariable(&quot;id&quot;) Integer id) {
        return paymentService.paymentInfo_Ok(id);
    }
	// 超时
    @GetMapping(&quot;/payment/hystrix/timeout/{id}&quot;)
    private String paymentInfo_Timeout(@PathVariable(&quot;id&quot;) Integer id) {
        // service 层有 Thread.sleep() 操作，保证超时
        return paymentService.paymentInfo_Timeout(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class PaymentService {
    public String paymentInfo_Ok(Integer id) {
        return &quot;线程池: &quot; + Thread.currentThread().getName() + &quot;paymentInfo_OK, id:  &quot; + id&quot;;
    }

    public String paymentInfo_Timeout(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return &quot;线程池: &quot; + Thread.currentThread().getName() + &quot; payment_Timeout, id:  &quot; + id;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jmeter 压测两个接口，发现接口 paymentInfo_Ok 也变的卡顿&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Service 接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@FeignClient(value = &quot;CLOUD-PROVIDER-HYSTRIX-PAYMENT&quot;)
public interface PaymentHystrixService {
    @GetMapping(&quot;/payment/hystrix/ok/{id}&quot;)
    public String paymentInfo_Ok(@PathVariable(&quot;id&quot;) Integer id);

    @GetMapping(&quot;/payment/hystrix/timeout/{id}&quot;)
    public String paymentInfo_Timeout(@PathVariable(&quot;id&quot;) Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class OrderHystirxController {
    @Resource
    PaymentHystrixService paymentHystrixService;

    @GetMapping(&quot;/consumer/payment/hystrix/ok/{id}&quot;)
    public String paymentInfo_Ok(@PathVariable(&quot;id&quot;) Integer id) {
        return paymentHystrixService.paymentInfo_Ok(id);
    }

    @GetMapping(&quot;/consumer/payment/hystrix/timeout/{id}&quot;)
    public String paymentInfo_Timeout(@PathVariable(&quot;id&quot;) Integer id) {
        return paymentHystrixService.paymentInfo_Timeout(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试：使用的是 Feign 作为客户端，默认 1s 没有得到响应就会报超时错误，进行并发压测&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时导致服务器变慢（转圈）：超时不再等待&lt;/li&gt;
&lt;li&gt;出错（宕机或程序运行出错）：出错要有兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;降级操作&lt;/h5&gt;
&lt;p&gt;生产者端和消费者端都可以进行服务降级，使用 @HystrixCommand 注解指定降级后的方法&lt;/p&gt;
&lt;p&gt;生产者端：主启动类添加新注解 @EnableCircuitBreaker，业务类（Service）方法进行如下修改，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 模拟拥堵的情况
@HystrixCommand(fallbackMethod = &quot;paymentInfo_TimeoutHandler&quot;, commandProperties = {
    //规定这个线程的超时时间是3s，3s后就由fallbackMethod指定的方法“兜底”（服务降级）
    @HystrixProperty(name=&quot;execution.isolation.thread.timeoutInMilliseconds&quot;, value = &quot;3000&quot;)
})
public String paymentInfo_Timeout(Integer id) {
    // 超时或者出错
}

public String paymentInfo_TimeoutHandler(Integer id) {
    return &quot;线程池：&quot; + Thread.currentThread().getName() + &quot; paymentInfo_TimeoutHandler, id: &quot; + id&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务降级的方法和业务处理的方法混杂在了一块，耦合度很高，并且每个方法配置一个服务降级方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在业务类Controller上加 @DefaultProperties(defaultFallback = &quot;method_name&quot;) 注解&lt;/li&gt;
&lt;li&gt;在需要服务降级的方法上标注 @HystrixCommand 注解，如果 @HystrixCommand 里没有指明 fallbackMethod，就默认使用 @DefaultProperties 中指明的降级服务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
@DefaultProperties(defaultFallback = &quot;payment_Global_FallbackMethod&quot;)
public class OrderHystrixController {
    @Resource
    PaymentHystrixService paymentHystrixService;

    @GetMapping(&quot;/consumer/payment/hystrix/ok/{id}&quot;)
    public String paymentInfo_Ok(@PathVariable(&quot;id&quot;) Integer id) {
        return paymentHystrixService.paymentInfo_OK(id);
    }

    @HystrixCommand
    public String paymentInfo_Timeout(@PathVariable(&quot;id&quot;) Integer id) {
        return paymentHystrixService.paymentInfo_Timeout(id);
    }

    public String paymentTimeOutFallbackMethod(@PathVariable(&quot;id&quot;) Integer id) {
        return &quot;fallback&quot;;
    }

    // 下面是全局fallback方法
    public String payment_Global_FallbackMethod() {
        return &quot;Global fallback&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端调用服务端，遇到服务端宕机或关闭等极端情况，为 Feign 客户端定义的接口添加一个服务降级实现类即可实现解耦&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;application.yml：配置文件中开启了 Hystrix&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 用于服务降级 在注解 @FeignClient中添加fallbackFactory属性值
feign:
  hystrix:
    enabled: true  #在Feign中开启Hystrix
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service：统一为接口里面的方法进行异常处理，服务异常找 PaymentFallbackService，来统一进行服务降级的处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@FeignClient(value = &quot;PROVIDER-HYSTRIX-PAYMENT&quot;, fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {

    @GetMapping(&quot;/payment/hystrix/ok/{id}&quot;)
    public String paymentInfo_OK(@PathVariable(&quot;id&quot;) Integer id);

    @GetMapping(&quot;/payment/hystrix/timeout/{id}&quot;)
    public String paymentInfo_Timeout(@PathVariable(&quot;id&quot;) Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PaymentFallbackService：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return &quot;------PaymentFallbackService-paymentInfo_Ok, fallback&quot;;
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
        return &quot;------PaymentFallbackService-paymentInfo_Timeout, fallback&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;服务熔断&lt;/h4&gt;
&lt;h5&gt;熔断类型&lt;/h5&gt;
&lt;p&gt;熔断机制是应对雪崩效应的一种微服务链路保护机制，当扇出链路的某个微服务出错不可用或者响应时间太长时，会进行服务的降级，进而熔断该节点微服务的调用，快速返回错误的响应信息&lt;/p&gt;
&lt;p&gt;Hystrix 会监控微服务间调用的状况，当失败的调用到一定阈值，缺省时 5 秒内 20 次调用失败，就会启动熔断机制；当检测到该节点微服务调用响应正常后（检测方式是尝试性放开请求），自动恢复调用链路&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;熔断打开：请求不再进行调用当前服务，再有请求调用时将不会调用主逻辑，而是直接调用降级 fallback。实现了自动的发现错误并将降级逻辑切换为主逻辑，减少响应延迟效果。内部设置时钟一般为 MTTR（Mean time to repair，平均故障处理时间），当打开时长达到所设时钟则进入半熔断状态&lt;/li&gt;
&lt;li&gt;熔断关闭：熔断关闭不会对服务进行熔断，服务正常调用&lt;/li&gt;
&lt;li&gt;熔断半开：部分请求根据规则调用当前服务，如果请求成功且符合规则则认为当前服务恢复正常，关闭熔断，反之继续熔断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix%E7%86%94%E6%96%AD%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;Cloud-Hystrix熔断机制&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;熔断操作&lt;/h5&gt;
&lt;p&gt;涉及到断路器的四个重要参数：&lt;strong&gt;快照时间窗、请求总数阀值、窗口睡眠时间、错误百分比阀值&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;circuitBreaker.enabled：是否开启断路器&lt;/li&gt;
&lt;li&gt;metrics.rollingStats.timeInMilliseconds：快照时间窗口，断路器确定是否打开需要统计一些请求和错误数据，而统计的时间范围就是快照时间窗，默认为最近的 10 秒&lt;/li&gt;
&lt;li&gt;circuitBreaker.requestVolumeThreshold：请求总数阀值，该属性设置在快照时间窗内（默认 10s）使断路器跳闸的最小请求数量（默认是 20），如果 10s 内请求数小于设定值，就算请求全部失败也不会触发断路器&lt;/li&gt;
&lt;li&gt;circuitBreaker.sleepWindowInMilliseconds：窗口睡眠时间，短路多久以后开始尝试是否恢复进入半开状态，默认 5s&lt;/li&gt;
&lt;li&gt;circuitBreaker.errorThresholdPercentage：错误百分比阀值，失败率达到多少后将断路器打开&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; //总的意思就是在n(10)毫秒内的时间窗口期内，m次请求中有p% (60%)的请求失败了，那么断路器启动
@HystrixCommand(fallbackMethod = &quot;paymentCircuitBreaker_fallback&quot;, commandProperties = {
        @HystrixProperty(name = &quot;circuitBreaker.enabled&quot;, value = &quot;true&quot;),
        @HystrixProperty(name = &quot;circuitBreaker.requestVolumeThreshold&quot;, value = &quot;10&quot;), 
        @HystrixProperty(name = &quot;circuitBreaker.sleepWindowInMilliseconds&quot;, value = &quot;10000&quot;),
        @HystrixProperty(name = &quot;circuitBreaker.errorThresholdPercentage&quot;, value = &quot;60&quot;)  
})
public String paymentCircuitBreaker(@PathVariable(&quot;id&quot;) Integer id) {
    if (id &amp;lt; 0) {
        throw new RuntimeException(&quot;******id 不能负数&quot;);
    }
    String serialNumber = IdUtil.simpleUUID();  // 等价于UUID.randomUUID().toString()

    return Thread.currentThread().getName() + &quot;\t&quot; + &quot;调用成功，流水号: &quot; + serialNumber;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;开启：满足一定的阈值（默认 10 秒内超过 20 个请求次数）、失败率达到阈值（默认 10 秒内超过 50% 的请求失败）&lt;/li&gt;
&lt;li&gt;关闭：一段时间之后（默认是 5 秒），断路器是半开状态，会让其中一个请求进行转发，如果成功断路器会关闭，反之继续开启&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;工作流程&lt;/h4&gt;
&lt;p&gt;具体工作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建 HystrixCommand（用在依赖的服务返回单个操作结果的时候） 或 HystrixObserableCommand（用在依赖的服务返回多个操作结果的时候） 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令执行，其中 HystrixComand 实现了下面前两种执行方式，而 HystrixObservableCommand 实现了后两种执行方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;execute()：同步执行，从依赖的服务返回一个单一的结果对象， 或是在发生错误的时候抛出异常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;queue()：异步执行， 直接返回 一个 Future 对象， 其中包含了服务执行结束时要返回的单一结果对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;observe()：返回 Observable 对象，代表了操作的多个结果，它是一个 Hot Obserable（不论事件源是否有订阅者，都会在创建后对事件进行发布，所以对于 Hot Observable 的每个订阅者都有可能是从事件源的中途开始的，并可能只是看到了整个操作的局部过程）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;toObservable()：同样会返回 Observable 对象，也代表了操作的多个结果，但它返回的是一个 Cold Observable（没有订阅者的时候并不会发布事件，而是进行等待，直到有订阅者之后才发布事件，所以对于 Cold Observable 的订阅者，它可以保证从一开始看到整个操作的全部过程）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若当前命令的请求缓存功能是被启用的，并且该命令缓存命中，那么缓存的结果会立即以 Observable 对象的形式返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查断路器是否为打开状态，如果断路器是打开的，那么 Hystrix 不会执行命令，而是转接到 fallback 处理逻辑（第 8 步）；如果断路器是关闭的，检查是否有可用资源来执行命令（第 5 步）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池/请求队列/信号量是否占满，如果命令依赖服务的专有线程池和请求队列，或者信号量（不使用线程池时）已经被占满， 那么 Hystrix 也不会执行命令， 而是转接到 fallback 处理逻辑（第 8 步）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HystrixCommand.run()：返回一个单一的结果，或者抛出异常&lt;/li&gt;
&lt;li&gt;HystrixObservableCommand.construct()：返回一个Observable 对象来发射多个结果，或通过 onError 发送错误通知&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hystrix会将&quot;成功&quot;、&quot;失败&quot;、&quot;拒绝&quot;、&quot;超时&quot;等信息报告给断路器，而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开，来对某个依赖服务的请求进行&quot;熔断/短路&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当命令执行失败的时候，Hystrix 会进入 fallback 尝试回退处理，通常也称该操作为&quot;服务降级&quot;，而能够引起服务降级情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 4 步：当前命令处于&quot;熔断/短路&quot;状态，断路器是打开的时候&lt;/li&gt;
&lt;li&gt;第 5 步：当前命令的线程池、请求队列或 者信号量被占满的时候&lt;/li&gt;
&lt;li&gt;第 6 步：HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 Hystrix 命令执行成功之后， 它会将处理结果直接返回或是以 Observable 的形式返回&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：如果、没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常， Hystrix 依然会返回一个 Observable 对象， 但是它不会发射任何结果数据，而是通过 onError 方法通知命令立即中断请求，并通过 onError() 方法将引起命令失败的异常发送给调用者&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;Cloud-Hystrix工作流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;官方文档：https://github.com/Netflix/Hystrix/wiki/How-it-Works&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;服务监控&lt;/h4&gt;
&lt;p&gt;Hystrix 提供了准实时的调用监控（Hystrix Dashboard），Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息，并以统计报表和图形的形式展示给用户，包括每秒执行多少请求多少成功，多少失败等，Netflix 通过 &lt;code&gt;hystrix-metrics-event-stream&lt;/code&gt; 项目实现了对以上指标的监控，Spring Cloud 提供了 Hystrix Dashboard 的整合，对监控内容转化成可视化页面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-hystrix-dashboard&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：只需要端口即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 9001
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableHystrixDashboard // 开启Hystrix仪表盘
public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardMain9001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有微服务（生产者）提供类 8001/8002/8003 都需要监控依赖配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动测试：http://localhost:9001/hystrix&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix可视化界面.png&quot; alt=&quot;Cloud-Hystrix可视化界面&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新版本 Hystrix 需要在需要监控的微服务端的主启动类中指定监控路径，不然会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient  // 本服务启动后会自动注册进eureka服务中
@EnableCircuitBreaker  // 对hystrixR熔断机制的支持
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

    /** ======================================需要添加的代码==================
     *此配置是为了服务监控而配置，与服务容错本身无关，springcloud升级后的坑
     *ServletRegistrationBean因为springboot的默认路径不是&quot;/hystrix.stream&quot;，
     *只要在自己的项目里配置上下面的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings(&quot;/hystrix.stream&quot;);
        registrationBean.setName(&quot;HystrixMetricsStreamServlet&quot;);
        return registrationBean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指标说明：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix%E7%95%8C%E9%9D%A2%E5%9B%BE%E7%A4%BA%E8%AF%B4%E6%98%8E.png&quot; alt=&quot;Cloud-Hystrix界面图示说明&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;服务网关&lt;/h2&gt;
&lt;h3&gt;Zuul&lt;/h3&gt;
&lt;p&gt;SpringCloud 中所集成的 Zuul 版本，采用的是 Tomcat 容器，基于 Servlet 之上的一个阻塞式处理模型，不支持任何长连接，用 Java 实现，而 JVM 本身会有第一次加载较慢的情况，使得 Zuul 的性能相对较差&lt;/p&gt;
&lt;p&gt;官网：   https://github.com/Netflix/zuul/wiki&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Gateway&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;SpringCloud Gateway 是 Spring Cloud 的一个全新项目，基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关，旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 WebFlux 框架实现，而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty（异步非阻塞响应式的框架）&lt;/li&gt;
&lt;li&gt;基于 Filter 链的方式提供了网关基本的功能，例如：安全、监控/指标、限流等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gateway 的三个核心组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Route：路由是构建网关的基本模块，由 ID、目标 URI、一系列的断言和过滤器组成，如果断言为 true 则匹配该路由&lt;/li&gt;
&lt;li&gt;Predicate：断言，可以匹配 HTTP 请求中的所有内容（例如请求头或请求参数），如果请求参数与断言相匹配则进行路由&lt;/li&gt;
&lt;li&gt;Filter：指 Spring 框架中的 GatewayFilter实例，使用过滤器可以在请求被路由前或之后（拦截）对请求进行修改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;Cloud-Gateway工作流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心逻辑：路由转发 + 执行过滤器链&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端向 Spring Cloud Gateway 发出请求，然后在 Gateway Handler Mapping 中找到与请求相匹配的路由，将其发送到 Gateway Web Handler&lt;/li&gt;
&lt;li&gt;Handler 通过指定的过滤器链来将请求发送到际的服务执行业务逻辑，然后返回&lt;/li&gt;
&lt;li&gt;过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前（pre）或之后（post）执行业务逻辑&lt;/li&gt;
&lt;li&gt;Filter 在 pre 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等，在 post 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;网关使用&lt;/h4&gt;
&lt;h5&gt;配置方式&lt;/h5&gt;
&lt;p&gt;Gateway 网关路由有两种配置方式，分别为通过 yml 配置和注入 Bean&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：Gateway 不需要 spring-boot-starter-web 依赖，否在会报错，原因是底层使用的是 WebFlux 与 Web 冲突&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-gateway&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 9527

spring:
  application:
    name: cloud-gateway

eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类（网关不需要业务类）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以前访问 provider-payment8001 中的 Controller 方法，通过 localhost:8001/payment/get/id 和 localhost:8001/payment/lb，项目不想暴露 8001 端口号，希望在 8001 外面套一层 9527 端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 9527

spring:
  application:
    name: cloud-gateway
## =====================新增====================
  cloud:
    gateway:
      routes:
        - id: payment_routh # payment_route	#路由的ID，没有固定规则但要求【唯一】，建议配合服务名
          uri: http://localhost:8001		#匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**			# 断言，路径相匹配的进行路由

        - id: payment_routh2 # payment_route#路由的ID，没有固定规则但要求【唯一】，建议配合服务名
          uri: http://localhost:8001     	#匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**			# 断言，路径相匹配的进行路由
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;uri + predicate 拼接就是具体的接口请求路径，通过 localhost:9527 映射的地址&lt;/li&gt;
&lt;li&gt;predicate 断言 http://localhost:8001下面有一个 /payment/get/** 的地址，如果找到了该地址就返回 true，可以用 9527 端口访问，进行端口的适配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**&lt;/code&gt; 表示通配符，因为这是一个不确定的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;注入Bean&lt;/h5&gt;
&lt;p&gt;通过 9527 网关访问到百度的网址 https://www.baidu.com/，在 config 包下创建一个配置类，路由规则是访问 /baidu 跳转到百度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class GatewayConfig {
    // 配置了一个 id 为 path_route_cloud 的路由规则
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
        // 构建一个路由器，这个routes相当于yml配置文件中的routes
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        // 路由器的id是：path_route_cloud，规则是访问/baidu ，将会转发到 https://www.baidu.com/
        routes.route(&quot;path_route_cloud&quot;,
                r -&amp;gt; r.path(&quot;/baidu&quot;).uri(&quot; https://www.baidu.com&quot;)).build();
        return routes.build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;动态路由&lt;/h5&gt;
&lt;p&gt;Gateway 会根据注册中心注册的服务列表，以注册中心上微服务名为路径创建动态路由进行转发，从而实现动态路由和负载均衡，避免出现一个路由规则仅对应一个接口方法，当请求地址很多时需要很大的配置文件&lt;/p&gt;
&lt;p&gt;application.yml 开启动态路由功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true		# 开启从注册中心动态创建路由的功能，利用微服务名进行路由
      routes:
        - id: payment_routh1   					# 路由的ID，没有固定规则但要求唯一，建议配合服务名
          uri: lb://cloud-payment-service		# 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**              # 断言，路径相匹配的进行路由

        - id: payment_routh2      				#路由的ID，没有固定规则但要求唯一，建议配合服务名
          uri: lb://cloud-payment-service		#匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**               # 断言，路径相匹配的进行路由
            - After=2021-09-28T19:14:51.514+08:00[Asia/Shanghai]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lb:// 开头代表从注册中心中获取服务，后面是需要转发到的服务名称&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;断言类型&lt;/h4&gt;
&lt;p&gt;After Route Predicate：匹配该断言时间之后的 URI 请求&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TimeTest {
    public static void main(String[] args) {
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj); //2023-01-10T16:31:44.106+08:00[Asia/Shanghai]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 yml：动态路由小结有配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试：正常访问成功，将时间修改到 2023-01-10T16:31:44.106+08:00[Asia/Shanghai] 之后访问失败&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway%E6%97%B6%E9%97%B4%E6%96%AD%E8%A8%80.png&quot; alt=&quot;Cloud-Gateway时间断言&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见断言类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Before Route Predicate：匹配该断言时间之前的 URI 请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Between Route Predicate：匹配该断言时间之间的 URI 请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Between=2022-02-02T17:45:06.206+08:00[Asia/Shanghai],2022-03-25T18:59:06.206+08:00[Asia/Shanghai]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie Route Predicate：Cookie 断言，两个参数分别是 Cookie name 和正则表达式，路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配，如果匹配上就会执行路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Cookie=username, seazean # 只有发送的请求有 cookie，而且有username=seazean这个数据才能访问，反之404
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Header Route Predicate：请求头断言&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性，并且值为整数的正则表达式
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Host Route Predicate：指定主机可以访问，可以指定多个用 &lt;code&gt;,&lt;/code&gt; 分隔开&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Host=**.seazean.com
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Method Route Predicate：请求类型断言&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Method=GET	# 只有 Get 请求才能访问
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Path Route Predicate：路径匹配断言&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Query Route Predicate：请求参数断言&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Query=username, \d+ # 要有参数名 username 并且值还要是整数才能路由
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Filter使用&lt;/h4&gt;
&lt;p&gt;Filter 链是同时满足一系列的过滤链，路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应，路由过滤器只能指定路由进行使用，Spring Cloud Gateway 内置了多种路由过滤器，都由 GatewayFilter 的工厂类来产生&lt;/p&gt;
&lt;p&gt;配置文件：https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories&lt;/p&gt;
&lt;p&gt;自定义全局过滤器：实现两个主要接口 GlobalFilter, Ordered&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info(&quot;*********************come in MyLogGateWayFilter:  &quot;+ new Date());
        // 取出请求参数的uname对应的值
        String uname = exchange.getRequest().getQueryParams().getFirst(&quot;uname&quot;);
        // 如果 uname 为空，就直接过滤掉，不走路由
        if(uname == null){
            log.info(&quot;************* 用户名为 NULL 非法用户 o(╥﹏╥)o&quot;);

            // 判断该请求不通过时：给一个回应，返回
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        // 反之，调用下一个过滤器，也就是放行：在该环节判断通过的 exchange 放行，交给下一个 filter 判断
        return chain.filter(exchange);
    }
	
    // 设置这个过滤器在Filter链中的加载顺序，数字越小，优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;服务配置&lt;/h2&gt;
&lt;h3&gt;config&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持（Git/GitHub），为各个不同微服务应用的所有环境提供了一个中心化的外部配置（Config Server）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Config%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png&quot; alt=&quot;Cloud-Config工作原理&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SpringCloud Config 分为服务端和客户端两部分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端也称为分布式配置中心，是一个独立的微服务应用，连接配置服务器并为客户端提供获取配置信息，加密/解密等信息访问接口&lt;/li&gt;
&lt;li&gt;客户端通过指定的配置中心来管理应用资源，以及与业务相关的配置内容，并在启动时从配置中心获取和加载配置信息，配置服务器默认采用 Git 来存储配置信息，这样既有助于对环境配置进行版本管理，也可以通过 Git 客户端来方便的管理和访问配置内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集中管理配置文件&lt;/li&gt;
&lt;li&gt;不同环境不同配置，动态化的配置更新，分环境部署比如 dev/test/prod/beta/release&lt;/li&gt;
&lt;li&gt;运行期间动态调整配置，服务向配置中心统一拉取配置的信息，&lt;strong&gt;服务不需要重启即可感知到配置的变化并应用新的配置&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;将配置信息以 Rest 接口的形式暴露&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官网： https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;服务端&lt;/h4&gt;
&lt;p&gt;构建 Config Server 模块&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--springCloud Config Server--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-config-server&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 3344

spring:
  application:
    name:  cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          # GitHub上面的git仓库名字 这里可以写https地址跟ssh地址，https地址需要配置username和 password
          uri: git@github.com:seazean/springcloud-config.git
          default-label: main
          search-paths:
            - springcloud-config	# 搜索目录
          # username: 
          # password:
      label: main   # 读取分支,以前是master

#服务注册到eureka地址
eureka:
  client:
    service-url:
    defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #集群版
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;search-paths 表示远程仓库下有一个叫做 springcloud-config 的，label 则表示读取 main分支里面的内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer   //开启SpringCloud的
public class ConfigCenterMain3344 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain3344.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置读取规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;label：分支&lt;/li&gt;
&lt;li&gt;name：服务名&lt;/li&gt;
&lt;li&gt;profile：环境（dev/test/prod）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如：http://localhost:3344/master/config-dev.yaml&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;客户端&lt;/h4&gt;
&lt;h5&gt;基本配置&lt;/h5&gt;
&lt;p&gt;配置客户端 Config Client，客户端从配置中心（Config Server）获取配置信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--这里就是客户端的SpringCloud config 因为是客户端所以没有server--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-config&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bootstrap.yml：系统级配置，优先级更高，application.yml 是用户级的资源配置项&lt;/p&gt;
&lt;p&gt;Spring Cloud 会创建一个 Bootstrap Context 作为 Spring 应用的 Application Context 的父上下文，初始化的时候 Bootstrap Context 负责从外部源加载配置属性并解析配置，这两个上下文共享一个从外部获取的 Environment，为了配置文件的加载顺序和分级管理，这里使用 bootstrap.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 3355	# 构建多个微服务，3366 3377 等

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: main 	#分支名称 以前是master
      name: config 	#配置文件名称
      profile: dev 	#读取后缀名称   
      # main分支上config-dev.yml的配置文件被读取 http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 # 配置中心地址k

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：将配置信息以 REST 窗口的形式暴露&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class ConfigClientController {
    @Value(&quot;${config.info}&quot;)
    private String configInfo;

    @GetMapping(&quot;/configInfo&quot;)
    public String getConfigInfo() {
        return configInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;动态刷新&lt;/h5&gt;
&lt;p&gt;分布式配置的动态刷新问题，修改 GitHub 上的配置文件，Config Server 配置中心立刻响应，但是 Config Client 客户端没有任何响应，需要重启客户端&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--web/actuator这两个一般一起使用，写在一起--&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 yml，暴露监控端口：SpringBoot 的 actuator 启动端点监控 Web 端默认加载默认只有两个 info，health 可见的页面节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;management: 
  endpoints:
    web:
      exposure:
        include: &quot;*&quot; 		# 表示包含所有节点页面
        exclude: env,beans	# 表示排除env、beans
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：加 @RefreshScope 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RefreshScope
public class ConfigClientController {
    // 从配置文件中取前缀为server.port的值
    @Value(&quot;${config.info}&quot;)
    private String configInfo;
	// config-{profile}.yml
    @GetMapping(&quot;/configInfo&quot;)
    public String getConfigInfo() {
        return configInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时客户端还是没有刷新，需要发送 POST 请求刷新 3355：&lt;code&gt;curl -X POST &quot;http://localhost:3355/actuator/refresh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;引出问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在微服务多的情况下，每个微服务都需要执行一个 POST 请求，手动刷新成本太大&lt;/li&gt;
&lt;li&gt;可否广播，一次通知，处处生效，大范围的实现自动刷新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：Bus 总线&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;服务消息&lt;/h2&gt;
&lt;h3&gt;Bus&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Spring Cloud Bus 能管理和传播分布式系统间的消息，就像分布式执行器，可用于广播状态更改、事件推送、微服务间的通信通道等&lt;/p&gt;
&lt;p&gt;消息总线：在微服务架构的系统中，通常会使用轻量级的消息代理来构建一个共用的消息主题，并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费，所以称为消息总线&lt;/p&gt;
&lt;p&gt;基本原理：ConfigClient 实例都监听 MQ 中同一个 Topic（默认 springCloudBus)，当一个服务刷新数据时，会把信息放入 Topic 中，这样其它监听同一 Topic 的服务就能得到通知，然后去更新自身的配置&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;全局广播&lt;/h4&gt;
&lt;p&gt;利用消息总线接触一个服务端 ConfigServer 的 &lt;code&gt;/bus/refresh&lt;/code&gt; 断点，从而刷新所有客户端的配置&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Bus全局广播架构.png&quot; alt=&quot;Cloud-Bus全局广播架构&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;改造 ConfigClient：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 MQ 的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--添加消息总线RabbitMQ支持--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-bus-amqp&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;yml 文件添加 MQ 信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 3344

spring:
  application:
    name:  config-client #注册进Eureka服务器的微服务名
  cloud:
  # rabbitmq相关配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

# rabbitmq相关配置,暴露bus刷新配置的端点
management:
  endpoints: # 暴露bus刷新配置的端点
    web:
      exposure:
        include: &apos;bus-refresh&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只需要调用一次 &lt;code&gt;curl -X POST &quot;http://localhost:3344/actuator/bus-refresh&lt;/code&gt;，可以实现全局广播&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;定点通知&lt;/h4&gt;
&lt;p&gt;动态刷新情况下，只通知指定的微服务，比如只通知 3355 服务，不通知 3366，指定具体某一个实例生效，而不是全部&lt;/p&gt;
&lt;p&gt;公式：&lt;code&gt;http://localhost:port/actuator/bus-refresh/{destination}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;/bus/refresh 请求不再发送到具体的服务实例上，而是发给 Config Server 并通过 destination 参数类指定需要更新配置的服务或实例&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Bus%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;Cloud-Bus工作流程&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Stream&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Spring Cloud Stream 是一个构建消息驱动微服务的框架，通过定义绑定器 Binder 作为中间层，实现了应用程序与消息中间件细节之间的隔离，屏蔽底层消息中间件的差异，降低切换成本，统一消息的编程模型&lt;/p&gt;
&lt;p&gt;Stream 中的消息通信方式遵循了发布订阅模式，Binder 可以生成 Binding 用来绑定消息容器的生产者和消费者，Binding 有两种类型 Input 和 Output，Input 对应于消费者（消费者从 Stream 接收消息），Output 对应于生产者（生产者从 Stream 发布消息）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Stream%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;Cloud-Stream工作流程&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Binder：连接中间件&lt;/li&gt;
&lt;li&gt;Channel：通道，是队列 Queue 的一种抽象，在消息通讯系统中实现存储和转发的媒介，通过 Channel 对队列进行配置&lt;/li&gt;
&lt;li&gt;Source、Sink：生产者和消费者&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;中文手册：https://m.wang1314.com/doc/webapp/topic/20971999.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;Binder 是应用与消息中间件之间的封装，目前实现了 Kafka 和 RabbitMQ 的 Binder，可以动态的改变消息类型（Kafka 的 Topic 和 RabbitMQ 的 Exchange），可以通过配置文件实现，常用注解如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Input：标识输入通道，接收的消息通过该通道进入应用程序&lt;/li&gt;
&lt;li&gt;@Output：标识输出通道，发布的消息通过该通道离开应用程序&lt;/li&gt;
&lt;li&gt;@StreamListener：监听队列，用于消费者队列的消息接收&lt;/li&gt;
&lt;li&gt;@EnableBinding：信道 Channel 和 Exchange 绑定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产者发消息模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：RabbitMQ&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-stream-rabbit&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8801

spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息；
        defaultRabbit: # 表示定义的名称，用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: studyExchange 		# 表示要使用的Exchange名称定义
          content-type: application/json	# 设置消息类型，本次为json，文本则设置“text/plain”
          binder: defaultRabbit 			# 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔（默认是30秒）
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔（默认是90秒）
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：MessageChannel 的实例名必须是 output，否则无法启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 可以理解为定义消息的发送管道Source对应output(生产者)，Sink对应input(消费者)
@EnableBinding(Source.class)
// @Service：这里不需要，不是传统的controller调用service。这个service是和rabbitMQ打交道的
// IMessageProvider 只有一个 send 方法的接口
public class MessageProviderImpl implements IMessageProvider {
    @Resource
    private MessageChannel output; // 消息的发送管道

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();

        //创建消息，通过output这个管道向消息中间件发消息
        this.output.send(MessageBuilder.withPayload(serial).build());
        System.out.println(&quot;***serial: &quot; + serial);
        return serial;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class SendMessageController {
    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = &quot;/sendMessage&quot;)
    public String sendMessage() {
        return messageProvider.send();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消费者模块：8802 和 8803 两个消费者&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;application.yml：只标注出与生产者不同的地方&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      # ...
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          # ...
          binder: { defaultRabbit } # 设置要绑定的消息服务的具体设置

eureka:
  # ...
  instance:
    # ...
    instance-id: receive-8802.com  # 在信息列表时显示主机名称
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@EnableBinding(Sink.class) // 理解为定义一个消息消费者的接收管道
public class ReceiveMessageListener {
    @Value(&quot;${server.port}&quot;)
    private String serverPort;

    @StreamListener(Sink.INPUT) //输入源：作为一个消息监听者
    public void input(Message&amp;lt;String&amp;gt; message) {
        // 获取到消息
        String messageStr = message.getPayload();
        System.out.println(&quot;消费者1号，-------&amp;gt;接收到的消息：&quot; + messageStr + &quot;\t port: &quot; + serverPort);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;高级特性&lt;/h4&gt;
&lt;p&gt;重复消费问题：生产者 8801 发送一条消息后，8802 和 8803 会同时收到 8801 的消息&lt;/p&gt;
&lt;p&gt;解决方法：微服务应用放置于同一个 group 中，能够保证消息只会被其中一个应用消费一次。不同的组是可以全面消费的（重复消费），同一个组内的多个消费者会发生竞争关系，只有其中一个可以消费&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bindings:
  input:
    destination: studyExchange
    content-type: application/json
    binder: { defaultRabbit }
    group: seazean	# 设置分组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消息持久化问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停止 8802/8803 并去除掉 8802 的分组 group: seazean，8801 先发送 4 条消息到 MQ&lt;/li&gt;
&lt;li&gt;先启动 8802，无分组属性配置，后台没有打出来消息，消息丢失&lt;/li&gt;
&lt;li&gt;再启动 8803，有分组属性配置，后台打印出来了 MQ 上的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Sleuth&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Spring Cloud Sleuth 提供了一套完整的分布式请求链路跟踪的解决方案，并且兼容支持了 zipkin&lt;/p&gt;
&lt;p&gt;在微服务框架中，一个客户端发起的请求在后端系统中会经过多次不同的服务节点调用来协同产生最后的请求结果，形成一条复杂的分布式服务调用链路，链路中的任何一环出现高延时或错误都会引起整个请求最后的失败，所以需要链路追踪&lt;/p&gt;
&lt;p&gt;Sleuth 官网：https://github.com/spring-cloud/spring-cloud-sleuth&lt;/p&gt;
&lt;p&gt;zipkin 下载地址：https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;链路监控&lt;/h4&gt;
&lt;p&gt;Sleuth 负责跟踪整理，zipkin 负责可视化展示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar zipkin-server-2.12.9-exec.jar # 启动 zipkin 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 http://localhost:9411/zipkin/ 展示交互界面&lt;/p&gt;
&lt;p&gt;一条请求链路通过 Trace ID 唯一标识，Span 标识发起的请求信息&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Trace：类似于树结构的 Span 集合，表示一条调用链路，存在唯一 ID 标识&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Span：表示调用链路来源，通俗的理解 Span 就是一次请求信息，各个 Span 通过 ParentID 关联起来&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务生产者模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--包含了sleuth+zipkin--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-zipkin&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      #采样率值介于 0 到 1 之间，1 则表示全部采集
      probability: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/payment/zipkin&quot;)
public String paymentZipkin() {
    return &quot;hi ,i&apos;am paymentzipkin server fall back，welcome to seazean&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务消费者模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 80

# 微服务名称
spring:
  application:
    name: cloud-order-service
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      probability: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/comsumer/payment/zipkin&quot;)
public String paymentZipKin() {
    String result = restTemplate.getForObject(&quot;http://localhost:8001&quot; + &quot;/payment/zipkin/&quot;, String.class);
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Alibaba&lt;/h2&gt;
&lt;p&gt;Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案，此项目包含开发分布式应用微服务的必需组件，方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务限流降级：默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入，可以在运行时通过控制台实时修改限流降级规则，还支持查看限流降级 Metrics 监控。&lt;/li&gt;
&lt;li&gt;服务注册与发现：适配 Spring Cloud 服务注册与发现标准，默认集成了 Ribbon 的支持&lt;/li&gt;
&lt;li&gt;分布式配置管理：支持分布式系统中的外部化配置，配置更改时自动刷新&lt;/li&gt;
&lt;li&gt;消息驱动能力：基于 Spring Cloud Stream 为微服务应用构建消息驱动能力&lt;/li&gt;
&lt;li&gt;分布式事务：使用 @GlobalTransactional 注解， 高效并且对业务零侵入地解决分布式事务问题&lt;/li&gt;
&lt;li&gt;阿里云对象存储：阿里云提供的海量、安全、低成本、高可靠的云存储服务&lt;/li&gt;
&lt;li&gt;分布式任务调度：提供秒级、精准、高可靠、高可用的定时（基于 Cron 表达式）任务调度服务。同时提供分布式的任务执行模型，如网格任务。网格任务支持海量子任务均匀分配到所有 Worker（schedulerx-client）上执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方文档：https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md&lt;/p&gt;
&lt;p&gt;官方手册：https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html&lt;/p&gt;
&lt;h3&gt;Nacos&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Nacos 全称 Dynamic Naming and Configuration Service，一个更易于构建云原生应用的动态服务发现、配置管理和服务的管理平台，Nacos = Eureka + Config + Bus&lt;/p&gt;
&lt;p&gt;下载地址：https://github.com/alibaba/nacos/releases&lt;/p&gt;
&lt;p&gt;启动命令：命令运行成功后直接访问 http://localhost:8848/nacos，默认账号密码都是 nacos&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;startup.cmd -m standalone # standalone 代表着单机模式运行，非集群模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shutdown.cmd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注册中心对比：C 一致性，A 可用性，P 分区容错性&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注册中心&lt;/th&gt;
&lt;th&gt;CAP 模型&lt;/th&gt;
&lt;th&gt;控制台管理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zookeeper&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consul&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nacos&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;切换模式：&lt;code&gt;curl -X PUT &apos;$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&amp;amp;value=CP&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;官网：https://nacos.io&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;注册中心&lt;/h4&gt;
&lt;p&gt;Nacos 作为服务注册中心&lt;/p&gt;
&lt;p&gt;服务提供者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 9001

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址，注册到Nacos
# 做监控需要把这个全部暴露出来
management:
  endpoints:
    web:
      exposure:
        include: &apos;*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：注解是 EnableDiscoveryClient&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9001.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class PaymentController {
    @Value(&quot;${server.port}&quot;)
    private String serverPort;

    @GetMapping(value = &quot;/payment/nacos/{id}&quot;)
    public String getPayment(@PathVariable(&quot;id&quot;) Integer id) {
        return &quot;nacos registry, serverPort: &quot; + serverPort + &quot;\t id&quot; + id;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理后台服务：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos%E6%9C%8D%E5%8A%A1%E5%88%97%E8%A1%A8.png&quot; alt=&quot;Cloud-Nacos服务列表&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新建一个模块端口是 9002，其他与 9001 服务一样，nacos-payment-provider 的实例数就变为 2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务消费者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 83

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

# 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain83.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class ApplicationContextBean {
    @Bean
    @LoadBalanced // 生产者集群状态下，必须添加，防止找不到实例
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class OrderNacosController {
    @Resource
    private RestTemplate restTemplate;
	// 从配置文件中读取 URL
    @Value(&quot;${service-url.nacos-user-service}&quot;)
    private String serverURL;

    @GetMapping(&quot;/consumer/payment/nacos/{id}&quot;)
    public String paymentInfo(@PathVariable(&quot;id&quot;) Long id) {
        String result = restTemplate.getForObject(serverURL + &quot;/payment/nacos/&quot; + id, String.class);
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;配置中心&lt;/h4&gt;
&lt;h5&gt;基础配置&lt;/h5&gt;
&lt;p&gt;把配置文件写进 Nacos，然后再用 Nacos 做 config 这样的功能，直接从 Nacos 上抓取服务的配置信息&lt;/p&gt;
&lt;p&gt;在 Nacos 中，dataId 的完整格式如下 &lt;code&gt;${prefix}-${spring.profiles.active}.${file-extension}&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prefix&lt;/code&gt;：默认为 &lt;code&gt;spring.application.name&lt;/code&gt; 的值，也可以通过配置项 &lt;code&gt;spring.cloud.nacos.config.prefix&lt;/code&gt; 来配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spring.profiles.active&lt;/code&gt;：当前环境对应的 profile，当该值为空时，dataId 的拼接格式变成 &lt;code&gt;${prefix}.${file-extension}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;file-exetension&lt;/code&gt;：配置内容的数据格式，可以通过配置项 &lt;code&gt;spring.cloud.nacos.config.file-extension&lt;/code&gt; 来配置，目前只支持 properties 和 yaml 类型（不是 yml）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构建流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置两个 yml 文件：配置文件的加载是存在优先级顺序的，bootstrap 优先级高于 application&lt;/p&gt;
&lt;p&gt;bootstrap.yml：全局配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# nacos配置
server:
  port: 3377

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置

# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yml：服务独立配置，表示服务要去配置中心找名为 nacos-config-client-dev.yaml 的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  profiles:
    active: dev # 表示开发环境
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
    public static void main(String[] args) {
        SpringApplication.run(NacosConfigClientMain3377.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：@RefreshScope 注解使当前类下的配置支持 Nacos 的动态刷新功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RefreshScope
public class ConfigClientController {
    @Value(&quot;${config.info}&quot;)
    private String configInfo;

    @GetMapping(&quot;/config/info&quot;)
    public String getConfigInfo() {
        return configInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增配置，然后访问 http://localhost:3377/config/info&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos%E6%96%B0%E5%A2%9E%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;Cloud-Nacos新增配置&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;分类配置&lt;/h5&gt;
&lt;p&gt;分布式开发中的多环境多项目管理问题，Namespace 用于区分部署环境，Group 和 DataID 逻辑上区分两个目标对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos%E9%85%8D%E7%BD%AE%E8%AF%B4%E6%98%8E.png&quot; alt=&quot;Cloud-Nacos配置说明&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Namespace 默认 public，主要用来实现隔离，图示三个开发环境&lt;/p&gt;
&lt;p&gt;Group 默认是 DEFAULT_GROUP，Group 可以把不同的微服务划分到同一个分组里面去&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;加载配置&lt;/h5&gt;
&lt;p&gt;DataID 方案：指定 &lt;code&gt;spring.profile.active&lt;/code&gt; 和配置文件的 DataID 来使不同环境下读取不同的配置&lt;/p&gt;
&lt;p&gt;Group 方案：通过 Group 实现环境分区，在 config 下增加一条 Group 的配置即可&lt;/p&gt;
&lt;p&gt;Namespace 方案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 3377

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置
        group: DEV_GROUP
        namespace: 95d44530-a4a6-4ead-98c6-23d192cee298
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4.png&quot; alt=&quot;Cloud-Nacos命名空间&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;集群架构&lt;/h4&gt;
&lt;p&gt;集群部署参考官方文档，Nacos 支持的三种部署模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单机模式：用于测试和单机使用&lt;/li&gt;
&lt;li&gt;集群模式：用于生产环境，确保高可用&lt;/li&gt;
&lt;li&gt;多集群模式：用于多数据中心场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;集群部署文档：https://nacos.io/zh-cn/docs/v2/guide/admin/cluster-mode-quick-start.html&lt;/p&gt;
&lt;p&gt;默认 Nacos 使用嵌入式数据库 derby 实现数据的存储，重启 Nacos 后配置文件不会消失，但是多个 Nacos 节点数据存储存在一致性问题，每个 Nacos 都有独立的嵌入式数据库，所以 Nacos 采用了集中式存储的方式来支持集群化部署，目前只支持 MySQL 的存储&lt;/p&gt;
&lt;p&gt;Windows 下 Nacos 切换 MySQL 存储：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 Nacos 安装目录的 conf 目录下找到一个名为 &lt;code&gt;nacos-mysql.sql&lt;/code&gt; 的脚本并执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 conf 目录下找到 &lt;code&gt;application.properties&lt;/code&gt;，增加如下数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.datasource.platform=mysql
 
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&amp;amp;connectTimeout=1000&amp;amp;socketTimeout=3000&amp;amp;autoReconnect=true
db.user=username
db.password=password
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新启动 Nacos，可以看到是个全新的空记录界面&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux 参考：https://www.yuque.com/mrlinxi/pxvr4g/rnahsn#dPvMy&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Sentinel&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&lt;/p&gt;
&lt;p&gt;Sentinel 分为两个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心库（Java 客户端）不依赖任何框架/库，能够运行于 Java 8 及以上的版本的运行时环境&lt;/li&gt;
&lt;li&gt;控制台（Dashboard）主要负责管理推送规则、监控、管理机器信息等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下载到本地，运行命令：&lt;code&gt;java -jar sentinel-dashboard-1.8.2.jar&lt;/code&gt; （要求 Java8，且 8080 端口不能被占用），访问 http://localhost:8080/，账号密码均为 sentinel&lt;/p&gt;
&lt;p&gt;官网：https://sentinelguard.io&lt;/p&gt;
&lt;p&gt;下载地址：https://github.com/alibaba/Sentinel/releases&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;构建演示工程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-sentinel&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：sentinel.transport.port 端口配置会在应用对应的机器上启动一个 HTTP Server，该 Server 与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了 1 个限流规则，会把规则数据 Push 给 Server 接收，Server 再将规则注册到 Sentinel 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos 服务注册中心地址【需要启动Nacos8848】
    sentinel:
      transport:
        # 配置Sentinel dashboard地址
        dashboard: localhost:8080
        # 默认8719端口，假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

management:
  endpoints:
    web:
      exposure:
        include: &apos;*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableDiscoveryClient
@SpringBootApplication
public class SentinelMainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(SentinelMainApp8401.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;流量控制 Controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
public class FlowLimitController {
    @GetMapping(&quot;/testA&quot;)
    public String testA() {
        return &quot;------testA&quot;;
    }

    @GetMapping(&quot;/testB&quot;)
    public String testB() {
        return &quot;------testB&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sentinel 采用懒加载机制，需要先访问 http://localhost:8401/testA，控制台才能看到&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;流控规则&lt;/h4&gt;
&lt;p&gt;流量控制规则 FlowRule：同一个资源可以同时有多个限流规则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源名 resource：限流规则的作用对象，Demo 中为 testA&lt;/li&gt;
&lt;li&gt;针对资源 limitApp：针对调用者进行限流，默认为 default 代表不区分调用来源&lt;/li&gt;
&lt;li&gt;阈值类型 grade：QPS 或线程数模式&lt;/li&gt;
&lt;li&gt;单机阈值 count：限流阈值&lt;/li&gt;
&lt;li&gt;流控模式 strategy：调用关系限流策略
&lt;ul&gt;
&lt;li&gt;直接：资源本身达到限流条件直接限流&lt;/li&gt;
&lt;li&gt;关联：当关联的资源达到阈值时，限流自身&lt;/li&gt;
&lt;li&gt;链路：只记录指定链路上的流量，从入口资源进来的流量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;流控效果 controlBehavior：
&lt;ul&gt;
&lt;li&gt;快速失败：直接失败，抛出异常&lt;/li&gt;
&lt;li&gt;Warm Up：冷启动，根据 codeFactory（冷加载因子，默认 3）的值，从 count/codeFactory 开始缓慢增加，给系统预热时间&lt;/li&gt;
&lt;li&gt;排队等待：匀速排队，让请求以匀速的方式通过，阈值类型必须设置为 QPS，否则无效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Sentinel增加流控规则.png&quot; alt=&quot;Cloud-Sentinel增加流控规则&quot; style=&quot;zoom: 40%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;通过调用 &lt;code&gt;SystemRuleManager.loadRules()&lt;/code&gt; 方法来用硬编码的方式定义流量控制规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void initSystemProtectionRule() {
  List&amp;lt;SystemRule&amp;gt; rules = new ArrayList&amp;lt;&amp;gt;();
  SystemRule rule = new SystemRule();
  rule.setHighestSystemLoad(10);
  rules.add(rule);
  SystemRuleManager.loadRules(rules);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细内容参考文档：https://sentinelguard.io/zh-cn/docs/flow-control.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;降级熔断&lt;/h4&gt;
&lt;p&gt;Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时，对这个资源的调用进行限制，让请求快速失败，避免影响到其它的资源而导致级联错误。当资源被降级后，在接下来的降级时间窗口之内，对该资源的调用都自动熔断（默认行为是抛出 DegradeException）&lt;/p&gt;
&lt;p&gt;Sentinel 提供以下几种熔断策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源名 resource：限流规则的作用对象，Demo 中为 testA&lt;/li&gt;
&lt;li&gt;熔断策略 grade：
&lt;ul&gt;
&lt;li&gt;慢调用比例（SLOW_REQUEST_RATIO）：以慢调用比例作为阈值，需要设置允许的慢调用 RT（即最大的响应时间），请求的响应时间大于该值则统计为慢调用。当单位统计时长（statIntervalMs）内请求数目大于设置的最小请求数目，并且慢调用的比例大于阈值，则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态（HALF-OPEN 状态），若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断，若大于设置的慢调用 RT 则会再次被熔断&lt;/li&gt;
&lt;li&gt;异常比例（ERROR_RATIO）：当单位统计时长内请求数目大于设置的最小请求数目，并且异常的比例大于阈值，则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态，若接下来的一个请求成功完成（没有错误）则结束熔断，否则会再次被熔断。异常比率的阈值范围是 &lt;code&gt;[0.0, 1.0]&lt;/code&gt;，代表 0% - 100%&lt;/li&gt;
&lt;li&gt;异常数 （ERROR_COUNT）：当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态，若接下来的一个请求成功完成（没有错误）则结束熔断，否则会再次被熔断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单机阈值 count：慢调用比例模式下为慢调用临界 RT；异常比例/异常数模式下为对应的阈值&lt;/li&gt;
&lt;li&gt;熔断时长 timeWindow：单位为 s&lt;/li&gt;
&lt;li&gt;最小请求数 minRequestAmount：熔断触发的最小请求数，请求数小于该值时即使异常比率超出阈值也不会熔断，默认 5&lt;/li&gt;
&lt;li&gt;统计时长 statIntervalMs：单位统计时长&lt;/li&gt;
&lt;li&gt;慢调用比例阈值 slowRatioThreshold：仅慢调用比例模式有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Sentinel增加熔断规则.png&quot; alt=&quot;Cloud-Sentinel增加熔断规则&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;注意异常降级仅针对业务异常，对 Sentinel 限流降级本身的异常 BlockException 不生效，为了统计异常比例或异常数，需要通过 &lt;code&gt;Tracer.trace(ex)&lt;/code&gt; 记录业务异常或者通过&lt;code&gt;@SentinelResource&lt;/code&gt; 注解会自动统计业务异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entry entry = null;
try {
  entry = SphU.entry(resource);

  // Write your biz code here.
  // &amp;lt;&amp;lt;BIZ CODE&amp;gt;&amp;gt;
} catch (Throwable t) {
  if (!BlockException.isBlockException(t)) {
    Tracer.trace(t);
  }
} finally {
  if (entry != null) {
    entry.exit();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细内容参考文档：https://sentinelguard.io/zh-cn/docs/circuit-breaking.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;热点限流&lt;/h4&gt;
&lt;p&gt;热点参数限流会统计传入参数中的热点参数，并根据配置的限流阈值与模式，对包含热点参数的资源调用进行限流，Sentinel 利用 LRU 策略统计最近最常访问的热点参数，结合令牌桶算法来进行参数级别的流控&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Sentinel热点参数限流.png&quot; alt=&quot;Cloud-Sentinel热点参数限流&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;引入 @SentinelResource 注解：https://sentinelguard.io/zh-cn/docs/annotation-support.html&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;value：Sentinel 资源名，默认为请求路径，这里 value 的值可以任意写，但是约定与 Restful 地址一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;blockHandler：表示触发了 Sentinel 中配置的流控规则，就会调用兜底方法 &lt;code&gt;del_testHotKey&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;blockHandlerClass：如果设置了该值，就会去该类中寻找 blockHandler 方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fallback：用于在抛出异常的时候提供 fallback 处理逻辑&lt;/p&gt;
&lt;p&gt;说明：fallback 对应服务降级（服务出错了需要有个兜底方法），blockHandler 对应服务熔断（服务不可用的兜底方法）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/testHotKey&quot;)
@SentinelResource(value = &quot;testHotKey&quot;, blockHandler = &quot;del_testHotKey&quot;)
public String testHotkey(@RequestParam(value = &quot;p1&quot;, required = false) String p1,
                         @RequestParam(value = &quot;p1&quot;, required = false) String p2) {
    return &quot;------testHotkey&quot;;
}

// 自定义的兜底方法，必须是 BlockException
public String del_testHotKey(String p1, String p2, BlockException e) {
    return &quot;不用默认的兜底提示 Blocked by Sentinel(flow limiting)，自定义提示：del_testHotKeyo.&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图示设置 p1 参数限流，规则是 1s 访问 1 次，当 p1=5 时 QPS &amp;gt; 100，只访问 p2 不会出现限流 &lt;code&gt;http://localhost:8401/testHotKey?p2=b&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Sentinel增加热点规则.png&quot; alt=&quot;Cloud-Sentinel增加热点规则&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数索引 paramIdx：热点参数的索引，图中索引 0 对应方法中的 p1 参数&lt;/li&gt;
&lt;li&gt;参数例外项 paramFlowItemList：针对指定的参数值单独设置限流阈值，不受 count 阈值的限制，&lt;strong&gt;仅支持基本类型和字符串类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说明：@SentinelResource 只管控制台配置规则问题，出现运行时异常依然会报错&lt;/p&gt;
&lt;p&gt;详细内容参考文档：https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;系统规则&lt;/h4&gt;
&lt;p&gt;Sentinel 系统自适应保护从整体维度对&lt;strong&gt;应用入口流量&lt;/strong&gt;进行控制，让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性&lt;/p&gt;
&lt;p&gt;系统规则支持以下的阈值类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load（仅对 Linux/Unix-like 机器生效）：当系统 load1 超过阈值，且系统当前的并发线程数超过系统容量时才会触发系统保护，系统容量由系统的 &lt;code&gt;maxQps * minRt&lt;/code&gt; 计算得出，设定参考值一般是 &lt;code&gt;CPU cores * 2.5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;RT：当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护，单位是毫秒&lt;/li&gt;
&lt;li&gt;线程数：当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护&lt;/li&gt;
&lt;li&gt;入口 QPS：当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护&lt;/li&gt;
&lt;li&gt;CPU usage：当系统 CPU 使用率超过阈值即触发系统保护（取值范围 0.0-1.0）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Sentinel增加系统规则.png&quot; alt=&quot;Cloud-Sentinel增加系统规则&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;详细内容参考文档：https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;服务调用&lt;/h4&gt;
&lt;p&gt;消费者需要进行服务调用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：激活 Sentinel 对 Feign 的支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feign:
  sentinel:
    enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主启动类：加上 @EnableFeignClient 注解开启 OpenFeign&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指明调用失败的兜底方法在PaymentFallbackService，使用 fallback 方式是无法获取异常信息的，
// 如果想要获取异常信息，可以使用 fallbackFactory 参数
@FeignClient(value = &quot;nacos-payment-provider&quot;, fallback = PaymentFallbackService.class)
public interface PaymentFeignService {
    // 去生产则服务中找相应的接口，方法签名一定要和生产者中 controller 的一致
    @GetMapping(value = &quot;/paymentSQL/{id}&quot;)
    public CommonResult&amp;lt;Payment&amp;gt; paymentSQL(@PathVariable(&quot;id&quot;) Long id);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component   //不要忘记注解，降级方法
public class PaymentFallbackService implements PaymentFeignService {
    @Override
    public CommonResult&amp;lt;Payment&amp;gt; paymentSQL(Long id) {
        return new CommonResult&amp;lt;&amp;gt;(444,&quot;服务降级返回,没有该流水信息-------PaymentFallbackSe
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;持久化&lt;/h4&gt;
&lt;p&gt;配置持久化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.csp&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;sentinel-datasource-nacos&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 Nacos 数据源配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      # 关闭默认收敛所有URL的入口context，不然链路限流不生效
      web-context-unify: false
      # filter:
        # enabled: false  # 关闭自动收敛

      #持久化配置
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Seata&lt;/h3&gt;
&lt;h4&gt;分布事物&lt;/h4&gt;
&lt;p&gt;一个分布式事务过程，可以用分布式处理过程的一 ID + 三组件模型来描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;XID (Transaction ID)：全局唯一的事务 ID，在这个事务ID下的所有事务会被统一控制&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TC (Transaction Coordinator)：事务协调者，维护全局和分支事务的状态，驱动全局事务提交或回滚&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TM (Transaction Manager)：事务管理器，定义全局事务的范围，开始全局事务、提交或回滚全局事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RM (Resource Manager)：资源管理器，管理分支事务处理的资源，与 TC 交谈以注册分支事务和报告分支事务的状态，并驱动分支事务提交或回滚&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型的分布式事务流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TM 向 TC 申请开启一个全局事务，全局事务创建成功并生成一个全局唯一的 XID&lt;/li&gt;
&lt;li&gt;XID 在微服务调用链路的上下文中传播（也就是在多个 TM，RM 中传播）&lt;/li&gt;
&lt;li&gt;RM 向 TC 注册分支事务，将其纳入 XID 对应全局事务的管辖&lt;/li&gt;
&lt;li&gt;TM 向 TC 发起针对 XID 的全局提交或回滚决议&lt;/li&gt;
&lt;li&gt;TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-分布式事务流程.png&quot; alt=&quot;Cloud-分布式事务流程&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本配置&lt;/h4&gt;
&lt;p&gt;Seata 是一款开源的分布式事务解决方案，致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式，为用户打造一站式的分布式解决方案&lt;/p&gt;
&lt;p&gt;下载 seata-server 文件修改 conf 目录下的配置文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file.conf：自定义事务组名称、事务日志存储模式为 db、数据库连接信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;事务分组&lt;/strong&gt;：seata 的资源逻辑，可以按微服务的需要，在应用程序（客户端）对自行定义事务分组，每组取一个名字&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Seata配置文件.png&quot; alt=&quot;Cloud-Seata配置文件&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据库新建库 seata，建表 db_store.sql 在 https://github.com/seata/seata/tree/2.x/script/server/db 目录里面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;registry.conf：指明注册中心为 Nacos，及修改 Nacos 连接信息&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Seata注册中心配置.png&quot; alt=&quot;Cloud-Seata注册中心配置&quot; style=&quot;zoom: 71%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动 Nacos 和 Seata，如果 DB 报错，需要把将 lib 文件夹下 mysql-connector-java-5.1.30.jar 删除，替换为自己 MySQL 连接器版本&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Seata配置成功.png&quot; alt=&quot;Cloud-Seata配置成功&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;官网：https://seata.io&lt;/p&gt;
&lt;p&gt;下载地址：https://github.com/seata/seata/releases&lt;/p&gt;
&lt;p&gt;基本介绍：https://seata.io/zh-cn/docs/overview/what-is-seata.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;两个注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 提供的本地事务：@Transactional&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Seata 提供的全局事务：&lt;strong&gt;@GlobalTransactional&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;搭建简单 Demo：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建 UNDO_LOG 表：SEATA AT 模式需要 &lt;code&gt;UNDO_LOG&lt;/code&gt; 表，如果一个模块的事务提交了，Seata 会把提交了哪些数据记录到 undo_log 表中，如果这时 TC 通知全局事务回滚，那么 RM 就从 undo_log 表中获取之前修改了哪些资源，并根据这个表回滚&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入 pom 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-seata&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${spring-cloud-alibaba.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        # 自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应
        # vgroup_mapping.my_test_tx_group = &quot;my_group&quot;
        tx-service-group: my_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&amp;amp;characterEncoding=UTF-8&amp;amp;useSSL=false&amp;amp;serverTimezone=UTC
    username: root
    password: 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构建三个服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 仓储服务
public interface StorageService {
    // 扣除存储数量
    void deduct(String commodityCode, int count);
}

// 订单服务
public interface OrderService {
	// 创建订单
    Order create(String userId, String commodityCode, int orderCount);
}

// 帐户服务
public interface AccountService {
    // 从用户账户中借出
    void debit(String userId, int money);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;业务逻辑：增加 @GlobalTransactional 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {
	@Resource
    private OrderDAO orderDAO;
	@Resource
    private AccountService accountService;
    
	@Transactional(rollbackFor = Exception.class)
    public Order create(String userId, String commodityCode, int orderCount) {
        int orderMoney = calculate(commodityCode, orderCount);
		// 账户扣钱
        accountService.debit(userId, orderMoney);

        Order order = new Order();
        order.userId = userId;
        order.commodityCode = commodityCode;
        order.count = orderCount;
        order.money = orderMoney;

        return orderDAO.insert(order);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class BusinessServiceImpl implements BusinessService {
	@Resource
    private StorageService storageService;
	@Resource
    private OrderService orderService;

    // 采购，涉及多服务的分布式事务问题
    @GlobalTransactional
    @Transactional(rollbackFor = Exception.class)
    public void purchase(String userId, String commodityCode, int orderCount) {
        storageService.deduct(commodityCode, orderCount);
        orderService.create(userId, commodityCode, orderCount);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;详细示例参考：https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata&lt;/p&gt;
</content:encoded></item><item><title>TCP/IP 四层模型</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/tcp%E5%9B%9B%E5%B1%82%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/tcp%E5%9B%9B%E5%B1%82%E6%A8%A1%E5%9E%8B/</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/11abnz9-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;TCP/IP 四层模型&lt;/h1&gt;
&lt;p&gt;TCP/IP 四层模型是一个分层网络通信模型，将网络通信过程分为四个层次，这四层分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络接口层 以太网，WIFI,PPP&lt;/li&gt;
&lt;li&gt;网络层 IP,ICMP,ARP&lt;/li&gt;
&lt;li&gt;传输层 TCP,UDP&lt;/li&gt;
&lt;li&gt;应用层 HTTP,FTP,SMTP
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/119q2ig-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SpringMVC工作流程</title><link>https://blog.meowrain.cn/posts/java/spring/springmvc%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/springmvc%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B/</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ovqrfw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/oz4od3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Spring中拦截器和过滤器的区别</title><link>https://blog.meowrain.cn/posts/java/spring/spring%E4%B8%AD%E6%8B%A6%E6%88%AA%E5%99%A8%E5%92%8C%E8%BF%87%E6%BB%A4%E5%99%A8%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/spring%E4%B8%AD%E6%8B%A6%E6%88%AA%E5%99%A8%E5%92%8C%E8%BF%87%E6%BB%A4%E5%99%A8%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://www.mianshiya.com/question/1907425766060380162#heading-0
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/p1p5sy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;过滤器&lt;/h1&gt;
&lt;h2&gt;实现机制&lt;/h2&gt;
&lt;p&gt;过滤器是Servlet规范的一部分，独立于Spring存在，主要用于过滤请求和响应，可以对所有类型的请求进行处理。&lt;/p&gt;
&lt;h2&gt;使用范围&lt;/h2&gt;
&lt;p&gt;可以过滤所有的请求，包括静态资源,jsp,html等，因为它在Servlet容器层面生效。&lt;/p&gt;
&lt;h2&gt;配置方法&lt;/h2&gt;
&lt;p&gt;需要实现Filter接口，通过标准的Servlet配置方式进行注册：
https://www.cnblogs.com/xfeiyun/p/15790555.html&lt;/p&gt;
&lt;p&gt;https://juejin.cn/post/7000950677409103880&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nk3hly-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nker0x-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nkq0yb-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nl2e0e-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nq0cv2-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nq24rg-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nq3xj5-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nqp845-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nqzx04-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/nr35ne-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;执行顺序&lt;/h2&gt;
&lt;p&gt;先于拦截器执行，因为过滤器作用于Servlet容器层面，拦截器作用在Spring MVC 的处理器映射器找到控制器前或者后执行。&lt;/p&gt;
&lt;h2&gt;功能侧重&lt;/h2&gt;
&lt;p&gt;侧重于过滤请求和响应的内容，比如设置编码格式，安全控制等。&lt;/p&gt;
&lt;h1&gt;拦截器&lt;/h1&gt;
&lt;h2&gt;实现机制&lt;/h2&gt;
&lt;p&gt;拦截器是Spring框架的一部分，基于Java的反射机制实现，主要针对的是Handler的调用&lt;/p&gt;
&lt;h2&gt;使用范围&lt;/h2&gt;
&lt;p&gt;主要用于拦截访问DispathcherServlet的请求，通常只适用于Spring MVC的应用程序中的请求处理方法。&lt;/p&gt;
&lt;h2&gt;配置方式&lt;/h2&gt;
&lt;p&gt;需要实现org.springframework.web.servlet.HandlerInterceptor接口，并在Spring配置文件中进行注册。
可以通过实现WebMvcConfigurer接口的addInterceptors方法来进行注册。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/p51up4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * @author wipe
 * @version 1.0
 */

public class MyInterceptor1 implements HandlerInterceptor {

    @Override//目标资源方法执行前执行。 返回true：放行    返回false：不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println(&quot;MyInterceptor1 ... preHandle&quot;);
        return true;
    }

    @Override//目标资源方法执行后执行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(&quot;MyInterceptor1 ... postHandle&quot;);
    }

    @Override//视图渲染完毕后执行，最后执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(&quot;MyInterceptor1 ... afterCompletion&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.config;

import com.example.filter.MyFilter1;
import com.example.filter.MyFilter2;
import com.example.interceptor.MyInterceptor1;
import com.example.interceptor.MyInterceptor2;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @author wipe
 * @version 1.0
 */
@Configuration
public class MyConfig implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器，并指定执行顺序，也可以通过将拦截器声明成 bean 对象，然后通过 @Order 注解或者实现 Order 接口指定执行顺序
        registry.addInterceptor(new MyInterceptor1()).order(1);
        registry.addInterceptor(new MyInterceptor2()).order(2);
    }


    @Bean// 这样配置可以指定过滤器的执行顺序
    public FilterRegistrationBean&amp;lt;MyFilter1&amp;gt; myFilter1() {
        FilterRegistrationBean&amp;lt;MyFilter1&amp;gt; filter = new FilterRegistrationBean&amp;lt;&amp;gt;();
        filter.setFilter(new MyFilter1());
        filter.addUrlPatterns(&quot;/*&quot;);
        filter.setOrder(1);
        return filter;
    }

    @Bean
    public FilterRegistrationBean&amp;lt;MyFilter2&amp;gt; myFilter2() {
        FilterRegistrationBean&amp;lt;MyFilter2&amp;gt; filter = new FilterRegistrationBean&amp;lt;&amp;gt;();
        filter.setFilter(new MyFilter2());
        filter.addUrlPatterns(&quot;/*&quot;);
        filter.setOrder(2);
        return filter;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接用@Component注册Interceptor&lt;/p&gt;
&lt;h2&gt;执行顺序&lt;/h2&gt;
&lt;p&gt;可以指定多个拦截器之间的执行顺序，通过实现Ordered接口或者使用@Order注解来控制。&lt;/p&gt;
&lt;h2&gt;功能侧重&lt;/h2&gt;
&lt;p&gt;侧重于业务逻辑的前置检查，权限验证，日志记录等。&lt;/p&gt;
</content:encoded></item><item><title>排序算法</title><link>https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;冒泡排序&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/12fnqrb-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;算法复杂度： O(n^2)
稳定性： 是稳定排序算法，相等的元素不会变换位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;
func main() {
    var n int
    fmt.Scan(&amp;amp;n)
    arr := make([]int,n)
    for i:= range arr {
        fmt.Scan(&amp;amp;arr[i])
    }
    bubbleSort(arr)
    for i,v := range arr {
        if i &amp;gt; 0 {
            fmt.Print(&quot; &quot;)
        }
        fmt.Print(v)
    }
    
}
func bubbleSort(arr []int) {
    n := len(arr)
    for i:=0;i&amp;lt;n-1;i++ {
        for j:=0;j&amp;lt;n-1-i;j++ {
            if arr[j] &amp;gt; arr[j + 1] {
                arr[j],arr[j + 1] = arr[j + 1],arr[j]
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr = new int[n];
        for(int i = 0;i&amp;lt;n;i++) {
            arr[i] = sc.nextInt();
        }
        bubbleSort(arr);
        for(int i = 0;i&amp;lt;n;i++) {
            System.out.print(arr[i] + &quot; &quot;);
        }
        
    }
    static void bubbleSort(int[] arr) {
        int n = arr.length;
        for(int i = 0;i&amp;lt;n - 1;i++) {
            for(int j = 0;j&amp;lt;n - 1 - i;j++) {
                int temp = arr[j];
                if(arr[j + 1] &amp;lt; arr[j]) {
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;快速排序&lt;/h1&gt;
&lt;h2&gt;时间复杂度&lt;/h2&gt;
&lt;p&gt;平均时间复杂度： O(nlogn)
最坏时间复杂度： O(n^2) 在已经排序或者大部分已排序的情况下
最好时间复杂度： O(nlogn)&lt;/p&gt;
&lt;h2&gt;空间复杂度&lt;/h2&gt;
&lt;p&gt;是原地排序，只占用少量的额外空间，空间复杂度是O(logn)，主要是递归调用栈的空间&lt;/p&gt;
&lt;h2&gt;稳定性&lt;/h2&gt;
&lt;p&gt;快速排序是 不稳定 排序。&lt;/p&gt;
&lt;p&gt;采用分治策略排列元素，把数组分成两个子数组，递归排序，再合并。&lt;/p&gt;
&lt;p&gt;分区的基本思想
分区的目标：选择一个基准元素（pivot），将数组重新排列，使得：&lt;/p&gt;
&lt;h2&gt;分区思想&lt;/h2&gt;
&lt;p&gt;基准左边的所有元素 &amp;lt;= 基准
基准右边的所有元素 &amp;gt;= 基准
基准本身位于最终的正确位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;
func main() {
    var n int
    fmt.Scan(&amp;amp;n)
    arr := make([]int,n)
    for i:= range arr {
        fmt.Scan(&amp;amp;arr[i])
    }
    quickSort(arr,0,len(arr) - 1)
}
func quickSort(arr []int,low int,high int) {
    if low &amp;lt; high {
        pi := partition(arr,low,high)
        quickSort(arr,low,pi - 1)
        quickSort(arr,pi + 1,high)
    }
}
func partition(arr []int,low int,high int) int {
    pivot := arr[high]
    i := low - 1
    for j := low;j &amp;lt; high;j++ {
        if arr[j] &amp;lt;= pivot {
            i++
            arr[i],arr[j] = arr[j],arr[i]
        }
    }
    arr[i + 1],arr[high] = arr[high],arr[i + 1]
    return i + 1
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>HTTP中GET和POST的区别是什么</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/http%E4%B8%ADget%E5%92%8Cpost%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/http%E4%B8%ADget%E5%92%8Cpost%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;HTTP中GET和POST的区别是什么&lt;/h1&gt;
&lt;h2&gt;从HTTP定义看&lt;/h2&gt;
&lt;p&gt;从HTTP定义看
GET用来获取资源。通常用来请求数据，不改变服务器状态
POST用来提交数据到服务器，通常会改变服务器状态或者产生副作用（比如创建或者更新资源）&lt;/p&gt;
&lt;h2&gt;参数传递&lt;/h2&gt;
&lt;p&gt;GET是通过URL拼接来实现参数传递的，暴露在请求URL中，有可见性，长度有限。（2048字节）
POST把参数放在请求体里面，通常不可见而且长度理论上也没有限制，更适合传递大量数据（nginx默认限制为1M）。&lt;/p&gt;
&lt;h2&gt;安全性&lt;/h2&gt;
&lt;p&gt;GET： 参数可见，数据容易暴露在浏览器历史记录，日志和缓存中，不适合传递敏感信息。
POST： 数据放在请求体中，相对安全，但需要HTTPS才能保证数据加密传输。&lt;/p&gt;
&lt;h2&gt;幂等性&lt;/h2&gt;
&lt;p&gt;GET: 幂等的（重复请求不会改变服务器状态）
POST: 非幂等的（多次请求可能导致重复创建资源或者执行多次相同的操作）&lt;/p&gt;
&lt;h2&gt;RESTful API设计中的角色分工&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;GET 用来查询或者检索资源数据&lt;/li&gt;
&lt;li&gt;POST： 用来创建资源或者执行某些动作&lt;/li&gt;
&lt;li&gt;PUT和PATCH： 用来更新资源 PUT替换整个资源，PATCH更新部分资源&lt;/li&gt;
&lt;li&gt;DELETE: 用来删除资源。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP请求中包含哪些内容请求头和请求体有哪些内容</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/http%E8%AF%B7%E6%B1%82%E4%B8%AD%E5%8C%85%E5%90%AB%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9%E8%AF%B7%E6%B1%82%E5%A4%B4%E5%92%8C%E8%AF%B7%E6%B1%82%E4%BD%93%E6%9C%89%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/http%E8%AF%B7%E6%B1%82%E4%B8%AD%E5%8C%85%E5%90%AB%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9%E8%AF%B7%E6%B1%82%E5%A4%B4%E5%92%8C%E8%AF%B7%E6%B1%82%E4%BD%93%E6%9C%89%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;HTTP请求由以下几部分组成：&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;请求行（请求方法，请求资源路径，HTTP协议版本）&lt;/li&gt;
&lt;li&gt;请求头&lt;/li&gt;
&lt;li&gt;空行&lt;/li&gt;
&lt;li&gt;请求体&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;常见请求头与请求体&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/uduata-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/uab784-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/ua8smj-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/u993u3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;URI URL URN&lt;/h1&gt;
&lt;p&gt;URI&lt;br /&gt;
├─ URL（通过“位置+协议”定位资源）&lt;br /&gt;
└─ URN（通过“唯一名称”标识资源，与位置无关）&lt;/p&gt;
</content:encoded></item><item><title>常见HTTP状态码</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E5%B8%B8%E8%A7%81http%E7%8A%B6%E6%80%81%E7%A0%81/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E5%B8%B8%E8%A7%81http%E7%8A%B6%E6%80%81%E7%A0%81/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1xx信息响应&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;100 Continue: 服务器已接收请求的初步部分，客户端应该继续请求&lt;/li&gt;
&lt;li&gt;101 Switching Protocols: 服务器同意切换协议，比如从HTTP切换到Webscoket&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2xx信息响应&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;200 OK: 请求成功，服务器返回请求的资源或者数据&lt;/li&gt;
&lt;li&gt;201 Created： 请求成功并创建了新资源，常用于POST请求&lt;/li&gt;
&lt;li&gt;204 No Content: 请求成功但服务器不返回任何信息，常用于删除操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;3xx 重定向&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;301 Moved Permanently: 资源已永久移动到新的URL，客户端应该用新URL访问。&lt;/li&gt;
&lt;li&gt;302 Found: 资源临时移动到新的URL，客户端应该继续使用原来的URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见重定向机制&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/ukxf35-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Cookie和Session的区别</title><link>https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/cookie%E5%92%8Csession%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/cookie%E5%92%8Csession%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Cookie&lt;/h1&gt;
&lt;p&gt;Cookie是存储在用户浏览器端的一个小型数据文件，用于跟踪和保护用户的状态信息。
主要用于保持用户的登录状态，跟踪用户行为，存储用户偏好等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;是存储在浏览器的&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么需要Cookie&lt;/h2&gt;
&lt;p&gt;HTTP协议是无状态的，意味着每次请求都是独立的，服务器不会记住用户之前的行为。&lt;/p&gt;
&lt;p&gt;用户登录后，服务器无法知道后续请求是否来自同一个用户。&lt;/p&gt;
&lt;p&gt;就像你登录了一个电商网站（如淘宝），假如 HTTP是无状态的，那么你每次刷新页面或跳转到其他页面时，系统都会提示“请重新登录”，这是因为服务器无法记住你之前的登录状态。
而通过Cookie，服务器在用户登录成功后设置一个标识。浏览器会将这个标识保存下来，并在后续请求中自动附加到请求头中，服务器通过解析这个标识就能知道用户已登录。&lt;/p&gt;
&lt;p&gt;浏览器在每次HTTP请求中，自动携带Cookie信息，服务器通过解析这些信息识别用户身份。虽然HTTP协议本身无状态，但通过Cookie的“附加行为”，实现了有状态的交互。&lt;/p&gt;
&lt;h2&gt;如何设置Cookie&lt;/h2&gt;
&lt;h3&gt;SpringBoot&lt;/h3&gt;
&lt;h4&gt;在 Controller 中设置 Cookie&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CookieController {

    @GetMapping(&quot;/set-cookie&quot;)
    public String setCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie(&quot;token&quot;, &quot;abc123&quot;);
        cookie.setPath(&quot;/&quot;);          // 设置路径（整个项目可用）
        cookie.setMaxAge(60 * 60);    // 有效期 1 小时
        cookie.setHttpOnly(true);     // 防止 JS 读取，提升安全性
        cookie.setSecure(false);      // true 代表仅 https 传输
        response.addCookie(cookie);   // 写入响应头

        return &quot;Cookie 已设置！&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;在 Response Header 中手动写入 Cookie&lt;/h4&gt;
&lt;p&gt;有时需要更细控制，可以直接操作 Set-Cookie 响应头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/set-cookie-header&quot;)
public String setCookieHeader(HttpServletResponse response) {
    response.addHeader(&quot;Set-Cookie&quot;, &quot;sessionId=xyz789; Path=/; HttpOnly; Max-Age=3600&quot;);
    return &quot;Header Cookie 已设置！&quot;;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读取 Cookie&lt;/h4&gt;
&lt;p&gt;使用 @CookieValue：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/get-cookie&quot;)
public String getCookie(@CookieValue(value = &quot;token&quot;, defaultValue = &quot;null&quot;) String token) {
    return &quot;当前 token = &quot; + token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/get-cookie2&quot;)
public String getCookie2(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie c : cookies) {
            if (&quot;token&quot;.equals(c.getName())) {
                return &quot;找到 token: &quot; + c.getValue();
            }
        }
    }
    return &quot;未找到 token&quot;;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cookie组成&lt;/h3&gt;
&lt;p&gt;用户可通过 f12打开控制台，然后应用-&amp;gt;Cookie-&amp;gt;找到当前地址，就能看到当前的Cookie 信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xev89u-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;key=value&lt;/code&gt;: 核心数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set-Cookie&lt;/code&gt;字段： 服务器通过这个字段定义Cookie的内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Path=/&lt;/code&gt;： 指定Cookie的而作用路径（/表示所有路径均可访问）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpOnly&lt;/code&gt;: 防止JavaScript 访问Cookie（提高安全性）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Secure&lt;/code&gt;: 要求Cookie仅能通过HTTPS传输&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Max-Age&lt;/code&gt;: 设置Cookie在多久后过期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expires&lt;/code&gt;： 设置Cookie在指定时间过期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Domain&lt;/code&gt;： 作用域，指定Cookie所属的域名，允许跨子域名共享&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Max-Age&lt;/strong&gt; 和&lt;strong&gt;Expires&lt;/strong&gt;是设置Cookie过期时间的两种方式。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x7ivoo-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x7kr9o-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xfy5w7-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xfwwfm-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xfutn8-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xg132o-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xg1u42-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xg3flm-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Cookie按照生命周期分为两种&lt;/h2&gt;
&lt;p&gt;会话期cookie是最简单的cookie：浏览器关闭后会被自动删除。会话期cookie不需要指定过期时间（Expires）或者有效期（Max-Age)。需要注意的是，有些浏览器提供了会话恢复功能，这种情况即使关闭了浏览器，会话期cookie也会被保留下来，这会导致cookie的生命周期无限期延长
持久性cookie的生命周期取决于过期时间（Expires）或者有效期（Max-Age）指定的一段时间。
当Cookie的过期时间被设定时，设定的日期和时间只与客户端相关，而不是服务端。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x6dk9p-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x6ftip-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x6htww-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Cookie过期时间配置&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Max-Age&lt;/strong&gt; 和&lt;strong&gt;Expires&lt;/strong&gt;是设置Cookie过期时间的两种方式。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x7ivoo-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/x7kr9o-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;两者都用于设置“持久级 Cookie”（会话级 Cookie 不设置它们）
同时存在时，Max-Age 优先&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Cookie大小限制&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单个Cookie最大约为4KB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cookie数量限制&lt;/h2&gt;
&lt;p&gt;浏览器对同一域名的Cookie数量有限制（如Chrome为200个）&lt;/p&gt;
&lt;h2&gt;安全建议：&lt;/h2&gt;
&lt;p&gt;永远不要在Cookie中存储敏感信息（如密码），改用Token或Session ID。
始终启用HttpOnly和Secure，防止XSS和中间人攻击。&lt;/p&gt;
&lt;h2&gt;Cookie优缺点和注意事项&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/xhd3rc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Session&lt;/h1&gt;
&lt;p&gt;session是另一种记录客户状态的机制，不同的是Cookie保存在客户端浏览器中，而session保存在服务器上,客户端浏览器访问服务器的时候，服务器把客户端信息以某种形式记录在服务器上，这就是session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了;&lt;/p&gt;
&lt;h2&gt;安全性&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ia3jrw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Session 的工作流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/i8sx9o-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;大小&lt;/h2&gt;
&lt;p&gt;Session是无大小限制的&lt;/p&gt;
&lt;h2&gt;缺陷&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ib38t6-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ib9eq3-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ib4quq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Cookie和Session的区别&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ibdvwc-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/22/ibpwz6-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>常见开发模型</title><link>https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/%E5%B8%B8%E8%A7%81%E5%BC%80%E5%8F%91%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/%E5%B8%B8%E8%A7%81%E5%BC%80%E5%8F%91%E6%A8%A1%E5%9E%8B/</guid><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/21/wedsm1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Git不小心提交了100M以上文件导致无法提交解决方案</title><link>https://blog.meowrain.cn/posts/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/git%E4%B8%8D%E5%B0%8F%E5%BF%83%E6%8F%90%E4%BA%A4%E4%BA%86100m%E4%BB%A5%E4%B8%8A%E6%96%87%E4%BB%B6%E5%AF%BC%E8%87%B4%E6%97%A0%E6%B3%95%E6%8F%90%E4%BA%A4%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/git%E4%B8%8D%E5%B0%8F%E5%BF%83%E6%8F%90%E4%BA%A4%E4%BA%86100m%E4%BB%A5%E4%B8%8A%E6%96%87%E4%BB%B6%E5%AF%BC%E8%87%B4%E6%97%A0%E6%B3%95%E6%8F%90%E4%BA%A4%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</guid><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10gf9ih-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10h4100-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10hjla3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10hnekj-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10i2nlq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以直接用这个Scala写的bfg,点击下载:
https://blog.meowrain.cn/web/bfg-1.15.0.jar&lt;/p&gt;
&lt;p&gt;https://github.com/rtyley/bfg-repo-cleaner&lt;/p&gt;
&lt;p&gt;文档： https://rtyley.github.io/bfg-repo-cleaner/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10gsu60-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/20/10i6pio-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>常见设计模式</title><link>https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</guid><pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/18/119exsd-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;策略模式&lt;/h1&gt;
&lt;p&gt;现实生活中我们到商场买东西的时候，卖场往往根据不同的客户制定不同的报价策略，比如针对新客户不打折扣，针对老客户打9折，针对VIP客户打8折...&lt;/p&gt;
&lt;p&gt;现在我们要做一个报价管理的模块，简要点就是要针对不同的客户，提供不同的折扣报价。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.strategy;

import java.math.BigDecimal;

public class QuoteManager {
    public BigDecimal quote(BigDecimal originalPrice, String customType){
        if (&quot;新客户&quot;.equals(customType)) {
            System.out.println(&quot;抱歉！新客户没有折扣！&quot;);
            return originalPrice;
        }else if (&quot;老客户&quot;.equals(customType)) {
            System.out.println(&quot;恭喜你！老客户打9折！&quot;);
            originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);
            return originalPrice;
        }else if(&quot;VIP客户&quot;.equals(customType)){
            System.out.println(&quot;恭喜你！VIP客户打8折！&quot;);
            originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
            return originalPrice;
        }
        //其他人员都是原价
        return originalPrice;
    }

}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Redis主从复制的实现原理是什么</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88/</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;为什么需要主从复制&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;降低数据冗余&lt;/li&gt;
&lt;li&gt;提高故障恢复&lt;/li&gt;
&lt;li&gt;支持负载均衡&lt;/li&gt;
&lt;li&gt;高可用&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;主从库之间采用读写分离方式：&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/upct3j-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;两种同步方式&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;全量复制： 第一次同步的时候&lt;/li&gt;
&lt;li&gt;增量复制： 只会把主从库网络断联期间主库收到的命令同步给从库&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;全量复制&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/w9rfaa-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;增量复制&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/w9vq4z-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis持久化机制</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%8C%81%E4%B9%85%E5%8C%96%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%8C%81%E4%B9%85%E5%8C%96%E6%9C%BA%E5%88%B6/</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/umd7ep-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Redis 持久化机制&lt;/h1&gt;
&lt;h1&gt;Redis和Memcached的不同&lt;/h1&gt;
&lt;p&gt;Redis 不同于 Memcached 的很重要一点就是，Redis 支持持久化，而且支持 3 种持久化方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快照（snapshotting，RDB）&lt;/li&gt;
&lt;li&gt;只追加文件（append-only file, AOF）&lt;/li&gt;
&lt;li&gt;RDB 和 AOF 的混合持久化(Redis 4.0 新增)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis支持丰富的数据结构，Memcached仅仅支持简单的键值对存储，而且只能是字符串&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/02/22/EXaKzG1740216393513470290.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;RDB持久化&lt;/h1&gt;
&lt;p&gt;Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后，可以对快照进行备份，可以将快照复制到其他服务器从而创建具有相同数据的服务器副本（Redis 主从结构，主要用来提高 Redis 性能），还可以将快照留在原地以便重启服务器的时候使用。&lt;/p&gt;
&lt;p&gt;快照持久化是 Redis 默认采用的持久化方式，在 redis.conf 配置文件中默认有此下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;save 900 1           #在900秒(15分钟)之后，如果至少有1个key发生变化，Redis就会自动触发bgsave命令创建快照。

save 300 10          #在300秒(5分钟)之后，如果至少有10个key发生变化，Redis就会自动触发bgsave命令创建快照。

save 60 10000        #在60秒(1分钟)之后，如果至少有10000个key发生变化，Redis就会自动触发bgsave命令创建快照。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;RDB 创建快照时会阻塞主线程吗？&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/02/22/IzY7nu1740216454633699951.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;什么是 AOF 持久化？&lt;/h1&gt;
&lt;p&gt;与快照持久化相比，AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF（append only file）方式的持久化（Redis 6.0 之后已经默认是开启了），可以通过 appendonly 参数开启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;appendonly yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令，Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中，然后再写入到 AOF 文件中（此时还在系统内核缓存区未同步到磁盘），最后再根据持久化方式（ fsync策略）的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。&lt;/p&gt;
&lt;p&gt;只有同步到磁盘中才算持久化保存了，否则依然存在数据丢失的风险，比如说：系统内核缓存区的数据还未同步，磁盘机器就宕机了，那这部分数据就算丢失了。&lt;/p&gt;
&lt;p&gt;AOF 文件的保存位置和 RDB 文件的位置相同，都是通过 dir 参数设置的，默认的文件名是 appendonly.aof。&lt;/p&gt;
&lt;p&gt;AOF持久化功能的实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;命令追加&lt;/li&gt;
&lt;li&gt;文件写入 （写到内核缓冲区了，还没同步到硬盘）&lt;/li&gt;
&lt;li&gt;文件同步 （根据持久化方式向硬盘做同步操作）&lt;/li&gt;
&lt;li&gt;文件重写： 随着AOF文件越来越大，需要定期对AOF文件进行重写，达到压缩目的。&lt;/li&gt;
&lt;li&gt;重启加载： Redis重启的时候，可以加载AOF文件进行数据恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/02/22/XSEcK61740216646975756864.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;AOF 持久化方式有哪些？&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/02/22/DZSVso1740216693705553035.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;AOF 为什么是在执行完命令之后记录日志&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/02/22/3nEOWT1740216736909899485.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis数据过期后的删除策略</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E5%90%8E%E7%9A%84%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E5%90%8E%E7%9A%84%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5/</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/10egvdw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redis数据过期主要有两种删除策略&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/10hp6uw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;定期删除&lt;/h2&gt;
&lt;p&gt;Redis每隔一段时间会随机检查一定数量的键，如果发现过期的键，就把它删除。这种方式能够在后台持续清除过期数据，防止内存膨胀。&lt;/p&gt;
&lt;p&gt;缺点： CPU占用稍微有点儿大
优点：能及时清除过期的键，防止内存膨胀。&lt;/p&gt;
&lt;h2&gt;惰性删除&lt;/h2&gt;
&lt;p&gt;在每次访问键的时候，去看这个键是不是已经过期了，如果过期了就删除它。
这种策略保证了在使用过程中只删除不再需要的数据，但在不访问过期键的时候不会被清除。&lt;/p&gt;
&lt;p&gt;优点： 减少CPU占用
缺点： 如果一直没查到某个key，这个键就可能不会被删除，时间久了可能导致内存膨胀。&lt;/p&gt;
&lt;h1&gt;内存淘汰策略&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/10iw8w4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Redis键过期时间的设置&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/17/10f1n1j-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>如何解决Redis中热点key的问题</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3redis%E4%B8%AD%E7%83%AD%E7%82%B9key%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3redis%E4%B8%AD%E7%83%AD%E7%82%B9key%E7%9A%84%E9%97%AE%E9%A2%98/</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;如何解决Redis中热点key的问题&lt;/h1&gt;
&lt;p&gt;Redis中的热点Key问题指的是某些Key被频繁访问，导致Redis的压力过大，进而影响整体性能甚至导致集群节点故障。
解决热点Key问题的主要方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;热点Key拆分： 把热点数据分散到多个Key中，例如通过引入随机前缀，使得不同的用户请求能分散到多个Key，多个key分布在多实例中，避免几种访问单一key&lt;/li&gt;
&lt;li&gt;多级缓存： 在Redis前增加其他缓存层（比如CDN，本地缓存），来分担Redis的访问压力&lt;/li&gt;
&lt;li&gt;读写分离： 通过Redis主从复制，把读请求分发到多个从节点，减轻单节点压力&lt;/li&gt;
&lt;li&gt;限流和降级: 在热点Key访问过高的时候，应用限流策略，减少对Redis的请求，或者在必要的时候返回降级的数据或空值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/18/8d4y9-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/18/8or7m-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>说说TCP四次挥手</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/tcp%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/tcp%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded/></item><item><title>说说TCP拥塞控制步骤</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AF%B4%E8%AF%B4tcp%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AF%B4%E8%AF%B4tcp%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://www.bilibili.com/video/BV1z142197Kn/
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/t3lybk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/slhvr2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/slolf2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sm5ngj-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/smohqn-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/snv3yw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sp4xft-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sqx5id-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sv8wqb-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/svq261-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sxcx7d-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sy5ft1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/szf72t-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/szzqxu-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/t0f29d-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/t1894n-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/t1n0o7-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/tsjuiw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/tsyvbr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/tt6785-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/rcsxk4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;https://www.bilibili.com/video/BV1xC4y1S78T/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=f7d0ce024b059d57a0319d78217fa104&lt;/p&gt;
&lt;h1&gt;慢启动&lt;/h1&gt;
&lt;p&gt;发送方在连接建立初期，缓慢地增加数据发送速率。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s4e9am-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s8f7jc-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s8kg4t-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;拥塞避免&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s8m7zi-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s8x9i0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s90p0f-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s91r9m-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s942bz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s954fw-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s97ozk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;拥塞发生&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s996bt-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9j62w-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9k9xw-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9mcew-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9o1fm-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9pnms-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9qvtz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;快速恢复&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9tjrb-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/s9udvm-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sa4di7-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sa6axn-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sa858d-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/sa92ie-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/saaosr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>说说TCP的三次握手</title><link>https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AF%B4%E8%AF%B4tcp%E7%9A%84%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AF%B4%E8%AF%B4tcp%E7%9A%84%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;流程&lt;/h1&gt;
&lt;p&gt;客户端给服务端发送一个SYN（同步序列号消息）给服务器，服务器收到后回复一个SYN + ACK（同步序列编号-确认）消息，最后客户端再发送一个ACK(确认)消息确认服务器已经收到了SYN-ACK消息，从而完成三次握手，建立起可靠的TCP连接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/qkipdz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;为什么需要三次握手&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;避免历史错误连接的建立，减少通信双方不必要的资源消耗&lt;/li&gt;
&lt;li&gt;帮助通信双方同步初始化序列号&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;所以为什么三次能解决历史错误连接的问题？
网络情况可能比较复杂，发送方第一次发送请求后，可能由于网络原因被阻塞住了，这个时候发送方可能又会再次发送请求，如果说握手只有两次，那么接收方只能拒绝或者接受，但是无法分清请求是旧的还是新的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/qz2t5q-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/qzb8ro-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/r2w1yc-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/qnblw0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/r3b7la-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;为什么不是四次握手&lt;/h1&gt;
&lt;p&gt;中间的syn + ack把两步合并了，精简了连接过程。&lt;/p&gt;
</content:encoded></item><item><title>什么是循环依赖</title><link>https://blog.meowrain.cn/posts/java/spring/%E4%BB%80%E4%B9%88%E6%98%AF%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/%E4%BB%80%E4%B9%88%E6%98%AF%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是循环依赖&lt;/h1&gt;
&lt;p&gt;循环依赖就是指两个或者多个模块，类组件之间互相依赖，形成一个闭环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

//或者自己依赖自己
@Service
public class A {
    @Autowired
    private A a;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就像上面这种情况，就属于循环依赖&lt;/p&gt;
</content:encoded></item><item><title>Spring如何解决循环依赖</title><link>https://blog.meowrain.cn/posts/java/spring/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring如何解决循环依赖&lt;/h1&gt;
&lt;p&gt;关键是&lt;code&gt;提前暴露未完全创建完毕的Bean&lt;/code&gt;
Spring中采用了&lt;code&gt;三级缓存&lt;/code&gt;解决了循环依赖&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/p86lpo-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/p88uqg-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/p8o5kd-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们拿下面这个例子来讲&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

//或者自己依赖自己
@Service
public class A {
    @Autowired
    private A a;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先要创建Bean A,去一级缓存里面找，发现没有，二级缓存里面找，发现也没有，三级里面也没有
这个时候进入Bean A 的对象创建流程&lt;/p&gt;
&lt;p&gt;接下来我们利用反射创建对象A，调用其无参构造方法，创建一个对象A 的实例，并将其包装成ObjectFactory放入三级缓存中。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/p9xvzy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pa0adk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来要填充属性
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pac0j5-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为A对象的属性是B对象&lt;/p&gt;
&lt;p&gt;所以现在要开始创建Bean B
到一级，二级，三级缓存中找B对象，发现不存在，所以进入B对象的创建流程&lt;/p&gt;
&lt;p&gt;依然是通过反射，调用B的无参构造方法创建B的实例，并将其包装成ObjectFactory放入三级缓存中。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pb08w0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来要填充B对象的属性，就又要进入Bean A的创建流程中，再去缓存中查找A对象，我们能发现在三级缓存中已经有A的ObjectFactory了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pbu2du-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pc7z5d-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以从源码中看到，我们会调用存放在三级缓存中A的ObjectFactory的getObject方法，创建单例对象，存放在earlySingletonObjects里面（二级缓存），然后从三级缓存中移除A的ObjectFactory&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pdc4ej-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好的，这样的话，我们就可以把A填充到B对象需要的属性里面了
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pde1m5-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pdwvlu-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pdzjbq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pe21e7-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看看缓存转移的关键源码
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pe4gjp-1.webp&quot; alt=&quot;&quot; /&gt;
第一步先把B的完整对象放到一级缓存中，然后从三级缓存中移除B的ObjectFactory，再从二级缓存中移除B（当然二级缓存中也没有B），接下来完成B的单例注册。这样缓存转移就完成了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pf7oj6-1.webp&quot; alt=&quot;&quot; /&gt;
这样就完成了B对象的初始化
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pfvlkh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是我们B对象的创建流程是在A对象的填充属性流程里，所以会继续A的填充属性流程，这个时候再去一级缓存里找B，就能找到B了，填充B并进行缓存转移，移除二级，三级缓存中的A对象，就可以注入B完成A初始化了
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pgzg8n-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;三级缓存的作用&lt;/h1&gt;
&lt;p&gt;为了AOP
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/phhvgv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/phke37-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/phmmeq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当Spring没有循环依赖的情况下，是把普通对象创建好后再生成代理对象，Spring也没有办法提前知道对象之间的依赖关系。也不能把每个对象都创建出代理对象来，所以就需要把对象包装成objectFactory这个类型，通过其中的ObjectFactory对象中的getObject方法获取到生成的代理对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/pitrj2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL-binlog</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-binlog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-binlog/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;binlog（二进制日志）&lt;/h1&gt;
&lt;p&gt;二进制日志主要用于记录所有针对数据库表结构的变更
以及对表数据的修改操作，不包括SELECT,SHOW等读取类的操作。
Binlog是在事务提交成功以后，在服务层生成的日志文件&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据恢复： 通过详尽的记录所有影响数据状态的SQL命令，binlog为从特定时间点或者由于意外操作导致的数据丢失提供了恢复手段。一旦发生数据损坏或者丢失事件，可以通过重放binlog中的历史更改来恢复到先前的状态。&lt;/li&gt;
&lt;li&gt;主从复制： 对于需要跨多台服务器实现数据备份的应用场景，binlog提供了基础。通过将主服务器的binlog传输到从服务器，从服务器可以重放这些日志以实现数据的同步。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;binlog格式类型&lt;/h1&gt;
&lt;p&gt;MySQL支持三种类型的binlog格式：
&lt;code&gt;STATEMENT&lt;/code&gt;,&lt;code&gt;ROW&lt;/code&gt;和&lt;code&gt;MIXED &lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;STATEMENT模式： 在这个模式下，每一条引起数据变化的SQL语句都会被记录下来。这种方式的优点在于减少了日志大小并且提高了处理速度。
然而，如果使用了SYSDATE（），NOW()之类的非确定性函数，就有可能导致在执行数据恢复或主从复制过程中产生一致性问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ROW模式： 与记录整个SQL不同，ROW模式仅追踪实际受到影响的数据行的变化情况。这种方法避免了STATEMENT模式下的动态内容带来的挑战，但是代价是增加了日志文件的体积&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MIXED模式： 前两者的折中方案。根据具体情况自动选择最合适的记录方式。当系统认为STATEMENT更优的时候，使用STATEMENT模式；当系统认为ROW更优的时候，使用ROW模式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;记录方式&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11buhw1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;主从复制&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11c1t5t-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL-redolog</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-redolog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-redolog/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Redo Log（实现持久化）&lt;/h1&gt;
&lt;p&gt;在InnoDB存储引擎中，大部分Redo Log记录的是物理日志，也就是对特定数据页进行的具体修改。
那么为啥要称呼它为大部分是物理日志呢？是因为Redo Log系统由两部分构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是位于内存中的重做日志缓冲区（redolog buffer），这部分信息容易因为断电等原因丢失。&lt;/li&gt;
&lt;li&gt;二是保存于磁盘上的重做日志文件（redolog file）提供持久化存储&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;引入redo log的必要性&lt;/h2&gt;
&lt;p&gt;尽管buffer pool确实极大提升了数据库操作的性能，但是由于它基于内存的特点，存在着固有的不稳定性，一旦发生系统崩溃或断电等故障，内存中的数据就可能会丢失。为了避免这种情况，Redo Log应运而生。
通过与buffer pool和change buffer协同工作，redolog负责记录所有尚未同步到磁盘的更改操作，确保即使发生故障重启以后也能恢复这些更新，直到相关页面被最终安全地写入到磁盘为止。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10r7qnc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;redo log和undo log之间的差异&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Redo Log专注于 记录事务完成后的新状态，也就是变更后的值&lt;/li&gt;
&lt;li&gt;Undo Log用来追踪事务开始前的原始状态，保存的是变更前的旧值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10uklpi-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10wvrf2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10xmq2l-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10xp98x-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;崩溃恢复&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/10xs9rk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL-relaylog（中继日志）</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-relaylog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-relaylog/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Relay Log（中继日志）&lt;/h1&gt;
&lt;p&gt;中继日志（relay log）只在主从服务器架构的从服务器上存在。从服务器（slave）为了与主服务器(Master)保持一致，要从主服务器读取二进制日志的内容，并且把读取到的信息写入本地的日志文件中，这个从服务器本地的日志文件就叫中继日志。然后，从服务器读取中继日志，并根据中继日志的内容对从服务器的数据进行更新，完成主从服务器的数据同步。&lt;/p&gt;
&lt;p&gt;搭建好主从服务器之后，中继日志默认会保存在从服务器的数据目录下。&lt;/p&gt;
&lt;p&gt;文件名的格式是：从服务器名 - relay-bin.序号。中继日志还有一个索引文件：从服务器名 - relay-bin.index，用来定位当前正在使用的中继日志。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11d3ogv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL-undolog（回滚日志）</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-undolog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-undolog/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;回滚日志 Undo Log&lt;/h1&gt;
&lt;p&gt;回滚日志是数据库引擎层生成的一种日志，主要用于确保事务的ACID特性中的&lt;code&gt;原子性&lt;/code&gt;。它记录的是逻辑操作，也就是&lt;strong&gt;数据在被修改之前的状态。&lt;/strong&gt;
这些逻辑操作包括 插入，删除和更新&lt;/p&gt;
&lt;h2&gt;主要功能&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;事务回滚： 当事务需要回滚的时候，通过执行undo log记录的逆向操作来恢复到事务开始前的数据状态&lt;/li&gt;
&lt;li&gt;多版本并发控制 MVCC： 结合ReadView机制，利用undo log实现多版本并发控制，从而支持高并发读写操作&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;记录内容&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/u7dxwr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/u80n69-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;事务回滚&lt;/h2&gt;
&lt;p&gt;每条记录在进行更新操作的时候，产生的undo日志都包含一个roll_pointer指针和一个trx_id事务标识符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trx_id用于识别对特定记录执行修改的操作的具体事务&lt;/li&gt;
&lt;li&gt;roll_pointer 则允许把一系列相关的undolog日志链接起来形成所谓的&lt;strong&gt;版本链&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/ua6n2r-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当某一个事务需要回滚的时候，并不是通过逆向执行SQL语句来恢复数据状态的，而是依据事务中roll_pointer指向的undolog日志条目来进行数据复原。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/vqnibz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>ConcurrentHashMap1.7和1.8的区别</title><link>https://blog.meowrain.cn/posts/java/juc/concurrenthashmap17%E5%92%8C18%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/concurrenthashmap17%E5%92%8C18%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JDK1.7&lt;/h1&gt;
&lt;p&gt;ConcurrentHashMap用的是分段锁，每个Segment是独立的，可以并发访问不同的Segment,默认是16个Segment,所以最多有16个线程可以并发执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/15/119ff4d-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先通过key的hash判断得到Segment数组的下标，将这个Segment上锁，然后再次通过key的hash得到Segment里面HashEntry数组的下标。可以这么理解：每个Segment数组存放的就是一个单独的HashMap&lt;/p&gt;
&lt;p&gt;缺点是Segment数组一旦初始化了之后就不会扩容，只有HashEntry数组会扩容，这就导致并发度过于死板&lt;/p&gt;
&lt;h1&gt;JDK1.8&lt;/h1&gt;
&lt;p&gt;移除了分段锁，锁的粒度更加细化，锁只在链表或者红黑树&lt;strong&gt;节点级别&lt;/strong&gt;上进行。通过CAS进行插入操作，只有在更新链表或者红黑树的时候才使用&lt;code&gt;synchronized&lt;/code&gt;，并且只锁住链表或者树的头节点，进一步减少了锁的竞争，并发度大大增加。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/15/12al42k-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.8版本的ConcurrentHashMap也不借助ReentrantLock了，直接用synchronized。&lt;/p&gt;
&lt;p&gt;当塞入一个值的时候，先计算key的hash后的下标，如果计算到的下标还没有Node，那么就通过CAS塞入新的Node，如果已经有node，就通过synchronized给这个node上锁，这样别的线程就无法访问这个node和它之后的所有节点了。
然后判断key是不是相等，相等就直接替换value，反之新增一个node。&lt;/p&gt;
&lt;h1&gt;扩容上面的区别&lt;/h1&gt;
&lt;p&gt;JDK1.7的扩容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于Segment: ConcurrentHashMap是由多个Segment组成的，每个Segment中包含一个HashMap，当某个Segment内的HashMap达到扩容阈值的时候，单独为该Segment进行扩容，不会影响到其他Segment&lt;/li&gt;
&lt;li&gt;扩容过程： 每个Segment维护自己的负载因子，当Segment中的元素数量超过阈值的时候，这个Segment的HashMap会扩容，整体的ConcurrentHashMap并不是一次性全部扩容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK1.8的扩容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全局扩容： ConcurrentHashMap取消了Segment,变成了一个全局的数组（类似于HashMap）。因此当ConcurrentHashMap中任意位置的元素超过阈值的时候，整个ConcurrentHashMap的数组都会被扩容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基于CAS扩容： 扩容的时候，ConcurrentHashMap采用了类似HashMap的方式。通过CAS确保线程安全，避免锁住整个数组。扩容的时候，多个线程可以同时帮助完成扩容操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;渐进性扩容： JDK1.8的ConcurrentHashMap引入了渐进式扩容机制，&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;size逻辑区别&lt;/h1&gt;
&lt;p&gt;1.7 是尝试，调用size方法的时候不加锁，三次结果一样那说明没有线程竞争，如果不一样，就加锁计算。&lt;/p&gt;
&lt;p&gt;1.8的话，是直接计算返回结果，用的是LongAdder完成的累加。&lt;/p&gt;
</content:encoded></item><item><title>SpringBoot是如何实现自动配置的</title><link>https://blog.meowrain.cn/posts/java/spring/springboot%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE%E7%9A%84/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/springboot%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE%E7%9A%84/</guid><pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SpringBoot是如何实现自动配置的&lt;/h1&gt;
&lt;p&gt;Spring Boot的自动配置是通过 &lt;code&gt;@EnableAutoConfiguration&lt;/code&gt; 注解来实现的。
这个注解包含 &lt;code&gt;@Import({AutoConfigurationImportSelector.class})&lt;/code&gt;注解
导入的这两个类会扫描classpath下所有的&lt;code&gt;META-INF/spring.factories&lt;/code&gt;中的文件，根据文件中指定的配置类加载相应的Bean的自动配置。&lt;/p&gt;
&lt;p&gt;这些Bean通常会使用 &lt;code&gt;@ConditionOnClass&lt;/code&gt;,&lt;code&gt;@ConditionOnMissingBean&lt;/code&gt;,&lt;code&gt;@ConditionalOnProperty&lt;/code&gt;等注解来控制自动配置的加载条件，例如仅在类路径中存在某个类的时候，才加载某些配置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/ip1efv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/ipewwv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/iqpz9m-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/16/iquhuf-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL联合索引失效情况</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E6%83%85%E5%86%B5/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E6%83%85%E5%86%B5/</guid><pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL联合索引失效情况&lt;/h1&gt;
&lt;h2&gt;1. 不满足最左匹配原则&lt;/h2&gt;
&lt;h2&gt;2. 在索引上使用函数或者运算&lt;/h2&gt;
&lt;h2&gt;3. 索引列参与隐式类型转换&lt;/h2&gt;
&lt;h2&gt;4. 使用NOT IN,!=,&amp;lt;&amp;gt;等否定操作符&lt;/h2&gt;
&lt;h2&gt;5. 模糊匹配 like %xxx%&lt;/h2&gt;
&lt;h2&gt;6.OR操作符&lt;/h2&gt;
&lt;p&gt;如果在Where子句中使用了OR操作符，并且OR前的条件列是索引列，OR后的不是索引列，那么索引可能会失效。&lt;/p&gt;
&lt;h2&gt;7. 使用 not exists关键字，索引也会失效（本质上是Where查询范围太大）&lt;/h2&gt;
&lt;h2&gt;8. 使用Order By 注意最左匹配，要加limit或者Where关键字，否则索引会失效&lt;/h2&gt;
</content:encoded></item><item><title>Redis大Key问题</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%A4%A7key%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%A4%A7key%E9%97%AE%E9%A2%98/</guid><pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/15/ucp5bt-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redis中的 big key指一个内存空间占用比较大的键，它有什么危害？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存分布不均，集群模式下，不同slot分配到不同实例中，如果大key被映射到同一个实例，那么分布不均，查询效率也会受到影响。&lt;/li&gt;
&lt;li&gt;Redis单线程执行命令，操作大Key的时候耗时比较长，会导致Redis出现其他命令 阻塞的问题。&lt;/li&gt;
&lt;li&gt;大key对资源占用很大，在进行网络I/O传输的时候，导致获取过程中产生的网络流量较大，从而产生网络传输时间延长甚至网络传输发生阻塞现象。&lt;/li&gt;
&lt;li&gt;客户端超时，因为操作大key时的时耗比较长，所以可能导致客户端超时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何解决大Key问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发方面，对要存储的数据进行压缩，压缩之后再存储。&lt;/li&gt;
&lt;li&gt;大化小，大对象拆分成小对象。把一个大Key拆分成若干小key，降低单个Key的内存大小。&lt;/li&gt;
&lt;li&gt;使用合适的数据结构进行存储，比如一些用String存储的场景，可以考虑用Hash,Set等结构进行优化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;业务层面：
根据实际情况，调整存储策略，不要把不必要的信息存储到里面&lt;/p&gt;
&lt;p&gt;数据分布方面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采用Redis集群方式进行Redis的部署，然后把大Key拆分散落到不同的服务器上面，加快响应速度。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java内存模型</title><link>https://blog.meowrain.cn/posts/java/java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/</guid><description> Java内存模型 </description><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Java内存模型，是Java虚拟机定义的一种规范，用来描述多线程程序中的变量如何在内存中读取数据，何时会把数据写回主内存。&lt;/p&gt;
&lt;p&gt;JMM的核心目标是确保多线程环境下的&lt;strong&gt;可见性，有序性和原子性&lt;/strong&gt;，从而避免由于硬件和编译器优化带来的不一致问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可见性：确保一个线程对共享变量的修改，其他线程能够及时看到。关键字 &lt;code&gt;volatile&lt;/code&gt;是用来保证可见性的，它强制线程每次读写的时候都从主内存中获取最新值。&lt;/li&gt;
&lt;li&gt;有序性：确保程序执行的顺序符合代码的书写顺序。JMM允许某些指令重排序，来提高性能，但会保证线程内的操作顺序不会被破坏，通过happens-before关系保证跨线程的有序性。&lt;/li&gt;
&lt;li&gt;原子性：确保操作的不可分割性，要么全部成功，要么全部失败。 例如synchronized关键字能确保方法或者代码块的原子性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;JMM 会把内存分为本地内存和主存，每个线程都有它自己的私有化的本地内存，还有个存储共享数据的主存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/p26o8x-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>分库分表场景</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E5%9C%BA%E6%99%AF/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E5%9C%BA%E6%99%AF/</guid><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;分库分表&lt;/h1&gt;
&lt;h2&gt;什么场景分库&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当单个数据库支持的连接数不足以满足客户端需求&lt;/li&gt;
&lt;li&gt;数据量超过了单个数据库实例的处理能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么场景分表&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单表数据量太大&lt;/li&gt;
&lt;li&gt;单表存在较高的写入场景&lt;/li&gt;
&lt;li&gt;当表中存在大量的TEXT,LONGTEXT 或者BLOB字段&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么场景分库分表&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;高并发写入场景： 当应用面临高并发的写入请求的时候，单库单表无法满足需求，需要进行分库分表。&lt;/li&gt;
&lt;li&gt;海量数据场景： 当数据量非常大的时候，单库单表无法满足需求，需要进行分库分表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分库分表的优缺点&lt;/h2&gt;
&lt;h3&gt;优点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;提高系统的性能和可扩展性&lt;/li&gt;
&lt;li&gt;提高系统的可用性和可靠性&lt;/li&gt;
&lt;li&gt;提高系统的可维护性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;增加系统的复杂性&lt;/li&gt;
&lt;li&gt;增加系统的成本&lt;/li&gt;
&lt;li&gt;增加系统的维护成本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分库分表如何设计&lt;/h2&gt;
&lt;p&gt;我们分库分表，是有分片键的，这个分片键怎么用的呢？
分片键是用来决定一条数据应该存储在哪个库或者表中的字段，它直接影响数据的分布，查询效率和系统扩展性。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Hash分片
原理： 对分片键的值进行哈希运算，然后对库或者表取模
适用场景： 数据访问随机性强，读写均衡
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/n8ryrj-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Range分片（范围分片）
原理： 根据分片键的值范围划分数据
使用场景： 时间序列数据，范围查询频繁
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/n99aoy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lookup映射分片
原理： 维护一个映射表，指定某个分片键值属于哪个分片
使用场景： 分片键是枚举值，比如国家，地区等
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/n9u3ge-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/nmff34-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis如何实现分布式锁</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/</guid><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;分布式锁&lt;/h1&gt;
&lt;p&gt;在Redis中实现分布式锁的常见方法是通过 set ex nx命令+lua脚本组合使用，确保多个客户端不会获得同一个资源锁的同时，也保证了安全解锁和意外情况下锁的自动释放。&lt;/p&gt;
&lt;h2&gt;理解Redis实现的分布式锁&lt;/h2&gt;
&lt;p&gt;如果基于Redis来实现分布式锁，需要使用set ex nx命令+ lua脚本&lt;/p&gt;
&lt;p&gt;加锁： &lt;code&gt;SET lock_key uniqueValue EX expiretime NX&lt;/code&gt;
解锁： 使用lua脚本，先get获取key的value判断锁是否是自己加的，如果是则del&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if redis.call(&quot;GET&quot;,KEYS[1]) == ARGV[1]
then
    return redis.call(&quot;DEL&quot;,KEYS[1])
else
    return 0
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;锁需要有过期机制，假设某个客户端加锁后宕机了，锁没设置过期机制，就会让其他客户端抢不到锁。&lt;/p&gt;
&lt;p&gt;EX expiretime 设置的单位是秒，PX expiretime设置的是毫秒&lt;/p&gt;
&lt;p&gt;上面为啥要用&lt;code&gt;uniqueValue&lt;/code&gt;呢，这个就是唯一的值，是为了防止锁被其他客户端释放掉。&lt;/p&gt;
&lt;h2&gt;实现分布式锁的步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;加锁：使用&lt;code&gt;SET lock_key uniqueValue EX expiretime NX&lt;/code&gt;命令加锁&lt;/li&gt;
&lt;li&gt;解锁：使用lua脚本，先get获取key的value判断锁是否是自己加的，如果是则del&lt;/li&gt;
&lt;li&gt;锁需要有过期机制，假设某个客户端加锁后宕机了，锁没设置过期机制，就会让其他客户端抢不到锁。&lt;/li&gt;
&lt;li&gt;EX expiretime 设置的单位是秒，PX expiretime设置的是毫秒&lt;/li&gt;
&lt;li&gt;上面为啥要用&lt;code&gt;uniqueValue&lt;/code&gt;呢，这个就是唯一的值，是为了防止锁被其他客户端释放掉。&lt;/li&gt;
&lt;li&gt;锁需要有过期机制，假设某个客户端加锁后宕机了，锁没设置过期机制，就会让其他客户端抢不到锁。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Redis实现分布式锁的时候可能遇到的问题有哪些</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E6%97%B6%E5%80%99%E5%8F%AF%E8%83%BD%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98%E6%9C%89%E5%93%AA%E4%BA%9B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E6%97%B6%E5%80%99%E5%8F%AF%E8%83%BD%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98%E6%9C%89%E5%93%AA%E4%BA%9B/</guid><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/13/lqg62s-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis单点故障问题</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E5%8D%95%E7%82%B9%E6%95%85%E9%9A%9C%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E5%8D%95%E7%82%B9%E6%95%85%E9%9A%9C%E9%97%AE%E9%A2%98/</guid><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单点故障问题&lt;/h1&gt;
&lt;p&gt;单台Redis实现分布式锁存在单点故障问题，如果采用主从读写分离架构，如果一个客户端在主节点上锁成功，但是主节点突然宕机，由于主从延迟导致节点还没有同步到这个锁，此时可能有另一个客户端抢到新晋升的主节点，这个时候会导致两个客户端抢到了锁，产生数据不一致。&lt;/p&gt;
&lt;h1&gt;红锁&lt;/h1&gt;
&lt;p&gt;红锁基本思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部署多个Redis实例（通常5个）&lt;/li&gt;
&lt;li&gt;客户端在大多数实例（至少3个）上请求锁，并在一定时间内获得成功，表示加锁成功。&lt;/li&gt;
&lt;li&gt;使用RedLock可以提供更高的容错性，即使部分Redis实例故障，仍然能获得锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;红锁实现步骤&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;客户端尝试在每个Redis实例上加锁，必须在有限时间内完成所有实例的加锁。&lt;/li&gt;
&lt;li&gt;如果大多数实例（N / 2 + 1）加锁成功，就表示加锁成功。&lt;/li&gt;
&lt;li&gt;否则，客户端将会释放所有已经加锁的实例，重新尝试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;红锁缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;复杂性： 实现ReadLock要多个Redis实例，会增加复杂性。&lt;/li&gt;
&lt;li&gt;性能： 需要多个Redis实例，会增加性能开销。&lt;/li&gt;
&lt;li&gt;可靠性： 需要多个Redis实例，会增加可靠性开销。&lt;/li&gt;
&lt;li&gt;成本： 需要多个Redis实例，会增加成本。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>RedisString底层</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redisstring%E5%BA%95%E5%B1%82/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redisstring%E5%BA%95%E5%B1%82/</guid><description> RedisString底层 </description><pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;RedisString底层&lt;/h1&gt;
&lt;p&gt;RedisString底层是使用SDS（Simple Dynamic String）实现的，SDS是一个动态字符串，可以动态扩容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/12/12boul3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis缓存击穿缓存穿透缓存雪崩</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9/</guid><pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;缓存击穿&lt;/h1&gt;
&lt;p&gt;缓存击穿说的是，某个热点数据在缓存中失效，导致大量请求打到数据库。这时候由于瞬间的高并发，可能导致数据库崩溃。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;分布式锁： 在获取缓存的时候，使用分布式锁，保证只有一个请求获取到缓存。这样的话，就不会因为热点数据过期失效，而让请求都打到数据库里了。&lt;/li&gt;
&lt;li&gt;热点数据永不过期&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;缓存穿透&lt;/h1&gt;
&lt;p&gt;缓存穿透说的是，某个热点数据在缓存中不存在，导致大量请求打到数据库。这时候由于瞬间的高并发，可能导致数据库崩溃。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用布隆过滤器，布隆过滤器对于不存在的数据是能100%判断的，所以用布隆过滤器能过滤掉不存在的请求。&lt;/li&gt;
&lt;li&gt;对查询的空结果进行缓存，第二次访问直接返回空值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;缓存雪崩&lt;/h1&gt;
&lt;p&gt;多个缓存数据在同一时刻过期，导致大量请求同时访问数据库，导致数据库崩溃。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;采用随机时间过期策略，避免多个数据同时过期。&lt;/li&gt;
&lt;li&gt;使用双缓存策略，把数据存储在两层缓存里面，减少数据库的直接请求。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>缓存与数据库一致性问题</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E7%BC%93%E5%AD%98%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98/</guid><pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;缓存与数据库一致性&lt;/h1&gt;
&lt;p&gt;缓存和数据库一致性是说在使用缓存的情况下，保证缓存中的数据和数据库中数据一致的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“先写缓存再写数据库”、“先写数据库再写缓存”、“先删除缓存再写数据库”这三种策略确实存在较大的数据不一致风险，因此通常不建议直接使用&lt;/strong&gt;，特别是在高并发、分布式系统中。&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;h2&gt;先写缓存再写数据库&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;策略：先写缓存 (Redis)，再写数据库 (MySQL)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图示场景：&lt;/strong&gt; 有两个写请求 A 和 B，同时尝试更新票务余票。初始余票是 17，经过两个用户的扣减，&lt;strong&gt;期望&lt;/strong&gt;最终余票是 15。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发执行时序 (按图中箭头顺序)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;写请求 A:&lt;/p&gt;
&lt;p&gt;更新 Redis 缓存&lt;/p&gt;
&lt;p&gt;，将余票设为 16。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 16，MySQL: 17&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 B:&lt;/p&gt;
&lt;p&gt;更新 Redis 缓存&lt;/p&gt;
&lt;p&gt;，将余票设为 15。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 15，MySQL: 17 (请求 B 覆盖了请求 A 在 Redis 里的值)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 B:&lt;/p&gt;
&lt;p&gt;更新 MySQL 数据库&lt;/p&gt;
&lt;p&gt;，将余票设为 15。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 15，MySQL: 15 (到这里，Redis 和 MySQL 暂时一致)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 A:&lt;/p&gt;
&lt;p&gt;更新 MySQL 数据库&lt;/p&gt;
&lt;p&gt;，将余票设为 16。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 15，MySQL: 16 (请求 A 的数据库更新发生在 B 之后，覆盖了 B 在数据库里的值)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;先写数据库再写缓存&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;策略：先写数据库 (MySQL)，再写缓存 (Redis)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图示场景：&lt;/strong&gt; 同样是两个写请求 A 和 B，尝试更新余票，初始 17，期望最终是 15。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发执行时序 (按图中箭头顺序)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;写请求 A:&lt;/p&gt;
&lt;p&gt;更新 MySQL 数据库&lt;/p&gt;
&lt;p&gt;，将余票设为 16。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 17 (假设初始)，MySQL: 16&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 B:&lt;/p&gt;
&lt;p&gt;更新 MySQL 数据库&lt;/p&gt;
&lt;p&gt;，将余票设为 15。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 17，MySQL: 15 (数据库被 B 更新为最终期望值)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 B:&lt;/p&gt;
&lt;p&gt;更新 Redis 缓存&lt;/p&gt;
&lt;p&gt;，将余票设为 15。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 15，MySQL: 15 (暂时一致，且是正确期望值)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求 A:&lt;/p&gt;
&lt;p&gt;更新 Redis 缓存&lt;/p&gt;
&lt;p&gt;，将余票设为 16。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 16，MySQL: 15 (请求 A 的缓存更新发生在 B 的缓存更新之后，覆盖了 B 在缓存里的值)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最终结果：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 缓存中的余票是 &lt;strong&gt;16&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;MySQL 数据库中的余票是 &lt;strong&gt;15&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;先删除缓存，再写数据库&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;策略：先删除缓存 (Redis)，再写数据库 (MySQL)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图示场景：&lt;/strong&gt; 这次涉及一个写请求和一个读请求并发执行。初始时，假设 Redis 和 MySQL 中的余票都是 16。写请求想将余票更新为 15。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发执行时序 (按图中箭头顺序)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;写请求:&lt;/p&gt;
&lt;p&gt;删除 Redis 缓存&lt;/p&gt;
&lt;p&gt;，删除车站余票的 key (假定初始值是 16，所以图中箭头描述为“删除车站余票缓存 16”，但这只表示删的是这个key对应的旧数据)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 空，MySQL: 16&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读请求:&lt;/p&gt;
&lt;p&gt;在写请求&lt;/p&gt;
&lt;p&gt;还未来得及更新数据库之前&lt;/p&gt;
&lt;p&gt;，并发地发起&lt;/p&gt;
&lt;p&gt;读操作&lt;/p&gt;
&lt;p&gt;。读请求先&lt;/p&gt;
&lt;p&gt;查询缓存&lt;/p&gt;
&lt;p&gt;，发现&lt;/p&gt;
&lt;p&gt;缓存为空&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 空，MySQL: 16&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读请求:&lt;/p&gt;
&lt;p&gt;缓存未命中，读请求转向&lt;/p&gt;
&lt;p&gt;查询 MySQL 数据库&lt;/p&gt;
&lt;p&gt;。此时写请求尚未完成数据库更新，所以从数据库中读到的是&lt;/p&gt;
&lt;p&gt;旧数据 16&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 空，MySQL: 16，读请求获取到数据 16&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写请求:&lt;/p&gt;
&lt;p&gt;更新 MySQL 数据库&lt;/p&gt;
&lt;p&gt;，将余票设为 15。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 空，MySQL: 15&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读请求:&lt;/p&gt;
&lt;p&gt;将从数据库读到的&lt;/p&gt;
&lt;p&gt;旧数据 16 回写到 Redis 缓存&lt;/p&gt;
&lt;p&gt;中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 Redis: 16，MySQL: 15&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最终结果：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 缓存中的余票是 &lt;strong&gt;16&lt;/strong&gt; (旧值)。&lt;/li&gt;
&lt;li&gt;MySQL 数据库中的余票是 &lt;strong&gt;15&lt;/strong&gt; (新值)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;解决方案&lt;/h1&gt;
&lt;p&gt;根据业务场景选择下面的缓存一致性方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存双删&lt;/strong&gt;：如果公司现有消息队列中间件，可以考虑使用该方案，反之则不需要考虑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先写数据库再删缓存&lt;/strong&gt;：这种方案从实时性以及技术实现复杂度来说都比较不错，推荐大家使用这种方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binlog 异步更新缓存&lt;/strong&gt;：如果希望实现最终一致性以及数据多中心模式，该方案无疑是最合适的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缓存双删&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;先删除缓存 -&amp;gt; 写数据库 -&amp;gt; 延迟一段时间 -&amp;gt; 再次删除缓存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/20/qoh9qh-0.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/20/r7ob3u-0.webp&quot; alt=&quot;image-20250520164546344&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;先写入数据库，再删除缓存&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&quot;先写 DB 再删除缓存&quot;是一种常用的缓存一致性解决方案，也被称为“写回策略”或“Write-Through 策略”。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当应用程序进行写操作时，首先将数据写入数据库。&lt;/li&gt;
&lt;li&gt;然后，立即删除相应的缓存数据（或使缓存数据失效）。&lt;/li&gt;
&lt;li&gt;当下一个读取请求到达时，会发现缓存中没有相应的数据，于是从数据库中读取最新的数据，并将其存储在缓存中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种策略的优势在于它保证了数据库和缓存之间的一致性。如果应用程序写入数据库并删除缓存，下一个读取操作将从数据库中获取最新数据，从而避免了数据不一致的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/20/r9sbzc-0.webp&quot; alt=&quot;image-20250520164901899&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然，这种模型也不是完全没问题，如果说恰巧读缓存失效了，就会出现这种情况。&lt;/p&gt;
&lt;p&gt;当缓存过期（可能是缓存正常过期也可能是 Redis 内存满了触发清理策略）条件满足，同时读请求的回写缓存 Redis 的执行周期在数据库删除之前，那么就有可能触发缓存数据库不一致问题。&lt;/p&gt;
&lt;p&gt;上面说的两种情况，缺一不可，不过能同时满足这两种情况概率极低，低到可以忽略这种情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/20/rb7n2u-0.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种模型会存在一个很小周期的缓存与数据库&lt;strong&gt;不一致&lt;/strong&gt;的情况，不过对于绝大多数的情况来说，是可以容忍的。除去一些电商库存、列车余票等对数据比较敏感的情况，比较适合绝大多数业务场景。&lt;/p&gt;
&lt;h1&gt;AI问答，一些思考&lt;/h1&gt;
&lt;h1&gt;数据库缓存方案：先删后写与双删&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/556172201&quot;&gt;(99 封私信 / 80 条消息) 『延迟双删』和『先修改数据库，再删除缓存』哪个方案更好？ - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;🧑‍💻 User&lt;/h3&gt;
&lt;p&gt;所以先写数据库，再删缓存
和缓存双删
都是企业中正常使用的方案？&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Assistant&lt;/h3&gt;
&lt;p&gt;&amp;lt;details style=&quot;background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;&quot;&amp;gt;
&amp;lt;summary&amp;gt;已深度思考&amp;lt;/summary&amp;gt;&amp;lt;hr&amp;gt;
好的，用户现在在问关于数据库和缓存操作的两种常见方案：“先写数据库，再删缓存”和“缓存双删”是否都是企业中正常使用的方案。我需要先解释这两种方案，然后说明它们的适用场景和企业中的实际使用情况。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;首先，我需要回忆一下这两种策略的基本概念。先写数据库再删缓存，是指在更新数据时，先更新数据库，然后删除缓存中的旧值，这样下次读取时会从数据库重新加载最新数据到缓存。而缓存双删，是在更新数据库前后各删除一次缓存，防止在更新过程中有并发读请求导致缓存脏数据。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;接下来，我需要解释为什么企业会使用这些方案。先写数据库再删缓存是比较常见的策略，因为它简单且在大多数情况下有效，尤其是在并发不高的场景。但可能存在的问题是，如果在删除缓存之前有读请求，会读取到旧的缓存值，不过这种情况概率较低，因为数据库更新通常比缓存操作快。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;然后是缓存双删，这是为了解决先写后删可能存在的并发问题。比如，当更新数据库时，有一个读请求在数据库更新之后但缓存删除之前读取，这时候会从数据库拿到新数据，但可能在缓存删除前另一个写操作导致数据不一致。双删策略通过在更新前删一次，更新后等一段时间再删一次，来减少这种情况的发生。不过需要注意等待时间的设置，这取决于数据库的主从同步时间或其他延迟。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;接下来要说明企业中是否正常使用。实际上，这两种方案都是常见的，但适用场景不同。先写后删更简单，适用于大部分场景；双删则在高并发或对一致性要求较高的场景中使用。同时，可能还要提到其他方案，比如写缓存（更新缓存而不是删除），但这种方式可能有一致性问题，需要结合其他机制。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，需要总结这两种方案都是企业常用的，但需要根据具体场景选择，并可能结合其他优化措施，比如延迟双删中的等待时间，或者使用消息队列来确保缓存删除成功。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;现在需要按照用户要求的格式来组织回答：标题带emoji，解释部分要详细但易懂，可能用例子或类比，最后一个问题。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;标题可能用“数据库与缓存更新策略：先写后删 vs 双删 🗄️🔄”这样的。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;解释部分需要分点说明两种策略，它们的流程、优缺点、适用场景，以及企业中的实际应用情况。可能用一个例子，比如用户修改个人信息，先更新数据库再删缓存，或者双删的情况。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，问用户是否想了解更多细节，比如具体实现或其他缓存策略。&amp;lt;br&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;数据库与缓存更新策略：先写后删 vs 双删 🗄️🔄&lt;/p&gt;
&lt;p&gt;在企业级系统中，数据库（持久化存储）和缓存（高速临时存储）的协同工作是性能优化的核心。为了保证数据一致性（数据库和缓存中的数据同步），常见的两种策略是“先写数据库，再删缓存”和“缓存双删”，它们都是企业中&lt;strong&gt;根据场景灵活选用&lt;/strong&gt;的方案，我们逐一拆解：&lt;/p&gt;
&lt;h3&gt;1. 先写数据库，再删缓存：最基础的“保守派”&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：当需要更新数据时，先完成数据库的写操作（确保持久化存储是最新的），然后删除缓存中对应的旧值。下次读取时，由于缓存缺失，系统会从数据库读取最新数据并重新写入缓存（即“缓存击穿”后的自动填充）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么企业用它？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简单可靠&lt;/strong&gt;：逻辑清晰，只有两步操作（写库→删缓存），易于实现和维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险可控&lt;/strong&gt;：数据库是“数据源头”，优先保证数据库的正确性（即使缓存删除失败，最多缓存中暂时保留旧数据，下次读取会自动更新）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;潜在问题&lt;/strong&gt;：&lt;br /&gt;
如果在“写数据库”和“删缓存”之间，有一个读请求到达，此时缓存中的旧数据还未被删除，读请求会拿到旧值。但这种情况发生概率较低（数据库写操作通常比缓存操作快，时间窗口短），因此在&lt;strong&gt;并发量不高或对一致性要求不是极端严格&lt;/strong&gt;的场景（如用户信息、商品详情）中，企业普遍采用此方案。&lt;/p&gt;
&lt;h3&gt;2. 缓存双删：高并发下的“增强版”&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：为了进一步降低“先写后删”中旧缓存被读取的概率，在更新数据库前后各删除一次缓存（故称“双删”）：&lt;br /&gt;
① &lt;strong&gt;先删缓存&lt;/strong&gt;：删除旧缓存（防止读请求拿到旧值）；&lt;br /&gt;
② &lt;strong&gt;写数据库&lt;/strong&gt;：更新数据库；&lt;br /&gt;
③ &lt;strong&gt;延迟后再删缓存&lt;/strong&gt;：等待一段时间（比如1-5秒，具体取决于数据库主从同步或业务延迟），再次删除缓存（防止在写数据库期间，有新的读请求将旧数据重新写入缓存）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么企业用它？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提升一致性&lt;/strong&gt;：通过两次删除，大幅缩小“旧缓存被读取”的时间窗口，适用于&lt;strong&gt;高并发场景&lt;/strong&gt;（如秒杀活动、高频更新的订单状态）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应对复杂延迟&lt;/strong&gt;：如果数据库是主从架构（写主库→同步到从库有延迟），或缓存写入存在异步操作，双删能更好地覆盖这些延迟带来的不一致风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;潜在问题&lt;/strong&gt;：&lt;br /&gt;
需要额外处理“延迟删除”的逻辑（比如用定时任务或消息队列实现），增加了系统复杂度；延迟时间过长可能影响性能，过短可能无法覆盖所有延迟场景，需要根据业务经验调优。&lt;/p&gt;
&lt;h3&gt;企业实际使用场景总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先写后删&lt;/strong&gt;：是“默认方案”，适用于大多数业务（如用户个人信息、商品基础属性），简单高效，满足90%以上场景的一致性需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双删策略&lt;/strong&gt;：作为“增强方案”，用于高并发、强一致性要求的场景（如库存、交易订单），通过牺牲一定复杂度换取更高的数据一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你更想深入了解哪一种策略的具体实现细节？比如“先写后删”如何处理缓存删除失败，或“双删”中延迟时间如何计算？ 😊&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🧑‍💻 User&lt;/h3&gt;
&lt;p&gt;问题是缓存双删，你删了缓存，在删除和写入之间有读请求，哪个请求还是会拿到旧数据啊，这时候还是会让缓存是旧数据，但是数据库里是新的（短暂的），当然最后缓存还是要被二次删除&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Assistant&lt;/h3&gt;
&lt;p&gt;&amp;lt;details style=&quot;background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;&quot;&amp;gt;
&amp;lt;summary&amp;gt;已深度思考&amp;lt;/summary&amp;gt;&amp;lt;hr&amp;gt;
好的，用户现在在问缓存双删策略中的一个具体问题：在第一次删缓存之后，写数据库之前，如果有读请求进来，这时候读请求会从数据库读取旧数据并重新写入缓存，导致缓存里是旧数据，而数据库之后会被更新为新数据。这时候虽然最后会二次删除缓存，但中间可能存在短暂的不一致。我需要解释这个问题，并说明为什么双删策略仍然有效，或者如何处理这种情况。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;首先，用户的问题核心是双删策略中的时间窗口问题，即在第一次删除缓存后，写数据库前，读请求可能重新填充旧数据到缓存。这时候需要分析这个时间窗口的长度，以及如何通过延迟二次删除来解决。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;需要回顾双删的流程：第一次删缓存→写数据库→延迟后二次删缓存。用户提到的情况是在第一步删缓存后，写数据库前，有读请求，此时数据库还是旧数据（因为还没写），所以读请求会从数据库读旧数据，然后写入缓存。这时候缓存里是旧数据，之后数据库被更新为新数据，这时候缓存里的旧数据会存在，直到二次删除触发。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;需要解释为什么这种情况的影响有限，或者如何通过调整延迟时间来覆盖这个窗口。比如，假设写数据库的时间是T1，读请求的时间是在T0（第一次删缓存后，写数据库前），那么读请求会把旧数据写入缓存，之后数据库在T1被更新。此时缓存中的旧数据会存在，直到二次删除在T1+延迟时间执行。这时候如果延迟时间足够长，覆盖了可能的读请求重新填充缓存的时间，那么二次删除就能清除这个旧缓存，下次读请求就会从数据库读取新数据。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;另外，可能需要提到这种情况的概率较低，因为写数据库通常是一个相对较快的操作，时间窗口较短。或者企业中如何通过其他手段（如分布式锁、异步补偿）来进一步降低风险。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;还要注意用户可能的深层需求：是否双删策略真的能解决一致性问题，或者是否有其他更优的方案。需要明确双删是权衡后的选择，并非完美，但在大多数高并发场景下足够用。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，保持解释清晰，用例子或时间线来帮助理解，然后以问题结束，引导用户进一步提问。&amp;lt;br&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;缓存双删中的“旧数据回灌”问题：时间窗口与延迟删除的奥秘 🕒🔍&lt;/p&gt;
&lt;p&gt;你观察得非常细致！缓存双删策略确实存在一个“短暂的时间窗口”，可能让旧数据暂时回到缓存中。我们通过具体时间线模拟这个过程，再分析为什么企业仍选择这种方案，以及如何降低风险：&lt;/p&gt;
&lt;h3&gt;场景模拟：双删中的“旧数据回灌”&lt;/h3&gt;
&lt;p&gt;假设我们要更新一条用户的“账户余额”数据，流程如下（时间顺序）：&lt;br /&gt;
1️⃣ &lt;strong&gt;第一次删缓存&lt;/strong&gt;（T0时刻）：删除缓存中旧的“账户余额”（假设旧值是100元）。&lt;br /&gt;
2️⃣ &lt;strong&gt;读请求插入旧数据&lt;/strong&gt;（T1时刻，T0 &amp;lt; T1 &amp;lt; T2）：此时数据库还未更新（仍是旧值100元），一个读请求到达→缓存缺失→从数据库读取旧值100元→重新写入缓存（缓存现在是100元）。&lt;br /&gt;
3️⃣ &lt;strong&gt;写数据库&lt;/strong&gt;（T2时刻）：数据库更新为新值200元（完成持久化）。&lt;br /&gt;
4️⃣ &lt;strong&gt;第二次删缓存&lt;/strong&gt;（T3时刻，T3 = T2 + 延迟时间）：等待一段时间后，再次删除缓存（此时缓存中的100元被删除）。&lt;/p&gt;
&lt;h3&gt;为什么这种“短暂不一致”可以接受？&lt;/h3&gt;
&lt;p&gt;虽然T1到T3期间，缓存中是旧值（100元），数据库是新值（200元），但这种不一致的&lt;strong&gt;持续时间极短&lt;/strong&gt;（仅T3 - T1），且企业通过以下方式降低影响：&lt;/p&gt;
&lt;h4&gt;1. 延迟时间的“精准控制”&lt;/h4&gt;
&lt;p&gt;第二次删除的“延迟时间”（如1-5秒）是关键。它需要覆盖“数据库写操作的耗时 + 可能的读请求重新填充缓存的耗时”。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设数据库写操作耗时0.5秒，读请求从数据库读数据+写缓存耗时0.3秒，那么延迟时间设为1秒即可覆盖T2到T3的窗口，确保在第二次删除时，缓存中可能存在的旧数据被清除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 高并发下的“概率稀释”&lt;/h4&gt;
&lt;p&gt;在实际场景中，“读请求恰好出现在T0-T2窗口”的概率较低。因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写数据库操作通常是同步的（如事务提交），耗时极短（毫秒级）；&lt;/li&gt;
&lt;li&gt;即使有读请求进入，它们大多是“分散”的，不会集中在这个微小的时间窗口内。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 最终一致性的“兜底”&lt;/h4&gt;
&lt;p&gt;即使出现短暂不一致，第二次删除后，下一次读请求会从数据库读取最新值（200元）并重新填充缓存，最终保证缓存与数据库一致。&lt;/p&gt;
&lt;h3&gt;企业如何进一步优化？&lt;/h3&gt;
&lt;p&gt;如果业务对一致性要求极高（如金融交易），企业可能会结合以下策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分布式锁&lt;/strong&gt;：在写操作期间，对关键数据加锁，阻止读请求在T0-T2窗口内重新填充缓存（但会牺牲部分性能）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步补偿&lt;/strong&gt;：通过日志监控缓存与数据库的差异，发现不一致时主动触发缓存更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存版本号&lt;/strong&gt;：为缓存添加版本标识（如时间戳），读请求填充缓存时检查版本，旧版本数据直接丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结来说，双删策略的“旧数据回灌”是理论上存在的，但通过合理设置延迟时间、结合其他优化手段，企业可以将其影响控制在可接受范围内。它本质上是“性能与一致性”的权衡——用微小的短暂不一致，换取高并发下的整体高效。&lt;/p&gt;
&lt;p&gt;关于双删策略，你还想了解哪些细节？比如“如何计算具体的延迟时间”或“分布式锁如何与双删配合使用”？ 😊&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🧑‍💻 User&lt;/h3&gt;
&lt;p&gt;这不和先更新数据库，再删除缓存一样吗？&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Assistant&lt;/h3&gt;
&lt;p&gt;&amp;lt;details style=&quot;background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;&quot;&amp;gt;
&amp;lt;summary&amp;gt;已深度思考&amp;lt;/summary&amp;gt;&amp;lt;hr&amp;gt;
好的，用户现在的问题是：“这不和先更新数据库，再删除缓存一样吗？”需要解释缓存双删和先写数据库再删缓存的区别，以及为什么两者不同。首先，我需要回顾之前的对话，用户已经了解了这两种策略，现在可能混淆了它们的差异。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;首先，用户的疑问是双删和先写后删是否一样。需要明确两者的核心区别在于操作顺序和步骤数量。先写后删是两步：写库→删缓存；双删是三步：删缓存→写库→延迟删缓存。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;接下来，需要解释为什么双删不是简单的先写后删的重复。要指出时间窗口的不同，以及双删如何针对先写后删的缺陷进行优化。比如，先写后删的问题是在写库和删缓存之间可能有读请求拿到旧缓存，而双删通过提前删缓存和延迟再删来缩小这个窗口。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;然后，用具体的时间线对比两种策略，说明在同样的读请求干扰下，两种策略的表现差异。比如，先写后删中，读请求可能在写库之后、删缓存之前到达，导致缓存保留旧值；而双删中，第一次删缓存后，读请求可能在写库前重新填充旧值，但第二次删缓存会清除它。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;还要强调双删的“延迟”步骤的作用，以及为什么这额外的步骤能提升一致性，尽管仍有短暂窗口，但比先写后删更优。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，总结两者的适用场景，让用户明白它们不是一样的，而是针对不同并发和一致性需求的解决方案。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;需要保持解释清晰，用例子和时间线帮助理解，避免术语过于复杂。同时，按照系统提示的格式，标题要有emoji，解释部分分点，最后问用户想深入的问题。&amp;lt;br&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;缓存双删 vs 先写后删：步骤差异与一致性提升的本质区别 🧩🔄&lt;/p&gt;
&lt;p&gt;你的观察很敏锐！两者确实都涉及“写数据库”和“删缓存”，但核心差异在于&lt;strong&gt;操作顺序、步骤数量&lt;/strong&gt;和&lt;strong&gt;对“读请求干扰”的防御机制&lt;/strong&gt;。我们通过具体场景对比，就能清晰看到它们的不同。&lt;/p&gt;
&lt;h3&gt;先写数据库，再删缓存：“被动防御”的基础策略&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：写数据库（T1）→ 删缓存（T2）。&lt;br /&gt;
&lt;strong&gt;时间线示例&lt;/strong&gt;（假设要更新用户余额为200元）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T0：数据库旧值100元，缓存旧值100元（初始状态）。&lt;/li&gt;
&lt;li&gt;T1：写数据库完成（新值200元）。&lt;/li&gt;
&lt;li&gt;T2：删除缓存（此时缓存被清空）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险窗口&lt;/strong&gt;：若在T1到T2之间（写库后、删缓存前）有读请求：&lt;br /&gt;
读请求发现缓存未删（仍为100元）→ 直接返回旧值（缓存旧值，数据库新值），导致不一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存双删：“主动+延迟”的增强策略&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：删缓存（T0）→ 写数据库（T1）→ 延迟后再删缓存（T2）。&lt;br /&gt;
&lt;strong&gt;时间线示例&lt;/strong&gt;（同样更新用户余额为200元）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T0：第一次删缓存（缓存旧值100元被删除，缓存为空）。&lt;/li&gt;
&lt;li&gt;T1：写数据库完成（新值200元）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险窗口&lt;/strong&gt;：若在T0到T1之间（删缓存后、写库前）有读请求：&lt;br /&gt;
读请求发现缓存为空→ 从数据库读旧值100元→ 重新写入缓存（缓存现在是100元）。&lt;/li&gt;
&lt;li&gt;T2（延迟后）：第二次删缓存（清除此时可能存在的旧值100元）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;关键差异：风险窗口的“位置”与“处理方式”&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;策略&lt;/th&gt;
&lt;th&gt;风险窗口位置&lt;/th&gt;
&lt;th&gt;风险窗口内的读请求行为&lt;/th&gt;
&lt;th&gt;最终一致性如何保证？&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;先写后删&lt;/td&gt;
&lt;td&gt;写库后、删缓存前（T1-T2）&lt;/td&gt;
&lt;td&gt;读请求拿到缓存旧值（数据库已更新）&lt;/td&gt;
&lt;td&gt;下次读请求时缓存被删，重新加载新值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存双删&lt;/td&gt;
&lt;td&gt;删缓存后、写库前（T0-T1）&lt;/td&gt;
&lt;td&gt;读请求重新写入缓存旧值（数据库未更新）&lt;/td&gt;
&lt;td&gt;第二次删缓存清除旧值，下次读加载新值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;为什么双删不是“先写后删的重复”？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;风险窗口更小&lt;/strong&gt;：写数据库通常比删缓存耗时（如事务提交可能需要毫秒级），而“先删缓存”让风险窗口出现在“写库前”（此时数据库还是旧值），读请求即使回灌旧值，数据库本身未更新，后续第二次删缓存后，下次读请求会从已更新的数据库加载新值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动防御延迟&lt;/strong&gt;：双删通过“延迟第二次删除”覆盖了数据库写操作的耗时，确保即使有读请求在写库前回灌旧值，最终也会被清除。而先写后删没有这一步，风险窗口内的读请求可能长期保留旧缓存（直到下次自然失效）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;总结：两者是“互补”而非“相同”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;先写后删：简单、高效，适用于低并发或一致性要求不高的场景（风险窗口短，概率低）。&lt;/li&gt;
&lt;li&gt;缓存双删：通过“两次删除+延迟”主动缩小风险窗口，适用于高并发、一致性要求更高的场景（如库存、交易）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你更想了解哪种策略在具体代码中的实现细节？比如先写后删如何用代码处理“删缓存失败”，或双删的“延迟时间”如何用定时任务实现？ 😊&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🧑‍💻 User&lt;/h3&gt;
&lt;p&gt;奥我懂了，双删时候，风险窗口期间，数据库未更新，其实是不算不一致的对吧，就算请求B取数据库并且更新缓存，那个值也是那个时刻的正确数值。
但是我有个问题，后面更新数据库后，这时候缓存如果被之前的请求更新为旧的数据，这时候不就是不一致了吗？这期间进来的请求拿到的不就是旧的值了吗？&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Assistant&lt;/h3&gt;
&lt;p&gt;&amp;lt;details style=&quot;background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;&quot;&amp;gt;
&amp;lt;summary&amp;gt;已深度思考&amp;lt;/summary&amp;gt;&amp;lt;hr&amp;gt;
好的，用户现在的问题是关于缓存双删策略中的不一致情况。用户之前已经理解了双删的基本流程，现在进一步疑惑：当数据库更新后，如果缓存被之前的读请求填充了旧数据，这时候是否会导致不一致，以及后续请求是否会拿到旧值。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;首先，我需要确认用户的问题核心：在双删的时间窗口中，读请求可能在数据库更新前读取旧数据并填充缓存，而当数据库更新后，这个缓存中的旧数据是否会导致不一致，以及如何解决。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;接下来，我需要解释在数据库更新后，缓存中的旧数据确实会导致短暂的不一致，但双删的第二次删除会解决这个问题。需要详细说明时间线，让用户明确每个阶段的数据状态。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;然后，用户可能想知道为什么这种不一致是短暂的，以及第二次删除的作用。需要强调延迟时间的重要性，确保在第二次删除时，旧数据被清除，后续请求会从数据库获取新值。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;另外，用户可能隐含担心这种短暂不一致是否会影响业务，需要说明实际场景中这种情况的概率和持续时间都很低，以及企业如何通过优化手段（如调整延迟时间、监控）来进一步降低风险。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，保持回答的结构：标题、解释、总结问题，确保符合之前的格式要求，用易懂的语言和例子帮助用户理解。&amp;lt;br&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;缓存双删中的“短暂不一致”与二次删除的关键作用 🕰️🔧&lt;/p&gt;
&lt;p&gt;你完全抓住了问题的核心！这种情况下确实会出现“数据库已更新，但缓存仍存旧值”的短暂不一致，但双删策略的“第二次删除”正是为了解决这个问题。我们通过时间线拆解和具体例子，来彻底理清这个过程。&lt;/p&gt;
&lt;h3&gt;时间线模拟：数据库更新后，缓存为何会有旧值？&lt;/h3&gt;
&lt;p&gt;假设要更新用户“账户余额”（旧值100元→新值200元），双删流程的时间线如下（关键节点标注）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间点&lt;/th&gt;
&lt;th&gt;操作/事件&lt;/th&gt;
&lt;th&gt;数据库状态&lt;/th&gt;
&lt;th&gt;缓存状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T0&lt;/td&gt;
&lt;td&gt;第一次删缓存（主动删除旧缓存）&lt;/td&gt;
&lt;td&gt;仍为100元（未更新）&lt;/td&gt;
&lt;td&gt;缓存被清空（无数据）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;读请求A到达（在T0-T2之间）&lt;/td&gt;
&lt;td&gt;仍为100元（未更新）&lt;/td&gt;
&lt;td&gt;缓存空→读数据库取100元→写缓存（缓存现在是100元）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;写数据库完成（更新为200元）&lt;/td&gt;
&lt;td&gt;变为200元（已更新）&lt;/td&gt;
&lt;td&gt;缓存仍为100元（旧值）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;第二次删缓存（延迟后执行）&lt;/td&gt;
&lt;td&gt;200元（已更新）&lt;/td&gt;
&lt;td&gt;缓存中的100元被删除（缓存再次清空）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;读请求B到达（T3之后）&lt;/td&gt;
&lt;td&gt;200元（已更新）&lt;/td&gt;
&lt;td&gt;缓存空→读数据库取200元→写缓存（缓存现在是200元）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;为什么T2到T3期间会“不一致”？&lt;/h3&gt;
&lt;p&gt;在T2（数据库更新完成）到T3（第二次删缓存）的时间段内：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库：已存储最新值200元（正确）；&lt;/li&gt;
&lt;li&gt;缓存：被读请求A在T1时刻填充了旧值100元（错误）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，若有新的读请求在T2-T3之间到达，会直接读取缓存中的旧值100元，导致“缓存旧、数据库新”的不一致。&lt;/p&gt;
&lt;h3&gt;但这种不一致为何是“短暂可控”的？&lt;/h3&gt;
&lt;p&gt;关键在&lt;strong&gt;第二次删除（T3）&lt;strong&gt;和&lt;/strong&gt;延迟时间的设计&lt;/strong&gt;：&lt;/p&gt;
&lt;h4&gt;1. 第二次删除强制清空旧缓存&lt;/h4&gt;
&lt;p&gt;无论缓存中是否有旧值，T3时刻的二次删除会直接清除缓存中的100元。此时缓存变为空，后续读请求（如T4的读请求B）只能从数据库读取最新的200元，并重新填充缓存为正确值。&lt;/p&gt;
&lt;h4&gt;2. 延迟时间覆盖“数据库写操作耗时”&lt;/h4&gt;
&lt;p&gt;T3 = T2 + 延迟时间（如1秒）。延迟时间需要足够长，确保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库写操作（T0到T2）已完全完成；&lt;/li&gt;
&lt;li&gt;所有可能在T0-T2期间触发的读请求（如读请求A）都已完成缓存填充。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，第二次删除时，缓存中可能存在的旧值（100元）会被彻底清除，避免长期留存。&lt;/p&gt;
&lt;h3&gt;极端情况：如果第二次删除失败了怎么办？&lt;/h3&gt;
&lt;p&gt;假设T3时刻二次删除因网络问题失败，缓存中的旧值100元未被清除，此时会发生什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后续读请求仍会读取到旧值，但这种情况属于“异常”，企业通常会通过以下方式兜底：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存过期时间&lt;/strong&gt;：即使没有二次删除，缓存的TTL（如5分钟）会自动失效，最终强制从数据库加载新值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控与补偿&lt;/strong&gt;：通过日志监控缓存与数据库的差异，发现不一致时手动或自动触发缓存更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结：双删的“不一致”是“阶段性的”，而非“永久性的”&lt;/h3&gt;
&lt;p&gt;虽然T2-T3期间可能出现“缓存旧、数据库新”的短暂不一致，但：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持续时间由延迟时间决定（通常仅几秒）；&lt;/li&gt;
&lt;li&gt;第二次删除强制清除旧缓存，后续请求会自动加载新值；&lt;/li&gt;
&lt;li&gt;结合缓存过期时间，最终一致性得以保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于这种“短暂不一致”的业务影响，你还想了解哪些场景？比如“金融系统如何容忍这种短暂不一致”或“如何通过监控发现这类问题”？ 😊&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🧑‍💻 User&lt;/h3&gt;
&lt;p&gt;这个第二次删缓存的延迟是什么？能讲一下吗&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Assistant&lt;/h3&gt;
&lt;p&gt;&amp;lt;details style=&quot;background-color: #f5f5f5; padding: 5px; border-radius: 10px; margin-bottom: 10px;&quot;&amp;gt;
&amp;lt;summary&amp;gt;已深度思考&amp;lt;/summary&amp;gt;&amp;lt;hr&amp;gt;
好的，用户现在问的是缓存双删中第二次删除的延迟是什么，需要详细解释。首先，我需要明确延迟的作用、如何确定延迟时间，以及实际应用中的例子。用户可能已经理解了双删的基本流程，但对延迟的具体作用和设置方法不太清楚，需要用易懂的方式解释。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;首先，要解释延迟的定义：在写数据库完成后，等待一段时间再执行第二次删缓存的操作。然后，说明为什么需要这个延迟——覆盖数据库写操作的耗时，确保所有可能在风险窗口内的读请求已经完成缓存填充，这样第二次删除才能有效清除旧值。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;接下来，需要具体说明延迟时间的确定方法。比如，要考虑数据库写操作的最大耗时，加上网络延迟、应用处理时间等。举个例子，假设写数据库平均耗时200ms，最大可能到500ms，那么延迟可以设置为1秒（500ms*2），确保所有可能的读请求已经完成。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;然后，可以用时间线的例子来演示延迟的作用，比如没有延迟的话，第二次删除可能在数据库还没写完时就执行，导致无效；而有了延迟，确保数据库已经更新完成，此时删除缓存才能真正清除可能存在的旧值。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;还要提到实际应用中如何实现这个延迟，比如使用定时任务、消息队列的延迟投递，或者简单的线程睡眠（但要注意分布式系统中的问题）。同时，需要指出延迟时间不是固定的，需要根据业务场景调整，比如高并发系统可能需要更长的延迟，而低并发可以缩短。&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;最后，总结延迟的核心目的是确保第二次删除的有效性，避免缓存中残留旧值，从而减少不一致的时间窗口。同时，保持回答的结构，符合之前的格式要求：标题带emoji，解释部分分点或举例，最后提一个问题引导用户继续提问。&amp;lt;br&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;缓存双删中“延迟时间”的作用与设计逻辑 ⏳🔍&lt;/p&gt;
&lt;p&gt;你问到了双删策略的核心细节！这里的“延迟”不是随意等待，而是经过设计的“时间缓冲带”，它的存在直接决定了双删策略能否有效减少缓存与数据库的不一致。我们通过“为什么需要延迟”“如何确定延迟时长”“实际如何实现”三个维度来拆解。&lt;/p&gt;
&lt;h3&gt;一、为什么需要“延迟时间”？——覆盖“风险窗口”的残留旧值&lt;/h3&gt;
&lt;p&gt;在双删流程中，第一次删缓存（T0）后，可能有读请求在“写数据库前”（T0-T1）从数据库读取旧值并重新写入缓存（比如读请求A在T0-T1期间将旧值100元写回缓存）。此时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库会在T1时刻更新为新值200元；&lt;/li&gt;
&lt;li&gt;但缓存中已被读请求A写入了旧值100元（这是问题的根源）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有延迟，第二次删缓存（T2）会在写数据库（T1）完成后&lt;strong&gt;立即执行&lt;/strong&gt;。但此时可能存在两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读请求A的缓存写入操作（旧值100元）还未完成（网络延迟或线程调度延迟），导致第二次删除时缓存中还没有旧值，删除无效；&lt;/li&gt;
&lt;li&gt;即使删除了旧值，若后续还有其他读请求在“写数据库完成后、第二次删除前”再次读取到旧值，仍会导致短暂不一致。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;延迟的作用&lt;/strong&gt;：让“第二次删除”等待足够长的时间，确保所有在“写数据库前”触发的读请求（如读请求A）已经完成缓存写入（旧值100元），此时再删除，就能彻底清除这些残留的旧缓存。&lt;/p&gt;
&lt;h3&gt;二、如何确定“延迟时长”？——基于数据库写操作的耗时&lt;/h3&gt;
&lt;p&gt;延迟时间的核心依据是“数据库写操作的最大耗时”（包括事务提交、主从同步等）。具体计算逻辑如下：&lt;/p&gt;
&lt;h4&gt;公式：延迟时间 ≈ 数据库写操作的最大耗时 × 2（或1.5~3倍）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据库写操作耗时&lt;/strong&gt;：指从执行写命令（如&lt;code&gt;UPDATE user SET balance=200 WHERE id=1&lt;/code&gt;）到数据库确认写入完成（如事务提交、主库同步到从库）的时间。例如，在MySQL中，写操作可能需要50ms~200ms（高并发时可能更长）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;乘以倍数的原因&lt;/strong&gt;：覆盖网络延迟、应用服务器与数据库的通信耗时，以及可能的读请求处理延迟（如读请求A的线程可能因CPU调度延迟执行缓存写入）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：&lt;br /&gt;
假设数据库写操作的最大耗时是300ms（包括主从同步），那么延迟时间可以设置为500ms~1秒。这样，即使读请求A在写数据库完成后（T1）才完成缓存写入（旧值100元），第二次删除（T2=T1+500ms）也能在缓存写入完成后执行，确保旧值被清除。&lt;/p&gt;
&lt;h3&gt;三、实际如何实现“延迟删除”？——技术方案与注意事项&lt;/h3&gt;
&lt;p&gt;在代码中实现“延迟删除”有以下常见方式：&lt;/p&gt;
&lt;h4&gt;1. 线程睡眠（简单但局限）&lt;/h4&gt;
&lt;p&gt;在写数据库完成后，让当前线程休眠指定时间（如500ms），然后执行第二次删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 伪代码示例  
public void updateUserBalance(Long userId, Integer newBalance) {  
    // 第一次删缓存  
    redis.del(&quot;user:balance:&quot; + userId);  

    // 写数据库（假设耗时200ms）  
    userMapper.updateBalance(userId, newBalance);  

    // 延迟500ms（覆盖写操作耗时+可能的读请求处理）  
    Thread.sleep(500);  

    // 第二次删缓存  
    redis.del(&quot;user:balance:&quot; + userId);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;局限&lt;/strong&gt;：在分布式系统中，若多个实例同时更新同一数据，可能因线程休眠导致性能问题；且无法精准控制延迟时间（受JVM调度影响）。&lt;/p&gt;
&lt;h4&gt;2. 定时任务/延迟队列（更可靠）&lt;/h4&gt;
&lt;p&gt;将第二次删除操作提交到延迟队列（如RocketMQ的延迟消息、Redis的ZSET+定时扫描），由队列在指定时间后触发删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用RocketMQ延迟消息示例  
Message message = new Message(  
    &quot;cacheDeleteTopic&quot;,  
    &quot;user:balance:&quot; + userId,  // 消息内容为缓存键  
    (&quot;deleteCache:&quot; + userId).getBytes()  
);  
message.setDelayTimeLevel(3);  // 延迟10秒（具体级别根据MQ配置）  
producer.send(message);  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;/strong&gt;：解耦业务逻辑，避免阻塞主线程；支持分布式场景，延迟时间更精准。&lt;/p&gt;
&lt;h4&gt;3. 异步线程池（折衷方案）&lt;/h4&gt;
&lt;p&gt;通过线程池提交一个异步任务，任务中先休眠指定时间，再执行删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 伪代码示例  
ExecutorService executor = Executors.newSingleThreadExecutor();  
executor.submit(() -&amp;gt; {  
    try {  
        Thread.sleep(500);  // 延迟500ms  
        redis.del(&quot;user:balance:&quot; + userId);  
    } catch (InterruptedException e) {  
        Thread.currentThread().interrupt();  
    }  
});  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四、极端情况：延迟时间不够会怎样？&lt;/h3&gt;
&lt;p&gt;如果延迟时间过短（如小于数据库写操作耗时），可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二次删除时，数据库写操作尚未完成，此时缓存中可能被读请求写入更早的旧值（数据库还未更新），导致删除无效；&lt;/li&gt;
&lt;li&gt;或者，读请求在“第二次删除后”才完成缓存写入（旧值），此时缓存中仍会残留旧值，需要等待缓存过期才能恢复一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结：延迟是双删策略的“时间保险栓”&lt;/h3&gt;
&lt;p&gt;延迟时间的设计本质是&lt;strong&gt;用时间换一致性&lt;/strong&gt;——通过等待足够长的时间，确保所有可能干扰缓存的读请求已完成旧值写入，再通过第二次删除彻底清除这些旧值。它是双删策略中“主动防御”的关键一环。&lt;/p&gt;
&lt;p&gt;关于延迟实现的具体技术细节（比如如何用Redis的ZSET实现延迟队列），或者不同业务场景下延迟时间的经验值（如电商库存vs用户信息），你更想深入了解哪部分？ 😊&lt;/p&gt;
</content:encoded></item><item><title>RedisZset实现原理</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/rediszset%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/rediszset%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis ZSet实现原理&lt;/h1&gt;
&lt;p&gt;是一种由跳表和哈希表组成的数据结构。ZSet结合了集合的特性和排序功能，能存储具有唯一性的成员，并且根据成员的分数进行排序。&lt;/p&gt;
&lt;p&gt;由&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跳表： 用于存储数据的排序和快速查找&lt;/li&gt;
&lt;li&gt;哈希表： 用于存储成员和它分数的映射，提供快速查找&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当元素数量较少的时候，Redis采用压缩列表来节省内存。
元素个数&amp;lt;=zset-max-ziplist-entries，并且每个元素的值小于zset-max-ziplist-value&lt;/p&gt;
&lt;p&gt;如果任何一个条件都不满足，Zset采用跳表加哈希表作为底层实现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/12i8qms-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis的Hash是什么</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E7%9A%84hash%E6%98%AF%E4%BB%80%E4%B9%88/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E7%9A%84hash%E6%98%AF%E4%BB%80%E4%B9%88/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis的Hash是什么&lt;/h1&gt;
&lt;p&gt;Redis的Hash是一种键值对集合，可以把多个字段和值存储在同一个键中，便于管理一些关联数据&lt;/p&gt;
&lt;p&gt;比如一个用户信息，可以存储在Hash中，key为user:1，value为name:张三,age:18,gender:男
命令： HSET user:1 name 张三 age 18 gender 男&lt;/p&gt;
&lt;p&gt;适合存储小数据，能够在内存中高效存储和操作
支持快速字段操作，比如增删改查，适合存储对象属性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/111qad1-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/111rxfa-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Hash底层&lt;/h1&gt;
&lt;p&gt;Hash是Redis中的一种数据基础数据结构，类似于数据结构中的哈希表，一个Hash可以存储2的32次方-1个键值对（差不多40亿）。底层结构按照Redis版本分成两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis6.0之前，Hash的底层是压缩列表加上哈希表的数据结构（ziplist + hashtable）&lt;/li&gt;
&lt;li&gt;Redis7之后，Hash的底层是紧凑列表(Listpack)加上哈希表的数据结构（Listpack + hashtable）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ziplist和listpack的效率差不多，时间复杂度都是O(n)
但是listpack解决了ziplist的级联更新问题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/114aurs-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;hash-max-ziplist-entries 512：这是 Hash 类型使用 ziplist（压缩列表）编码的最大条目数阈值。当 Hash 的字段-值对数量 ≤ 512 时，Redis 会优先使用 ziplist 进行内存高效存储，以减少开销。
hash-max-ziplist-value 64：这是每个字段名（key）和值（value）的最大字节长度阈值。当所有字段名和值的字节长度均 ≤ 64 字节时，结合条目数阈值，Hash 会被编码为 ziplist。
优化目的：ziplist 是一种紧凑的序列化存储方式，能显著降低内存使用（平均节省 5 倍，最高 10 倍），但当超过阈值时，Redis 会自动转换为标准哈希表（hashtable），以保证性能。
Redis 版本注意：在 Redis 7.0 中，ziplist 被 listpack 取代（后者是其改进版），配置参数也相应更新为 hash-max-listpack-entries 和 hash-max-listpack-value，但默认值和行为保持一致。为了向后兼容，旧参数仍被支持作为别名。你的描述适用于 7.0 及更早版本。&lt;/p&gt;
&lt;h1&gt;Hashtable&lt;/h1&gt;
&lt;p&gt;当Hash的键值对数量超过hash-max-ziplist-entries或者键和值的长度大于hash-max-ziplist-value时，Redis会自动转换为Hashtable&lt;/p&gt;
&lt;p&gt;Hashtable其实就是哈希表实现，查询时间复杂度是O（1），效率很快&lt;/p&gt;
&lt;h1&gt;rehash&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/11b5y0t-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis跳表</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E8%B7%B3%E8%A1%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E8%B7%B3%E8%A1%A8/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis跳表&lt;/h1&gt;
&lt;p&gt;https://www.cnblogs.com/yfceshi/p/19079750&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/126uk28-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;跳表的应用场景&lt;/h2&gt;
&lt;p&gt;Redis 中的有序集合（Sorted Set，zset）&lt;/p&gt;
&lt;p&gt;用跳表实现的，可以敏捷完成范围查询和排序。&lt;/p&gt;
&lt;p&gt;内存数据库 / 搜索引擎&lt;/p&gt;
&lt;p&gt;用来做索引，加速查找。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/11/128jwcb-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis中常见的数据类型有哪些</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%93%AA%E4%BA%9B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%93%AA%E4%BA%9B/</guid><description>Redis中常见的数据类型有哪些</description><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://www.mianshiya.com/question/1780933295593254915&lt;/p&gt;
&lt;h1&gt;Redis中常见的数据类型有哪些&lt;/h1&gt;
&lt;p&gt;常见的五种数据结构&lt;/p&gt;
&lt;p&gt;5种数据类型示意图
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/suhd1q-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;String&lt;/h1&gt;
&lt;p&gt;字符串是Redis种最基本的数据类型，可以存储任何类型的数据，包括文本，数字和二进制数据，最大长度是512MB&lt;/p&gt;
&lt;p&gt;使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存： 存储临时数据，比如用户会话，页面缓存&lt;/li&gt;
&lt;li&gt;计数器： 用于统计访问量，点赞数等，通过原子操作增加或者减少&lt;/li&gt;
&lt;li&gt;分布式锁： 用于分布式锁，通过原子操作设置和释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/su1qk8-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/suonmx-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/suzbio-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sv0s0r-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sv2leb-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sv4dpg-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;List&lt;/h1&gt;
&lt;p&gt;列表是有序的字符串集合，支持从两端推入和弹出元素，底层实现是双向链表&lt;/p&gt;
&lt;p&gt;使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息队列： 用于简单任务调度，消息传递场景，通过LPUSH和RPOP操作实现生产者和消费者模式&lt;/li&gt;
&lt;li&gt;历史记录： 存储用户操作的历史记录，便于快速访问。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/svjzbo-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/svm8vj-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/svoe51-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/svqxit-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Set&lt;/h1&gt;
&lt;p&gt;集合是无需而且不重复的字符串集合，使用哈希表实现，支持快速查找和去重操作。
使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标签： 用于存储标签，便于快速查找&lt;/li&gt;
&lt;li&gt;集合运算： 用于存储集合，便于进行集合运算，如交集，差集，并集等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxh62n-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxjlh8-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxlmx2-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxn9zd-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxxtmw-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxzjm2-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sy11fg-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;ZSet&lt;/h1&gt;
&lt;p&gt;有序集合是按分数排序的字符串集合，使用跳表实现，支持快速查找和范围查询。
使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排行榜： 用于存储排行榜，便于快速查找
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sy75as-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sy8xwf-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sympbg-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/syqfgo-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sysm37-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/syuiyd-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sz50a7-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sz7ftu-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sz8r8r-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Hash&lt;/h1&gt;
&lt;p&gt;哈希是键值对的集合，使用哈希表实现，支持快速查找和存储对象。
使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象存储： 可以用来缓存对象，比如用户信息，商品信息等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sw5vdr-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sw8g23-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/swc1j3-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sweoot-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/swzinu-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/sxei2s-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;其它数据结构&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/szef96-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redis为什么快</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BF%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BF%AB/</guid><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis为什么快&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;使用内存存储&lt;/li&gt;
&lt;li&gt;Redis采用了IO多路复用技术的事件驱动模型来处理客户端请求，执行Redis命令&lt;/li&gt;
&lt;li&gt;Redis6.0引入多线程机制，把网络和I/O处理放到多个线程中，减少了单线程的瓶颈，网络IO交给线程池处理，命令仍然在主线程中进行。充分利用CPU多核的优势，提升了性能。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/sbddqg-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Redis 对底层数据结构做了极致的优化，比如说 String 的底层数据结构动态字符串支持动态扩容、预分配冗余空间，能够减少内存碎片和内存分配的开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1.基于内存，内存IO比磁盘快
2. 采用 单线程 + 非阻塞 I/O + I/O 多路复用（高效处理并发）模型
3. 网络IO用上了多线程
4. 底层数据结构被专门优化过&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单线程&lt;/strong&gt;：单线程可以避免多线程的数据竞争和上下文切换开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非阻塞IO&lt;/strong&gt;，Redis对客户端连接的I/O操作设置为非阻塞，主线程发起I/O操作后，不需要等待结果返回，可以继续处理其它事件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O多路复用&lt;/strong&gt;: 主线程可以同时监听多个客户端连接的I/O事件，一旦某个事件就绪，在进行集中处理。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>为什么Redis设计为单线程而后面版本为啥又引入多线程呢</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E4%B8%BA%E4%BB%80%E4%B9%88redis%E8%AE%BE%E8%AE%A1%E4%B8%BA%E5%8D%95%E7%BA%BF%E7%A8%8B%E8%80%8C%E5%90%8E%E9%9D%A2%E7%89%88%E6%9C%AC%E4%B8%BA%E5%95%A5%E5%8F%88%E5%BC%95%E5%85%A5%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%91%A2/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E4%B8%BA%E4%BB%80%E4%B9%88redis%E8%AE%BE%E8%AE%A1%E4%B8%BA%E5%8D%95%E7%BA%BF%E7%A8%8B%E8%80%8C%E5%90%8E%E9%9D%A2%E7%89%88%E6%9C%AC%E4%B8%BA%E5%95%A5%E5%8F%88%E5%BC%95%E5%85%A5%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%91%A2/</guid><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;为什么Redis设计为单线程而后面版本为啥又引入多线程呢&lt;/h1&gt;
&lt;h2&gt;单线程设计原因：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Redis基于内存，大多数操作性能瓶颈来自于CPU&lt;/li&gt;
&lt;li&gt;使用单线程模型，代码简单，处理逻辑清晰，也减少了上下文切换带来的性能开销。&lt;/li&gt;
&lt;li&gt;Redis在单线程情况下，使用I/O多路复用模型，就能提高Redis的I/O利用率了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;6.0 引入多线程的原因&lt;/h2&gt;
&lt;p&gt;主要是为了应对网络I/O的瓶颈，提高网络I/O处理速度。&lt;/p&gt;
&lt;p&gt;随着数据规模的增长和请求量的增加，Redis的执行瓶颈主要在网络I/O，引入多线程能提高网络I/O处理速度。&lt;/p&gt;
&lt;h2&gt;Redis引入多线程后，有没有线程安全问题&lt;/h2&gt;
&lt;p&gt;没有
Redis6.0只针对网络请求模块采用的是多线程，对于读写命令部分还是用的单线程，所以所谓的线程安全问题也就不存在了。&lt;/p&gt;
&lt;p&gt;Redis6.0想要开启多线程，需要配置&lt;code&gt;io-threads-do-reads&lt;/code&gt;参数为yes&lt;/p&gt;
</content:encoded></item><item><title>MySQL中如何解决深度分页问题</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%B7%B1%E5%BA%A6%E5%88%86%E9%A1%B5%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%B7%B1%E5%BA%A6%E5%88%86%E9%A1%B5%E9%97%AE%E9%A2%98/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL中如何解决深度分页问题&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/xp3bld-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/xh9385-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;深度分页，指的是当数据量很大的时候，按照分页访问后面的数据，例如 &lt;code&gt;limit 9999990,10&lt;/code&gt; 这会使得数据库要扫描前面的9999990条数据，才能得到最终的10条数据，大批量的扫描数据会增加数据库的负载，影响性能。&lt;/p&gt;
&lt;h2&gt;三种优化方式&lt;/h2&gt;
&lt;h3&gt;记录id&lt;/h3&gt;
&lt;p&gt;每次分页都返回当前的最大id，然后下次查询的时候，带上这个id，就能利用id &amp;gt; maxid过滤了。
这种查询适合 连续查询的情况，如果跳页的话就不生效了。&lt;/p&gt;
&lt;p&gt;普通分页的痛点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM article ORDER BY id LIMIT 10 OFFSET 100000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 需要先扫描并跳过 前 100000 行，然后再返回后 10 行。（这里说下底层原因）&lt;/p&gt;
&lt;p&gt;OFFSET 越大，性能越差。&lt;/p&gt;
&lt;p&gt;我们每次查询时带上 上一次返回的最大 id，下一页就只要取 id &amp;gt; last_id 的记录。&lt;/p&gt;
&lt;p&gt;不依赖 OFFSET，直接利用索引顺序扫描。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from products limit 0,10; -- 第一页
select * from products where id &amp;gt; 10 limit 10; -- 第二页
select * from products where id &amp;gt; 20 limit 10; -- 第三页
select * from products where id &amp;gt; 30 limit 10; -- 第四页
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;子查询&lt;/h3&gt;
&lt;p&gt;这里其实和记录id的优化方式是一样的，只不过这里用的是子查询。理论上我们应该先去查询到上一页的最大id，然后再查询下一页的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * from products where id &amp;gt; (
SELECT id from products order by created_at desc limit 199999,1) order by created_at desc limit 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们给表的created_at建索引，可以利用created_at的二级索引进行扫描，然后利用id &amp;gt; 上一次查询的最大id进行过滤，最后再利用created_at的二级索引进行排序，最后再利用limit进行分页。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/xmphei-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;子查询只读索引列（最好覆盖：筛选列 + 排序列 + id），IO 最小化。
外层 JOIN 回表范围仅为一页大小（如 20 条），成本可控。&lt;/p&gt;
&lt;p&gt;相较于原来的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM products order by created_at desc limit 200000,10;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个虽然可以利用created_at的二级索引进行扫描，但是它需要对每条记录进行一次回表操作，还要丢弃掉前200000条记录，性能较差。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/xn0e7q-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;join方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SELECT p.* FROM products p INNER JOIN (
  SELECT id FROM products ORDER BY created_at DESC limit 10 OFFSET 200000 ) AS page_results on p.id = page_results.id order by p.created_at desc;
  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个和上面的子查询方式是一样的，只不过这里用的是join。&lt;/p&gt;
&lt;h3&gt;使用es&lt;/h3&gt;
&lt;p&gt;直接上elasticsearch，利用它本身分页的特性，进行优化。&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;use pages;
-- 创建商品表
CREATE TABLE `products` (
  `id` BIGINT AUTO_INCREMENT COMMENT &apos;自增主键ID&apos;,
  `product_name` VARCHAR(255) NOT NULL COMMENT &apos;商品名称&apos;,
  `category_id` INT NOT NULL COMMENT &apos;分类ID&apos;,
  `price` DECIMAL(10, 2) NOT NULL COMMENT &apos;价格&apos;,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT &apos;创建时间&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=&apos;商品表&apos;;

-- 在排序字段上创建索引，这是至关重要的
CREATE INDEX `idx_created_at` ON `products` (`created_at`);

-- （可选）创建一个更实用的联合索引，例如按分类查找再按时间排序
CREATE INDEX `idx_category_created` ON `products` (`category_id`, `created_at`);


-- 修改MySQL的语句结束符，以便在存储过程中使用分号
DELIMITER $$

-- 创建一个名为 insert_mock_products 的存储过程
CREATE PROCEDURE `insert_mock_products`(IN insert_count INT)
BEGIN
    -- 定义一个循环计数器
    DECLARE i INT DEFAULT 1;

    -- 开始循环
    WHILE i &amp;lt;= insert_count DO
        INSERT INTO `products` (
            `product_name`,
            `category_id`,
            `price`,
            `created_at`
        ) VALUES (
            -- 生成一个像 &apos;Product 123&apos; 这样的随机商品名
            CONCAT(&apos;Product &apos;, i),
            -- 生成一个 1 到 50 之间的随机分类ID
            FLOOR(1 + RAND() * 50),
            -- 生成一个 10.00 到 1000.99 之间的随机价格
            ROUND(10 + RAND() * 990.99, 2),
            -- 生成一个从现在开始，逐步往前推移的时间，确保时间戳的唯一和顺序性
            -- 这里用秒作为递减单位，可以确保排序的稳定性
            DATE_SUB(NOW(), INTERVAL i SECOND)
        );
        -- 计数器加1
        SET i = i + 1;
    END WHILE;
END$$

-- 将语句结束符恢复为默认的分号
DELIMITER ;

-- 调用存储过程，并传入你想要插入的数据量
CALL insert_mock_products(1000000);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>什么是MySQL的主从同步机制</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E4%BB%80%E4%B9%88%E6%98%AFmysql%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E4%BB%80%E4%B9%88%E6%98%AFmysql%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是MySQL的主从同步机制&lt;/h1&gt;
&lt;p&gt;MySQL的主从同步机制是一种数据复制技术，用于将住数据库上的数据同步到一个或者多个从数据库中。
主要是通过二进制日志 binlog 实现数据的复制。
主数据库在执行写操作的时候，会把这些操作记录在binlog里面，然后推送给从数据库，从数据库重放对应的日志即可完成复制。&lt;/p&gt;
&lt;h1&gt;MySQL主从复制类型&lt;/h1&gt;
&lt;p&gt;MySQL支持异步复制，同步复制，半同步复制&lt;/p&gt;
&lt;p&gt;异步复制： 主库不需要等待从库的响应（性能高，一致性低）
同步复制： 主库同步等待所有从库确认收到的数据（性能差，一致性高）
半同步复制： 主库等待至少一个从库确认收到数据（性能折中，数据一致性比较高）&lt;/p&gt;
&lt;h2&gt;异步复制&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/y283pk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MySQL默认是异步复制。&lt;/p&gt;
&lt;h1&gt;主从复制流程&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;线程创建，从服务器创建一个IO线程，一个SQL线程，IO线程负责读取主服务器上的binlog，并写入到本地relay log中，SQL线程负责读取relay log中的日志，并执行到从服务器上&lt;/li&gt;
&lt;li&gt;连接建立： 从服务器的IO线程与主服务器建立连接，主服务器的binlog dump线程和从服务器的IO线程进行交互&lt;/li&gt;
&lt;li&gt;从服务器的IO线程告诉主服务器开始日志传送的对应位置&lt;/li&gt;
&lt;li&gt;主服务器更新的时候把记录保存到binlog中&lt;/li&gt;
&lt;li&gt;主服务器dump线程检测到binlog变化，从指定位置开始读取，从服务器进行拉取。&lt;/li&gt;
&lt;li&gt;中继日志存储： 从服务器的IO线程把接收到的内容保存到relay log中&lt;/li&gt;
&lt;li&gt;数据写入： 从服务器的SQL线程读取relay log中的内容，进行数据写入。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;主从复制延迟&lt;/h1&gt;
&lt;p&gt;主从复制延迟是指主服务器和从服务器之间数据同步的时间差。
主从复制延迟的原因有很多，例如网络延迟，主服务器和从服务器之间的硬件差异，主服务器和从服务器之间的操作系统差异，主服务器和从服务器之间的MySQL版本差异，主服务器和从服务器之间的MySQL配置差异等。&lt;/p&gt;
&lt;p&gt;解决方法：
优化网络
提高从服务器性能
利用MySQL并行复制功能提升效率，减少延迟。https://blog.csdn.net/weixin_42587823/article/details/144842206&lt;/p&gt;
</content:encoded></item><item><title>如何处理MySQL的主从同步延迟</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86mysql%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E5%BB%B6%E8%BF%9F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86mysql%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E5%BB%B6%E8%BF%9F/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;如何处理MySQL的主从同步延迟&lt;/h1&gt;
&lt;p&gt;当我们开启主从同步以后，这种延迟就是必然存在的，不论怎么优化都是没办法避免延迟的存在的，只能说去减少延迟的时间。&lt;/p&gt;
&lt;p&gt;常见的解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二次查询。如果说从库查不到数据，就去主库查一遍，用API封装这个逻辑就行，当作兜底策略。不过这样等于读的压力又转移到主库上去了，如果有人故意查询不存在的记录，那就会把查询的读请求都打到主库上了。&lt;/li&gt;
&lt;li&gt;强制把写之后立马读的操作转移到主库上（写后读主策略）。 写请求完成后，后续一段时间或者同一会话内的查询强制路由到主库，或者在从库追上指定位点前都读主。这个方法虽然简单可靠，能保证用户操作后的可见性，但是会增加主库的读压力，削弱负载分摊。&lt;/li&gt;
&lt;li&gt;关键业务读写都走主库。像我们用户注册这种，就可以读写主库，就不会出现说登录报用户不存在的问题了，这种访问量的频率也不高。&lt;/li&gt;
&lt;li&gt;使用缓存。 主库写入一行同步到缓存里面，这样查询的时候可以先查缓存，避免延迟，但是这样又有数据一致性问题了，我们就要去考虑数据库和缓存的数据一致性问题了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有可能是 &lt;strong&gt;主库的配置高，从库的配置低&lt;/strong&gt;，这样的话，也会导致主从同步延迟，我们可以提高从库的配置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/10/k90qnk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>搭建MySQL主从服务器</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E6%90%AD%E5%BB%BAmysql%E4%B8%BB%E4%BB%8E%E6%9C%8D%E5%8A%A1%E5%99%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E6%90%AD%E5%BB%BAmysql%E4%B8%BB%E4%BB%8E%E6%9C%8D%E5%8A%A1%E5%99%A8/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;买服务器&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zh72vh-1.webp&quot; alt=&quot;&quot; /&gt;
先买两台服务器，装ubuntu系统&lt;/p&gt;
&lt;h1&gt;安装mysql&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zgrhyz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zh539x-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zhoxxy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zhr1zz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;修改配置，让两个服务器能够互相连接&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zhxile-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开启监听&lt;/h2&gt;
&lt;p&gt;root@ecs-f95f-0002:/etc/mysql/mysql.conf.d# vim mysqld.cnf&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zlvtzi-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;修改从库
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zmmg8c-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;root@ecs-f95f-0001:/etc/mysql/mysql.conf.d# sudo systemctl restart mysql
root@ecs-f95f-0002:/etc/mysql/mysql.conf.d# sudo systemctl restart mysql&lt;/p&gt;
&lt;p&gt;重启一下主库和从库的mysql&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zo9kra-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/zoe4t0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为root开启远程访问&lt;/h2&gt;
&lt;p&gt;mysql&amp;gt; ALTER USER &apos;root&apos;@&apos;%&apos; IDENTIFIED WITH mysql_native_password BY &apos;root&apos;;
Query OK, 0 rows affected (0.00 sec)&lt;/p&gt;
&lt;p&gt;mysql&amp;gt; FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)&lt;/p&gt;
&lt;p&gt;启用密码验证&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10irv8h-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10iulwy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在可以连接上了&lt;/p&gt;
&lt;h2&gt;修改主库配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
server-id = 153
# 启用二进制日志功能，这是复制的基础
log-bin                 = /var/log/mysql/mysql-bin.log
# (可选) 设置二进制日志的格式，建议使用ROW格式，可以更好地保证数据一致性
binlog_format           = ROW
binlog_ignore_db        = mysql

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建远程用户&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;-- 创建远程用户
CREATE USER &apos;repl_user&apos;@&apos;%&apos; IDENTIFIED WITH mysql_native_password BY &apos;remote&apos;;
-- 给予复制权限
 GRANT REPLICATION SLAVE ON *.* TO &apos;repl_user&apos;@&apos;%&apos;;

-- 刷新权限
FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10notnv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 锁定所有表，防止新的数据写入，确保数据一致性
FLUSH TABLES WITH READ LOCK;

-- 查看主服务器状态
SHOW MASTER STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10o5bfr-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;备份主数据库，传到从服务器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt; mysqldump -u root -p --all-databases --source-data &amp;gt; ./master_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10pdpxq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10qxsbs-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建密钥&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;密钥创建好以后，把公钥放到从服务器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.93
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10r22n7-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10rla5b-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host mysql-slave
    HostName 192.168.0.93
    User root
    Port 22
    IdentityFile ~/.ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10txbl0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从服务器配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.153
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10s7nn8-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10sa32o-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host mysql-master
    HostName 192.168.0.153
    User root
    Port 22
    IdentityFile ~/.ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10tejl0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从主服务器上把备份好的数据库传到从库上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@ecs-f95f-0002:~# scp master_backup.sql mysql-slave:~
master_backup.sql                                             
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从库已经可以看到了
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/10ujgs1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;导入
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/122nj25-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主库解锁
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/122tx0s-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从库mysql配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
server-id = 93
relay-log       = /var/log/mysql/mysql-relay-bin
read-only       = 1
# (可选但推荐) 记录从服务器的数据更改到自己的二进制日志，以便将来可以作为其他从服务器的主服务器
log-bin         = /var/log/mysql/mysql-bin.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启从库并且登录从库
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/125oncs-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stop slave;
CHANGE MASTER TO 
    MASTER_HOST = &apos;192.168.0.153&apos;,
    MASTER_USER = &apos;repl_user&apos;,
    MASTER_PASSWORD = &apos;remote&apos;,
    MASTER_LOG_FILE = &apos;mysql-bin.000005&apos;,
    MASTER_LOG_POS = 157;
START SLAVE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/129zoko-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
mysql&amp;gt; show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.0.153
                  Master_User: repl_user
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000005
          Read_Master_Log_Pos: 1826
               Relay_Log_File: mysql-relay-bin.000002
                Relay_Log_Pos: 326
        Relay_Master_Log_File: mysql-bin.000005
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 1826
              Relay_Log_Space: 536
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 153
                  Master_UUID: 014654cd-8d83-11f0-b940-fa163e8fe780
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 
            Executed_Gtid_Set: 
                Auto_Position: 0
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
            Network_Namespace: 
1 row in set, 1 warning (0.00 sec)

ERROR: 
No query specified
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/12i6cmk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在主库里面
创建库，表，插入数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建 UTF8MB4 数据库
CREATE DATABASE testdb
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

use testdb;
CREATE TABLE user_data (
    id INT AUTO_INCREMENT PRIMARY KEY,        -- 主键，自动递增
    username VARCHAR(255) NOT NULL,           -- 用户名，最大长度255
    comment TEXT CHARACTER SET utf8mb4        -- 评论，支持存储表情符号和其他Unicode字符
) ENGINE=InnoDB CHARSET=utf8mb4;

-- 插入常规数据
INSERT INTO user_data (username, comment) VALUES (&apos;Alice&apos;, &apos;Hello, world!&apos;);

-- 插入带表情符号的数据
INSERT INTO user_data (username, comment) VALUES (&apos;Bob&apos;, &apos;This is fun! 😊&apos;);

-- 插入中文字符
INSERT INTO user_data (username, comment) VALUES (&apos;Charlie&apos;, &apos;你好，世界！&apos;);

-- 插入混合内容
INSERT INTO user_data (username, comment) VALUES (&apos;David&apos;, &apos;中文＋Emoji 🌟😄&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/12jzoy9-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/12k2qso-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们看看从库状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/09/12kp3qc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL中发生死锁如何解决</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%8F%91%E7%94%9F%E6%AD%BB%E9%94%81%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%8F%91%E7%94%9F%E6%AD%BB%E9%94%81%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3/</guid><description>MySQL中发生死锁如何解决</description><pubDate>Mon, 08 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/10jgi3w-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;自动检测与回滚&lt;/h1&gt;
&lt;p&gt;MySQL自带死锁检测机制（innodb_deadlock_detect），当检测到死锁的时候，数据库会自动回滚其中一个事务，以接触死锁，通常会回滚事务中持有最少资源的那个。&lt;/p&gt;
&lt;p&gt;也有锁等待超时的参数（innodb_lock_wait_timeout），当锁等待超过这个时间后，MySQL会自动回滚。&lt;/p&gt;
&lt;h1&gt;手动kill发生死锁的语句&lt;/h1&gt;
&lt;p&gt;可以通过命令，手动快速找出被阻塞的事务以及线程ID，然后手动Kill掉，及时释放资源。&lt;/p&gt;
&lt;h1&gt;常见降低/排除死锁出现情况的方法&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;避免大事务： 大事务占据锁耗时长，可以把大事务拆分成多个小事务执行快速释放锁，可以降低死锁产生的概率和冲突&lt;/li&gt;
&lt;li&gt;调整申请锁的顺序： 在写操作的时候保证能获得足够范围的锁，如修改操作的时候先获取排他锁再获取共享锁，固定顺序访问数据&lt;/li&gt;
&lt;li&gt;更改数据隔离级别： 可重复读比读已提交多了间隙锁和临键锁，使用读已提交能降低死锁出现的情况。&lt;/li&gt;
&lt;li&gt;合理建立索引，减少加锁范围&lt;/li&gt;
&lt;li&gt;开启死锁检测，适当调整锁等待超时时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/11d3nuc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/11d5hm7-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;实际测试&lt;/h1&gt;
&lt;p&gt;innodb_print_all_deadlocks：开启死锁打印&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show VARIABLES like &apos;innodb_print_all_deadlocks&apos;;

set GLOBAL innodb_print_all_deadlocks = 1;

flush PRIVILEGES;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
create table deadlock_test (
  id bigint not null,
  name varchar(255),
  primary key(id)
);
insert into deadlock_test values(1, &apos;zhangsan&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/10tx11y-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/10u159n-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show engine innodb status;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经典“交叉持锁、互等”死锁：
事务 (1) 当前语句是 select * from deadlock_test where id = 1 for update，日志显示它已持有 id=2 的记录锁，正等待获取 id=1 的锁。
事务 (2) 当前语句是 select * from deadlock_test where id = 2 for update，日志显示它已持有 id=1 的记录锁，正等待获取 id=2 的锁。
二者形成环：T1 持 id=2 等 id=1；T2 持 id=1 等 id=2。
锁类型：lock_mode X locks rec but not gap 为记录级行锁（非 GAP 锁），锁定的是主键记录本身。
仲裁结果：InnoDB 回滚了事务 (2)（“WE ROLL BACK TRANSACTION (2)”），说明它评估回滚成本更低（未必是开始时间靠后）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-09-08 14:25:29 138110019045056
*** (1) TRANSACTION:
TRANSACTION 48777, ACTIVE 96 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 638, OS thread handle 138111594002112, query id 479129 10.0.0.8 root statistics
select * from deadlock_test where id = 1 for update

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48777 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 00000000be89; asc       ;;
 2: len 7; hex 82000001230110; asc     #  ;;
 3: len 7; hex 77616e6773616e; asc wangsan;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48777 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;
 1: len 6; hex 00000000be80; asc       ;;
 2: len 7; hex 01000000be2b14; asc      + ;;
 3: len 4; hex 6c697369; asc lisi;;


*** (2) TRANSACTION:
TRANSACTION 48768, ACTIVE 149 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 636, OS thread handle 138111598196416, query id 479151 10.0.0.8 root statistics
select * from deadlock_test where id = 2 for update

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48768 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;
 1: len 6; hex 00000000be80; asc       ;;
 2: len 7; hex 01000000be2b14; asc      + ;;
 3: len 4; hex 6c697369; asc lisi;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48768 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 00000000be89; asc       ;;
 2: len 7; hex 82000001230110; asc     #  ;;
 3: len 7; hex 77616e6773616e; asc wangsan;;

*** WE ROLL BACK TRANSACTION (2)
------------
TRANSACTIONS
------------
Trx id counter 48783
Purge done for trx&apos;s n:o &amp;lt; 48783 undo n:o &amp;lt; 0 state: running but idle
History list length 1
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 419586600079360, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 419586600080976, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 419586600080168, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 419586600078552, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 419586600077744, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 419586600076936, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 48777, ACTIVE 121 sec
5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 638, OS thread handle 138111594002112, query id 479167 10.0.0.8 root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过MySQL系统库查询被阻塞的事务以及线程ID，手动kill释放资源&lt;/p&gt;
&lt;p&gt;查询锁信息表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 8.0 版本以前
select * from information_schema.innodb_locks;
-- 8.0版本开始
select * from performance_schema.data_locks;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关闭死锁检测&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;innodb_deadlock_detect&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/110r5za-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
SET GLOBAL innodb_deadlock_detect = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/111e4i9-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们再次开两个事务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/112zyds-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/11321yv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/114vhsk-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查询锁等待信息表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 8.0版本之前
select * from information_schema.innodb_lock_waits;
-- 8.0版本开始
select * from performance_schema.data_lock_waits;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/114xwmu-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/115w1hh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/116hus4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查询innodb事务信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * from information_schema.INNODB_TRX;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/11a35ow-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/118hy05-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/118d2ss-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/118f5gd-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/118gs9v-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 列出当前的阻塞者（含可直接 KILL 的进程号）
SELECT
  b.ENGINE_TRANSACTION_ID   AS blocking_trx_id,
  th.PROCESSLIST_ID         AS blocking_pid,
  trx.trx_started,
  trx.trx_state,
  trx.trx_rows_locked,
  trx.trx_query
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks b
  ON w.blocking_engine_lock_id = b.engine_lock_id
JOIN information_schema.INNODB_TRX trx
  ON b.engine_transaction_id = trx.trx_id
JOIN performance_schema.threads th
  ON b.thread_id = th.thread_id
GROUP BY blocking_trx_id, blocking_pid, trx.trx_started, trx.trx_state, trx.trx_rows_locked, trx.trx_query;

-- 杀掉阻塞会话
KILL CONNECTION &amp;lt;blocking_pid&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/08/119g2ic-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL事务的两阶段提交是什么</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E6%98%AF%E4%BB%80%E4%B9%88/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E6%98%AF%E4%BB%80%E4%B9%88/</guid><pubDate>Mon, 08 Sep 2025 00:00:00 GMT</pubDate><content:encoded/></item><item><title>MySQL-VARCHAR支持的最大长度</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-varchar%E6%94%AF%E6%8C%81%E7%9A%84%E6%9C%80%E5%A4%A7%E9%95%BF%E5%BA%A6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql-varchar%E6%94%AF%E6%8C%81%E7%9A%84%E6%9C%80%E5%A4%A7%E9%95%BF%E5%BA%A6/</guid><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MySQL中，最大行长度限制为65535字节，如果一行中仅仅有一个varchar字段，它的最大长度是多少呢？
（InnoDB/MyISAM 中一行最大长度限制是 65535 字节（65 KB 左右）。）
长度&amp;gt; 255,存储varchar长度需要2字节，长度&amp;lt;255，存储varchar长度需要1字节。&lt;/p&gt;
&lt;p&gt;所以&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当长度&amp;gt;255且非空的时候，可以存储65535 - 2 = 65533 字节。&lt;/li&gt;
&lt;li&gt;当长度&amp;gt;255且可以为空的时候，可以存储65535 - 2 - 1（存储NULL标志） = 65532字节。&lt;/li&gt;
&lt;li&gt;当长度&amp;lt;255且非空的时候，可以存储65535 - 1 = 65534 字节。&lt;/li&gt;
&lt;li&gt;当长度&amp;lt;255且可以为空的时候，可以存储65535 - 1 - 1（存储NULL标志） = 65533字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果只有一个 VARCHAR 字段&lt;/p&gt;
&lt;p&gt;假设表里只有这一列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  v VARCHAR(N)
) ENGINE=InnoDB;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行最大长度：65535 字节。&lt;/p&gt;
&lt;p&gt;除了数据外，还有：&lt;/p&gt;
&lt;p&gt;NULL 标志位（至少 1 字节，即使只有一列）。&lt;/p&gt;
&lt;p&gt;VARCHAR 长度字节（1 或 2）。&lt;/p&gt;
&lt;p&gt;所以最大能用来存储 v 的 = 65535 - 1 (NULL 标志) - 2 (长度字节) = 65532 字节。&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;p&gt;✅ 单列 VARCHAR 最大可定义为 VARCHAR(65532)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果是非null，就不需要占用那一字节null标志位了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  v VARCHAR(N) NOT NULL 
) ENGINE=InnoDB;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里N就可以是65533了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/xz52lm-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/xy0628-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL中CHAR和VARCHAR的区别</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%ADchar%E5%92%8Cvarchar%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%ADchar%E5%92%8Cvarchar%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/xy0628-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;CHAR(n)&lt;/h1&gt;
&lt;p&gt;char(n) 是固定长度的字符串，CHAR列的长度是固定的，即使存储的字符串长度小于定义的长度，MySQL也会在字符串的末尾填充空格以达到指定的长度。&lt;/p&gt;
&lt;h1&gt;VARCHAR(n)&lt;/h1&gt;
&lt;p&gt;可变长度的字符串，varchar列的长度是可变的，存储的字符串长度与实际数据长度相等，并且在存储数据的时候会额外增加1到2个字节（字符串长度超过255，就用两个字节） 用于存储字符串的长度信息。&lt;/p&gt;
&lt;p&gt;理论上char比varchar会快，因为varchar长度不固定，处理需要多一次运算，但是实际上这种运算耗时微乎其微，而固定大小在很多场景下比较浪费空间，除非存储的字符确认是固定大小或者本身就很短，不然业务上推荐使用varchar.&lt;/p&gt;
</content:encoded></item><item><title>MySQL中几种count的区别</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%87%A0%E7%A7%8Dcount%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E5%87%A0%E7%A7%8Dcount%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><description>MySQL中count(*),count(1)和count(字段名)的区别</description><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;count(*) 和 count(1)&lt;/h1&gt;
&lt;p&gt;是用来统计行数的聚合函数，统计表中的全部行的数量，包括 null 值&lt;/p&gt;
&lt;h1&gt;count(字段名)&lt;/h1&gt;
&lt;p&gt;也是用来统计行数的聚合函数，会统计指定字段下不为 null 的行数，这种写法会对指定的字段进行计数，只会统计字段值不为 null 的行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/zckucc-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/zebv8e-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL中的数据排序是怎么实现的</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E6%8E%92%E5%BA%8F%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E7%9A%84/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E6%8E%92%E5%BA%8F%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E7%9A%84/</guid><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded/></item><item><title>MySQL事务隔离级别</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB/</guid><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/yscnoh-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;1 读未提交（脏读，不可重复读，幻读 问题）&lt;/h1&gt;
&lt;p&gt;最低的事务隔离级别，在这个事务隔离级别下，一个事务能看到另外一个事务未提交的数据修改，会导致 &lt;strong&gt;脏读&lt;/strong&gt; 的问题（读取到其他事务未提交的数据）&lt;/p&gt;
&lt;h1&gt;2 读已提交（不可重复读，幻读）&lt;/h1&gt;
&lt;p&gt;这个事务隔离级别虽然解决了脏读问题，也就是只能读取到另外一个事务已经提交的数据，读取不到另外一个事务没有提交的数据，但是它有&lt;strong&gt;不可重复读&lt;/strong&gt;的问题（同一个事务中，相同的查询会返回不同的结果）&lt;/p&gt;
&lt;h1&gt;3 可重复读(幻读) MySQL 默认事务隔离级别&lt;/h1&gt;
&lt;p&gt;这个事务隔离级别，使用 MVCC（快照读）的方式，解决了不可重复读的问题，但是还是有&lt;strong&gt;幻读&lt;/strong&gt;的问题（幻读也就是在一个事务中，读取到另外一个事务插入的行，导致这个事务查询到的结果集行数不同）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/09/07/yu38tz-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;4 串行化&lt;/h1&gt;
&lt;p&gt;最高的事务隔离级别，使用排他锁（Exclusive Lock）来保证事务的完全隔离。&lt;/p&gt;
</content:encoded></item><item><title>动态规划解题思路</title><link>https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E8%A7%A3%E9%A2%98%E6%80%9D%E8%B7%AF/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E8%A7%A3%E9%A2%98%E6%80%9D%E8%B7%AF/</guid><pubDate>Sun, 17 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;动态规划解题思路&lt;/h1&gt;
&lt;h2&gt;斐波那契数列&lt;/h2&gt;
&lt;p&gt;暴力递归&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; // f(n) 计算第 n 个斐波那契数
    static int fib(int n) {
        if(n &amp;lt;= 1 &amp;amp;&amp;amp; n &amp;gt;= 0) {
            return n;
        }
        return fib(n - 1) + fib(n - 2);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用Memo数组存储，避免重复计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
    static long fib2(int n) {
        long[] memo = new long[n + 1];
        Arrays.fill(memo,-1);
        return dp(memo,n);

    }
    static long dp(long[] memo,int n) {
        if(n == 0 || n == 1) {
            return n;
        }
        if(memo[n] != -1) {
            return memo[n];
        }
        memo[n] = dp(memo,n - 1) + dp(memo,n - 2);
        return memo[n];
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;零钱兑换&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/17/julgjy-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        Arrays.fill(dp,amount + 1);
        dp[0] = 0;
        for(int i = 0;i&amp;lt;dp.length;i++) {
            for(int coin : coins) {
                if(i - coin &amp;lt; 0) {
                    continue;
                }
                dp[i] = Math.min(dp[i],1 + dp[i - coin]);
            }
        }
        return (dp[amount] == amount + 1) ? -1 : dp[amount];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    int[] memo;

    public int coinChange(int[] coins, int amount) {
        memo = new int[amount + 1];
        // 备忘录初始化为一个不会被取到的特殊值，代表还未被计算
        Arrays.fill(memo, -666);

        return dp(coins, amount);
    }

    int dp(int[] coins, int amount) {
        if (amount == 0) return 0;
        if (amount &amp;lt; 0) return -1;
        // 查备忘录，防止重复计算
        if (memo[amount] != -666)
            return memo[amount];

        int res = Integer.MAX_VALUE;
        for (int coin : coins) {
            // 计算子问题的结果
            int subProblem = dp(coins, amount - coin);
            // 子问题无解则跳过
            if (subProblem == -1) continue;
            // 在子问题中选择最优解，然后加一
            res = Math.min(res, subProblem + 1);
        }
        // 把计算结果存入备忘录
        memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
        return memo[amount];
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MySQL索引类型有哪些</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%93%AA%E4%BA%9B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%93%AA%E4%BA%9B/</guid><pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL 索引类型有哪些&lt;/h1&gt;
&lt;h2&gt;按数据结构分&lt;/h2&gt;
&lt;p&gt;分为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;B-Tree 索引&lt;/li&gt;
&lt;li&gt;Hash 索引&lt;/li&gt;
&lt;li&gt;Full-text 索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/15/5dxy1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建表的时候，InnoDB 存储引擎会根据不同的场景选择不同的列作为索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有主键：会使用主键作为聚簇索引的索引键&lt;/li&gt;
&lt;li&gt;没有主键： 选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键&lt;/li&gt;
&lt;li&gt;上面两个都没有的情况下，InnoDB 会自动生成一个隐式自增 id 列作为聚簇索引的索引键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他索引都属于辅助索引，也就是非聚簇索引或者二级索引。&lt;/p&gt;
&lt;h2&gt;按物理存储分&lt;/h2&gt;
&lt;p&gt;分为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚簇索引（主键索引）&lt;/li&gt;
&lt;li&gt;二级索引（辅助索引）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;按字段特性分&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;主键索引&lt;/li&gt;
&lt;li&gt;唯一索引&lt;/li&gt;
&lt;li&gt;普通索引&lt;/li&gt;
&lt;li&gt;前缀索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主键索引是唯一的，且不允许为 NULL。每个表只能有一个主键索引。&lt;/p&gt;
&lt;h2&gt;按字段个数分&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单列索引&lt;/li&gt;
&lt;li&gt;联合索引&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;按数据结构分&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;B+树索引&lt;/li&gt;
&lt;li&gt;哈希索引&lt;/li&gt;
&lt;li&gt;倒排索引（Full-text 索引）&lt;/li&gt;
&lt;li&gt;R-树索引 （多维树空间）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从 InnoDB b+树索引来看，分为聚簇索引和非聚簇索引
聚簇索引也就是主键索引，叶子节点存储整行的数据，非叶子节点存储主键值和指向子节点的指针。
非聚簇索引叶子节点存储主键，非叶子节点存储主键值和指向子节点的指针。
因此，非聚簇索引查询需要回表查询&lt;/p&gt;
&lt;h1&gt;从索引性质看&lt;/h1&gt;
&lt;p&gt;有
普通索引
主键索引
唯一索引
联合索引
全文索引
空间索引&lt;/p&gt;
</content:encoded></item><item><title>flowable学习</title><link>https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/flowable%E5%AD%A6%E4%B9%A0/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E5%B7%A5%E4%BD%9C/flowable%E5%AD%A6%E4%B9%A0/</guid><pubDate>Tue, 12 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/12/h1jv9s-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发布流程后，会在下面几张表里有记录
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/12/h9knpj-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/12/h9qh26-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/12/h9ujzp-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/13/xu7b2n-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>HashMap和ConcurrentHashMap的区别</title><link>https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/hashmap%E5%92%8Cconcurrenthashmap%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/hashmap%E5%92%8Cconcurrenthashmap%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JDK1.7版本&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内存结构： HashMap采用数组+链表的结构，数组是HashMap的主体，链表用于解决哈希冲突。当两个不同的键通过哈希函数计算得到相同的索引时，它们会被存储在同一个数组位置的链表中。ConcurrentHashMap在JDK1.7中采用了分段锁的机制，内部是一个Segment数组，每个Segment类似一个小的HashMap，有自己的数组和链表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程安全性： HashMap不是线程安全的，在多线程环境下，如果多个线程同时对HashMap进行读写操作，可能会导致数据不一致，死循环的问题。ConcurrentHashMap是线程安全的，它通过分段锁的机制来保证并发访问时的线程安全。只有当多个线程访问同一个Segment时，才会发生锁竞争，从而提高了并发性能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能： hashmap由于没有锁的开销，所以在单线程环境下性能较好，但是在多线程环境下，为了保证线程安全，需要额外的同步机制，这回降低性能。但是ConcurrentHashMap通过分段所机制，在多线程环境下可以实现更高的并发性能，不同的线程可以同时访问不同的Segment，从而减少了锁竞争的可能性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JDK1.8版本&lt;/h1&gt;
&lt;p&gt;内存结构： hashMap引入了红黑树，从Jdk1.8开始，hashmap采用数组+ 链表+ 红黑树的结构。当链表长度超过一定阈值（8）的时候，链表会转换为红黑树，小于6的时候会转换为链表，以提高查找效率。ConcurrentHashMap放弃了分段锁机制，采用&lt;code&gt;CAS + synchronized&lt;/code&gt;的方式保证线程安全，内部结构和HashMap一样，也引入了红黑树，是数组+ 链表+ 红黑树的结构。&lt;/p&gt;
&lt;p&gt;线程安全性： ConcurrentHashMap通过CAS和synchronized的方式保证线程安全。在插入元素的时候，首先会尝试用CAS更新节点，如果CAS失败，则使用synchronized锁住当前节点，再进行插入操作。&lt;/p&gt;
&lt;p&gt;性能： hashmap在单线程环境下，由于红黑树的引入，当链表较长的时候查找效率会有所提升。ConcurrentHashMap在多线程环境下，由于摒弃了分段锁，减少了锁的粒度，进一步提高了并发性能。同时，红黑树的引入也提高了查找效率。&lt;/p&gt;
</content:encoded></item><item><title>MVCC-多版本并发控制</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mvcc/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mvcc/</guid><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MVCC&lt;/h1&gt;
&lt;p&gt;MVCC，也就是多版本并发控制&lt;/p&gt;
&lt;p&gt;它的目的是： 提高数据库并发性能，用更好的方式处理读写冲突，也就是即使有读写冲突的时候，也能做到不加锁。&lt;/p&gt;
&lt;h2&gt;并发控制的挑战&lt;/h2&gt;
&lt;p&gt;在数据库系统中，同时执行的事务可能涉及相同的数据，因此需要一种机制来保证数据的一致性，传统的锁机制可以实现并发控制，但会导致阻塞和死锁等问题。&lt;/p&gt;
&lt;h2&gt;传统锁机制&lt;/h2&gt;
&lt;h2&gt;当前读和快照读&lt;/h2&gt;
&lt;h3&gt;当前读&lt;/h3&gt;
&lt;p&gt;在MySQL中，当前读是一种读取数据的操作方式，它可以直接读取最新的数据版本，读取时还要保证其他并发事务不能修改当前记录，会对读取的记录进行加锁，MySQL提供了两种实现当前读的机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁定读：
&lt;ul&gt;
&lt;li&gt;锁定读是一种特殊情况下的当前读方式，在某些场景下使用&lt;/li&gt;
&lt;li&gt;在使用锁定读的时候，MySQL会在执行读取操作前获取共享锁或者排他锁，确保数据一致性。&lt;/li&gt;
&lt;li&gt;共享锁允许多个事务读取统一数据，而排他锁组织其他事务读取或者写入该数据。&lt;/li&gt;
&lt;li&gt;锁定读适用于需要严格控制并发访问的场景，但是由于加锁带来的性能开销较大，所以只在必要的时候才使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/lsvw6z-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/ltng7m-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种就属于悲观锁实现。&lt;/p&gt;
&lt;h3&gt;快照读&lt;/h3&gt;
&lt;p&gt;快照读就是在读取数据的时候，读取一个一致性视图中的数据，MySQL通过MVCC机制来支持快照读。&lt;/p&gt;
&lt;p&gt;具体而言，每个食物在开始的时候都会创建一个一致性视图，这个一致性视图会记录当前事务开始时已经提交的数据版本。&lt;/p&gt;
&lt;p&gt;执行查询的时候，MySQL会根据事务的一致性视图来决定可见的数据版本。只有那些在事务开始之前就已经提交的数据版本才是可见的，未提交或在事务开始后修改的数据则对当前事务不可见。&lt;/p&gt;
&lt;p&gt;像不加锁的select操作就是快照读，也就是不加锁的非阻塞读。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一致性读：
&lt;ul&gt;
&lt;li&gt;默认隔离级别下（可重复读），MySQL使用一致性来实现当前读&lt;/li&gt;
&lt;li&gt;在事务开始的时候，MySQL会创建一个一致性视图，这个视图反映了事务开始时刻的数据库快照。&lt;/li&gt;
&lt;li&gt;在事务执行期间，无论其他事务对数据进行了何种修改，事务始终使用一致性视图来读取数据。&lt;/li&gt;
&lt;li&gt;可以保证在同一事务内多次查询返回的结果是一致的.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/m7rmhb-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;快照读的前提是隔离级别不是串行级别，在串行级别下，事务之间完全串行执行，快照读会退化为当前读中的加锁读。&lt;/p&gt;
&lt;p&gt;MVCC主要就是为了实现读-写冲突不加锁，这个读就是指的快照读，是乐观锁的实现。&lt;/p&gt;
&lt;h1&gt;事务的mvcc机制原理是什么？&lt;/h1&gt;
&lt;p&gt;MVCC允许多个事务同时读取同一行数据，而不会彼此阻塞，每个事务看到的数据版本是该事务开始时候的数据版本，这意味着，如果其他事务在此期间修改了数据，正在运行的事务仍然看到的是它开始时候的数据状态，从而实现了非阻塞读操作。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;读已提交&lt;/code&gt; 和 &lt;code&gt;可重复读&lt;/code&gt; 隔离级别的事务来说，它们是通过ReadView来实现的，它们的区别在于创建ReadView的时机不同。
ReadView可以理解为当时的一个快照视图，它记录了在创建时刻可见的数据版本。&lt;/p&gt;
&lt;p&gt;读提交隔离级别： 在每个select语句执行前，都会重新生成一个ReadView。每个SELECT生成新的ReadView
只能读到其他事务已提交的版本
不能读到未提交事务的修改
这保证了不会出现&quot;脏读&quot;
但会出现&quot;不可重复读&quot;&lt;/p&gt;
&lt;p&gt;可重复读隔离级别： 在事务中，执行第一条select语句的时候，生成一个ReadView，然后整个事务期间都在使用这个ReadView&lt;/p&gt;
&lt;p&gt;ReadView有四个重要字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;creator_trx_id 创建该Read View的事务的事务id&lt;/li&gt;
&lt;li&gt;m_ids 创建ReadView的时候，当前数据库中活跃且未提交的事务id列表，所谓活跃事务，指的就是启动了但是还没提交的事务&lt;/li&gt;
&lt;li&gt;min_trx_id 创建ReadView的时候当前数据库中活跃且未提交的事务中最小的事务的事务id&lt;/li&gt;
&lt;li&gt;max_trx_id 创建ReadView的时候，当前数据库中应该给下一个事务的id值，也就是全局事务中最大的事务id + 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于使用InnoDB存储引擎的数据库表，它的聚簇索引记录中都包含下面两个隐藏列&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trx_id 记录最后修改该行数据的事务的事务id&lt;/li&gt;
&lt;li&gt;roll_pointer 记录该行数据的回滚指针，用于实现MVCC（也就是undo日志）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次对某条聚簇索引记录进行改动的时候，都会把旧版本的记录写入到undo日志中，然后这个隐藏列是个指针，指向每个旧版本记录，于是就可以通过它找到修改前的记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/plbf8e-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个事务去访问记录的时候，除了自己的更新记录总是可见之外，还有这几种情况：&lt;/p&gt;
&lt;p&gt;如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值，表示这个版本的记录是在创建 Read View 前已经提交的事务生成的，所以该版本的记录对当前事务可见。
如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值，表示这个版本的记录是在创建 Read View 后才启动的事务生成的，所以该版本的记录对当前事务不可见。
如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间，需要判断 trx_id 是否在 m_ids 列表中：
如果记录的 trx_id 在 m_ids 列表中，表示生成该版本记录的活跃事务依然活跃着（还没提交事务），所以该版本的记录对当前事务不可见。
如果记录的 trx_id 不在 m_ids列表中，表示生成该版本记录的活跃事务已经被提交，所以该版本的记录对当前事务可见。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC（多版本并发控制）。&lt;/p&gt;
</content:encoded></item><item><title>MySQLbinlog,redolog和undolog</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysqlbinlogredolog%E5%92%8Cundolog/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysqlbinlogredolog%E5%92%8Cundolog/</guid><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MySQL的binlog、redolog和undolog详解&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/kh6tf8-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;binlog&lt;/h2&gt;
&lt;p&gt;binlog
用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主从复制&lt;/li&gt;
&lt;li&gt;数据恢复&lt;/li&gt;
&lt;li&gt;审计&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;redolog（保证持久性）&lt;/h2&gt;
&lt;p&gt;redo log
目的： 确保事务的持久性
作用： 记录了数据被修改之后的值。当事务提交以后，即使数据还没有完全写入磁盘，只要redo log已经落盘,数据库在发生宕机等意外情况之后，仍然可以通过redo log来&apos;重做&apos;这些修改，从而恢复到宕机前的最新状态，保证了已提交事务的数据不可丢失，这是一种前滚操作。&lt;/p&gt;
&lt;h2&gt;undolog（保证原子性）&lt;/h2&gt;
&lt;p&gt;目的： 保证事务的原子性和实现多版本并发控制。
作用： 记录的是数据被修改之前的旧版本。当一个事务需要回滚的时候，数据库可以利用undo log中的信息将数据恢复到事务开始前的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/lnk04g-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/lmvm5q-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;区别&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/kexum3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/kgn410-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL全局锁，表级锁，行级锁</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E7%9A%84%E5%85%A8%E5%B1%80%E9%94%81%E8%A1%A8%E7%BA%A7%E9%94%81%E8%A1%8C%E7%BA%A7%E9%94%81%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/mysql%E4%B8%AD%E7%9A%84%E5%85%A8%E5%B1%80%E9%94%81%E8%A1%A8%E7%BA%A7%E9%94%81%E8%A1%8C%E7%BA%A7%E9%94%81%E6%9C%BA%E5%88%B6/</guid><description>MySQL中的全局锁，表级锁，行级锁机制</description><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/qrldag-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;MySQL的锁&lt;/h1&gt;
&lt;h2&gt;全局锁&lt;/h2&gt;
&lt;p&gt;如果要使用全局锁
要执行下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flush table with read lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/quecvs-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;执行全局锁以后，数据库就变成只读状态了，插入和更新操作都会被阻塞&lt;/p&gt;
&lt;p&gt;这个全局锁一般是用于数据库全局备份的。在备份数据库期间，不会因为数据和表结构的更新，出现备份文件的数据和预期的不一样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/qwcl26-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到会卡主&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/qwsqeo-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解锁以后就可以插入了&lt;/p&gt;
&lt;p&gt;备份数据库的时候又不想停机，可以在用 mysqldump的时候加上 --single-transaction参数，就会在备份数据之前先开启事务。这种方法只适用于支持可重复读隔离级别的事务的存储引擎。&lt;/p&gt;
&lt;h2&gt;表级锁&lt;/h2&gt;
&lt;p&gt;MySQL中的表级锁有哪些？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁&lt;/li&gt;
&lt;li&gt;元数据锁&lt;/li&gt;
&lt;li&gt;意向锁&lt;/li&gt;
&lt;li&gt;AUTO-INC锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;表锁&lt;/h3&gt;
&lt;p&gt;如果我们相对student表加上表锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 允许当前会话读取被锁定的表，但是会组织其他会话对这些表进行写操作
lock table student_t read;

-- 表级别的独占锁，也就是写锁
-- 允许当前会话对表进行读写操作，但会阻止其他会话对这些表进行任何操作
lock table student_t write;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，表锁除了会限制别的线程的读写外，也会限制本线程接下来的读写操作。&lt;/p&gt;
&lt;h3&gt;元数据锁&lt;/h3&gt;
&lt;p&gt;元数据锁不需要显示调用，因为当我们对数据库表进行操作的时候，会自动给这个表加上MDL&lt;/p&gt;
&lt;p&gt;当我们对一张表进行CRUD操作的时候，加的是MDL读锁
当我们对一张表做结构变更操作的时候，加的是MDL写锁&lt;/p&gt;
&lt;p&gt;MDL是为了保证当用户对表执行CRUD操作的时候，防止其他线程对这个表结构做变更。&lt;/p&gt;
&lt;p&gt;比如说，一个线程正在执行查询操作（加了MDL读锁），如果有其他线程来修改表结构，就会被阻塞，直到查询结束。
同理，一个线程在修改表结构的时候（申请了MDL写锁），其他线程的查询操作就会被阻塞，直到说表结构变更完成&lt;/p&gt;
&lt;h3&gt;意向锁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在使用InnoDB引擎的表里对某些记录加上共享锁之前，需要先在表级别上加一个意向共享锁。&lt;/li&gt;
&lt;li&gt;在使用InnoDB引擎的表里对某些记录加上独占锁之前，需要先在表级别加上一个意向独占锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通的select是不会加行级锁的，因为它是用MVCC（多版本并发控制）实现的，是无锁的。&lt;/p&gt;
&lt;p&gt;不过select也是可以对记录加共享锁和独占锁的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//先在表上加上意向共享锁，然后对读取的记录加共享锁
select ... lock in share mode;

//先表上加上意向独占锁，然后对读取的记录加独占锁
select ... for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;意向共享锁和意向独占锁是表级锁，不会和行级的共享锁和独占锁发生冲突，而且意向锁之间也不会发生冲突，只会和共享表锁和独占锁发生冲突。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;意向锁的目的是为了快速判断表里是否有记录被加锁。
比如说，当一个事务想要对某个记录加锁时，可以先检查表级的意向锁，如果表级的意向锁是共享锁，就说明有其他事务正在读取这个表中的记录；如果是独占锁，就说明有其他事务正在修改这个表中的记录。&lt;/p&gt;
&lt;p&gt;如果表级的意向锁是共享锁，那么其他事务可以对表上共享锁，但是不能加独占锁。如果是表级意向锁是独占锁，其他事务就不能对表上加任何锁。&lt;/p&gt;
&lt;h3&gt;AUTO-INC锁&lt;/h3&gt;
&lt;p&gt;表里的主键通常会设置成自增的，这是通过主键字段声明 AUTO_INCREMENT 属性实现的。&lt;/p&gt;
&lt;p&gt;之后可以在插入数据的时候，可以不指定主键的值，数据库会自动给主键赋值递增的值，这主要是通过 AUTO-INC锁实现的。&lt;/p&gt;
&lt;p&gt;AUTO-INC锁是特殊的表锁机制，锁不是在一个事务提交后才释放，而是在执行完插入语句后就会立刻释放。&lt;/p&gt;
&lt;p&gt;在插入数据的时候，会加一个表级别的AUTO-INC锁，然后为被 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 修饰的字段赋值递增的值，等插入语句执行完成后，才会把AUTO-INC锁释放掉。&lt;/p&gt;
&lt;p&gt;那么在一个事务持有AUTO-INC锁的过程中，其他事务如果要向该表插入语句都会被阻塞，从而保证了插入数据的时候，被AUTO_INCREMENT修饰的字段的值是连续递增的。&lt;/p&gt;
&lt;p&gt;因此，在MySQL5.1.22开始,InnoDB存储引擎提供了一种轻量级的锁来实现自增。&lt;/p&gt;
&lt;p&gt;一样也是在插入数据的时候，会为被auto_increment修饰的字段加上轻量级锁，然后给该字段赋值一个自增的值，然后就把这个轻量级锁释放了，不需要等待整个插入语句执行完成后才释放锁。&lt;/p&gt;
&lt;h2&gt;行级锁&lt;/h2&gt;
&lt;p&gt;InnoDB引擎是支持行级锁的，而MyISAM不支持行级锁&lt;/p&gt;
&lt;p&gt;可以使用下面这两个方式，这种查询会加锁的语句称为锁定读。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//对读取的记录加共享锁
select ... lock in share mode;

//对读取的记录加独占锁
select ... for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/10o8753-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;行级锁类型&lt;/h3&gt;
&lt;p&gt;有三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Record Lock，记录锁，也就是仅仅把一条记录锁上&lt;/li&gt;
&lt;li&gt;Gap Lock 间隙锁，锁定一个范围，但是不包含记录本身&lt;/li&gt;
&lt;li&gt;Next-Key Lock： Record Lock + Gap Lock的组合，锁定一个范围，并且锁定记录本身&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Record Lock 记录锁&lt;/h3&gt;
&lt;p&gt;Record Lock被称为记录锁，锁住的锁一条记录，而且记录锁是有S锁和X锁之分的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个事务对一条记录加了S型记录锁后，其他事务也可以继续对该记录加S型记录锁，但是不可以对该记录加X型记录锁&lt;/li&gt;
&lt;li&gt;当一个事务对一条记录加了X型记录锁后，其他事务不可以对该记录加S型记录锁，也不可对该记录加X型记录锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/10qlyoo-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Gap Lock 间隙锁&lt;/h3&gt;
&lt;p&gt;Gap Lock被称为间隙锁，存在于可重复读隔离级别和串行化隔离级别，目的是为了解决可重复读隔离级别下幻读的现象&lt;/p&gt;
&lt;p&gt;假设表中有一个范围id为(3,5)的间隙锁，那么其他事务就无法插入id = 4这条记录了，这样就有效地防止了幻读现象的发生。&lt;/p&gt;
&lt;p&gt;间隙锁虽然也存在X型和S型间隙锁，但是没什么区别，间隙锁之间是兼容的，两个事务可以同时持有并包含共同间隙范围的间隙锁，并不存在互斥关系，因为间隙锁的目的是防止插入幻影记录而提出的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/124cfwm-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/124ikdt-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/12542r3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Next-Key Lock 临键锁&lt;/h3&gt;
&lt;p&gt;Next-Key-Lock称为临键锁，是Record Lock和Gap Lock的组合。锁定一个范围，并且锁定记录本身。
假设表中有个范围id为(3,5]的next-key-lock，那么其它事务既不能插入id = 4的记录，也不能修改id = 5这条记录。&lt;/p&gt;
&lt;p&gt;所以next-key lock既能保护该记录，又能阻止其它事务将新记录插入到被保护记录前面的间隙中。&lt;/p&gt;
&lt;p&gt;Next-key lock 是数据库中 InnoDB 存储引擎（常见于 MySQL）使用的一种锁机制，主要用于防止 &lt;strong&gt;幻读（Phantom Read）&lt;/strong&gt; 问题，确保事务在可重复读（Repeatable Read）隔离级别下的一致性。它的意义在于通过结合 &lt;strong&gt;记录锁（Record Lock）&lt;/strong&gt; 和 &lt;strong&gt;间隙锁（Gap Lock）&lt;/strong&gt;，对索引记录及其前后的间隙进行锁定，从而避免其他事务插入或修改数据导致的幻读现象。&lt;/p&gt;
&lt;h4&gt;具体意义和作用：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;防止幻读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;幻读是指在同一事务中，多次执行相同查询时，由于其他事务插入了新记录，导致查询结果集发生变化。&lt;/li&gt;
&lt;li&gt;Next-key lock 锁定一个索引记录及其前后的间隙，防止其他事务插入新记录到这个范围内，从而保证查询结果的稳定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结合记录锁和间隙锁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;记录锁&lt;/strong&gt;：锁定具体的索引记录，防止其他事务修改或删除该记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;间隙锁&lt;/strong&gt;：锁定索引记录之间的“间隙”，防止其他事务在该间隙内插入新记录。&lt;/li&gt;
&lt;li&gt;Next-key lock 是两者的结合，锁定一个记录及其左侧或右侧的间隙。例如，对于索引值 10，Next-key lock 可能锁定 (5, 10] 范围（假设 5 是前一个索引值）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提高并发控制的精度&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next-key lock 是一种范围锁，比表级锁更精细，能够在保证数据一致性的同时，尽量减少锁的粒度，提高并发性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;支持可重复读隔离级别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 MySQL 的可重复读（Repeatable Read）隔离级别下，Next-key lock 是默认的锁机制，用于确保事务在多次读取时看到一致的数据快照。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;工作原理：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当事务对某一行记录进行操作（例如 SELECT ... FOR UPDATE 或 UPDATE），InnoDB 会锁定该记录以及其前后的间隙。&lt;/li&gt;
&lt;li&gt;例如，假设表中有一个索引列 &lt;code&gt;id&lt;/code&gt; 包含值 10、20、30。如果事务 A 对 &lt;code&gt;id = 20&lt;/code&gt; 加锁，Next-key lock 可能会锁定 (10, 20] 或 (20, 30] 的范围，防止其他事务插入值在该范围内的记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;注意事项：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能影响&lt;/strong&gt;：Next-key lock 锁定范围较大，可能导致锁冲突，降低并发性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁风险&lt;/strong&gt;：多个事务竞争相同的间隙锁可能导致死锁，需要合理设计事务逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖索引&lt;/strong&gt;：Next-key lock 依赖于索引。如果查询没有使用索引，可能会退化为表级锁，影响性能。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结来说，Next-key lock 的核心意义在于通过锁定记录和间隙，防止幻读，维护事务隔离级别的一致性，同时在高并发场景下提供较好的数据保护机制。&lt;/p&gt;
&lt;h3&gt;插入意向锁&lt;/h3&gt;
&lt;p&gt;一个事务在插入一条记录的时候，需要判断插入位置是否已被其他事务加了间隙锁（next-key lock 也包含间隙锁）。&lt;/p&gt;
&lt;p&gt;如果有的话，插入操作就会发生阻塞，直到拥有间隙锁的那个事务提交为止（释放间隙锁的时刻），在此期间会生成一个插入意向锁，表明有事务想在某个区间插入新记录，但是现在处于等待状态。&lt;/p&gt;
</content:encoded></item><item><title>Spring中的BeanFactory与FactoryBean</title><link>https://blog.meowrain.cn/posts/java/spring/spring%E4%B8%AD%E7%9A%84beanfactory%E4%B8%8Efactorybean/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/spring%E4%B8%AD%E7%9A%84beanfactory%E4%B8%8Efactorybean/</guid><description>深入理解Spring容器的核心接口BeanFactory与特殊工厂Bean——FactoryBean的区别、使用场景与最佳实践</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;BeanFactory&lt;/h1&gt;
&lt;p&gt;BeanFactory是一个工厂接口，是一个负责生产和管理bean的一个工厂。BeanFactory是工厂的顶层接口，是IOC容器的核心接口，BeanFactory定义了管理Bean的通用方法，如getBean和containsBean等，它的职责包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bean实例化: 根据XML注解等创建Bean对象。&lt;/li&gt;
&lt;li&gt;依赖注入： 自动将Bean所需的依赖注入进去。&lt;/li&gt;
&lt;li&gt;生命周期管理： 管理Bean的初始化，销毁等生命周期方法。&lt;/li&gt;
&lt;li&gt;延迟加载： 默认采用懒加载策略，只有在调用getBean()时才创建Bean实例。&lt;/li&gt;
&lt;li&gt;Bean获取： 提供getBean()方法来获取Bean实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/08/fkc5xn-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BeanFactory只是一个接口，不是IOC容器的具体实现，所以Spring容器给出了很多种实现，如XmlBeanFactory、AnnotationConfigApplicationContext,ApplicationContext等。&lt;/p&gt;
&lt;h2&gt;BeanFactory 的常见实现类&lt;/h2&gt;
&lt;p&gt;Spring 提供了多种 BeanFactory 的实现，每种实现都有其特定的使用场景：&lt;/p&gt;
&lt;h3&gt;DefaultListableBeanFactory&lt;/h3&gt;
&lt;p&gt;最常用的完整实现，支持完整的Bean生命周期管理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

public class BeanFactoryExample {
    public static void main(String[] args) {
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        
        // 手动注册Bean定义
        RootBeanDefinition beanDefinition = new RootBeanDefinition(MyService.class);
        factory.registerBeanDefinition(&quot;myService&quot;, beanDefinition);
        
        // 获取Bean
        MyService service = factory.getBean(&quot;myService&quot;, MyService.class);
        service.doSomething();
    }
}

class MyService {
    public void doSomething() {
        System.out.println(&quot;Service is working!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;XmlBeanFactory（已废弃）&lt;/h3&gt;
&lt;p&gt;基于XML配置的BeanFactory实现，Spring 5.x后已废弃，推荐使用ApplicationContext：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统用法（不推荐）
// XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource(&quot;beans.xml&quot;));

// 现代替代方案
import org.springframework.context.support.ClassPathXmlApplicationContext;

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(&quot;beans.xml&quot;);
MyService service = context.getBean(&quot;myService&quot;, MyService.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;StaticListableBeanFactory&lt;/h3&gt;
&lt;p&gt;静态Bean工厂，适用于Bean集合固定的场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.support.StaticListableBeanFactory;

StaticListableBeanFactory factory = new StaticListableBeanFactory();
factory.addBean(&quot;myService&quot;, new MyService());
factory.addBean(&quot;anotherService&quot;, new AnotherService());

MyService service = factory.getBean(&quot;myService&quot;, MyService.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ApplicationContext实现类&lt;/h3&gt;
&lt;p&gt;作为BeanFactory的高级实现，提供更多企业级特性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注解配置
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

AnnotationConfigApplicationContext context = 
    new AnnotationConfigApplicationContext(AppConfig.class);

// XML配置
import org.springframework.context.support.ClassPathXmlApplicationContext;

ClassPathXmlApplicationContext xmlContext = 
    new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);

// Web环境
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

AnnotationConfigWebApplicationContext webContext = 
    new AnnotationConfigWebApplicationContext();
webContext.register(WebConfig.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实际应用示例&lt;/h3&gt;
&lt;p&gt;结合Bean定义构建器的完整示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

public class CustomBeanFactoryDemo {
    public static void main(String[] args) {
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        
        // 使用BeanDefinitionBuilder构建复杂Bean
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .rootBeanDefinition(DatabaseService.class)
            .addPropertyValue(&quot;url&quot;, &quot;jdbc:mysql://localhost:3306/mydb&quot;)
            .addPropertyValue(&quot;username&quot;, &quot;root&quot;)
            .setScope(&quot;singleton&quot;)
            .setLazyInit(true);
            
        factory.registerBeanDefinition(&quot;dbService&quot;, builder.getBeanDefinition());
        
        // 懒加载验证
        System.out.println(&quot;Bean定义已注册，但未实例化&quot;);
        
        DatabaseService dbService = factory.getBean(&quot;dbService&quot;, DatabaseService.class);
        System.out.println(&quot;现在Bean被实例化了&quot;);
    }
}

class DatabaseService {
    private String url;
    private String username;
    
    // getters and setters
    public void setUrl(String url) { this.url = url; }
    public void setUsername(String username) { this.username = username; }
    
    public void connect() {
        System.out.println(&quot;连接到: &quot; + url + &quot; 用户: &quot; + username);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BeanFactory 与 ApplicationContext 的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;预实例化策略
&lt;ul&gt;
&lt;li&gt;BeanFactory：单例默认懒加载。&lt;/li&gt;
&lt;li&gt;ApplicationContext：默认预实例化单例（提高启动后首次访问的吞吐）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;扩展能力
&lt;ul&gt;
&lt;li&gt;ApplicationContext 额外提供国际化、事件发布、AOP自动代理、资源模式解析等企业特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;适用场景
&lt;ul&gt;
&lt;li&gt;BeanFactory：资源受限、极致冷启动、强控制懒加载/条件加载。&lt;/li&gt;
&lt;li&gt;ApplicationContext：大多数应用优先选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;调优提示
&lt;ul&gt;
&lt;li&gt;需要懒加载时，可结合ApplicationContext + @Lazy 或者使用ObjectProvider/Provider按需获取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;FactoryBean&lt;/h1&gt;
&lt;p&gt;在Spring中，所有的Bean都是由BeanFactory管理的（IOC容器），
这个FactoryBean不是简单的Bean，而是一个能生产或者修饰对象生成的工厂Bean，它的实现与设计模式中的工厂模式和修饰器模式类似。&lt;/p&gt;
&lt;h2&gt;FactoryBean 的作用&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/08/gzh7kd-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将“复杂对象的创建逻辑”封装到工厂Bean中，对外暴露的是“产品对象”而不是工厂本身。&lt;/li&gt;
&lt;li&gt;常用于：动态代理（AOP/远程代理）、框架桥接（如MyBatis的SqlSessionFactoryBean）、复杂构建（连接池、客户端SDK）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心接口方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;getObject(): 返回实际产品对象（对外暴露的Bean）。&lt;/li&gt;
&lt;li&gt;getObjectType(): 返回产品类型，便于类型匹配与自动装配。&lt;/li&gt;
&lt;li&gt;isSingleton(): 决定产品是否为单例（影响缓存与生命周期）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;获取“产品”还是“工厂本身”&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;普通名：context.getBean(&quot;beanName&quot;) 获取的是产品对象（getObject返回值）。&lt;/li&gt;
&lt;li&gt;带&amp;amp;前缀：context.getBean(&quot;&amp;amp;beanName&quot;) 获取的是FactoryBean自身。&lt;/li&gt;
&lt;li&gt;命名规则：注册名为 x 的 FactoryBean，会对外暴露“产品”名为 x，“工厂自身”为 &amp;amp;x。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最小可运行示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 一个业务产品
public class ApiClient {
  private final String endpoint;
  public ApiClient(String endpoint) { this.endpoint = endpoint; }
  public String call(String path) { return &quot;GET &quot; + endpoint + path; }
}

// FactoryBean 实现
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

public class ApiClientFactoryBean implements FactoryBean&amp;lt;ApiClient&amp;gt;, InitializingBean {
  private String endpoint;
  public void setEndpoint(String endpoint) { this.endpoint = endpoint; }

  @Override
  public ApiClient getObject() {
    // 可在此放入复杂构建/代理/缓存等逻辑
    return new ApiClient(endpoint);
  }

  @Override
  public Class&amp;lt;?&amp;gt; getObjectType() { return ApiClient.class; }

  @Override
  public boolean isSingleton() { return true; }

  @Override
  public void afterPropertiesSet() {
    Assert.hasText(endpoint, &quot;endpoint must not be empty&quot;);
  }
}

// Java 配置注册 FactoryBean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
  @Bean(name = &quot;apiClient&quot;)
  public ApiClientFactoryBean apiClientFactoryBean() {
    ApiClientFactoryBean fb = new ApiClientFactoryBean();
    fb.setEndpoint(&quot;https://api.example.com&quot;);
    return fb;
  }
}

// 取产品与取工厂本身
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo {
  public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    ApiClient client = ctx.getBean(&quot;apiClient&quot;, ApiClient.class);     // 产品
    ApiClientFactoryBean fb = ctx.getBean(&quot;&amp;amp;apiClient&quot;, ApiClientFactoryBean.class); // 工厂本身
    System.out.println(client.call(&quot;/ping&quot;));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见坑与最佳实践&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;getObjectType 切勿返回 null：尽量返回接口/具体类型，便于按类型注入与AOT分析。&lt;/li&gt;
&lt;li&gt;isSingleton 与产品生命周期：单例将被容器缓存；非单例每次getBean都会重新创建产品。&lt;/li&gt;
&lt;li&gt;懒加载与预实例化：在ApplicationContext中，如希望延迟创建可使用@Lazy或将FactoryBean产品设为非单例。&lt;/li&gt;
&lt;li&gt;自动装配歧义：按类型注入时，注入到的是产品类型而非FactoryBean；需要注入工厂本身时使用@Qualifier(&quot;&amp;amp;name&quot;)或@Resource(name=&quot;&amp;amp;name&quot;)。&lt;/li&gt;
&lt;li&gt;原型产品与循环依赖：原型产品不参与循环依赖的三级缓存提前暴露，避免在原型链路中引入循环依赖。&lt;/li&gt;
&lt;li&gt;命名规范：确保文档/注释标明“&amp;amp;”语义，避免团队误用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候用哪一个？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;仅需容器功能：优先ApplicationContext（功能更全，默认预实例化）。&lt;/li&gt;
&lt;li&gt;需要懒加载到极致：考虑BeanFactory或在ApplicationContext中对关键Bean标注@Lazy。&lt;/li&gt;
&lt;li&gt;对象构建复杂/需代理/外部SDK桥接：使用FactoryBean封装构建细节，对外仅暴露产品。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BeanFactory 是IoC容器的最小抽象；ApplicationContext在其上增强了企业级特性。&lt;/li&gt;
&lt;li&gt;FactoryBean 是“创建Bean的Bean”，对外暴露产品；使用“&amp;amp;name”获取工厂本身。&lt;/li&gt;
&lt;li&gt;合理利用FactoryBean可显著简化复杂对象创建，并保持应用装配的清晰与解耦。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Spring配置相关的注解</title><link>https://blog.meowrain.cn/posts/java/spring/spring%E9%85%8D%E7%BD%AE%E7%9B%B8%E5%85%B3%E7%9A%84%E6%B3%A8%E8%A7%A3/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/spring%E9%85%8D%E7%BD%AE%E7%9B%B8%E5%85%B3%E7%9A%84%E6%B3%A8%E8%A7%A3/</guid><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当然！这是一个非常重要且实用的主题。在 Spring 和 Spring Boot 中，与属性（Property）相关的注解是实现“配置与代码分离”这一核心原则的关键。&lt;/p&gt;
&lt;p&gt;我将为你全面、系统地讲解这些注解，从最基础到最常用，再到高级用法，并配上清晰的示例。&lt;/p&gt;
&lt;p&gt;我们将主要围绕以下几个核心注解展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@Value&lt;/code&gt;&lt;/strong&gt;: 最基础的，用于注入单个属性值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@PropertySource&lt;/code&gt;&lt;/strong&gt;: 用于加载指定的属性文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@ConfigurationProperties&lt;/code&gt;&lt;/strong&gt;: 最强大、最推荐的，用于类型安全地将一组属性绑定到Java对象上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@EnableConfigurationProperties&lt;/code&gt;&lt;/strong&gt;: 与 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 配合使用，用于激活属性绑定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@TestPropertySource&lt;/code&gt;&lt;/strong&gt;: 在测试环境中加载或覆盖属性。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;注解族谱概览&lt;/h3&gt;
&lt;p&gt;为了方便理解，我们可以把它们分为三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;值注入 (Value Injection)&lt;/strong&gt;: &lt;code&gt;@Value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置源 (Configuration Source)&lt;/strong&gt;: &lt;code&gt;@PropertySource&lt;/code&gt;, &lt;code&gt;@TestPropertySource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量绑定 (Bulk Binding)&lt;/strong&gt;: &lt;code&gt;@ConfigurationProperties&lt;/code&gt;, &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;1. &lt;code&gt;@Value&lt;/code&gt;：简单直接的“单兵作战”&lt;/h3&gt;
&lt;p&gt;这是注入属性最基本的方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 将 Spring 环境（Environment）中的单个属性值注入到类的字段或方法参数中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语法&lt;/strong&gt;: 使用 SpEL (Spring Expression Language) 表达式 &lt;code&gt;&quot;${property.key}&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 简单、直接，适合注入少量、分散的配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;当属性很多时，代码会显得分散和混乱。&lt;/li&gt;
&lt;li&gt;类型安全性较弱（都是字符串，需要Spring转换）。&lt;/li&gt;
&lt;li&gt;重构时（如修改前缀）非常痛苦。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/main/resources/application.properties&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.name=My Awesome App
app.version=2.1.5
app.author.name=Alex
# 如果某个属性可能不存在，可以提供默认值
# mail.default.sender=default@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Java 类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppInfoService {

    // 注入 app.name 属性
    @Value(&quot;${app.name}&quot;)
    private String appName;

    // 注入 app.version 属性
    @Value(&quot;${app.version}&quot;)
    private String appVersion;
  
    // 注入一个不存在的属性，但提供了默认值 &quot;unknown&quot;
    @Value(&quot;${app.description:unknown description}&quot;)
    private String appDescription;

    // 也可以注入其他 Bean 的属性（使用 SpEL）
    @Value(&quot;#{someOtherBean.someProperty}&quot;)
    private String otherProperty;

    public void printAppInfo() {
        System.out.println(&quot;App Name: &quot; + appName);
        System.out.println(&quot;App Version: &quot; + appVersion);
        System.out.println(&quot;App Description: &quot; + appDescription);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;2. &lt;code&gt;@PropertySource&lt;/code&gt;：指定“情报来源”&lt;/h3&gt;
&lt;p&gt;默认情况下，Spring Boot 会自动加载 &lt;code&gt;application.properties&lt;/code&gt; 或 &lt;code&gt;application.yml&lt;/code&gt;。如果你想加载其他配置文件，就需要用到 &lt;code&gt;@PropertySource&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 将指定的属性文件加载到 Spring 的 &lt;code&gt;Environment&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;模块化配置，将不同功能的配置放在不同文件里（如 &lt;code&gt;mail.properties&lt;/code&gt;, &lt;code&gt;db.properties&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;加载 classpath 之外的文件系统中的配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;: 它只负责&lt;strong&gt;加载&lt;/strong&gt;，不负责注入。加载后，你可以用 &lt;code&gt;@Value&lt;/code&gt; 或 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 来使用这些属性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/main/resources/mail.properties&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mail.host=smtp.gmail.com
mail.port=587
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Java 配置类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.beans.factory.annotation.Value;

@Configuration
// 加载 classpath 下的 mail.properties 文件
@PropertySource(&quot;classpath:mail.properties&quot;)
public class MailConfig {

    @Value(&quot;${mail.host}&quot;)
    private String host;

    @Value(&quot;${mail.port}&quot;)
    private int port;
  
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3. &lt;code&gt;@ConfigurationProperties&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这是 Spring Boot &lt;strong&gt;最推荐&lt;/strong&gt;的属性管理方式。它将一组相关的属性映射到一个类型安全的 Java 对象（POJO）上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 将具有相同前缀的属性批量绑定到一个 POJO 的字段上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;: 直接映射到 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;Duration&lt;/code&gt; 等各种类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构清晰&lt;/strong&gt;: 将相关配置聚合在一个类中，非常易于管理和维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强大的绑定&lt;/strong&gt;: 支持复杂的对象图，比如嵌套对象、列表、Map等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE 友好&lt;/strong&gt;: 主流 IDE（如 IntelliJ IDEA）支持对 &lt;code&gt;application.properties&lt;/code&gt; 中这类属性的自动补全和导航（需要添加 &lt;code&gt;spring-boot-configuration-processor&lt;/code&gt; 依赖）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;application.properties&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.server.name=prod-server
app.server.ip-address=192.168.1.100
app.server.timeout=30s # Spring Boot 2.x 支持时间单位
app.server.admins[0].name=admin1
app.server.admins[0].email=admin1@corp.com
app.server.admins[1].name=admin2
app.server.admins[1].email=admin2@corp.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Java 属性类 (POJO)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.List;

// 告诉 Spring 这个类要绑定前缀为 &quot;app.server&quot; 的属性
@ConfigurationProperties(prefix = &quot;app.server&quot;)
public class ServerProperties {

    private String name;
    private String ipAddress;
    private Duration timeout; // 自动将 &quot;30s&quot; 转换为 Duration 对象
    private List&amp;lt;Admin&amp;gt; admins;
  
    // 嵌套类
    public static class Admin {
        private String name;
        private String email;
        // Getters and Setters for Admin
    }

    // ⭐ 重要: 必须为所有字段提供 public Getters and Setters
    // Spring 通过它们来注入值
    // ... Getters and Setters for ServerProperties ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;4. &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@ConfigurationProperties&lt;/code&gt; 只是一个声明，它本身不会让这个 POJO 成为一个 Spring Bean。你需要一种方式来“激活”它。&lt;code&gt;@EnableConfigurationProperties&lt;/code&gt; 就是这个开关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;告诉 Spring 去处理被 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 注解的类。&lt;/li&gt;
&lt;li&gt;将被注解的类（如 &lt;code&gt;ServerProperties&lt;/code&gt;）注册到 Spring 容器中，让它成为一个 Bean。这样你就可以在其他地方 &lt;code&gt;@Autowired&lt;/code&gt; 注入它了。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通常放在哪&lt;/strong&gt;: 主启动类或任何 &lt;code&gt;@Configuration&lt;/code&gt; 类上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
// 激活对 ServerProperties 类的绑定，并将其注册为 Bean
@EnableConfigurationProperties(ServerProperties.class)
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

// 现在可以在任何其他组件中注入它
@Service
public class MyService {
    private final ServerProperties serverProps;

    @Autowired
    public MyService(ServerProperties serverProps) {
        this.serverProps = serverProps;
        System.out.println(&quot;Server Name: &quot; + serverProps.getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;快捷方式&lt;/strong&gt;: 如果你在 &lt;code&gt;ServerProperties&lt;/code&gt; 类上同时加上 &lt;code&gt;@Component&lt;/code&gt; 和 &lt;code&gt;@ConfigurationProperties&lt;/code&gt;，就可以省略 &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt;。但显式使用 &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt; 通常被认为是更清晰的做法，因为它明确表达了这是一个配置类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;5. &lt;code&gt;@TestPropertySource&lt;/code&gt;：为测试“定制情报”&lt;/h3&gt;
&lt;p&gt;在进行单元测试或集成测试时，我们经常需要使用一套不同于生产环境的配置（比如连接到内存数据库 H2）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;: 在测试上下文中加载属性，它可以覆盖&lt;code&gt;application.properties&lt;/code&gt;中的属性或添加新属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常用属性&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;locations&lt;/code&gt;: 指定要加载的属性文件路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;properties&lt;/code&gt;: 以 &lt;code&gt;key=value&lt;/code&gt; 形式直接定义内联属性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.TestPropertySource;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
// 1. 加载 test.properties 文件
// 2. 直接定义一个内联属性，它会覆盖 test.properties 或 application.properties 中的同名属性
@TestPropertySource(
    locations = &quot;classpath:test.properties&quot;,
    properties = &quot;app.version=test-1.0&quot; 
)
public class AppInfoServiceTest {

    @Autowired
    private Environment env;

    @Test
    void testPropertiesAreLoaded() {
        // 来自 test.properties
        assertThat(env.getProperty(&quot;app.name&quot;)).isEqualTo(&quot;Test App&quot;);
      
        // 被内联属性覆盖
        assertThat(env.getProperty(&quot;app.version&quot;)).isEqualTo(&quot;test-1.0&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;总结与最佳实践&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;何时使用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@Value&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;注入单个值&lt;/td&gt;
&lt;td&gt;当你只需要一两个简单的配置时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@PropertySource&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;加载额外的属性文件&lt;/td&gt;
&lt;td&gt;当你的配置分散在多个自定义文件中时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@ConfigurationProperties&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;批量&lt;/strong&gt;、&lt;strong&gt;类型安全&lt;/strong&gt;地绑定属性到对象&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;首选方式&lt;/strong&gt;。当你有一组相关配置时（如数据库、邮件、API密钥等）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@EnableConfigurationProperties&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;激活 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 的类&lt;/td&gt;
&lt;td&gt;总是与 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 配合使用（除非用了&lt;code&gt;@Component&lt;/code&gt;快捷方式）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@TestPropertySource&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在测试中覆盖或提供配置&lt;/td&gt;
&lt;td&gt;编写需要特定配置的集成测试或单元测试时。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优先使用 &lt;code&gt;@ConfigurationProperties&lt;/code&gt;&lt;/strong&gt;：对于任何超过两三个的相关配置，都应创建一个专用的 &lt;code&gt;Properties&lt;/code&gt; 类。这会让你的代码更健壮、更易于维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集中管理&lt;/strong&gt;: 将 &lt;code&gt;@EnableConfigurationProperties&lt;/code&gt; 放在主配置类或一个集中的 &lt;code&gt;AppConfig&lt;/code&gt; 类中，而不是到处分散。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;利用元数据&lt;/strong&gt;: 添加 &lt;code&gt;spring-boot-configuration-processor&lt;/code&gt; 依赖到 &lt;code&gt;pom.xml&lt;/code&gt; 或 &lt;code&gt;build.gradle&lt;/code&gt;，以获得强大的 IDE 支持。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>redis配置json序列化</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E9%85%8D%E7%BD%AE%E5%BA%8F%E5%88%97%E5%8C%96/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E9%85%8D%E7%BD%AE%E5%BA%8F%E5%88%97%E5%8C%96/</guid><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis配置序列化&lt;/h1&gt;
&lt;p&gt;序列化的最终目的是为了对象可以跨平台存储，进行网络传输。
Redis默认用的是JdkSerializationRedisSerializer，它使用JDK提供的序列化功能，优点是反序列化的时候不需要提供类型信息，但缺点是序列化后的数据体积较大，性能较低。
因此，通常会使用更高效的序列化方式，如JSON、Protobuf等&lt;/p&gt;
&lt;p&gt;Jackson2JsonRedisSerializer： 使用Jackson库将对象序列化为JSON字符串。
优点是速度快，序列化后的字符串短小精悍，不需要实现Serializable接口。&lt;/p&gt;
&lt;p&gt;但缺点也非常致命，那就是此类的构造函数中有一个类型参数，必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码，发现其只在反序列化过程中用到了类型信息。&lt;/p&gt;
&lt;p&gt;现在的问题是： 如果使用默认的JDK序列化方式，在Redis可视化工具中查看kv值的时候会出现乱码，而使用Jackson2JsonRedisSerializer序列化后，kv值在Redis可视化工具中查看时是正常的。&lt;/p&gt;
&lt;p&gt;StringRedisTemplate → Key 和 Value 都是 String 序列化，简单粗暴，适合存验证码、token、计数器、纯 JSON 文本之类的轻量数据。&lt;/p&gt;
&lt;p&gt;自定义 RedisTemplate&amp;lt;String, Object&amp;gt; → 用了 JSON 序列化器，直接存 Java 对象，取出来就能反序列化成原类型，适合缓存业务对象、集合、复杂结构等。&lt;/p&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;
package org.example.config;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class MyRedisConfig {

    /**
     * RedisTemplate 配置类
     * - 使用自定义的 ObjectMapper + GenericJackson2JsonRedisSerializer
     * - 实现 key 用字符串序列化，value 用 JSON 序列化
     */
    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        // 创建 RedisTemplate，指定 key 类型为 String，value 类型为 Object
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();

        // 设置 Redis 连接工厂（由 Spring Boot 配置的连接信息决定）
        template.setConnectionFactory(factory);

        // 创建并配置 Jackson 的 ObjectMapper（用于 JSON 序列化/反序列化）
        ObjectMapper mapper = new ObjectMapper();

        // 设置可见性：让所有字段（包括 private）都参与序列化和反序列化
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 启用默认类型信息（多态类型序列化）
        // NON_FINAL 表示对所有非 final 类（如普通类、接口实现类）写入类型信息
        // 好处：反序列化时可以还原原始对象类型（避免反成 LinkedHashMap）
        mapper.activateDefaultTyping(
                mapper.getPolymorphicTypeValidator(), // 类型验证器，防止反序列化攻击
                ObjectMapper.DefaultTyping.NON_FINAL  // 应用于所有非 final 的类
        );

        // 创建 JSON 序列化器，并注入自定义的 ObjectMapper
        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(mapper);

        // key 采用字符串序列化器，保证可读性（在 Redis CLI 中能直接看到）
        template.setKeySerializer(new StringRedisSerializer());
        // value 采用 JSON 序列化器，支持存储任意对象
        template.setValueSerializer(serializer);

        // hash 结构的 key 也用字符串序列化
        template.setHashKeySerializer(new StringRedisSerializer());
        // hash 结构的 value 也用 JSON 序列化
        template.setHashValueSerializer(serializer);

        // 初始化 RedisTemplate 的配置
        template.afterPropertiesSet();

        return template;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;什么是反序列化攻击？
反序列化攻击是指攻击者通过构造恶意的序列化数据，利用应用程序在反序列化过程中执行不安全的代码或操作，从而导致安全漏洞。攻击者可以通过发送特制的序列化数据包，触发应用程序执行未授权的操作、获取敏感信息或执行任意代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;使用&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;package org.example;

import org.example.entity.Human;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new SpringApplicationBuilder(Main.class)
                .web(WebApplicationType.NONE) // 不启动 Tomcat
                .run(args);

        // 按名字拿，避免和 stringRedisTemplate 冲突
        RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate =
                (RedisTemplate&amp;lt;String, Object&amp;gt;) ctx.getBean(&quot;redisTemplate&quot;);

        // 打印一下序列化器，确认确实是你配置的
        System.out.println(&quot;KeySerializer   = &quot; + redisTemplate.getKeySerializer().getClass().getName());
        System.out.println(&quot;ValueSerializer = &quot; + redisTemplate.getValueSerializer().getClass().getName());

        String key = &quot;test:human:&quot; + System.currentTimeMillis();
        Human h = new Human(&quot;jackv&quot;, &quot;dfasdfssfsdf&quot;);

        redisTemplate.opsForValue().set(key, h);
        Object v = redisTemplate.opsForValue().get(key);

        System.out.println(&quot;Fetched value class = &quot; + (v == null ? &quot;null&quot; : v.getClass().getName()));
        System.out.println(&quot;Fetched value       = &quot; + v);

        // 简单校验：能拿回对象、类型是你期望的
        if (!(v instanceof Human)) {
            throw new IllegalStateException(&quot;不是 Human，而是：&quot; + (v == null ? &quot;null&quot; : v.getClass()));
        }
        // 可选：清理
        redisTemplate.delete(key);

        System.out.println(&quot;OK, JSON 序列化/反序列化正常。&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在其他类的时候直接@Autowired注入RedisTemplate即可使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
@Qualifier(&quot;redisTemplate&quot;)
private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>volatile-实现单例模式的双重锁</title><link>https://blog.meowrain.cn/posts/java/juc/volatile%E5%8F%8C%E9%87%8D%E9%94%81/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/volatile%E5%8F%8C%E9%87%8D%E9%94%81/</guid><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是单例模式的双重锁&lt;/h1&gt;
&lt;p&gt;单例模式的双重锁是一种实现单例模式的技术，通过两次检查实例是否为null，结合同步锁来保证在多线程环境下只创建一个实例，并试图通过减少同步的次数来提高性能。为了确保线程安全，尤其在涉及到对象创建的指令重排的问题的时候，通常需要使用 &lt;code&gt;volatile&lt;/code&gt;关键字来修饰单例类的实例变量。&lt;/p&gt;
&lt;h1&gt;非线程安全的单例模式&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static Singleton instance;
    private Singleton() {
        
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;多线程环境下，上面的简单实现在并发调用 &lt;code&gt;getInstance()&lt;/code&gt;方法时候可能出现问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/24/rajbl4-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;常见的做法是使用synchronized&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static Singleton instance;
    private Singleton() {

    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种同步方式能确保线程安全，因为在同一时间，只有一个线程能够进入getInstance方法，但是每次调用&lt;code&gt;getInstance&lt;/code&gt;方法都需要获取锁，即使在实例已经创建之后也是如此，这样会带来额外的性能开销，尤其是在频繁调用&lt;code&gt;getInstance()&lt;/code&gt;的情况下&lt;/p&gt;
&lt;h1&gt;什么是单例模式的双重检查锁定 -&amp;gt; 可能会导致半初始化问题&lt;/h1&gt;
&lt;p&gt;双重检查锁定就是为了保证在线程安全的前提下，尽量减少同步带来的性能开销&lt;/p&gt;
&lt;p&gt;核心思想：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一次检查： 在进入同步块之前，先检查insatnce是否为null，如果不是null，说明实例已经创建，可以直接返回，避免进入同步块。&lt;/li&gt;
&lt;li&gt;同步块： 如果第一次检查发现instance是null，就进入同步块&lt;/li&gt;
&lt;li&gt;第二次检查： 在同步块内，再次检查instance是否为null，这是至关重要的一部，因为可能多个线程都通过了第一次检查，但只有一个线程进入同步块，在同步块内再次检查可以确保只有一个线程会智行对象的创建操作。&lt;/li&gt;
&lt;li&gt;创建实例：如果第二次检查发现instance仍然为null，才真正创建对象并把引用赋值给instance&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
   private static Singleton instance;
   private Singleton() {

   }
   public static Singleton getInstance() {
       if (instance == null) {
           synchronized (Singleton.class) {
               if (instance == null) {
                   instance = new Singleton();
               }
           }
       }
       return instance;
   }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;尽管双重检查锁看起来聪明地减少了同步磁化，但是在JMM(JAVA 内存模型）种，没有使用&lt;code&gt;volatile&lt;/code&gt;的双重检查锁仍然存在&lt;code&gt;指令重排&lt;/code&gt;的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对象创建的过程 &lt;code&gt;instance = new SimpleSingleton();&lt;/code&gt; 实际上能分解为三个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为对象分配内存空间&lt;/li&gt;
&lt;li&gt;初始化对象&lt;/li&gt;
&lt;li&gt;将分配的内存空间的地址赋值给&lt;code&gt;instance&lt;/code&gt;变量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在某些情况下，JVM为了优化性能，可能会对这三个步骤进行重排序，例如，可能会将步骤三排在步骤2之前&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/24/s9qvuj-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;为什么用volatile？&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;可见性： volatile确保了所有线程都能看到instance变量的最新值，当一个线程修改了instance值，这个改变会立即对其他线程可见。&lt;/li&gt;
&lt;li&gt;禁止指令重排：解决了半初始化的问题，确保instance变量被赋值为非null之前，对象已经被完全初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {

    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>线程安全单例</title><link>https://blog.meowrain.cn/posts/java/juc/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E5%8D%95%E4%BE%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E5%8D%95%E4%BE%8B/</guid><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/27/11a6ta3-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;1 解决反序列化导致的单例破坏现象&lt;/h1&gt;
&lt;p&gt;这里的单例问题是，如果对一个可序列化对象进行反序列化，会创建一个新的对象，这就违背了我们想要全局单例的目标。因此要重写readResolve方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/26/sjvikb-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example.sigletons;

import java.io.Serializable;

public class Singleton1 implements Serializable {
    private Singleton1(){}
    private static final Singleton1 INSTANCE = new Singleton1();
    public Singleton1 getInstance() {
        return INSTANCE;
    }
    public Object readResolve() {
        return INSTANCE;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2 使用枚举实现单例模式&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/26/swwpe2-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example.sigletons;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public enum Singleton2 {
    INSTANCE;
    private final Properties properties;
    private Singleton2() {
        properties = new Properties();
        String configFile = &quot;application.properties&quot;;
        System.out.println(&quot;ConfigurationManager: Initializing and loading &quot; + configFile);
        try(InputStream inputStream = Singleton2.class.getClassLoader().getResourceAsStream(configFile)){
            if(inputStream == null){
                System.out.println(&quot;ConfigurationManager: Sorry, unable to find &quot; + configFile);
                // 在实际应用中，这里可能抛出异常或有更复杂的错误处理
                return;
            }
            properties.load(inputStream);
            System.out.println(&quot;ConfigurationManager: Configuration loaded successfully.&quot;);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
    public String getProperty(String key,String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }

    // 可以添加其他需要的方法，比如重新加载配置等（需要考虑线程安全）
    public void listProperties() {
        properties.forEach((key, value) -&amp;gt; System.out.println(key + &quot;=&quot; + value));
    }
}
class TestSingleton2 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.INSTANCE;
        singleton2.listProperties();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/26/swav0i-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;💎 枚举单例：全面解答这些问题！📜✨&lt;/p&gt;
&lt;p&gt;枚举单例是一种非常推荐的单例实现方式，因为它不仅简单、易用，还天然地具备线程安全和防止反序列化、反射破坏单例的能力。接下来，我们重点针对 &lt;strong&gt;枚举单例&lt;/strong&gt; 来回答这些问题！&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 1：枚举单例是如何限制实例个数的？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;枚举单例通过枚举的机制天然地保证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;枚举类的每一个枚举实例（如单例对象）都在 &lt;strong&gt;类加载阶段&lt;/strong&gt; 就完成初始化，并且整个应用程序中只有一个实例。&lt;/li&gt;
&lt;li&gt;枚举类型底层由 JVM 的实现机制保证，它不像普通类那样允许通过反射或 &lt;code&gt;new&lt;/code&gt; 额外创建实例。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;示例：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public enum SingletonEnum {
    INSTANCE; // 枚举单例实例

    public void doSomething() {
        System.out.println(&quot;Doing something...&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用方式：&lt;/h4&gt;
&lt;p&gt;即便通过 &lt;code&gt;SingletonEnum.INSTANCE&lt;/code&gt; 多次获取，得到的始终是同一个实例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1 == instance2); // 输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 2：枚举单例在创建时是否有并发问题？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;枚举单例天然线程安全，因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;枚举类型的初始化由 JVM 保证，是在类加载时完成的。&lt;/li&gt;
&lt;li&gt;类加载过程是线程安全的，JVM 使用了类加载的同步机制，保证枚举单例的初始化不会因多线程而发生竞争。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;举例：&lt;/h4&gt;
&lt;p&gt;即使多个线程同时调用 &lt;code&gt;SingletonEnum.INSTANCE&lt;/code&gt;，它们都会得到在类加载阶段构造好的唯一对象，无需额外同步。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 3：枚举单例能否被反射破坏单例？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;不会！&lt;/strong&gt; 枚举类型的结构特殊，无法被反射破坏单例。这是因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;枚举的构造器是私有的，并且其底层会检测反射调用。&lt;/li&gt;
&lt;li&gt;如果尝试通过反射显式调用枚举类的构造器，JVM 会抛出 &lt;code&gt;IllegalArgumentException&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;验证代码：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Constructor;

public class EnumReflectionTest {
    public static void main(String[] args) {
        try {
            // 获取枚举的构造器
            Constructor&amp;lt;SingletonEnum&amp;gt; constructor = SingletonEnum.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            SingletonEnum instance = constructor.newInstance(); // 反射创建枚举对象
        } catch (Exception e) {
            e.printStackTrace(); // 会抛出 IllegalArgumentException
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;运行结果：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;java.lang.IllegalArgumentException: Cannot reflectively create enum objects
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 4：枚举单例能否被反序列化破坏单例？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;枚举单例天然具备防止反序列化破坏的特性，原因是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Enum&lt;/code&gt; 类型的序列化机制是由 JVM 内部实现的，不走普通的对象序列化流程。&lt;/li&gt;
&lt;li&gt;反序列化枚举对象时，JVM 会直接返回枚举类中的现有实例，而不是从序列化流中创建新对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;验证代码：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;

public class EnumSerializationTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingletonEnum instance1 = SingletonEnum.INSTANCE;

        // 序列化枚举对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(&quot;enum_singleton.obj&quot;));
        oos.writeObject(instance1);
        oos.close();

        // 反序列化枚举对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(&quot;enum_singleton.obj&quot;));
        SingletonEnum instance2 = (SingletonEnum) ois.readObject();

        // 判断是否破坏单例
        System.out.println(instance1 == instance2); // 输出：true，单例没有破坏
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 5：枚举单例属于懒汉式还是饿汉式？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;枚举单例本质上属于饿汉式单例&lt;/strong&gt;。它的特点是&lt;strong&gt;在类加载阶段完成初始化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;枚举的实例在类加载时就被创建并初始化。&lt;/li&gt;
&lt;li&gt;即使程序中从未访问过 &lt;code&gt;SingletonEnum.INSTANCE&lt;/code&gt;，枚举实例依然会被加载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;优点：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;线程安全，无需为单例初始化额外编写同步代码。&lt;/li&gt;
&lt;li&gt;实现简洁，JVM 自动保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缺点：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果枚举实例较多，并且包含较大的初始化逻辑，会导致类加载阶段性能开销增加。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;问题 6：枚举单例如果希望加入一些初始化逻辑，该如何做？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;可以通过添加枚举的构造方法和静态方法来实现初始化逻辑。枚举的构造方法是私有的，可以用来在实例创建时执行初始化。&lt;/p&gt;
&lt;h4&gt;修改代码：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public enum SingletonEnum {
    INSTANCE; // 枚举单例实例

    private String configuration;

    // 枚举的构造方法
    SingletonEnum() {
        // 初始化逻辑
        configuration = &quot;System Configuration Loaded&quot;;
    }

    public String getConfiguration() {
        return configuration;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class TestEnumInitialization {
    public static void main(String[] args) {
        SingletonEnum instance = SingletonEnum.INSTANCE;
        System.out.println(instance.getConfiguration()); // 输出：System Configuration Loaded
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分析：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;枚举类型的构造器会在类加载时调用，且只调用一次。&lt;/li&gt;
&lt;li&gt;可用枚举构造器实现单例实例的初始化逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为何枚举单例完美适合单例模式？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它是天生线程安全的，JVM 保障了枚举实例的唯一性。&lt;/li&gt;
&lt;li&gt;枚举实例不能通过反射或序列化破坏。&lt;/li&gt;
&lt;li&gt;枚举的初始化流程天然符合饿汉式单例的特点。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;3 Double Check&lt;/h1&gt;
&lt;p&gt;https://meowrain.cn/archives/volatile-shi-xian-dan-li-mo-shi-de-shuang-zhong-suo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

public class DoubleSingleton {
    private static volatile DoubleSingleton INSTANCE = null;
    public static DoubleSingleton getInstance() {
        if(INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (DoubleSingleton.class){
            if(INSTANCE != null) {
                return INSTANCE;
            }
            INSTANCE = new DoubleSingleton();
            return INSTANCE;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/27/10zdcs1-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;4 静态内部类懒汉式创建线程安全单例&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

public class Singleton2 {
    private Singleton2(){}
    // 问题1： 属于懒汉式还是饿汉式
    private static class LazyLoader{
        static final Singleton2 INSTANCE = new Singleton2();
    }
    // 在创建的时候是否有并发问题
    public static Singleton2 getInstance() {
        return  LazyLoader.INSTANCE;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/27/1132c75-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/27/1144xf0-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>数据库ACID四大特性</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93acid%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93acid%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7/</guid><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是ACID四大特性&lt;/h1&gt;
&lt;p&gt;A ： Atomicity（原子性）
C ： Consistency（一致性）
I ： Isolation（隔离性）
D ： Durability（持久性）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/09/qrczot-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Atomicity（原子性）&lt;/h1&gt;
&lt;p&gt;这里要先讲一下什么是事务：  简单说，事务就是一组原子性的SQL执行单元。如果数据库引擎能够成功地对数据库应 用该组査询的全部语句，那么就执行该组SQL。如果其中有任何一条语句因为崩溃或其 他原因无法执行，那么所有的语句都不会执行。要么全部执行成功（commit），要么全部执行失败（rollback）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原子性指的是一个事务中所有操作要么全部成功要么全部失败。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Consistency（一致性）&lt;/h1&gt;
&lt;p&gt;数据库的一致性指的是： 每个事务必须使数据库从一个合法的状态，转变到另一个合法的状态，并且在事务执行前后，数据库的各种完整性约束都得以保持。&lt;/p&gt;
&lt;p&gt;什么是完整性约束呢？
完整性约束是指数据库中数据的规则和限制，比如主键约束、外键约束、唯一性约束等。
主键约束： 确保每条记录都有唯一标识。
外键约束： 确保数据之间的引用关系正确。
唯一性约束： 确保某列的值在表中是唯一的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/08/f8ed5e-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举个例子：
当我们向订单表插入一条记录的时候，如果指定的&lt;code&gt;customer_id&lt;/code&gt;在客户表中不存在，那么这个事务就不应该被提交，因为这会破坏数据的一致性。因此，外键约束会阻止事务提交，确保数据库的一致性。抛出外键约束异常&lt;/p&gt;
&lt;h1&gt;Isolation（隔离性）&lt;/h1&gt;
&lt;p&gt;数据库的隔离性指的是： 每个事务的执行都应该是独立的，互不干扰。即使多个事务同时执行，也不会影响彼此的结果。
隔离性确保了事务之间的独立性，防止了脏读、不可重复读和幻读等问题。&lt;/p&gt;
&lt;p&gt;如果没有隔离性，在多个用户并发访问数据库的情况下，可能会出现以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脏读(Dirty Read):
一个事务读取到另一个事务尚未提交的修改，如果该事务回滚，那么读取到的就是无效数据。&lt;/li&gt;
&lt;li&gt;不可重复读(Non-repeatable Read):
一个事务在同一事务内多次读取同一数据，却得到不同的结果，这是因为其他事务修改并提交了数据。&lt;/li&gt;
&lt;li&gt;幻读(Phantom Read):
一个事务在同一事务内多次执行相同的查询，但是每次查询返回的结果集不同，这是因为其他事务插入或删除了数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/08/08/f82hdp-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;事务隔离级别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读未提交 &amp;gt;&amp;gt; 事务可以读取其他事务未提交的数据，可能会导致脏读。&lt;/li&gt;
&lt;li&gt;读已提交 &amp;gt;&amp;gt; 事务只能读取已提交的数据，防止脏读。&lt;/li&gt;
&lt;li&gt;可重复读 &amp;gt;&amp;gt; 事务在执行期间多次读取同一数据，结果保持一致，防止不可重复读。&lt;/li&gt;
&lt;li&gt;串行化 &amp;gt;&amp;gt; 事务完全隔离，按顺序执行，防止幻读。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据库的默认隔离级别是&lt;strong&gt;可重复读（Repeatable Read）&lt;/strong&gt;，它可以防止脏读和不可重复读，但可能会出现幻读。（也就是无法避免读取数据的时候，其他事务提交新的数据或者删除数据，导致查询的结果集发生变化。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之前老把幻读和不可重复读搞混，现在再讲一下，所谓幻读，就是说读取数据过程中，另外一个数据库事务插入或者删除了数据，导致查询的数据结果集发生变化。而不可重复读是指在同一个事务中多次读取同一个数据，期间其他事务修改了数据，导致数据结果集不一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每个隔离级别都提供了不同程度的隔离性和性能，具体选择取决于应用场景和需求。&lt;/p&gt;
&lt;h1&gt;Durability（持久性）&lt;/h1&gt;
&lt;p&gt;数据库的持久性指的是： 一旦事务提交，对数据库的修改就会永久保存，即使系统崩溃也不会丢失。
持久性确保了数据的可靠性和稳定性，即使在系统故障或崩溃后，已提交的事务数据仍然可以恢复。&lt;/p&gt;
</content:encoded></item><item><title>ArrayList和LinkedList的区别</title><link>https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/arraylist%E5%92%8Clinkedlist%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/arraylist%E5%92%8Clinkedlist%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java ArrayList和LinkedList的区别&lt;/h1&gt;
&lt;p&gt;ArrayList基于动态数组实现，LinkedList基于双向链表实现，这是它们所有性能差异的根本原因&lt;/p&gt;
&lt;p&gt;ArrayList随机访问是O(1)，但是中间插入是O(n),LinkedList则相反，随机访问是O（n），但在已知位置的插入删除是O(1)&lt;/p&gt;
&lt;p&gt;LinkedList由于要存储前后节点的引用，每个元素的内存开销更大，ArrayList更节省内存，但可能因为扩容机制造成一定的浪费。&lt;/p&gt;
&lt;h1&gt;实际应用场景&lt;/h1&gt;
&lt;p&gt;在实际项目中，如果需要频繁随机访问元素，会选择ArrayList，如果需要频繁在两端添加删除元素，比如实现队列和栈，我会选择LinkedList&lt;/p&gt;
&lt;p&gt;ArrayList和LinkedList都是Java中常见的集合类，它们都实现了List接口。
底层数据结构不同：ArrayList使用数组实现，通过索引进行快速访问元素。
LinkedList使用链表实现，通过节点之间的指针进行元素的访问和操作。
插入和删除操作的效率不同：ArrayList在尾部的插入和删除操作效率较高，但在中间或开头的插入和删除操作效率较低，需要移动元素。
LinkedList在任意位置的插入和删除操作效率都比较高，因为只需要调整节点之间的指针。随机访问的效率不同：ArrayList支持通过索引进行快速随机访问，时间复杂度为O(1)。
LinkedList需要从头或尾开始遍历链表，时间复杂度为O(n)。
空间占用：ArrayList在创建时需要分配一段连续的内存空间，因此会占用较大的空间。LinkedList每个节点只需要存储元素和指针，因此相对较小。
使用场景：ArrayList适用于频繁随机访问和尾部的插入删除操作，而LinkedList适用于频繁的中间插入删除操作和不需要随机访问的场景。
线程安全：这两个集合都不是线程安全的，Vector是线程安全的&lt;/p&gt;
</content:encoded></item><item><title>concurrenthashmap的实现原理</title><link>https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/concurrenthashmap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/concurrenthashmap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</guid><pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;ConcurrentHashMap实现原理&lt;/h1&gt;
&lt;p&gt;ConcurrentHashMap是Java并发包中一种线程安全的哈希表实现。
HashMap在多线程环境下扩容会出现CPU接近100%的情况，因为HashMap并不是线程安全的，我们可以通过Collections里面的Map&amp;lt;K,V&amp;gt; synchronizedMap(Map&amp;lt;K,V&amp;gt; m) 把HashMap包装成一个线程安全的map&lt;/p&gt;
&lt;p&gt;比如SynchronizedMap的put方法就是加锁过的&lt;/p&gt;
&lt;h1&gt;ConcurrentHashMap的变化&lt;/h1&gt;
&lt;p&gt;ConcurrentHashMap在JDK1.7中，提供了一种粒度更细的加锁机制，这种机制叫分段锁，整个哈希表被分为多个段，每个段都独立锁定。读取操作不需要锁，写入操作仅锁定相关的段，这减小了锁冲突的几率，提高了并发性能。&lt;/p&gt;
&lt;p&gt;这种机制的优点是： 在并发环境下将实现更高的吞吐量，在单线程环境下只损失非常小的性能。&lt;/p&gt;
&lt;p&gt;可以这样理解分段锁，就是将数据分段，对每一段数据分配一把锁，当一个线程占用锁访问其中一个段数据的时候，其他段的数据也能被其他线程访问。&lt;/p&gt;
&lt;p&gt;有些方法需要跨段，比如size(),isEmpty(),containsValue()，它们可能需要锁定整个表而不仅仅是某个段，这需要按顺序锁定所有段，操作完以后，再按顺序释放所有段的锁。&lt;/p&gt;
&lt;p&gt;ConcurrentHashMap是由Segment数组结构和HashEntry构成的，Segment是一种可重入的锁，HashEntry则用于存储键值对数据。&lt;/p&gt;
&lt;p&gt;一个ConcurrentHashMap里面包含一个Segment数组，Segment的结构和HashMap类似，是一种数组和链表结构，一个Segment里包含一个HashEntry数组，每个HashEntry是一个链表结构的元素，每个Segment守护着一个HashEntry数组里的元素，当HashEntry数组的数据进行修改的时候，必须首先获得它对应的Segment锁。&lt;/p&gt;
&lt;p&gt;在外部：有一个 Segment 数组，作为并发控制的“总入口”，每个 Segment 都是一个独立的锁喵～
在内部：每个 Segment 自己就是一个完整的小型 HashMap！它有自己的哈希表数组，里面的每个桶都可以通过 next 指针挂着一个或多个 Entry 组成的链表.&lt;/p&gt;
&lt;h1&gt;ConcurrentHashMap 读写过程&lt;/h1&gt;
&lt;h2&gt;get方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为输入的key做hash运算，得到hash值&lt;/li&gt;
&lt;li&gt;通过Hash值，定位到对应的Segment对象&lt;/li&gt;
&lt;li&gt;再次通过hash值，定位到Segment当中数组的具体位置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;put方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为输入的key做hash运算，得到hash值&lt;/li&gt;
&lt;li&gt;通过hash值，定位到对应的Segment对象&lt;/li&gt;
&lt;li&gt;获取可重入锁&lt;/li&gt;
&lt;li&gt;再次通过hash值，定位到Segment当中数组的具体位置&lt;/li&gt;
&lt;li&gt;插入或者覆盖HashEntry对象&lt;/li&gt;
&lt;li&gt;释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JDK1.8&lt;/h1&gt;
&lt;p&gt;在JDK1.8中，ConcurrentHashMap主要做了两个优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和HashMap一样，链表也会在长度到达8的时候转换为红黑树，这样可以提升大量冲突的时候的查询效率。&lt;/li&gt;
&lt;li&gt;以某个位置的头结点为锁，配合自旋 + CAS 避免不必要的锁开销，进一步提升并发性能。&lt;/li&gt;
&lt;li&gt;相比JDK1.7中的ConcurrentHashMap,JDK1.8的ConcurrentHashMap取消了Segment分段锁，采用CAS + synchronized来保证并发安全性。整个容器只分为一个Segment，也就是table数组。&lt;/li&gt;
&lt;li&gt;JDK1.8中的ConcurrentHashMap对节点Node类中的共享变量，和JDK1.7一样，使用volatile关键字，保证多线程操作的时候，变量的可见性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;ConcurrentHashMap的字段&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;table
这个装载Node的数组，作为ConcurrentHashMap的底层容器，采用加载的方式，直到第一次插入数据的时候才会进行初始化操作
数组的大小是2的幂次方。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Java HashMap为什么在jdk8引入红黑树</title><link>https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/javahashmap%E4%B8%BA%E4%BB%80%E4%B9%88%E5%9C%A8jdk8%E5%BC%95%E5%85%A5%E7%BA%A2%E9%BB%91%E6%A0%91/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/javahashmap%E4%B8%BA%E4%BB%80%E4%B9%88%E5%9C%A8jdk8%E5%BC%95%E5%85%A5%E7%BA%A2%E9%BB%91%E6%A0%91/</guid><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java HashMap为什么在jdk8引入红黑树&lt;/h1&gt;
&lt;p&gt;在JDK8之前，HashMap的内部实现主要依赖于数组+链表的结构
当多个元素的哈希值相同的时候（也就是发生哈希冲突的时候），这些元素会被存储在同一个桶里面，形成一个链表。
但这种实现方式在特定情况下会导致性能问题。&lt;/p&gt;
&lt;h1&gt;JDK8之前的问题&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;时间复杂度退化： 在最坏情况下（大量元素哈希到同一个桶），查找，插入和删除操作的时间复杂度会从理想的O(1)退化为O(n)，其中n是链表的长度&lt;/li&gt;
&lt;li&gt;哈希冲突攻击： 恶意攻击者可以构造大量哈希冲突的数据，使得HashMap的性能急剧下降，导致潜在的拒接服务攻击。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;红黑树的引入&lt;/h1&gt;
&lt;p&gt;JDK8对HashMap进行了优化，引入了红黑树来解决上面的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;性能提升： 当一个桶中的元素数量超过一定阈值的时候，链表会被转换成红黑树。红黑树是一种自平衡的二叉搜索树，即使在最坏的情况下，它查找，插入和删除操作的时间复杂度也能保持在O（logn)，大大提高了性能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阈值机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当桶中元素超过8个的时候，链表转换为红黑树&lt;/li&gt;
&lt;li&gt;当桶中元素少于6个的时候，红黑树会退化回链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全性增强： 通过引入红黑树，即使面对哈希冲突的攻击，HashMap也能保持相对稳定的性能，提高系统安全性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为什么选择红黑树&lt;/h1&gt;
&lt;p&gt;平衡性： 红黑树是一种近似平衡的二叉搜索树，能保证最坏情况下的O(logn)的性能。&lt;/p&gt;
&lt;p&gt;内存占用： 相比AVL树等其它平衡树，红黑树的平衡条件较为宽松，旋转操作更少，内存占用更小。&lt;/p&gt;
&lt;p&gt;实现更好的复杂度与性能平衡&lt;/p&gt;
</content:encoded></item><item><title>Java迭代器Iterator和Iterable</title><link>https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/java%E8%BF%AD%E4%BB%A3%E5%99%A8iterator%E5%92%8Citerable/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E9%9B%86%E5%90%88/java%E8%BF%AD%E4%BB%A3%E5%99%A8iterator%E5%92%8Citerable/</guid><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java迭代器Iterator和Iterable&lt;/h1&gt;
</content:encoded></item><item><title>SpringBean生命周期</title><link>https://blog.meowrain.cn/posts/java/spring/springbean%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/springbean%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</guid><pubDate>Mon, 28 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Bean 的生命周期&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/skj7xz.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Bean生命周期可以粗略的划分为五大步：&lt;/p&gt;
&lt;p&gt;第一步：实例化Bean
第二步：Bean属性赋值
第三步：初始化Bean
第四步：使用Bean
第五步：销毁Bean
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/si308s.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.powercode.spring6.beans;

public class User {
    private String name;
    public User() {
        System.out.println(&quot;1.实例化Bean&quot;);
    }

    public void setName(String name) {
        this.name = name;
        System.out.println(&quot;2.Bean属性赋值&quot;);
    }

    public void initBean(){
        System.out.println(&quot;3.初始化Bean&quot;);
    }

    public void destroyBean(){
        System.out.println(&quot;5.销毁Bean&quot;);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;2024-01-11 12:21:23 618 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023
2024-01-11 12:21:23 715 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring12.xml]
2024-01-11 12:21:23 732 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean &apos;userBean&apos;
1.实例化Bean
2.Bean属性赋值
3.初始化Bean
4.使用Bean
2024-01-11 12:21:23 774 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023, started on Thu Jan 11 12:21:23 CST 2024
5.销毁Bean
2024-01-11 12:21:23 774 [main] DEBUG org.springframework.beans.factory.support.DisposableBeanAdapter - Custom destroy method &apos;destroyBean&apos; on bean with name &apos;userBean&apos; completed

进程已结束，退出代码为 0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sd8zrp.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Bean后处理器&lt;/h2&gt;
&lt;p&gt;加上后处理器就变成七步了：
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sj8wqs.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;BeanPostProcessor 的核心作用&lt;/h3&gt;
&lt;p&gt;BeanPostProcessor 本身并不属于某个特定 Bean 的生命周期，而是作用于容器中所有 Bean 的 “全局处理器”。它的核心功能是：在 Bean 完成实例化和属性赋值后、初始化方法（如 afterPropertiesSet() 或 init-method）执行前后，对 Bean 进行加工或增强。&lt;/p&gt;
&lt;p&gt;上图中检查Bean是否实现了Aware的相关接口是什么意思？&lt;/p&gt;
&lt;h2&gt;Aware相关接口&lt;/h2&gt;
&lt;p&gt;Aware相关的接口包括：BeanNameAware、BeanClassLoaderAware、BeanFactoryAware&lt;/p&gt;
&lt;p&gt;当Bean实现了BeanNameAware，Spring会将Bean的名字传递给Bean。
当Bean实现了BeanClassLoaderAware，Spring会将加载该Bean的类加载器传递给Bean。
当Bean实现了BeanFactoryAware，Spring会将Bean工厂对象传递给Bean。
测试以上10步，可以让User类实现5个接口，并实现所有方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BeanNameAware&lt;/li&gt;
&lt;li&gt;BeanClassLoaderAware&lt;/li&gt;
&lt;li&gt;BeanFactoryAware&lt;/li&gt;
&lt;li&gt;InitializingBean&lt;/li&gt;
&lt;li&gt;DisposableBean&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;InitializingBean 的核心作用&lt;/h2&gt;
&lt;p&gt;当一个 Bean 实现了 InitializingBean 接口后，Spring 容器会在该 Bean 的所有属性都被成功设置（即完成属性注入）之后，自动调用其 afterPropertiesSet() 方法。这一特性使得开发者可以在 Bean 正式投入使用前，进行一些必要的初始化操作，例如数据校验、资源加载、状态初始化等。&lt;/p&gt;
&lt;h2&gt;DisposableBean核心作用&lt;/h2&gt;
&lt;p&gt;DisposableBean 是 Spring 提供的销毁回调接口，其核心作用是在 Bean 即将被容器销毁前，触发自定义的清理操作。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sfkvri.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.powercode.spring6.beans;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
    private String name;

    public User() {
        System.out.println(&quot;1.实例化Bean&quot;);
    }

    public void setName(String name) {
        this.name = name;
        System.out.println(&quot;2.Bean属性赋值&quot;);
    }

    public void initBean(){
        System.out.println(&quot;6.初始化Bean&quot;);
    }

    public void destroyBean(){
        System.out.println(&quot;10.销毁Bean&quot;);
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println(&quot;3.类加载器：&quot; + classLoader);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println(&quot;3.Bean工厂：&quot; + beanFactory);
    }

    @Override
    public void setBeanName(String name) {
        System.out.println(&quot;3.bean名字：&quot; + name);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println(&quot;9.DisposableBean destroy&quot;);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&quot;5.afterPropertiesSet执行&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Spring常见面试题</title><link>https://blog.meowrain.cn/posts/java/spring/spring%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/spring/spring%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98/</guid><pubDate>Mon, 28 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;@Autowired 和 @Resource 的区别是什么？&lt;/h1&gt;
&lt;p&gt;@Autowired属于Spring内置的注解，默认的注入方式是byType，也就是根据类型匹配，当有多个实现时
byType就没办法正确注入了，这个时候可以结合@Qualifier注解一起使用，指定注入的名称。当然也可以使用byName，也就是根据名称注入，但是需要结合@Qualifier注解一起使用。&lt;/p&gt;
&lt;p&gt;@Resource 是Java自带注解，属于J2EE的，默认注入方式是byName，也就是根据名称注入，当找不到与名称匹配的bean时，根据类型注入。当然也可以结合@Qualifier注解一起使用，指定注入的名称。&lt;/p&gt;
&lt;p&gt;@Resource 有两个比较重要且日常开发常用的属性：name（名称）、type（类型）。
如果仅指定 name 属性则注入方式为byName，如果仅指定type属性则注入方式为byType，如果同时指定name 和type属性（不建议这么做）则注入方式为byType+byName。&lt;/p&gt;
&lt;p&gt;@Autowired 支持在构造函数、方法、字段和参数上使用。
@Resource 主要用于字段和方法上的注入，不支持在构造函数或参数上使用。&lt;/p&gt;
&lt;h1&gt;Bean 的生命周期&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/skj7xz.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Bean生命周期可以粗略的划分为五大步：&lt;/p&gt;
&lt;p&gt;第一步：实例化Bean
第二步：Bean属性赋值
第三步：初始化Bean
第四步：使用Bean
第五步：销毁Bean
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/si308s.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.powercode.spring6.beans;

public class User {
    private String name;
    public User() {
        System.out.println(&quot;1.实例化Bean&quot;);
    }

    public void setName(String name) {
        this.name = name;
        System.out.println(&quot;2.Bean属性赋值&quot;);
    }

    public void initBean(){
        System.out.println(&quot;3.初始化Bean&quot;);
    }

    public void destroyBean(){
        System.out.println(&quot;5.销毁Bean&quot;);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;2024-01-11 12:21:23 618 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023
2024-01-11 12:21:23 715 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring12.xml]
2024-01-11 12:21:23 732 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean &apos;userBean&apos;
1.实例化Bean
2.Bean属性赋值
3.初始化Bean
4.使用Bean
2024-01-11 12:21:23 774 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023, started on Thu Jan 11 12:21:23 CST 2024
5.销毁Bean
2024-01-11 12:21:23 774 [main] DEBUG org.springframework.beans.factory.support.DisposableBeanAdapter - Custom destroy method &apos;destroyBean&apos; on bean with name &apos;userBean&apos; completed

进程已结束，退出代码为 0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sd8zrp.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Bean后处理器&lt;/h2&gt;
&lt;p&gt;加上后处理器就变成七步了：
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sj8wqs.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;BeanPostProcessor 的核心作用&lt;/h3&gt;
&lt;p&gt;BeanPostProcessor 本身并不属于某个特定 Bean 的生命周期，而是作用于容器中所有 Bean 的 “全局处理器”。它的核心功能是：在 Bean 完成实例化和属性赋值后、初始化方法（如 afterPropertiesSet() 或 init-method）执行前后，对 Bean 进行加工或增强。&lt;/p&gt;
&lt;p&gt;上图中检查Bean是否实现了Aware的相关接口是什么意思？&lt;/p&gt;
&lt;h2&gt;Aware相关接口&lt;/h2&gt;
&lt;p&gt;Aware相关的接口包括：BeanNameAware、BeanClassLoaderAware、BeanFactoryAware&lt;/p&gt;
&lt;p&gt;当Bean实现了BeanNameAware，Spring会将Bean的名字传递给Bean。
当Bean实现了BeanClassLoaderAware，Spring会将加载该Bean的类加载器传递给Bean。
当Bean实现了BeanFactoryAware，Spring会将Bean工厂对象传递给Bean。
测试以上10步，可以让User类实现5个接口，并实现所有方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BeanNameAware&lt;/li&gt;
&lt;li&gt;BeanClassLoaderAware&lt;/li&gt;
&lt;li&gt;BeanFactoryAware&lt;/li&gt;
&lt;li&gt;InitializingBean&lt;/li&gt;
&lt;li&gt;DisposableBean&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;InitializingBean 的核心作用&lt;/h2&gt;
&lt;p&gt;当一个 Bean 实现了 InitializingBean 接口后，Spring 容器会在该 Bean 的所有属性都被成功设置（即完成属性注入）之后，自动调用其 afterPropertiesSet() 方法。这一特性使得开发者可以在 Bean 正式投入使用前，进行一些必要的初始化操作，例如数据校验、资源加载、状态初始化等。&lt;/p&gt;
&lt;h2&gt;DisposableBean核心作用&lt;/h2&gt;
&lt;p&gt;DisposableBean 是 Spring 提供的销毁回调接口，其核心作用是在 Bean 即将被容器销毁前，触发自定义的清理操作。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/28/sfkvri.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.powercode.spring6.beans;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
    private String name;

    public User() {
        System.out.println(&quot;1.实例化Bean&quot;);
    }

    public void setName(String name) {
        this.name = name;
        System.out.println(&quot;2.Bean属性赋值&quot;);
    }

    public void initBean(){
        System.out.println(&quot;6.初始化Bean&quot;);
    }

    public void destroyBean(){
        System.out.println(&quot;10.销毁Bean&quot;);
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println(&quot;3.类加载器：&quot; + classLoader);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println(&quot;3.Bean工厂：&quot; + beanFactory);
    }

    @Override
    public void setBeanName(String name) {
        System.out.println(&quot;3.bean名字：&quot; + name);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println(&quot;9.DisposableBean destroy&quot;);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&quot;5.afterPropertiesSet执行&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>模板方法模式</title><link>https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F/</guid><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/27/qllmzm-1.webp&quot; alt=&quot;&quot; /&gt;
对原理类图的说明：
AbstractClass 抽象类， 类中实现了模板方法(template)，定义了算法的骨架，具体子类需要去实现 其它的抽象方法
ConcreteClass 具体类， 实现了抽象类中的抽象方法&lt;/p&gt;
&lt;p&gt;也就是父类模板方法定义不变的流程，子类重写流程中的方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/27/qne5sq-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;①、AbstractClass 抽象模板&lt;/h1&gt;
&lt;h2&gt;一、基本方法&lt;/h2&gt;
&lt;p&gt;上面的 baseOperation() 或者 customOperation() 方法，也叫基本操作，是由子类实现的方法，并且在模板方法中被调用。&lt;/p&gt;
&lt;p&gt;基本方法尽量设计为protected类型， 符合迪米特法则， 不需要暴露的属性或方法尽量不要设置为protected类型。 实现类若非必要， 尽量不要扩大父类中的访权限。&lt;/p&gt;
&lt;h2&gt;二、模板方法&lt;/h2&gt;
&lt;p&gt;上面的 templateMethod() 方法，可以有一个或者几个，实现对基本方法的调度，完成固定的逻辑。&lt;/p&gt;
&lt;p&gt;为了防止恶意操作，通常模板方法都加上 final 关键字，不允许覆写。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/27/qp4v65-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;②、ConcreteClass 具体模板&lt;/h1&gt;
&lt;p&gt;实现父类定义的一个或多个抽象方法，也就是父类定义的基本方法在子类中得以实现。&lt;/p&gt;
&lt;h1&gt;应用&lt;/h1&gt;
&lt;p&gt;这个设计模式我们可以拿来做mq的发送和封装&lt;/p&gt;
&lt;p&gt;我们在抽象模板中实现通用的&lt;code&gt;sendMessage方法&lt;/code&gt;，在具体模板中实现具体的消息构建和封装发送逻辑。&lt;/p&gt;
&lt;h2&gt;定义消息发送事件基础扩充属性实体&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public final class BaseSendExtendDTO {

    /**
     * 事件名称
     */
    private String eventName;

    /**
     * 主题
     */
    private String topic;

    /**
     * 标签
     */
    private String tag;

    /**
     * 业务标识
     */
    private String keys;

    /**
     * 发送消息超时时间
     */
    private Long sentTimeout;

    /**
     * 具体延迟时间
     */
    private Long delayTime;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这样定义抽象模板： &lt;code&gt;AbstractCommonSendProduceTemplate&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;抽象模板&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * RocketMQ 抽象公共发送消息组件
 */
@RequiredArgsConstructor
@Slf4j(topic = &quot;CommonSendProduceTemplate&quot;)
public abstract class AbstractCommonSendProduceTemplate&amp;lt;T&amp;gt; {

    private final RocketMQTemplate rocketMQTemplate;

    /**
     * 构建消息发送事件基础扩充属性实体
     *
     * @param messageSendEvent 消息发送事件
     * @return 扩充属性实体
     */
    protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);

    /**
     * 构建消息基本参数，请求头、Keys...
     *
     * @param messageSendEvent 消息发送事件
     * @param requestParam     扩充属性实体
     * @return 消息基本参数
     */
    protected abstract Message&amp;lt;?&amp;gt; buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);

    /**
     * 消息事件通用发送
     *
     * @param messageSendEvent 消息发送事件
     * @return 消息发送返回结果
     */
    public SendResult sendMessage(T messageSendEvent) {
        BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);
        SendResult sendResult;
        try {
            // 构建 Topic 目标落点 formats: `topicName:tags`
            StringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());
            if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {
                destinationBuilder.append(&quot;:&quot;).append(baseSendExtendDTO.getTag());
            }

            // 延迟时间不为空，发送任意延迟消息，否则发送普通消息
            if (baseSendExtendDTO.getDelayTime() != null) {
                sendResult = rocketMQTemplate.syncSendDeliverTimeMills(
                        destinationBuilder.toString(),
                        buildMessage(messageSendEvent, baseSendExtendDTO),
                        baseSendExtendDTO.getDelayTime()
                );
            } else {
                sendResult = rocketMQTemplate.syncSend(
                        destinationBuilder.toString(),
                        buildMessage(messageSendEvent, baseSendExtendDTO),
                        baseSendExtendDTO.getSentTimeout()
                );
            }

            log.info(&quot;[生产者] {} - 发送结果：{}，消息ID：{}，消息Keys：{}&quot;, baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());
        } catch (Throwable ex) {
            log.error(&quot;[生产者] {} - 消息发送失败，消息体：{}&quot;, baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
            throw ex;
        }

        return sendResult;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;具体模板&lt;/h1&gt;
&lt;p&gt;这里举个例子，我们做一个短信通知的，定义MessageNotifyEvent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Component
public class SmsNotificationProducer extends AbstractCommonSendProduceTemplate&amp;lt;MessageNotifyEvent&amp;gt; {

    private final ConfigurableEnvironment environment;

    public SmsNotificationProducer(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {
        super(rocketMQTemplate);
        this.environment = environment;
    }

    @Override
    protected BaseSendExtendDTO buildBaseSendExtendParam(MessageNotifyEvent messageSendEvent) {
        return BaseSendExtendDTO.builder()
                .eventName(&quot;短信通知发送&quot;)
                .keys(String.valueOf(messageSendEvent.getNotificationId()))
                .topic(environment.resolvePlaceholders(MerchantAdminRocketMQConstant.SMS_NOTIFICATION_TOPIC_KEY))
                .sentTimeout(3000L)
                .build();
    }

    @Override
    protected Message&amp;lt;?&amp;gt; buildMessage(MessageNotifyEvent messageSendEvent, BaseSendExtendDTO requestParam) {
        String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();
        return MessageBuilder
                .withPayload(new MessageWrapper(keys, messageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag())
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;补充说明&lt;/h2&gt;
&lt;p&gt;为了使上述示例能够完整运行，还需要定义以下类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;MessageNotifyEvent&lt;/code&gt; - 短信通知事件类：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MessageNotifyEvent {
    private Long notificationId;
    private String phoneNumber;
    private String messageContent;
    private String sender;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;MessageWrapper&lt;/code&gt; - 消息包装类：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
public class MessageWrapper {
    private String key;
    private Object payload;
    
    public MessageWrapper(String key, Object payload) {
        this.key = key;
        this.payload = payload;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SmsNotificationConsumer&lt;/code&gt; - 短信通知消费者类：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Component
@RocketMQMessageListener(
    topic = MerchantAdminRocketMQConstant.SMS_NOTIFICATION_TOPIC_KEY,
    consumerGroup = MerchantAdminRocketMQConstant.SMS_NOTIFICATION_CONSUMER_GROUP
)
public class SmsNotificationConsumer implements RocketMQListener&amp;lt;MessageWrapper&amp;gt; {

    @Override
    public void onMessage(MessageWrapper messageWrapper) {
        MessageNotifyEvent event = (MessageNotifyEvent) messageWrapper.getPayload();
        log.info(&quot;[短信消费者] 收到短信通知消息 - ID: {}, 手机号: {}, 内容: {}, 发送者: {}&quot;, 
                 event.getNotificationId(), 
                 event.getPhoneNumber(), 
                 event.getMessageContent(), 
                 event.getSender());
        
        // 模拟发送短信
        System.out.println(&quot;[短信服务] 向 &quot; + event.getPhoneNumber() + &quot; 发送短信: &quot; + event.getMessageContent());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过以上完整的代码示例，我们可以看到模板方法模式在MQ消息发送场景中的应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbstractCommonSendProduceTemplate&lt;/code&gt; 定义了消息发送的通用流程（模板方法）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SmsNotificationProducer&lt;/code&gt; 实现了具体的业务逻辑（构建参数和消息）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SmsNotificationConsumer&lt;/code&gt; 监听消息并处理&lt;/li&gt;
&lt;li&gt;这样既保证了消息发送流程的一致性，又允许不同业务场景自定义具体实现&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Redisson延时队列架构</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redisson%E5%BB%B6%E6%97%B6%E9%98%9F%E5%88%97%E6%9E%B6%E6%9E%84/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redisson%E5%BB%B6%E6%97%B6%E9%98%9F%E5%88%97%E6%9E%B6%E6%9E%84/</guid><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;延时队列是一种特殊的消息队列，消息在发送后不会立即被消费，而是等待指定的时间后才被消费者处理。就像设置了一个&quot;闹钟&quot;，到时间才响。&lt;/p&gt;
&lt;h1&gt;阻塞队列 RBlockingDeque - 阻塞双端队列&lt;/h1&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双端： 可以从两端插入和取出元素&lt;/li&gt;
&lt;li&gt;阻塞： 当队列为空的时候，取元素会阻塞等待&lt;/li&gt;
&lt;li&gt;线程安全： 多个线程可以安全操作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 特点：
// - 双端：可以从两端插入和取出元素
// - 阻塞：当队列为空时，取元素会阻塞等待
// - 线程安全：多个线程可以安全操作

RBlockingDeque&amp;lt;String&amp;gt; deque = redissonClient.getBlockingDeque(&quot;myDeque&quot;);
deque.offerFirst(&quot;头部元素&quot;);
deque.offerLast(&quot;尾部元素&quot;);
String element = deque.takeFirst(); // 阻塞获取

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;RDelayedQueue - 延时队列&lt;/h1&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动延时：消息在指定时间后自动变为可消费状态&lt;/li&gt;
&lt;li&gt;精确控制：可以精确控制每个消息的延时时间&lt;/li&gt;
&lt;li&gt;Redis实现：基于Redis的有序集合(ZSet)实现&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
RDelayedQueue&amp;lt;String&amp;gt; delayedQueue = redissonClient.getDelayedQueue(deque);
delayedQueue.offer(&quot;消息内容&quot;, 30, TimeUnit.SECONDS); // 30秒后可消费
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整实现示例&lt;/h2&gt;
&lt;h3&gt;生产者端（消息发送）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class DelayQueueProducer {
  
    @Autowired
    private RedissonClient redissonClient;
  
    public void sendDelayedMessage(String message, long delaySeconds) {
        try {
            // 创建队列
            RBlockingDeque&amp;lt;String&amp;gt; blockingDeque = redissonClient
                .getBlockingDeque(&quot;DELAY_QUEUE_EXAMPLE&quot;);
            RDelayedQueue&amp;lt;String&amp;gt; delayedQueue = redissonClient
                .getDelayedQueue(blockingDeque);
          
            // 发送延时消息
            delayedQueue.offer(message, delaySeconds, TimeUnit.SECONDS);
          
            System.out.println(&quot;发送延时消息: &quot; + message + 
                             &quot;, 延时: &quot; + delaySeconds + &quot;秒&quot;);
        } catch (Exception e) {
            log.error(&quot;发送延时消息失败&quot;, e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;消费者端（消息处理）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class DelayQueueConsumer {
  
    @Autowired
    private RedissonClient redissonClient;
  
    @PostConstruct
    public void startConsumer() {
        // 启动独立线程消费延时消息
        new Thread(this::consumeMessages, &quot;DelayQueueConsumer&quot;).start();
    }
  
    private void consumeMessages() {
        try {
            RBlockingDeque&amp;lt;String&amp;gt; blockingDeque = redissonClient
                .getBlockingDeque(&quot;DELAY_QUEUE_EXAMPLE&quot;);
          
            while (!Thread.currentThread().isInterrupted()) {
                // 阻塞获取消息（自动等待延时到期）
                String message = blockingDeque.take();
                System.out.println(&quot;消费延时消息: &quot; + message);
              
                // 处理业务逻辑
                processMessage(message);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.info(&quot;消费者线程被中断&quot;);
        } catch (Exception e) {
            log.error(&quot;消费消息异常&quot;, e);
        }
    }
  
    private void processMessage(String message) {
        // 实际的业务处理逻辑
        System.out.println(&quot;处理业务消息: &quot; + message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;底层实现原理&lt;/h2&gt;
&lt;h3&gt;Redis数据结构使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Redisson使用以下数据结构：
# 1. 有序集合(ZSet) - 存储延时消息和到期时间
ZADD delay_queue 1640995200 &quot;message1&quot;  # 到期时间戳作为score

# 2. 列表(List) - 存储已到期可消费的消息
LPUSH ready_queue &quot;message1&quot;

# 3. 定时任务 - 定期检查到期消息
# Redisson内部使用定时任务扫描ZSet，将到期消息移动到List
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;延时检查机制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Redisson内部逻辑（简化版）
public class DelayedQueueChecker {
    public void checkExpiredMessages() {
        long now = System.currentTimeMillis();
      
        // 从有序集合中获取已到期的消息
        Set&amp;lt;String&amp;gt; expiredMessages = redisTemplate
            .opsForZSet()
            .rangeByScore(&quot;delay_queue&quot;, 0, now);
      
        for (String message : expiredMessages) {
            // 移动到可消费队列
            redisTemplate.opsForList().leftPush(&quot;ready_queue&quot;, message);
            redisTemplate.opsForZSet().remove(&quot;delay_queue&quot;, message);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用场景&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 1. 订单超时处理
public void handleOrderTimeout(String orderId) {
    delayedQueue.offer(orderId, 30, TimeUnit.MINUTES);
}

// 2. 优惠券到期提醒
public void couponExpireReminder(String couponId) {
    delayedQueue.offer(couponId, 24, TimeUnit.HOURS);
}

// 3. 消息重试机制
public void messageRetry(String messageId) {
    delayedQueue.offer(messageId, 5, TimeUnit.SECONDS); // 5秒后重试
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>BigDecimal高精度计算</title><link>https://blog.meowrain.cn/posts/java/bigdecimal%E9%AB%98%E7%B2%BE%E5%BA%A6%E8%AE%A1%E7%AE%97/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/bigdecimal%E9%AB%98%E7%B2%BE%E5%BA%A6%E8%AE%A1%E7%AE%97/</guid><pubDate>Sat, 26 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://javaguide.cn/java/basis/bigdecimal.html&lt;/p&gt;
&lt;h1&gt;BigDecimal详解&lt;/h1&gt;
&lt;p&gt;Java中，浮点数的运算有精度丢失的风险&lt;/p&gt;
&lt;p&gt;为什么浮点数运算的时候会有精度丢失的风险？
计算机是二进制的，浮点数在计算机中是通过二进制的方式来表示的。但是，浮点数的表示方式是有限的，所以在进行浮点数运算的时候，会存在精度丢失的风险。&lt;/p&gt;
&lt;p&gt;例如，在Java中，浮点数的表示方式是 IEEE 754 标准，使用 64 位二进制来表示一个浮点数。其中，1 位用于表示符号位，11 位用于表示指数位，52 位用于表示尾数位。但是，浮点数的表示方式是有限的，所以在进行浮点数运算的时候，会存在精度丢失的风险。&lt;/p&gt;
&lt;h1&gt;BigDecimal 类的常用方法&lt;/h1&gt;
&lt;p&gt;BigDecimal可以实现对小数的运算，不会造成精度损失&lt;/p&gt;
&lt;p&gt;通常情况下，大部分需要小数精确运算结果的业务场景都是通过BigDecimal来做的。&lt;/p&gt;
&lt;p&gt;《阿里巴巴 Java 开发手册》中提到：浮点数之间的等值判断，基本数据类型不能用 == 来比较，包装数据类型不能用 equals 来判断。&lt;/p&gt;
&lt;h2&gt;创建&lt;/h2&gt;
&lt;p&gt;我们在使用BigDecimal的时候，需要注意以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不能使用new BigDecimal(double)的方式来创建BigDecimal对象，因为double类型的精度是有限的，所以在创建BigDecimal对象的时候，会存在精度丢失的风险。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用new BigDecimal(String)的方式来创建BigDecimal对象，因为String类型的精度是无限的，所以在创建BigDecimal对象的时候，不会存在精度丢失的风险。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用BigDecimal的valueOf()方法来创建BigDecimal对象，因为valueOf()方法的参数是double类型，但是在内部会将double类型的参数转换为String类型，所以在创建BigDecimal对象的时候，不会存在精度丢失的风险。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/26/zjj9sd-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;加减乘除&lt;/h2&gt;
&lt;p&gt;add
subtract
multiply
divide&lt;/p&gt;
&lt;p&gt;divide可以指定保留的小数位数，以及四舍五入的方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
    return divide(divisor, scale, roundingMode.oldMode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用 divide 方法的时候尽量使用 3 个参数版本，并且RoundingMode 不要选择 UNNECESSARY，否则很可能会遇到 ArithmeticException（无法除尽出现无限循环小数的时候），其中 scale 表示要保留几位小数，roundingMode 代表保留规则。&lt;/p&gt;
&lt;p&gt;scale是保留几位小数，roundingMode是保留规则。&lt;/p&gt;
&lt;p&gt;roundingMode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UP 向上四舍五入&lt;/li&gt;
&lt;li&gt;DOWN 向下截取&lt;/li&gt;
&lt;li&gt;CEILING 向上截取&lt;/li&gt;
&lt;li&gt;FLOOR 向下截取&lt;/li&gt;
&lt;li&gt;HALF_UP 四舍五入&lt;/li&gt;
&lt;li&gt;HALF_DOWN 五舍六入&lt;/li&gt;
&lt;li&gt;HALF_EVEN 四舍六入五取偶&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;RoundingMode枚举详解 📚📊&lt;/h1&gt;
&lt;h2&gt;各种舍入模式详细说明&lt;/h2&gt;
&lt;h3&gt;1. UP - 向上舍入 ⬆️&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 绝对值增大方向舍入，远离零的方向
2.4 -&amp;gt; 3    // 正数向上
1.6 -&amp;gt; 2    // 正数向上
-1.6 -&amp;gt; -2  // 负数向更小（绝对值更大）
-2.4 -&amp;gt; -3  // 负数向更小
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. DOWN - 向下舍入 ⬇️&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 绝对值减小方向舍入，趋向零的方向
2.4 -&amp;gt; 2    // 正数向下
1.6 -&amp;gt; 1    // 正数向下
-1.6 -&amp;gt; -1  // 负数向更大（绝对值更小）
-2.4 -&amp;gt; -2  // 负数向更大
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. CEILING - 向正无穷舍入 ☁️&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 向数轴右侧舍入
2.4 -&amp;gt; 3    // 正数向上
1.6 -&amp;gt; 2    // 正数向上
-1.6 -&amp;gt; -1  // 负数向更大（向右）
-2.4 -&amp;gt; -2  // 负数向更大
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. FLOOR - 向负无穷舍入 ⚡&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 向数轴左侧舍入
2.4 -&amp;gt; 2    // 正数向下
1.6 -&amp;gt; 1    // 正数向下
-1.6 -&amp;gt; -2  // 负数向更小（向左）
-2.4 -&amp;gt; -3  // 负数向更小
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. HALF_UP - 四舍五入 🎯&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 遇5向上舍入
2.4 -&amp;gt; 2    // 小于5，向下
2.5 -&amp;gt; 3    // 等于5，向上
2.6 -&amp;gt; 3    // 大于5，向上
-1.5 -&amp;gt; -2  // 负数也一样，-1.5 -&amp;gt; -2
-1.4 -&amp;gt; -1  // -1.4 -&amp;gt; -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实际代码示例 💡&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.math.BigDecimal;
import java.math.RoundingMode;

public class RoundingModeDemo {
    public static void main(String[] args) {
        BigDecimal[] testNumbers = {
            new BigDecimal(&quot;2.4&quot;),
            new BigDecimal(&quot;2.5&quot;),
            new BigDecimal(&quot;2.6&quot;),
            new BigDecimal(&quot;-1.4&quot;),
            new BigDecimal(&quot;-1.5&quot;),
            new BigDecimal(&quot;-1.6&quot;)
        };
      
        for (BigDecimal num : testNumbers) {
            System.out.println(&quot;\n原数: &quot; + num);
            System.out.println(&quot;UP: &quot; + num.setScale(0, RoundingMode.UP));
            System.out.println(&quot;DOWN: &quot; + num.setScale(0, RoundingMode.DOWN));
            System.out.println(&quot;CEILING: &quot; + num.setScale(0, RoundingMode.CEILING));
            System.out.println(&quot;FLOOR: &quot; + num.setScale(0, RoundingMode.FLOOR));
            System.out.println(&quot;HALF_UP: &quot; + num.setScale(0, RoundingMode.HALF_UP));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;输出结果展示 📊&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;原数: 2.4
UP: 3
DOWN: 2
CEILING: 3
FLOOR: 2
HALF_UP: 2

原数: 2.5
UP: 3
DOWN: 2
CEILING: 3
FLOOR: 2
HALF_UP: 3

原数: -1.5
UP: -2
DOWN: -1
CEILING: -1
FLOOR: -2
HALF_UP: -2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用场景建议 🎯&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class RoundingMode应用场景 {
    public static void main(String[] args) {
        // 金融计算 - 通常使用HALF_UP（银行家舍入）
        BigDecimal money = new BigDecimal(&quot;123.455&quot;);
        BigDecimal roundedMoney = money.setScale(2, RoundingMode.HALF_UP);
      
        // 统计计算 - 可能使用HALF_EVEN（银行家舍入）
        BigDecimal average = new BigDecimal(&quot;87.345&quot;);
        BigDecimal roundedAvg = average.setScale(2, RoundingMode.HALF_EVEN);
      
        // 科学计算 - 根据需要选择合适的模式
        BigDecimal scientific = new BigDecimal(&quot;99.999&quot;);
        BigDecimal ceilingResult = scientific.setScale(2, RoundingMode.CEILING);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;BigDecimal等值比较&lt;/h1&gt;
&lt;p&gt;使用compareTo进行比较，因为equals会比较值和精度，但是compareTo会忽略精度&lt;/p&gt;
&lt;p&gt;compareTo() 方法可以比较两个 BigDecimal 的值，如果相等就返回 0，如果第 1 个数比第 2 个数大则返回 1，反之返回-1。&lt;/p&gt;
&lt;h1&gt;BigDecimal工具类&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 简化BigDecimal计算的小工具类
 */
public class BigDecimalUtil {

    /**
     * 默认除法运算精度
     */
    private static final int DEF_DIV_SCALE = 10;

    private BigDecimalUtil() {
    }

    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double subtract(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double multiply(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供（相对）精确的除法运算，当发生除不尽的情况时，精确到
     * 小数点以后10位，以后的数字四舍六入五成双。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2) {
        return divide(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供（相对）精确的除法运算。当发生除不尽的情况时，由scale参数指
     * 定精度，以后的数字四舍六入五成双。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2, int scale) {
        if (scale &amp;lt; 0) {
            throw new IllegalArgumentException(
                    &quot;The scale must be a positive integer or zero&quot;);
        }
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
    }

    /**
     * 提供精确的小数位四舍六入五成双处理。
     *
     * @param v     需要四舍六入五成双的数字
     * @param scale 小数点后保留几位
     * @return 四舍六入五成双后的结果
     */
    public static double round(double v, int scale) {
        if (scale &amp;lt; 0) {
            throw new IllegalArgumentException(
                    &quot;The scale must be a positive integer or zero&quot;);
        }
        BigDecimal b = BigDecimal.valueOf(v);
        BigDecimal one = new BigDecimal(&quot;1&quot;);
        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
    }

    /**
     * 提供精确的类型转换(Float)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static float convertToFloat(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.floatValue();
    }

    /**
     * 提供精确的类型转换(Int)不进行四舍六入五成双
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static int convertsToInt(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.intValue();
    }

    /**
     * 提供精确的类型转换(Long)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static long convertsToLong(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.longValue();
    }

    /**
     * 返回两个数中大的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中大的一个值
     */
    public static double returnMax(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.max(b2).doubleValue();
    }

    /**
     * 返回两个数中小的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中小的一个值
     */
    public static double returnMin(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.min(b2).doubleValue();
    }

    /**
     * 精确对比两个数字
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 如果两个数一样则返回0，如果第一个数比第二个数大则返回1，反之返回-1
     */
    public static int compareTo(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.compareTo(b2);
    }

}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>java基础面试题</title><link>https://blog.meowrain.cn/posts/java/java%E5%9F%BA%E7%A1%80%E9%9D%A2%E8%AF%95%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/java%E5%9F%BA%E7%A1%80%E9%9D%A2%E8%AF%95%E9%A2%98/</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://javaguide.cn/java&lt;/p&gt;
&lt;h1&gt;Java基本数据类型&lt;/h1&gt;
&lt;p&gt;整数型，浮点型，布尔型，字符型
整数型：
byte,short,int,long
浮点型: float,double
布尔型： boolean
字符型： char&lt;/p&gt;
&lt;h1&gt;基本类型和包装类型的区别&lt;/h1&gt;
&lt;p&gt;基本数据类型成员变量（未被static修饰） 存放在Java虚拟机的堆中&lt;/p&gt;
&lt;p&gt;基本类型不一定被放在Java虚拟机的栈中，这取决于这个基本类型变量在哪个地方，如果它是作为方法中的局部变量，那么它是存放在栈中的，当这个基本类型变量被放在成员变量里面的时候，它才会被放到堆中。&lt;/p&gt;
&lt;p&gt;当然被static修饰的基本类型一定是存放在Java虚拟机的堆中的。&lt;/p&gt;
&lt;h1&gt;包装类型的缓存机制&lt;/h1&gt;
&lt;p&gt;Java基本数据类型的包装类大部分都用到了缓存机制来提升性能
Byte,Short,Integer,Long这4种包装类默认创建了[-128,127]相应类型的缓存数据，Character创建了数值在[0,127]范围的缓存数据，Boolean直接返回TRUE或者FALSE&lt;/p&gt;
&lt;p&gt;对于Integer，可以通过JVM参数 -XX：AutoBoxCacheMax 来设定范围，但是不能修改下限
实际使用的时候，不建议设置过大的值，防止浪费内存，或者OOM&lt;/p&gt;
&lt;h1&gt;equals方法和==的区别&lt;/h1&gt;
&lt;p&gt;== 对于基本类型是判断值是否相等，对于引用类型是判断地址是否相等
equals方法，因为所有类的顶层父类都是Object类，所以Object类中的equals方法判断的也是两个对象的内存地址是否相同
因此需要重写equals方法，实现对象和对象之间的内容比较，当然了，重写equals方法的时候也需要重写hashCode方法，来保证在集合中使用的正确性。&lt;/p&gt;
&lt;h1&gt;自动装箱和自动拆箱&lt;/h1&gt;
&lt;p&gt;什么是自动装箱？
自动装箱是Java在基本数据和包装类型之间的自动转换，在基本类型到包装类型转换时，会调用包装类型的valueOf方法&lt;/p&gt;
&lt;p&gt;什么是自动拆箱？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

public class Main{
    public static void main(String[] args) {
        Integer i = Integer.valueOf(10);
        int j = i.intValue();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;为什么浮点数运算的时候会有精度丢失的风险？&lt;/h1&gt;
&lt;p&gt;计算机在表示一个数字时，宽度是有限的，无限循环的小数存储在计算机时，只能被截断，所以就会导致小数精度发生损失的情况&lt;/p&gt;
&lt;h1&gt;如何解决浮点数运算的精度丢失问题？&lt;/h1&gt;
&lt;p&gt;BigDecimal 可以实现对浮点数的运算，不会造成精度丢失。通常情况下，大部分需要浮点数精确运算结果的业务场景（比如涉及到钱的场景）都是通过 BigDecimal 来做的。&lt;/p&gt;
&lt;p&gt;BigDecimal的equals方法会比较精度还有值是否相等,BigDecimal的compareTo方法会比较值是否相等&lt;/p&gt;
&lt;h1&gt;Java高精度&lt;/h1&gt;
&lt;p&gt;BigDecimal, BigInteger&lt;/p&gt;
&lt;h1&gt;面向对象和面向过程的区别&lt;/h1&gt;
&lt;p&gt;面边过程编程和面向对象编程是两种常见的编程范式，两者的主要区别在于解决问题的方式不同：&lt;/p&gt;
&lt;p&gt;面向过程编程： 面向过程会把解决问题的过程拆成一个一个方法，通过一个一个方法的执行去解决问题&lt;/p&gt;
&lt;p&gt;面向对象编程： 会先抽象出对象，然后用对象执行方法的方式解决问题&lt;/p&gt;
&lt;p&gt;面向对象编程开发的程序一般有下面的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;易维护： 由于良好的结构和封装性，面向对象程序通常更容易维护&lt;/li&gt;
&lt;li&gt;易复用： 通过继承和多态，OOP设计使得代码更具有复用性，方便扩展功能&lt;/li&gt;
&lt;li&gt;易扩展： 模块化设计使得系统扩展变得更加容易和灵活。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;封装继承多态&lt;/h1&gt;
&lt;p&gt;封装是指将对象的属性和（数据）和方法（行为）捆绑在一起，并隐藏对象的内部实现细节，只暴露必要的接口给外部世界。有助于保护数据不被直接访问和修改，从而提高代码的安全性和可维护性。&lt;/p&gt;
&lt;p&gt;继承是面向对象编程中的另外一个核心概念，允许一个类从另一个类中继承属性和方法，从而实现代码复用和层次结构，子类可以扩展或者修改父类的行为，不需要重新编写代码。&lt;/p&gt;
&lt;p&gt;多态是指允许不同的对象对同一消息做出不同的响应，即同一方法可以根据发送对象的不同而采用多种不同的行为方式。多态的实现方式有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法重载： 同一个类中，方法名相同，参数列表不同，返回值类型可以相同也可以不同&lt;/li&gt;
&lt;li&gt;方法重写： 子类中，方法名和参数列表与父类相同，返回值类型和异常类型也相同，但是方法体不同&lt;/li&gt;
&lt;li&gt;接口实现： 一个类实现了一个接口，那么这个类就可以被视为是这个接口的一个实例，从而可以调用接口中的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;接口和抽象类的区别&lt;/h1&gt;
&lt;p&gt;接口偏向于定义行为规范，是对行为的抽象，强调“能不能做”，“具备什么能力”
抽象类偏向于定义共同的属性和方法，是对类的抽象，强调“是什么”的关系&lt;/p&gt;
&lt;p&gt;共同点：
接口和多态都不能被实例化，只能被实现或者继承后才能创建具体的对象。&lt;/p&gt;
&lt;h1&gt;为什么要有hashCode&lt;/h1&gt;
&lt;p&gt;当你把对象加入 HashSet 时，HashSet 会先计算对象的 hashCode 值来判断对象加入的位置，同时也会与其他已经加入的对象的 hashCode 值作比较，如果没有相符的 hashCode，HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象，这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同，HashSet 就不会让其加入操作成功。如果不同的话，就会重新散列到其他位置。这样我们就大大减少了 equals 的次数，相应就大大提高了执行速度。&lt;/p&gt;
&lt;p&gt;那为什么 JDK 还要同时提供这两个方法呢？
这是因为在一些容器（比如 HashMap、HashSet）中，有了 hashCode() 之后，判断元素是否在对应容器中的效率会更高（参考添加元素进HashSet的过程）！我们在前面也提到了添加元素进HashSet的过程，如果 HashSet 在对比的时候，同样的 hashCode 有多个对象，它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。&lt;/p&gt;
&lt;p&gt;equals 方法判断两个对象是相等的，那这两个对象的 hashCode 值也要相等。&lt;/p&gt;
&lt;h1&gt;String类的不可变性是如何被保证的？&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;String类被final修饰，也就是说String类是不可被继承的，不可被继承意味着没人能通过继承String类来修改String类的行为，从而保证了String类的不可变性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;底层的字符数组被final修饰&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    private final char value[];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着这个value的引用是不可变的，不能指向其他数组，但是数组中的字符是可以变的。
还需要进一步保护&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;没有对外暴露value的引用，可以看到前面用了private修饰，无法被外部类通过数组引用修改数组内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;String类也没有提供可以修改String内部数组的方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;String、StringBuffer、StringBuilder 的区别？&lt;/h1&gt;
&lt;p&gt;String是不可变的，StringBuilder和StringBuffer都继承自AbstractStringBuilder类，在AbstractStringBuilder中也是使用字符数组保存字符串，不过没有使用final和private关键字修饰&lt;/p&gt;
&lt;p&gt;StringBuffer是线程安全的，里面大量使用了synchronized关键字来保证线程安全，而StringBuilder是线程不安全的。&lt;/p&gt;
&lt;h1&gt;String#equals() 和 Object#equals() 有何区别？&lt;/h1&gt;
&lt;p&gt;因为String是引用类型，String中的equals方法是被重写过的，比较的是String字符串的值是否相等，Object中的equals方法是没有被重写的，比较的是对象的内存地址是否相等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/26/w2nt42-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
    /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;String#intern 方法有什么作用?&lt;/h1&gt;
&lt;p&gt;String.intern() 是一个 native (本地) 方法，用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况：
常量池中已有相同内容的字符串对象：如果字符串常量池中已经有一个与调用 intern() 方法的字符串内容相同的 String 对象，intern() 方法会直接返回常量池中该对象的引用。&lt;/p&gt;
&lt;p&gt;常量池中没有相同内容的字符串对象：如果字符串常量池中还没有一个与调用 intern() 方法的字符串内容相同的对象，intern() 方法会将当前字符串对象的引用添加到字符串常量池中，并返回该引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example;

public class Tests {

    public static void main(String[] args) {
        // 已经有一个字符串常量 &quot;abc&quot;
        String abc = &quot;abc&quot;;
        String str = new String(&quot;abc&quot;);
        System.out.println(str.intern() == str);
        System.out.println(abc == str.intern());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/26/w8e77e-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，str.intern() 返回的是字符串常量池中的引用，而不是字符串对象的引用，所以 str.intern() != str。
而 abc 是字符串常量池中的引用，所以 abc == str.intern()。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example;

public class Tests {

    public static void main(String[] args) {
        // 字符串常量池中之前没有&quot;abc&quot;，所以 intern() 方法会将其添加到常量池中，并返回这个新创建的字符串对象的引用。
        String str = new String(&quot;abc&quot;);
        System.out.println(str.intern() == str);

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子中，str.intern() == str 为 false，因为 str.intern() 返回的是字符串常量池中的引用，而 str 是字符串对象的引用。&lt;/p&gt;
&lt;p&gt;也就是说new String(&quot;abc&quot;)的时候，字面量&quot;abc&quot;在编译时就已经确定在常量池中了，运行时的new String()操作是基于已存在的字面量创建新对象，放在堆内存中。&lt;/p&gt;
&lt;h1&gt;异常&lt;/h1&gt;
&lt;h2&gt;Exception 和 Error 有什么区别？&lt;/h2&gt;
&lt;p&gt;在Java中，所有的异常都有一个共同的祖先java.lang包中的Throwable类，Throwable类有两个重要的子类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exception： 程序本身可以处理的异常，可以通过Catch进行捕获，Exception可以分为Checked Exception和Unchecked Exception。&lt;/li&gt;
&lt;li&gt;Error： 程序无法处理的异常，Error类的异常是由JVM抛出的 语法上虽然可以捕获，但是一般不建议捕获Error类的异常，因为Error类的异常是由JVM抛出的，程序中无法捕获，也无法处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Checked Exception 和 Unchecked Exception 有什么区别？&lt;/h2&gt;
&lt;p&gt;Checked Exception 即 受检查异常 ，Java 代码在编译过程中，如果受检查异常没有被 catch或者throws 关键字处理的话，就没办法通过编译。&lt;/p&gt;
&lt;p&gt;Unchecked Exception 即 非受检查异常 ，Java 代码在编译过程中，如果非受检查异常没有被 catch或者throws 关键字处理的话，也可以通过编译，但是在运行时会抛出异常。
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/26/x7qqjv-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;除了RuntimeException及其子类以外，其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有：IO 相关的异常、ClassNotFoundException、SQLException...。&lt;/p&gt;
&lt;p&gt;RuntimeException 及以下的异常类都被称为非受检查异常（Unchecked Exception），常见的非受检查异常有：ArrayIndexOutOfBoundsException、NullPointerException、ClassCastException...。&lt;/p&gt;
&lt;h2&gt;Throwable 类常用方法有哪些？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;String getMessage(): 返回异常发生时的详细信息&lt;/li&gt;
&lt;li&gt;String toString(): 返回异常发生时的简要描述&lt;/li&gt;
&lt;li&gt;String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法，可以生成本地化信息。如果子类没有覆盖该方法，则该方法返回的信息与 getMessage()返回的结果相同&lt;/li&gt;
&lt;li&gt;void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时，try 语句块中的 return 语句会被忽略。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;什么是反射？&lt;/h1&gt;
&lt;p&gt;反射是一种在程序运行的时候，动态地获取类的信息并且操作类或者对象的能力。&lt;/p&gt;
</content:encoded></item><item><title>postgresql一些容易混淆的概念</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/postgresql/postgresql%E4%B8%80%E4%BA%9B%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84%E6%A6%82%E5%BF%B5/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/postgresql/postgresql%E4%B8%80%E4%BA%9B%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84%E6%A6%82%E5%BF%B5/</guid><pubDate>Mon, 21 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;PostgreSQL一些容易混淆的概念&lt;/h1&gt;
&lt;p&gt;https://www.cnblogs.com/noodlesmars/p/11850559.html&lt;/p&gt;
&lt;h1&gt;Schema&lt;/h1&gt;
&lt;p&gt;在数据库创建的同时，就已经默认为数据库创建了一个模式--public，这也是该数据库的默认模式。所有为此数据库创建的对象(表、函数、试图、索引、序列等)都是创建在这个模式中的&lt;/p&gt;
&lt;p&gt;一个数据库包含一个或多个Schema，一个Schema包含一个或多个表，一个表包含一个或多个字段，一个字段包含一个或多个值。
我拿我们熟悉的MySQL数据库举例子，MySQL数据库中，数据库就是Schema，表就是Table，字段就是Column，值就是Value。
但是在PgSQL中，数据库不是像MySQL那样的数据库了，它下面可以放很多Schema，而Schema下面才是Table，字段就是Column，值就是Value。&lt;/p&gt;
&lt;p&gt;打个比方，mysql的database就是一个书柜，table是一个个的抽屉，column是抽屉里的书，value就是书的内容。
但是在pgsql中，database是一个书房，schema是一个个书柜，table是一个个抽屉，column是抽屉里的书，value就是书的内容。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;PostgreSQL 的分层结构 (Database -&amp;gt; Schema -&amp;gt; Table) 的优势：&lt;/p&gt;
&lt;p&gt;更好的逻辑隔离和组织： 在一个数据库内部，您可以使用 Schema 来对表、视图、函数等数据库对象进行逻辑分组。这对于大型项目、多租户应用或需要区分不同模块的数据非常有用。例如，在一个 my_app_db 数据库中，您可以有 public Schema (默认)、users Schema、orders Schema、analytics Schema 等。
避免命名冲突： 不同的 Schema 可以包含同名的表。例如，users.accounts 和 orders.accounts 可以是两个完全不同的表，而不会冲突。这在 MySQL 中是不可能的，因为所有表都直接位于数据库下。
权限管理： 您可以对 Schema 设置权限，控制用户对特定 Schema 内对象的访问，这提供了更细粒度的权限控制。
数据迁移和管理： 在某些情况下，可以更容易地在 Schema 级别进行数据迁移或管理。
MySQL 的扁平结构 (Database -&amp;gt; Table) 的特点：&lt;/p&gt;
&lt;p&gt;简单直观： 对于小型项目或初学者来说，MySQL 的结构可能更直接和易于理解，因为没有额外的 Schema 层。
兼容性： 许多其他数据库系统（如 SQL Server）也支持 Schema，但其概念可能与 PostgreSQL 更接近，而与 MySQL 的“数据库即 Schema”有所不同。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;User &amp;amp; Role&lt;/h1&gt;
&lt;p&gt;在PostgreSQL中，存在两个容易混淆的概念：角色/用户。之所以说这两个概念容易混淆，是因为对于PostgreSQL来说，这是完全相同的两个对象。唯一的区别是在创建的时候：&lt;/p&gt;
&lt;p&gt;1.我用下面的psql创建了角色custom:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE ROLE custom PASSWORD &apos;custom&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着我使用新创建的角色custom登录，PostgreSQL给出拒绝信息：
FATAL：role &apos;custom&apos; is not permitted to log in.&lt;/p&gt;
&lt;p&gt;说明该角色没有登录权限，系统拒绝其登录&lt;/p&gt;
&lt;p&gt;2.我又使用下面的psql创建了用户guest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE USER guest PASSWORD &apos;guest&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着我使用guest登录，登录成功&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;难道这两者有区别吗？查看文档，又这么一段说明：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;CREATE USER is the same as CREATE ROLE except that it implies LOGIN. ----CREATE USER除了默认具有LOGIN权限之外，其他与CREATE ROLE是完全相同的。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;表空间&lt;/h1&gt;
&lt;p&gt;表空间是数据库中一个逻辑上的存储单元，它将数据库的物理存储位置（磁盘上的目录或文件）抽象出来，供数据库对象（如表、索引、大对象等）使用。&lt;/p&gt;
&lt;p&gt;简单来说，你可以把表空间想象成数据库在磁盘上的一个个“存储分区”或“数据仓库”。数据库管理员 (DBA) 可以创建这些“仓库”，并指定它们实际位于哪个硬盘、哪个目录下。&lt;/p&gt;
&lt;p&gt;核心要点：&lt;/p&gt;
&lt;p&gt;逻辑与物理的桥梁： 表空间是连接数据库逻辑结构（Schema、表）与物理存储（文件系统）的桥梁。
存储位置的抽象： 它不存储数据本身，而是定义了数据应该存储在哪个物理位置。
管理单元： 它是 DBA 管理和优化存储资源的基本单位。&lt;/p&gt;
&lt;p&gt;创建表空间 (CREATE TABLESPACE)
这是定义一个新的存储位置的第一步。您需要指定表空间的名称和它在文件系统上的物理路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 示例 1: 创建一个用于快速访问数据的表空间 (假设路径在 SSD 上)
CREATE TABLESPACE fast_data_ts LOCATION &apos;/mnt/ssd_data/pg_tablespaces/fast_data&apos;;

-- 示例 2: 创建一个用于归档或不常用数据的表空间 (假设路径在 HDD 上)
CREATE TABLESPACE archive_data_ts LOCATION &apos;/mnt/hdd_data/pg_tablespaces/archive_data&apos;;

-- 示例 3: 创建一个用于索引的专用表空间
CREATE TABLESPACE index_ts LOCATION &apos;/mnt/ssd_data/pg_tablespaces/indexes&apos;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重要提示：&lt;/p&gt;
&lt;p&gt;LOCATION 指定的路径必须是绝对路径。
PostgreSQL 服务器进程必须对该路径拥有读写权限。
在创建表空间之前，您需要手动在文件系统上创建这些目录（例如：mkdir -p /mnt/ssd_data/pg_tablespaces/fast_data）。&lt;/p&gt;
</content:encoded></item><item><title>深入理解Java反射与泛型_类型擦除与强制类型转换</title><link>https://blog.meowrain.cn/posts/java/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E5%8F%8D%E5%B0%84%E4%B8%8E%E6%B3%9B%E5%9E%8B_%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E4%B8%8E%E5%BC%BA%E5%88%B6%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E5%8F%8D%E5%B0%84%E4%B8%8E%E6%B3%9B%E5%9E%8B_%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E4%B8%8E%E5%BC%BA%E5%88%B6%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;深入理解Java反射与泛型:类型擦除与强制类型转换&lt;/h1&gt;
&lt;p&gt;在 Java 编程中，反射（Reflection）和泛型（Generics）是两个强大且常用的特性。反射允许我们在运行时检查和操作类、方法、字段等，而泛型则允许我们编写更加通用和类型安全的代码。然而，Java 的泛型机制与类型擦除（Type Erasure）密切相关，这使得泛型在反射中的应用变得复杂。本文将深入探讨 Java 反射与泛型的结合使用，特别是类型擦除的影响以及如何通过强制类型转换来解决这些问题。&lt;/p&gt;
&lt;h2&gt;1. 泛型简介&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/04/10vqzk7-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;类型擦除&lt;/h2&gt;
&lt;h3&gt;1. 什么是类型擦除？&lt;/h3&gt;
&lt;p&gt;类型擦除（Type Erasure）是 Java 泛型的核心机制。它指的是&lt;strong&gt;在编译阶段，Java 会移除所有泛型类型信息&lt;/strong&gt;，即只在源代码层面检查泛型参数的类型，到了运行时，相关类型信息就被“擦除”掉了。&lt;/p&gt;
&lt;h3&gt;2. 为什么会有类型擦除？&lt;/h3&gt;
&lt;p&gt;Java 为了兼容早期版本（Java 5 之前没有泛型），采用了类型擦除的方式实现泛型，这样泛型代码能够和老代码共存而不冲突。&lt;/p&gt;
&lt;h3&gt;3. 类型擦除具体表现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;编译后不保留泛型类型参数信息。&lt;/strong&gt;&lt;br /&gt;
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; stringList = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Integer&amp;gt; integerList = new ArrayList&amp;lt;&amp;gt;();
System.out.println(stringList.getClass() == integerList.getClass()); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行时 &lt;code&gt;stringList&lt;/code&gt; 和 &lt;code&gt;integerList&lt;/code&gt; 其实都是 &lt;code&gt;ArrayList&lt;/code&gt; 类型，不区分里面装的东西。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;泛型类的字节码文件和“裸类型”一致。&lt;/strong&gt;&lt;br /&gt;
例如 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt; 会被编译成一样的 &lt;code&gt;List&lt;/code&gt; 类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方法中的类型参数会被替换成它的限定类型（如果有），否则直接替换为 Object。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Box&amp;lt;T&amp;gt; {
    T value;
}
// 编译后其实相当于
class Box {
    Object value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 类型擦除带来的影响&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行时无法通过反射获得泛型参数的具体类型。&lt;/strong&gt; 除非通过继承和明确指定泛型参数，否则无法在运行时获得泛型具体类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能直接创建泛型数组。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;某些类型强制转换失去编译器检查。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 可以通过什么方式间接获取泛型类型？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通过创建“带泛型参数的子类”并用反射获取 &lt;code&gt;getGenericSuperclass()&lt;/code&gt;，有时可以拿到实际类型参数。&lt;/li&gt;
&lt;li&gt;可以通过一些第三方库（如 Gson、Jackson）的特殊用法间接保存类型信息，但这些都是通过 hack 或特殊设计实现的。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;总结一句话&lt;/h3&gt;
&lt;p&gt;Java 泛型只在编译阶段保证类型安全，运行阶段所有泛型信息都会被类型擦除，代码在运行时只知道原始类型，不再区分泛型参数。&lt;/p&gt;
</content:encoded></item><item><title>Golang垃圾回收机制</title><link>https://blog.meowrain.cn/posts/golang/golang%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/golang/golang%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Go GC机制&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/aceld/golang/zhzanb#77fdf35b&quot;&gt;5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;垃圾回收(Garbage Collection，简称GC)是编程语言中提供的自动的内存管理机制，自动释放不需要的内存对象，让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持，GC能力的性能与优劣也是不同语言之间对比度指标之一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;发展过程&lt;/h2&gt;
&lt;p&gt;Go V1.3之前的标记-清除(mark and sweep)算法，Go V1.3之前的标记-清扫(mark and sweep)的缺点&lt;/p&gt;
&lt;h2&gt;Go V1.3之前的标记-清除(mark and sweep)算法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/C6W4Y71720498342584015950.webp&quot; alt=&quot;image-20240709121221919&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们来看一下在Golang1.3之前的时候主要用的普通的标记-清除算法，此算法主要有两个主要的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标记(Mark phase)&lt;/li&gt;
&lt;li&gt;清除(Sweep phase)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/ANh9c11720498052447247658.webp&quot; alt=&quot;image-20240709120731505&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/yWrUwk1720498077557020958.webp&quot; alt=&quot;image-20240709120757145&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;STW会对可达对象做上标记，然后对不可达对象进行GC回收&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/29Wcxv1720498140387778591.webp&quot; alt=&quot;image-20240709120900088&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;操作非常简单，但是有一点需要额外注意：mark and sweep算法在执行的时候，需要程序暂停！即 &lt;code&gt;STW(stop the world)&lt;/code&gt;，STW的过程中，CPU不执行用户代码，全部用于垃圾回收，这个过程的影响很大，所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间，程序会暂定停止任何工作，卡在那等待回收执行完毕。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;mark and sweep 算法 缺点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;STW会让程序暂停，使程序出现卡顿(重要问题)&lt;/li&gt;
&lt;li&gt;标记需要扫描整个heap&lt;/li&gt;
&lt;li&gt;清除数据会产生heap碎片&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;stw暂停范围&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/kMFipT1720498794174933847.webp&quot; alt=&quot;image-20240709121953696&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上图来看，全部的GC时间都是包裹在STW范围之内的，这样貌似程序暂停的时间过长，影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/rI4lNh1720498833454407229.webp&quot; alt=&quot;54-STW2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图主要是将STW的步骤提前了一步，因为在Sweep清除的时候，可以不需要STW停止，因为这些对象已经是不可达对象了，不会出现回收写冲突等问题。&lt;/p&gt;
&lt;p&gt;但是无论怎么优化，Go V1.3都面临这个一个重要问题，就是&lt;strong&gt;mark-and-sweep 算法会暂停整个程序&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;Go是如何面对并这个问题的呢？接下来G V1.5版本 就用&lt;strong&gt;三色并发标记法&lt;/strong&gt;来优化这个问题.&lt;/p&gt;
&lt;h2&gt;GoV1.5三色标记法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/u0ZJ951720499063811507708.webp&quot; alt=&quot;image-20240709122423404&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/MRhIFy1720499208514108528.webp&quot; alt=&quot;image-20240709122647686&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/Z6DyjS1720499274479089970.webp&quot; alt=&quot;image-20240709122753872&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/OPgFix1720499361118341644.webp&quot; alt=&quot;image-20240709122920596&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/ZkEIjD1720499418393168076.webp&quot; alt=&quot;image-20240709123017964&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/wULnvE1720499469045471792.webp&quot; alt=&quot;image-20240709123108479&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/VpPh5n1720499488250837040.webp&quot; alt=&quot;image-20240709123127729&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/lGPm8C1720499504716064921.webp&quot; alt=&quot;image-20240709123144258&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/qkpbys1720499549310981229.webp&quot; alt=&quot;image-20240709123228889&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;三色标记法无STW的问题&lt;/h2&gt;
&lt;p&gt;我们加入如果没有STW，那么也就不会再存在性能上的问题，那么接下来我们假设如果三色标记法不加入STW会发生什么事情？
我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改，会影响标记结果的正确性，我们来看看一个场景，如果三色标记法, 标记过程不使用STW将会发生什么事情?&lt;/p&gt;
&lt;p&gt;我们把初始状态设置为已经经历了第一轮扫描，目前黑色的有对象1和对象4， 灰色的有对象2和对象7，其他的为白色对象，且对象2是通过指针p指向对象3的，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/V3y0mh1720502068945434434.webp&quot; alt=&quot;55-三色标记问题1.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在如何三色标记过程不启动STW，那么在GC扫描过程中，任意的对象均可能发生读写操作，如图所示，在还没有扫描到对象2的时候，已经标记为黑色的对象4，此时创建指针q，并且指向白色的对象3。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/FKfHeh1720502103967556957.webp&quot; alt=&quot;56-三色标记问题2.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;与此同时灰色的对象2将指针p移除，那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/vja2PL1720502115722049746.webp&quot; alt=&quot;57-三色标记问题3.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们正常指向三色标记的算法逻辑，将所有灰色的对象标记为黑色，那么对象2和对象7就被标记成了黑色，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/w2ane51720502140258068700.webp&quot; alt=&quot;58-三色标记问题4.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么就执行了三色标记的最后一步，将所有白色对象当做垃圾进行回收，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/bzlj9c1720502156829093691.webp&quot; alt=&quot;59-三色标记问题5.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是最后我们才发现，本来是对象4合法引用的对象3，却被GC给“误杀”回收掉了。&lt;/p&gt;
&lt;h3&gt;GC误杀条件&lt;/h3&gt;
&lt;p&gt;可以看出，有两种情况，在三色标记法中，是不希望被发生的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**&lt;/li&gt;
&lt;li&gt;条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**
如果当以上两个条件同时满足时，就会出现对象丢失现象!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;屏障机制&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为了防止这种现象的发生，最简单的方式就是STW，直接禁止掉其他用户程序对对象引用关系的干扰，但是&lt;strong&gt;STW的过程有明显的资源浪费，对所有的用户程序都有很大影响&lt;/strong&gt;。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率，减少STW时间呢？答案是可以的，我们只要使用一种机制，尝试去破坏上面的两个必要条件就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/2cO2yL1720502505096278545.webp&quot; alt=&quot;image-20240709132144714&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;强三色不变式&lt;/h3&gt;
&lt;p&gt;强制性的不允许黑色对象引用白色对象&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;破坏条件1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/iZbHhI1720502294165051623.webp&quot; alt=&quot;image-20240709131813359&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;弱三色不变式&lt;/h3&gt;
&lt;p&gt;黑色对象可以引用白色对象，但是要保证白色独享存在其它灰色对象对它的引用，或者可达它的链路上游存在灰色对象&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;破坏条件2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/SwzQBu1720502412929353413.webp&quot; alt=&quot;image-20240709132012351&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了遵循上述的两个方式，GC算法演进到两种屏障方式，他们“插入屏障”, “删除屏障”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/JjkcAo1720503203424780995.webp&quot; alt=&quot;image-20240709133322663&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;插入屏蔽&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;不在栈上使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;具体操作&lt;/code&gt;: 在A对象引用B对象的时候，B对象被标记为灰色。(将B挂在A下游，B必须被标记为灰色)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;满足&lt;/code&gt;: &lt;strong&gt;强三色不变式&lt;/strong&gt;. (不存在黑色对象引用白色对象的情况了， 因为白色会强制变成灰色)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;添加下游对象(当前下游对象slot, 新下游对象ptr) {   
  //1
  标记灰色(新下游对象ptr)   
  
  //2
  当前下游对象slot = 新下游对象ptr        
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里说一下这个过程，首先因为插入屏障不在栈上使用&lt;/p&gt;
&lt;p&gt;下面的图里面，已经进行了一次三色标记，外界向对象4添加对象8，对象1添加对象9，但是我们知道，对象1在栈上，所以它不会应用插入屏障，也就是说，这个时候对象 9不会按照插入屏障的规则设置为灰色，而对象4在堆上，因此它会应用插入屏障，所以会把对象8设置为灰色，然后我们进行第二次三色标记，从灰色对象出发(对象2，对象7，对象8) ，找可达对象(对象3)，因此将对象3设置为灰色，然后对象2,7,8设置为黑色，接着进行第三次三色标记，从灰色对象出发(对象3)，发现没有可达对象，因此设置对象3为黑色，这个时候我们有黑色对象: 对象1，对象2，对象3，对象4，对象7，对象8.&lt;/p&gt;
&lt;p&gt;按照常理我们这个时候应该进行垃圾回收了对吧，其实不然，我们这个时候要把栈空间的对象全部设置为白色，然后使用STW暂停栈空间(对象1，对象2，对象3，对象9，对象5)，防止外界干扰（再有对象被添加到黑色对象下)&lt;/p&gt;
&lt;p&gt;然后我们对栈空间重新进行一次三色标记，直到没有灰色对象&lt;/p&gt;
&lt;p&gt;过程如下：&lt;/p&gt;
&lt;p&gt;从对象1出发，设置对象1为灰色，接下来看从对象1走的可达对象，发现可达对象有对象2和对象9，因此我们把对象2和对象9设置为灰色对象，把对象1设置为黑色对象，然后我们再从灰色对象出发(对象2和对象9)，发现对象2可达对象3，对象9没有可达对象，因此把对象3设置为灰色对象，对象2,9设置为黑色对象，接下来从灰色对象(此时只有对象3)出发，发现对象3没有可达对象，设置对象3为黑色对象。至此栈里面已经没有灰色对象，我们先暂停STW，然后进行最后的GC回收，可以发现白色对象只有 对象5，对象6，因此对白色对象进行清除。&lt;/p&gt;
&lt;p&gt;至此，GC三色标记并发情况下的插入屏障流程完毕&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/LKKoCr1720504284631136649.webp&quot; alt=&quot;image-20240709135123289&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/c9akf61720504314509112134.webp&quot; alt=&quot;image-20240709135153851&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/9ggDq01720504361239129518.webp&quot; alt=&quot;image-20240709135240616&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/brrrcs1720504410886565715.webp&quot; alt=&quot;image-20240709135330243&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/huazYX1720504451233838741.webp&quot; alt=&quot;image-20240709135410526&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/WENeFq1720504489239707269.webp&quot; alt=&quot;image-20240709135448742&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/AYQ3tv1720504535821911058.webp&quot; alt=&quot;image-20240709135535312&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;删除屏蔽&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;具体操作&lt;/code&gt;: 被删除的对象，如果自身为灰色或者白色，那么被标记为灰色。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;满足&lt;/code&gt;: &lt;strong&gt;弱三色不变式&lt;/strong&gt;. (保护灰色对象到白色对象的路径不会断)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;添加下游对象(当前下游对象slot， 新下游对象ptr) {
  //1
  if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
    标记灰色(当前下游对象slot)     //slot为被删除对象， 标记为灰色
  }
  
  //2
  当前下游对象slot = 新下游对象ptr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/YIsQlm1720506425416637589.webp&quot; alt=&quot;72-三色标记删除写屏障1.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/l7zqib1720506436481765589.webp&quot; alt=&quot;73-三色标记删除写屏障2.jpeg&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/9CZbQB1720506459636243158.webp&quot; alt=&quot;74-三色标记删除写屏障3.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/jsDCqs1720506469140624748.webp&quot; alt=&quot;75-三色标记删除写屏障4.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/ccDlph1720506476790209274.webp&quot; alt=&quot;76-三色标记删除写屏障5.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/zWf7Gz1720506482597765808.webp&quot; alt=&quot;77-三色标记删除写屏障6.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/bLHxhy1720506492796935675.webp&quot; alt=&quot;78-三色标记删除写屏障7.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种方式的回收精度低，一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮，在下一轮GC中被清理掉。&lt;/p&gt;
&lt;h3&gt;混合屏障Go V1.8&lt;/h3&gt;
&lt;p&gt;插入写屏障和删除写屏障的短板：&lt;/p&gt;
&lt;p&gt;● 插入写屏障：结束时需要STW来重新扫描栈，标记栈上引用的白色对象的存活；
● 删除写屏障：回收精度低，GC开始时STW扫描堆栈来记录初始快照，这个过程会保护开始时刻的所有存活对象。&lt;/p&gt;
&lt;p&gt;Go V1.8版本引入了混合写屏障机制（hybrid write barrier），避免了对栈re-scan的过程，极大的减少了STW的时间。结合了两者的优点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/mIzEEG1720506565775368039.webp&quot; alt=&quot;image-20240709142925523&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/WfkvFx1720506886093721996.webp&quot; alt=&quot;79-三色标记混合写屏障1.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/07/09/mhPr4L1720506893765506689.webp&quot; alt=&quot;80-三色标记混合写屏障2.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;具体操作&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描，无需STW)，&lt;/p&gt;
&lt;p&gt;2、GC期间，任何在栈上创建的新对象，均为黑色。&lt;/p&gt;
&lt;p&gt;3、被删除的对象标记为灰色。&lt;/p&gt;
&lt;p&gt;4、被添加的对象标记为灰色。&lt;/p&gt;
</content:encoded></item><item><title>Go_map底层结构</title><link>https://blog.meowrain.cn/posts/golang/go_map%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/golang/go_map%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Golang map底层数据结构&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://golang.design/go-questions/map/principal/&quot;&gt;https://golang.design/go-questions/map/principal/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzkxMjQzMjA0OQ==&amp;amp;mid=2247483868&amp;amp;idx=1&amp;amp;sn=6e954af8e5e98ec0a9d9fc5c8ceb9072&amp;amp;chksm=c10c4f02f67bc614ff40a152a848508aa1631008eb5a600006c7552915d187179c08d4adf8d7&amp;amp;scene=0&amp;amp;xtrack=1&amp;amp;subscene=90#rd&quot;&gt;Golang map 实现原理&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;map是一种常用的数据结构，核心特征包括下面三点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存储基于key-value对映射的模式&lt;/li&gt;
&lt;li&gt;基于key维度实现存储数据的去重&lt;/li&gt;
&lt;li&gt;读，写，删操作控制，时间复杂度O(1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/n5y1Lh1743600215837401704.avif&quot; alt=&quot;image-20250402212335440&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;初始化方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;map1 := make(map[string]int)

map2 := map[string]int{
    &quot;m1&quot;: 1,
    &quot;m2&quot;:2,
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;key 类型要求&lt;/h3&gt;
&lt;p&gt;map中,key的数据类型必须是可以比较的类型,slice,chan,func,map不可比较，所以不能作为map的key&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/fFJnr51743599129052146367.avif&quot; alt=&quot;image-20250402210528197&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/3eTMZz1743599137424628575.avif&quot; alt=&quot;image-20250402210536019&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/yP4UYM1743599162191602503.avif&quot; alt=&quot;image-20250402210601926&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/KZhyJp1743599167648587617.avif&quot; alt=&quot;image-20250402210607311&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/QFpAk21743599181398433044.avif&quot; alt=&quot;image-20250402210620988&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;核心原理&lt;/h1&gt;
&lt;p&gt;map又称为hash map，算法上基于hash实现key的映射和寻址，在数据结构上基于桶数组实现key-value对的存储&lt;/p&gt;
&lt;p&gt;以一组key-value对写入map的流程进行简述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过哈希方法去的key的hash值‘&lt;/li&gt;
&lt;li&gt;hash值对同数组长度取模，确定它所属的桶&lt;/li&gt;
&lt;li&gt;在桶中插入key value对&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/MmAiV11743599321939050023.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;hash&lt;/h2&gt;
&lt;p&gt;hash 译作散列，是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程，由于这种转换属于压缩映射，输入空间远大于输出空间，因此不同输入可能会映射成相同的输出结果. 此外，hash在压缩过程中会存在部分信息的遗失，因此这种映射关系具有不可逆的特质.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;hash的可重入性： 相同的key，必然产生相同的hash值&lt;/li&gt;
&lt;li&gt;hash的离散性： 只要两个key不相同，不论他们相似度的高低，产生的hash值会在整个输出域内均匀地离散化&lt;/li&gt;
&lt;li&gt;hash的单向性： 企图通过hash值反向映射会key是无迹可寻的。&lt;/li&gt;
&lt;li&gt;hash冲突： 由于输入域无穷大，输出域有限，必然存在不同key映射到相同hash值的情况，这种情况叫做哈希冲突&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/RV0Syj1743599459574600284.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;桶数组&lt;/h2&gt;
&lt;p&gt;map中，会通过长度为2的整数次幂的桶数组进行key-value对的存储&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个桶固定可以存放8个key-value对&lt;/li&gt;
&lt;li&gt;倘若超过8个key-value对打到桶数组的同一个索引当中，此时会通过创建桶链表的方式来化解这个问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/X7NMOa1743599952016346994.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;拉链法解决hash冲突&lt;/h2&gt;
&lt;p&gt;首先，由于hash冲突的存在，不同的key可能存在相同的hash值&lt;/p&gt;
&lt;p&gt;再者，hash值会对桶数组长度取模，因此不同的hash值可能被打到同一个桶中&lt;/p&gt;
&lt;p&gt;综上，不同的key-value可能被映射到map的同一个桶当中。&lt;/p&gt;
&lt;p&gt;拉链法中，将命中同一个桶的元素通过链表的形式进行连接，因此便于动态扩展&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只有当一个桶已经满了（8 个 kv 对），并且又有新的 key 哈希到这个桶时，才会创建溢出桶，并将新的 key-value 对存储到溢出桶中，然后将该溢出桶链接到原桶的尾部。  后续再有冲突的 kv 对，也会被添加到溢出桶或者新的溢出桶中，形成一个链表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/lgobAo1743600543664079674.avif&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开放寻址法解决hash冲突&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;开放寻址法是一种解决哈希冲突的方法，它在哈希表中寻找另一个空闲位置存储冲突的元素，也就是说，所有元素都直接存储在哈希表的桶中&lt;/p&gt;
&lt;p&gt;开放寻址法是一种在哈希表中解决冲突的方法。当两个不同的键映射到同一个索引位置时，就会发生冲突。开放寻址法不是使用链表等额外的数据结构来存储冲突的键值对，而是尝试在哈希表本身中寻找一个空闲的位置来存储新的键值对。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/GNSRsu1743600902616857141.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;常见开放寻址技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线性寻址： 如果在索引&lt;code&gt;i&lt;/code&gt;发生冲突，线性探测会依次检查&lt;code&gt;i+1&lt;/code&gt;,&lt;code&gt;i+2&lt;/code&gt;,&lt;code&gt;i+3&lt;/code&gt;等位置，直到找到一个空闲的槽位&lt;/li&gt;
&lt;li&gt;二次探测检查 &lt;code&gt;i + 1^2&lt;/code&gt;、&lt;code&gt;i + 2^2&lt;/code&gt;、&lt;code&gt;i + 3^2&lt;/code&gt; 等位置。与线性探测相比，这有助于减少聚集现象。&lt;/li&gt;
&lt;li&gt;双重哈希： 双重哈希使用第二个哈希函数来确定探测的步长。如果第一个哈希函数在索引&lt;code&gt;i&lt;/code&gt;导致哈希冲突，第二个哈希函数hash2(key)用于确定探测的间隔（例如，&lt;code&gt;i + hash2(key)&lt;/code&gt;、&lt;code&gt;i + 2*hash2(key)&lt;/code&gt;、&lt;code&gt;i + 3*hash2(key)&lt;/code&gt; 等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/lsNJNR1743600915626536212.avif&quot; alt=&quot;image-20250402213515236&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们的golang map解决哈希冲突的方式结合了拉链法和开放寻址法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;桶： map的底层数据结构是一个桶数组，每个桶严格意义上是一个单向桶链表&lt;/li&gt;
&lt;li&gt;桶的大小： 每个桶可以固定存放8个key value对&lt;/li&gt;
&lt;li&gt;当key命中一个桶的时候，首先根据开放寻址法，在桶的8个位置中寻找空位进行插入&lt;/li&gt;
&lt;li&gt;倘若8个位置都已经被占满，就基于桶的溢出桶指针，找到下一个桶（重复第三步）&lt;/li&gt;
&lt;li&gt;倘若遍历到链表尾部，还没找到空位，就用拉链法，在桶链表尾部接入新桶，并且插入key-value对&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/PB9PuR1743602071901331051.avif&quot; alt=&quot;image-20250402215431186&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/Xlpg4R1743602154258822359.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;扩容性能优化&lt;/h2&gt;
&lt;p&gt;倘若map的桶数组长度固定不变，那么随着key-value对数量的增长，当一个桶下挂载的key-value达到一定的量级，此时操作的时间复杂度会趋于线性，无法满足诉求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;桶数组长度固定不变 + key-value 对数量持续增加 =&amp;gt; 哈希冲突加剧 =&amp;gt; Bucket 链表变长 =&amp;gt; 查找/插入/删除 需要遍历长链表 =&amp;gt; 操作时间复杂度接近 O(n) （线性）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此在设计上，map桶的数组长度会随着key-value对的数量变化而实时调整。保证每个桶内的key-value对数量始终控制在常量级别。&lt;/p&gt;
&lt;p&gt;扩容类型分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增量扩容&lt;/li&gt;
&lt;li&gt;等量扩容&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;增量扩容&lt;/h3&gt;
&lt;p&gt;触发条件： &lt;code&gt;key-value总数 / 桶数组长度 &amp;gt; 6.5&lt;/code&gt;的时候，发生增量扩容&lt;/p&gt;
&lt;p&gt;扩容方式： 桶数组长度增长为原来的&lt;code&gt;两倍&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;目的： 减少负载因子，降低平均查找时间&lt;/p&gt;
&lt;p&gt;负载因子： &lt;code&gt;key-value总数 / 桶的数量&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/exh1He1743605454120683710.avif&quot; alt=&quot;image-20250402225053461&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;等量扩容&lt;/h3&gt;
&lt;p&gt;触发条件： 当桶内溢出桶数量大于等于2^B时（B 为桶数组长度的指数，B 最大取 15)，发生等量扩容。）&lt;/p&gt;
&lt;p&gt;扩容方式： 桶的长度保持为原来的值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目的：&lt;/strong&gt; 解决哈希冲突严重的问题，可能由于哈希函数选择不佳导致大量 key 映射到相同的桶，即使负载因子不高，也会出现大量溢出桶。 等量扩容旨在重新组织数据，减少溢出桶的数量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/m4tdlZ1743607184556640257.avif&quot; alt=&quot;image-20250402231943679&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/7Rrm4l1743607170611676452.avif&quot; alt=&quot;image-20250402231929805&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;渐进式扩容&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/8hpZdr1743607972891808021.avif&quot; alt=&quot;image-20250402233251365&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/2Cb2MO1743608023551743628.avif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;数据结构&lt;/h1&gt;
&lt;h2&gt;hmap&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type hmap struct {
    count int // map中键值对的数量
    flags uint8 // map的状态标志位，用来指示map的当前状态（正在写入，正在扩容等）
    B uint8 // buckets 数组的对数大小，2^B 是buckets数组的长度，比如B是5，那么桶数组的长度就是2^5 = 32
    noverflow uint16 //溢出桶数量的近似值 用来判断是否需要扩容
    hash0 uint32 // 哈希种子
    buckets unsafe.Pointer //指向bucket数组的指针，数组大小为2 ^ B，如果count == 0,那么buckets可能为nil
    oldbuckets unsafe.Pointer // 如果发生扩容，指向旧的buckets数组
    nevacuate uintptr // 扩容的时候，表示旧buckcet数组已经迁移到新bucket数组的数量计数器
    extra *mapextra // 可选字段，用来保存overflow buckets的信息
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flags: map状态标识，其包含的主要状态为（这里面牵扯到很多概念还没有涉及，可以先大致的了解一下各自的含义）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iterator(&lt;code&gt;0b0001&lt;/code&gt;): 当前map可能正在被遍历&lt;/li&gt;
&lt;li&gt;oldIterator(&lt;code&gt;0b0010&lt;/code&gt;): 当前map的旧桶可能正在被遍历&lt;/li&gt;
&lt;li&gt;hashWrting(&lt;code&gt;0b0100&lt;/code&gt;): 一个goroutine正在向map中写入数据&lt;/li&gt;
&lt;li&gt;sameSizeGrow(&lt;code&gt;0b1000&lt;/code&gt;): 等量扩容标志字段&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;bmap&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/Nb8mWR1743757559555396698.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/R3jihc1743757664615047610.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bmap就是map中的桶，可以存储8组key-value对数据，以及一个只想下一个溢出桶的指针&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/cH27qX1743757980953367677.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每一组key-value对数据包含key高8位hash值tophash，key,value三部分&lt;/p&gt;
&lt;p&gt;我们来看看bmap（桶）的内存模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/4iwDeb1743757807687319535.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果按照 &lt;code&gt;key/value/key/value/...&lt;/code&gt; 这样的模式存储，那在每一个 key/value 对之后都要额外 padding 7 个字节；而将所有的 key，value 分别绑定到一起，这种形式 &lt;code&gt;key/key/.../value/value/...&lt;/code&gt;，则只需要在最后添加 padding。&lt;/p&gt;
&lt;p&gt;每个 bucket 设计成最多只能放 8 个 key-value 对，如果有第 9 个 key-value 落入当前的 bucket，那就需要再构建一个 bucket ，通过 &lt;code&gt;overflow&lt;/code&gt; 指针连接起来。&lt;/p&gt;
&lt;h3&gt;tophash的作用？&lt;/h3&gt;
&lt;p&gt;是key 哈希值的高8位&lt;/p&gt;
&lt;p&gt;tophash的核心作用是&lt;strong&gt;判断一个键是否可能存在于当前桶中，从而优化查询效率。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;溢出桶数据结构 mapextra&lt;/h2&gt;
&lt;p&gt;在map初始化的时候会根据初始数据量不同，自动创建不同数量的溢出桶。在物理结构上初始的正常同和溢出桶是连续存放的，正常桶和溢出桶之间的关系是靠链表来维护的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;mapextra&lt;/code&gt; 就是在扩容时提供了一批预备的 &lt;code&gt;bmap&lt;/code&gt;，然后利用 &lt;code&gt;bmap.overflow&lt;/code&gt; 把它们链接起来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;type mapextra struct {
    overflow *[]*bmap // overflow buckets 的指针数组
    oldoverflow *[]*bmap // 旧的 overflow buckets 的指针数组

    nextOverflow *bmap // 指向空闲的 overflow bucket
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在map初始化的时候，倘若容量过大，会提前申请好一批溢出桶，供后续使用，这部分溢出桶存放在hmap.mapextra当中：&lt;/p&gt;
&lt;p&gt;mapextra.overflow 是一个指向溢出桶切片的指针，这个切片里面的溢出桶是当前使用的，用于存储hmap.buckets中的桶的溢出数据。&lt;/p&gt;
&lt;p&gt;mapextra.oldoverflow 也是一个指向溢出桶切片的指针，但是它指向的是旧的桶数组的溢出桶。&lt;/p&gt;
&lt;p&gt;nextOverflow指向下一个可用的溢出桶&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/eZLvxe1743757352736850834.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;什么是哈希种子？&lt;/h1&gt;
&lt;p&gt;哈希种子(hash seed)是一个随机生成的数值，被用作哈希函数的一部分，来增加哈希值的随机性和不可预测性，可以把它理解为哈希函数的“盐”&lt;/p&gt;
&lt;h1&gt;go map 如何根据key的哈希值确定键值存储到哪个桶中？&lt;/h1&gt;
&lt;h2&gt;哈希值的作用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先，当你在 Go map 中插入一个键值对时，Go runtime 会对键进行哈希运算，生成一个哈希值（一个整数）。 优秀的哈希函数应该能够将不同的键尽可能均匀地映射到不同的哈希值，以减少哈希碰撞的概率。&lt;/li&gt;
&lt;li&gt;这个哈希值是确定键值对存储位置的关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;go map 数据结构中hmap 中B的作用&lt;/h2&gt;
&lt;p&gt;我们通过哈希值的低B位作为bucket数组的索引， 来选择键值该存储到哪个bucket中。&lt;/p&gt;
&lt;p&gt;公式 &lt;code&gt;bucketIndex = hash &amp;amp; ((1 &amp;lt;&amp;lt; B)  - 1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;上面的公式 用来&lt;strong&gt;保留 &lt;code&gt;hash&lt;/code&gt; 的低 &lt;code&gt;B&lt;/code&gt; 位，并将其他位设置为 0&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/02/Vtatge1743608558267235069.avif&quot; alt=&quot;image-20250402234237409&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;key定位过程&lt;/h1&gt;
&lt;p&gt;key经过哈希计算后得到哈希值，共64个bit位，计算它到底要落在哪个桶的时候，只会用到最后B个bit位（log2BucketCount）&lt;/p&gt;
&lt;p&gt;例如，现在有一个key经过哈希函数计算后，得到的哈希结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而我们的B是5，也就是有2^5 = 32个桶&lt;/p&gt;
&lt;p&gt;取最后五位，也就是 &lt;strong&gt;01010&lt;/strong&gt; 转换为10进制也就是10，也就是 &lt;strong&gt;10号桶&lt;/strong&gt;,这个操作其实是 &lt;strong&gt;取余操作&lt;/strong&gt;，但是取余数开销太大，就用上面的位运算代替了。&lt;/p&gt;
&lt;p&gt;接下来我们再用 &lt;strong&gt;hash值的高8位&lt;/strong&gt;找到key在 &lt;strong&gt;10号桶&lt;/strong&gt;中的位置 &lt;strong&gt;1001011转换为10进制也就是 75&lt;/strong&gt;.最开始桶内还没有 key，新加入的 key 会找到第一个空位，放入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/JcLsW91743759107613607466.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/04/dCIofJ1743759450720106234.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;流程&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/05/VUcqQy1743839544909227250.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;写入流程&lt;/h1&gt;
&lt;p&gt;写入流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行hmap是否为nil的检查，如果为空，就触发panic&lt;/li&gt;
&lt;li&gt;进行并发读写的检查，倘若已经设置了并发读写标记，就抛出&quot;concurrent map writes&quot;异常。&lt;/li&gt;
&lt;li&gt;处理桶迁移。如果正在扩容，把key所在的旧桶数据迁移到新桶，同时迁移index位h.nevacuate的桶，迁移完成后h.nevacuate自增。更新迁移进度。如果所有桶迁移完毕，清除正在扩容的标记。&lt;/li&gt;
&lt;li&gt;查找 key 所在的位置，并记录桶链表的第一个空闲位置（若此 key 之前不存在，则将该位置作为插入位置）。&lt;/li&gt;
&lt;li&gt;若此 key 在桶链表中不存在，判断是否需要扩容，若溢出桶过多，则进行相同容量的扩容，否则进行双倍容量的扩容。&lt;/li&gt;
&lt;li&gt;若桶链表没有空闲位置，则申请溢出桶来存放 key - value 对。&lt;/li&gt;
&lt;li&gt;设置 key 和 tophash[i] 的值。&lt;/li&gt;
&lt;li&gt;返回 value 的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;删除流程&lt;/h1&gt;
&lt;p&gt;删除流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行并发读写检查。&lt;/li&gt;
&lt;li&gt;处理桶迁移，如果map处于正在扩容的状态，就迁移两个桶&lt;/li&gt;
&lt;li&gt;定位key所在的位置&lt;/li&gt;
&lt;li&gt;删除kv对的占用，这里是伪删除，只有在下次扩容的时候，被删除的key所占用的同空间才会得到释放。&lt;/li&gt;
&lt;li&gt;map首先会将对应位置的tophash[i]设置为emptyOne，表示该位置被删除&lt;/li&gt;
&lt;li&gt;如果tophash[i]后面还有有效的节点，就仅设置为emptyOne标志，意味着这个节点后面仍然存在有效的key-value对 ，后续在查找某个key的时候，这个节点只后仍然需要继续查找&lt;/li&gt;
&lt;li&gt;要是tophash[i]是桶链表的最后一个有效节点，那么从这个节点往前遍历，将链表最后面所有标志位emptyOne的位置，都设置为emptyRest。这样在查找某个key的时候，emptyRest之后的节点不需要继续查找。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;emptyOne&lt;/code&gt;：&lt;/strong&gt; 表示当前 cell 是空的，但&lt;strong&gt;不能保证&lt;/strong&gt;后面的 cell 也是空的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;emptyRest&lt;/code&gt;：&lt;/strong&gt; 表示当前 cell 是空的，并且&lt;strong&gt;保证&lt;/strong&gt;后面的所有 cell 也是空的，直到遇到一个非空 cell 或者到达桶的末尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h1&gt;迭代流程&lt;/h1&gt;
&lt;p&gt;在每次对 map 进行循环时，会调用 mapiterinit 函数，以确定迭代从哪个桶以及桶内的哪个位置起始。由于 mapiterinit 内部是通过随机数来决定起始位置的，所以 map 循环是无序的，每次循环所返回的 key - value 对的顺序都各不相同。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/04/05/TABXTR1743840105513843585.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Go_slice切片原理</title><link>https://blog.meowrain.cn/posts/golang/go_slice%E5%88%87%E7%89%87%E5%8E%9F%E7%90%86/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/golang/go_slice%E5%88%87%E7%89%87%E5%8E%9F%E7%90%86/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;slice数据结构&lt;/h1&gt;
&lt;p&gt;数据结构
我们每定义一个slice变量，golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成：&lt;/p&gt;
&lt;p&gt;array表示数组指针，数组用于存储数据。
len表示切片长度，也就是数组index从0到len-1已存储数据。
cap表示切片容量，当切片长度超过最大容量时，需要扩容申请更大长度的数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type slice struct {
    array unsafe.Pointer // 数组指针
    len   int // 切片长度
    cap   int // 切片容量
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;扩容原理&lt;/h1&gt;
&lt;p&gt;切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中，其中核心步骤如下：&lt;/p&gt;
&lt;p&gt;• 倘若扩容后预期的新容量小于原切片的容量，则 panic&lt;/p&gt;
&lt;p&gt;• 倘若切片元素大小为 0（元素类型为 struct{}），则直接复用一个全局的 zerobase 实例，直接返回&lt;/p&gt;
&lt;p&gt;• 倘若预期的新容量超过老容量的两倍，则直接采用预期的新容量&lt;/p&gt;
&lt;p&gt;• 倘若老容量小于 256，则直接采用老容量的2倍作为新容量&lt;/p&gt;
&lt;p&gt;• 倘若老容量已经大于等于 256，则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值，持续这样处理，直到得到的新容量已经大于等于预期的新容量为止&lt;/p&gt;
&lt;p&gt;• 结合 mallocgc 流程中，对内存分配单元 mspan 的等级制度，推算得到实际需要申请的内存空间大小&lt;/p&gt;
&lt;p&gt;• 调用 mallocgc，对新切片进行内存初始化&lt;/p&gt;
&lt;p&gt;• 调用 memmove 方法，将老切片中的内容拷贝到新切片中&lt;/p&gt;
&lt;p&gt;• 返回扩容后的新切片&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
 newcap := oldCap // 将新容量初始化为旧容量
 doublecap := newcap + newcap // 计算旧容量的两倍

 // 如果所需的新长度大于旧容量的两倍，则直接使用所需的新长度
 if newLen &amp;gt; doublecap {
  return newLen
 }

 const threshold = 256 // 定义一个阈值，用于区分小切片和大切片

 // 如果旧容量小于阈值，则直接将新容量设置为旧容量的两倍
 // 这种策略适用于小切片，可以快速扩容，减少扩容次数
 if oldCap &amp;lt; threshold {
  return doublecap
 }

 // 对于大切片，使用更平滑的扩容策略，避免过度分配内存
 // 从 2 倍增长过渡到 1.25 倍增长。 此公式给出了两者之间的平滑过渡。
 for {
  // 每次循环，将新容量增加 (newcap + 3*threshold) / 4
  // 相当于 newcap 增加 1/4 的比例，再加上 3/4 的 threshold(256)，即 192
  // 这样可以在一定程度上减少内存浪费，并保证切片的增长
  newcap += (newcap + 3*threshold) &amp;gt;&amp;gt; 2

  // Check for overflow and determine if the new calculated capacity
  // is greater or equal to the required new length.
  // newLen is guaranteed to be larger than zero, hence
  // when newcap overflows then `uint(newcap) &amp;gt; uint(newLen)`.
  // This allows to check for both with the same comparison.

  // 我们需要检查`newcap &amp;gt;= newLen`以及`newcap`是否溢出。
  // 保证 newLen 大于零，因此当 newcap 溢出时，&apos;uint(newcap) &amp;gt; uint(newLen)&apos;。
  // 这允许使用相同的比较来检查两者。

  // 检查新容量是否大于等于所需的新长度，并且检查是否发生了溢出
  if uint(newcap) &amp;gt;= uint(newLen) {
   break // 如果新容量足够大，或者发生了溢出，则退出循环
  }
 }

 // 当新容量计算溢出时，将新容量设置为请求的容量。
 // 如果计算过程中发生了溢出，则直接将新容量设置为所需的新长度，以确保切片能够容纳所有元素
 if newcap &amp;lt;= 0 {
  return newLen
 }

 return newcap // 返回计算得到的新容量
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Golang 切片原理&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/STHBnZ1737969258402080877.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/L5OPBU1737969429035465587.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;扩容规律&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/my5VWv1737969803395420365.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;切片作为参数&lt;/h2&gt;
&lt;p&gt;Go 语言的函数参数传递，只有值传递，没有引用传递，切片作为参数也是如此&lt;/p&gt;
&lt;p&gt;我们来验证这一点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/34ZRq21737970293711745015.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
 sl := []int{6, 6, 6}
 f(sl)
 fmt.Println(sl)
}

func f(sl []int) {
 for i := 0; i &amp;lt; 3; i++ {
  sl = append(sl, i)
 }
 fmt.Println(sl)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，输出的 sl 的值是不一样的，也就是说，f 函数没能修改主函数中的 sl 变量，而只是修改了形参 sl 变量的内容&lt;/p&gt;
&lt;p&gt;当我们传递一个切片给函数的时候，函数接收到的其实是这个切片的一个副本，但是他们的 array 字段指向的是同一个底层数组。&lt;/p&gt;
&lt;p&gt;这意味着，如果我们修改底层数组，是会影响到实参和形参的。&lt;/p&gt;
&lt;p&gt;我们看下面的例子：形参通过改变底层数组影响实参&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
 sl := []int{6, 6, 6}
 f(sl)
 fmt.Println(sl)
}

func f(sl []int) {
 sl[1] = 1
 sl[2] = 2
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/f395pe1737970003488259606.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;通过指针传递影响实参&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
 sl := []int{6, 6, 6}
 f(&amp;amp;sl)
 fmt.Println(sl)
}

func f(sl *[]int) {
 *sl = append(*sl, 200)
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/01/27/igiBeJ1737970227764617103.avif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java函数式接口</title><link>https://blog.meowrain.cn/posts/java/java%E5%87%BD%E6%95%B0%E5%BC%8F%E6%8E%A5%E5%8F%A3/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/java%E5%87%BD%E6%95%B0%E5%BC%8F%E6%8E%A5%E5%8F%A3/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/dgwblog/p/11739500.html&quot;&gt;https://www.cnblogs.com/dgwblog/p/11739500.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903892166148110&quot;&gt;https://juejin.cn/post/6844903892166148110&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/31/x6m66n-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/31/x722c1-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/31/x74ils-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1. &lt;code&gt;Supplier&amp;lt;T&amp;gt;&lt;/code&gt; - 数据的供给者 🎁&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口定义&lt;/strong&gt;：&lt;code&gt;@FunctionalInterface public interface Supplier&amp;lt;T&amp;gt; { T get(); }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：
&lt;code&gt;Supplier&lt;/code&gt; 接口的核心职责是&lt;strong&gt;生产或提供数据&lt;/strong&gt;，它不接受任何参数，但会返回一个 &lt;code&gt;T&lt;/code&gt; 类型的结果。你可以把它想象成一个“工厂”或者“源头”，当你需要一个特定类型的对象时，就调用它的 &lt;code&gt;get()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法详解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T get()&lt;/code&gt;: 这是 &lt;code&gt;Supplier&lt;/code&gt; 接口中唯一的抽象方法。调用它时，会执行你提供的 Lambda 表达式或方法引用所定义的逻辑，并返回一个结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常见应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟加载/创建对象&lt;/strong&gt;：当某个对象的创建成本较高，或者并非立即需要时，可以使用 &lt;code&gt;Supplier&lt;/code&gt; 来推迟其创建，直到真正使用时才调用 &lt;code&gt;get()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成默认值或配置信息&lt;/strong&gt;：提供一个默认对象或从某个源（如配置文件、数据库）获取配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成随机数据&lt;/strong&gt;：如示例中的随机数生成器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作为工厂方法&lt;/strong&gt;：在更复杂的场景中，&lt;code&gt;Supplier&lt;/code&gt; 可以作为创建对象的简单工厂。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;您的示例代码分析&lt;/strong&gt; (&lt;code&gt;SupplierExample.java&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Random;
import java.util.function.Supplier;

public class SupplierExample {

    // 示例方法1: 接收一个 Supplier 来获取随机整数
    public static Integer getRandomNumber(Supplier&amp;lt;Integer&amp;gt; randomNumberSupplier) {
        // 调用 randomNumberSupplier 的 get 方法来执行其提供的逻辑
        return randomNumberSupplier.get();
    }

    // 示例方法2: 接收一个 Supplier 来创建问候语字符串
    public static String createGreetingMessage(Supplier&amp;lt;String&amp;gt; greetingSupplier) {
        return greetingSupplier.get();
    }

    public static void main(String[] args) {
        // 场景1: 获取随机数
        // Lambda 表达式实现 Supplier: () -&amp;gt; new Random().nextInt(100)
        // 这个 Lambda 不接受参数，返回一个 0-99 的随机整数
        Supplier&amp;lt;Integer&amp;gt; randomIntSupplier = () -&amp;gt; new Random().nextInt(100);
        Integer num = getRandomNumber(randomIntSupplier); // 传递行为
        System.out.println(&quot;随机数: &quot; + num);

        // 场景2: 获取固定数字
        // Lambda 表达式实现 Supplier: () -&amp;gt; 42
        // 这个 Lambda 总是返回固定的数字 42
        Supplier&amp;lt;Integer&amp;gt; fixedIntSupplier = () -&amp;gt; 42;
        Integer fixedNum = getRandomNumber(fixedIntSupplier);
        System.out.println(&quot;固定数字: &quot; + fixedNum);

        // 场景3: 创建不同的问候语
        Supplier&amp;lt;String&amp;gt; englishGreeting = () -&amp;gt; &quot;Hello, World!&quot;;
        System.out.println(createGreetingMessage(englishGreeting)); // 输出: Hello, World!

        Supplier&amp;lt;String&amp;gt; spanishGreeting = () -&amp;gt; &quot;¡Hola, Mundo!&quot;;
        System.out.println(createGreetingMessage(spanishGreeting)); // 输出: ¡Hola, Mundo!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码解读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getRandomNumber&lt;/code&gt; 和 &lt;code&gt;createGreetingMessage&lt;/code&gt; 方法本身并不关心数字或字符串是如何产生的，它们只依赖传入的 &lt;code&gt;Supplier&lt;/code&gt; 来提供结果。这体现了&lt;strong&gt;行为参数化&lt;/strong&gt;——方法接受行为（通过函数式接口）作为参数。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main&lt;/code&gt; 方法中：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;randomIntSupplier&lt;/code&gt;: 定义了一个行为——“生成一个0到99的随机整数”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixedIntSupplier&lt;/code&gt;: 定义了另一个行为——“总是提供数字42”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;englishGreeting&lt;/code&gt; 和 &lt;code&gt;spanishGreeting&lt;/code&gt;: 定义了不同的行为来提供特定的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过将不同的 &lt;code&gt;Supplier&lt;/code&gt; 实现传递给同一个方法 (&lt;code&gt;getRandomNumber&lt;/code&gt; 或 &lt;code&gt;createGreetingMessage&lt;/code&gt;)，我们可以获得不同的结果，而无需修改方法本身。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键益处&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;灵活性&lt;/strong&gt;：可以轻松替换不同的供给逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：数据的使用者和数据的生产者解耦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可测试性&lt;/strong&gt;：可以方便地传入 mock 的 &lt;code&gt;Supplier&lt;/code&gt; 进行单元测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt; - 数据的转换器/映射器 🔄&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口定义&lt;/strong&gt;：&lt;code&gt;@FunctionalInterface public interface Function&amp;lt;T, R&amp;gt; { R apply(T t); }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：
&lt;code&gt;Function&lt;/code&gt; 接口的核心职责是&lt;strong&gt;将一个类型 &lt;code&gt;T&lt;/code&gt; 的输入参数转换或映射成另一个类型 &lt;code&gt;R&lt;/code&gt; 的输出结果&lt;/strong&gt;。它就像一个数据处理管道中的一个环节，接收数据，进行处理，然后传递给下一个环节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法详解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;R apply(T t)&lt;/code&gt;: 这是 &lt;code&gt;Function&lt;/code&gt; 的核心方法。它接受一个 &lt;code&gt;T&lt;/code&gt; 类型的参数 &lt;code&gt;t&lt;/code&gt;，对其执行Lambda表达式或方法引用中定义的转换逻辑，并返回一个 &lt;code&gt;R&lt;/code&gt; 类型的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常见应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据转换&lt;/strong&gt;：例如，将字符串转换为整数，将日期对象格式化为字符串，或者如示例中计算字符串长度、数字平方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象属性提取&lt;/strong&gt;：从一个复杂对象中提取某个特定属性的值。例如，&lt;code&gt;Person -&amp;gt; String (person.getName())&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链式操作&lt;/strong&gt;：&lt;code&gt;Function&lt;/code&gt; 接口提供了 &lt;code&gt;andThen()&lt;/code&gt; 和 &lt;code&gt;compose()&lt;/code&gt; 默认方法，可以方便地将多个 &lt;code&gt;Function&lt;/code&gt; 串联起来形成一个处理流水线。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;您的示例代码分析&lt;/strong&gt; (&lt;code&gt;FunctionExample.java&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.function.Function;

public class FunctionExample {

    // 示例方法1: 接收一个 Function 来计算字符串长度
    public static Integer getStringLength(String text, Function&amp;lt;String, Integer&amp;gt; lengthCalculator) {
        // 调用 lengthCalculator 的 apply 方法，传入 text，执行其转换逻辑
        return lengthCalculator.apply(text);
    }

    // 示例方法2: 接收一个 Function 来计算数字的平方
    public static Integer squareNumber(Integer number, Function&amp;lt;Integer, Integer&amp;gt; squareFunction) {
        return squareFunction.apply(number);
    }

    public static void main(String[] args) {
        // 场景1: 计算字符串长度
        String myString = &quot;Java Functional&quot;;
        // Lambda 表达式实现 Function: s -&amp;gt; s.length()
        // 这个 Lambda 接受一个 String s，返回其长度 (Integer)
        Function&amp;lt;String, Integer&amp;gt; lengthLambda = s -&amp;gt; s.length();
        Integer length = getStringLength(myString, lengthLambda);
        System.out.println(&quot;字符串 &apos;&quot; + myString + &quot;&apos; 的长度是: &quot; + length);

        // 使用方法引用 (Method Reference) 实现 Function: String::length
        // String::length 等价于 s -&amp;gt; s.length()，更为简洁
        Integer lengthUsingMethodRef = getStringLength(&quot;Test&quot;, String::length);
        System.out.println(&quot;字符串 &apos;Test&apos; 的长度是: &quot; + lengthUsingMethodRef);

        // 场景2: 计算数字平方
        Integer num = 5;
        // Lambda 表达式实现 Function: n -&amp;gt; n * n
        // 接受一个 Integer n，返回 n 的平方 (Integer)
        Function&amp;lt;Integer, Integer&amp;gt; squareLambda = n -&amp;gt; n * n;
        Integer squared = squareNumber(num, squareLambda);
        System.out.println(num + &quot; 的平方是: &quot; + squared);

        Integer anotherNum = 10;
        // 多行 Lambda 表达式
        Function&amp;lt;Integer, Integer&amp;gt; verboseSquareLambda = x -&amp;gt; {
            System.out.println(&quot;正在计算 &quot; + x + &quot; 的平方...&quot;); // Lambda 可以包含多条语句
            return x * x;
        };
        Integer squaredAgain = squareNumber(anotherNum, verboseSquareLambda);
        System.out.println(anotherNum + &quot; 的平方是: &quot; + squaredAgain);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码解读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getStringLength&lt;/code&gt; 和 &lt;code&gt;squareNumber&lt;/code&gt; 方法定义了操作的框架，但具体的转换逻辑由传入的 &lt;code&gt;Function&lt;/code&gt; 对象决定。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main&lt;/code&gt; 方法中：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s -&amp;gt; s.length()&lt;/code&gt; 和 &lt;code&gt;String::length&lt;/code&gt; 都是 &lt;code&gt;Function&amp;lt;String, Integer&amp;gt;&lt;/code&gt; 的实例，它们定义了“从字符串到其长度整数”的转换。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n -&amp;gt; n * n&lt;/code&gt; 是 &lt;code&gt;Function&amp;lt;Integer, Integer&amp;gt;&lt;/code&gt; 的实例，定义了“从整数到其平方整数”的转换。&lt;/li&gt;
&lt;li&gt;多行 Lambda &lt;code&gt;verboseSquareLambda&lt;/code&gt; 展示了更复杂的转换逻辑可以被封装。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这种方式使得我们可以为同一个通用方法（如 &lt;code&gt;getStringLength&lt;/code&gt;）提供不同的转换策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键益处&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码复用&lt;/strong&gt;：通用的转换逻辑可以被封装成 &lt;code&gt;Function&lt;/code&gt; 并在多处使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可组合性&lt;/strong&gt;：通过 &lt;code&gt;andThen&lt;/code&gt; 和 &lt;code&gt;compose&lt;/code&gt; 可以构建复杂的转换流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清晰性&lt;/strong&gt;：将数据转换的意图明确表达出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. &lt;code&gt;BiConsumer&amp;lt;T, U&amp;gt;&lt;/code&gt; - 双参数的消费者/执行者 🤝&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口定义&lt;/strong&gt;：&lt;code&gt;@FunctionalInterface public interface BiConsumer&amp;lt;T, U&amp;gt; { void accept(T t, U u); }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：
&lt;code&gt;BiConsumer&lt;/code&gt; 接口的核心职责是&lt;strong&gt;对两个不同类型（或相同类型）的输入参数 &lt;code&gt;T&lt;/code&gt; 和 &lt;code&gt;U&lt;/code&gt; 执行某个操作或产生某种副作用，但它不返回任何结果 (void)&lt;/strong&gt;。你可以把它看作是需要两个输入才能完成其工作的“执行者”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法详解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void accept(T t, U u)&lt;/code&gt;: 这是 &lt;code&gt;BiConsumer&lt;/code&gt; 的核心方法。它接受两个参数 &lt;code&gt;t&lt;/code&gt; 和 &lt;code&gt;u&lt;/code&gt;，并对它们执行 Lambda 表达式或方法引用中定义的操作。由于返回类型是 &lt;code&gt;void&lt;/code&gt;，它通常用于执行有副作用的操作，如打印、修改集合、更新数据库等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常见应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理键值对&lt;/strong&gt;：非常适合用于迭代 &lt;code&gt;Map&lt;/code&gt; 的条目，如 &lt;code&gt;Map.forEach()&lt;/code&gt; 方法就接受一个 &lt;code&gt;BiConsumer&amp;lt;K, V&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同时操作两个相关对象&lt;/strong&gt;：当一个操作需要两个输入，并且不产生新的独立结果时。例如，将一个对象的属性设置到另一个对象上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置或初始化&lt;/strong&gt;：使用两个参数来配置某个组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;您的示例代码分析&lt;/strong&gt; (&lt;code&gt;BiConsumerExample.java&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class BiConsumerExample {

    // 示例方法1: 接收 BiConsumer 来打印键和值
    public static &amp;lt;K, V&amp;gt; void printMapEntry(K key, V value, BiConsumer&amp;lt;K, V&amp;gt; entryPrinter) {
        // 调用 entryPrinter 的 accept 方法，传入 key 和 value
        entryPrinter.accept(key, value);
    }

    // 示例2 在 main 中直接演示了更常见的 Map 操作方式

    // 辅助内部类，如果 BiConsumer 需要一次性接收多个信息 (在此示例中未直接用于核心 BiConsumer 演示)
    // static class Pair&amp;lt;F, S&amp;gt; {
    //     F first; S second;
    //     Pair(F f, S s) { this.first = f; this.second = s; }
    // }

    public static void main(String[] args) {
        // 场景1: 使用 printMapEntry 打印键值
        // Lambda 表达式实现 BiConsumer: (k, v) -&amp;gt; System.out.println(&quot;键: &quot; + k + &quot;, 值: &quot; + v)
        // 接受一个 String k 和一个 Integer v，然后打印它们
        BiConsumer&amp;lt;String, Integer&amp;gt; simplePrinter = (k, v) -&amp;gt; System.out.println(&quot;键: &quot; + k + &quot;, 值: &quot; + v);
        printMapEntry(&quot;年龄&quot;, 30, simplePrinter);
        printMapEntry(&quot;数量&quot;, 100, simplePrinter);

        // 场景2: 使用 BiConsumer 来填充 Map
        Map&amp;lt;String, String&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        // Lambda 表达式实现 BiConsumer: (key, value) -&amp;gt; config.put(key, value)
        // 这个 Lambda 捕获了外部的 &apos;config&apos; Map 对象。
        // 它接受 String key 和 String value，并将它们放入 config Map 中。
        BiConsumer&amp;lt;String, String&amp;gt; mapPutter = (key, value) -&amp;gt; config.put(key, value);

        mapPutter.accept(&quot;user.name&quot;, &quot;Alice&quot;); // 执行操作：config.put(&quot;user.name&quot;, &quot;Alice&quot;)
        mapPutter.accept(&quot;user.role&quot;, &quot;Admin&quot;);   // 执行操作：config.put(&quot;user.role&quot;, &quot;Admin&quot;)
        System.out.println(&quot;配置Map: &quot; + config);

        // 场景3: Map.forEach() 的典型用法
        // Map 的 forEach 方法直接接受一个 BiConsumer&amp;lt;K, V&amp;gt;
        System.out.println(&quot;遍历Map:&quot;);
        config.forEach((key, value) -&amp;gt; { // 这里的 (key, value) -&amp;gt; {...} 就是一个 BiConsumer
            System.out.println(&quot;配置项 - &quot; + key + &quot;: &quot; + value);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码解读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;printMapEntry&lt;/code&gt; 方法接受一个键、一个值和一个 &lt;code&gt;BiConsumer&lt;/code&gt;，该 &lt;code&gt;BiConsumer&lt;/code&gt; 定义了如何处理这对键值。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main&lt;/code&gt; 方法中：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;simplePrinter&lt;/code&gt;: 定义了一个行为——“接收一个键和一个值，并将它们打印到控制台”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapPutter&lt;/code&gt;: 定义了一个行为——“接收一个键和一个字符串值，并将它们存入外部的 &lt;code&gt;config&lt;/code&gt; Map”。这里 Lambda 表达式捕获了外部变量 &lt;code&gt;config&lt;/code&gt;，这是一种常见的用法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config.forEach(...)&lt;/code&gt;: 这是 &lt;code&gt;BiConsumer&lt;/code&gt; 最经典的用例之一。&lt;code&gt;forEach&lt;/code&gt; 方法遍历 &lt;code&gt;Map&lt;/code&gt; 中的每个条目，并对每个键值对执行提供的 &lt;code&gt;BiConsumer&lt;/code&gt; 逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键益处&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理成对数据&lt;/strong&gt;：专门设计用于需要两个输入的场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与集合（尤其是Map）的良好集成&lt;/strong&gt;：&lt;code&gt;Map.forEach&lt;/code&gt; 是一个很好的例子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装副作用操作&lt;/strong&gt;：可以将对两个参数的副作用操作（如修改、打印）封装起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. &lt;code&gt;Consumer&amp;lt;T&amp;gt;&lt;/code&gt; - 数据的消费者/执行者 🍽️&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口定义&lt;/strong&gt;：&lt;code&gt;@FunctionalInterface public interface Consumer&amp;lt;T&amp;gt; { void accept(T t); }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：
&lt;code&gt;Consumer&lt;/code&gt; 接口的核心职责是&lt;strong&gt;对单个输入参数 &lt;code&gt;T&lt;/code&gt; 执行某个操作或产生某种副作用，它不返回任何结果 (void)&lt;/strong&gt;。你可以把它看作是数据的“终点”或某个动作的执行者，它“消费”数据但不产生新的输出数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法详解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void accept(T t)&lt;/code&gt;: 这是 &lt;code&gt;Consumer&lt;/code&gt; 的核心方法。它接受一个 &lt;code&gt;T&lt;/code&gt; 类型的参数 &lt;code&gt;t&lt;/code&gt;，并对其执行 Lambda 表达式或方法引用中定义的操作。因为返回 &lt;code&gt;void&lt;/code&gt;，它主要用于执行那些为了副作用而进行的操作（如打印、修改对象状态、写入文件等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常见应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;迭代集合并处理元素&lt;/strong&gt;：&lt;code&gt;List.forEach()&lt;/code&gt; 方法接受一个 &lt;code&gt;Consumer&amp;lt;T&amp;gt;&lt;/code&gt;，对列表中的每个元素执行指定操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打印/日志记录&lt;/strong&gt;：将信息输出到控制台、文件或其他日志系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新对象状态&lt;/strong&gt;：修改传入对象的属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回调&lt;/strong&gt;：在某个异步操作完成后执行一个 &lt;code&gt;Consumer&lt;/code&gt; 定义的动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;您的示例代码分析&lt;/strong&gt; (&lt;code&gt;ConsumerExample.java&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class ConsumerExample {

    // 示例方法1: 接收 Consumer 来展示单个项目
    public static &amp;lt;T&amp;gt; void displayItem(T item, Consumer&amp;lt;T&amp;gt; itemDisplayer) {
        // 调用 itemDisplayer 的 accept 方法，传入 item，执行其消费逻辑
        itemDisplayer.accept(item);
    }

    // 示例方法2: 接收 Consumer 来处理列表中的每个项目
    public static &amp;lt;T&amp;gt; void processListItems(List&amp;lt;T&amp;gt; list, Consumer&amp;lt;T&amp;gt; itemProcessor) {
        for (T item : list) {
            itemProcessor.accept(item); // 对列表中的每个 item 执行 itemProcessor 的逻辑
        }
    }

    public static void main(String[] args) {
        // 场景1: 使用 displayItem 打印信息
        // Lambda 表达式实现 Consumer: message -&amp;gt; System.out.println(&quot;消息: &quot; + message)
        // 接受一个 String message，然后打印它
        Consumer&amp;lt;String&amp;gt; consolePrinter = message -&amp;gt; System.out.println(&quot;消息: &quot; + message);
        displayItem(&quot;你好，函数式接口!&quot;, consolePrinter);

        // 多行 Lambda 实现 Consumer，进行更复杂的打印
        Consumer&amp;lt;Integer&amp;gt; detailedPrinter = number -&amp;gt; {
            System.out.println(&quot;--- 数字详情 ---&quot;);
            System.out.println(&quot;值: &quot; + number);
            System.out.println(&quot;是否偶数: &quot; + (number % 2 == 0));
            System.out.println(&quot;----------------&quot;);
        };
        displayItem(10, detailedPrinter);
        displayItem(7, System.out::println); // 方法引用: System.out::println 等价于 x -&amp;gt; System.out.println(x)

        // 场景2: 使用 processListItems 处理列表
        List&amp;lt;String&amp;gt; names = Arrays.asList(&quot;爱丽丝&quot;, &quot;鲍勃&quot;, &quot;查理&quot;);

        System.out.println(&quot;\n打印名字:&quot;);
        // Lambda: name -&amp;gt; System.out.println(&quot;你好, &quot; + name + &quot;!&quot;)
        // 对列表中的每个名字，执行打印问候语的操作
        processListItems(names, name -&amp;gt; System.out.println(&quot;你好, &quot; + name + &quot;!&quot;));

        System.out.println(&quot;\n将名字转换为大写并打印 (仅打印，不修改原列表):&quot;);
        // Lambda: name -&amp;gt; System.out.println(name.toUpperCase())
        // 对列表中的每个名字，先转大写，然后打印
        processListItems(names, name -&amp;gt; System.out.println(name.toUpperCase()));

        // Consumer 也可以有副作用，比如修改外部状态 (通常需谨慎使用以避免复杂性)
        StringBuilder allNames = new StringBuilder();
        // Lambda: name -&amp;gt; allNames.append(name).append(&quot; &quot;)
        // 这个 Consumer 修改了外部的 allNames 对象
        processListItems(names, name -&amp;gt; allNames.append(name).append(&quot; &quot;));
        System.out.println(&quot;\n拼接所有名字: &quot; + allNames.toString().trim());

        // List.forEach 的典型用法
        System.out.println(&quot;\n使用 List.forEach 打印名字（大写）:&quot;);
        names.forEach(name -&amp;gt; System.out.println(name.toUpperCase())); // name -&amp;gt; System.out.println(...) 是一个Consumer
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码解读&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;displayItem&lt;/code&gt; 方法接受一个项目和一个 &lt;code&gt;Consumer&lt;/code&gt;，该 &lt;code&gt;Consumer&lt;/code&gt; 定义了如何“消费”或处理这个项目。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processListItems&lt;/code&gt; 方法遍历列表，并对每个元素应用传入的 &lt;code&gt;Consumer&lt;/code&gt; 逻辑。这与 &lt;code&gt;List.forEach()&lt;/code&gt; 的行为非常相似。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main&lt;/code&gt; 方法中：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;consolePrinter&lt;/code&gt; 和 &lt;code&gt;detailedPrinter&lt;/code&gt; 定义了不同的打印行为。&lt;code&gt;System.out::println&lt;/code&gt; 是一个简洁的方法引用，用于直接打印。&lt;/li&gt;
&lt;li&gt;在处理 &lt;code&gt;names&lt;/code&gt; 列表时，通过传递不同的 &lt;code&gt;Consumer&lt;/code&gt; 给 &lt;code&gt;processListItems&lt;/code&gt;，实现了不同的处理逻辑（简单问候、转换为大写打印、追加到 &lt;code&gt;StringBuilder&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allNames.append(...)&lt;/code&gt; 的例子展示了 &lt;code&gt;Consumer&lt;/code&gt; 如何产生副作用（修改外部对象的状态）。虽然强大，但在复杂系统中应谨慎使用副作用，以保持代码的可预测性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;names.forEach(...)&lt;/code&gt; 直接使用了 &lt;code&gt;List&lt;/code&gt; 接口内置的 &lt;code&gt;forEach&lt;/code&gt; 方法，该方法就接受一个 &lt;code&gt;Consumer&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键益处&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;执行动作&lt;/strong&gt;：非常适合表示对数据执行的无返回值的操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代与处理&lt;/strong&gt;：与集合框架（如 &lt;code&gt;List.forEach&lt;/code&gt;）完美配合，简化迭代代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装副作用&lt;/strong&gt;：将有副作用的操作（如I/O、UI更新）封装到 &lt;code&gt;Consumer&lt;/code&gt; 中，使得代码意图更清晰。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>Gin框架快速入门</title><link>https://blog.meowrain.cn/posts/golang/gin%E6%A1%86%E6%9E%B6%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/golang/gin%E6%A1%86%E6%9E%B6%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Go 语言 Web 框架 Gin&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;参考
&lt;a href=&quot;https://docs.fengfengzhidao.com&quot;&gt;https://docs.fengfengzhidao.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.liwenzhou.com/posts/Go/gin/#c-0-7-2&quot;&gt;https://www.liwenzhou.com/posts/Go/gin/#c-0-7-2&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;返回各种值&lt;/h1&gt;
&lt;h2&gt;返回字符串&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func main() {

    router := gin.Default()

    router.GET(&quot;/&quot;, func(c *gin.Context) {

        c.String(http.StatusOK, &quot;helloworld&quot;)

    })

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;返回 json&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



type Student struct {

    Name string `json:&quot;name&quot;`

    Age int `json:&quot;age&quot;`

    Number string `json: &quot;number&quot;`

}



func main() {

    router := gin.Default()

    router.GET(&quot;/&quot;, func(c *gin.Context) {

        var student Student = Student{

            Name: &quot;meowrain&quot;,

            Age: 20,

            Number: &quot;10086&quot;,

        }

        c.JSON(http.StatusOK, student)

    })

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/vs14ff-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;返回 map&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func main() {

    router := gin.Default()

    router.GET(&quot;/&quot;, func(c *gin.Context) {

        userMap := map[string]any{

            &quot;username&quot;: &quot;meowrain&quot;,

            &quot;age&quot;: 20,

            &quot;number&quot;: 10086,

        }

        c.JSON(http.StatusOK, userMap)

    })

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/vu58ct-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;返回原始 json&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func main() {

    router := gin.Default()

    router.GET(&quot;/&quot;, func(c *gin.Context) {



        c.JSON(http.StatusOK, gin.H{

            &quot;username&quot;: &quot;meowrain&quot;,



            &quot;age&quot;: 20,



            &quot;number&quot;: 10086,

        })

    })

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/vuxhez-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/vv144f-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;返回 html 并传递参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func _html(c *gin.Context) {

    type UserInfo struct {

        Username string `json:&quot;username&quot;`

        Age int `json:&quot;age&quot;`

        Password string `json:&quot;-&quot;`

    }

    user := UserInfo{

        Username: &quot;meowrain&quot;,

        Age: 20,

        Password: &quot;12345678&quot;,

    }

    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{&quot;obj&quot;: user})

}

func main() {

    router := gin.Default()

    router.LoadHTMLGlob(&quot;template/*&quot;)

    router.GET(&quot;/&quot;, _html)

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;

&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;

    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;

    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;User Information&amp;lt;/h1&amp;gt;

    &amp;lt;p&amp;gt;Username: {{.obj.Username}}&amp;lt;/p&amp;gt;

    &amp;lt;p&amp;gt;Age: {{.obj.Age}}&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/10gipeq-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;静态文件配置&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;router.Static&lt;/code&gt;和&lt;code&gt;router.StaticFS&lt;/code&gt;都是用于处理静态文件的 Gin 框架路由处理方法，但它们有一些区别。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;router.Static&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;router.Static&lt;/code&gt; 时，Gin 会简单地将请求的 URL 路径与提供的本地文件系统路径进行映射。通常，这适用于将 URL 路径直接映射到一个静态文件或目录。&lt;/li&gt;
&lt;li&gt;示例：&lt;code&gt;router.Static(&quot;/static&quot;, &quot;./static&quot;)&lt;/code&gt; 将 &lt;code&gt;/static&lt;/code&gt; 映射到当前工作目录下的 &lt;code&gt;./static&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;router.StaticFS&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;router.StaticFS&lt;/code&gt; 则允许你使用 &lt;code&gt;http.FileSystem&lt;/code&gt; 对象，这可以提供更多的灵活性。你可以使用 &lt;code&gt;http.Dir&lt;/code&gt; 创建 &lt;code&gt;http.FileSystem&lt;/code&gt;，并将其传递给 &lt;code&gt;router.StaticFS&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这允许你更灵活地处理静态文件，例如从不同的源（内存、数据库等）加载静态文件，而不仅限于本地文件系统。&lt;/li&gt;
&lt;li&gt;示例：&lt;code&gt;router.StaticFS(&quot;/static&quot;, http.Dir(&quot;/path/to/static/files&quot;))&lt;/code&gt; 使用本地文件系统路径创建一个 &lt;code&gt;http.FileSystem&lt;/code&gt; 对象，然后将 &lt;code&gt;/static&lt;/code&gt; 映射到这个文件系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体而言，&lt;code&gt;router.Static&lt;/code&gt;更简单，适用于基本的静态文件服务，而&lt;code&gt;router.StaticFS&lt;/code&gt;提供了更多的灵活性，允许你自定义静态文件的加载方式。选择使用哪一个取决于你的具体需求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func _html(c *gin.Context) {

    type UserInfo struct {

        Username string `json:&quot;username&quot;`

        Age int `json:&quot;age&quot;`

        Password string `json:&quot;-&quot;`

    }

    user := UserInfo{

        Username: &quot;meowrain&quot;,

        Age: 20,

        Password: &quot;12345678&quot;,

    }

    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{&quot;obj&quot;: user})

}

func main() {

    router := gin.Default()

    router.LoadHTMLGlob(&quot;template/*&quot;)

    router.Static(&quot;/static/&quot;, &quot;./static&quot;)

    router.GET(&quot;/&quot;, _html)

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/10q03tz-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;

&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;

    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;

    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;User Information&amp;lt;/h1&amp;gt;

    &amp;lt;p&amp;gt;Username: {{.obj.Username}}&amp;lt;/p&amp;gt;

    &amp;lt;p&amp;gt;Age: {{.obj.Age}}&amp;lt;/p&amp;gt;

    &amp;lt;img src=&quot;/static/c68a16221f5bdf5486749d0993052981178827471.jpg&quot; /&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/10qf6z6-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;重定向&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func _html(c *gin.Context) {

    type UserInfo struct {

        Username string `json:&quot;username&quot;`

        Age int `json:&quot;age&quot;`

        Password string `json:&quot;-&quot;`

    }

    user := UserInfo{

        Username: &quot;meowrain&quot;,

        Age: 20,

        Password: &quot;12345678&quot;,

    }

    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{&quot;obj&quot;: user})

}

func _redirect(c *gin.Context) {

    c.Redirect(301, &quot;https://www.baidu.com&quot;)

}

func main() {

    router := gin.Default()

    router.LoadHTMLGlob(&quot;template/*&quot;)

    router.Static(&quot;/static/&quot;, &quot;./static&quot;)

    router.GET(&quot;/&quot;, _html)

    router.GET(&quot;/baidu&quot;, _redirect)

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/10xi5tr-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;301 和 302 的区别&lt;/h3&gt;
&lt;p&gt;HTTP 状态码中的 301 和 302 分别表示重定向（Redirect）。它们之间的主要区别在于重定向的性质和原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;301 Moved Permanently（永久重定向）&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当服务器返回状态码 301 时，它告诉客户端请求的资源已经被永久移动到新的位置。&lt;/li&gt;
&lt;li&gt;客户端收到 301 响应后，应该更新书签、链接等，将这个新的位置作为将来所有对该资源的请求的目标。&lt;/li&gt;
&lt;li&gt;搜索引擎在遇到 301 时，通常会更新索引，将原始 URL 替换为新的 URL。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;302 Found（临时重定向）&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当服务器返回状态码 302 时，它表示请求的资源暂时被移动到了另一个位置。&lt;/li&gt;
&lt;li&gt;客户端收到 302 响应后，可以在不更新书签和链接的情况下继续使用原始 URL。&lt;/li&gt;
&lt;li&gt;搜索引擎在遇到 302 时，通常会保留原始 URL 在索引中，并不会立即更新为新的 URL。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体来说，使用 301 通常是在确定资源永久移动的情况下，而 302 通常用于暂时性的重定向，即资源可能在将来回到原始位置。选择使用哪种状态码取决于你希望客户端和搜索引擎如何处理被重定向的资源。&lt;/p&gt;
&lt;h1&gt;路由&lt;/h1&gt;
&lt;h2&gt;默认路由&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;当访问路径不被匹配的时候返回默认路由内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目录结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/y21i4t-3.webp&quot; alt=&quot;image-20240308205926484&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//main.go
package main

import (
 &quot;awesomeProject/pkg/controller&quot;
 &quot;github.com/gin-gonic/gin&quot;
)

func main() {
 router := gin.Default()
 router.LoadHTMLGlob(&quot;templates/*&quot;)
 router.GET(&quot;/&quot;, func(c *gin.Context) {
  c.String(200, &quot;helloworld&quot;)
 })
 router.NoRoute(controller.Default_route)
 router.Run(&quot;:80&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//server.go
package controller

import (
 &quot;github.com/gin-gonic/gin&quot;
 &quot;net/http&quot;
)

func Default_route(c *gin.Context) {
 c.HTML(http.StatusNotFound, &quot;404.html&quot;, nil)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--404.html--&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;404 NOT FOUND&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;404 Not Found&amp;lt;/h1&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;效果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/y25trv-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/yqb64w-3.webp&quot; alt=&quot;image-20240308210004320&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;路由组&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://www.liwenzhou.com/posts/Go/gin/#c-0-7-2&quot;&gt;https://www.liwenzhou.com/posts/Go/gin/#c-0-7-2&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以将拥有共同 URL 前缀的路由划分为一个路由组。习惯性一对&lt;code&gt;{}&lt;/code&gt;包裹同组的路由，这只是为了看着清晰，你用不用&lt;code&gt;{}&lt;/code&gt;包裹功能上没什么区别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/z7tbui-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//main.go
package main

import (
 &quot;awesomeProject/pkg/controller&quot;
 &quot;github.com/gin-gonic/gin&quot;
)

func main() {

 router := gin.Default()
 userGroup := router.Group(&quot;/user&quot;)
 {
  userGroup.GET(&quot;/all&quot;, controller.GetUserList)
  userGroup.GET(&quot;/detail&quot;, controller.GetUserDetail)
 }
 router.LoadHTMLGlob(&quot;templates/*&quot;)
 router.NoRoute(controller.Default_route)
 router.Run(&quot;:80&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//controller/userController.go
package controller

import (
 . &quot;awesomeProject/pkg/entity&quot;
 &quot;github.com/gin-gonic/gin&quot;
 &quot;net/http&quot;
 &quot;strconv&quot;
)

func GetUserList(c *gin.Context) {
 c.JSON(http.StatusOK, Response{
  Code: http.StatusOK,
  Data: UserList,
  Msg:  &quot;返回成功&quot;,
 })
}
func GetUserDetail(c *gin.Context) {
 id := c.Query(&quot;id&quot;)
 for _, res := range UserList {
  if strconv.Itoa(res.ID) == id {
   c.JSON(http.StatusOK, Response{
    Code: http.StatusOK,
    Data: res,
    Msg:  &quot;get successfully&quot;,
   })
  }
 }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//user.go
package entity

type User struct {
 ID   int    `json:&quot;id&quot;`
 Name string `json:&quot;name&quot;`
 Age  int    `json:&quot;age&quot;`
}

type Response struct {
 Code int    `json:&quot;code&quot;`
 Data any    `json:&quot;data&quot;`
 Msg  string `json:&quot;msg&quot;`
}

var UserList []User = []User{
 {
  ID:   1,
  Name: &quot;meowrian&quot;,
  Age:  20,
 },
 {
  ID:   2,
  Name: &quot;Mike&quot;,
  Age:  30,
 },
 {
  ID:   3,
  Name: &quot;Amy&quot;,
  Age:  23,
 },
 {
  ID:   4,
  Name: &quot;John&quot;,
  Age:  24,
 },
}


&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//server.go
package controller

import (
 &quot;github.com/gin-gonic/gin&quot;
 &quot;net/http&quot;
)

func Default_route(c *gin.Context) {
 c.HTML(http.StatusNotFound, &quot;404.html&quot;, nil)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/z8h8cb-3.webp&quot; alt=&quot;image-20240308213055236&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/z8u72c-3.webp&quot; alt=&quot;image-20240308213116279&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;路由组也是支持嵌套的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;参数&lt;/h1&gt;
&lt;h2&gt;查询参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main



import (

    &quot;net/http&quot;



    &quot;github.com/gin-gonic/gin&quot;

)



func _query(c *gin.Context) {

    user := c.Query(&quot;user&quot;)

    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{

        &quot;user&quot;: user,

    })

}

func main() {

    router := gin.Default()

    router.LoadHTMLGlob(&quot;template/*&quot;)

    router.Static(&quot;/static&quot;, &quot;./static&quot;)

    router.GET(&quot;/&quot;, _query)

    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/112i3vb-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
    &quot;github.com/gin-gonic/gin&quot;)

func _query(c *gin.Context) {
    user, ok := c.GetQuery(&quot;user&quot;)
    ids := c.QueryArray(&quot;id&quot;) //拿到多个相同的查询参数
    maps := c.QueryMap(&quot;id&quot;)
    fmt.Println(maps)
    if ok {
        c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{
            &quot;user&quot;: user,
            &quot;id&quot;:   ids,
        })
    } else {
        c.String(http.StatusOK, &quot;No query!&quot;)
    }
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob(&quot;template/*&quot;)
    router.Static(&quot;static&quot;, &quot;./static&quot;)
    router.GET(&quot;/&quot;, _query)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;请求为： &lt;a href=&quot;http://127.0.0.1:8080/?user=good&amp;amp;id=1&amp;amp;id=2&amp;amp;id=3&amp;amp;id%5Bgood%5D=meowrain&quot;&gt;http://127.0.0.1:8080/?user=good&amp;amp;id=1&amp;amp;id=2&amp;amp;id=3&amp;amp;id[good]=meowrain&lt;/a&gt; &amp;gt; &lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/12532gz-3.webp&quot; alt=&quot;&quot; /&gt; &amp;gt; &lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/1267bmh-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;动态参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;    &quot;net/http&quot;)

func _param(c *gin.Context) {
    param := c.Param(&quot;user_id&quot;)
    fmt.Println(param)
    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{
        &quot;param&quot;: param,
    })

}
func main() {
    router := gin.Default()
    router.LoadHTMLGlob(&quot;template/*&quot;)
    router.Static(&quot;static&quot;, &quot;./static&quot;)
    router.GET(&quot;/param/:user_id&quot;, _param)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/12ac8nv-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;表单参数 PostForm&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;github.com/gin-gonic/gin&quot;
    &quot;net/http&quot;)

func postForm(c *gin.Context) {
    name := c.PostForm(&quot;name&quot;)
    password := c.PostForm(&quot;password&quot;)
    c.JSON(http.StatusOK, gin.H{
       &quot;name&quot;:     name,
       &quot;password&quot;: password,
    })
}
func index(c *gin.Context) {
    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{})
}
func main() {
    router := gin.Default()
    router.LoadHTMLGlob(&quot;template/*&quot;)
    router.Static(&quot;static&quot;, &quot;./static&quot;)
    router.GET(&quot;/&quot;, index)
    router.POST(&quot;/post&quot;, postForm)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Post Form Test&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Post Form Test&amp;lt;/h1&amp;gt;
    &amp;lt;form id=&quot;myForm&quot; action=&quot;/post&quot; method=&quot;post&quot;&amp;gt;
      &amp;lt;label for=&quot;name&quot;&amp;gt;Name:&amp;lt;/label&amp;gt;
      &amp;lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; required /&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;label for=&quot;password&quot;&amp;gt;Password: &amp;lt;/label&amp;gt;
      &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; required /&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;button type=&quot;button&quot; onclick=&quot;postData()&quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;h3 id=&quot;response&quot;&amp;gt;Response:&amp;lt;/h3&amp;gt;
    &amp;lt;script&amp;gt;
      function postData() {
        var form = document.getElementById(&quot;myForm&quot;);
        var formData = new FormData(form);
        var resp = document.getElementById(&quot;response&quot;);
        fetch(&quot;http://127.0.0.1:8080/post&quot;, {
          method: &quot;POST&quot;,
          body: formData,
        })
          .then((response) =&amp;gt; response.json())
          .then((data) =&amp;gt; {
            console.log(&quot;Success:&quot;, data);
            resp.innerText = &quot;Response: &quot; + JSON.stringify(data);
          })
          .catch((error) =&amp;gt; {
            console.error(&quot;Error:&quot;, error);
            resp.innerText = &quot;Response Error: &quot; + JSON.stringify(error);
          });
      }
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/12lcebn-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;postFormArray 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;github.com/gin-gonic/gin&quot;
    &quot;net/http&quot;)

func postForm(c *gin.Context) {
    name := c.PostForm(&quot;name&quot;)
    password := c.PostForm(&quot;password&quot;)
    respArr := c.PostFormArray(&quot;name&quot;)
    c.JSON(http.StatusOK, gin.H{
       &quot;name&quot;:      name,
       &quot;password&quot;:  password,
       &quot;respArray&quot;: respArr,
    })
}
func index(c *gin.Context) {
    c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{})
}
func main() {
    router := gin.Default()
    router.LoadHTMLGlob(&quot;template/*&quot;)
    router.Static(&quot;static&quot;, &quot;./static&quot;)
    router.GET(&quot;/&quot;, index)
    router.POST(&quot;/post&quot;, postForm)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/07/12n0072-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;原始参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/*
原始参数
*/
package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;)

func _raw(c *gin.Context) {
    buf, err := c.GetRawData()
    if err != nil {
       fmt.Println(&quot;error:&quot;, err)
       return
    }
    fmt.Println(string(buf))
}
func main() {
    router := gin.Default()
    router.POST(&quot;/&quot;, _raw)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/extkqj-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/exvpa7-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;解析 json 数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
原始参数
*/
package main

import (
    &quot;encoding/json&quot;
    &quot;fmt&quot;    &quot;github.com/gin-gonic/gin&quot;)

func bindJSON(c *gin.Context, obj any) error {
    body, err := c.GetRawData()
    contentType := c.GetHeader(&quot;Content-Type&quot;)
    fmt.Println(&quot;ContentType:&quot;, contentType)
    if err != nil {
       fmt.Println(&quot;error:&quot;, err)
       return err
    }
    switch contentType {
    case &quot;application/json&quot;:
       err := json.Unmarshal(body, obj)
       if err != nil {
          fmt.Println(err.Error())
          return err
       }
    }
    return nil
}

func raw(c *gin.Context) {
    type User struct {
       Name     string `json:&quot;name&quot;`
       Age      int    `json:&quot;age&quot;`
       Password string `json:&quot;-&quot;`
    }
    var user User
    err := bindJSON(c, &amp;amp;user)
    if err != nil {
       fmt.Println(&quot;Error binding JSON:&quot;, err)
       return
    }
    fmt.Println(user)
}

func main() {
    router := gin.Default()
    router.POST(&quot;/&quot;, raw)
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/fkacjn-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/fki60h-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;四大请求方式&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/fnulpk-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;简单实现以下 CRUD&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;github.com/gin-gonic/gin&quot;
    &quot;net/http&quot;    &quot;strconv&quot;)

type Article struct {
    Id      int    `json:&quot;id&quot;`
    Title   string `json:&quot;title&quot;`
    Content string `json:&quot;content&quot;`
    Author  string `json:&quot;author&quot;`
}

type Response struct {
    Code int    `json:&quot;code&quot;`
    Data any    `json:&quot;data&quot;`
    Msg  string `json:&quot;msg&quot;`
}

var articleList []Article = []Article{
    {
       1,
       &quot;Go语言从入门到精通&quot;,
       &quot;Learn better&quot;,
       &quot;Mike Jason&quot;,
    },
    {
       2,
       &quot;Java从入门到精通&quot;,
       &quot;Java is good&quot;,
       &quot;Jack Smith&quot;,
    },
    {
       3,
       &quot;Javascript从入门到精通&quot;,
       &quot;Javascript is a nice programming language!&quot;,
       &quot;Amy Gorden&quot;,
    },
    {
       4,
       &quot;Python从入门到精通&quot;,
       &quot;Python is a simple language!&quot;,
       &quot;Jack Buffer&quot;,
    },
}

/*简单增删改查*/
func _getList(c *gin.Context) {

    c.JSON(http.StatusOK, Response{Code: 200, Data: articleList, Msg: &quot;获取成功&quot;})
}
func _getDetail(c *gin.Context) {
    id := c.Param(&quot;id&quot;)
    flag := false
    for _, res := range articleList {
       if strconv.Itoa(res.Id) == id {
          flag = true
          c.JSON(http.StatusOK, Response{
             Code: 200,
             Data: res,
             Msg:  &quot;获取成功！&quot;,
          })
       }
    }
    if flag == false {
       c.JSON(404, Response{
          Code: 404,
          Data: &quot;Not Found the data&quot;,
          Msg:  &quot;获取失败，因为数据不存在&quot;,
       })
    }
}
func _create(c *gin.Context) {
    id, _ := strconv.ParseInt(c.PostForm(&quot;id&quot;), 10, 0)
    title := c.PostForm(&quot;title&quot;)
    content := c.PostForm(&quot;content&quot;)
    author := c.PostForm(&quot;author&quot;)
    var article Article = Article{
       Id:      int(id),
       Title:   title,
       Content: content,
       Author:  author,
    }
    articleList = append(articleList, article)
    c.JSON(200, Response{Code: 200, Data: article, Msg: &quot;添加成功！&quot;})
}
func _delete(c *gin.Context) {
    id := c.Param(&quot;id&quot;)
    index := -1
    for i, res := range articleList {
       if strconv.Itoa(res.Id) == id {
          index = i
          break
       }
    }
    if index != -1 {
       articleList = append(articleList[:index], articleList[index+1:]...)
       c.JSON(http.StatusOK, Response{Code: 200, Data: nil, Msg: &quot;删除成功&quot;})
    } else {
       c.JSON(http.StatusNotFound, Response{Code: 404, Data: &quot;Not Found the data&quot;, Msg: &quot;删除失败，数据不存在&quot;})
    }
}
func _update(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param(&quot;id&quot;))
    title := c.PostForm(&quot;title&quot;)
    content := c.PostForm(&quot;content&quot;)
    author := c.PostForm(&quot;author&quot;)
    found := false
    for i, res := range articleList {
       if res.Id == id {
          found = true
          articleList[i] = Article{
             id,
             title,
             content,
             author,
          }
          break
       }
    }
    if found {
       c.JSON(http.StatusOK, Response{
          Code: 200,
          Data: nil,
          Msg:  &quot;更新成功&quot;,
       })
       return
    } else {
       c.JSON(http.StatusNotFound, Response{
          Code: 404,
          Data: &quot;Not found the data&quot;,
          Msg:  &quot;更新失败，因为数据不存在&quot;,
       })
    }

}

func main() {
    router := gin.Default()
    router.GET(&quot;/articles&quot;, _getList)
    router.GET(&quot;/articles/:id&quot;, _getDetail)
    router.POST(&quot;/articles&quot;, _create)
    router.PUT(&quot;/articles/:id&quot;, _update)
    router.DELETE(&quot;/articles/:id&quot;, _delete)
    router.Run(&quot;:8080&quot;)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文件上传&lt;/h2&gt;
&lt;h3&gt;上传单个文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
 &quot;awesomeProject/pkg/controller&quot;
 &quot;github.com/gin-gonic/gin&quot;
)

func main() {

 router := gin.Default()
 router.LoadHTMLGlob(&quot;templates/*&quot;)
 router.GET(&quot;/&quot;, func(c *gin.Context) {
  c.HTML(200, &quot;upload.html&quot;, nil)
 })
 router.POST(&quot;/upload&quot;,controller.Upload_file)
 router.NoRoute(controller.Default_route)
 router.Run(&quot;:80&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package controller

import (
 &quot;fmt&quot;
 &quot;github.com/gin-gonic/gin&quot;
 &quot;log&quot;
 &quot;net/http&quot;
)

func Default_route(c *gin.Context) {
 c.HTML(http.StatusNotFound, &quot;404.html&quot;, nil)
}
//文件上传
func Upload_file(c *gin.Context) {
 file, err := c.FormFile(&quot;f1&quot;)
 if err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{
   &quot;message&quot;: err.Error(),
  })
  return
 }
 log.Println(file.Filename)
 dst := fmt.Sprintf(&quot;./tmp/%s&quot;, file.Filename)
 c.SaveUploadedFile(file, dst)
 c.JSON(http.StatusOK, gin.H{
  &quot;message&quot;: fmt.Sprintf(&quot;&apos;%s&apos; uploaded&quot;, file.Filename),
 })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;上传文件示例&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        font-family: Arial, sans-serif;
        background-color: #f2f2f2;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
      }

      form {
        background-color: #fff;
        padding: 30px;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        text-align: center;
      }

      input[type=&quot;file&quot;] {
        margin-bottom: 20px;
      }

      input[type=&quot;submit&quot;] {
        background-color: #4caf50;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }

      input[type=&quot;submit&quot;]:hover {
        background-color: #45a049;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;form id=&quot;uploadForm&quot;&amp;gt;
      &amp;lt;input type=&quot;file&quot; id=&quot;fileInput&quot; name=&quot;f1&quot; /&amp;gt;
      &amp;lt;input type=&quot;button&quot; value=&quot;上传&quot; onclick=&quot;uploadFile()&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;script&amp;gt;
      function uploadFile() {
        let fileInput = document.getElementById(&quot;fileInput&quot;);
        let file = fileInput.files[0];

        if (file) {
          let formData = new FormData();
          formData.append(&quot;f1&quot;, file);

          fetch(&quot;/upload&quot;, {
            method: &quot;POST&quot;,
            body: formData,
          })
            .then((response) =&amp;gt; response.json())
            .then((result) =&amp;gt; {
              console.log(result);
            })
            .catch((error) =&amp;gt; {
              console.error(&quot;Error:&quot;, error);
            });
        } else {
          console.error(&quot;No file selected.&quot;);
        }
      }
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/zhxp6e-3.webp&quot; alt=&quot;image-20240308214643811&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10f5j2o-3.webp&quot; alt=&quot;image-20240308220222511&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;上传多个文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
 &quot;awesomeProject/pkg/controller&quot;
 &quot;github.com/gin-gonic/gin&quot;
)

func main() {

 router := gin.Default()
 router.LoadHTMLGlob(&quot;templates/*&quot;)
 router.GET(&quot;/&quot;, func(c *gin.Context) {
  c.HTML(200, &quot;upload.html&quot;, nil)
 })
 router.POST(&quot;/upload&quot;, controller.UploadFiles)
 router.NoRoute(controller.Default_route)
 router.Run(&quot;:80&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package controller

import (
 &quot;fmt&quot;
 &quot;github.com/gin-gonic/gin&quot;
 &quot;log&quot;
 &quot;net/http&quot;
)

func Default_route(c *gin.Context) {
 c.HTML(http.StatusNotFound, &quot;404.html&quot;, nil)
}
func UploadFiles(c *gin.Context) {
 err := c.Request.ParseMultipartForm(100 &amp;lt;&amp;lt; 20) // 100 MB limit
 if err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{
   &quot;message&quot;: err.Error(),
  })
  return
 }

 form := c.Request.MultipartForm
 if form == nil || form.File == nil {
  c.JSON(http.StatusBadRequest, gin.H{
   &quot;message&quot;: &quot;No files provided in the request&quot;,
  })
  return
 }

 files := form.File[&quot;f1&quot;]

 for _, file := range files {
  dst := fmt.Sprintf(&quot;./tmp/%s&quot;, file.Filename)
  if err := c.SaveUploadedFile(file, dst); err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
    &quot;message&quot;: fmt.Sprintf(&quot;Failed to save file %s: %s&quot;, file.Filename, err.Error()),
   })
   return
  }
  log.Println(file.Filename)
 }

 c.JSON(http.StatusOK, gin.H{
  &quot;message&quot;: &quot;Files uploaded successfully&quot;,
 })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;上传文件示例&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        font-family: Arial, sans-serif;
        background-color: #f2f2f2;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
      }

      form {
        background-color: #fff;
        padding: 30px;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        text-align: center;
      }

      input[type=&quot;file&quot;] {
        margin-bottom: 20px;
      }

      input[type=&quot;submit&quot;] {
        background-color: #4caf50;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }

      input[type=&quot;submit&quot;]:hover {
        background-color: #45a049;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;form id=&quot;uploadForm&quot;&amp;gt;
      &amp;lt;input type=&quot;file&quot; id=&quot;fileInput&quot; name=&quot;f1&quot; multiple /&amp;gt;
      &amp;lt;input type=&quot;button&quot; value=&quot;上传&quot; onclick=&quot;uploadFile()&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;script&amp;gt;
      function uploadFile() {
        let fileInput = document.getElementById(&quot;fileInput&quot;);
        let formData = new FormData();

        for (const file of fileInput.files) {
          formData.append(&quot;f1&quot;, file);
        }

        fetch(&quot;/upload&quot;, {
          method: &quot;POST&quot;,
          body: formData,
        })
          .then((response) =&amp;gt; response.json())
          .then((result) =&amp;gt; {
            console.log(result);
          })
          .catch((error) =&amp;gt; {
            console.error(&quot;Error:&quot;, error);
          });
      }
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10em1sq-3.webp&quot; alt=&quot;image-20240308220131880&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10enova-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10er6r4-3.webp&quot; alt=&quot;image-20240308220155287&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;判断上传文件的类型&lt;/h3&gt;
&lt;p&gt;在 Gin 框架中,可以使用&lt;code&gt;binding&lt;/code&gt;模块提供的&lt;code&gt;FormFile&lt;/code&gt;函数来获取上传的文件,然后检查文件的 MIME 类型。具体步骤如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在处理函数中使用&lt;code&gt;c.FormFile&lt;/code&gt;获取上传的文件:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;file, err := c.FormFile(&quot;file&quot;)
if err != nil {
    c.String(http.StatusBadRequest, &quot;获取文件失败&quot;)
    return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;打开文件并读取文件头部的几个字节,以识别文件的 MIME 类型:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;f, err := file.Open()
if err != nil {
    c.String(http.StatusInternalServerError, &quot;打开文件失败&quot;)
    return
}
defer f.Close()

buffer := make([]byte, 512)
_, err = f.Read(buffer)
if err != nil {
    c.String(http.StatusInternalServerError, &quot;读取文件失败&quot;)
    return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;http.DetectContentType&lt;/code&gt;函数检测文件的 MIME 类型:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;contentType := http.DetectContentType(buffer)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;判断文件类型是否允许:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;allowedTypes := []string{&quot;image/jpeg&quot;, &quot;image/png&quot;, &quot;application/pdf&quot;}
allowed := false
for _, t := range allowedTypes {
    if t == contentType {
        allowed = true
        break
    }
}

if !allowed {
    c.String(http.StatusBadRequest, &quot;不支持的文件类型&quot;)
    return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的示例代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func uploadFile(c *gin.Context) {
    file, err := c.FormFile(&quot;file&quot;)
    if err != nil {
        c.String(http.StatusBadRequest, &quot;获取文件失败&quot;)
        return
    }

    f, err := file.Open()
    if err != nil {
        c.String(http.StatusInternalServerError, &quot;打开文件失败&quot;)
        return
    }
    defer f.Close()

    buffer := make([]byte, 512)
    _, err = f.Read(buffer)
    if err != nil {
        c.String(http.StatusInternalServerError, &quot;读取文件失败&quot;)
        return
    }

    contentType := http.DetectContentType(buffer)
    allowedTypes := []string{&quot;image/jpeg&quot;, &quot;image/png&quot;, &quot;application/pdf&quot;}
    allowed := false
    for _, t := range allowedTypes {
        if t == contentType {
            allowed = true
            break
        }
    }

    if !allowed {
        c.String(http.StatusBadRequest, &quot;不支持的文件类型&quot;)
        return
    }

    // 处理文件...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的示例中，我们定义了一个允许的 MIME 类型列表&lt;code&gt;allowedTypes&lt;/code&gt;，包括&lt;code&gt;image/jpeg&lt;/code&gt;、&lt;code&gt;image/png&lt;/code&gt;和&lt;code&gt;application/pdf&lt;/code&gt;。如果上传的文件类型不在允许列表中，就会返回错误响应。你可以根据需求修改允许的文件类型列表。&lt;/p&gt;
&lt;h3&gt;使用 gin 编写文件服务器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package controller

import (
 &quot;fmt&quot;
 &quot;github.com/gin-gonic/gin&quot;
 &quot;log&quot;
 &quot;net/http&quot;
 &quot;os&quot;
)

func Default_route(c *gin.Context) {
 c.HTML(http.StatusNotFound, &quot;404.html&quot;, nil)
}
func UploadFiles(c *gin.Context) {
 err := c.Request.ParseMultipartForm(100 &amp;lt;&amp;lt; 20) // 100 MB limit
 if err != nil {
  c.JSON(http.StatusInternalServerError, gin.H{
   &quot;message&quot;: err.Error(),
  })
  return
 }

 form := c.Request.MultipartForm
 if form == nil || form.File == nil {
  c.JSON(http.StatusBadRequest, gin.H{
   &quot;message&quot;: &quot;No files provided in the request&quot;,
  })
  return
 }

 files := form.File[&quot;f1&quot;]

 for _, file := range files {
  dst := fmt.Sprintf(&quot;./tmp/%s&quot;, file.Filename)
  if err := c.SaveUploadedFile(file, dst); err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
    &quot;message&quot;: fmt.Sprintf(&quot;Failed to save file %s: %s&quot;, file.Filename, err.Error()),
   })
   return
  }
  log.Println(file.Filename)
 }

 c.JSON(http.StatusOK, gin.H{
  &quot;message&quot;: &quot;Files uploaded successfully&quot;,
 })
}
func ListFiles(c *gin.Context) {
 // 读取 ./tmp 目录下的所有文件
 files, err := os.ReadDir(&quot;./tmp&quot;)
 if err != nil {
  c.String(http.StatusInternalServerError, err.Error())
  return
 }

 // 渲染模板
 c.HTML(http.StatusOK, &quot;download.html&quot;, gin.H{
  &quot;Files&quot;: files,
 })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
 &quot;awesomeProject/pkg/controller&quot;
 &quot;github.com/gin-gonic/gin&quot;
)

func main() {
 r := gin.Default()

 // 设置静态文件路径为 ./tmp
 r.Static(&quot;/tmp&quot;, &quot;./tmp&quot;)

 // 设置模板目录
 r.LoadHTMLGlob(&quot;templates/*&quot;)

 // 定义路由
 r.GET(&quot;/&quot;, func(c *gin.Context) {
  c.HTML(200, &quot;upload.html&quot;, nil)
 })
 r.POST(&quot;/upload&quot;, controller.UploadFiles)

 //文件列表服务器
 r.GET(&quot;/files&quot;, controller.ListFiles)

 // 启动HTTP服务器
 r.Run(&quot;:8080&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--download.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;File List&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;File List&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      {{ range .Files }}
      &amp;lt;li&amp;gt;&amp;lt;a href=&quot;/tmp/{{ .Name }}&quot;&amp;gt;{{ .Name }}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      {{ end }}
    &amp;lt;/ul&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--美化版--&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;File List&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        font-family: Arial, sans-serif;
        background-color: #f5f5f5;
        padding: 20px;
      }

      h1 {
        color: #333;
        text-align: center;
      }

      ul {
        list-style-type: none;
        margin: 0;
        padding: 0;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
      }

      li {
        background-color: #fff;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        margin: 10px;
        padding: 10px;
        border-radius: 5px;
        text-align: center;
      }

      a {
        text-decoration: none;
        color: #333;
      }

      a:hover {
        color: #666;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;File List&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      {{ range .Files }}
      &amp;lt;li&amp;gt;&amp;lt;a href=&quot;/tmp/{{ .Name }}&quot;&amp;gt;{{ .Name }}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      {{ end }}
    &amp;lt;/ul&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10pw52c-3.webp&quot; alt=&quot;image-20240308222026615&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10r2oqf-3.webp&quot; alt=&quot;image-20240308222225060&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/10svloq-3.webp&quot; alt=&quot;image-20240308222527025&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;请求头相关&lt;/h1&gt;
&lt;h2&gt;获取所有请求头&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/iu6r5m-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;    &quot;net/http&quot;)

func main() {
    router := gin.Default()
    router.LoadHTMLGlob(&quot;template/*&quot;)
    router.Static(&quot;static&quot;, &quot;./static&quot;)
    router.GET(&quot;/&quot;, func(c *gin.Context) {
       c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{
          &quot;header&quot;: c.Request.Header,
       })
       fmt.Println(c.Request.Header)
    })
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Post Form Test&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Header Test&amp;lt;/h1&amp;gt;
    &amp;lt;h3&amp;gt;Header: {{.header}}&amp;lt;/h3&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绑定参数 bind&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;绑定 post 发送的 json 数据转换为 Student 结构体的成员变量值，然后再把这个结构体转换为 json 对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;    &quot;net/http&quot;)

type Student struct {
    Name string `json:&quot;name&quot;`
    Age  int    `json:&quot;age&quot;`
}

func main() {
    router := gin.Default()
    router.POST(&quot;/&quot;, func(c *gin.Context) {
       var stu Student
       err := c.BindJSON(&amp;amp;stu)
       if err != nil {
          fmt.Println(&quot;error: &quot;, err)
          c.JSON(http.StatusBadGateway, err)
          return
       }
       c.JSON(http.StatusOK, stu)
    })
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/shcott-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;绑定查询参数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;    &quot;net/http&quot;)

type Student struct {
    Name string `json:&quot;name&quot; form:&quot;name&quot;`
    Age  int    `json:&quot;age&quot; form:&quot;age&quot;`
}

func main() {
    router := gin.Default()
    router.GET(&quot;/&quot;, func(c *gin.Context) {
       var stu Student
       err := c.BindQuery(&amp;amp;stu)
       if err != nil {
          fmt.Println(&quot;error: &quot;, err)
          c.JSON(http.StatusBadGateway, err)
          return
       }
       c.JSON(http.StatusOK, stu)
    })
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/sjnhl2-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bind URI&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/gin-gonic/gin&quot;    &quot;net/http&quot;)

type Student struct {
    Name string `json:&quot;name&quot; form:&quot;name&quot; uri:&quot;name&quot;`
    Age  int    `json:&quot;age&quot; form:&quot;age&quot; uri:&quot;age&quot;`
}

func main() {
    router := gin.Default()
    router.GET(&quot;/uri/:name/:age&quot;, func(c *gin.Context) {
       var stu Student
       err := c.ShouldBindUri(&amp;amp;stu)
       if err != nil {
          fmt.Println(&quot;error: &quot;, err)
          c.JSON(http.StatusBadGateway, err)
          return
       }
       c.JSON(http.StatusOK, stu)
    })
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/sm69pi-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;常用验证器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 不能为空，并且不能没有这个字段
required： 必填字段，如：binding:&quot;required&quot;

// 针对字符串的长度
min 最小长度，如：binding:&quot;min=5&quot;
max 最大长度，如：binding:&quot;max=10&quot;
len 长度，如：binding:&quot;len=6&quot;

// 针对数字的大小
eq 等于，如：binding:&quot;eq=3&quot;
ne 不等于，如：binding:&quot;ne=12&quot;
gt 大于，如：binding:&quot;gt=10&quot;
gte 大于等于，如：binding:&quot;gte=10&quot;
lt 小于，如：binding:&quot;lt=10&quot;
lte 小于等于，如：binding:&quot;lte=10&quot;

// 针对同级字段的
eqfield 等于其他字段的值，如：PassWord string `binding:&quot;eqfield=Password&quot;`
nefield 不等于其他字段的值


- 忽略字段，如：binding:&quot;-&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;github.com/gin-gonic/gin&quot;
    &quot;net/http&quot;)

type User struct {
    Name        string `json:&quot;name&quot; binding:&quot;required&quot;`
    Password    string `json:&quot;password&quot; binding:&quot;eqfield=Re_Password&quot;`
    Re_Password string `json:&quot;re_password&quot;`
}
type Response struct {
    Code int    `json:&quot;code&quot;`
    Data any    `json:&quot;data&quot;`
    Msg  string `json:&quot;msg&quot;`
}

func main() {
    router := gin.Default()
    router.POST(&quot;/login&quot;, func(c *gin.Context) {
       var user User
       err := c.ShouldBindJSON(&amp;amp;user)
       if err != nil {
          c.JSON(http.StatusBadGateway, Response{
             Code: http.StatusBadGateway,
             Data: err.Error(),
             Msg:  &quot;bad response&quot;,
          })
          return
       }
       c.JSON(http.StatusOK, Response{
          Code: http.StatusOK,
          Data: user,
          Msg:  &quot;post successfully&quot;,
       })
    })
    router.Run(&quot;:8080&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;密码相同
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/t2zolw-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;密码不同
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2024/03/08/t3ebk2-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;我们看到报错对用户不是很友好，我们可以自定义验证的错误信息&lt;/p&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;https://docs.fengfengzhidao.com/#/docs/Gin%E6%A1%86%E6%9E%B6%E6%96%87%E6%A1%A3/4.bind%E7%BB%91%E5%AE%9A%E5%99%A8?id=gin%e5%86%85%e7%bd%ae%e9%aa%8c%e8%af%81%e5%99%a8&quot;&gt;gin 内置验证器&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 枚举  只能是red 或green
oneof=red green

// 字符串
contains=fengfeng  // 包含fengfeng的字符串
excludes // 不包含
startswith  // 字符串前缀
endswith  // 字符串后缀

// 数组
dive  // dive后面的验证就是针对数组中的每一个元素

// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符，可以唯一标识一个资源。
// url 在于Locater，是统一资源定位符，提供找到该资源的确切路径

// 日期验证  1月2号下午3点4分5秒在2006年
datetime=2006-01-02
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Gin 中间件&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.liwenzhou.com/posts/Go/gin/#c-0-8-3&quot;&gt;https://www.liwenzhou.com/posts/Go/gin/#c-0-8-3&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gin 中的中间件必须是一个&lt;code&gt;gin.HandlerFunc&lt;/code&gt;类型。&lt;/p&gt;
&lt;p&gt;Gin 框架允许开发者在处理请求的过程中，加入用户自己的钩子（Hook）函数。这个钩子函数就叫中间件，中间件适合处理一些公共的业务逻辑，比如登录认证、权限校验、数据分页、记录日志、耗时统计等。&lt;/p&gt;
</content:encoded></item><item><title>TrieMap实现</title><link>https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/triemap%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/triemap%E5%AE%9E%E7%8E%B0/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://labuladong.online/algo/data-structure/trie-implement/#trieset-%E7%9A%84%E5%AE%9E%E7%8E%B0&quot;&gt;https://labuladong.online/algo/data-structure/trie-implement/#trieset-的实现&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://labuladong.online/algo/data-structure-basic/trie-map-basic/&quot;&gt;https://labuladong.online/algo/data-structure-basic/trie-map-basic/&lt;/a&gt;
&lt;strong&gt;TrieMap 是什么？&lt;/strong&gt;
Tire树又称字典树/前缀树，具有如下特点&lt;/p&gt;
&lt;p&gt;根节点不包含字符 除根节点外每个节点只包含一个字符
树的每一个路径都是一个字符串
每个节点的子节点包含的字符都不相同&lt;/p&gt;
&lt;p&gt;简单来说，&lt;strong&gt;TrieMap 就是一个将 Trie（前缀树）的数据结构与 Map（映射）的功能结合起来的集合。&lt;/strong&gt; 它不仅仅是一个键值对的存储器，更是一个能够高效地处理与字符串（或任何序列）键相关的各种操作的强大工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心思想：Trie + Map&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trie（前缀树）作为底层结构：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trie 的核心思想是利用键的公共前缀来共享节点，从而节省空间和提高查找效率。&lt;/li&gt;
&lt;li&gt;每个节点通常代表键的一个字符或序列中的一个元素。&lt;/li&gt;
&lt;li&gt;从根节点到任意一个节点的路径，代表了一个前缀。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Map 的功能扩展：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Trie 的基础上，将值 &quot;挂载&quot; 到 Trie 的特定节点上。&lt;/li&gt;
&lt;li&gt;通常，当一个键的完整路径在一个 Trie 节点处结束时，这个节点会包含与该键关联的值。&lt;/li&gt;
&lt;li&gt;一个节点可以有多个子节点（对应不同的下一个字符），也可以存储一个值（表示该前缀本身就是一个完整的键）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TrieMap 的结构和工作原理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;节点（Node）：&lt;/strong&gt; TrieMap 的基本构建单元。每个节点至少包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;子节点指针（Children）：&lt;/strong&gt; 通常是一个 Map 或数组，用于存储指向下一个字符/元素的子节点的引用。例如，&lt;code&gt;Map&amp;lt;Character, Node&amp;gt;&lt;/code&gt; 或 &lt;code&gt;Node[26]&lt;/code&gt; (针对英文字母)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;值（Value）：&lt;/strong&gt; 一个可选的字段，如果当前节点代表一个完整的键的结束，则该字段存储与该键关联的值。如果一个节点只是一个前缀，但不是一个完整的键，则此字段可能为 &lt;code&gt;null&lt;/code&gt; 或表示没有值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;isEndOfWord/isKey (布尔值):&lt;/strong&gt; 一个标记，指示当前节点是否代表一个完整的键的结束。这在区分一个前缀 vs. 一个完整的键时非常有用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;插入（&lt;code&gt;put(key, value)&lt;/code&gt;）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从根节点开始。&lt;/li&gt;
&lt;li&gt;遍历键的每个字符。对于每个字符：
&lt;ul&gt;
&lt;li&gt;如果当前节点的子节点中已经存在指向该字符的节点，则移动到该子节点。&lt;/li&gt;
&lt;li&gt;如果不存在，则创建一个新的子节点，并将其添加到当前节点的子节点集合中，然后移动到新创建的节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当遍历完所有字符后，到达最终节点。将该节点的 &lt;code&gt;value&lt;/code&gt; 字段设置为传入的 &lt;code&gt;value&lt;/code&gt;，并设置 &lt;code&gt;isEndOfWord&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查找（&lt;code&gt;get(key)&lt;/code&gt;）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从根节点开始。&lt;/li&gt;
&lt;li&gt;遍历键的每个字符。对于每个字符：
&lt;ul&gt;
&lt;li&gt;如果当前节点的子节点中不存在指向该字符的节点，则说明键不存在，返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果存在，则移动到该子节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当遍历完所有字符后，到达最终节点。检查该节点的 &lt;code&gt;isEndOfWord&lt;/code&gt; 标记。如果为 &lt;code&gt;true&lt;/code&gt;，则返回该节点的 &lt;code&gt;value&lt;/code&gt;；否则（如果只是一个前缀），返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除（&lt;code&gt;remove(key)&lt;/code&gt;）：&lt;/strong&gt;
删除操作相对复杂，需要考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到键对应的最终节点。&lt;/li&gt;
&lt;li&gt;将该节点的 &lt;code&gt;value&lt;/code&gt; 设置为 &lt;code&gt;null&lt;/code&gt;，并 &lt;code&gt;isEndOfWord&lt;/code&gt; 设置为 &lt;code&gt;false&lt;/code&gt;（逻辑删除）。&lt;/li&gt;
&lt;li&gt;如果删除后，该节点不再是任何其他键的前缀，并且也没有任何子节点，那么它可以从树中物理删除，需要回溯父节点并移除指向它的引用，一直回溯到第一个有其他作用（是其他键的前缀或有其他子节点）的节点。这通常需要递归实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TrieMap 的特性和优势&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高效的前缀匹配和查找：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找时间复杂度：O(L)&lt;/strong&gt;，其中 L 是键的长度。这比基于哈希表的 Map 在最坏情况下（哈希冲突严重）的 O(L) 或 O(N) 性能更好，而且在平均情况下哈希表是 O(1)，但 TrieMap 在键长较短时表现出色，且无哈希冲突问题。&lt;/li&gt;
&lt;li&gt;特别适合**“前缀搜索”&lt;strong&gt;或&lt;/strong&gt;“自动补全”**：可以直接遍历一个前缀对应的节点及其所有子树，找到所有以该前缀开头的键。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空间效率（部分情况下）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当键之间有大量公共前缀时，可以显著节省空间，因为共享了节点。&lt;/li&gt;
&lt;li&gt;然而，如果键之间前缀很少，或者键的字符集非常大，每个节点有大量子节点指针，那么空间开销可能会比 HashMap 大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有序性（基于键的前缀）：&lt;/strong&gt;
虽然不是像 TreeMap 那样按键的整体排序，但 TrieMap 在结构上体现了键的前缀有序性，这使得前缀相关的操作非常自然和高效。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;键可以是任意序列：&lt;/strong&gt;
尽管最常见的是字符串，但只要能定义元素的顺序和比较，键可以是任何序列（例如，字节数组，整数数组）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TrieMap 的应用场景&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动补全/拼写检查：&lt;/strong&gt; 用户输入时，快速提供以当前输入为前缀的建议词汇。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由表：&lt;/strong&gt; 网络路由器可以使用 Trie 来存储 IP 地址或网络前缀，从而快速查找匹配的路由规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;词典/字典树：&lt;/strong&gt; 存储大量词汇，进行快速查找、前缀匹配等操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 地址查找：&lt;/strong&gt; 查找某个 IP 地址是否在某个大的网段中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 解析：&lt;/strong&gt; 查找域名对应的 IP 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文本搜索匹配：&lt;/strong&gt; 在文本中查找特定模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据压缩：&lt;/strong&gt; 通过共享前缀来降低存储冗余。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TrieMap 的潜在缺点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;空间开销：&lt;/strong&gt; 如果键的前缀共享不多，或者键的字符集很大（导致每个节点子节点Map/数组大而稀疏），空间效率可能不高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现复杂度：&lt;/strong&gt; 相对于 HashMap，实现 TrieMap 更复杂，尤其是删除操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非随机访问：&lt;/strong&gt; 无法像数组那样通过索引直接访问，访问任何一个键都需要从根节点遍历到对应节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;与 HashMap/TreeMap 的比较&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HashMap：&lt;/strong&gt; 最佳平均时间复杂度 O(1) 用于 &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;put&lt;/code&gt;。不保证键的顺序。不擅长前缀搜索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TreeMap：&lt;/strong&gt; 基于红黑树实现，所有操作都是 O(log N)。键是排序的。支持范围查询，但前缀搜索不如 TrieMap 直观和高效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TrieMap：&lt;/strong&gt; 最佳时间复杂度 O(L) (键长)。特别擅长前缀搜索及自动补全。在大量键有公共前缀时空间效率高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TrieMap 是一种非常有用且强大的数据结构，它利用前缀树的特性，在处理字符串（或其他序列）键的映射和前缀相关操作时展现出卓越的性能。理解其节点结构和操作原理是掌握它的关键。在需要高效前缀搜索和存储大量相关键的场景下，TrieMap 是一个值得考虑的优秀选择。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package trie_map

// TrieNode 表示字典树中的一个节点，包含可选值和子节点指针数组
type TrieNode[T any] struct {
 val      *T             // 节点存储的值，如果为 nil 表示不是一个完整 key
 children []*TrieNode[T] // 子节点数组，长度为字符集大小 R
}

// NewTrieNode 创建一个新的 TrieNode，初始化子节点数组长度为 R
func NewTrieNode[T any](R int) *TrieNode[T] {
 return &amp;amp;TrieNode[T]{
  children: make([]*TrieNode[T], R), // 初始化长度为 R 的子节点数组
 }
}

// TrieMap 是一个基于 Trie 的映射结构，支持字符串键和值的泛型映射
type TrieMap[T any] struct {
 R    int          // 字符集大小，例如 256 表示 ASCII 字符集
 size int          // 当前存储的键值对数量
 root *TrieNode[T] // Trie 树的根节点
}

// NewTrieMap 创建一个空的 TrieMap，使用默认的 ASCII 字符集大小 256
func NewTrieMap[T any]() *TrieMap[T] {
 trieMap := &amp;amp;TrieMap[T]{
  size: 0,
  R:    256, // 默认支持 ASCII 范围内的字符
 }
 trieMap.root = NewTrieNode[T](trieMap.R) // 初始化根节点
 return trieMap
}

// Size 返回 TrieMap 中键值对的数量
func (tm *TrieMap[T]) Size() int {
 return tm.size
}

// GetNode 查找 key 对应的终止节点（若存在）
func GetNode[T any](node *TrieNode[T], key string) *TrieNode[T] {
 if node == nil {
  return nil
 }
 p := node
 for i := 0; i &amp;lt; len(key); i++ {
  if p == nil {
   return nil
  }
  var c byte = key[i]
  p = p.children[c] // 向下查找子节点
 }
 return p
}

// Get 返回 key 对应的值指针，若不存在则返回 nil
func (tm *TrieMap[T]) Get(key string) *T {
 node := GetNode(tm.root, key)
 if node == nil || node.val == nil {
  return nil
 }
 return node.val
}

// ContainsKey 判断是否存在指定 key
func (tm *TrieMap[T]) ContainsKey(key string) bool {
 return tm.Get(key) != nil
}

// HasKeyWithPrefix 判断是否存在某个以 prefix 为前缀的 key
func (tm *TrieMap[T]) HasKeyWithPrefix(prefix string) bool {
 return GetNode(tm.root, prefix) != nil
}

// ShortestPrefixOf 查找 query 的最短前缀，该前缀在 TrieMap 中存在
func (tm *TrieMap[T]) ShortestPrefixOf(query string) string {
 p := tm.root
 for i := 0; i &amp;lt; len(query); i++ {
  if p == nil {
   break
  }
  if p.val != nil {
   return query[:i] // 找到前缀匹配
  }
  var c byte = query[i]
  p = p.children[c]
 }
 if p != nil &amp;amp;&amp;amp; p.val != nil {
  return query // 整个 query 是前缀
 }
 return &quot;&quot; // 没有任何前缀匹配
}

// LongestPrefixOf 查找 query 的最长前缀，该前缀在 TrieMap 中存在
func (tm *TrieMap[T]) LongestPrefixOf(query string) string {
 node := tm.root
 max_len := 0
 for i := 0; i &amp;lt; len(query); i++ {
  if node == nil {
   break
  }
  if node.val != nil {
   max_len = i
  }
  var c byte = query[i]
  node = node.children[c]
 }
 if node != nil &amp;amp;&amp;amp; node.val != nil {
  return query // 整个 query 是匹配项
 }
 return query[:max_len] // 返回最长匹配前缀
}

// KeysWithPrefix 返回所有以 prefix 开头的键
func (tm *TrieMap[T]) KeysWithPrefix(prefix string) []string {
 var keys []string = make([]string, 0)
 node := GetNode[T](tm.root, prefix)
 if node == nil {
  return keys // 没有该前缀
 }
 tm.traverseForKeysWithPrefix(node, prefix, &amp;amp;keys)
 return keys
}

// traverseForKeysWithPrefix 递归收集所有以当前路径为前缀的 key
func (tm *TrieMap[T]) traverseForKeysWithPrefix(node *TrieNode[T], currentPath string, res *[]string) {
 if node == nil {
  return
 }
 if node.val != nil {
  *res = append(*res, currentPath) // 找到一个完整 key
 }
 for i := 0; i &amp;lt; tm.R; i++ {
  currentPath = currentPath + string(byte(i))
  tm.traverseForKeysWithPrefix(node.children[i], currentPath, res)
  currentPath = currentPath[:len(currentPath)-1] // 回溯
 }
}

// KeysWithPattern 查找所有匹配模式的 key，支持通配符 &apos;.&apos;
func (tm *TrieMap[T]) KeysWithPattern(pattern string) []string {
 var keys []string = make([]string, 0)
 tm.traverseForKeysWithPattern(tm.root, &quot;&quot;, pattern, 0, &amp;amp;keys)
 return keys
}

// traverseForKeysWithPattern 回溯遍历支持通配符的模式匹配
func (tm *TrieMap[T]) traverseForKeysWithPattern(node *TrieNode[T], path string, pattern string, i int, keys *[]string) {
 if node == nil {
  return
 }
 if i == len(pattern) {
  if node.val != nil {
   *keys = append(*keys, path)
  }
  return
 }
 c := pattern[i]
 if c == &apos;.&apos; {
  for j := 0; j &amp;lt; tm.R; j++ {
   path = path + string(byte(j))
   tm.traverseForKeysWithPattern(node.children[j], path, pattern, i+1, keys)
   path = path[:len(path)-1]
  }
 } else {
  path = path + string(byte(c))
  tm.traverseForKeysWithPattern(node.children[c], path, pattern, i+1, keys)
  path = path[:len(path)-1]
 }
}

// HasKeyWithPattern 判断是否存在匹配指定模式的 key
func (tm *TrieMap[T]) HasKeyWithPattern(pattern string) bool {
 return len(tm.KeysWithPattern(pattern)) &amp;gt; 0
}

// Put 插入或更新 key 对应的值
func (tm *TrieMap[T]) Put(key string, v T) {
 if !tm.ContainsKey(key) {
  tm.size++ // 是新增 key
 }
 tm.root = tm.putNode(tm.root, key, &amp;amp;v, 0)
}

// putNode 递归构建节点路径，直到 key 的末尾
func (tm *TrieMap[T]) putNode(node *TrieNode[T], key string, val *T, i int) *TrieNode[T] {
 if node == nil {
  node = NewTrieNode[T](tm.R)
 }
 if i == len(key) {
  node.val = val // 在最后一个节点上存储值
  return node
 }
 c := key[i]
 node.children[c] = tm.putNode(node.children[c], key, val, i+1)
 return node
}

// Remove 从 TrieMap 中删除 key
func (tm *TrieMap[T]) Remove(key string) {
 if !tm.ContainsKey(key) {
  return // key 不存在
 }
 tm.root = tm.removeNode(tm.root, key, 0)
 tm.size--
}

// removeNode 删除 key 路径上的值，必要时清除无用节点
func (tm *TrieMap[T]) removeNode(node *TrieNode[T], key string, i int) *TrieNode[T] {
 if node == nil {
  return nil
 }
 if i == len(key) {
  node.val = nil // 删除节点值
 } else {
  c := key[i]
  node.children[c] = tm.removeNode(node.children[c], key, i+1)
 }
 if node.val != nil {
  return node
 }
 for i := 0; i &amp;lt; tm.R; i++ {
  if node.children[i] != nil {
   return node // 有孩子不能删除
  }
 }
 return nil // 无值无子，删除此节点
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;package trie_map

import (
 &quot;reflect&quot;
 &quot;testing&quot;
)

func TestTrieMap_BasicOperations(t *testing.T) {
 trie := NewTrieMap[int]()

 // Test Put and Get
 trie.Put(&quot;apple&quot;, 100)
 trie.Put(&quot;app&quot;, 200)
 trie.Put(&quot;banana&quot;, 300)

 if v := trie.Get(&quot;apple&quot;); v == nil || *v != 100 {
  t.Errorf(&quot;expected 100, got %v&quot;, v)
 }

 if v := trie.Get(&quot;app&quot;); v == nil || *v != 200 {
  t.Errorf(&quot;expected 200, got %v&quot;, v)
 }

 if v := trie.Get(&quot;banana&quot;); v == nil || *v != 300 {
  t.Errorf(&quot;expected 300, got %v&quot;, v)
 }

 if v := trie.Get(&quot;unknown&quot;); v != nil {
  t.Errorf(&quot;expected nil for unknown key, got %v&quot;, *v)
 }

 // Test ContainsKey
 if !trie.ContainsKey(&quot;apple&quot;) {
  t.Error(&quot;expected ContainsKey(\&quot;apple\&quot;) to be true&quot;)
 }

 if trie.ContainsKey(&quot;unknown&quot;) {
  t.Error(&quot;expected ContainsKey(\&quot;unknown\&quot;) to be false&quot;)
 }

 // Test Size
 if size := trie.Size(); size != 3 {
  t.Errorf(&quot;expected size 3, got %d&quot;, size)
 }
}

func TestTrieMap_PrefixAndPattern(t *testing.T) {
 trie := NewTrieMap[int]()
 trie.Put(&quot;apple&quot;, 1)
 trie.Put(&quot;app&quot;, 2)
 trie.Put(&quot;apricot&quot;, 3)
 trie.Put(&quot;bat&quot;, 4)
 trie.Put(&quot;ball&quot;, 5)

 // Test KeysWithPrefix
 prefixKeys := trie.KeysWithPrefix(&quot;ap&quot;)
 expected := []string{&quot;app&quot;, &quot;apple&quot;, &quot;apricot&quot;}
 if !reflect.DeepEqual(stringSet(prefixKeys), stringSet(expected)) {
  t.Errorf(&quot;KeysWithPrefix failed, got %v, expected %v&quot;, prefixKeys, expected)
 }

 // Test ShortestPrefixOf
 query := &quot;applepie&quot;
 shortest := trie.ShortestPrefixOf(query)
 if shortest != &quot;app&quot; {
  t.Errorf(&quot;expected shortest prefix to be &apos;app&apos;, got %s&quot;, shortest)
 }

 // Test LongestPrefixOf
 longest := trie.LongestPrefixOf(query)
 if longest != &quot;apple&quot; {
  t.Errorf(&quot;expected longest prefix to be &apos;apple&apos;, got %s&quot;, longest)
 }

 // Test KeysWithPattern
 trie.Put(&quot;bake&quot;, 6)
 patternKeys := trie.KeysWithPattern(&quot;ba..&quot;)
 expectedPattern := []string{&quot;ball&quot;, &quot;bake&quot;}
 if !reflect.DeepEqual(stringSet(patternKeys), stringSet(expectedPattern)) {
  t.Errorf(&quot;KeysWithPattern failed, got %v, expected %v&quot;, patternKeys, expectedPattern)
 }

 // Test HasKeyWithPattern
 if !trie.HasKeyWithPattern(&quot;b.ll&quot;) {
  t.Error(&quot;expected HasKeyWithPattern(\&quot;b.ll\&quot;) to be true&quot;)
 }
}

func TestTrieMap_Remove(t *testing.T) {
 trie := NewTrieMap[int]()
 trie.Put(&quot;dog&quot;, 10)
 trie.Put(&quot;dot&quot;, 20)

 trie.Remove(&quot;dog&quot;)
 if trie.ContainsKey(&quot;dog&quot;) {
  t.Error(&quot;expected &apos;dog&apos; to be removed&quot;)
 }

 if trie.Size() != 1 {
  t.Errorf(&quot;expected size to be 1 after removal, got %d&quot;, trie.Size())
 }

 // remove nonexistent
 trie.Remove(&quot;notfound&quot;)
 if trie.Size() != 1 {
  t.Error(&quot;removing nonexistent key should not change size&quot;)
 }
}

// Helper: make order-insensitive string slice comparison
func stringSet(list []string) map[string]struct{} {
 set := make(map[string]struct{})
 for _, s := range list {
  set[s] = struct{}{}
 }
 return set
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>IO多路复用技术</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E6%8A%80%E6%9C%AF/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E6%8A%80%E6%9C%AF/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.tencent.com/developer/article/2383534&quot;&gt;万字图解| 深入揭秘IO多路复用&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;为什么要有IO多路复用技术？&lt;/h1&gt;
&lt;p&gt;在没有 I/O 多路复用（如 select/poll/epoll）时，同步 I/O 确实主要分为以下两种模式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/squh53-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/sqxtqj-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/sqzp5w-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当处理 大量并发连接 时，上述两种同步模型存在致命缺陷：&lt;/p&gt;
&lt;p&gt;阻塞 I/O：需要 1 线程/连接 → 线程切换开销大（C10K 问题）
非阻塞 I/O：CPU 空转轮询 → 资源浪费严重&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/srg062-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/srhn4e-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;IO多路复用技术&lt;/h1&gt;
&lt;p&gt;IO多路复用是一种允许单个进程同时监视多个文件描述符的技术，使得程序能高效处理多个并发连接而无需创建大量线程。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;select/poll/epoll&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;select 的缺点是单个进程能监视的文件描述符数量有限，一般为 1024 个，且每次调用都需要将文件描述符集合从用户态复制到内核态，然后遍历找出就绪的描述符，性能较差。&lt;/p&gt;
&lt;p&gt;poll 的优点是没有最大文件描述符数量的限制，但是每次调用仍然需要将文件描述符集合从用户态复制到内核态，依然需要遍历，性能仍然较差。&lt;/p&gt;
&lt;p&gt;epoll 是 Linux 特有的 IO 多路复用机制，支持大规模并发连接，使用事件驱动模型，性能更高。其工作原理是将文件描述符注册到内核中，然后通过事件通知机制来处理就绪的文件描述符，不需要轮询，也不需要数据拷贝，更没有数量限制，所以性能非常高。&lt;/p&gt;
&lt;p&gt;epoll使用了事件驱动模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/u32dv8-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>ABA问题</title><link>https://blog.meowrain.cn/posts/java/juc/aba%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/aba%E9%97%AE%E9%A2%98/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;ABA问题是并发编程中，在使用无锁（lock-free）算法，特别是基于 比较并交换（Compare-And-Swap, CAS） 操作时可能出现的一种逻辑错误。&lt;/p&gt;
&lt;p&gt;它之所以被称为&quot;ABA&quot;问题，是因为一个变量的值从 A 变成了 B，然后又变回了 A。对于一个只检查当前值是否等于期望值的CAS操作来说，它会认为值没有发生变化，从而成功执行操作，但实际上变量在期间已经被修改过了。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;ABA问题发生的场景及危害&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;想象一个无锁的栈（Stack），其 &lt;code&gt;pop()&lt;/code&gt; 操作需要原子地更新栈顶元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;假设初始状态：&lt;/strong&gt;
栈顶 &lt;code&gt;top&lt;/code&gt; 指向元素 &lt;code&gt;A&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正常 &lt;code&gt;pop&lt;/code&gt; 操作流程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程1读取当前栈顶元素 &lt;code&gt;A&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;线程1准备将栈顶更新为 &lt;code&gt;A.next&lt;/code&gt; (假设是 &lt;code&gt;null&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;线程1执行 &lt;code&gt;top.compareAndSet(A, A.next)&lt;/code&gt;，如果成功，&lt;code&gt;A&lt;/code&gt; 被弹出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;ABA问题发生过程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程1&lt;/strong&gt; 读取当前栈顶元素，发现是 &lt;code&gt;A&lt;/code&gt;。它记下 &lt;code&gt;A&lt;/code&gt;，并准备执行 &lt;code&gt;CAS(A, C)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top -&amp;gt; A -&amp;gt; B -&amp;gt; D
Thread 1 reads top: A
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程2&lt;/strong&gt; 此时突然执行，它将 &lt;code&gt;A&lt;/code&gt; 弹出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top -&amp;gt; B -&amp;gt; D  (A is now removed)
Thread 2 pops A
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程2&lt;/strong&gt; 又将一个**新的元素 &lt;code&gt;A&lt;/code&gt; （或者一个值和 &lt;code&gt;A&lt;/code&gt; 相同但实际上是不同对象的元素）**压入栈。
&lt;em&gt;注意：这里的“新的元素A”指的是一个与最开始的A值相同，但内存地址可能不同，或者即便内存地址相同，其内部状态已经发生过变化的对象。&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top -&amp;gt; A -&amp;gt; B -&amp;gt; D  (This A is NOT the original A, it&apos;s a new one!)
Thread 2 pushes A back
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程1&lt;/strong&gt; 恢复执行 &lt;code&gt;CAS(A, C)&lt;/code&gt;。它检查当前栈顶是否是它之前读取的 &lt;code&gt;A&lt;/code&gt;。
由于栈顶现在又指向了 &lt;code&gt;A&lt;/code&gt;（尽管是新的 &lt;code&gt;A&lt;/code&gt;），&lt;code&gt;compareAndSet&lt;/code&gt; 操作会认为当前值等于期望值 &lt;code&gt;A&lt;/code&gt;，并成功将栈顶更新为 &lt;code&gt;C&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top -&amp;gt; C       (Thread 1&apos;s CAS(A, C) succeeds!)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;危害：&lt;/strong&gt;
尽管线程1的CAS操作成功了，但它操作的实际上是一个&lt;strong&gt;新的 &lt;code&gt;A&lt;/code&gt;&lt;/strong&gt;，而不是它最初读取的那个 &lt;code&gt;A&lt;/code&gt;。如果 &lt;code&gt;A&lt;/code&gt; 的内部状态（比如它的 &lt;code&gt;next&lt;/code&gt; 指针）在这期间被改变了，那么线程1的后续操作可能会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构损坏&lt;/strong&gt;：例如，在链表中，节点指针可能指向错误的位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑错误&lt;/strong&gt;：程序基于过时的或不正确的状态信息做出决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存泄漏&lt;/strong&gt;：旧的 &lt;code&gt;A&lt;/code&gt; （或其他被弹出又压入的元素）可能永远无法被垃圾回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;package org.example.aba;

import java.util.concurrent.atomic.AtomicReference;

class Node {
    public final String item; // 节点内容
    public Node next; // 下一个节点的引用

    public Node(String item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return item;
    }
}

class LockFreeStackABA {
    private AtomicReference&amp;lt;Node&amp;gt; top = new AtomicReference&amp;lt;&amp;gt;();

    // 压入栈顶
    public void push(String item) {
        Node newHead = new Node(item);
        Node oldHead;
        do {
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
        System.out.println(Thread.currentThread().getName() + &quot; 压入: &quot; + item + &quot; (当前栈顶: &quot; + top.get() + &quot;)&quot;);
    }

    // 弹出栈顶
    public Node pop() {
        Node oldHead;
        Node newHead;
        do {
            oldHead = top.get();
            if (oldHead == null) {
                System.out.println(Thread.currentThread().getName() + &quot; 尝试弹出，但栈为空！&quot;);
                return null;
            }
            newHead = oldHead.next;
            System.out.println(Thread.currentThread().getName() + &quot; 尝试弹出 &quot; + oldHead.item +
                    &quot; (期望栈顶: &quot; + oldHead + &quot;, 更新栈顶至: &quot; + newHead + &quot;)&quot;);
        } while (!top.compareAndSet(oldHead, newHead)); // CAS操作：如果当前栈顶仍是oldHead，则更新为newHead
        System.out.println(Thread.currentThread().getName() + &quot; 成功弹出: &quot; + oldHead.item + &quot; (当前栈顶: &quot; + top.get() + &quot;)&quot;);
        return oldHead;
    }

    // 打印栈内容
    public void printStack() {
        System.out.print(&quot;当前栈: &quot;);
        Node current = top.get();
        if (current == null) {
            System.out.println(&quot;空&quot;);
            return;
        }
        StringBuilder sb = new StringBuilder();
        while (current != null) {
            sb.append(current.item).append(&quot; -&amp;gt; &quot;);
            current = current.next;
        }
        sb.setLength(sb.length() - 4); // 移除最后的 &quot; -&amp;gt; &quot;
        System.out.println(sb.toString());
    }

    // 获取栈顶节点
    public Node getTop() {
        return top.get();
    }
}

public class AbaAppear {
    public static void main(String[] args) throws InterruptedException {
        LockFreeStackABA stack = new LockFreeStackABA();

        // 1. 初始状态：栈中逐步压入 A、B、C
        stack.push(&quot;C&quot;); // 栈顶：C
        stack.push(&quot;B&quot;); // 栈顶：B → C
        stack.push(&quot;A&quot;); // 栈顶：A → B → C

        Node originalNodeA = stack.getTop(); // 获取当前栈顶的 A 节点引用

        System.out.println(&quot;\n--- 初始栈内容 ---&quot;);
        stack.printStack();

        // 2. 线程1 启动，读取栈顶元素后等待
        Thread thread1 = new Thread(() -&amp;gt; {
            Node readNode = stack.getTop(); // 线程1在原栈中看到栈顶元素 A
            System.out.println(&quot;\n线程-1 读取到栈顶节点: &quot; + readNode);
            try {
                Thread.sleep(200); // 等待线程2的干扰行为发生
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(&quot;\n线程-1 开始尝试弹出栈顶节点...&quot;);
            Node popNode = stack.pop(); // 线程1尝试弹出栈顶（被线程2修改为新 A）
            if (readNode == popNode) {
                System.out.println(&quot;同一个节点&quot;);
            }else {
                System.out.println(&quot;不是同一个节点，ABA问题已重现！&quot;);
            }
        }, &quot;线程-1&quot;);

        // 3. 线程2 启动，执行 ABA 序列
        Thread thread2 = new Thread(() -&amp;gt; {
            System.out.println(&quot;\n--- 线程-2 执行 ABA 序列 ---&quot;);
            stack.pop(); // 弹出 A，栈顶变为 B
            stack.pop(); // 弹出 B，栈顶变为 C
            stack.push(&quot;X&quot;); // 压入一个新节点 X，栈顶变为 X → C
            stack.push(&quot;A&quot;); // 再压入一个新的 A，栈顶变为 A → X → C
            System.out.println(&quot;--- 线程-2 完成 ABA 序列 ---&quot;);
            stack.printStack();
        }, &quot;线程-2&quot;);

        thread1.start(); // 启动线程1
        thread2.start(); // 启动线程2

        thread1.join(); // 等待线程1完成
        thread2.join(); // 等待线程2完成

        System.out.println(&quot;\n--- 最终栈内容 ---&quot;);
        stack.printStack();
        System.out.println(&quot;当前栈顶节点: &quot; + stack.getTop());
        if (stack.getTop() != null) {
            System.out.println(&quot;栈顶节点的 next: &quot; + stack.getTop().next);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/28/117anny-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/28/11banhc-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/28/11betve-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;如何解决ABA问题&lt;/h1&gt;
&lt;p&gt;解决ABA问题的主要方法是引入一个 版本号（或时间戳） 机制。每次修改变量时，不仅修改值，也同时修改版本号。CAS操作时，需要同时比较值和版本号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/28/10lo3io-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用AtomicStampedReference解决问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example.aba;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

@Slf4j
public class AbaSolve {
    static AtomicStampedReference&amp;lt;String&amp;gt; ref = new AtomicStampedReference&amp;lt;&amp;gt;(&quot;A&quot;, 0);

    public static void main(String[] args) {
        log.debug(&quot;main start ....&quot;);
        String prev = ref.getReference();
        int stamp = ref.getStamp();
        log.debug(&quot;stamp: {}&quot;, stamp);

        other();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug(&quot;change A -&amp;gt; C {} &quot;, ref.compareAndSet(prev, &quot;C&quot;, stamp, stamp + 1));
    }

    private static void other() {
        new Thread(() -&amp;gt; {
            int stamp = ref.getStamp();
            log.debug(&quot;{} &apos;s stamp is : {}&quot;,Thread.currentThread().getName(),stamp);
            log.debug(&quot;change A-&amp;gt; B {} &quot;, ref.compareAndSet(ref.getReference(), &quot;B&quot;, stamp, stamp + 1));
            stamp = ref.getStamp();
            log.debug(&quot;{} &apos;s changed stamp is : {}&quot;,Thread.currentThread().getName(),stamp);
        }, &quot;t1&quot;).start();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(()-&amp;gt;{
            int stamp = ref.getStamp();
            log.debug(&quot;{} &apos;s stamp is : {}&quot;,Thread.currentThread().getName(),stamp);
            log.debug(&quot;change B-&amp;gt;A {}&quot;,ref.compareAndSet(ref.getReference(),&quot;A&quot;,stamp,stamp + 1));
            stamp = ref.getStamp();
            log.debug(&quot;{} &apos;s changed stamp is : {}&quot;,Thread.currentThread().getName(),stamp);
        },&quot;t2&quot;).start();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/31/t2rzcb-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>策略模式</title><link>https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;策略模式是一种行为型设计模式。&lt;/p&gt;
&lt;p&gt;在策略模式定义了一系列算法或策略，并将每个算法封装在独立的类中，使得它们可以互相替换。通过使用策略模式，可以在运行时根据需要选择不同的算法，而不需要修改客户端代码。&lt;/p&gt;
&lt;p&gt;在策略模式中，我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。&lt;/p&gt;
&lt;p&gt;策略模式平常我们多用来消除if-else switch等多重判断的代码，可以有效地应对代码复杂性。&lt;/p&gt;
&lt;p&gt;下述代码对应的业务，根据对应的优惠类型，对价格作出相应的优惠。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

import java.util.Objects;

public class DiscountTest {
    public static void main(String[] args) {
        Double result = DiscountTest.discount(&quot;1&quot;, 100.00);
        System.out.println(result);
    }

    public static Double discount(String type, Double price) {
        if (Objects.equals(type, &quot;1&quot;)) {
            return price * 0.8;
        } else if (Objects.equals(type, &quot;2&quot;)) {
            return price * 0.6;
        } else if (Objects.equals(type, &quot;3&quot;)) {
            return price * 0.5;
        } else {
            return price;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;但是我们很快就能发现问题了，这还是个案例，if else代码块就这么多了，真实的业务会多少if else可想而知了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以应用策略模式解决这个问题：
1.将不同的优惠类型定义为不同的策略算法实现类。
2. 保证开闭原则，增加程序的健壮性以及可扩展性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/29/k2ah0p-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

import java.util.HashMap;
import java.util.Map;

interface DiscountStrategy {
    Double discount(Double price);
}

/**
 * Implements 80% discount logic.
 */
class Discount80Strategy implements DiscountStrategy {
    static {
        DiscountStrategyFactory.registry(&quot;1&quot;, new Discount80Strategy());
    }

    @Override
    public Double discount(Double price) {
        return price * 0.8; // Bug 1: Incorrect operator `_0.8`
    }
}

/**
 * Implements 60% discount logic.
 */
class Discount60Strategy implements DiscountStrategy {
    static {
        DiscountStrategyFactory.registry(&quot;2&quot;, new Discount60Strategy());
    }

    @Override
    public Double discount(Double price) {
        return price * 0.6; // Bug 2: Incorrect operator `_ 0.6`
    }
}

/**
 * Implements 50% discount logic.
 */
class Discount50Strategy implements DiscountStrategy {
    static {
        DiscountStrategyFactory.registry(&quot;3&quot;, new Discount50Strategy());
    }

    @Override
    public Double discount(Double price) {
        return price * 0.5;
    }
}

class DiscountStrategyFactory {
    private static final Map&amp;lt;String, DiscountStrategy&amp;gt; strategyMap = new HashMap&amp;lt;&amp;gt;();

    public static void registry(String type, DiscountStrategy strategy) {
        strategyMap.put(type, strategy);
    }

    public static DiscountStrategy getStrategy(String type) {
        return strategyMap.get(type);
    }
}

public class DiscountTest2 {
    public static void main(String[] args) {
        new Discount80Strategy();
        new Discount60Strategy();
        new Discount50Strategy();
        Double result1 = DiscountStrategyFactory.getStrategy(&quot;1&quot;).discount(100.00);
        System.out.println(&quot;80% Discount: &quot; + result1);

        Double result2 = DiscountStrategyFactory.getStrategy(&quot;2&quot;).discount(100.00);
        System.out.println(&quot;60% Discount: &quot; + result2);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/29/k8w76g-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用spring&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.example.discount;

import org.springframework.stereotype.Component;

@Component
public class Discount90Strategy implements DiscountStrategy {
    @Override
    public Double discount(Double price) {
        return price * 0.9;
    }

    @Override
    public String mark() {
        return &quot;1&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.discount;

import org.springframework.stereotype.Component;

@Component
public class Discount80Strategy implements DiscountStrategy {
    @Override
    public Double discount( Double price) {
        return price * 0.8;
    }

    @Override
    public String mark() {
        return &quot;2&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.discount;

import org.springframework.stereotype.Component;

@Component
public class Discount50Strategy implements DiscountStrategy {
    @Override
    public Double discount(Double price) {
        return price * 0.5;
    }

    @Override
    public String mark() {
        return &quot;3&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.discount;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class DiscountFactory implements InitializingBean {
    @Autowired
    private ApplicationContext context;


    private final Map&amp;lt;String, DiscountStrategy&amp;gt; discountStrategies = new HashMap&amp;lt;&amp;gt;();

    public DiscountStrategy chooseStrategy(String type) {
        return discountStrategies.get(type);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Map&amp;lt;String, DiscountStrategy&amp;gt; beans = context.getBeansOfType(DiscountStrategy.class);
        beans.forEach((k, v) -&amp;gt; discountStrategies.put(v.mark(), v));
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.discount;

public interface DiscountStrategy {
    Double discount(Double price);

    String mark();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example;

import org.example.config.AppConfig;
import org.example.discount.DiscountFactory;
import org.example.discount.DiscountStrategy;
import lombok.extern.slf4j.Slf4j;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class) // Add this annotation
@SpringBootTest(classes = AppConfig.class)
@Slf4j
public class DiscountTest {
    @Autowired
    private DiscountFactory discountFactory;

    @Test
    public void test() {
        DiscountStrategy strategy = discountFactory.chooseStrategy(&quot;2&quot;);
        Double result = strategy.discount(1000.0);
        log.info(String.valueOf(result));
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.2.5&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;groupId&amp;gt;org.example&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;learn_juc&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;17&amp;lt;/java.version&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;slf4j-api&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;excludes&amp;gt;
                        &amp;lt;exclude&amp;gt;
                            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
                            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
                        &amp;lt;/exclude&amp;gt;
                    &amp;lt;/excludes&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/05/29/nazpnl-0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;优点&lt;/h1&gt;
&lt;p&gt;提高灵活性和可维护性：通过将算法的实现与使用分离开来，当需要修改或添加新算法时，只需定义新的策略类并将其传递给环境类即可，无需修改环境类代码。&lt;/p&gt;
&lt;p&gt;提高代码复用性：算法被封装在独立的策略类中，使得这些算法可以被多个不同的客户（环境类）复用。&lt;/p&gt;
&lt;p&gt;动态切换算法：允许在程序运行时根据需要动态地改变和选择算法，从而实现不同的功能和行为，使程序更灵活。&lt;/p&gt;
&lt;p&gt;算法实现与使用分离使代码更清晰：客户端代码仅需关注如何选择和使用不同的算法，而不必关心算法的具体实现细节，使代码更简洁、易于理解和扩展。&lt;/p&gt;
&lt;p&gt;避免大量条件语句：当需要根据不同条件选择不同算法时，策略模式可以避免使用复杂的 if-else 或 switch 语句，使代码结构更清晰，更易于维护。&lt;/p&gt;
</content:encoded></item><item><title>Redis有哪些数据类型</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%9C%89%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/redis%E6%9C%89%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;官方文档&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/&quot;&gt;Redis官方文档&lt;/a&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/pesc98-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://javabetter.cn/sidebar/sanfene/redis.html#_3-%F0%9F%8C%9Fredis%E6%9C%89%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B&quot; alt=&quot;JavaBetter&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;基本数据类型&lt;/h1&gt;
&lt;p&gt;Redis支持五种基本数据类型&lt;/p&gt;
&lt;h2&gt;字符串&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/pfuo04-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/#strings&quot; alt=&quot;Redis数据类型-字符串&quot; /&gt;
&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/strings/&quot; alt=&quot;详细文档&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;字符串是最基本的数据类型，可以存储文本，数字或者二进制数据，最大的容量是512MB。适合缓存单个对象，比如验证码,token，计数器等。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列表&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/phht75-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/#lists&quot; alt=&quot;Redis数据类型-列表&quot; /&gt;
&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/lists/&quot; alt=&quot;详细文档&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;列表是一个有序的字符串集合，可以在头部或尾部插入元素，适合用于消息队列，任务调度等场景。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;哈希&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/pldpst-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/#hashes&quot; alt=&quot;Redis数据类型-哈希&quot; /&gt;
&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/hashes/&quot; alt=&quot;详细文档&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;哈希是一个键值对集合，适合用于存储对象。可以通过字段名快速访问字段值，支持对单个字段的操作，节省内存。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集合&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/plo11d-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/#sets&quot; alt=&quot;Redis数据类型-集合&quot; /&gt;
&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/sets/&quot; alt=&quot;详细文档&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;集合是一个无序的字符串集合，支持快速的成员查找，适合用于标签，好友关系等场景。
可以进行集合运算，如交集，差集，并集等。
平常拿来做一些去重操作。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;有序集合&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/plwl0i-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/#sorted-sets&quot; alt=&quot;Redis数据类型-有序集合&quot; /&gt;
&lt;img src=&quot;https://redis.io/docs/latest/develop/data-types/sorted-sets/&quot; alt=&quot;详细文档&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;有序集合是一个有序的字符串集合，每个元素都有一个分数，支持根据分数进行范围查询，适合用于排行榜，消息队列等场景。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/pmk3s0-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/pn3s6u-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;扩展数据类型&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://pdai.tech/md/db/nosql-redis/db-redis-data-type-special.html#redis%E5%85%A5%E9%97%A8---%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B3%E7%A7%8D%E7%89%B9%E6%AE%8A%E7%B1%BB%E5%9E%8B%E8%AF%A6%E8%A7%A3&quot;&gt;redis3种特殊类型详解&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;位图bitmap&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/bitmaps/&quot;&gt;详细文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/rfx8t3-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;位图是一个特殊的字符串类型，用于存储二进制位。可以用来统计用户活跃度，签到等场景。&lt;/p&gt;
&lt;h2&gt;基数统计HyperLogLog&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/&quot;&gt;详细文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/rek2t9-1.webp&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/19/repxn1-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基数统计通常用于统计不重复的元素数量，比如网站访问量，用户注册量等。&lt;/p&gt;
&lt;h2&gt;地理位置Geo&lt;/h2&gt;
&lt;p&gt;存储地理信息&lt;/p&gt;
&lt;h2&gt;Bloom Filter&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/&quot;&gt;详细文档&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>什么是Redis</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E4%BB%80%E4%B9%88%E6%98%AFredis/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/redis/%E4%BB%80%E4%B9%88%E6%98%AFredis/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是Redis&lt;/h1&gt;
&lt;p&gt;Redis是一个开源的额高性能键值对存储系统，它可以用作数据库、缓存和消息代理。Redis支持多种数据结构，如字符串、哈希、列表、集合和有序集合等。
主要特点是把数据存放在内存中，相比于直接访问磁盘的关系型数据库，读写速度会更快。&lt;/p&gt;
&lt;h2&gt;Redis的特点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高性能&lt;/strong&gt;：Redis可以每秒处理数百万个请求，读写速度非常快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久化&lt;/strong&gt;：Redis支持将数据持久化到磁盘，可以在重启后恢复数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;丰富的数据结构&lt;/strong&gt;：支持字符串、哈希、列表、集合、有序集合等多种数据类型，适用于不同的应用场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子操作&lt;/strong&gt;：Redis支持对数据的原子操作，保证数据的一致性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式&lt;/strong&gt;：支持主从复制、分片和高可用集群，适合大规模应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布/订阅&lt;/strong&gt;：支持发布/订阅模式，可以实现消息通知和实时数据更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lua脚本&lt;/strong&gt;：支持Lua脚本，可以在服务器端执行复杂的操作，减少网络传输延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事务支持&lt;/strong&gt;：支持事务操作，可以保证一组命令要么全部执行成功，要么全部不执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地理位置支持&lt;/strong&gt;：支持地理位置数据，可以进行地理位置查询和计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多种客户端支持&lt;/strong&gt;：提供多种编程语言的客户端库，如Java、Python、Node.js等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易于部署和使用&lt;/strong&gt;：Redis的安装和配置相对简单，社区活跃，有丰富的文档和教程。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;使用场景&lt;/h1&gt;
&lt;p&gt;Redis常用于以下场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：可以用来缓存数据库查询结果，减少数据库负载，提高&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话存储&lt;/strong&gt;：可以用来存储用户会话信息，支持高并发访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时数据分析&lt;/strong&gt;：可以用来存储实时数据，如用户行为分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息队列&lt;/strong&gt;：可以用作消息队列系统，支持发布/订阅模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排行榜&lt;/strong&gt;：可以用来实现排行榜功能，支持有序集合数据结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式锁&lt;/strong&gt;：可以用来实现分布式锁，支持高并发场景下的资源控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地理位置服务&lt;/strong&gt;：可以用来存储地理位置信息，支持地理位置查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计数器&lt;/strong&gt;：可以用来实现计数器功能，如网站访问量统计。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Redis分布式部署的方式&lt;/h1&gt;
&lt;p&gt;Redis的分布式部署方式主要有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主从复制（Master-Slave Replication）&lt;/strong&gt;：通过设置主节点和多个从节点，实现数据的复制和备份。主节点负责写操作，从节点负责读操作，可以提高读性能和数据安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分片（Sharding）&lt;/strong&gt;：将数据分布到多个Redis实例中，每个实例存储一部分数据。可以通过哈希算法将数据分配到不同的实例，实现数据的水平扩展。常用的分片方式有一致性哈希（Consistent Hashing）和范围分片（Range Sharding）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis集群（Redis Cluster）&lt;/strong&gt;：Redis官方提供的集群模式，支持自动分片和故障转移。Redis集群可以将数据分布到多个节点上，每个节点存储一部分数据，并且支持动态扩容和缩容。集群模式下，客户端可以通过集群节点的地址直接访问数据，无需额外的代理层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentinel模式&lt;/strong&gt;：Redis Sentinel是Redis的高可用解决方案，可以监控Redis实例的状态，并在主节点发生故障时自动进行故障转移。Sentinel可以与主从复制结合使用，提供高可用性和自动恢复能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;和MySQL的区别&lt;/h1&gt;
&lt;p&gt;Redis不是关系型数据库，而是一个键值对存储系统。
Redis把数据存放在内存中，读写速度非常快，而MySQL是基于磁盘的关系型数据库，读写速度相对较慢。&lt;/p&gt;
&lt;p&gt;实际开发中，会把Redis作为缓存层，存储一些热点数据，减少对MySQL的访问压力，提高系统性能。&lt;/p&gt;
</content:encoded></item><item><title>nacos安装</title><link>https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/nacos/nacos%E5%AE%89%E8%A3%85/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E4%B8%AD%E9%97%B4%E4%BB%B6/nacos/nacos%E5%AE%89%E8%A3%85/</guid><pubDate>Sat, 19 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;nacos安装&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;docker run --name nacos-standalone-derby \
    -e MODE=standalone \
    -e NACOS_AUTH_TOKEN=bWVvd3JhaW55eWRzNjY2Nm1lb3dyYWlueXlkczY2NjY= \
    -e NACOS_AUTH_IDENTITY_KEY=meowrain \
    -e NACOS_AUTH_IDENTITY_VALUE=meowrain \
    -p 8085:8080 \
    -p 8848:8848 \
    -p 9848:9848 \
    -d nacos/nacos-server:latest
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>用户态和内核态</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%94%A8%E6%88%B7%E6%80%81%E5%92%8C%E5%86%85%E6%A0%B8%E6%80%81/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%94%A8%E6%88%B7%E6%80%81%E5%92%8C%E5%86%85%E6%A0%B8%E6%80%81/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;用户态（User Mode）与内核态（Kernel Mode）总结&lt;/h3&gt;
&lt;h4&gt;1. &lt;strong&gt;基本概念&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用户态（User Mode）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户程序运行的状态，只能访问受限的资源。&lt;/li&gt;
&lt;li&gt;无法直接访问硬件设备或内核数据结构。&lt;/li&gt;
&lt;li&gt;当需要执行敏感操作（如磁盘读写、网络通信）时，需要通过&lt;strong&gt;系统调用&lt;/strong&gt;进入内核态。&lt;/li&gt;
&lt;li&gt;主要用于运行应用程序、库函数等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内核态（Kernel Mode）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统内核运行的状态，具有最高权限。&lt;/li&gt;
&lt;li&gt;可以直接访问硬件资源和系统内存。&lt;/li&gt;
&lt;li&gt;负责进程管理、内存管理、设备管理、文件系统、网络协议栈等核心功能。&lt;/li&gt;
&lt;li&gt;系统调用的请求会触发用户态切换到内核态执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;2. &lt;strong&gt;用户空间与内核空间&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内核空间（Kernel Space）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存放操作系统内核代码、数据结构。&lt;/li&gt;
&lt;li&gt;拥有完整的资源访问权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用户空间（User Space）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为应用程序分配的内存区域。&lt;/li&gt;
&lt;li&gt;应用程序无法直接操作硬件或内核数据，需要通过系统调用与内核交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;3. &lt;strong&gt;切换过程&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当用户程序调用系统调用接口（如 &lt;code&gt;open()&lt;/code&gt;、&lt;code&gt;read()&lt;/code&gt;）时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从用户态切换到内核态。&lt;/li&gt;
&lt;li&gt;内核完成对应的底层操作（如文件打开、设备读写）。&lt;/li&gt;
&lt;li&gt;返回结果，再切换回用户态。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;4. &lt;strong&gt;区别与特点&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;用户态&lt;/th&gt;
&lt;th&gt;内核态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;权限&lt;/td&gt;
&lt;td&gt;受限权限&lt;/td&gt;
&lt;td&gt;最高权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能否访问硬件&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要运行内容&lt;/td&gt;
&lt;td&gt;应用程序、库函数&lt;/td&gt;
&lt;td&gt;操作系统内核、驱动程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换开销&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;需要上下文切换，有一定开销&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;5. &lt;strong&gt;优化点&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;频繁切换用户态与内核态&lt;/strong&gt;会带来性能开销（上下文切换 + 权限检查）。&lt;/li&gt;
&lt;li&gt;因此在系统设计中，会尽量减少不必要的内核调用次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>线程和进程的区别</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BA%BF%E7%A8%8B%E5%92%8C%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BA%BF%E7%A8%8B%E5%92%8C%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线程和进程的区别&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10r4a6l-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进程： 是操作系统分配资源的基本单位。每个进程都有自己独立的内存空间，可以看作是一个正在运行的程序实例，进程之间是相互独立的。&lt;/p&gt;
&lt;p&gt;线程： 是CPU/任务调度的基本单位，属于进程，一个进程中可以包含多个线程。线程共享进程的内存空间和资源，但每个线程有自己独立的栈和寄存器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10r4hzg-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>进程的几种状态</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8A%B6%E6%80%81/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8A%B6%E6%80%81/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;进程的几种状态&lt;/h1&gt;
&lt;p&gt;运行 - 就绪 - 阻塞&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10y3u6x-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10y44ei-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>零拷贝技术</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%8A%80%E6%9C%AF/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%8A%80%E6%9C%AF/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;零拷贝技术&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10vy5ev-1.webp&quot; alt=&quot;OS-1E5~1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10vz716-1.webp&quot; alt=&quot;图片.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;sendfile原理&lt;/h1&gt;
&lt;p&gt;sendfile()系统调用能让内核直接把文件描述符的内容传送到另外一个文件描述符。&lt;/p&gt;
&lt;p&gt;数据绕过了用户空间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10vzeqy-1.webp&quot; alt=&quot;图片.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;mmap原理&lt;/h1&gt;
&lt;p&gt;把文件或者其他对象映射到进程虚拟地址空间，它的零拷贝主要体现在数据共享和懒加载上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10vzg41-1.webp&quot; alt=&quot;图片.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10vzxk4-1.webp&quot; alt=&quot;图片.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>阻塞IO和非阻塞IO，同步IO与异步IO</title><link>https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%98%BB%E5%A1%9Eio%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9Eio/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%98%BB%E5%A1%9Eio%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9Eio/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;阻塞和非阻塞IO&lt;/h1&gt;
&lt;h1&gt;阻塞IO&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;线程发起阻塞IO（如read）
    ↓（数据没好，被阻塞）
线程切换为“阻塞态”
    ↓
CPU调度其它进程/线程
    ↓（IO完成）
线程被唤醒，进入可运行态
    ↓
等待被操作系统调度分到CPU
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当⽤户程序执⾏ read ，线程会被阻塞，⼀直等到内核数据准备好，并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中，当拷⻉过程完成， read 才会返回。&lt;/p&gt;
&lt;p&gt;注意，&lt;strong&gt;阻塞等待的是&lt;code&gt;内核数据准备好&lt;/code&gt;和&lt;code&gt;数据从内核态拷⻉到⽤户态&lt;/code&gt;这两个过程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10rukfc-1.webp&quot; alt=&quot;OS-F06~1&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;非阻塞IO&lt;/h1&gt;
&lt;p&gt;⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回，可以继续往下执⾏，此时应⽤程序不断轮询内核，直到数据准备好，内核将数据拷⻉到应⽤程序缓冲区， read 调⽤才可以获取到结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10rskv8-1.webp&quot; alt=&quot;OS-771~1&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;同步，异步IO&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10t1y6z-1.webp&quot; alt=&quot;图片.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;同步IO&lt;/h1&gt;
&lt;p&gt;同步IO只有当IO操作完成以后，程序调用才会返回&lt;/p&gt;
&lt;h1&gt;异步IO&lt;/h1&gt;
&lt;p&gt;I/O请求发出以后，立刻返回，不管数据是否马上准备好&lt;/p&gt;
&lt;p&gt;IO完成的时候，操作系统以通知的方式告诉应用。&lt;/p&gt;
&lt;h1&gt;区别&lt;/h1&gt;
&lt;p&gt;阻塞 vs 非阻塞：关注 进程/线程是否在等待数据就绪。&lt;/p&gt;
&lt;p&gt;同步 vs 异步：关注 I/O 操作是否完成（包括数据准备 + 数据拷贝）时，谁来通知结果。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解释
阻塞/非阻塞
阻塞 I/O：调用 I/O 时，如果数据还没准备好，进程就会停在那里等（什么都不能做）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;非阻塞 I/O：调用 I/O 时，如果数据没准备好，立即返回，不会卡住进程；用户程序可以去做其他事，但需要主动检查数据是否准备好。&lt;/p&gt;
&lt;p&gt;同步/异步
同步 I/O：用户发起 I/O 请求后，必须等 I/O 完成（数据准备 + 数据复制）才能继续，比如 read() 等数据拷贝完才返回。&lt;/p&gt;
&lt;p&gt;异步 I/O：用户发起 I/O 请求后，立即返回；等 I/O 完成（数据准备 + 拷贝）时，内核主动通知用户（回调或信号）。&lt;/p&gt;
</content:encoded></item><item><title>JVM GC相关参数</title><link>https://blog.meowrain.cn/posts/java/jvm/gc%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/jvm/gc%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;GC相关参数&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/10kn2xz-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;1. 堆初始大小 (&lt;code&gt;Xms&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;Xms&amp;lt;size&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 设置 JVM 启动时&lt;strong&gt;初始分配的堆内存大小&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;决定了 Java 程序一启动时，JVM 向操作系统申请的内存大小。&lt;/li&gt;
&lt;li&gt;如果设置得太小，JVM 可能会在程序运行初期频繁地进行堆内存扩展，这会带来一定的性能开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例:&lt;/strong&gt; &lt;code&gt;Xms512m&lt;/code&gt; 表示设置初始堆大小为 512MB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最佳实践:&lt;/strong&gt; 在生产环境中，通常建议将 &lt;code&gt;Xms&lt;/code&gt; 和 &lt;code&gt;Xmx&lt;/code&gt; 设置为相同的值，以避免堆的动态扩展和收缩带来的性能抖动。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. 堆最大大小 (&lt;code&gt;Xmx&lt;/code&gt; 或 &lt;code&gt;XX:MaxHeapSize&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;Xmx&amp;lt;size&amp;gt;&lt;/code&gt; 或 &lt;code&gt;XX:MaxHeapSize=&amp;lt;size&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 设置 JVM &lt;strong&gt;允许分配的最大堆内存大小&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是堆内存的硬性上限。如果应用程序需要的内存超过了这个值，就会抛出 &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;合理设置此值可以防止应用程序因内存泄漏等问题耗尽所有服务器内存，从而影响其他进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例:&lt;/strong&gt; &lt;code&gt;Xmx2g&lt;/code&gt; 表示设置最大堆大小为 2GB。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3. 新生代大小 (&lt;code&gt;Xmn&lt;/code&gt; 或 &lt;code&gt;XX:NewSize&lt;/code&gt; + &lt;code&gt;XX:MaxNewSize&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;Xmn&amp;lt;size&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 设置&lt;strong&gt;新生代（Young Generation）的大小&lt;/strong&gt;。这是一个快捷参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;新生代是绝大多数新对象产生的地方，也是 Minor GC 发生的主要区域。&lt;/li&gt;
&lt;li&gt;设置一个合理的新生代大小非常重要。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过小:&lt;/strong&gt; 会导致 Minor GC 过于频繁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过大:&lt;/strong&gt; 会挤占老年代的空间，可能导致更频繁的 Full GC。同时，单次 Minor GC 的时间可能会变长。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;补充:&lt;/strong&gt; &lt;code&gt;Xmn&lt;/code&gt; 实际上是同时设置了 &lt;code&gt;XX:NewSize&lt;/code&gt;（新生代初始大小）和 &lt;code&gt;XX:MaxNewSize&lt;/code&gt;（新生代最大大小）。如果希望新生代大小动态变化，可以分别设置这两个参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4. 幸存区比例 (&lt;code&gt;XX:SurvivorRatio&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:SurvivorRatio=&amp;lt;ratio&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 设置新生代中 &lt;strong&gt;Eden 区与一个 Survivor 区的大小比例&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算公式:&lt;/strong&gt; &lt;code&gt;ratio = Eden区大小 / Survivor区大小&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这个比例决定了新生代中用于创建新对象（Eden）和存放幸存对象（Survivor）的空间分配。&lt;/li&gt;
&lt;li&gt;例如，&lt;code&gt;XX:SurvivorRatio=8&lt;/code&gt; 表示 Eden:S0:S1 的比例是 8:1:1。这意味着 Eden 区将占用新生代 8/10 的空间，而每个 Survivor 区占用 1/10。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意:&lt;/strong&gt; 这个参数在启用了自适应大小策略（&lt;code&gt;XX:+UseAdaptiveSizePolicy&lt;/code&gt;，在某些 GC 算法中默认开启）时，其设置的固定比例可能会被 JVM 动态调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;5. 幸存区比例 (动态) (&lt;code&gt;XX:InitialSurvivorRatio&lt;/code&gt; 和 &lt;code&gt;XX:+UseAdaptiveSizePolicy&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:+UseAdaptiveSizePolicy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; &lt;strong&gt;启用 GC 自适应大小策略&lt;/strong&gt;。这个策略允许 JVM 根据应用程序的运行情况（如吞吐量、停顿时间目标）动态调整堆中各区域的大小，包括 Eden/Survivor 的比例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;开启后，JVM 会自动优化内存分配，省去了手动精细调优的麻烦。这是 Parallel GC 等收集器默认开启的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XX:InitialSurvivorRatio&lt;/code&gt; 用于设定自适应策略下的&lt;strong&gt;初始&lt;/strong&gt; SurvivorRatio 值，后续 JVM 可能会根据需要进行调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论:&lt;/strong&gt; 如果你看到这个参数，意味着 JVM 正在自动管理新生代的比例，&lt;code&gt;XX:SurvivorRatio&lt;/code&gt; 的静态设置可能不会生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;6. 晋升阈值 (&lt;code&gt;XX:MaxTenuringThreshold&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:MaxTenuringThreshold=&amp;lt;threshold&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 设置对象从新生代晋升到老年代的&lt;strong&gt;年龄阈值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一个对象在 Survivor 区每熬过一次 Minor GC，其年龄就加 1。当年龄达到这个阈值时，就会被移动到老年代。&lt;/li&gt;
&lt;li&gt;默认值通常是 15（或 6，取决于 GC）。&lt;/li&gt;
&lt;li&gt;如果设置得太高，对象可能长时间停留在 Survivor 区，增加了复制成本；如果设置得太低，可能导致一些生命周期不长的对象过早进入老年代，增加了 Full GC 的压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;7. 晋升详情 (&lt;code&gt;XX:+PrintTenuringDistribution&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:+PrintTenuringDistribution&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 一个诊断参数，用于在每次 Minor GC 后&lt;strong&gt;打印出 Survivor 区中对象的年龄分布情况&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是调优 &lt;code&gt;XX:MaxTenuringThreshold&lt;/code&gt; 的重要工具。&lt;/li&gt;
&lt;li&gt;通过观察日志，你可以看到每个年龄段有多少对象，以及 JVM 计算出的动态晋升阈值，从而判断当前设置是否合理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;8. GC 详情 (&lt;code&gt;XX:+PrintGCDetails&lt;/code&gt; 和 &lt;code&gt;verbose:gc&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:+PrintGCDetails&lt;/code&gt; 或 &lt;code&gt;verbose:gc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 打印详细的 GC 日志信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是进行 GC 性能分析和故障排查的&lt;strong&gt;必备参数&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verbose:gc&lt;/code&gt; 是一个标准参数，输出基本的 GC 信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XX:+PrintGCDetails&lt;/code&gt; 会提供更详尽的信息，包括每次 GC 前后堆各区域的大小、GC 耗时等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐:&lt;/strong&gt; 通常与 &lt;code&gt;XX:+PrintGCTimeStamps&lt;/code&gt; 或 &lt;code&gt;XX:+PrintGCDateStamps&lt;/code&gt; 一起使用，为日志增加时间戳。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;9. FullGC 前 MinorGC (&lt;code&gt;XX:+ScavengeBeforeFullGC&lt;/code&gt;)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; &lt;code&gt;XX:+ScavengeBeforeFullGC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义:&lt;/strong&gt; 指示 JVM 在执行 Full GC 之前，先强制进行一次 Minor GC。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;理论上，这可以清理掉新生代中大部分可以被回收的对象，从而减轻 Full GC 的负担，因为 Full GC 需要处理整个堆（包括新生代）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;此参数在现代的 GC（如 G1）中已不推荐使用或被废弃，因为它们有更智能的回收策略。&lt;/li&gt;
&lt;li&gt;在某些情况下，它可能会引入一次额外的、不必要的停顿（Minor GC 的停顿）。因此，除非有明确的测试数据支持，否则一般不建议开启。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JVM内存模型分区</title><link>https://blog.meowrain.cn/posts/java/jvm/jvm%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E5%88%86%E5%8C%BA/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/jvm/jvm%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E5%88%86%E5%8C%BA/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JVM内存模型分⼏个区，每个区放什么对象&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/12an835-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/12b818c-1.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/12b1jr4-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.meowrain.cn/api/i/2025/07/18/12awf70-1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分为方法区，堆，本地方法栈，虚拟机栈，程序计数器&lt;/p&gt;
&lt;p&gt;方法区（元空间）：用于存储已经被虚拟机加载的类信息，常量，静态变量等数据。虽然方法区被描述为堆的逻辑部分，但有”非堆“的别名。方法区可以选择不实现垃圾收集，内存不足的时候，会抛出OutOfMemoryError异常。&lt;/p&gt;
&lt;p&gt;程序计数器： 当前线程所执行的字节码的行号指示器，存储当前线程正在执行的Java方法的JVM指令地址。&lt;/p&gt;
&lt;p&gt;JVM虚拟机栈：每个线程都有自己独立的Java虚拟机栈，生命周期和线程相同，每个方法在执行的时候都会创建一个栈帧，用来存储局部变量表，操作数栈，动态链接，方法出口等信息。&lt;/p&gt;
&lt;p&gt;本地方法栈： 与Java虚拟机栈差不读多，执行本地方法，其中堆和方法区是线程共有的。&lt;/p&gt;
&lt;p&gt;Java堆： 存放和管理对象实例，被所有线程共享。&lt;/p&gt;
</content:encoded></item><item><title>JUC笔记</title><link>https://blog.meowrain.cn/posts/java/juc/juc%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://blog.meowrain.cn/posts/java/juc/juc%E7%AC%94%E8%AE%B0/</guid><pubDate>Fri, 18 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JUC&lt;/h1&gt;
&lt;h2&gt;进程&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;进程：程序是静止的，进程实体的运行过程就是进程，是系统进行&lt;strong&gt;资源分配的基本单位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进程的特征：并发性、异步性、动态性、独立性、结构性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程&lt;/strong&gt;：线程是属于进程的，是一个基本的 CPU 执行单元，是程序执行流的最小单元。线程是进程中的一个实体，是系统&lt;strong&gt;独立调度的基本单位&lt;/strong&gt;，线程本身不拥有系统资源，只拥有一点在运行中必不可少的资源，与同属一个进程的其他线程共享进程所拥有的全部资源&lt;/p&gt;
&lt;p&gt;关系：一个进程可以包含多个线程，这就是多线程，比如看视频是进程，图画、声音、广告等就是多个线程&lt;/p&gt;
&lt;p&gt;线程的作用：使多道程序更好的并发执行，提高资源利用率和系统吞吐量，增强操作系统的并发性能&lt;/p&gt;
&lt;p&gt;并发并行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并行：在同一时刻，有多个指令在多个 CPU 上同时执行&lt;/li&gt;
&lt;li&gt;并发：在同一时刻，有多个指令在单个 CPU 上交替执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步异步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要等待结果返回，才能继续运行就是同步&lt;/li&gt;
&lt;li&gt;不需要等待结果返回，就能继续运行就是异步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：&lt;a href=&quot;https://www.bilibili.com/video/BV16J411h7Rd&quot;&gt;https://www.bilibili.com/video/BV16J411h7Rd&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;笔记的整体结构依据视频编写，并随着学习的深入补充了很多知识&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;p&gt;线程进程对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进程基本上相互独立的，而线程存在于进程内，是进程的一个子集&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程拥有共享的资源，如内存空间等，供其&lt;strong&gt;内部的线程共享&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程间通信较为复杂&lt;/p&gt;
&lt;p&gt;同一台计算机的进程通信称为 IPC（Inter-process communication）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量：信号量是一个计数器，用于多进程对共享数据的访问，解决同步相关的问题并避免竞争条件&lt;/li&gt;
&lt;li&gt;共享存储：多个进程可以访问同一块内存空间，需要使用信号量用来同步对共享存储的访问&lt;/li&gt;
&lt;li&gt;管道通信：管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件，该文件同一时间只允许一个进程访问，所以只支持&lt;strong&gt;半双工通信&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;匿名管道（Pipes）：用于具有亲缘关系的父子进程间或者兄弟进程之间的通信&lt;/li&gt;
&lt;li&gt;命名管道（Names Pipes）：以磁盘文件的方式存在，可以实现本机任意两个进程通信，遵循 FIFO&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;消息队列：内核中存储消息的链表，由消息队列标识符标识，能在不同进程之间提供&lt;strong&gt;全双工通信&lt;/strong&gt;，对比管道：
&lt;ul&gt;
&lt;li&gt;匿名管道存在于内存中的文件；命名管道存在于实际的磁盘介质或者文件系统；消息队列存放在内核中，只有在内核重启（操作系统重启）或者显示地删除一个消息队列时，该消息队列才被真正删除&lt;/li&gt;
&lt;li&gt;读进程可以根据消息类型有选择地接收消息，而不像 FIFO 那样只能默认地接收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同计算机之间的&lt;strong&gt;进程通信&lt;/strong&gt;，需要通过网络，并遵守共同的协议，例如 HTTP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;套接字：与其它通信机制不同的是，可用于不同机器间的互相通信&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程通信相对简单，因为线程之间共享进程内的内存，一个例子是多个线程可以访问同一个共享变量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 中的通信机制&lt;/strong&gt;：volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程更轻量，线程上下文切换成本一般上要比进程上下文切换低&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;线程&lt;/h2&gt;
&lt;h3&gt;创建线程&lt;/h3&gt;
&lt;h4&gt;Thread&lt;/h4&gt;
&lt;p&gt;Thread 创建线程方式：创建线程类，匿名内部类方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;start() 方法底层其实是给 CPU 注册当前线程，并且触发 run() 方法执行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程的启动必须调用 start() 方法，如果线程直接调用 run() 方法，相当于变成了普通类的执行，此时主线程将只有执行该线程&lt;/li&gt;
&lt;li&gt;建议线程先创建子线程，主线程的任务放在之后，否则主线程（main）永远是先执行完&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thread 构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Thread()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Thread(String name)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        for(int i = 0 ; i &amp;lt; 100 ; i++ ){
            System.out.println(&quot;main线程&quot; + i)
        }
        // main线程输出放在上面 就变成有先后顺序了，因为是 main 线程驱动的子线程运行
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0 ; i &amp;lt; 100 ; i++ ) {
            System.out.println(&quot;子线程输出：&quot;+i)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承 Thread 类的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：编码简单&lt;/li&gt;
&lt;li&gt;缺点：线程类已经继承了 Thread 类无法继承其他类了，功能不能通过继承拓展（单继承的局限性）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Runnable&lt;/h4&gt;
&lt;p&gt;Runnable 创建线程方式：创建线程类，匿名内部类方式&lt;/p&gt;
&lt;p&gt;Thread 的构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Thread(Runnable target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public Thread(Runnable target, String name)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Runnable target = new MyRunnable();
        Thread t1 = new Thread(target,&quot;1号线程&quot;);
  t1.start();
        Thread t2 = new Thread(target);//Thread-0
    }
}

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i &amp;lt; 10 ; i++ ){
            System.out.println(Thread.currentThread().getName() + &quot;-&amp;gt;&quot; + i);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Thread 类本身也是实现了 Runnable 接口&lt;/strong&gt;，Thread 类中持有 Runnable 的属性，执行线程 run 方法底层是调用 Runnable#run：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Thread implements Runnable {
    private Runnable target;
    
    public void run() {
        if (target != null) {
           // 底层调用的是 Runnable 的 run 方法
            target.run();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runnable 方式的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;缺点：代码复杂一点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;线程任务类只是实现了 Runnable 接口，可以继续继承其他类，避免了单继承的局限性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同一个线程任务对象可以被包装成多个线程对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适合多个多个线程去共享同一个资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现解耦操作，线程任务代码可以被多个线程共享，线程任务代码和线程独立&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池可以放入实现 Runnable 或 Callable 线程任务对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Callable&lt;/h4&gt;
&lt;p&gt;实现 Callable 接口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义一个线程任务类实现 Callable 接口，申明线程执行的结果类型&lt;/li&gt;
&lt;li&gt;重写线程任务类的 call 方法，这个方法可以直接返回执行的结果&lt;/li&gt;
&lt;li&gt;创建一个 Callable 的线程任务对象&lt;/li&gt;
&lt;li&gt;把 Callable 的线程任务对象&lt;strong&gt;包装成一个未来任务对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;把未来任务对象包装成线程对象&lt;/li&gt;
&lt;li&gt;调用线程的 start() 方法启动线程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;public FutureTask(Callable&amp;lt;V&amp;gt; callable)&lt;/code&gt;：未来任务对象，在线程执行完后得到线程的执行结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FutureTask 就是 Runnable 对象，因为 &lt;strong&gt;Thread 类只能执行 Runnable 实例的任务对象&lt;/strong&gt;，所以把 Callable 包装成未来任务对象&lt;/li&gt;
&lt;li&gt;线程池部分详解了 FutureTask 的源码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;public V get()&lt;/code&gt;：同步等待 task 执行完毕的结果，如果在线程中获取另一个线程执行结果，会阻塞等待，用于线程同步&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get() 线程会阻塞等待任务执行完成&lt;/li&gt;
&lt;li&gt;run() 执行完后会把结果设置到 FutureTask  的一个成员变量，get() 线程可以获取到该变量的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：同 Runnable，并且能得到线程执行的结果&lt;/li&gt;
&lt;li&gt;缺点：编码复杂&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadDemo {
    public static void main(String[] args) {
        Callable call = new MyCallable();
        FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(call);
        Thread t = new Thread(task);
        t.start();
        try {
            String s = task.get(); // 获取call方法返回的结果（正常/异常结果）
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }
    }

public class MyCallable implements Callable&amp;lt;String&amp;gt; {
    @Override//重写线程任务类方法
    public String call() throws Exception {
        return Thread.currentThread().getName() + &quot;-&amp;gt;&quot; + &quot;Hello World&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;线程方法&lt;/h3&gt;
&lt;h4&gt;API&lt;/h4&gt;
&lt;p&gt;Thread 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public void start()&lt;/td&gt;
&lt;td&gt;启动一个新线程，Java虚拟机调用此线程的 run 方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void run()&lt;/td&gt;
&lt;td&gt;线程启动后调用该方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void setName(String name)&lt;/td&gt;
&lt;td&gt;给当前线程取名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void getName()&lt;/td&gt;
&lt;td&gt;获取当前线程的名字&amp;lt;br /&amp;gt;线程存在默认名称：子线程是 Thread-索引，主线程是 main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static Thread currentThread()&lt;/td&gt;
&lt;td&gt;获取当前线程对象，代码在哪个线程中执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static void sleep(long time)&lt;/td&gt;
&lt;td&gt;让当前线程休眠多少毫秒再继续执行&amp;lt;br /&amp;gt;&lt;strong&gt;Thread.sleep(0)&lt;/strong&gt; : 让操作系统立刻重新进行一次 CPU 竞争&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static native void yield()&lt;/td&gt;
&lt;td&gt;提示线程调度器让出当前线程对 CPU 的使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getPriority()&lt;/td&gt;
&lt;td&gt;返回此线程的优先级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void setPriority(int priority)&lt;/td&gt;
&lt;td&gt;更改此线程的优先级，常用 1 5 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void interrupt()&lt;/td&gt;
&lt;td&gt;中断这个线程，异常处理机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public static boolean interrupted()&lt;/td&gt;
&lt;td&gt;判断当前线程是否被打断，清除打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public boolean isInterrupted()&lt;/td&gt;
&lt;td&gt;判断当前线程是否被打断，不清除打断标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void join()&lt;/td&gt;
&lt;td&gt;等待这个线程结束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void join(long millis)&lt;/td&gt;
&lt;td&gt;等待这个线程死亡 millis 毫秒，0 意味着永远等待&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final native boolean isAlive()&lt;/td&gt;
&lt;td&gt;线程是否存活（还没有运行完毕）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final void setDaemon(boolean on)&lt;/td&gt;
&lt;td&gt;将此线程标记为守护线程或用户线程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;run start&lt;/h4&gt;
&lt;p&gt;run：称为线程体，包含了要执行的这个线程的内容，方法运行结束，此线程随即终止。直接调用 run 是在主线程中执行了 run，没有启动新的线程，需要顺序执行&lt;/p&gt;
&lt;p&gt;start：使用 start 是启动新的线程，此线程处于就绪（可运行）状态，通过新的线程间接执行 run 中的代码&lt;/p&gt;
&lt;p&gt;说明：&lt;strong&gt;线程控制资源类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;run() 方法中的异常不能抛出，只能 try/catch&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为父类中没有抛出任何异常，子类不能比父类抛出更多的异常&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常不能跨线程传播回 main() 中&lt;/strong&gt;，因此必须在本地进行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;sleep yield&lt;/h4&gt;
&lt;p&gt;sleep：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 sleep 会让当前线程从 &lt;code&gt;Running&lt;/code&gt; 进入 &lt;code&gt;Timed Waiting&lt;/code&gt; 状态（阻塞）&lt;/li&gt;
&lt;li&gt;sleep() 方法的过程中，&lt;strong&gt;线程不会释放对象锁&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;其它线程可以使用 interrupt 方法打断正在睡眠的线程，这时 sleep 方法会抛出 InterruptedException&lt;/li&gt;
&lt;li&gt;睡眠结束后的线程未必会立刻得到执行，需要抢占 CPU&lt;/li&gt;
&lt;li&gt;建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;yield：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用&lt;/li&gt;
&lt;li&gt;具体的实现依赖于操作系统的任务调度器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会放弃 CPU 资源，锁资源不会释放&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;join&lt;/h4&gt;
&lt;p&gt;public final void join()：等待这个线程结束&lt;/p&gt;
&lt;p&gt;原理：调用者轮询检查线程 alive 状态，t1.join() 等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final synchronized void join(long millis) throws InterruptedException {
    // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
    while (isAlive()) {
        wait(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;join 方法是被 synchronized 修饰的，本质上是一个对象锁，其内部的 wait 方法调用也是释放锁的，但是&lt;strong&gt;释放的是当前的线程对象锁，而不是外面的锁&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用某个线程（t1）的 join 方法后，该线程（t1）抢占到 CPU 资源，就不再释放，直到线程执行完毕&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程同步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;join 实现线程同步，因为会阻塞等待另一个线程的结束，才能继续向下运行
&lt;ul&gt;
&lt;li&gt;需要外部共享变量，不符合面向对象封装的思想&lt;/li&gt;
&lt;li&gt;必须等待线程结束，不能配合线程池使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Future 实现（同步）：get() 方法阻塞等待执行结果
&lt;ul&gt;
&lt;li&gt;main 线程接收结果&lt;/li&gt;
&lt;li&gt;get 方法是让调用线程同步等待&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
        });
        t1.start();
        t1.join();//不等待线程执行结束，输出的10
        System.out.println(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;interrupt&lt;/h4&gt;
&lt;h5&gt;打断线程&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;public void interrupt()&lt;/code&gt;：打断这个线程，异常处理机制&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public static boolean interrupted()&lt;/code&gt;：判断当前线程是否被打断，打断返回 true，&lt;strong&gt;清除打断标记&lt;/strong&gt;，连续调用两次一定返回 false&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public boolean isInterrupted()&lt;/code&gt;：判断当前线程是否被打断，不清除打断标记&lt;/p&gt;
&lt;p&gt;打断的线程会发生上下文切换，操作系统会保存线程信息，抢占到 CPU 后会从中断的地方接着运行（打断不是停止）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sleep、wait、join 方法都会让线程进入阻塞状态，打断线程&lt;strong&gt;会清空打断状态&lt;/strong&gt;（false）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()-&amp;gt;{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, &quot;t1&quot;);
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
    System.out.println(&quot; 打断状态: {}&quot; + t1.isInterrupted());// 打断状态: {}false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打断正常运行的线程：不会清空打断状态（true）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    Thread t2 = new Thread(()-&amp;gt;{
        while(true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if(interrupted) {
                System.out.println(&quot; 打断状态: {}&quot; + interrupted);//打断状态: {}true
                break;
            }
        }
    }, &quot;t2&quot;);
    t2.start();
    Thread.sleep(500);
    t2.interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;打断 park&lt;/h5&gt;
&lt;p&gt;park 作用类似 sleep，打断 park 线程，不会清空打断状态（true）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(() -&amp;gt; {
        System.out.println(&quot;park...&quot;);
        LockSupport.park();
        System.out.println(&quot;unpark...&quot;);
        System.out.println(&quot;打断状态：&quot; + Thread.currentThread().isInterrupted());//打断状态：true
    }, &quot;t1&quot;);
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果打断标记已经是 true, 则 park 会失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LockSupport.park();
System.out.println(&quot;unpark...&quot;);
LockSupport.park();//失效，不会阻塞
System.out.println(&quot;unpark...&quot;);//和上一个unpark同时执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以修改获取打断状态方法，使用 &lt;code&gt;Thread.interrupted()&lt;/code&gt;，清除打断标记&lt;/p&gt;
&lt;p&gt;LockSupport 类在 同步 → park-un 详解&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;终止模式&lt;/h5&gt;
&lt;p&gt;终止模式之两阶段终止模式：Two Phase Termination&lt;/p&gt;
&lt;p&gt;目标：在一个线程 T1 中如何优雅终止线程 T2？优雅指的是给 T2 一个后置处理器&lt;/p&gt;
&lt;p&gt;错误思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用线程对象的 stop() 方法停止线程：stop 方法会真正杀死线程，如果这时线程锁住了共享资源，当它被杀死后就再也没有机会释放锁，其它线程将永远无法获取锁&lt;/li&gt;
&lt;li&gt;使用 System.exit(int) 方法停止线程：目的仅是停止一个线程，但这种做法会让整个程序都停止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两阶段终止模式图示：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-两阶段终止模式.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;打断线程可能在任何时间，所以需要考虑在任何时刻被打断的处理方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    private Thread monitor;
    // 启动监控线程
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
                        System.out.println(&quot;后置处理&quot;);
                        break;
                    }
                    try {
                        Thread.sleep(1000);     // 睡眠
                        System.out.println(&quot;执行监控记录&quot;); // 在此被打断不会异常
                    } catch (InterruptedException e) {  // 在睡眠期间被打断，进入异常处理的逻辑
                        e.printStackTrace();
                        // 重新设置打断标记，打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;daemon&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;public final void setDaemon(boolean on)&lt;/code&gt;：如果是 true ，将此线程标记为守护线程&lt;/p&gt;
&lt;p&gt;线程&lt;strong&gt;启动前&lt;/strong&gt;调用此方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println(&quot;running&quot;);
    }
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户线程：平常创建的普通线程&lt;/p&gt;
&lt;p&gt;守护线程：服务于用户线程，只要其它非守护线程运行结束了，即使守护线程代码没有执行完，也会强制结束。守护进程是&lt;strong&gt;脱离于终端并且在后台运行的进程&lt;/strong&gt;，脱离终端是为了避免在执行的过程中的信息在终端上显示&lt;/p&gt;
&lt;p&gt;说明：当运行的线程都是守护线程，Java 虚拟机将退出，因为普通线程执行完后，JVM 是守护线程，不会继续运行下去&lt;/p&gt;
&lt;p&gt;常见的守护线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垃圾回收器线程就是一种守护线程&lt;/li&gt;
&lt;li&gt;Tomcat 中的 Acceptor 和 Poller 线程都是守护线程，所以 Tomcat 接收到 shutdown 命令后，不会等待它们处理完当前请求&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;不推荐&lt;/h4&gt;
&lt;p&gt;不推荐使用的方法，这些方法已过时，容易破坏同步代码块，造成线程死锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void stop()&lt;/code&gt;：停止线程运行&lt;/p&gt;
&lt;p&gt;废弃原因：方法粗暴，除非可能执行 finally 代码块以及释放 synchronized 外，线程将直接被终止，如果线程持有 JUC 的互斥锁可能导致锁来不及释放，造成其他线程永远等待的局面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void suspend()&lt;/code&gt;：&lt;strong&gt;挂起（暂停）线程运行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;废弃原因：如果目标线程在暂停时对系统资源持有锁，则在目标线程恢复之前没有线程可以访问该资源，如果&lt;strong&gt;恢复目标线程的线程&lt;/strong&gt;在调用 resume 之前会尝试访问此共享资源，则会导致死锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;public final void resume()&lt;/code&gt;：恢复线程运行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;线程原理&lt;/h3&gt;
&lt;h4&gt;运行机制&lt;/h4&gt;
&lt;p&gt;Java Virtual Machine Stacks（Java 虚拟机栈）：每个线程启动后，虚拟机就会为其分配一块栈内存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个栈由多个栈帧（Frame）组成，对应着每次方法调用时所占用的内存&lt;/li&gt;
&lt;li&gt;每个线程只能有一个活动栈帧，对应着当前正在执行的那个方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程上下文切换（Thread Context Switch）：一些原因导致 CPU 不再执行当前线程，转而执行另一个线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程的 CPU 时间片用完&lt;/li&gt;
&lt;li&gt;垃圾回收&lt;/li&gt;
&lt;li&gt;有更高优先级的线程需要运行&lt;/li&gt;
&lt;li&gt;线程自己调用了 sleep、yield、wait、join、park 等方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序计数器（Program Counter Register）：记住下一条 JVM 指令的执行地址，是线程私有的&lt;/p&gt;
&lt;p&gt;当 Context Switch 发生时，需要由操作系统保存当前线程的状态（PCB 中），并恢复另一个线程的状态，包括程序计数器、虚拟机栈中每个栈帧的信息，如局部变量、操作数栈、返回地址等&lt;/p&gt;
&lt;p&gt;JVM 规范并没有限定线程模型，以 HotSopot 为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 的线程是内核级线程（1:1 线程模型），每个 Java 线程都映射到一个操作系统原生线程，需要消耗一定的内核资源（堆栈）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程的调度是在内核态运行的，而线程中的代码是在用户态运行&lt;/strong&gt;，所以线程切换（状态改变）会导致用户与内核态转换进行系统调用，这是非常消耗性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 中 main 方法启动的是一个进程也是一个主线程，main 方法里面的其他线程均为子线程，main 线程是这些线程的父线程&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;线程调度&lt;/h4&gt;
&lt;p&gt;线程调度指系统为线程分配处理器使用权的过程，方式有两种：协同式线程调度、抢占式线程调度（Java 选择）&lt;/p&gt;
&lt;p&gt;协同式线程调度：线程的执行时间由线程本身控制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：线程做完任务才通知系统切换到其他线程，相当于所有线程串行执行，不会出现线程同步问题&lt;/li&gt;
&lt;li&gt;缺点：线程执行时间不可控，如果代码编写出现问题，可能导致程序一直阻塞，引起系统的奔溃&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抢占式线程调度：线程的执行时间由系统分配&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：线程执行时间可控，不会因为一个线程的问题而导致整体系统不可用&lt;/li&gt;
&lt;li&gt;缺点：无法主动为某个线程多分配时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 提供了线程优先级的机制，优先级会提示（hint）调度器优先调度该线程，但这仅仅是一个提示，调度器可以忽略它。在线程的就绪状态时，如果 CPU 比较忙，那么优先级高的线程会获得更多的时间片，但 CPU 闲时，优先级几乎没作用&lt;/p&gt;
&lt;p&gt;说明：并不能通过优先级来判断线程执行的先后顺序&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;未来优化&lt;/h4&gt;
&lt;p&gt;内核级线程调度的成本较大，所以引入了更轻量级的协程。用户线程的调度由用户自己实现（多对一的线程模型，多&lt;strong&gt;个用户线程映射到一个内核级线程&lt;/strong&gt;），被设计为协同式调度，所以叫协程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有栈协程：协程会完整的做调用栈的保护、恢复工作，所以叫有栈协程&lt;/li&gt;
&lt;li&gt;无栈协程：本质上是一种有限状态机，状态保存在闭包里，比有栈协程更轻量，但是功能有限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有栈协程中有一种特例叫纤程，在新并发模型中，一段纤程的代码被分为两部分，执行过程和调度器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行过程：用于维护执行现场，保护、恢复上下文状态&lt;/li&gt;
&lt;li&gt;调度器：负责编排所有要执行的代码顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;线程状态&lt;/h3&gt;
&lt;p&gt;进程的状态参考操作系统：创建态、就绪态、运行态、阻塞态、终止态&lt;/p&gt;
&lt;p&gt;线程由生到死的完整过程（生命周期）：当线程被创建并启动以后，既不是一启动就进入了执行状态，也不是一直处于执行状态，在 API 中 &lt;code&gt;java.lang.Thread.State&lt;/code&gt; 这个枚举中给出了六种线程状态：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;线程状态&lt;/th&gt;
&lt;th&gt;导致状态发生条件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NEW（新建）&lt;/td&gt;
&lt;td&gt;线程刚被创建，但是并未启动，还没调用 start 方法，只有线程对象，没有线程特征&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runnable（可运行）&lt;/td&gt;
&lt;td&gt;线程可以在 Java 虚拟机中运行的状态，可能正在运行自己代码，也可能没有，这取决于操作系统处理器，调用了 t.start() 方法：就绪（经典叫法）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked（阻塞）&lt;/td&gt;
&lt;td&gt;当一个线程试图获取一个对象锁，而该对象锁被其他的线程持有，则该线程进入 Blocked 状态；当该线程持有锁时，该线程将变成 Runnable 状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting（无限等待）&lt;/td&gt;
&lt;td&gt;一个线程在等待另一个线程执行一个（唤醒）动作时，该线程进入 Waiting 状态，进入这个状态后不能自动唤醒，必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timed Waiting （限期等待）&lt;/td&gt;
&lt;td&gt;有几个方法有超时参数，调用将进入 Timed Waiting 状态，这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teminated（结束）&lt;/td&gt;
&lt;td&gt;run 方法正常退出而死亡，或者因为没有捕获的异常终止了 run 方法而死亡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B6%E7%A7%8D%E7%8A%B6%E6%80%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NEW → RUNNABLE：当调用 t.start() 方法时，由 NEW → RUNNABLE&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; WAITING：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 obj.wait() 方法时&lt;/p&gt;
&lt;p&gt;调用 obj.notify()、obj.notifyAll()、t.interrupt()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;竞争锁成功，t 线程从 WAITING → RUNNABLE&lt;/li&gt;
&lt;li&gt;竞争锁失败，t 线程从 WAITING → BLOCKED&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前线程调用 t.join() 方法，注意是当前线程在 t 线程对象的监视器上等待&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前线程调用 LockSupport.park() 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; TIMED_WAITING：调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RUNNABLE &amp;lt;--&amp;gt; BLOCKED：t 线程用 synchronized(obj) 获取了对象锁时竞争失败&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;查看线程&lt;/h3&gt;
&lt;p&gt;Windows：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务管理器可以查看进程和线程数，也可以用来杀死进程&lt;/li&gt;
&lt;li&gt;tasklist 查看进程&lt;/li&gt;
&lt;li&gt;taskkill 杀死进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ps -ef 查看所有进程&lt;/li&gt;
&lt;li&gt;ps -fT -p &amp;lt;PID&amp;gt; 查看某个进程（PID）的所有线程&lt;/li&gt;
&lt;li&gt;kill 杀死进程&lt;/li&gt;
&lt;li&gt;top 按大写 H 切换是否显示线程&lt;/li&gt;
&lt;li&gt;top -H -p &amp;lt;PID&amp;gt; 查看某个进程（PID）的所有线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jps 命令查看所有 Java 进程&lt;/li&gt;
&lt;li&gt;jstack &amp;lt;PID&amp;gt; 查看某个 Java 进程（PID）的所有线程状态&lt;/li&gt;
&lt;li&gt;jconsole 来查看某个 Java 进程中线程的运行情况（图形界面）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;同步&lt;/h2&gt;
&lt;h3&gt;临界区&lt;/h3&gt;
&lt;p&gt;临界资源：一次仅允许一个进程使用的资源成为临界资源&lt;/p&gt;
&lt;p&gt;临界区：访问临界资源的代码块&lt;/p&gt;
&lt;p&gt;竞态条件：多个线程在临界区内执行，由于代码的执行序列不同而导致结果无法预测，称之为发生了竞态条件&lt;/p&gt;
&lt;p&gt;一个程序运行多个线程是没有问题，多个线程读共享资源也没有问题，在多个线程对共享资源读写操作时发生指令交错，就会出现问题&lt;/p&gt;
&lt;p&gt;为了避免临界区的竞态条件发生（解决线程安全问题）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞式的解决方案：synchronized，lock&lt;/li&gt;
&lt;li&gt;非阻塞式的解决方案：原子变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;管程（monitor）：由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块，保证同一时刻只有一个进程在管程内活动，即管程内定义的操作在同一时刻只被一个进程调用（由编译器实现）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;synchronized：对象锁，保证了临界区内代码的原子性&lt;/strong&gt;，采用互斥的方式让同一时刻至多只有一个线程能持有对象锁，其它线程获取这个对象锁时会阻塞，保证拥有锁的线程可以安全的执行临界区内的代码，不用担心线程上下文切换&lt;/p&gt;
&lt;p&gt;互斥和同步都可以采用 synchronized 关键字来完成，区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥是保证临界区的竞态条件发生，同一时刻只能有一个线程执行临界区代码&lt;/li&gt;
&lt;li&gt;同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程安全，性能差&lt;/li&gt;
&lt;li&gt;线程不安全性能好，假如开发中不会存在多线程安全问题，建议使用线程不安全的设计类&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;syn-ed&lt;/h3&gt;
&lt;h4&gt;使用锁&lt;/h4&gt;
&lt;h5&gt;同步块&lt;/h5&gt;
&lt;p&gt;锁对象：理论上可以是&lt;strong&gt;任意的唯一对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized 是可重入、不公平的重量级锁&lt;/p&gt;
&lt;p&gt;原则上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁对象建议使用共享资源&lt;/li&gt;
&lt;li&gt;在实例方法中使用 this 作为锁对象，锁住的 this 正好是共享资源&lt;/li&gt;
&lt;li&gt;在静态方法中使用类名 .class 字节码作为锁对象，因为静态成员属于类，被所有实例对象共享，所以需要锁住类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步代码块格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized(锁对象){
 // 访问共享资源的核心代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class demo {
    static int counter = 0;
    //static修饰，则元素是属于类本身的，不属于对象  ，与类一起加载一次，只有一个
    static final Object room = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (room) {
                    counter++;
                }
            }
        }, &quot;t1&quot;);
        Thread t2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5000; i++) {
                synchronized (room) {
                    counter--;
                }
            }
        }, &quot;t2&quot;);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;同步方法&lt;/h5&gt;
&lt;p&gt;把出现线程安全问题的核心方法锁起来，每次只能一个线程进入访问&lt;/p&gt;
&lt;p&gt;synchronized 修饰的方法的不具备继承性，所以子类是线程不安全的，如果子类的方法也被 synchronized 修饰，两个锁对象其实是一把锁，而且是&lt;strong&gt;子类对象作为锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用法：直接给方法加上一个修饰符 synchronized&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) { 
 方法体；
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) { 
 方法体；
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步方法底层也是有锁对象的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果方法是实例方法：同步方法默认用 this 作为的锁对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public synchronized void test() {} //等价于
public void test() {
    synchronized(this) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果方法是静态方法：同步方法默认用类名 .class 作为的锁对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test{
 public synchronized static void test() {}
}
//等价于
class Test{
    public void test() {
        synchronized(Test.class) {}
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;线程八锁&lt;/h5&gt;
&lt;p&gt;线程八锁就是考察 synchronized 锁住的是哪个对象，直接百度搜索相关的实例&lt;/p&gt;
&lt;p&gt;说明：主要关注锁住的对象是不是同一个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁住类对象，所有类的实例的方法都是安全的，类的所有实例都相当于同一把锁&lt;/li&gt;
&lt;li&gt;锁住 this 对象，只有在当前实例对象的线程内是安全的，如果有多个实例就不安全&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程不安全：因为锁住的不是同一个对象，线程 1 调用 a 方法锁住的类对象，线程 2 调用 b 方法锁住的 n2 对象，不是同一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Number{
    public static synchronized void a(){
  Thread.sleep(1000);
        System.out.println(&quot;1&quot;);
    }
    public synchronized void b() {
        System.out.println(&quot;2&quot;);
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()-&amp;gt;{ n1.a(); }).start();
    new Thread(()-&amp;gt;{ n2.b(); }).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程安全：因为 n1 调用 a() 方法，锁住的是类对象，n2 调用 b() 方法，锁住的也是类对象，所以线程安全&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Number{
    public static synchronized void a(){
  Thread.sleep(1000);
        System.out.println(&quot;1&quot;);
    }
    public static synchronized void b() {
        System.out.println(&quot;2&quot;);
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()-&amp;gt;{ n1.a(); }).start();
    new Thread(()-&amp;gt;{ n2.b(); }).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;锁原理&lt;/h4&gt;
&lt;h5&gt;Monitor&lt;/h5&gt;
&lt;p&gt;Monitor 被翻译为监视器或管程&lt;/p&gt;
&lt;p&gt;每个 Java 对象都可以关联一个 Monitor 对象，Monitor 也是 class，其&lt;strong&gt;实例存储在堆中&lt;/strong&gt;，如果使用 synchronized 给对象上锁（重量级）之后，该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针，这就是重量级锁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mark Word 结构：最后两位是&lt;strong&gt;锁标志位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord%E7%BB%93%E6%9E%8432%E4%BD%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;64 位虚拟机 Mark Word：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord%E7%BB%93%E6%9E%8464%E4%BD%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开始时 Monitor 中 Owner 为 null&lt;/li&gt;
&lt;li&gt;当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2，Monitor 中只能有一个 Owner，&lt;strong&gt;obj 对象的 Mark Word 指向 Monitor&lt;/strong&gt;，把&lt;strong&gt;对象原有的 MarkWord 存入线程栈中的锁记录&lt;/strong&gt;中（轻量级锁部分详解）
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理1.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/li&gt;
&lt;li&gt;在 Thread-2 上锁的过程，Thread-3、Thread-4、Thread-5 也执行 synchronized(obj)，就会进入 EntryList BLOCKED（双向链表）&lt;/li&gt;
&lt;li&gt;Thread-2 执行完同步代码块的内容，根据 obj 对象头中 Monitor 地址寻找，设置 Owner 为空，把线程栈的锁记录中的对象头的值设置回 MarkWord&lt;/li&gt;
&lt;li&gt;唤醒 EntryList 中等待的线程来竞争锁，竞争是&lt;strong&gt;非公平的&lt;/strong&gt;，如果这时有新的线程想要获取锁，可能直接就抢占到了，阻塞队列的线程就会继续阻塞&lt;/li&gt;
&lt;li&gt;WaitSet 中的 Thread-0，是以前获得过锁，但条件不满足进入 WAITING 状态的线程（wait-notify 机制）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 必须是进入同一个对象的 Monitor 才有上述的效果&lt;/li&gt;
&lt;li&gt;不加 synchronized 的对象不会关联监视器，不遵从以上规则&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;字节码&lt;/h5&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println(&quot;ok&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0:  new    #2  // new Object
3:  dup
4:  invokespecial  #1   // invokespecial &amp;lt;init&amp;gt;:()V，非虚方法
7:  astore_1     // lock引用 -&amp;gt; lock
8:  aload_1     // lock （synchronized开始）
9:  dup      // 一份用来初始化，一份用来引用
10: astore_2     // lock引用 -&amp;gt; slot 2
11: monitorenter    // 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic   #3  // System.out
15: ldc    #4  // &quot;ok&quot;
17: invokevirtual  #5   // invokevirtual println:(Ljava/lang/String;)V
20: aload_2     // slot 2(lock引用)
21: monitorexit    // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3     // any -&amp;gt; slot 3
26: aload_2     // slot 2(lock引用)
27: monitorexit    // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
    from to target type
      12 22 25   any
      25 28 25   any
LineNumberTable: ...
LocalVariableTable:
    Start Length Slot Name Signature
     0  31   0 args [Ljava/lang/String;
     8  23   1 lock Ljava/lang/Object;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过异常 &lt;strong&gt;try-catch 机制&lt;/strong&gt;，确保一定会被解锁&lt;/li&gt;
&lt;li&gt;方法级别的 synchronized 不会在字节码指令中有所体现&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁升级&lt;/h4&gt;
&lt;h5&gt;升级过程&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;synchronized 是可重入、不公平的重量级锁&lt;/strong&gt;，所以可以对其进行优化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无锁 -&amp;gt; 偏向锁 -&amp;gt; 轻量级锁 -&amp;gt; 重量级锁 // 随着竞争的增加，只能锁升级，不能降级
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%94%81%E5%8D%87%E7%BA%A7%E8%BF%87%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;偏向锁&lt;/h5&gt;
&lt;p&gt;偏向锁的思想是偏向于让第一个获取锁对象的线程，这个线程之后重新获取该锁不再需要同步操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当锁对象第一次被线程获得的时候进入偏向状态，标记为 101，同时&lt;strong&gt;使用 CAS 操作将线程 ID 记录到 Mark Word&lt;/strong&gt;。如果 CAS 操作成功，这个线程以后进入这个锁相关的同步块，查看这个线程 ID 是自己的就表示没有竞争，就不需要再进行任何同步操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当有另外一个线程去尝试获取这个锁对象时，偏向状态就宣告结束，此时撤销偏向（Revoke Bias）后恢复到未锁定或轻量级锁状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构64位.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;一个对象创建时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果开启了偏向锁（默认开启），那么对象创建后，MarkWord 值为 0x05 即最后 3 位为 101，thread、epoch、age 都为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;偏向锁是默认是延迟的，不会在程序启动时立即生效，如果想避免延迟，可以加 VM 参数 &lt;code&gt;-XX:BiasedLockingStartupDelay=0&lt;/code&gt; 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因：在刚开始执行代码时，会有好多线程来抢锁，如果开偏向锁效率反而降低&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个对象已经计算过 hashCode，就再也无法进入偏向状态了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 VM 参数 &lt;code&gt;-XX:-UseBiasedLocking&lt;/code&gt; 禁用偏向锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;撤销偏向锁的状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用对象的 hashCode：偏向锁的对象 MarkWord 中存储的是线程 id，调用 hashCode 导致偏向锁被撤销&lt;/li&gt;
&lt;li&gt;当有其它线程使用偏向锁对象时，会将偏向锁升级为轻量级锁&lt;/li&gt;
&lt;li&gt;调用 wait/notify，需要申请 Monitor，进入 WaitSet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;批量撤销&lt;/strong&gt;：如果对象被多个线程访问，但没有竞争，这时偏向了线程 T1 的对象仍有机会重新偏向 T2，重偏向会重置对象的 Thread ID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;批量重偏向：当撤销偏向锁阈值超过 20 次后，JVM 会觉得是不是偏向错了，于是在给这些对象加锁时重新偏向至加锁线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;批量撤销：当撤销偏向锁阈值超过 40 次后，JVM 会觉得自己确实偏向错了，根本就不该偏向，于是整个类的所有对象都会变为不可偏向的，新建的对象也是不可偏向的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;轻量级锁&lt;/h5&gt;
&lt;p&gt;一个对象有多个线程要加锁，但加锁的时间是错开的（没有竞争），可以使用轻量级锁来优化，轻量级锁对使用者是透明的（不可见）&lt;/p&gt;
&lt;p&gt;可重入锁：线程可以进入任何一个它已经拥有的锁所同步着的代码块，可重入锁最大的作用是&lt;strong&gt;避免死锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;轻量级锁在没有竞争时（锁重入时），每次重入仍然需要执行 CAS 操作，Java 6 才引入的偏向锁来优化&lt;/p&gt;
&lt;p&gt;锁重入实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
     // 同步块 B
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建锁记录（Lock Record）对象，每个线程的&lt;strong&gt;栈帧&lt;/strong&gt;都会包含一个锁记录的结构，存储锁定对象的 Mark Word&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让锁记录中 Object reference 指向锁住的对象，并尝试用 CAS 替换 Object 的 Mark Word，将 Mark Word 的值存入锁记录&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 替换成功，对象头中存储了锁记录地址和状态 00（轻量级锁） ，表示由该线程给对象加锁
&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 CAS 失败，有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是其它线程已经持有了该 Object 的轻量级锁，这时表明有竞争，进入锁膨胀过程&lt;/li&gt;
&lt;li&gt;如果是线程自己执行了 synchronized 锁重入，就添加一条 Lock Record 作为重入的计数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%863.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当退出 synchronized 代码块（解锁时）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有取值为 null 的锁记录，表示有重入，这时重置锁记录，表示重入计数减 1&lt;/li&gt;
&lt;li&gt;如果锁记录的值不为 null，这时使用 CAS &lt;strong&gt;将 Mark Word 的值恢复给对象头&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;成功，则解锁成功&lt;/li&gt;
&lt;li&gt;失败，说明轻量级锁进行了锁膨胀或已经升级为重量级锁，进入重量级锁解锁流程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;锁膨胀&lt;/h5&gt;
&lt;p&gt;在尝试加轻量级锁的过程中，CAS 操作无法成功，可能是其它线程为此对象加上了轻量级锁（有竞争），这时需要进行锁膨胀，将轻量级锁变为&lt;strong&gt;重量级锁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当 Thread-1 进行轻量级加锁时，Thread-0 已经对该对象加了轻量级锁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Thread-1 加轻量级锁失败，进入锁膨胀流程：为 Object 对象申请 Monitor 锁，&lt;strong&gt;通过 Object 对象头获取到持锁线程&lt;/strong&gt;，将 Monitor 的 Owner 置为 Thread-0，将 Object 的对象头指向重量级锁地址，然后自己进入 Monitor 的 EntryList BLOCKED&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 Thread-0 退出同步块解锁时，使用 CAS 将 Mark Word 的值恢复给对象头失败，这时进入重量级解锁流程，即按照 Monitor 地址找到 Monitor 对象，设置 Owner 为 null，唤醒 EntryList 中 BLOCKED 线程&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;锁优化&lt;/h4&gt;
&lt;h5&gt;自旋锁&lt;/h5&gt;
&lt;p&gt;重量级锁竞争时，尝试获取锁的线程不会立即阻塞，可以使用&lt;strong&gt;自旋&lt;/strong&gt;（默认 10 次）来进行优化，采用循环的方式去尝试获取锁&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自旋占用 CPU 时间，单核 CPU 自旋就是浪费时间，因为同一时刻只能运行一个线程，多核 CPU 自旋才能发挥优势&lt;/li&gt;
&lt;li&gt;自旋失败的线程会进入阻塞状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：不会进入阻塞状态，&lt;strong&gt;减少线程上下文切换的消耗&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缺点：当自旋的线程越来越多时，会不断的消耗 CPU 资源&lt;/p&gt;
&lt;p&gt;自旋锁情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自旋成功的情况：
&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-自旋成功.png&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自旋失败的情况：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-自旋失败.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自旋锁说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Java 6 之后自旋锁是自适应的，比如对象刚刚的一次自旋操作成功过，那么认为这次自旋成功的可能性会高，就多自旋几次；反之，就少自旋甚至不自旋，比较智能&lt;/li&gt;
&lt;li&gt;Java 7 之后不能控制是否开启自旋功能，由 JVM 控制&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//手写自旋锁
public class SpinLock {
    // 泛型装的是Thread，原子引用线程
    AtomicReference&amp;lt;Thread&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + &quot; come in&quot;);

        //开始自旋，期望值为null，更新值是当前线程
        while (!atomicReference.compareAndSet(null, thread)) {
            Thread.sleep(1000);
            System.out.println(thread.getName() + &quot; 正在自旋&quot;);
        }
        System.out.println(thread.getName() + &quot; 自旋成功&quot;);
    }

    public void unlock() {
        Thread thread = Thread.currentThread();

        //线程使用完锁把引用变为null
  atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + &quot; invoke unlock&quot;);
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        new Thread(() -&amp;gt; {
            //占有锁
            lock.lock();
            Thread.sleep(10000); 

            //释放锁
            lock.unlock();
        },&quot;t1&quot;).start();

        // 让main线程暂停1秒，使得t1线程，先执行
        Thread.sleep(1000);

        new Thread(() -&amp;gt; {
            lock.lock();
            lock.unlock();
        },&quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;锁消除&lt;/h5&gt;
&lt;p&gt;锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除，这是 JVM &lt;strong&gt;即时编译器的优化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;锁消除主要是通过&lt;strong&gt;逃逸分析&lt;/strong&gt;来支持，如果堆上的共享数据不可能逃逸出去被其它线程访问到，那么就可以把它们当成私有数据对待，也就可以将它们的锁进行消除（同步消除：JVM 逃逸分析）&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;锁粗化&lt;/h5&gt;
&lt;p&gt;对相同对象多次加锁，导致线程发生多次重入，频繁的加锁操作就会导致性能损耗，可以使用锁粗化方式优化&lt;/p&gt;
&lt;p&gt;如果虚拟机探测到一串的操作都对同一个对象加锁，将会把加锁的范围扩展（粗化）到整个操作序列的外部&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一些看起来没有加锁的代码，其实隐式的加了很多锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;String 是一个不可变的类，编译器会对 String 的拼接自动优化。在 JDK 1.5 之前，转化为 StringBuffer 对象的连续 append() 操作，每个 append() 方法中都有一个同步块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩展到第一个 append() 操作之前直至最后一个 append() 操作之后，只需要加锁一次就可以&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;多把锁&lt;/h4&gt;
&lt;p&gt;多把不相干的锁：一间大屋子有两个功能睡觉、学习，互不相干。现在一人要学习，一人要睡觉，如果只用一间屋子（一个对象锁）的话，那么并发度很低&lt;/p&gt;
&lt;p&gt;将锁的粒度细分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处，是可以增强并发度&lt;/li&gt;
&lt;li&gt;坏处，如果一个线程需要同时获得多把锁，就容易发生死锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：准备多个对象锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    BigRoom bigRoom = new BigRoom();
    new Thread(() -&amp;gt; { bigRoom.study(); }).start();
    new Thread(() -&amp;gt; { bigRoom.sleep(); }).start();
}
class BigRoom {
    private final Object studyRoom = new Object();
    private final Object sleepRoom = new Object();

    public void sleep() throws InterruptedException {
        synchronized (sleepRoom) {
            System.out.println(&quot;sleeping 2 小时&quot;);
            Thread.sleep(2000);
        }
    }

    public void study() throws InterruptedException {
        synchronized (studyRoom) {
            System.out.println(&quot;study 1 小时&quot;);
            Thread.sleep(1000);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;活跃性&lt;/h4&gt;
&lt;h5&gt;死锁&lt;/h5&gt;
&lt;h6&gt;形成&lt;/h6&gt;
&lt;p&gt;死锁：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放，由于线程被无限期地阻塞，因此程序不可能正常终止&lt;/p&gt;
&lt;p&gt;Java 死锁产生的四个必要条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;互斥条件，即当资源被一个线程使用（占有）时，别的线程不能使用&lt;/li&gt;
&lt;li&gt;不可剥夺条件，资源请求者不能强制从资源占有者手中夺取资源，资源只能由资源占有者主动释放&lt;/li&gt;
&lt;li&gt;请求和保持条件，即当资源请求者在请求其他的资源的同时保持对原有资源的占有&lt;/li&gt;
&lt;li&gt;循环等待条件，即存在一个等待循环队列：p1 要 p2 的资源，p2 要 p1 的资源，形成了一个等待环路&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;四个条件都成立的时候，便形成死锁。死锁情况下打破上述任何一个条件，便可让死锁消失&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Dead {
    public static Object resources1 = new Object();
    public static Object resources2 = new Object();
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            // 线程1：占用资源1 ，请求资源2
            synchronized(resources1){
                System.out.println(&quot;线程1已经占用了资源1，开始请求资源2&quot;);
                Thread.sleep(2000);//休息两秒，防止线程1直接运行完成。
                //2秒内线程2肯定可以锁住资源2
                synchronized (resources2){
                    System.out.println(&quot;线程1已经占用了资源2&quot;);
                }
        }).start();
        new Thread(() -&amp;gt; {
            // 线程2：占用资源2 ，请求资源1
            synchronized(resources2){
                System.out.println(&quot;线程2已经占用了资源2，开始请求资源1&quot;);
                Thread.sleep(2000);
                synchronized (resources1){
                    System.out.println(&quot;线程2已经占用了资源1&quot;);
                }
            }}
        }).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h6&gt;定位&lt;/h6&gt;
&lt;p&gt;定位死锁的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 jps 定位进程 id，再用 &lt;code&gt;jstack id&lt;/code&gt; 定位死锁，找到死锁的线程去查看源码，解决优化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Thread-1&quot; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000]
 java.lang.Thread.State: BLOCKED (on object monitor)
#省略    
&quot;Thread-1&quot; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000]
 java.lang.Thread.State: BLOCKED (on object monitor)
#省略

Found one Java-level deadlock:
===================================================
&quot;Thread-1&quot;:
    waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
    which is held by &quot;Thread-0&quot;
&quot;Thread-0&quot;:
    waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
    which is held by &quot;Thread-1&quot;
    
Java stack information for the threads listed above:
===================================================
&quot;Thread-1&quot;:
    at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
    - waiting to lock &amp;lt;0x000000076b5bf1c0&amp;gt; (a java.lang.Object)
    - locked &amp;lt;0x000000076b5bf1d0&amp;gt; (a java.lang.Object)
    at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)
&quot;Thread-0&quot;:
    at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
    - waiting to lock &amp;lt;0x000000076b5bf1d0&amp;gt; (a java.lang.Object)
    - locked &amp;lt;0x000000076b5bf1c0&amp;gt; (a java.lang.Object)
    at thread.TestDeadLock$$Lambda$1/495053715
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程，再利用 &lt;code&gt;top -Hp 进程id&lt;/code&gt; 来定位是哪个线程，最后再用 jstack &amp;lt;pid&amp;gt;的输出来看各个线程栈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免死锁：避免死锁要注意加锁顺序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用 jconsole 工具，在 &lt;code&gt;jdk\bin&lt;/code&gt; 目录下&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;活锁&lt;/h5&gt;
&lt;p&gt;活锁：指的是任务或者执行者没有被阻塞，由于某些条件没有满足，导致一直重复尝试—失败—尝试—失败的过程&lt;/p&gt;
&lt;p&gt;两个线程互相改变对方的结束条件，最后谁也无法结束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            // 期望减到 0 退出循环
            while (count &amp;gt; 0) {
                Thread.sleep(200);
                count--;
                System.out.println(&quot;线程一count:&quot; + count);
            }
        }, &quot;t1&quot;).start();
        new Thread(() -&amp;gt; {
            // 期望超过 20 退出循环
            while (count &amp;lt; 20) {
                Thread.sleep(200);
                count++;
                System.out.println(&quot;线程二count:&quot;+ count);
            }
        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;饥饿&lt;/h5&gt;
&lt;p&gt;饥饿：一个线程由于优先级太低，始终得不到 CPU 调度执行，也不能够结束&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;wait-ify&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;p&gt;需要获取对象锁后才可以调用 &lt;code&gt;锁对象.wait()&lt;/code&gt;，notify 随机唤醒一个线程，notifyAll 唤醒所有线程去竞争 CPU&lt;/p&gt;
&lt;p&gt;Object 类 API：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待，直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待，或是被唤醒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;strong&gt;wait 是挂起线程，需要唤醒的都是挂起操作&lt;/strong&gt;，阻塞线程可以自己去争抢锁，挂起的线程需要唤醒后去争抢锁&lt;/p&gt;
&lt;p&gt;对比 sleep()：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理不同：sleep() 方法是属于 Thread 类，是线程用来控制自身流程的，使此线程暂停执行一段时间而把执行机会让给其他线程；wait() 方法属于 Object 类，用于线程间通信&lt;/li&gt;
&lt;li&gt;对&lt;strong&gt;锁的处理机制&lt;/strong&gt;不同：调用 sleep() 方法的过程中，线程不会释放对象锁，当调用 wait() 方法的时候，线程会放弃对象锁，进入等待此对象的等待锁定池（不释放锁其他线程怎么抢占到锁执行唤醒操作），但是都会释放 CPU&lt;/li&gt;
&lt;li&gt;使用区域不同：wait() 方法必须放在**同步控制方法和同步代码块（先获取锁）**中使用，sleep() 方法则可以放在任何地方使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;底层原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owner 线程发现条件不满足，调用 wait 方法，即可进入 WaitSet 变为 WAITING 状态&lt;/li&gt;
&lt;li&gt;BLOCKED 和 WAITING 的线程都处于阻塞状态，不占用 CPU 时间片&lt;/li&gt;
&lt;li&gt;BLOCKED 线程会在 Owner 线程释放锁时唤醒&lt;/li&gt;
&lt;li&gt;WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒，唤醒后并不意味者立刻获得锁，&lt;strong&gt;需要进入 EntryList 重新竞争&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用wait和notify编写生产者消费者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cn.meowrain;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

public class WaitNotifyExample {
    public static void main(String[] args) {
        SharedQueue sharedQueue = new SharedQueue(100);
        Producer producer = new Producer(sharedQueue);
        Consumer consumer = new Consumer(sharedQueue);
        new Thread(producer,&quot;producer&quot;).start();
        new Thread(consumer,&quot;consumer&quot;).start();
    }
}

class SharedQueue {
    private final Queue&amp;lt;Integer&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
    private final int maxSize;

    public SharedQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    public synchronized void produce(int value) throws InterruptedException {
        //如果队列满了，生产者等待
        while (queue.size() == maxSize) {
            System.out.println(Thread.currentThread().getName() + &quot;:Queue is full,waiting for consumer to consume...&quot;);
            wait();
        }
        queue.add(value);
        System.out.println(Thread.currentThread().getName() + &quot;: Produced &quot; + value + &quot;, Queue size: &quot; + queue.size());
        notify();
    }

    public synchronized void consume() throws InterruptedException {
        while (queue.isEmpty()) {
            System.out.println(Thread.currentThread().getName() + &quot;:Queue is empty,waiting for producer to produce...&quot;);
            wait();
        }
        int value = queue.poll();
        System.out.println(Thread.currentThread().getName() + &quot;: Consumed &quot; + value + &quot;, Queue size: &quot; + queue.size());
        notify();

    }

}

class Producer implements Runnable {
    private final SharedQueue sharedQueue;

    public Producer(SharedQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i &amp;lt;= 100; i++) {
                sharedQueue.produce(i);
                TimeUnit.MILLISECONDS.sleep(500);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

class Consumer implements Runnable {
    private final SharedQueue sharedQueue;

    public Consumer(SharedQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i &amp;lt;= 100; i++) {
                sharedQueue.consume();
                TimeUnit.MILLISECONDS.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;代码优化&lt;/h4&gt;
&lt;p&gt;虚假唤醒：notify 只能随机唤醒一个 WaitSet 中的线程，这时如果有其它线程也在等待，那么就可能唤醒不了正确的线程&lt;/p&gt;
&lt;p&gt;解决方法：采用 notifyAll&lt;/p&gt;
&lt;p&gt;notifyAll 仅解决某个线程的唤醒问题，使用 if + wait 判断仅有一次机会，一旦条件不成立，无法重新判断&lt;/p&gt;
&lt;p&gt;解决方法：用 while + wait，当条件不成立，再次 wait&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j(topic = &quot;c.demo&quot;)
public class demo {
    static final Object room = new Object();
    static boolean hasCigarette = false;    //有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -&amp;gt; {
            synchronized (room) {
                log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
                while (!hasCigarette) {//while防止虚假唤醒
                    log.debug(&quot;没烟，先歇会！&quot;);
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug(&quot;有烟没？[{}]&quot;, hasCigarette);
                if (hasCigarette) {
                    log.debug(&quot;可以开始干活了&quot;);
                } else {
                    log.debug(&quot;没干成活...&quot;);
                }
            }
        }, &quot;小南&quot;).start();

        new Thread(() -&amp;gt; {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
                if (!hasTakeout) {
                    log.debug(&quot;没外卖，先歇会！&quot;);
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug(&quot;外卖送到没？[{}]&quot;, hasTakeout);
                if (hasTakeout) {
                    log.debug(&quot;可以开始干活了&quot;);
                } else {
                    log.debug(&quot;没干成活...&quot;);
                }
            }
        }, &quot;小女&quot;).start();


        Thread.sleep(1000);
        new Thread(() -&amp;gt; {
        // 这里能不能加 synchronized (room)？
            synchronized (room) {
                hasTakeout = true;
    //log.debug(&quot;烟到了噢！&quot;);
                log.debug(&quot;外卖到了噢！&quot;);
                room.notifyAll();
            }
        }, &quot;送外卖的&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;park-un&lt;/h3&gt;
&lt;p&gt;LockSupport 是用来创建锁和其他同步类的&lt;strong&gt;线程原语&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LockSupport 类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LockSupport.park()&lt;/code&gt;：暂停当前线程，挂起原语&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LockSupport.unpark(暂停的线程对象)&lt;/code&gt;：恢复某个线程的运行&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    Thread t1 = new Thread(() -&amp;gt; {
        System.out.println(&quot;start...&quot;); //1
  Thread.sleep(1000);// Thread.sleep(3000)
        // 先 park 再 unpark 和先 unpark 再 park 效果一样，都会直接恢复线程的运行
        System.out.println(&quot;park...&quot;); //2
        LockSupport.park();
        System.out.println(&quot;resume...&quot;);//4
    },&quot;t1&quot;);
    t1.start();
    Thread.sleep(2000);
    System.out.println(&quot;unpark...&quot;); //3
    LockSupport.unpark(t1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LockSupport 出现就是为了增强 wait &amp;amp; notify 的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wait，notify 和 notifyAll 必须配合 Object Monitor 一起使用，而 park、unpark 不需要&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark &lt;strong&gt;以线程为单位&lt;/strong&gt;来阻塞和唤醒线程，而 notify 只能随机唤醒一个等待线程，notifyAll 是唤醒所有等待线程&lt;/li&gt;
&lt;li&gt;park &amp;amp; unpark 可以先 unpark，而 wait &amp;amp; notify 不能先 notify。类比生产消费，先消费发现有产品就消费，没有就等待；先生产就直接产生商品，然后线程直接消费&lt;/li&gt;
&lt;li&gt;wait 会释放锁资源进入等待队列，&lt;strong&gt;park 不会释放锁资源&lt;/strong&gt;，只负责阻塞当前线程，会释放 CPU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理：类似生产者消费者&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先 park：
&lt;ol&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 0，这时获得_mutex 互斥锁&lt;/li&gt;
&lt;li&gt;线程进入 _cond 条件变量挂起&lt;/li&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置_counter 为 1&lt;/li&gt;
&lt;li&gt;唤醒 _cond 条件变量中的 Thread_0，Thread_0 恢复运行，设置_counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park%E5%8E%9F%E7%90%861.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;先 unpark：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 Unsafe.unpark(Thread_0) 方法，设置_counter 为 1&lt;/li&gt;
&lt;li&gt;当前线程调用 Unsafe.park() 方法&lt;/li&gt;
&lt;li&gt;检查 _counter ，本情况为 1，这时线程无需挂起，继续运行，设置_counter 为 0&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park%E5%8E%9F%E7%90%862.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;安全分析&lt;/h3&gt;
&lt;p&gt;成员变量和静态变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果它们没有共享，则线程安全&lt;/li&gt;
&lt;li&gt;如果它们被共享了，根据它们的状态是否能够改变，分两种情况：
&lt;ul&gt;
&lt;li&gt;如果只有读操作，则线程安全&lt;/li&gt;
&lt;li&gt;如果有读写操作，则这段代码是临界区，需要考虑线程安全问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量是线程安全的&lt;/li&gt;
&lt;li&gt;局部变量引用的对象不一定线程安全（逃逸分析）：
&lt;ul&gt;
&lt;li&gt;如果该对象没有逃离方法的作用访问，它是线程安全的（每一个方法有一个栈帧）&lt;/li&gt;
&lt;li&gt;如果该对象逃离方法的作用范围，需要考虑线程安全问题（暴露引用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见线程安全类：String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程安全的是指，多个线程调用它们同一个实例的某个方法时，是线程安全的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;每个方法是原子的，但多个方法的组合不是原子的&lt;/strong&gt;，只能保证调用的方法内部安全：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hashtable table = new Hashtable();
// 线程1，线程2
if(table.get(&quot;key&quot;) == null) {
 table.put(&quot;key&quot;, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无状态类线程安全，就是没有成员变量的类&lt;/p&gt;
&lt;p&gt;不可变类线程安全：String、Integer 等都是不可变类，&lt;strong&gt;内部的状态不可以改变&lt;/strong&gt;，所以方法是线程安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;replace 等方法底层是新建一个对象，复制过去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;(); // 线程不安全
String S1 = &quot;...&quot;;       // 线程安全
final String S2 = &quot;...&quot;;     // 线程安全
Date D1 = new Date();      // 线程不安全
final Date D2 = new Date();     // 线程不安全，final让D2引用的对象不能变，但对象的内容可以变
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抽象方法如果有参数，被重写后行为不确定可能造成线程不安全，被称之为外星方法：&lt;code&gt;public abstract foo(Student s);&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;同步模式&lt;/h3&gt;
&lt;h4&gt;保护性暂停&lt;/h4&gt;
&lt;h5&gt;单任务版&lt;/h5&gt;
&lt;p&gt;Guarded Suspension，用在一个线程等待另一个线程的执行结果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一个结果需要从一个线程传递到另一个线程，让它们关联同一个 GuardedObject&lt;/li&gt;
&lt;li&gt;如果有结果不断从一个线程到另一个线程那么可以使用消息队列（见生产者/消费者）&lt;/li&gt;
&lt;li&gt;JDK 中，join 的实现、Future 的实现，采用的就是此模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BF%9D%E6%8A%A4%E6%80%A7%E6%9A%82%E5%81%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    GuardedObject object = new GuardedObjectV2();
    new Thread(() -&amp;gt; {
        sleep(1);
        object.complete(Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;));
    }).start();
    
    Object response = object.get(2500);
    if (response != null) {
        log.debug(&quot;get response: [{}] lines&quot;, ((List&amp;lt;String&amp;gt;) response).size());
    } else {
        log.debug(&quot;can&apos;t get response&quot;);
    }
}

class GuardedObject {
    private Object response;
    private final Object lock = new Object();

    //获取结果
    //timeout :最大等待时间
    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4) 假设 millis 是 1000，结果在 400 时唤醒了，那么还有 600 要等
                long waitTime = millis - timePassed;
                log.debug(&quot;waitTime: {}&quot;, waitTime);
                //经历时间超过最大等待时间退出循环
                if (waitTime &amp;lt;= 0) {
                    log.debug(&quot;break...&quot;);
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒，这时已经经历的时间假设为 400
                timePassed = System.currentTimeMillis() - begin;
                log.debug(&quot;timePassed: {}, object is null {}&quot;,
                        timePassed, response == null);
            }
            return response;
        }
    }

    //产生结果
    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足，通知等待线程
            this.response = response;
            log.debug(&quot;notify...&quot;);
            lock.notifyAll();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;多任务版&lt;/h5&gt;
&lt;p&gt;多任务版保护性暂停：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BF%9D%E6%8A%A4%E6%80%A7%E6%9A%82%E5%81%9C%E5%A4%9A%E4%BB%BB%E5%8A%A1%E7%89%88.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i &amp;lt; 3; i++) {
        new People().start();
    }
    Thread.sleep(1000);
    for (Integer id : Mailboxes.getIds()) {
        new Postman(id, id + &quot;号快递到了&quot;).start();
    }
}

@Slf4j(topic = &quot;c.People&quot;)
class People extends Thread{
    @Override
    public void run() {
        // 收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug(&quot;开始收信i d:{}&quot;, guardedObject.getId());
        Object mail = guardedObject.get(5000);
        log.debug(&quot;收到信id:{}，内容:{}&quot;, guardedObject.getId(),mail);
    }
}

class Postman extends Thread{
    private int id;
    private String mail;
    //构造方法
    @Override
    public void run() {
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.debug(&quot;开始送信i d:{}，内容:{}&quot;, guardedObject.getId(),mail);
        guardedObject.complete(mail);
    }
}

class  Mailboxes {
    private static Map&amp;lt;Integer, GuardedObject&amp;gt; boxes = new Hashtable&amp;lt;&amp;gt;();
    private static int id = 1;

    //产生唯一的id
    private static synchronized int generateId() {
        return id++;
    }

    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    public static Set&amp;lt;Integer&amp;gt; getIds() {
        return boxes.keySet();
    }
}
class GuardedObject {
    //标识，Guarded Object
    private int id;//添加get set方法
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;顺序输出&lt;/h4&gt;
&lt;p&gt;顺序输出 2  1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -&amp;gt; {
        while (true) {
            //try { Thread.sleep(1000); } catch (InterruptedException e) { }
            // 当没有许可时，当前线程暂停运行；有许可时，用掉这个许可，当前线程恢复运行
            LockSupport.park();
            System.out.println(&quot;1&quot;);
        }
    });
    Thread t2 = new Thread(() -&amp;gt; {
        while (true) {
            System.out.println(&quot;2&quot;);
            // 给线程 t1 发放『许可』（多次连续调用 unpark 只会发放一个『许可』）
            LockSupport.unpark(t1);
            try { Thread.sleep(500); } catch (InterruptedException e) { }
        }
    });
    t1.start();
    t2.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;交替输出&lt;/h4&gt;
&lt;p&gt;连续输出 5 次 abc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class day2_14 {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;a&quot;, a, b);
        }).start();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;b&quot;, b, c);
        }).start();
        new Thread(() -&amp;gt; {
            awaitSignal.print(&quot;c&quot;, c, a);
        }).start();

        Thread.sleep(1000);
        awaitSignal.lock();
        try {
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    //参数1：打印内容  参数二：条件变量  参数二：唤醒下一个
    public void print(String str, Condition condition, Condition next) {
        for (int i = 0; i &amp;lt; loopNumber; i++) {
            lock();
            try {
                condition.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;异步模式&lt;/h3&gt;
&lt;h4&gt;传统版&lt;/h4&gt;
&lt;p&gt;异步模式之生产者/消费者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ShareData {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception{
        // 同步代码块，加锁
        lock.lock();
        try {
            // 判断  防止虚假唤醒
            while(number != 0) {
                // 等待不能生产
                condition.await();
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + &quot;\t &quot; + number);
            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws Exception{
        // 同步代码块，加锁
        lock.lock();
        try {
            // 判断 防止虚假唤醒
            while(number == 0) {
                // 等待不能消费
                condition.await();
            }
            // 干活
            number--;
            System.out.println(Thread.currentThread().getName() + &quot;\t &quot; + number);
            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class TraditionalProducerConsumer {
 public static void main(String[] args) {
        ShareData shareData = new ShareData();
        // t1线程，生产
        new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5; i++) {
             shareData.increment();
            }
        }, &quot;t1&quot;).start();

        // t2线程，消费
        new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; 5; i++) {
    shareData.decrement();
            }
        }, &quot;t2&quot;).start(); 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;改进版&lt;/h4&gt;
&lt;p&gt;异步模式之生产者/消费者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费队列可以用来平衡生产和消费的线程资源，不需要产生结果和消费结果的线程一一对应&lt;/li&gt;
&lt;li&gt;生产者仅负责产生结果数据，不关心数据该如何处理，而消费者专心处理结果数据&lt;/li&gt;
&lt;li&gt;消息队列是有容量限制的，满时不会再加入数据，空时不会再消耗数据&lt;/li&gt;
&lt;li&gt;JDK 中各种阻塞队列，采用的就是这种模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class demo {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        for (int i = 0; i &amp;lt; 3; i++) {
            int id = i;
            new Thread(() -&amp;gt; {
                queue.put(new Message(id,&quot;值&quot;+id));
            }, &quot;生产者&quot; + i).start();
        }
        
        new Thread(() -&amp;gt; {
            while (true) {
                try {
                    Thread.sleep(1000);
                    Message message = queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },&quot;消费者&quot;).start();
    }
}

//消息队列类，Java间线程之间通信
class MessageQueue {
    private LinkedList&amp;lt;Message&amp;gt; list = new LinkedList&amp;lt;&amp;gt;();//消息的队列集合
    private int capacity;//队列容量
    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    //获取消息
    public Message take() {
        //检查队列是否为空
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    sout(Thread.currentThread().getName() + &quot;:队列为空，消费者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列的头部获取消息返回
            Message message = list.removeFirst();
            sout(Thread.currentThread().getName() + &quot;：已消费消息--&quot; + message);
            list.notifyAll();
            return message;
        }
    }

    //存入消息
    public void put(Message message) {
        synchronized (list) {
            //检查队列是否满
            while (list.size() == capacity) {
                try {
                    sout(Thread.currentThread().getName()+&quot;:队列为已满，生产者线程等待&quot;);
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将消息加入队列尾部
            list.addLast(message);
            sout(Thread.currentThread().getName() + &quot;:已生产消息--&quot; + message);
            list.notifyAll();
        }
    }
}

final class Message {
    private int id;
    private Object value;
 //get set
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;阻塞队列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ExecutorService consumer = Executors.newFixedThreadPool(1);
    ExecutorService producer = Executors.newFixedThreadPool(1);
    BlockingQueue&amp;lt;Integer&amp;gt; queue = new SynchronousQueue&amp;lt;&amp;gt;();
    producer.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;生产...&quot;);
            Thread.sleep(1000);
            queue.put(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    consumer.submit(() -&amp;gt; {
        try {
            System.out.println(&quot;等待消费...&quot;);
            Integer result = queue.take();
            System.out.println(&quot;结果为:&quot; + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;内存&lt;/h2&gt;
&lt;h3&gt;JMM&lt;/h3&gt;
&lt;h4&gt;内存模型&lt;/h4&gt;
&lt;p&gt;Java 内存模型是 Java Memory Model（JMM），本身是一种&lt;strong&gt;抽象的概念&lt;/strong&gt;，实际上并不存在，描述的是一组规则或规范，通过这组规范定义了程序中各个变量（包括实例字段，静态字段和构成数组对象的元素）的访问方式&lt;/p&gt;
&lt;p&gt;JMM 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;屏蔽各种硬件和操作系统的内存访问差异，实现让 Java 程序在各种平台下都能达到一致的内存访问效果&lt;/li&gt;
&lt;li&gt;规定了线程和内存之间的一些关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据 JMM 的设计，系统存在一个主内存（Main Memory），Java 中所有变量都存储在主存中，对于所有线程都是共享的；每条线程都有自己的工作内存（Working Memory），工作内存中保存的是主存中某些&lt;strong&gt;变量的拷贝&lt;/strong&gt;，线程对所有变量的操作都是先对变量进行拷贝，然后在工作内存中进行，不能直接操作主内存中的变量；线程之间无法相互直接访问，线程间的通信（传递）必须通过主内存来完成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主内存和工作内存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主内存：计算机的内存，也就是经常提到的 8G 内存，16G 内存，存储所有共享变量的值&lt;/li&gt;
&lt;li&gt;工作内存：存储该线程使用到的共享变量在主内存的的值的副本拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;JVM 和 JMM 之间的关系&lt;/strong&gt;：JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分，这两者基本上是没有关系的，如果两者一定要勉强对应起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主内存主要对应于 Java 堆中的对象实例数据部分，而工作内存则对应于虚拟机栈中的部分区域&lt;/li&gt;
&lt;li&gt;从更低层次上说，主内存直接对应于物理硬件的内存，工作内存对应寄存器和高速缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;内存交互&lt;/h4&gt;
&lt;p&gt;Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作，每个操作都是&lt;strong&gt;原子&lt;/strong&gt;的&lt;/p&gt;
&lt;p&gt;非原子协定：没有被 volatile 修饰的 long、double 外，默认按照两次 32 位的操作&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-内存交互.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lock：作用于主内存，将一个变量标识为被一个线程独占状态（对应 monitorenter）&lt;/li&gt;
&lt;li&gt;unclock：作用于主内存，将一个变量从独占状态释放出来，释放后的变量才可以被其他线程锁定（对应 monitorexit）&lt;/li&gt;
&lt;li&gt;read：作用于主内存，把一个变量的值从主内存传输到工作内存中&lt;/li&gt;
&lt;li&gt;load：作用于工作内存，在 read 之后执行，把 read 得到的值放入工作内存的变量副本中&lt;/li&gt;
&lt;li&gt;use：作用于工作内存，把工作内存中一个变量的值传递给&lt;strong&gt;执行引擎&lt;/strong&gt;，每当遇到一个使用到变量的操作时都要使用该指令&lt;/li&gt;
&lt;li&gt;assign：作用于工作内存，把从执行引擎接收到的一个值赋给工作内存的变量&lt;/li&gt;
&lt;li&gt;store：作用于工作内存，把工作内存的一个变量的值传送到主内存中&lt;/li&gt;
&lt;li&gt;write：作用于主内存，在 store 之后执行，把 store 得到的值放入主内存的变量中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：&lt;a href=&quot;https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md&quot;&gt;https://github.com/CyC2018/CS-Notes/blob/master/notes/Java 并发.md&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;三大特性&lt;/h4&gt;
&lt;h5&gt;可见性&lt;/h5&gt;
&lt;p&gt;可见性：是指当多个线程访问同一个变量时，一个线程修改了这个变量的值，其他线程能够立即看得到修改的值&lt;/p&gt;
&lt;p&gt;存在不可见问题的根本原因是由于缓存的存在，线程持有的是共享变量的副本，无法感知其他线程对于共享变量的更改，导致读取的值不是最新的。但是 final 修饰的变量是&lt;strong&gt;不可变&lt;/strong&gt;的，就算有缓存，也不会存在不可见的问题&lt;/p&gt;
&lt;p&gt;main 线程对 run 变量的修改对于 t 线程不可见，导致了 t 线程无法停止：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static boolean run = true; //添加volatile
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()-&amp;gt;{
        while(run){
        // ....
        }
 });
    t.start();
    sleep(1);
    run = false; // 线程t不会如预想的停下来
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始状态， t 线程刚开始从主内存读取了 run 的值到工作内存&lt;/li&gt;
&lt;li&gt;因为 t 线程要频繁从主内存中读取 run 的值，JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中，减少对主存中 run 的访问，提高效率&lt;/li&gt;
&lt;li&gt;1 秒之后，main 线程修改了 run 的值，并同步至主存，而 t 是从自己工作内存中的高速缓存中读取这个变量的值，结果永远是旧值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-%E5%8F%AF%E8%A7%81%E6%80%A7%E4%BE%8B%E5%AD%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;原子性&lt;/h5&gt;
&lt;p&gt;原子性：不可分割，完整性，也就是说某个线程正在做某个具体业务时，中间不可以被分割，需要具体完成，要么同时成功，要么同时失败，保证指令不会受到线程上下文切换的影响&lt;/p&gt;
&lt;p&gt;定义原子操作的使用规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不允许 read 和 load、store 和 write 操作之一单独出现，必须顺序执行，但是不要求连续&lt;/li&gt;
&lt;li&gt;不允许一个线程丢弃 assign 操作，必须同步回主存&lt;/li&gt;
&lt;li&gt;不允许一个线程无原因地（没有发生过任何 assign 操作）把数据从工作内存同步会主内存中&lt;/li&gt;
&lt;li&gt;一个新的变量只能在主内存中诞生，不允许在工作内存中直接使用一个未被初始化（assign 或者 load）的变量，即对一个变量实施 use 和 store 操作之前，必须先自行 assign 和 load 操作&lt;/li&gt;
&lt;li&gt;一个变量在同一时刻只允许一条线程对其进行 lock 操作，但 lock 操作可以被同一线程重复执行多次，多次执行 lock 后，只有&lt;strong&gt;执行相同次数的 unlock&lt;/strong&gt; 操作，变量才会被解锁，&lt;strong&gt;lock 和 unlock 必须成对出现&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果对一个变量执行 lock 操作，将会&lt;strong&gt;清空工作内存中此变量的值&lt;/strong&gt;，在执行引擎使用这个变量之前需要重新从主存加载&lt;/li&gt;
&lt;li&gt;如果一个变量事先没有被 lock 操作锁定，则不允许执行 unlock 操作，也不允许去 unlock 一个被其他线程锁定的变量&lt;/li&gt;
&lt;li&gt;对一个变量执行 unlock 操作之前，必须&lt;strong&gt;先把此变量同步到主内存&lt;/strong&gt;中（执行 store 和 write 操作）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h5&gt;有序性&lt;/h5&gt;
&lt;p&gt;有序性：在本线程内观察，所有操作都是有序的；在一个线程观察另一个线程，所有操作都是无序的，无序是因为发生了指令重排序&lt;/p&gt;
&lt;p&gt;CPU 的基本工作是执行存储的指令序列，即程序，程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程，为了提高性能，编译器和处理器会对指令重排，一般分为以下三种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;源代码 -&amp;gt; 编译器优化的重排 -&amp;gt; 指令并行的重排 -&amp;gt; 内存系统的重排 -&amp;gt; 最终执行指令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现代 CPU 支持多级指令流水线，几乎所有的冯•诺伊曼型计算机的 CPU，其工作都可以分为 5 个阶段：取指令、指令译码、执行指令、访存取数和结果写回，可以称之为&lt;strong&gt;五级指令流水线&lt;/strong&gt;。CPU 可以在一个时钟周期内，同时运行五条指令的&lt;strong&gt;不同阶段&lt;/strong&gt;（每个线程不同的阶段），本质上流水线技术并不能缩短单条指令的执行时间，但变相地提高了指令地吞吐率&lt;/p&gt;
&lt;p&gt;处理器在进行重排序时，必须要考虑&lt;strong&gt;指令之间的数据依赖性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单线程环境也存在指令重排，由于存在依赖性，最终执行结果和代码顺序的结果一致&lt;/li&gt;
&lt;li&gt;多线程环境中线程交替执行，由于编译器优化重排，会获取其他线程处在不同阶段的指令同时执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指令周期是取出一条指令并执行这条指令的时间，一般由若干个机器周期组成&lt;/li&gt;
&lt;li&gt;机器周期也称为 CPU 周期，一条指令的执行过程划分为若干个阶段（如取指、译码、执行等），每一阶段完成一个基本操作，完成一个基本操作所需要的时间称为机器周期&lt;/li&gt;
&lt;li&gt;振荡周期指周期性信号作周期性重复变化的时间间隔&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;cache&lt;/h3&gt;
&lt;h4&gt;缓存机制&lt;/h4&gt;
&lt;h5&gt;缓存结构&lt;/h5&gt;
&lt;p&gt;在计算机系统中，CPU 高速缓存（CPU Cache，简称缓存）是用于减少处理器访问内存所需平均时间的部件；在存储体系中位于自顶向下的第二层，仅次于 CPU 寄存器；其容量远小于内存，但速度却可以接近处理器的频率&lt;/p&gt;
&lt;p&gt;CPU 处理器速度远远大于在主内存中的，为了解决速度差异，在它们之间架设了多级缓存，如 L1、L2、L3 级别的缓存，这些缓存离 CPU 越近就越快，将频繁操作的数据缓存到这里，加快访问速度&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-CPU缓存结构.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;从 CPU 到&lt;/th&gt;
&lt;th&gt;大约需要的时钟周期&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;寄存器&lt;/td&gt;
&lt;td&gt;1 cycle (4GHz 的 CPU 约为 0.25ns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;3~4 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;10~20 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;40~45 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;120~240 cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;缓存使用&lt;/h5&gt;
&lt;p&gt;当处理器发出内存访问请求时，会先查看缓存内是否有请求数据，如果存在（命中），则不用访问内存直接返回该数据；如果不存在（失效），则要先把内存中的相应数据载入缓存，再将其返回处理器&lt;/p&gt;
&lt;p&gt;缓存之所以有效，主要因为程序运行时对内存的访问呈现局部性（Locality）特征。既包括空间局部性（Spatial Locality），也包括时间局部性（Temporal Locality），有效利用这种局部性，缓存可以达到极高的命中率&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;伪共享&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;缓存以缓存行 cache line 为单位&lt;/strong&gt;，每个缓存行对应着一块内存，一般是 64 byte（8 个 long），在 CPU 从主存获取数据时，以 cache line 为单位加载，于是相邻的数据会一并加载到缓存中&lt;/p&gt;
&lt;p&gt;缓存会造成数据副本的产生，即同一份数据会缓存在不同核心的缓存行中，CPU 要保证数据的一致性，需要做到某个 CPU 核心更改了数据，其它 CPU 核心对应的&lt;strong&gt;整个缓存行必须失效&lt;/strong&gt;，这就是伪共享&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-内存伪共享.png&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;padding：通过填充，让数据落在不同的 cache line 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@Contended：原理参考 无锁 → Adder → 优化机制 → 伪共享&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux 查看 CPU 缓存行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令：&lt;code&gt;cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内存地址格式：[高位组标记] [低位索引] [偏移量]&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;缓存一致&lt;/h4&gt;
&lt;p&gt;缓存一致性：当多个处理器运算任务都涉及到同一块主内存区域的时候，将可能导致各自的缓存数据不一样&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-缓存一致性.png&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;MESI（Modified Exclusive Shared Or Invalid）是一种广泛使用的&lt;strong&gt;支持写回策略的缓存一致性协议&lt;/strong&gt;，CPU 中每个缓存行（caceh line）使用 4 种状态进行标记（使用额外的两位 bit 表示)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;M：被修改（Modified）&lt;/p&gt;
&lt;p&gt;该缓存行只被缓存在该 CPU 的缓存中，并且是被修改过的，与主存中的数据不一致 (dirty)，该缓存行中的内存需要写回 (write back) 主存。该状态的数据再次被修改不会发送广播，因为其他核心的数据已经在第一次修改时失效一次&lt;/p&gt;
&lt;p&gt;当被写回主存之后，该缓存行的状态会变成独享 (exclusive) 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;E：独享的（Exclusive）&lt;/p&gt;
&lt;p&gt;该缓存行只被缓存在该 CPU 的缓存中，是未被修改过的 (clear)，与主存中数据一致，修改数据不需要通知其他 CPU 核心，该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared)&lt;/p&gt;
&lt;p&gt;当 CPU 修改该缓存行中内容时，该状态可以变成 Modified 状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;S：共享的（Shared）&lt;/p&gt;
&lt;p&gt;该状态意味着该缓存行可能被多个 CPU 缓存，并且各个缓存中的数据与主存数据一致，当 CPU 修改该缓存行中，会向其它 CPU 核心广播一个请求，使该缓存行变成无效状态 (Invalid)，然后再更新当前 Cache 里的数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I：无效的（Invalid）&lt;/p&gt;
&lt;p&gt;该缓存是无效的，可能有其它 CPU 修改了该缓存行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法：各个处理器访问缓存时都遵循一些协议，在读写时要根据协议进行操作，协议主要有 MSI、MESI 等&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;处理机制&lt;/h4&gt;
&lt;p&gt;单核 CPU 处理器会自动保证基本内存操作的原子性&lt;/p&gt;
&lt;p&gt;多核 CPU 处理器，每个 CPU 处理器内维护了一块内存，每个内核内部维护着一块缓存，当多线程并发读写时，就会出现缓存数据不一致的情况。处理器提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总线锁定：当处理器要操作共享变量时，在 BUS 总线上发出一个 LOCK 信号，其他处理器就无法操作这个共享变量，该操作会导致大量阻塞，从而增加系统的性能开销（&lt;strong&gt;平台级别的加锁&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;缓存锁定：当处理器对缓存中的共享变量进行了操作，其他处理器有嗅探机制，将各自缓存中的该共享变量的失效，读取时会重新从主内存中读取最新的数据，基于 MESI 缓存一致性协议来实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有如下两种情况处理器不会使用缓存锁定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当操作的数据跨多个缓存行，或没被缓存在处理器内部，则处理器会使用总线锁定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有些处理器不支持缓存锁定，比如：Intel 486 和 Pentium 处理器也会调用总线锁定&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总线机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;总线嗅探：每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了，当处理器发现自己的缓存对应的内存地址的数据被修改，就&lt;strong&gt;将当前处理器的缓存行设置为无效状态&lt;/strong&gt;，当处理器对这个数据进行操作时，会重新从内存中把数据读取到处理器缓存中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总线风暴：当某个 CPU 核心更新了 Cache 中的数据，要把该事件广播通知到其他核心（&lt;strong&gt;写传播&lt;/strong&gt;），CPU 需要每时每刻监听总线上的一切活动，但是不管别的核心的 Cache 是否缓存相同的数据，都需要发出一个广播事件，不断的从主内存嗅探和 CAS 循环，无效的交互会导致总线带宽达到峰值；因此不要大量使用 volatile 关键字，使用 volatile、syschonized 都需要根据实际场景&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;volatile&lt;/h3&gt;
&lt;h4&gt;同步机制&lt;/h4&gt;
&lt;p&gt;volatile 是 Java 虚拟机提供的&lt;strong&gt;轻量级&lt;/strong&gt;的同步机制（三大特性）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证可见性&lt;/li&gt;
&lt;li&gt;不保证原子性&lt;/li&gt;
&lt;li&gt;保证有序性（禁止指令重排）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：volatile 修饰的变量进行读操作与普通变量几乎没什么差别，但是写操作相对慢一些，因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行，但是开销比锁要小&lt;/p&gt;
&lt;p&gt;synchronized 无法禁止指令重排和处理器优化，为什么可以保证有序性可见性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加了锁之后，只能有一个线程获得到了锁，获得不到锁的线程就要阻塞，所以同一时间只有一个线程执行，相当于单线程，由于数据依赖性的存在，单线程的指令重排是没有问题的&lt;/li&gt;
&lt;li&gt;线程加锁前，将&lt;strong&gt;清空工作内存&lt;/strong&gt;中共享变量的值，使用共享变量时需要从主内存中重新读取最新的值；线程解锁前，必须把共享变量的最新值&lt;strong&gt;刷新到主内存&lt;/strong&gt;中（JMM 内存交互章节有讲）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;指令重排&lt;/h4&gt;
&lt;p&gt;volatile 修饰的变量，可以禁用指令重排&lt;/p&gt;
&lt;p&gt;指令重排实例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;example 1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void mySort() {
 int x = 11; //语句1
 int y = 12; //语句2  谁先执行效果一样
 x = x + 5; //语句3
 y = x * x; //语句4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行顺序是：1 2 3 4、2 1 3 4、1 3 2 4&lt;/p&gt;
&lt;p&gt;指令重排也有限制不会出现：4321，语句 4 需要依赖于 y 以及 x 的申明，因为存在数据依赖，无法首先执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;example 2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
     r.r1 = num + num;
    } else {
     r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
 num = 2;
 ready = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情况一：线程 1 先执行，ready = false，结果为 r.r1 = 1&lt;/p&gt;
&lt;p&gt;情况二：线程 2 先执行 num = 2，但还没执行 ready = true，线程 1 执行，结果为 r.r1 = 1&lt;/p&gt;
&lt;p&gt;情况三：线程 2 先执行 ready = true，线程 1 执行，进入 if 分支结果为 r.r1 = 4&lt;/p&gt;
&lt;p&gt;情况四：线程 2 执行 ready = true，切换到线程 1，进入 if 分支为 r.r1 = 0，再切回线程 2 执行 num = 2，发生指令重排&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;底层原理&lt;/h4&gt;
&lt;h5&gt;缓存一致&lt;/h5&gt;
&lt;p&gt;使用 volatile 修饰的共享变量，底层通过汇编 lock 前缀指令进行缓存锁定，在线程修改完共享变量后写回主存，其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态，读取时会重新从主内存中读取最新的数据&lt;/p&gt;
&lt;p&gt;lock 前缀指令就相当于内存屏障，Memory Barrier（Memory Fence）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 volatile 变量的写指令后会加入写屏障&lt;/li&gt;
&lt;li&gt;对 volatile 变量的读指令前会加入读屏障&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内存屏障有三个作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保对内存的读-改-写操作原子执行&lt;/li&gt;
&lt;li&gt;阻止屏障两侧的指令重排序&lt;/li&gt;
&lt;li&gt;强制把缓存中的脏数据写回主内存，让缓存行中相应的数据失效&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;内存屏障&lt;/h5&gt;
&lt;p&gt;保证&lt;strong&gt;可见性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写屏障（sfence，Store Barrier）保证在该屏障之前的，对共享变量的改动，都同步到主存当中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读屏障（lfence，Load Barrier）保证在该屏障之后的，对共享变量的读取，从主存刷新变量值，加载的是主存中最新数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
     r.r1 = num + num;
    } else {
     r.r1 = 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-volatile保证可见性.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全能屏障：mfence（modify/mix Barrier），兼具 sfence 和 lfence 的功能&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保证&lt;strong&gt;有序性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后&lt;/li&gt;
&lt;li&gt;读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不能解决指令交错：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写屏障仅仅是保证之后的读能够读到最新的结果，但不能保证其他线程的读跑到写屏障之前&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有序性的保证也只是保证了本线程内相关代码不被重排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile i = 0;
new Thread(() -&amp;gt; {i++});
new Thread(() -&amp;gt; {i--});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i++ 反编译后的指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: iconst_1   // 当int取值 -1~5 时，JVM采用iconst指令将常量压入栈中
1: istore_1   // 将操作数栈顶数据弹出，存入局部变量表的 slot 1
2: iinc  1, 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-volatile不能保证原子性.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;交互规则&lt;/h5&gt;
&lt;p&gt;对于 volatile 修饰的变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程对变量的 use 与 load、read 操作是相关联的，所以变量使用前必须先从主存加载&lt;/li&gt;
&lt;li&gt;线程对变量的 assign 与 store、write 操作是相关联的，所以变量使用后必须同步至主存&lt;/li&gt;
&lt;li&gt;线程 1 和线程 2 谁先对变量执行 read 操作，就会先进行 write 操作，防止指令重排&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;双端检锁&lt;/h4&gt;
&lt;h5&gt;检锁机制&lt;/h5&gt;
&lt;p&gt;Double-Checked Locking：双端检锁机制&lt;/p&gt;
&lt;p&gt;DCL（双端检锁）机制不一定是线程安全的，原因是有指令重排的存在，加入 volatile 可以禁止指令重排&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2，这里的判断不是线程安全的
            // 首次访问会同步，而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                // 这里是线程安全的判断，防止其他线程在当前线程等待锁的期间完成了初始化
                if (INSTANCE == null) { 
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不锁 INSTANCE 的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;INSTANCE 要重新赋值&lt;/li&gt;
&lt;li&gt;INSTANCE 是 null，线程加锁之前需要获取对象的引用，设置对象头，null 没有引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;懒惰初始化&lt;/li&gt;
&lt;li&gt;首次使用 getInstance() 才使用 synchronized 加锁，后续使用时无需加锁&lt;/li&gt;
&lt;li&gt;第一个 if 使用了 INSTANCE 变量，是在同步块之外，但在多线程环境下会产生问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;DCL问题&lt;/h5&gt;
&lt;p&gt;getInstance 方法对应的字节码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0:  getstatic   #2   // Field INSTANCE:Ltest/Singleton;
3:  ifnonnull   37
6:  ldc    #3   // class test/Singleton
8:  dup
9:  astore_0
10: monitorenter
11: getstatic   #2   // Field INSTANCE:Ltest/Singleton;
14: ifnonnull 27
17: new    #3   // class test/Singleton
20: dup
21: invokespecial  #4   // Method &quot;&amp;lt;init&amp;gt;&quot;:()V
24: putstatic   #2   // Field INSTANCE:Ltest/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic   #2   // Field INSTANCE:Ltest/Singleton;
40: areturn
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;17 表示创建对象，将对象引用入栈&lt;/li&gt;
&lt;li&gt;20 表示复制一份对象引用，引用地址&lt;/li&gt;
&lt;li&gt;21 表示利用一个对象引用，调用构造方法初始化对象&lt;/li&gt;
&lt;li&gt;24 表示利用一个对象引用，赋值给 static INSTANCE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步骤 21 和 24 之间不存在数据依赖关系&lt;/strong&gt;，而且无论重排前后，程序的执行结果在单线程中并没有改变，因此这种重排优化是允许的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关键在于 0:getstatic 这行代码在 monitor 控制之外，可以越过 monitor 读取 INSTANCE 变量的值&lt;/li&gt;
&lt;li&gt;当其他线程访问 INSTANCE 不为 null 时，由于 INSTANCE 实例未必已初始化，那么 t2 拿到的是将是一个未初始化完毕的单例返回，这就造成了线程安全的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL%E5%87%BA%E7%8E%B0%E7%9A%84%E9%97%AE%E9%A2%98.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;解决方法&lt;/h5&gt;
&lt;p&gt;指令重排只会保证串行语义的执行一致性（单线程），但并不会关系多线程间的语义一致性&lt;/p&gt;
&lt;p&gt;引入 volatile，来保证出现指令重排的问题，从而保证单例模式的线程安全性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static volatile SingletonDemo INSTANCE = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;ha-be&lt;/h3&gt;
&lt;p&gt;happens-before 先行发生&lt;/p&gt;
&lt;p&gt;Java 内存模型具备一些先天的“有序性”，即不需要通过任何同步手段（volatile、synchronized 等）就能够得到保证的安全，这个通常也称为 happens-before 原则，它是可见性与有序性的一套规则总结&lt;/p&gt;
&lt;p&gt;不符合 happens-before 规则，JMM 并不能保证一个线程的可见性和有序性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;程序次序规则 (Program Order Rule)：一个线程内，逻辑上书写在前面的操作先行发生于书写在后面的操作 ，因为多个操作之间有先后依赖关系，则不允许对这些操作进行重排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁定规则 (Monitor Lock Rule)：一个 unlock 操作先行发生于后面（时间的先后）对同一个锁的 lock 操作，所以线程解锁 m 之前对变量的写（解锁前会刷新到主内存中），对于接下来对 m 加锁的其它线程对该变量的读可见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;volatile 变量规则&lt;/strong&gt;  (Volatile Variable Rule)：对 volatile 变量的写操作先行发生于后面对这个变量的读&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递规则 (Transitivity)：具有传递性，如果操作 A 先行发生于操作 B，而操作 B 又先行发生于操作 C，则可以得出操作 A 先行发生于操作 C&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程启动规则 (Thread Start Rule)：Thread 对象的 start()方 法先行发生于此线程中的每一个操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int x = 10;//线程 start 前对变量的写，对该线程开始后对该变量的读可见
new Thread(()-&amp;gt;{ System.out.println(x); },&quot;t1&quot;).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程中断规则 (Thread Interruption Rule)：对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程终止规则 (Thread Termination Rule)：线程中所有的操作都先行发生于线程的终止检测，可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象终结规则（Finaizer Rule）：一个对象的初始化完成（构造函数执行结束）先行发生于它的 finalize() 方法的开始&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;设计模式&lt;/h3&gt;
&lt;h4&gt;终止模式&lt;/h4&gt;
&lt;p&gt;终止模式之两阶段终止模式：停止标记用 volatile 是为了保证该变量在多个线程之间的可见性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TwoPhaseTermination {
    // 监控线程
    private Thread monitor;
    // 停止标记
    private volatile boolean stop = false;;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -&amp;gt; {
            while (true) {
                Thread thread = Thread.currentThread();
                if (stop) {
                    System.out.println(&quot;后置处理&quot;);
                    break;
                }
                try {
                    Thread.sleep(1000);// 睡眠
                    System.out.println(thread.getName() + &quot;执行监控记录&quot;);
                } catch (InterruptedException e) {
                    System.out.println(&quot;被打断，退出睡眠&quot;);
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        monitor.interrupt();// 让线程尽快退出Timed Waiting
    }
}
// 测试
public static void main(String[] args) throws InterruptedException {
    TwoPhaseTermination tpt = new TwoPhaseTermination();
    tpt.start();
    Thread.sleep(3500);
    System.out.println(&quot;停止监控&quot;);
    tpt.stop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Balking&lt;/h4&gt;
&lt;p&gt;Balking （犹豫）模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事，那么本线程就无需再做了，直接结束返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MonitorService {
    // 用来表示是否已经有线程已经在执行启动了
    private volatile boolean starting = false;
    public void start() {
        System.out.println(&quot;尝试启动监控线程...&quot;);
        synchronized (this) {
            if (starting) {
             return;
            }
            starting = true;
        }
        // 真正启动监控线程...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比保护性暂停模式：保护性暂停模式用在一个线程等待另一个线程的执行结果，当条件不满足时线程等待&lt;/p&gt;
&lt;p&gt;例子：希望 doInit() 方法仅被调用一次，下面的实现出现的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 t1 线程进入 init() 准备 doInit()，t2 线程进来，initialized 还为f alse，则 t2 就又初始化一次&lt;/li&gt;
&lt;li&gt;volatile 适合一个线程写，其他线程读的情况，这个代码需要加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class TestVolatile {
    volatile boolean initialized = false;
    
    void init() {
        if (initialized) {
            return;
        }
     doInit();
     initialized = true;
    }
    private void doInit() {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;无锁&lt;/h2&gt;
&lt;h3&gt;CAS&lt;/h3&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;p&gt;无锁编程：Lock Free&lt;/p&gt;
&lt;p&gt;CAS 的全称是 Compare-And-Swap，是 &lt;strong&gt;CPU 并发原语&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法，调用 UnSafe 类中的 CAS 方法，JVM 会实现出 CAS 汇编指令，这是一种完全依赖于硬件的功能，实现了原子操作&lt;/li&gt;
&lt;li&gt;CAS 是一种系统原语，原语属于操作系统范畴，是由若干条指令组成 ，用于完成某个功能的一个过程，并且原语的执行必须是连续的，执行过程中不允许被中断，所以 CAS 是一条 CPU 的原子指令，不会造成数据不一致的问题，是线程安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;底层原理：CAS 的底层是 &lt;code&gt;lock cmpxchg&lt;/code&gt; 指令（X86 架构），在单核和多核 CPU 下都能够保证比较交换的原子性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;程序是在单核处理器上运行，会省略 lock 前缀，单处理器自身会维护处理器内的顺序一致性，不需要 lock 前缀的内存屏障效果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序是在多核处理器上运行，会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时，CPU 会执行&lt;strong&gt;总线锁定或缓存锁定&lt;/strong&gt;，将修改的变量写入到主存，这个过程不会被线程的调度机制所打断，保证了多个线程对内存操作的原子性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用：比较当前工作内存中的值和主物理内存中的值，如果相同则执行规定操作，否则继续比较直到主内存和工作内存的值一致为止&lt;/p&gt;
&lt;p&gt;CAS 特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CAS 体现的是&lt;strong&gt;无锁并发、无阻塞并发&lt;/strong&gt;，线程不会陷入阻塞，线程不需要频繁切换状态（上下文切换，系统调用）&lt;/li&gt;
&lt;li&gt;CAS 是基于乐观锁的思想&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CAS 缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行的是循环操作，如果比较不成功一直在循环，最差的情况某个线程一直取到的值和预期值都不一样，就会无限循环导致饥饿，&lt;strong&gt;使用 CAS 线程数不要超过 CPU 的核心数&lt;/strong&gt;，采用分段 CAS 和自动迁移机制&lt;/li&gt;
&lt;li&gt;只能保证一个共享变量的原子操作
&lt;ul&gt;
&lt;li&gt;对于一个共享变量执行操作时，可以通过循环 CAS 的方式来保证原子操作&lt;/li&gt;
&lt;li&gt;对于多个共享变量操作时，循环 CAS 就无法保证操作的原子性，这个时候&lt;strong&gt;只能用锁来保证原子性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;引出来 ABA 问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;乐观锁&lt;/h4&gt;
&lt;p&gt;CAS 与 synchronized 总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;synchronized 是从悲观的角度出发：总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会阻塞（共享资源每次只给一个线程使用，其它线程阻塞，用完后再把资源转让给其它线程），因此 synchronized 也称之为悲观锁，ReentrantLock 也是一种悲观锁，性能较差&lt;/li&gt;
&lt;li&gt;CAS 是从乐观的角度出发：总是假设最好的情况，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。&lt;strong&gt;如果别人修改过，则获取现在最新的值，如果别人没修改过，直接修改共享数据的值&lt;/strong&gt;，CAS 这种机制也称之为乐观锁，综合性能较好&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Atomic&lt;/h3&gt;
&lt;h4&gt;常用API&lt;/h4&gt;
&lt;p&gt;常见原子类：AtomicInteger、AtomicBoolean、AtomicLong&lt;/p&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public AtomicInteger()&lt;/code&gt;：初始化一个默认值为 0 的原子型 Integer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public AtomicInteger(int initialValue)&lt;/code&gt;：初始化一个指定值的原子型 Integer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public final int get()&lt;/td&gt;
&lt;td&gt;获取 AtomicInteger 的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getAndIncrement()&lt;/td&gt;
&lt;td&gt;以原子方式将当前值加 1，返回的是自增前的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int incrementAndGet()&lt;/td&gt;
&lt;td&gt;以原子方式将当前值加 1，返回的是自增后的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int getAndSet(int value)&lt;/td&gt;
&lt;td&gt;以原子方式设置为 newValue 的值，返回旧值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public final int addAndGet(int data)&lt;/td&gt;
&lt;td&gt;以原子方式将输入的数值与实例中的值相加并返回&amp;lt;br /&amp;gt;实例：AtomicInteger 里的 value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;原理分析&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;AtomicInteger 原理&lt;/strong&gt;：自旋锁  + CAS 算法&lt;/p&gt;
&lt;p&gt;CAS 算法：有 3 个操作数（内存值 V， 旧的预期值 A，要修改的值 B）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当旧的预期值 A == 内存值 V   此时可以修改，将 V 改为 B&lt;/li&gt;
&lt;li&gt;当旧的预期值 A !=  内存值 V   此时不能修改，并重新获取现在的最新值，重新获取的动作就是自旋&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析 getAndSet 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AtomicInteger：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final int getAndSet(int newValue) {
    /**
    * this:   当前对象
    * valueOffset: 内存偏移量，内存地址
    */
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;valueOffset：偏移量表示该变量值相对于当前对象地址的偏移，Unsafe 就是根据内存偏移地址获取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField(&quot;value&quot;));
//调用本地方法   --&amp;gt;
public native long objectFieldOffset(Field var1);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unsafe 类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// val1: AtomicInteger对象本身，var2: 该对象值得引用地址，var4: 需要变动的数
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // var5: 用 var1 和 var2 找到的内存中的真实值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;var5：从主内存中拷贝到工作内存中的值（每次都要从主内存拿到最新的值到本地内存），然后执行 &lt;code&gt;compareAndSwapInt()&lt;/code&gt; 再和主内存的值进行比较，假设方法返回 false，那么就一直执行 while 方法，直到期望的值和真实值一样，修改数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变量 value 用 volatile 修饰，保证了多线程之间的内存可见性，避免线程从工作缓存中获取失效的变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析 getAndUpdate 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;getAndUpdate：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final int getAndUpdate(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get(); //当前值，cas的期望值
        next = updateFunction.applyAsInt(prev);//期望值更新到该值
    } while (!compareAndSet(prev, next));//自旋
    return prev;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数式接口：可以自定义操作逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AtomicInteger a = new AtomicInteger();
a.getAndUpdate(i -&amp;gt; i + 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;compareAndSet：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final boolean compareAndSet(int expect, int update) {
    /**
    * this:   当前对象
    * valueOffset: 内存偏移量，内存地址
    * expect:  期望的值
    * update:   更新的值
    */
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;原子引用&lt;/h4&gt;
&lt;p&gt;原子引用：对 Object 进行原子操作，提供一种读和写都是原子性的对象引用变量&lt;/p&gt;
&lt;p&gt;原子引用类：AtomicReference、AtomicStampedReference、AtomicMarkableReference&lt;/p&gt;
&lt;p&gt;AtomicReference 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;code&gt;AtomicReference&amp;lt;T&amp;gt; atomicReference = new AtomicReference&amp;lt;T&amp;gt;()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public final boolean compareAndSet(V expectedValue, V newValue)&lt;/code&gt;：CAS 操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public final void set(V newValue)&lt;/code&gt;：将值设置为 newValue&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public final V get()&lt;/code&gt;：返回当前值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Student s1 = new Student(33, &quot;z3&quot;);
        
        // 创建原子引用包装类
        AtomicReference&amp;lt;Student&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();
        // 设置主内存共享变量为s1
        atomicReference.set(s1);

        // 比较并交换，如果现在主物理内存的值为 z3，那么交换成 l4
        while (true) {
            Student s2 = new Student(44, &quot;l4&quot;);
            if (atomicReference.compareAndSet(s1, s2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

class Student {
    private int id;
    private String name;
    //。。。。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子数组&lt;/h4&gt;
&lt;p&gt;原子数组类：AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray&lt;/p&gt;
&lt;p&gt;AtomicIntegerArray 类方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
*   i  the index
* expect  the expected value
* update  the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子更新器&lt;/h4&gt;
&lt;p&gt;原子更新器类：AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater&lt;/p&gt;
&lt;p&gt;利用字段更新器，可以针对对象的某个域（Field）进行原子操作，只能配合 volatile 修饰的字段使用，否则会出现异常 &lt;code&gt;IllegalArgumentException: Must be volatile type&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static &amp;lt;U&amp;gt; AtomicIntegerFieldUpdater&amp;lt;U&amp;gt; newUpdater(Class&amp;lt;U&amp;gt; c, String fieldName)&lt;/code&gt;：构造方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abstract boolean compareAndSet(T obj, int expect, int update)&lt;/code&gt;：CAS&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UpdateDemo {
    private volatile int field;
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
              .newUpdater(UpdateDemo.class, &quot;field&quot;);
        UpdateDemo updateDemo = new UpdateDemo();
        fieldUpdater.compareAndSet(updateDemo, 0, 10);
        System.out.println(updateDemo.field);//10
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;原子累加器&lt;/h4&gt;
&lt;p&gt;原子累加器类：LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator&lt;/p&gt;
&lt;p&gt;LongAdder 和 LongAccumulator 区别：&lt;/p&gt;
&lt;p&gt;相同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的&lt;/li&gt;
&lt;li&gt;LongAddr 类是 LongAccumulator 类的一个特例，只是 LongAccumulator 提供了更强大的功能，可以自定义累加规则，当accumulatorFunction 为 null 时就等价于 LongAddr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 casBase 时，LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算，LongAddr 使用 casBase(b = base, b + x)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LongAccumulator 类功能更加强大，构造方法参数中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;accumulatorFunction 是一个双目运算器接口，可以指定累加规则，比如累加或者相乘，其根据输入的两个参数返回一个计算值，LongAdder 内置累加规则&lt;/li&gt;
&lt;li&gt;identity 则是 LongAccumulator 累加器的初始值，LongAccumulator 可以为累加器提供非0的初始值，而 LongAdder 只能提供默认的 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Adder&lt;/h3&gt;
&lt;h4&gt;优化机制&lt;/h4&gt;
&lt;p&gt;LongAdder 是 Java8 提供的类，跟 AtomicLong 有相同的效果，但对 CAS 机制进行了优化，尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能&lt;/p&gt;
&lt;p&gt;CAS 底层实现是在一个循环中不断地尝试修改目标值，直到修改成功。如果竞争不激烈修改成功率很高，否则失败率很高，失败后这些重复的原子性操作会耗费性能（导致大量线程&lt;strong&gt;空循环，自旋转&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;优化核心思想：数据分离，将 AtomicLong 的&lt;strong&gt;单点的更新压力分担到各个节点，空间换时间&lt;/strong&gt;，在低并发的时候直接更新，可以保障和 AtomicLong 的性能基本一致，而在高并发的时候通过分散减少竞争，提高了性能&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分段 CAS 机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在发生竞争时，创建 Cell 数组用于将不同线程的操作离散（通过 hash 等算法映射）到不同的节点上&lt;/li&gt;
&lt;li&gt;设置多个累加单元（会根据需要扩容，最大为 CPU 核数），Therad-0 累加 Cell[0]，而 Thread-1 累加 Cell[1] 等，最后将结果汇总&lt;/li&gt;
&lt;li&gt;在累加时操作的不同的 Cell 变量，因此减少了 CAS 重试失败，从而提高性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;自动分段迁移机制&lt;/strong&gt;：某个 Cell 的 value 执行 CAS 失败，就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;伪共享&lt;/h4&gt;
&lt;p&gt;Cell 为累加单元：数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的，这个域是 ThreadLocalRandom 更新的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Striped64.Cell
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 用 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
     return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cell 是数组形式，&lt;strong&gt;在内存中是连续存储的&lt;/strong&gt;，64 位系统中，一个 Cell 为 24 字节（16 字节的对象头和 8 字节的 value），每一个 cache line 为 64 字节，因此缓存行可以存下 2 个的 Cell 对象，当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1]，无论谁修改成功都会导致当前缓存行失效，从而导致对方的数据失效，需要重新去主存获取，影响效率&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BC%AA%E5%85%B1%E4%BA%AB1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;@sun.misc.Contended：防止缓存行伪共享，在使用此注解的对象或字段的前后各增加 128 字节大小的 padding，使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时&lt;strong&gt;占用不同的缓存行&lt;/strong&gt;，这样就不会造成对方缓存行的失效&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E4%BC%AA%E5%85%B1%E4%BA%AB2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;源码解析&lt;/h4&gt;
&lt;p&gt;Striped64 类成员属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示当前计算机CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors()
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域，当 cells 扩容时，也会将数据写到 base 中
transient volatile long base;
// 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁
transient volatile int cellsBusy;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cells 占用内存是相对比较大的，是惰性加载的，在无竞争或者其他线程正在初始化 cells 数组的情况下，直接更新 base 域&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在第一次发生竞争时（casBase 失败）会创建一个大小为 2 的 cells 数组，将当前累加的值包装为 Cell 对象，放入映射的槽位上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分段累加的过程中，如果当前线程对应的 cells 槽位为空，就会新建 Cell 填充，如果出现竞争，就会重新计算线程对应的槽位，继续自旋尝试修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍，然后 rehash，&lt;strong&gt;数组长度总是 2 的 n 次幂&lt;/strong&gt;，默认最大为 CPU 核数，但是可以超过，如果核数是 6 核，数组最长是 8&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;LongAdder#add：累加方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void add(long x) {
    // as 为累加单元数组的引用，b 为基础值，v 表示期望值
    // m 表示 cells 数组的长度 - 1，a 表示当前线程命中的 cell 单元格
    Cell[] as; long b, v; int m; Cell a;
    
    // cells 不为空说明 cells 已经被初始化，线程发生了竞争，去更新对应的 cell 槽位
    // 进入 || 后的逻辑去更新 base 域，更新失败表示发生竞争进入条件
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // uncontended 为 true 表示 cell 没有竞争
        boolean uncontended = true;
        
        // 条件一: true 说明 cells 未初始化，多线程写 base 发生竞争需要进行初始化 cells 数组
        //    fasle 说明 cells 已经初始化，进行下一个条件寻找自己的 cell 去累加
        // 条件二: getProbe() 获取 hash 值，&amp;amp; m 的逻辑和 HashMap 的逻辑相同，保证散列的均匀性
        //     true 说明当前线程对应下标的 cell 为空，需要创建 cell
        //        false 说明当前线程对应的 cell 不为空，进行下一个条件【将 x 值累加到对应的 cell 中】
        // 条件三: 有取反符号，false 说明 cas 成功，直接返回，true 说明失败，当前线程对应的 cell 有竞争
        if (as == null || (m = as.length - 1) &amp;lt; 0 ||
            (a = as[getProbe() &amp;amp; m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
         // 【uncontended 在对应的 cell 上累加失败的时候才为 false，其余情况均为 true】
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Striped64#longAccumulate：cell 数组创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       // x     null    false | true
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell
    if ((h = getProbe()) == 0) {
        // 初始化 probe，获取 hash 值
        ThreadLocalRandom.current(); 
        h = getProbe(); 
        // 默认情况下 当前线程肯定是写入到了 cells[0] 位置，不把它当做一次真正的竞争
        wasUncontended = true;
    }
    // 表示【扩容意向】，false 一定不会扩容，true 可能会扩容
    boolean collide = false; 
    //自旋
    for (;;) {
        // as 表示cells引用，a 表示当前线程命中的 cell，n 表示 cells 数组长度，v 表示 期望值
        Cell[] as; Cell a; int n; long v;
        // 【CASE1】: 表示 cells 已经初始化了，当前线程应该将数据写入到对应的 cell 中
        if ((as = cells) != null &amp;amp;&amp;amp; (n = as.length) &amp;gt; 0) {
            // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null，需要创建 new Cell
            if ((a = as[(n - 1) &amp;amp; h]) == null) {
                // 判断 cellsBusy 是否被锁
                if (cellsBusy == 0) {   
                    // 创建 cell, 初始累加值为 x
                    Cell r = new Cell(x);  
                    // 加锁
                    if (cellsBusy == 0 &amp;amp;&amp;amp; casCellsBusy()) {
                        // 创建成功标记，进入【创建 cell 逻辑】
                        boolean created = false; 
                        try {
                            Cell[] rs; int m, j;
                            // 把当前 cells 数组赋值给 rs，并且不为 null
                            if ((rs = cells) != null &amp;amp;&amp;amp;
                                (m = rs.length) &amp;gt; 0 &amp;amp;&amp;amp;
                                // 再次判断防止其它线程初始化过该位置，当前线程再次初始化该位置会造成数据丢失
                                // 因为这里是线程安全的判断，进行的逻辑不会被其他线程影响
                                rs[j = (m - 1) &amp;amp; h] == null) {
                                // 把新创建的 cell 填充至当前位置
                                rs[j] = r;
                                created = true; // 表示创建完成
                            }
                        } finally {
                            cellsBusy = 0;  // 解锁
                        }
                        if (created)   // true 表示创建完成，可以推出循环了
                            break;
                        continue;
                    }
                }
                collide = false;
            }
            // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas
            else if (!wasUncontended)
                wasUncontended = true;
            // CASE 1.3: 当前线程 rehash 过，如果新命中的 cell 不为空，就尝试累加，false 说明新命中也有竞争
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
            // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容
            else if (n &amp;gt;= NCPU || cells != as)
                collide = false;   // 扩容意向改为false，【表示不能扩容了】
            // CASE 1.5: 更改扩容意向，如果 n &amp;gt;= NCPU，这里就永远不会执行到，case1.4 永远先于 1.5 执行
            else if (!collide)
                collide = true;
            // CASE 1.6: 【扩容逻辑】，进行加锁
            else if (cellsBusy == 0 &amp;amp;&amp;amp; casCellsBusy()) {
                try {
                    // 线程安全的检查，防止期间被其他线程扩容了
                    if (cells == as) {     
                        // 扩容为以前的 2 倍
                        Cell[] rs = new Cell[n &amp;lt;&amp;lt; 1];
                        // 遍历移动值
                        for (int i = 0; i &amp;lt; n; ++i)
                            rs[i] = as[i];
                        // 把扩容后的引用给 cells
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0; // 解锁
                }
                collide = false; // 扩容意向改为 false，表示不扩容了
                continue;
            }
            // 重置当前线程 Hash 值，这就是【分段迁移机制】
            h = advanceProbe(h);
        }

        // 【CASE2】: 运行到这说明 cells 还未初始化，as 为null
        // 判断是否没有加锁，没有加锁就用 CAS 加锁
        // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells，这里不是线程安全的判断
        else if (cellsBusy == 0 &amp;amp;&amp;amp; cells == as &amp;amp;&amp;amp; casCellsBusy()) {
            // 初始化标志，开始 【初始化 cells 数组】
            boolean init = false;
            try { 
                // 再次判断 cells == as 防止其它线程已经提前初始化了，当前线程再次初始化导致丢失数据
                // 因为这里是【线程安全的，重新检查，经典 DCL】
                if (cells == as) {
                    Cell[] rs = new Cell[2]; // 初始化数组大小为2
                    rs[h &amp;amp; 1] = new Cell(x); // 填充线程对应的cell
                    cells = rs;
                    init = true;    // 初始化成功，标记置为 true
                }
            } finally {
                cellsBusy = 0;     // 解锁啊
            }
            if (init)
                break;       // 初始化成功直接跳出自旋
        }
        // 【CASE3】: 运行到这说明其他线程在初始化 cells，当前线程将值累加到 base，累加成功直接结束自旋
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sum：获取最终结果通过 sum 整合，&lt;strong&gt;保证最终一致性，不保证强一致性&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        // 遍历 累加
        for (int i = 0; i &amp;lt; as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ABA&lt;/h3&gt;
&lt;p&gt;ABA 问题：当进行获取主内存值时，该内存值在写入主内存时已经被修改了 N 次，但是最终又改成原来的值&lt;/p&gt;
&lt;p&gt;其他线程先把 A 改成 B 又改回 A，主线程&lt;strong&gt;仅能判断出共享变量的值与最初值 A 是否相同&lt;/strong&gt;，不能感知到这种从 A 改为 B 又 改回 A 的情况，这时 CAS 虽然成功，但是过程存在问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public AtomicStampedReference(V initialRef, int initialStamp)&lt;/code&gt;：初始值和初始版本号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)&lt;/code&gt;：&lt;strong&gt;期望引用和期望版本号都一致&lt;/strong&gt;才进行 CAS 修改数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void set(V newReference, int newStamp)&lt;/code&gt;：设置值和版本号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public V getReference()&lt;/code&gt;：返回引用的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int getStamp()&lt;/code&gt;：返回当前版本号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    AtomicStampedReference&amp;lt;Integer&amp;gt; atomicReference = new AtomicStampedReference&amp;lt;&amp;gt;(100,1);
    int startStamp = atomicReference.getStamp();
    new Thread(() -&amp;gt;{
        int stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(100, 101, stamp, stamp + 1);
        stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(101, 100, stamp, stamp + 1);
    },&quot;t1&quot;).start();

    new Thread(() -&amp;gt;{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {
            System.out.println(atomicReference.getReference());//100
            System.out.println(Thread.currentThread().getName() + &quot;线程修改失败&quot;);
        }
    },&quot;t2&quot;).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Unsafe&lt;/h3&gt;
&lt;p&gt;Unsafe 是 CAS 的核心类，由于 Java 无法直接访问底层系统，需要通过本地（Native）方法来访问&lt;/p&gt;
&lt;p&gt;Unsafe 类存在 sun.misc 包，其中所有方法都是 native 修饰的，都是直接调用&lt;strong&gt;操作系统底层资源&lt;/strong&gt;执行相应的任务，基于该类可以直接操作特定的内存数据，其内部方法操作类似 C 的指针&lt;/p&gt;
&lt;p&gt;模拟实现原子整数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
    if (atomicInteger.compareAndSwap(20)) {
        System.out.println(atomicInteger.getValue());
    }
}

class MyAtomicInteger {
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
    private volatile int value;

    static {
        try {
            //Unsafe unsafe = Unsafe.getUnsafe()这样会报错，需要反射获取
            Field theUnsafe = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
            // 获取 value 属性的内存地址，value 属性指向该地址，直接设置该地址的值可以修改 value 的值
            VALUE_OFFSET = UNSAFE.objectFieldOffset(
                     MyAtomicInteger.class.getDeclaredField(&quot;value&quot;));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }

    public boolean compareAndSwap(int update) {
        while (true) {
            int prev = this.value;
            int next = update;
            //       当前对象  内存偏移量    期望值 更新值
            if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
                System.out.println(&quot;CAS成功&quot;);
                return true;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;final&lt;/h3&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class TestFinal {
 final int a = 20;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字节码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: aload_0
1: invokespecial #1 // Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
4: aload_0
5: bipush 20  // 将值直接放入栈中
7: putfield #2   // Field a:I
&amp;lt;-- 写屏障
10: return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;final 变量的赋值通过 putfield 指令来完成，在这条指令之后也会加入写屏障，保证在其它线程读到它的值时不会出现为 0 的情况&lt;/p&gt;
&lt;p&gt;其他线程访问 final 修饰的变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复制一份放入栈中&lt;/strong&gt;直接访问，效率高&lt;/li&gt;
&lt;li&gt;大于 short 最大值会将其复制到类的常量池，访问时从常量池获取&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;不可变&lt;/h4&gt;
&lt;p&gt;不可变：如果一个对象不能够修改其内部状态（属性），那么就是不可变对象&lt;/p&gt;
&lt;p&gt;不可变对象线程安全的，不存在并发修改和可见性问题，是另一种避免竞争的方式&lt;/p&gt;
&lt;p&gt;String 类也是不可变的，该类和类中所有属性都是 final 的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类用 final 修饰保证了该类中的方法不能被覆盖，防止子类无意间破坏不可变性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无写入方法（set）确保外部不能对内部属性进行修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性用 final 修饰保证了该属性是只读的，不能修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class String
    implements java.io.Serializable, Comparable&amp;lt;String&amp;gt;, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更改 String 类数据时，会构造新字符串对象，生成新的 char[] value，通过&lt;strong&gt;创建副本对象来避免共享的方式称之为保护性拷贝&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;State&lt;/h3&gt;
&lt;p&gt;无状态：成员变量保存的数据也可以称为状态信息，无状态就是没有成员变量&lt;/p&gt;
&lt;p&gt;Servlet 为了保证其线程安全，一般不为 Servlet 设置成员变量，这种没有任何成员变量的类是线程安全的&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Local&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;ThreadLocal 类用来提供线程内部的局部变量，这种变量在多线程环境下访问（通过 get 和 set 方法访问）时能保证各个线程的变量相对独立于其他线程内的变量，分配在堆内的 &lt;strong&gt;TLAB&lt;/strong&gt; 中&lt;/p&gt;
&lt;p&gt;ThreadLocal 实例通常来说都是 &lt;code&gt;private static&lt;/code&gt; 类型的，属于一个线程的本地变量，用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据，所以是线程安全的&lt;/p&gt;
&lt;p&gt;ThreadLocal 作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程并发：应用在多线程并发的场景下&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递数据：通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量，减少传递复杂度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程隔离：每个线程的变量都是独立的，不会互相影响&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 synchronized：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;synchronized&lt;/th&gt;
&lt;th&gt;ThreadLocal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原理&lt;/td&gt;
&lt;td&gt;同步机制采用&lt;strong&gt;以时间换空间&lt;/strong&gt;的方式，只提供了一份变量，让不同的线程排队访问&lt;/td&gt;
&lt;td&gt;ThreadLocal 采用&lt;strong&gt;以空间换时间&lt;/strong&gt;的方式，为每个线程都提供了一份变量的副本，从而实现同时访问而相不干扰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;侧重点&lt;/td&gt;
&lt;td&gt;多个线程之间访问资源的同步&lt;/td&gt;
&lt;td&gt;多线程中让每个线程之间的数据相互隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;h5&gt;常用方法&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ThreadLocal&amp;lt;&amp;gt;()&lt;/td&gt;
&lt;td&gt;创建 ThreadLocal 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;protected T initialValue()&lt;/td&gt;
&lt;td&gt;返回当前线程局部变量的初始值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void set( T value)&lt;/td&gt;
&lt;td&gt;设置当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public T get()&lt;/td&gt;
&lt;td&gt;获取当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public void remove()&lt;/td&gt;
&lt;td&gt;移除当前线程绑定的局部变量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;public class MyDemo {

    private static ThreadLocal&amp;lt;String&amp;gt; tl = new ThreadLocal&amp;lt;&amp;gt;();

    private String content;

    private String getContent() {
        // 获取当前线程绑定的变量
        return tl.get();
    }

    private void setContent(String content) {
        // 变量content绑定到当前线程
        tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i &amp;lt; 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 设置数据
                    demo.setContent(Thread.currentThread().getName() + &quot;的数据&quot;);
                    System.out.println(&quot;-----------------------&quot;);
                    System.out.println(Thread.currentThread().getName() + &quot;---&amp;gt;&quot; + demo.getContent());
                }
            });
            thread.setName(&quot;线程&quot; + i);
            thread.start();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;应用场景&lt;/h5&gt;
&lt;p&gt;ThreadLocal 适用于下面两种场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个线程需要有自己单独的实例&lt;/li&gt;
&lt;li&gt;实例需要在多个方法中共享，但不希望被多线程共享&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ThreadLocal 方案有两个突出的优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;传递数据：保存每个线程绑定的数据，在需要的地方可以直接获取，避免参数直接传递带来的代码耦合问题&lt;/li&gt;
&lt;li&gt;线程隔离：各线程之间的数据相互隔离却又具备并发性，避免同步方式带来的性能损失&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ThreadLocal 用于数据连接的事务管理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class JdbcUtils {
    // ThreadLocal对象，将connection绑定在当前线程中
    private static final ThreadLocal&amp;lt;Connection&amp;gt; tl = new ThreadLocal();
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
            //如果没有，则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中，非常重要的操作
            tl.set(conn);
        }
        return conn;
    }
 // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadLocalDateUtil {
    private static ThreadLocal&amp;lt;DateFormat&amp;gt; threadLocal = new ThreadLocal&amp;lt;DateFormat&amp;gt;() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;h5&gt;底层结构&lt;/h5&gt;
&lt;p&gt;JDK8 以前：每个 ThreadLocal 都创建一个 Map，然后用线程作为 Map 的 key，要存储的局部变量作为 Map 的 value，达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露，因为 Thread 停止后无法通过 key 删除对应的数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84JDK8%E5%89%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK8 以后：每个 Thread 维护一个 ThreadLocalMap，这个 Map 的 key 是 ThreadLocal 实例本身，value 是真正要存储的值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个 Thread 线程内部都有一个 Map (ThreadLocalMap)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Map 里面存储 ThreadLocal 对象（key）和线程的私有变量（value）&lt;/li&gt;
&lt;li&gt;Thread 内部的 Map 是由 ThreadLocal 维护的，由 ThreadLocal 负责向 map 获取和设置线程的变量值&lt;/li&gt;
&lt;li&gt;对于不同的线程，每次获取副本值时，别的线程并不能获取到当前线程的副本值，形成副本的隔离，互不干扰&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84JDK8%E5%90%8E.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK8 前后对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 Map 存储的 Entry 数量会变少，因为之前的存储数量由 Thread 的数量决定，现在由 ThreadLocal 的数量决定，在实际编程当中，往往 ThreadLocal 的数量要少于 Thread 的数量&lt;/li&gt;
&lt;li&gt;当 Thread 销毁之后，对应的 ThreadLocalMap 也会随之销毁，能减少内存的使用，&lt;strong&gt;防止内存泄露&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员变量&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Thread 类的相关属性：&lt;strong&gt;每一个线程持有一个 ThreadLocalMap 对象&lt;/strong&gt;，存放由 ThreadLocal 和数据组成的 Entry 键值对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadLocal.ThreadLocalMap threadLocals = null
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 ThreadLocal 对象的哈希值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final int threadLocalHashCode = nextHashCode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;threadLocalHashCode &amp;amp; (table.length - 1)&lt;/code&gt; 计算当前 entry 需要存放的位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static AtomicInteger nextHashCode = new AtomicInteger()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;斐波那契数也叫黄金分割数，hash 的&lt;strong&gt;增量&lt;/strong&gt;就是这个数字，带来的好处是 hash 分布非常均匀：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final int HASH_INCREMENT = 0x61c88647
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;方法都是线程安全的，因为 ThreadLocal 属于一个线程的，ThreadLocal 中的方法，逻辑都是获取当前线程维护的 ThreadLocalMap 对象，然后进行数据的增删改查，没有指定初始值的 threadlcoal 对象默认赋值为 null&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;initialValue()：返回该线程局部变量的初始值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;延迟调用的方法，在执行 get 方法时才执行&lt;/li&gt;
&lt;li&gt;该方法缺省（默认）实现直接返回一个 null&lt;/li&gt;
&lt;li&gt;如果想要一个初始值，可以重写此方法， 该方法是一个 &lt;code&gt;protected&lt;/code&gt; 的方法，为了让子类覆盖而设计的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected T initialValue() {
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nextHashCode()：计算哈希值，ThreadLocal 的散列方式称之为&lt;strong&gt;斐波那契散列&lt;/strong&gt;，每次获取哈希值都会加上 HASH_INCREMENT，这样做可以尽量避免 hash 冲突，让哈希值能均匀的分布在 2 的 n 次方的数组中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int nextHashCode() {
    // 哈希值自增一个 HASH_INCREMENT 数值
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;set()：修改当前线程与当前 threadlocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否存在
    if (map != null)
        // 调用 threadLocalMap.set 方法进行重写或者添加
        map.set(this, value);
    else
        // map 为空，调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程，参数2是局部变量
        createMap(t, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 获取当前线程 Thread 对应维护的 ThreadLocalMap 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 创建当前线程Thread对应维护的ThreadLocalMap 
void createMap(Thread t, T firstValue) {
    // 【这里的 this 是调用此方法的 threadLocal】，创建一个新的 Map 并设置第一个数据
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get()：获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
        // 以当前的 ThreadLocal 为 key，调用 getEntry 获取对应的存储实体 e
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 对 e 进行判空 
        if (e != null) {
            // 获取存储实体 e 对应的 value值
            T result = (T)e.value;
            return result;
        }
    }
    /*有两种情况有执行当前代码
      第一种情况: map 不存在，表示此线程没有维护的 ThreadLocalMap 对象
      第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】，就会设置为默认值 */
    // 初始化当前线程与当前 threadLocal 对象相关联的 value
    return setInitialValue();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private T setInitialValue() {
    // 调用initialValue获取初始化的值，此方法可以被子类重写, 如果不重写默认返回 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否初始化过
    if (map != null)
        // 存在则调用 map.set 设置此实体 entry，value 是默认的值
        map.set(this, value);
    else
        // 调用 createMap 进行 ThreadLocalMap 对象的初始化中
        createMap(t, value);
    // 返回线程与当前 threadLocal 关联的局部变量
    return value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：移除当前线程与当前 threadLocal 对象相关联的线程局部变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void remove() {
    // 获取当前线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // map 存在则调用 map.remove，this时当前ThreadLocal，以this为key删除对应的实体
        m.remove(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;LocalMap&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;ThreadLocalMap 是 ThreadLocal 的内部类，没有实现 Map 接口，用独立的方式实现了 Map 的功能，其内部 Entry 也是独立实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;

// 存放数据的table，数组长度必须是2的整次幂。
private Entry[] table;

// 数组里面 entrys 的个数，可以用于判断 table 当前使用量是否超过阈值
private int size = 0;

// 进行扩容的阈值，表使用量大于它的时候进行扩容。
private int threshold;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存储结构 Entry：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Entry 继承 WeakReference，key 是弱引用，目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑&lt;/li&gt;
&lt;li&gt;Entry 限制只能用 ThreadLocal 作为 key，key 为 null (entry.get() == null) 意味着 key 不再被引用，entry 也可以从 table 中清除&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static class Entry extends WeakReference&amp;lt;ThreadLocal&amp;lt;?&amp;gt;&amp;gt; {
    Object value;
    Entry(ThreadLocal&amp;lt;?&amp;gt; k, Object v) {
        // this.referent = referent = key;
        super(k);
        value = v;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：延迟初始化的，线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadLocalMap(ThreadLocal&amp;lt;?&amp;gt; firstKey, Object firstValue) {
    // 初始化table，创建一个长度为16的Entry数组
    table = new Entry[INITIAL_CAPACITY];
    // 【寻址算法】计算索引
    int i = firstKey.threadLocalHashCode &amp;amp; (INITIAL_CAPACITY - 1);
    // 创建 entry 对象，存放到指定位置的 slot 中
    table[i] = new Entry(firstKey, firstValue);
    // 数据总量是 1
    size = 1;
    // 将阈值设置为 （当前数组长度 * 2）/ 3。
    setThreshold(INITIAL_CAPACITY);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;set()：添加数据，ThreadLocalMap 使用&lt;strong&gt;线性探测法来解决哈希冲突&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;该方法会一直探测下一个地址，直到有空的地址后插入，若插入后 Map 数量超过阈值，数组会扩容为原来的 2 倍&lt;/p&gt;
&lt;p&gt;假设当前 table 长度为16，计算出来 key 的 hash 值为 14，如果 table[14] 上已经有值，并且其 key 与当前 key 不一致，那么就发生了 hash 冲突，这个时候将 14 加 1 得到 15，取 table[15] 进行判断，如果还是冲突会回到 0，取 table[0]，以此类推，直到可以插入，可以把 Entry[]  table 看成一个&lt;strong&gt;环形数组&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线性探测法会出现&lt;strong&gt;堆积问题&lt;/strong&gt;，可以采取平方探测法解决&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象，并进行垃圾清理，防止出现内存泄漏&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private void set(ThreadLocal&amp;lt;?&amp;gt; key, Object value) {
    // 获取散列表
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (len-1);
    // 使用线性探测法向后查找元素，碰到 entry 为空时停止探测
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取当前元素 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // ThreadLocal 对应的 key 存在，【直接覆盖之前的值】
        if (k == key) {
            e.value = value;
            return;
        }
        // 【这两个条件谁先成立不一定，所以 replaceStaleEntry 中还需要判断 k == key 的情况】
        
        // key 为 null，但是值不为 null，说明之前的 ThreadLocal 对象已经被回收了，当前是【过期数据】
        if (k == null) {
            // 【碰到一个过期的 slot，当前数据复用该槽位，替换过期数据】
            // 这个方法还进行了垃圾清理动作，防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
 // 逻辑到这说明碰到 slot == null 的位置，则在空元素的位置创建一个新的 Entry
    tab[i] = new Entry(key, value);
    // 数量 + 1
    int sz = ++size;
    
    // 【做一次启发式清理】，如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义，那么进行 rehash
    if (!cleanSomeSlots(i, sz) &amp;amp;&amp;amp; sz &amp;gt;= threshold)
        // 扩容
        rehash();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 获取【环形数组】的下一个索引
private static int nextIndex(int i, int len) {
    // 索引越界后从 0 开始继续获取
    return ((i + 1 &amp;lt; len) ? i + 1 : 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 在指定位置插入指定的数据
private void replaceStaleEntry(ThreadLocal&amp;lt;?&amp;gt; key, Object value, int staleSlot) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
 // 探测式清理的开始下标，默认从当前 staleSlot 开始
    int slotToExpunge = staleSlot;
    // 以当前 staleSlot 开始【向前迭代查找】，找到索引靠前过期数据，找到以后替换 slotToExpunge 值
    // 【保证在一个区间段内，从最前面的过期数据开始清理】
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

 // 以 staleSlot 【向后去查找】，直到碰到 null 为止，还是线性探测
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 获取当前节点的 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
  // 条件成立说明是【替换逻辑】
        if (k == key) {
            e.value = value;
            // 因为本来要在 staleSlot 索引处插入该数据，现在找到了i索引处的key与数据一致
            // 但是 i 位置距离正确的位置更远，因为是向后查找，所以还是要在 staleSlot 位置插入当前 entry
            // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置，
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
   
            // 条件成立说明向前查找过期数据并未找到过期的 entry，但 staleSlot 位置已经不是过期数据了，i 位置才是
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            
            // 【清理过期数据，expungeStaleEntry 探测式清理，cleanSomeSlots 启发式清理】
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
  // 条件成立说明当前遍历的 entry 是一个过期数据，并且该位置前面也没有过期数据
        if (k == null &amp;amp;&amp;amp; slotToExpunge == staleSlot)
            // 探测式清理过期数据的开始下标修改为当前循环的 index，因为 staleSlot 会放入要添加的数据
            slotToExpunge = i;
    }
 // 向后查找过程中并未发现 k == key 的 entry，说明当前是一个【取代过期数据逻辑】
    // 删除原有的数据引用，防止内存泄露
    tab[staleSlot].value = null;
    // staleSlot 位置添加数据，【上面的所有逻辑都不会更改 staleSlot 的值】
    tab[staleSlot] = new Entry(key, value);

    // 条件成立说明除了 staleSlot 以外，还发现其它的过期 slot，所以要【开启清理数据的逻辑】
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static int prevIndex(int i, int len) {
    // 形成一个环绕式的访问，头索引越界后置为尾索引
    return ((i - 1 &amp;gt;= 0) ? i - 1 : len - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getEntry()：ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key，调用 getEntry 获取对应的存储实体 e&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Entry getEntry(ThreadLocal&amp;lt;?&amp;gt; key) {
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (table.length - 1);
    // 访问散列表中指定指定位置的 slot 
    Entry e = table[i];
    // 条件成立，说明 slot 有值并且 key 就是要寻找的 key，直接返回
    if (e != null &amp;amp;&amp;amp; e.get() == key)
        return e;
    else
        // 进行线性探测
        return getEntryAfterMiss(key, i, e);
}
// 线性探测寻址
private Entry getEntryAfterMiss(ThreadLocal&amp;lt;?&amp;gt; key, int i, Entry e) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;

    // 开始遍历，碰到 slot == null 的情况，搜索结束
    while (e != null) {
  // 获取当前 slot 中 entry 对象的 key
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // 条件成立说明找到了，直接返回
        if (k == key)
            return e;
        if (k == null)
             // 过期数据，【探测式过期数据回收】
            expungeStaleEntry(i);
        else
            // 更新 index 继续向后走
            i = nextIndex(i, len);
        // 获取下一个槽位中的 entry
        e = tab[i];
    }
    // 说明当前区段没有找到相应数据
    // 【因为存放数据是线性的向后寻找槽位，都是紧挨着的，不可能越过一个 空槽位 在后面放】，可以减少遍历的次数
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rehash()：触发一次全量清理，如果数组长度大于等于长度的 &lt;code&gt;2/3 * 3/4 = 1/2&lt;/code&gt;，则进行 resize&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void rehash() {
    // 清楚当前散列表内的【所有】过期的数据
    expungeStaleEntries();
    
    // threshold = len * 2 / 3，就是 2/3 * (1 - 1/4)
    if (size &amp;gt;= threshold - threshold / 4)
        resize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    // 【遍历所有的槽位，清理过期数据】
    for (int j = 0; j &amp;lt; len; j++) {
        Entry e = tab[j];
        if (e != null &amp;amp;&amp;amp; e.get() == null)
            expungeStaleEntry(j);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entry &lt;strong&gt;数组为扩容为原来的 2 倍&lt;/strong&gt; ，重新计算 key 的散列值，如果遇到 key 为 null 的情况，会将其 value 也置为 null，帮助 GC&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新数组的长度是老数组的二倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    // 统计新table中的entry数量
    int count = 0;
 // 遍历老表，进行【数据迁移】
    for (int j = 0; j &amp;lt; oldLen; ++j) {
        // 访问老表的指定位置的 entry
        Entry e = oldTab[j];
        // 条件成立说明老表中该位置有数据，可能是过期数据也可能不是
        if (e != null) {
            ThreadLocal&amp;lt;?&amp;gt; k = e.get();
            // 过期数据
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 非过期数据，在新表中进行哈希寻址
                int h = k.threadLocalHashCode &amp;amp; (newLen - 1);
                // 【线程探测】
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 将数据存放到新表合适的 slot 中
                newTab[h] = e;
                count++;
            }
        }
    }
 // 设置下一次触发扩容的指标：threshold = len * 2 / 3;
    setThreshold(newLen);
    size = count;
    // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用
    table = newTab;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;remove()：删除 Entry&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void remove(ThreadLocal&amp;lt;?&amp;gt; key) {
    Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode &amp;amp; (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 找到了对应的 key
        if (e.get() == key) {
            // 设置 key 为 null
            e.clear();
            // 探测式清理
            expungeStaleEntry(i);
            return;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;清理方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;探测式清理：沿着开始位置向后探测清理过期数据，沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位，重定位后的元素理论上更接近 &lt;code&gt;i = entry.key &amp;amp; (table.length - 1)&lt;/code&gt;，让&lt;strong&gt;数据的排列更紧凑&lt;/strong&gt;，会优化整个散列表查询性能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// table[staleSlot] 是一个过期数据，以这个位置开始继续向后查找过期数据
private int expungeStaleEntry(int staleSlot) {
    // 获取散列表和数组长度
    Entry[] tab = table;
    int len = tab.length;

    // help gc，先把当前过期的 entry 置空，在取消对 entry 的引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 数量-1
    size--;

    Entry e;
    int i;
    // 从 staleSlot 开始向后遍历，直到碰到 slot == null 结束，【区间内清理过期数据】
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal&amp;lt;?&amp;gt; k = e.get();
        // 当前 entry 是过期数据
        if (k == null) {
            // help gc
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 当前 entry 不是过期数据的逻辑，【rehash】
            // 重新计算当前 entry 对应的 index
            int h = k.threadLocalHashCode &amp;amp; (len - 1);
            // 条件成立说明当前 entry 存储时发生过 hash 冲突，向后偏移过了
            if (h != i) {
                // 当前位置置空
                tab[i] = null;
                // 以正确位置 h 开始，向后查找第一个可以存放 entry 的位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将当前元素放入到【距离正确位置更近的位置，有可能就是正确位置】
                tab[h] = e;
            }
        }
    }
    // 返回 slot = null 的槽位索引，图例是 7，这个索引代表【索引前面的区间已经清理完成垃圾了】
    return i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal探测式清理1.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal探测式清理2.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启发式清理：向后循环扫描过期数据，发现过期数据调用探测式清理方法，如果连续几次的循环都没有发现过期数据，就停止扫描&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//  i 表示启发式清理工作开始位置，一般是空 slot，n 一般传递的是 table.length 
private boolean cleanSomeSlots(int i, int n) {
    // 表示启发式清理工作是否清除了过期数据
    boolean removed = false;
    // 获取当前 map 的散列表引用
    Entry[] tab = table;
    int len = tab.length;
    do {
        // 获取下一个索引，因为探测式返回的 slot 为 null
        i = nextIndex(i, len);
        Entry e = tab[i];
        // 条件成立说明是过期的数据，key 被 gc 了
        if (e != null &amp;amp;&amp;amp; e.get() == null) {
            // 【发现过期数据重置 n 为数组的长度】
            n = len;
            // 表示清理过过期数据
            removed = true;
            // 以当前过期的 slot 为开始节点 做一次探测式清理工作
            i = expungeStaleEntry(i);
        }
        // 假设 table 长度为 16
        // 16 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 8，8 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 4，4 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 2，2 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 1，1 &amp;gt;&amp;gt;&amp;gt; 1 ==&amp;gt; 0
        // 连续经过这么多次循环【没有扫描到过期数据】，就停止循环，扫描到空 slot 不算，因为不是过期数据
    } while ((n &amp;gt;&amp;gt;&amp;gt;= 1) != 0);
    
    // 返回清除标记
    return removed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考视频：&lt;a href=&quot;https://space.bilibili.com/457326371/&quot;&gt;https://space.bilibili.com/457326371/&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;内存泄漏&lt;/h4&gt;
&lt;p&gt;Memory leak：内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放，造成系统内存的浪费，导致程序运行速度减慢甚至系统崩溃等严重后果，内存泄漏的堆积终将导致内存溢出&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 key 使用强引用：使用完 ThreadLocal ，threadLocal Ref 被回收，但是 threadLocalMap 的 Entry 强引用了 threadLocal，造成 threadLocal 无法被回收，无法完全避免内存泄漏&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal内存泄漏强引用.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 key 使用弱引用：使用完 ThreadLocal ，threadLocal Ref 被回收，ThreadLocalMap 只持有 ThreadLocal 的弱引用，所以threadlocal 也可以被回收，此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行，依然存在强引用链，value 不会被回收，而这块 value 永远不会被访问到，也会导致 value 内存泄漏&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal内存泄漏弱引用.png&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两个主要原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有手动删除这个 Entry&lt;/li&gt;
&lt;li&gt;CurrentThread 依然运行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根本原因：ThreadLocalMap 是 Thread的一个属性，&lt;strong&gt;生命周期跟 Thread 一样长&lt;/strong&gt;，如果没有手动删除对应 Entry 就会导致内存泄漏&lt;/p&gt;
&lt;p&gt;解决方法：使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以&lt;/p&gt;
&lt;p&gt;ThreadLocal 内部解决方法：在 ThreadLocalMap 中的 set/getEntry 方法中，通过线性探测法对 key 进行判断，如果 key 为 null（ThreadLocal 为 null）会对 Entry 进行垃圾回收。所以&lt;strong&gt;使用弱引用比强引用多一层保障&lt;/strong&gt;，就算不调用 remove，也有机会进行 GC&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;变量传递&lt;/h4&gt;
&lt;h5&gt;基本使用&lt;/h5&gt;
&lt;p&gt;父子线程：创建子线程的线程是父线程，比如实例中的 main 线程就是父线程&lt;/p&gt;
&lt;p&gt;ThreadLocal 中存储的是线程的局部变量，如果想&lt;strong&gt;实现线程间局部变量传递&lt;/strong&gt;可以使用 InheritableThreadLocal 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ThreadLocal&amp;lt;String&amp;gt; threadLocal = new InheritableThreadLocal&amp;lt;&amp;gt;();
    threadLocal.set(&quot;父线程设置的值&quot;);

    new Thread(() -&amp;gt; System.out.println(&quot;子线程输出：&quot; + threadLocal.get())).start();
}
// 子线程输出：父线程设置的值
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;实现原理&lt;/h5&gt;
&lt;p&gt;InheritableThreadLocal 源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InheritableThreadLocal&amp;lt;T&amp;gt; extends ThreadLocal&amp;lt;T&amp;gt; {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
                  // 该参数默认是 true
                  boolean inheritThreadLocals) {
   // ...
    Thread parent = currentThread();

    // 判断父线程（创建子线程的线程）的 inheritableThreadLocals 属性不为 null
    if (inheritThreadLocals &amp;amp;&amp;amp; parent.inheritableThreadLocals != null) {
        // 复制父线程的 inheritableThreadLocals 属性，实现父子线程局部变量共享
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
    }
    // ..
}
// 【本质上还是创建 ThreadLocalMap，只是把父类中的可继承数据设置进去了】
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 获取父线程的哈希表
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
 // 【逐个复制父线程 ThreadLocalMap 中的数据】
    for (int j = 0; j &amp;lt; len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            ThreadLocal&amp;lt;Object&amp;gt; key = (ThreadLocal&amp;lt;Object&amp;gt;) e.get();
            if (key != null) {
                // 调用的是 InheritableThreadLocal#childValue(T parentValue)
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode &amp;amp; (len - 1);
                // 线性探测
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考文章：&lt;a href=&quot;https://blog.csdn.net/feichitianxia/article/details/110495764&quot;&gt;https://blog.csdn.net/feichitianxia/article/details/110495764&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;线程池&lt;/h2&gt;
&lt;h3&gt;基本概述&lt;/h3&gt;
&lt;p&gt;线程池：一个容纳多个线程的容器，容器中的线程可以重复使用，省去了频繁创建和销毁线程对象的操作&lt;/p&gt;
&lt;p&gt;线程池作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;降低资源消耗，减少了创建和销毁线程的次数，每个工作线程都可以被重复利用，可执行多个任务&lt;/li&gt;
&lt;li&gt;提高响应速度，当任务到达时，如果有线程可以直接用，不会出现系统僵死&lt;/li&gt;
&lt;li&gt;提高线程的可管理性，如果无限制的创建线程，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;线程池的核心思想：&lt;strong&gt;线程复用&lt;/strong&gt;，同一个线程可以被重复使用，来处理多个任务&lt;/p&gt;
&lt;p&gt;池化技术 (Pool) ：一种编程技巧，核心思想是资源复用，在请求量大时能优化应用性能，降低系统频繁建连的资源开销&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;阻塞队列&lt;/h3&gt;
&lt;h4&gt;基本介绍&lt;/h4&gt;
&lt;p&gt;有界队列和无界队列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有界队列：有固定大小的队列，比如设定了固定大小的 LinkedBlockingQueue，又或者大小为 0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无界队列：没有设置固定大小的队列，这些队列可以直接入队，直到溢出（超过 Integer.MAX_VALUE），所以相当于无界&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现：&lt;strong&gt;FIFO 队列&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ArrayBlockQueue：由数组结构组成的有界阻塞队列&lt;/li&gt;
&lt;li&gt;LinkedBlockingQueue：由链表结构组成的无界（默认大小 Integer.MAX_VALUE）的阻塞队列&lt;/li&gt;
&lt;li&gt;PriorityBlockQueue：支持优先级排序的无界阻塞队列&lt;/li&gt;
&lt;li&gt;DelayedWorkQueue：使用优先级队列实现的延迟无界阻塞队列&lt;/li&gt;
&lt;li&gt;SynchronousQueue：不存储元素的阻塞队列，每一个生产线程会阻塞到有一个 put 的线程放入元素为止&lt;/li&gt;
&lt;li&gt;LinkedTransferQueue：由链表结构组成的无界阻塞队列&lt;/li&gt;
&lt;li&gt;LinkedBlockingDeque：由链表结构组成的&lt;strong&gt;双向&lt;/strong&gt;阻塞队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与普通队列（LinkedList、ArrayList等）的不同点在于阻塞队列中阻塞添加和阻塞删除方法，以及线程安全：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞添加 put()：当阻塞队列元素已满时，添加队列元素的线程会被阻塞，直到队列元素不满时才重新唤醒线程执行&lt;/li&gt;
&lt;li&gt;阻塞删除 take()：在队列元素为空时，删除队列元素的线程将被阻塞，直到队列不为空再执行删除操作（一般会返回被删除的元素)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;核心方法&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法类型&lt;/th&gt;
&lt;th&gt;抛出异常&lt;/th&gt;
&lt;th&gt;特殊值&lt;/th&gt;
&lt;th&gt;阻塞&lt;/th&gt;
&lt;th&gt;超时&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;插入（尾）&lt;/td&gt;
&lt;td&gt;add(e)&lt;/td&gt;
&lt;td&gt;offer(e)&lt;/td&gt;
&lt;td&gt;put(e)&lt;/td&gt;
&lt;td&gt;offer(e,time,unit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移除（头）&lt;/td&gt;
&lt;td&gt;remove()&lt;/td&gt;
&lt;td&gt;poll()&lt;/td&gt;
&lt;td&gt;take()&lt;/td&gt;
&lt;td&gt;poll(time,unit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;检查（队首元素）&lt;/td&gt;
&lt;td&gt;element()&lt;/td&gt;
&lt;td&gt;peek()&lt;/td&gt;
&lt;td&gt;不可用&lt;/td&gt;
&lt;td&gt;不可用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;抛出异常组：
&lt;ul&gt;
&lt;li&gt;当阻塞队列满时：在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full&lt;/li&gt;
&lt;li&gt;当阻塞队列空时：再往队列中 remove 移除元素，会抛出 NoSuchException&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊值组：
&lt;ul&gt;
&lt;li&gt;插入方法：成功 true，失败 false&lt;/li&gt;
&lt;li&gt;移除方法：成功返回出队列元素，队列没有就返回 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阻塞组：
&lt;ul&gt;
&lt;li&gt;当阻塞队列满时，生产者继续往队列里 put 元素，队列会一直阻塞生产线程直到队列有空间 put 数据或响应中断退出&lt;/li&gt;
&lt;li&gt;当阻塞队列空时，消费者线程试图从队列里 take 元素，队列会一直阻塞消费者线程直到队列中有可用元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;超时退出：当阻塞队列满时，队里会阻塞生产者线程一定时间，超过限时后生产者线程会退出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;链表队列&lt;/h4&gt;
&lt;h5&gt;入队出队&lt;/h5&gt;
&lt;p&gt;LinkedBlockingQueue 源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LinkedBlockingQueue&amp;lt;E&amp;gt; extends AbstractQueue&amp;lt;E&amp;gt;
   implements BlockingQueue&amp;lt;E&amp;gt;, java.io.Serializable {
 static class Node&amp;lt;E&amp;gt; {
        E item;
        /**
        * 下列三种情况之一
        * - 真正的后继节点
        * - 自己, 发生在出队时
        * - null, 表示是没有后继节点, 是尾节点了
        */
        Node&amp;lt;E&amp;gt; next;

        Node(E x) { item = x; }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;入队：&lt;strong&gt;尾插法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始化链表 &lt;code&gt;last = head = new Node&amp;lt;E&amp;gt;(null)&lt;/code&gt;，&lt;strong&gt;Dummy 节点用来占位&lt;/strong&gt;，item 为 null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public LinkedBlockingQueue(int capacity) {
    // 默认是 Integer.MAX_VALUE
    if (capacity &amp;lt;= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node&amp;lt;E&amp;gt;(null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个节点入队：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void enqueue(Node&amp;lt;E&amp;gt; node) {
    // 从右向左计算
    last = last.next = node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%85%A5%E9%98%9F%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再来一个节点入队 &lt;code&gt;last = last.next = node&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;出队：&lt;strong&gt;出队头节点&lt;/strong&gt;，FIFO&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;出队源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private E dequeue() {
    Node&amp;lt;E&amp;gt; h = head;
    // 获取临头节点
    Node&amp;lt;E&amp;gt; first = h.next;
    // 自己指向自己，help GC
    h.next = h;
    head = first;
    // 出队的元素
    E x = first.item;
    // 【当前节点置为 Dummy 节点】
    first.item = null;
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;h = head&lt;/code&gt; → &lt;code&gt;first = h.next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%87%BA%E9%98%9F%E6%B5%81%E7%A8%8B1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;h.next = h&lt;/code&gt; → &lt;code&gt;head = first&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue%E5%87%BA%E9%98%9F%E6%B5%81%E7%A8%8B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;first.item = null&lt;/code&gt;：当前节点置为 Dummy 节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;加锁分析&lt;/h5&gt;
&lt;p&gt;用了两把锁和 dummy 节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用一把锁，同一时刻，最多只允许有一个线程（生产者或消费者，二选一）执行&lt;/li&gt;
&lt;li&gt;用两把锁，同一时刻，可以允许两个线程同时（一个生产者与一个消费者）执行
&lt;ul&gt;
&lt;li&gt;消费者与消费者线程仍然串行&lt;/li&gt;
&lt;li&gt;生产者与生产者线程仍然串行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程安全分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当节点总数大于 2 时（包括 dummy 节点），&lt;strong&gt;putLock 保证的是 last 节点的线程安全，takeLock 保证的是 head 节点的线程安全&lt;/strong&gt;，两把锁保证了入队和出队没有竞争&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当节点总数等于 2 时（即一个 dummy 节点，一个正常节点）这时候，仍然是两把锁锁两个对象，不会竞争&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当节点总数等于 1 时（就一个 dummy 节点）这时 take 线程会被 notEmpty 条件阻塞，有竞争，会阻塞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition(); // 阻塞等待不满，说明已经满了

// 用于 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition(); // 阻塞等待不空，说明已经是空的
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;入队出队：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;put 操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void put(E e) throws InterruptedException {
    // 空指针异常
    if (e == null) throw new NullPointerException();
    int c = -1;
    // 把待添加的元素封装为 node 节点
    Node&amp;lt;E&amp;gt; node = new Node&amp;lt;E&amp;gt;(e);
    // 获取全局生产锁
    final ReentrantLock putLock = this.putLock;
    // count 用来维护元素计数
    final AtomicInteger count = this.count;
    // 获取可打断锁，会抛出异常
    putLock.lockInterruptibly();
    try {
     // 队列满了等待
        while (count.get() == capacity) {
            // 【等待队列不满时，就可以生产数据】，线程处于 Waiting
            notFull.await();
        }
        // 有空位, 入队且计数加一，尾插法
        enqueue(node);
        // 返回自增前的数字
        c = count.getAndIncrement();
        // put 完队列还有空位, 唤醒其他生产 put 线程，唤醒一个减少竞争
        if (c + 1 &amp;lt; capacity)
            notFull.signal();
    } finally {
        // 解锁
        putLock.unlock();
    }
    // c自增前是0，说明生产了一个元素，唤醒一个 take 线程
    if (c == 0)
        signalNotEmpty();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 调用 notEmpty.signal()，而不是 notEmpty.signalAll() 是为了减少竞争，因为只剩下一个元素
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;take 操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public E take() throws InterruptedException {
    E x;
    int c = -1;
    // 元素个数
    final AtomicInteger count = this.count;
    // 获取全局消费锁
    final ReentrantLock takeLock = this.takeLock;
    // 可打断锁
    takeLock.lockInterruptibly();
    try {
        // 没有元素可以出队
        while (count.get() == 0) {
            // 【阻塞等待队列不空，就可以消费数据】，线程处于 Waiting
            notEmpty.await();
        }
        // 出队，计数减一，FIFO，出队头节点
        x = dequeue();
        // 返回自减前的数字
        c = count.getAndDecrement();
        // 队列还有元素
        if (c &amp;gt; 1)
            // 唤醒一个消费take线程
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    // c 是消费前的数据，消费前满了，消费一个后还剩一个空位，唤醒生产线程
    if (c == capacity)
        // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
        signalNotFull();
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;性能比较&lt;/h5&gt;
&lt;p&gt;主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linked 支持有界，Array 强制有界&lt;/li&gt;
&lt;li&gt;Linked 实现是链表，Array 实现是数组&lt;/li&gt;
&lt;li&gt;Linked 是懒惰的，而 Array 需要提前初始化 Node 数组&lt;/li&gt;
&lt;li&gt;Linked 每次入队会生成新 Node，而 Array 的 Node 是提前创建好的&lt;/li&gt;
&lt;li&gt;Linked 两把锁，Array 一把锁&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;同步队列&lt;/h4&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;SynchronousQueue 是一个不存储元素的 BlockingQueue，&lt;strong&gt;每一个生产者必须阻塞匹配到一个消费者&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;运行当前程序的平台拥有 CPU 的数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int NCPUS = Runtime.getRuntime().availableProcessors()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定超时时间后，当前线程最大自旋次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只有一个 CPU 时自旋次数为 0，所有程序都是串行执行，多核 CPU 时自旋 32 次是一个经验值
static final int maxTimedSpins = (NCPUS &amp;lt; 2) ? 0 : 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自旋的原因：线程挂起唤醒需要进行上下文切换，涉及到用户态和内核态的转变，是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到，如果自旋期间被匹配到，那么直接就返回了，如果自旋次数达到某个指标后，还是会将当前线程挂起&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未指定超时时间，当前线程最大自旋次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final int maxUntimedSpins = maxTimedSpins * 16; // maxTimedSpins 的 16 倍
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定超时限制的阈值，小于该值的线程不会被挂起：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final long spinForTimeoutThreshold = 1000L; // 纳秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超时时间设置的小于该值，就会被禁止挂起，阻塞再唤醒的成本太高，不如选择自旋空转&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private transient volatile Transferer&amp;lt;E&amp;gt; transferer;
abstract static class Transferer&amp;lt;E&amp;gt; {
    /**
    * 参数一：可以为 null，null 时表示这个请求是一个 REQUEST 类型的请求，反之是一个 DATA 类型的请求
    * 参数二：如果为 true 表示指定了超时时间，如果为 false 表示不支持超时，会一直阻塞到匹配或者被打断
    * 参数三：超时时间限制，单位是纳秒
    
    * 返回值：返回值如果不为 null 表示匹配成功，DATA 类型的请求返回当前线程 put 的数据
    *       如果返回 null，表示请求超时或被中断
    */
    abstract E transfer(E e, boolean timed, long nanos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SynchronousQueue(boolean fair) {
    // fair 默认 false
    // 非公平模式实现的数据结构是栈，公平模式的数据结构是队列
    transferer = fair ? new TransferQueue&amp;lt;E&amp;gt;() : new TransferStack&amp;lt;E&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    return transferer.transfer(e, true, 0) != null;
}
public E poll() {
    return transferer.transfer(null, true, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;非公实现&lt;/h5&gt;
&lt;p&gt;TransferStack 是非公平的同步队列，因为所有的请求都被压入栈中，栈顶的元素会最先得到匹配，造成栈底的等待线程饥饿&lt;/p&gt;
&lt;p&gt;TransferStack 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示 Node 类型为请求类型
static final int REQUEST    = 0;
// 表示 Node类 型为数据类型
static final int DATA       = 1;
// 表示 Node 类型为匹配中类型
// 假设栈顶元素为 REQUEST-NODE，当前请求类型为 DATA，入栈会修改类型为 FULFILLING 【栈顶 &amp;amp; 栈顶之下的一个node】
// 假设栈顶元素为 DATA-NODE，当前请求类型为 REQUEST，入栈会修改类型为 FULFILLING 【栈顶 &amp;amp; 栈顶之下的一个node】
static final int FULFILLING = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;栈顶元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile SNode head;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内部类 SNode：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class SNode {
    // 指向下一个栈帧
    volatile SNode next; 
    // 与当前 node 匹配的节点
    volatile SNode match;
    // 假设当前node对应的线程自旋期间未被匹配成功，那么node对应的线程需要挂起，
    // 挂起前 waiter 保存对应的线程引用，方便匹配成功后，被唤醒。
    volatile Thread waiter;
    
    // 数据域，不为空表示当前 Node 对应的请求类型为 DATA 类型，反之则表示 Node 为 REQUEST 类型
    Object item; 
    // 表示当前Node的模式 【DATA/REQUEST/FULFILLING】
    int mode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SNode(Object item) {
    this.item = item;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置方法：设置 Node 对象的 next 字段，此处&lt;strong&gt;对 CAS 进行了优化&lt;/strong&gt;，提升了 CAS 的效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean casNext(SNode cmp, SNode val) {
    //【优化：cmp == next】，可以提升一部分性能。 cmp == next 不相等，就没必要走 cas指令。
    return cmp == next &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匹配方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean tryMatch(SNode s) {
    // 当前 node 尚未与任何节点发生过匹配，CAS 设置 match 字段为 s 节点，表示当前 node 已经被匹配
    if (match == null &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
        // 当前 node 如果自旋结束，会 park 阻塞，阻塞前将 node 对应的 Thread 保留到 waiter 字段
        // 获取当前 node 对应的阻塞线程
        Thread w = waiter;
        // 条件成立说明 node 对应的 Thread 正在阻塞
        if (w != null) {
            waiter = null;
            // 使用 unpark 方式唤醒线程
            LockSupport.unpark(w);
        }
        return true;
    }
    // 匹配成功返回 true
    return match == s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 取消节点的方法
void tryCancel() {
    // match 字段指向自己，表示这个 node 是取消状态，取消状态的 node，最终会被强制移除出栈
    UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}

boolean isCancelled() {
    return match == this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferStack 类成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;snode()：填充节点方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static SNode snode(SNode s, Object e, SNode next, int mode) {
    // 引用指向空时，snode 方法会创建一个 SNode 对象 
    if (s == null) s = new SNode(e);
    // 填充数据
    s.mode = mode;
    s.next = next;
    return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;transfer()：核心方法，请求匹配出栈，不匹配阻塞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E transfer(E e, boolean timed, long nanos) {
 // 包装当前线程的 node
    SNode s = null;
    // 根据元素判断当前的请求类型
    int mode = (e == null) ? REQUEST : DATA;
 // 自旋
    for (;;) {
        // 获取栈顶指针
        SNode h = head;
       // 【CASE1】：当前栈为空或者栈顶 node 模式与当前请求模式一致无法匹配，做入栈操作
        if (h == null || h.mode == mode) {
            // 当前请求是支持超时的，但是 nanos &amp;lt;= 0 说明这个请求不支持 “阻塞等待”
            if (timed &amp;amp;&amp;amp; nanos &amp;lt;= 0) { 
                // 栈顶元素是取消状态
                if (h != null &amp;amp;&amp;amp; h.isCancelled())
                    // 栈顶出栈，设置新的栈顶
                    casHead(h, h.next);
                else
                    // 表示【匹配失败】
                    return null;
            // 入栈
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                // 等待被匹配的逻辑，正常情况返回匹配的节点；取消情况返回当前节点，就是 s
                SNode m = awaitFulfill(s, timed, nanos);
                // 说明当前 node 是【取消状态】
                if (m == s) { 
                    // 将取消节点出栈
                    clean(s);
                    return null;
                }
                // 执行到这说明【匹配成功】了
                // 栈顶有节点并且 匹配节点还未出栈，需要协助出栈
                if ((h = head) != null &amp;amp;&amp;amp; h.next == s)
                    casHead(h, s.next);
                // 当前 node 模式为 REQUEST 类型，返回匹配节点的 m.item 数据域
                // 当前 node 模式为 DATA 类型：返回 node.item 数据域，当前请求提交的数据 e
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        // 【CASE2】：逻辑到这说明请求模式不一致，如果栈顶不是 FULFILLING 说明没被其他节点匹配，【当前可以匹配】
        } else if (!isFulfilling(h.mode)) {
            // 头节点是取消节点，match 指向自己，协助出栈
            if (h.isCancelled())
                casHead(h, h.next);
            // 入栈当前请求的节点
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { 
                    // m 是 s 的匹配的节点
                    SNode m = s.next;
                    // m 节点在 awaitFulfill 方法中被中断，clean 了自己
                    if (m == null) {
                        // 清空栈
                        casHead(s, null);
                        s = null;
                        // 返回到外层自旋中
                        break;
                    }
                    // 获取匹配节点的下一个节点
                    SNode mn = m.next;
                    // 尝试匹配，【匹配成功】，则将 fulfilling 和 m 一起出栈，并且唤醒被匹配的节点的线程
                    if (m.tryMatch(s)) {
                        casHead(s, mn);
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else
                        // 匹配失败，出栈 m
                        s.casNext(m, mn);
                }
            }
        // 【CASE3】：栈顶模式为 FULFILLING 模式，表示【栈顶和栈顶下面的节点正在发生匹配】，当前请求需要做协助工作
        } else {
            // h 表示的是 fulfilling 节点，m 表示 fulfilling 匹配的节点
            SNode m = h.next;
            if (m == null)
                // 清空栈
                casHead(h, null);
            else {
                SNode mn = m.next;
                // m 和 h 匹配，唤醒 m 中的线程
                if (m.tryMatch(h))
                    casHead(h, mn);
                else
                    h.casNext(m, mn);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;awaitFulfill()：阻塞当前线程等待被匹配，返回匹配的节点，或者被取消的节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    // 等待的截止时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 当前线程
    Thread w = Thread.currentThread();
    // 表示当前请求线程在下面的 for(;;) 自旋检查的次数
    int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    // 自旋检查逻辑：是否匹配、是否超时、是否被中断
    for (;;) {
        // 当前线程收到中断信号，需要设置 node 状态为取消状态
        if (w.isInterrupted())
            s.tryCancel();
        // 获取与当前 s 匹配的节点
        SNode m = s.match;
        if (m != null)
            // 可能是正常的匹配的，也可能是取消的
            return m;
        // 执行了超时限制就判断是否超时
        if (timed) {
            nanos = deadline - System.nanoTime();
            // 【超时了，取消节点】
            if (nanos &amp;lt;= 0L) {
                s.tryCancel();
                continue;
            }
        }
        // 说明当前线程还可以进行自旋检查
        if (spins &amp;gt; 0)
            // 自旋一次 递减 1
            spins = shouldSpin(s) ? (spins - 1) : 0;
        // 说明没有自旋次数了
        else if (s.waiter == null)
            //【把当前 node 对应的 Thread 保存到 node.waiter 字段中，要阻塞了】
            s.waiter = w;
        // 没有超时限制直接阻塞
        else if (!timed)
            LockSupport.park(this);
        // nanos &amp;gt; 1000 纳秒的情况下，才允许挂起当前线程
        else if (nanos &amp;gt; spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;boolean shouldSpin(SNode s) {
    // 获取栈顶
    SNode h = head;
    // 条件一成立说明当前 s 就是栈顶，允许自旋检查
    // 条件二成立说明当前 s 节点自旋检查期间，又来了一个与当前 s 节点匹配的请求，双双出栈后条件会成立
    // 条件三成立前提当前 s 不是栈顶元素，并且当前栈顶正在匹配中，这种状态栈顶下面的元素，都允许自旋检查
    return (h == s || h == null || isFulfilling(h.mode));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;clear()：指定节点出栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void clean(SNode s) {
    // 清空数据域和关联线程
    s.item = null;
    s.waiter = null;
    
 // 获取取消节点的下一个节点
    SNode past = s.next;
    // 判断后继节点是不是取消节点，是就更新 past
    if (past != null &amp;amp;&amp;amp; past.isCancelled())
        past = past.next;

    SNode p;
    // 从栈顶开始向下检查，【将栈顶开始向下的 取消状态 的节点全部清理出去】，直到碰到 past 或者不是取消状态为止
    while ((p = head) != null &amp;amp;&amp;amp; p != past &amp;amp;&amp;amp; p.isCancelled())
        // 修改的是内存地址对应的值，p 指向该内存地址所以数据一直在变化
        casHead(p, p.next);
 // 说明中间遇到了不是取消状态的节点，继续迭代下去
    while (p != null &amp;amp;&amp;amp; p != past) {
        SNode n = p.next;
        if (n != null &amp;amp;&amp;amp; n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;公平实现&lt;/h5&gt;
&lt;p&gt;TransferQueue 是公平的同步队列，采用 FIFO 的队列实现，请求节点与队尾模式不同，需要与队头发生匹配&lt;/p&gt;
&lt;p&gt;TransferQueue 类成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;指向队列的 dummy 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode head;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指向队列的尾节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode tail;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;被清理节点的前驱节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transient volatile QNode cleanMe;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;入队操作是两步完成的，第一步是 t.next = newNode，第二步是 tail = newNode，所以队尾节点出队，是一种非常特殊的情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferQueue 内部类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QNode：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class QNode {
    // 指向当前节点的下一个节点
    volatile QNode next;
    // 数据域，Node 代表的是 DATA 类型 item 表示数据，否则 Node 代表的 REQUEST 类型，item == null
    volatile Object item;
    // 假设当前 node 对应的线程自旋期间未被匹配成功，那么 node 对应的线程需要挂起，
    // 挂起前 waiter 保存对应的线程引用，方便匹配成功后被唤醒。
    volatile Thread waiter;
    // true 当前 Node 是一个 DATA 类型，false 表示当前 Node 是一个 REQUEST 类型
    final boolean isData;

 // 构建方法
    QNode(Object item, boolean isData) {
        this.item = item;
        this.isData = isData;
    }

    // 尝试取消当前 node，取消状态的 node 的 item 域指向自己
    void tryCancel(Object cmp) {
        UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
    }

    // 判断当前 node 是否为取消状态
    boolean isCancelled() {
        return item == this;
    }

    // 判断当前节点是否 “不在” 队列内，当 next 指向自己时，说明节点已经出队。
    boolean isOffList() {
        return next == this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TransferQueue 类成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置头尾节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void advanceHead(QNode h, QNode nh) {
    // 设置头指针指向新的节点，
    if (h == head &amp;amp;&amp;amp; UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
        // 老的头节点出队
        h.next = h;
}
void advanceTail(QNode t, QNode nt) {
    if (tail == t)
        // 更新队尾节点为新的队尾
        UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;transfer()：核心方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E transfer(E e, boolean timed, long nanos) {
    // s 指向当前请求对应的 node
    QNode s = null;
    // 是否是 DATA 类型的请求
    boolean isData = (e != null);
 // 自旋
    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)
            continue;
  // head 和 tail 同时指向 dummy 节点，说明是空队列
        // 队尾节点与当前请求类型是一致的情况，说明阻塞队列中都无法匹配，
        if (h == t || t.isData == isData) {
            // 获取队尾 t 的 next 节点
            QNode tn = t.next;
            // 多线程环境中其他线程可能修改尾节点
            if (t != tail)
                continue;
            // 已经有线程入队了，更新 tail
            if (tn != null) {
                advanceTail(t, tn);
                continue;
            }
            // 允许超时，超时时间小于 0，这种方法不支持阻塞等待
            if (timed &amp;amp;&amp;amp; nanos &amp;lt;= 0)
                return null;
            // 创建 node 的逻辑
            if (s == null)
                s = new QNode(e, isData);
            // 将 node 添加到队尾
            if (!t.casNext(null, s))
                continue;
   // 更新队尾指针
            advanceTail(t, s);
            
            // 当前节点 等待匹配....
            Object x = awaitFulfill(s, e, timed, nanos);
            
            // 说明【当前 node 状态为 取消状态】，需要做出队逻辑
            if (x == s) {
                clean(t, s);
                return null;
            }
   // 说明当前 node 仍然在队列内，匹配成功，需要做出队逻辑
            if (!s.isOffList()) {
                // t 是当前 s 节点的前驱节点，判断 t 是不是头节点，是就更新 dummy 节点为 s 节点
                advanceHead(t, s);
                // s 节点已经出队，所以需要把它的 item 域设置为它自己，表示它是个取消状态
                if (x != null)
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
  // 队尾节点与当前请求节点【互补匹配】
        } else {
            // h.next 节点，【请求节点与队尾模式不同，需要与队头发生匹配】，TransferQueue 是一个【公平模式】
            QNode m = h.next;
            // 并发导致其他线程修改了队尾节点，或者已经把 head.next 匹配走了
            if (t != tail || m == null || h != head)
                continue;
   // 获取匹配节点的数据域保存到 x
            Object x = m.item;
            // 判断是否匹配成功
            if (isData == (x != null) ||
                x == m ||
                !m.casItem(x, e)) {
                advanceHead(h, m);
                continue;
            }
   // 【匹配完成】，将头节点出队，让这个新的头结点成为 dummy 节点
            advanceHead(h, m);
            // 唤醒该匹配节点的线程
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;awaitFulfill()：阻塞当前线程等待被匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    // 表示等待截止时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    // 自选检查的次数
    int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        // 被打断就取消节点
        if (w.isInterrupted())
            s.tryCancel(e);
        // 获取当前 Node 数据域
        Object x = s.item;
        
        // 当前请求为 DATA 模式时：e 请求带来的数据
        // s.item 修改为 this，说明当前 QNode 对应的线程 取消状态
        // s.item 修改为 null 表示已经有匹配节点了，并且匹配节点拿走了 item 数据

        // 当前请求为 REQUEST 模式时：e == null
        // s.item 修改为 this，说明当前 QNode 对应的线程 取消状态
        // s.item != null 且 item != this  表示当前 REQUEST 类型的 Node 已经匹配到 DATA 了 
        if (x != e)
            return x;
        // 超时检查
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos &amp;lt;= 0L) {
                s.tryCancel(e);
                continue;
            }
        }
        // 自旋次数减一
        if (spins &amp;gt; 0)
            --spins;
        // 没有自旋次数了，把当前线程封装进去 waiter
        else if (s.waiter == null)
            s.waiter = w;
        // 阻塞
        else if (!timed)
            LockSupport.park(this);
        else if (nanos &amp;gt; spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;操作Pool&lt;/h3&gt;
&lt;h4&gt;创建方式&lt;/h4&gt;
&lt;h5&gt;Executor&lt;/h5&gt;
&lt;p&gt;存放线程的容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final HashSet&amp;lt;Worker&amp;gt; workers = new HashSet&amp;lt;Worker&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&amp;lt;Runnable&amp;gt; workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;corePoolSize：核心线程数，定义了最小可以同时运行的线程数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;maximumPoolSize：最大线程数，当队列中存放的任务达到队列容量时，当前可以同时运行的数量变为最大线程数，创建线程并立即执行最新的任务，与核心线程数之间的差值又叫救急线程数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;keepAliveTime：救急线程最大存活时间，当线程池中的线程数量大于 &lt;code&gt;corePoolSize&lt;/code&gt; 的时候，如果这时没有新的任务提交，核心线程外的线程不会立即销毁，而是会等到 &lt;code&gt;keepAliveTime&lt;/code&gt; 时间超过销毁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unit：&lt;code&gt;keepAliveTime&lt;/code&gt; 参数的时间单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;workQueue：阻塞队列，存放被提交但尚未被执行的任务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;threadFactory：线程工厂，创建新线程时用到，可以为线程创建时起名字&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;handler：拒绝策略，线程到达最大线程数仍有新任务时会执行拒绝策略&lt;/p&gt;
&lt;p&gt;RejectedExecutionHandler 下有 4 个实现类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbortPolicy：让调用者抛出 RejectedExecutionException 异常，&lt;strong&gt;默认策略&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;CallerRunsPolicy：让调用者运行的调节机制，将某些任务回退到调用者，从而降低新任务的流量&lt;/li&gt;
&lt;li&gt;DiscardPolicy：直接丢弃任务，不予任何处理也不抛出异常&lt;/li&gt;
&lt;li&gt;DiscardOldestPolicy：放弃队列中最早的任务，把当前任务加入队列中尝试再次提交当前任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补充：其他框架拒绝策略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dubbo：在抛出 RejectedExecutionException 异常前记录日志，并 dump 线程栈信息，方便定位问题&lt;/li&gt;
&lt;li&gt;Netty：创建一个新线程来执行任务&lt;/li&gt;
&lt;li&gt;ActiveMQ：带超时等待（60s）尝试放入队列&lt;/li&gt;
&lt;li&gt;PinPoint：它使用了一个拒绝策略链，会逐一尝试策略链中每种拒绝策略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工作原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建线程池，这时没有创建线程（&lt;strong&gt;懒惰&lt;/strong&gt;），等待提交过来的任务请求，调用 execute 方法才会创建线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当调用 execute() 方法添加一个请求任务时，线程池会做如下判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果正在运行的线程数量小于 corePoolSize，那么马上创建线程运行这个任务&lt;/li&gt;
&lt;li&gt;如果正在运行的线程数量大于或等于 corePoolSize，那么将这个任务放入队列&lt;/li&gt;
&lt;li&gt;如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize，那么会创建非核心线程&lt;strong&gt;立刻运行这个任务&lt;/strong&gt;，对于阻塞队列中的任务不公平。这是因为创建每个 Worker（线程）对象会绑定一个初始任务，启动 Worker 时会优先执行&lt;/li&gt;
&lt;li&gt;如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize，那么线程池会启动饱和&lt;strong&gt;拒绝策略&lt;/strong&gt;来执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个线程完成任务时，会从队列中取下一个任务来执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个线程空闲超过一定的时间（keepAliveTime）时，线程池会判断：如果当前运行的线程数大于 corePoolSize，那么这个线程就被停掉，所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;图片来源：&lt;a href=&quot;https://space.bilibili.com/457326371/&quot;&gt;https://space.bilibili.com/457326371/&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;Executors&lt;/h5&gt;
&lt;p&gt;Executors 提供了四种线程池的创建：newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;newFixedThreadPool：创建一个拥有 n 个线程的线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;核心线程数 == 最大线程数（没有救急线程被创建），因此也无需超时时间&lt;/li&gt;
&lt;li&gt;LinkedBlockingQueue 是一个单向链表实现的阻塞队列，默认大小为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;，也就是无界队列，可以放任意数量的任务，在任务比较多的时候会导致 OOM（内存溢出）&lt;/li&gt;
&lt;li&gt;适用于任务量已知，相对耗时的长期任务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;newCachedThreadPool：创建一个可扩容的线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                  new SynchronousQueue&amp;lt;Runnable&amp;gt;());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心线程数是 0， 最大线程数是 29 个 1，全部都是救急线程（60s 后可以回收），可能会创建大量线程，从而导致 &lt;strong&gt;OOM&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SynchronousQueue 作为阻塞队列，没有容量，对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止（类似一手交钱、一手交货）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适合任务数比较密集，但每个任务执行时间较短的情况&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;newSingleThreadExecutor：创建一个只有 1 个线程的单线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;保证所有任务按照&lt;strong&gt;指定顺序执行&lt;/strong&gt;，线程数固定为 1，任务数多于 1 时会放入无界队列排队，任务执行完毕，这唯一的线程也不会被释放&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个单线程串行执行任务，如果任务执行失败而终止那么没有任何补救措施，线程池会新建一个线程，保证池的正常工作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Executors.newSingleThreadExecutor() 线程个数始终为 1，不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式，只对外暴露了 ExecutorService 接口，因此不能调用 ThreadPoolExecutor 中特有的方法&lt;/p&gt;
&lt;p&gt;原因：父类不能直接调用子类中的方法，需要反射或者创建对象的方式，可以调用子类静态方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Executors.newFixedThreadPool(1) 初始时为 1，可以修改。对外暴露的是 ThreadPoolExecutor 对象，可以强转后调用 setCorePoolSize 等方法进行修改&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h5&gt;开发要求&lt;/h5&gt;
&lt;p&gt;阿里巴巴 Java 开发手册要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程资源必须通过线程池提供，不允许在应用中自行显式创建线程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销，解决资源不足的问题&lt;/li&gt;
&lt;li&gt;如果不使用线程池，有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池不允许使用 Executors 去创建，而是通过 ThreadPoolExecutor 的方式，这样的处理方式更加明确线程池的运行规则，规避资源耗尽的风险&lt;/p&gt;
&lt;p&gt;Executors 返回的线程池对象弊端如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FixedThreadPool 和 SingleThreadPool：请求队列长度为 Integer.MAX_VALUE，可能会堆积大量的请求，从而导致 OOM&lt;/li&gt;
&lt;li&gt;CacheThreadPool 和 ScheduledThreadPool：允许创建线程数量为 Integer.MAX_VALUE，可能会创建大量的线程，导致 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建多大容量的线程池合适？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一般来说池中&lt;strong&gt;总线程数是核心池线程数量两倍&lt;/strong&gt;，确保当核心池有线程停止时，核心池外有线程进入核心池&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过小会导致程序不能充分地利用系统资源、容易导致饥饿&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过大会导致更多的线程上下文切换，占用更多内存&lt;/p&gt;
&lt;p&gt;上下文切换：当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态，以便下次再切换回这个任务时，可以再加载这个任务的状态，任务从保存到再加载的过程就是一次上下文切换&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心线程数常用公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CPU 密集型任务 (N+1)：&lt;/strong&gt; 这种任务消耗的是 CPU 资源，可以将核心线程数设置为 N (CPU 核心数) + 1，比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断，或者其它原因导致的任务暂停而带来的影响。一旦任务暂停，CPU 某个核心就会处于空闲状态，而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间&lt;/p&gt;
&lt;p&gt;CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;I/O 密集型任务：&lt;/strong&gt; 这种系统 CPU 处于阻塞状态，用大部分的时间来处理 I/O 交互，而线程在处理 I/O 的时间段内不会占用 CPU 来处理，这时就可以将 CPU 交出给其它线程使用，因此在 I/O 密集型任务的应用中，我们可以多配置一些线程，具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数)，阻塞系数在 0.8~0.9 之间&lt;/p&gt;
&lt;p&gt;IO 密集型就是涉及到网络读取，文件读取此类任务 ，特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少，大部分时间都花在了等待 IO 操作完成上&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;提交方法&lt;/h4&gt;
&lt;p&gt;ExecutorService 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void execute(Runnable command)&lt;/td&gt;
&lt;td&gt;执行任务（Executor 类 API）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future&amp;lt;?&amp;gt; submit(Runnable task)&lt;/td&gt;
&lt;td&gt;提交任务 task()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future submit(Callable&amp;lt;T&amp;gt; task)&lt;/td&gt;
&lt;td&gt;提交任务 task，用返回值 Future 获得任务执行结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks, long timeout, TimeUnit unit)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务，超时时间针对所有task，超时会取消没有执行完的任务，并抛出超时异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T invokeAny(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)&lt;/td&gt;
&lt;td&gt;提交 tasks 中所有任务，哪个任务先成功执行完毕，返回此任务执行结果，其它任务取消&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;execute 和 submit 都属于线程池的方法，对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;execute 只能执行 Runnable 类型的任务，没有返回值； submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务，底层是&lt;strong&gt;封装成 FutureTask，然后调用 execute 执行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;execute 会直接抛出任务执行时的异常，submit 会吞掉异常，可通过 Future 的 get 方法将任务执行时的异常重新抛出&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;关闭方法&lt;/h4&gt;
&lt;p&gt;ExecutorService 类 API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void shutdown()&lt;/td&gt;
&lt;td&gt;线程池状态变为 SHUTDOWN，等待任务执行完后关闭线程池，不会接收新任务，但已提交任务会执行完，而且也可以添加线程（不绑定任务）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&amp;lt;Runnable&amp;gt; shutdownNow()&lt;/td&gt;
&lt;td&gt;线程池状态变为 STOP，用 interrupt 中断正在执行的任务，直接关闭线程池，不会接收新任务，会将队列中的任务返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isShutdown()&lt;/td&gt;
&lt;td&gt;不在 RUNNING 状态的线程池，此执行者已被关闭，方法返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isTerminated()&lt;/td&gt;
&lt;td&gt;线程池状态是否是 TERMINATED，如果所有任务在关闭后完成，返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean awaitTermination(long timeout, TimeUnit unit)&lt;/td&gt;
&lt;td&gt;调用 shutdown 后，由于调用线程不会等待所有任务运行结束，如果它想在线程池 TERMINATED 后做些事情，可以利用此方法等待&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;处理异常&lt;/h4&gt;
&lt;p&gt;execute 会直接抛出任务执行时的异常，submit 会吞掉异常，有两种处理方法&lt;/p&gt;
&lt;p&gt;方法 1：主动捉异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(1);
pool.submit(() -&amp;gt; {
    try {
        System.out.println(&quot;task1&quot;);
        int i = 1 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法 2：使用 Future 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreadPool(1);
Future&amp;lt;?&amp;gt; future = pool.submit(() -&amp;gt; {
    System.out.println(&quot;task1&quot;);
    int i = 1 / 0;
    return true;
});
System.out.println(future.get());
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;工作原理&lt;/h3&gt;
&lt;h4&gt;状态信息&lt;/h4&gt;
&lt;p&gt;ThreadPoolExecutor 使用 int 的&lt;strong&gt;高 3 位来表示线程池状态，低 29 位表示线程数量&lt;/strong&gt;。这些信息存储在一个原子变量 ctl 中，目的是将线程池状态与线程个数合二为一，这样就可以用一次 CAS 原子操作进行赋值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;状态表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 高3位：表示当前线程池运行状态，除去高3位之后的低位：表示当前线程池中所拥有的线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 表示在 ctl 中，低 COUNT_BITS 位，是用于存放当前线程数量的位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 低 COUNT_BITS 位所能表达的最大数值，000 11111111111111111111 =&amp;gt; 5亿多
private static final int CAPACITY   = (1 &amp;lt;&amp;lt; COUNT_BITS) - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;四种状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 111 000000000000000000，转换成整数后其实就是一个【负数】
private static final int RUNNING    = -1 &amp;lt;&amp;lt; COUNT_BITS;
// 000 000000000000000000
private static final int SHUTDOWN   =  0 &amp;lt;&amp;lt; COUNT_BITS;
// 001 000000000000000000
private static final int STOP       =  1 &amp;lt;&amp;lt; COUNT_BITS;
// 010 000000000000000000
private static final int TIDYING    =  2 &amp;lt;&amp;lt; COUNT_BITS;
// 011 000000000000000000
private static final int TERMINATED =  3 &amp;lt;&amp;lt; COUNT_BITS;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;高3位&lt;/th&gt;
&lt;th&gt;接收新任务&lt;/th&gt;
&lt;th&gt;处理阻塞任务队列&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RUNNING&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHUTDOWN&lt;/td&gt;
&lt;td&gt;000&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;不接收新任务，但处理阻塞队列剩余任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STOP&lt;/td&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;中断正在执行的任务，并抛弃阻塞队列任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIDYING&lt;/td&gt;
&lt;td&gt;010&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;任务全执行完毕，活动线程为 0 即将进入终结&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TERMINATED&lt;/td&gt;
&lt;td&gt;011&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;终止状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取当前线程池运行状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~CAPACITY = ~000 11111111111111111111 = 111 000000000000000000000（取反）
// c == ctl = 111 000000000000000000111
// 111 000000000000000000111
// 111 000000000000000000000
// 111 000000000000000000000 获取到了运行状态
private static int runStateOf(int c)     { return c &amp;amp; ~CAPACITY; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取当前线程池线程数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//        c = 111 000000000000000000111
// CAPACITY = 000 111111111111111111111
//            000 000000000000000000111 =&amp;gt; 7
private static int workerCountOf(int c)  { return c &amp;amp; CAPACITY; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重置当前线程池状态 ctl：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rs 表示线程池状态，wc 表示当前线程池中 worker（线程）数量，相与以后就是合并后的状态
private static int ctlOf(int rs, int wc) { return rs | wc; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较当前线程池 ctl 所表示的状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 比较当前线程池 ctl 所表示的状态，是否小于某个状态 s
// 状态对比：RUNNING &amp;lt; SHUTDOWN &amp;lt; STOP &amp;lt; TIDYING &amp;lt; TERMINATED
private static boolean runStateLessThan(int c, int s) { return c &amp;lt; s; }
// 比较当前线程池 ctl 所表示的状态，是否大于等于某个状态s
private static boolean runStateAtLeast(int c, int s) { return c &amp;gt;= s; }
// 小于 SHUTDOWN 的一定是 RUNNING，SHUTDOWN == 0
private static boolean isRunning(int c) { return c &amp;lt; SHUTDOWN; }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置线程池 ctl：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 CAS 方式 让 ctl 值 +1 ，成功返回 true, 失败返回 false
private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}
// 使用 CAS 方式 让 ctl 值 -1 ，成功返回 true, 失败返回 false
private boolean compareAndDecrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect - 1);
}
// 将 ctl 值减一，do while 循环会一直重试，直到成功为止
private void decrementWorkerCount() {
    do {} while (!compareAndDecrementWorkerCount(ctl.get()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员属性&lt;/h4&gt;
&lt;p&gt;成员变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程池中存放 Worker 的容器&lt;/strong&gt;：线程池没有初始化，直接往池中加线程即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final HashSet&amp;lt;Worker&amp;gt; workers = new HashSet&amp;lt;Worker&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程全局锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 增加减少 worker 或者时修改线程池运行状态需要持有 mainLock
private final ReentrantLock mainLock = new ReentrantLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可重入锁的条件变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当外部线程调用 awaitTermination() 方法时，会等待当前线程池状态为 Termination 为止
private final Condition termination = mainLock.newCondition()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程池相关参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile int corePoolSize;    // 核心线程数量
private volatile int maximumPoolSize;   // 线程池最大线程数量
private volatile long keepAliveTime;   // 空闲线程存活时间
private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂，默认是 DefaultThreadFactory
private final BlockingQueue&amp;lt;Runnable&amp;gt; workQueue;// 【超过核心线程提交任务就放入 阻塞队列】
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;private volatile RejectedExecutionHandler handler; // 拒绝策略，juc包提供了4中方式
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认策略
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录线程池相关属性的数值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int largestPoolSize;  // 记录线程池生命周期内线程数最大值
private long completedTaskCount; // 记录线程池所完成任务总数，当某个 worker 退出时将完成的任务累加到该属性
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制&lt;strong&gt;核心线程数量内的线程是否可以被回收&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// false（默认）代表不可以，为 true 时核心线程空闲超过 keepAliveTime 也会被回收
// allowCoreThreadTimeOut(boolean value) 方法可以设置该值
private volatile boolean allowCoreThreadTimeOut;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内部类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Worker 类：&lt;strong&gt;每个 Worker 对象会绑定一个初始任务&lt;/strong&gt;，启动 Worker 时优先执行，这也是造成线程池不公平的原因。Worker 继承自 AQS，本身具有锁的特性，采用独占锁模式，state = 0 表示未被占用，&amp;gt; 0 表示被占用，&amp;lt; 0 表示初始状态不能被抢锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
 final Thread thread;   // worker 内部封装的工作线程
    Runnable firstTask;    // worker 第一个执行的任务，普通的 Runnable 实现类或者是 FutureTask
    volatile long completedTasks; // 记录当前 worker 所完成任务数量
    
    // 构造方法
    Worker(Runnable firstTask) {
        // 设置AQS独占模式为初始化中状态，这个状态不能被抢占锁
        setState(-1);
        // firstTask不为空时，当worker启动后，内部线程会优先执行firstTask，执行完后会到queue中去获取下个任务
        this.firstTask = firstTask;
        // 使用线程工厂创建一个线程，并且【将当前worker指定为Runnable】，所以thread启动时会调用 worker.run()
        this.thread = getThreadFactory().newThread(this);
    }
    // 【不可重入锁】
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public Thread newThread(Runnable r) {
    // 将当前 worker 指定为 thread 的执行方法，线程调用 start 会调用 r.run()
    Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拒绝策略相关的内部类&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员方法&lt;/h4&gt;
&lt;h5&gt;提交方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AbstractExecutorService#submit()：提交任务，&lt;strong&gt;把 Runnable 或 Callable 任务封装成 FutureTask 执行&lt;/strong&gt;，可以通过方法返回的任务对象，调用 get 阻塞获取任务执行的结果或者异常，源码分析在笔记的 Future 部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Future&amp;lt;?&amp;gt; submit(Runnable task) {
    // 空指针异常
    if (task == null) throw new NullPointerException();
    // 把 Runnable 封装成未来任务对象，执行结果就是 null，也可以通过参数指定 FutureTask#get 返回数据
    RunnableFuture&amp;lt;Void&amp;gt; ftask = newTaskFor(task, null);
    // 执行方法
    execute(ftask);
    return ftask;
}
public &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Callable&amp;lt;T&amp;gt; task) {
    if (task == null) throw new NullPointerException();
    // 把 Callable 封装成未来任务对象
    RunnableFuture&amp;lt;T&amp;gt; ftask = newTaskFor(task);
    // 执行方法
    execute(ftask); 
    // 返回未来任务对象，用来获取返回值
    return ftask;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;protected &amp;lt;T&amp;gt; RunnableFuture&amp;lt;T&amp;gt; newTaskFor(Runnable runnable, T value) {
    // Runnable 封装成 FutureTask，【指定返回值】
    return new FutureTask&amp;lt;T&amp;gt;(runnable, value);
}
protected &amp;lt;T&amp;gt; RunnableFuture&amp;lt;T&amp;gt; newTaskFor(Callable&amp;lt;T&amp;gt; callable) {
    // Callable 直接封装成 FutureTask
    return new FutureTask&amp;lt;T&amp;gt;(callable);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;execute()：执行任务，&lt;strong&gt;但是没有返回值，没办法获取任务执行结果&lt;/strong&gt;，出现异常会直接抛出任务执行时的异常。根据线程池中的线程数，选择添加任务时的处理方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// command 可以是普通的 Runnable 实现类，也可以是 FutureTask，不能是 Callable
public void execute(Runnable command) {
    // 非空判断
    if (command == null)
        throw new NullPointerException();
   // 获取 ctl 最新值赋值给 c，ctl 高 3 位表示线程池状态，低位表示当前线程池线程数量。
    int c = ctl.get();
    // 【1】当前线程数量小于核心线程数，此次提交任务直接创建一个新的 worker，线程池中多了一个新的线程
    if (workerCountOf(c) &amp;lt; corePoolSize) {
        // addWorker 为创建线程的过程，会创建 worker 对象并且将 command 作为 firstTask，优先执行
        if (addWorker(command, true))
            return;
        
        // 执行到这条语句，说明 addWorker 一定是失败的，存在并发现象或者线程池状态被改变，重新获取状态
        // SHUTDOWN 状态下也有可能创建成功，前提 firstTask == null 而且当前 queue 不为空（特殊情况）
        c = ctl.get();
    }
    // 【2】执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败
    //  判断当前线程池是否处于running状态，成立就尝试将 task 放入到 workQueue 中
    if (isRunning(c) &amp;amp;&amp;amp; workQueue.offer(command)) {
        int recheck = ctl.get();
        // 条件一成立说明线程池状态被外部线程给修改了，可能是执行了 shutdown() 方法，该状态不能接收新提交的任务
        // 所以要把刚提交的任务删除，删除成功说明提交之后线程池中的线程还未消费（处理）该任务
        if (!isRunning(recheck) &amp;amp;&amp;amp; remove(command))
            // 任务出队成功，走拒绝策略
            reject(command);
        // 执行到这说明线程池是 running 状态，获取线程池中的线程数量，判断是否是 0
        // 【担保机制】，保证线程池在 running 状态下，最起码得有一个线程在工作
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 【3】offer失败说明queue满了
    // 如果线程数量尚未达到 maximumPoolSize，会创建非核心 worker 线程直接执行 command，【这也是不公平的原因】
    // 如果当前线程数量达到 maximumPoolSiz，这里 addWorker 也会失败，走拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;添加线程&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;prestartAllCoreThreads()：&lt;strong&gt;提前预热&lt;/strong&gt;，创建所有的核心线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(null, true))
        ++n;
    return n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addWorker()：&lt;strong&gt;添加线程到线程池&lt;/strong&gt;，返回 true 表示创建 Worker 成功，且线程启动。首先判断线程池是否允许添加线程，允许就让线程数量 + 1，然后去创建 Worker 加入线程池&lt;/p&gt;
&lt;p&gt;注意：SHUTDOWN 状态也能添加线程，但是要求新加的 Woker 没有 firstTask，而且当前 queue 不为空，所以创建一个线程来帮助线程池执行队列中的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// core == true 表示采用核心线程数量限制，false 表示采用 maximumPoolSize
private boolean addWorker(Runnable firstTask, boolean core) {
    // 自旋【判断当前线程池状态是否允许创建线程】，允许就设置线程数量 + 1
    retry:
    for (;;) {
        // 获取 ctl 的值
        int c = ctl.get();
        // 获取当前线程池运行状态
        int rs = runStateOf(c); 
        
        // 判断当前线程池状态【是否允许添加线程】
        
        // 当前线程池是 SHUTDOWN 状态，但是队列里面还有任务尚未处理完，需要处理完 queue 中的任务
        // 【不允许再提交新的 task，所以 firstTask 为空，但是可以继续添加 worker】
        if (rs &amp;gt;= SHUTDOWN &amp;amp;&amp;amp; !(rs == SHUTDOWN &amp;amp;&amp;amp; firstTask == null &amp;amp;&amp;amp; !workQueue.isEmpty()))
            return false;
        for (;;) {
            // 获取线程池中线程数量
            int wc = workerCountOf(c);
            // 条件一一般不成立，CAPACITY是5亿多，根据 core 判断使用哪个大小限制线程数量，超过了返回 false
            if (wc &amp;gt;= CAPACITY || wc &amp;gt;= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 记录线程数量已经加 1，类比于申请到了一块令牌，条件失败说明其他线程修改了数量
            if (compareAndIncrementWorkerCount(c))
                // 申请成功，跳出了 retry 这个 for 自旋
                break retry;
            // CAS 失败，没有成功的申请到令牌
            c = ctl.get();
            // 判断当前线程池状态是否发生过变化，被其他线程修改了，可能其他线程调用了 shutdown() 方法
            if (runStateOf(c) != rs)
                // 返回外层循环检查是否能创建线程，在 if 语句中返回 false
                continue retry;
           
        }
    }
    
    //【令牌申请成功，开始创建线程】
    
 // 运行标记，表示创建的 worker 是否已经启动，false未启动  true启动
    boolean workerStarted = false;
    // 添加标记，表示创建的 worker 是否添加到池子中了，默认false未添加，true是添加。
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 【创建 Worker，底层通过线程工厂 newThread 方法创建执行线程，指定了首先执行的任务】
        w = new Worker(firstTask);
        // 将新创建的 worker 节点中的线程赋值给 t
        final Thread t = w.thread;
        // 这里的判断为了防止 程序员自定义的 ThreadFactory 实现类有 bug，创造不出线程
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            // 加互斥锁，要添加 worker 了
            mainLock.lock();
            try {
                // 获取最新线程池运行状态保存到 rs
                int rs = runStateOf(ctl.get());
    // 判断线程池是否为RUNNING状态，不是再【判断当前是否为SHUTDOWN状态且firstTask为空，特殊情况】
                if (rs &amp;lt; SHUTDOWN || (rs == SHUTDOWN &amp;amp;&amp;amp; firstTask == null)) {
                    // 当线程start后，线程isAlive会返回true，这里还没开始启动线程，如果被启动了就需要报错
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    
                    //【将新建的 Worker 添加到线程池中】
                    workers.add(w);
                    int s = workers.size();
     // 当前池中的线程数量是一个新高，更新 largestPoolSize
                    if (s &amp;gt; largestPoolSize)
                        largestPoolSize = s;
                    // 添加标记置为 true
                    workerAdded = true;
                }
            } finally {
                // 解锁啊
                mainLock.unlock();
            }
            // 添加成功就【启动线程执行任务】
            if (workerAdded) {
                // Thread 类中持有 Runnable 任务对象，调用的是 Runnable 的 run ，也就是 FutureTask
                t.start();
                // 运行标记置为 true
                workerStarted = true;
            }
        }
    } finally {
        // 如果启动线程失败，做清理工作
        if (! workerStarted)
            addWorkerFailed(w);
    }
    // 返回新创建的线程是否启动
    return workerStarted;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addWorkerFailed()：清理任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    // 持有线程池全局锁，因为操作的是线程池相关的东西
    mainLock.lock();
    try {
        //条件成立需要将 worker 在 workers 中清理出去。
        if (w != null)
            workers.remove(w);
        // 将线程池计数 -1，相当于归还令牌。
        decrementWorkerCount();
        // 尝试停止线程池
        tryTerminate();
    } finally {
        //释放线程池全局锁。
        mainLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;运行方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Worker#run：Worker 实现了 Runnable 接口，当线程启动时，会调用 Worker 的 run() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    // ThreadPoolExecutor#runWorker()
    runWorker(this);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;runWorker()：线程启动就要&lt;strong&gt;执行任务&lt;/strong&gt;，会一直 while 循环获取任务并执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final void runWorker(Worker w) {
    Thread wt = Thread.currentThread(); 
    // 获取 worker 的 firstTask
    Runnable task = w.firstTask;
    // 引用置空，【防止复用该线程时重复执行该任务】
    w.firstTask = null;
    // 初始化 worker 时设置 state = -1，表示不允许抢占锁
    // 这里需要设置 state = 0 和 exclusiveOwnerThread = null，开始独占模式抢锁
    w.unlock();
    // true 表示发生异常退出，false 表示正常退出。
    boolean completedAbruptly = true;
    try {
        // firstTask 不是 null 就直接运行，否则去 queue 中获取任务
        // 【getTask 如果是阻塞获取任务，会一直阻塞在take方法，直到获取任务，不会走返回null的逻辑】
        while (task != null || (task = getTask()) != null) {
            // worker 加锁，shutdown 时会判断当前 worker 状态，【根据独占锁状态判断是否空闲】
            w.lock();
            
   // 说明线程池状态大于 STOP，目前处于 STOP/TIDYING/TERMINATION，此时给线程一个中断信号
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 // 说明线程处于 RUNNING 或者 SHUTDOWN 状态，清除打断标记
                 (Thread.interrupted() &amp;amp;&amp;amp; runStateAtLeast(ctl.get(), STOP))) &amp;amp;&amp;amp; !wt.isInterrupted())
                // 中断线程，设置线程的中断标志位为 true
                wt.interrupt();
            try {
                // 钩子方法，【任务执行的前置处理】
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 【执行任务】
                    task.run();
                } catch (Exception x) {
                  //.....
                } finally {
                    // 钩子方法，【任务执行的后置处理】
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;  // 将局部变量task置为null，代表任务执行完成
                w.completedTasks++; // 更新worker完成任务数量
                w.unlock();   // 解锁
            }
        }
        // getTask()方法返回null时会走到这里，表示queue为空并且线程空闲超过保活时间，【当前线程执行退出逻辑】
        completedAbruptly = false; 
    } finally {
        // 正常退出 completedAbruptly = false
        // 异常退出 completedAbruptly = true，【从 task.run() 内部抛出异常】时，跳到这一行
        processWorkerExit(w, completedAbruptly);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unlock()：重置锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unlock() { release(1); }
// 外部不会直接调用这个方法 这个方法是 AQS 内调用的，外部调用 unlock 时触发此方法
protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);  // 设置持有者为 null
    setState(0);      // 设置 state = 0
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;getTask()：获取任务，线程空闲时间超过 keepAliveTime 就会被回收，判断的依据是&lt;strong&gt;当前线程阻塞获取任务超过保活时间&lt;/strong&gt;，方法返回 null 就代表当前线程要被回收了，返回到 runWorker 执行线程退出逻辑。线程池具有担保机制，对于 RUNNING 状态下的超时回收，要保证线程池中最少有一个线程运行，或者任务阻塞队列已经是空&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Runnable getTask() {
    // 超时标记，表示当前线程获取任务是否超时，true 表示已超时
    boolean timedOut = false; 
    for (;;) {
        int c = ctl.get();
        // 获取线程池当前运行状态
        int rs = runStateOf(c);
  
        // 【tryTerminate】打断线程后执行到这，此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空
        // 所以下面的 if 条件一定是成立的，可以直接返回 null，线程就应该退出了
        if (rs &amp;gt;= SHUTDOWN &amp;amp;&amp;amp; (rs &amp;gt;= STOP || workQueue.isEmpty())) {
            // 使用 CAS 自旋的方式让 ctl 值 -1
            decrementWorkerCount();
            return null;
        }
        
  // 获取线程池中的线程数量
        int wc = workerCountOf(c);

        // 线程没有明确的区分谁是核心或者非核心线程，是根据当前池中的线程数量判断
        
        // timed = false 表示当前这个线程 获取task时不支持超时机制的，当前线程会使用 queue.take() 阻塞获取
        // timed = true 表示当前这个线程 获取task时支持超时机制，使用 queue.poll(xxx,xxx) 超时获取
        // 条件一代表允许回收核心线程，那就无所谓了，全部线程都执行超时回收
        // 条件二成立说明线程数量大于核心线程数，当前线程认为是非核心线程，有保活时间，去超时获取任务
        boolean timed = allowCoreThreadTimeOut || wc &amp;gt; corePoolSize;
        
  // 如果线程数量是否超过最大线程数，直接回收
        // 如果当前线程【允许超时回收并且已经超时了】，就应该被回收了，由于【担保机制】还要做判断：
        //    wc &amp;gt; 1 说明线程池还用其他线程，当前线程可以直接回收
        //    workQueue.isEmpty() 前置条件是 wc = 1，【如果当前任务队列也是空了，最后一个线程就可以退出】
        if ((wc &amp;gt; maximumPoolSize || (timed &amp;amp;&amp;amp; timedOut)) &amp;amp;&amp;amp; (wc &amp;gt; 1 || workQueue.isEmpty())) {
            // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程，返回 null，代表可以退出
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 根据当前线程是否需要超时回收，【选择从队列获取任务的方法】是超时获取或者阻塞获取
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
            // 获取到任务返回任务，【阻塞获取会阻塞到获取任务为止】，不会返回 null
            if (r != null)
                return r;
            // 获取任务为 null 说明超时了，将超时标记设置为 true，下次自旋时返 null
            timedOut = true;
        } catch (InterruptedException retry) {
            // 阻塞线程被打断后超时标记置为 false，【说明被打断不算超时】，要继续获取，直到超时或者获取到任务
            // 如果线程池 SHUTDOWN 状态下的打断，会在循环获取任务前判断，返回 null
            timedOut = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;processWorkerExit()：&lt;strong&gt;线程退出线程池&lt;/strong&gt;，也有担保机制，保证队列中的任务被执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常退出 completedAbruptly = false，异常退出为 true
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 条件成立代表当前 worker 是发生异常退出的，task 任务执行过程中向上抛出异常了
    if (completedAbruptly) 
        // 从异常时到这里 ctl 一直没有 -1，需要在这里 -1
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    // 加锁
    mainLock.lock();
    try {
        // 将当前 worker 完成的 task 数量，汇总到线程池的 completedTaskCount
        completedTaskCount += w.completedTasks;
  // 将 worker 从线程池中移除
        workers.remove(w);
    } finally {
        mainLock.unlock(); // 解锁
    }
 // 尝试停止线程池，唤醒下一个线程
    tryTerminate();

    int c = ctl.get();
    // 线程池不是停止状态就应该有线程运行【担保机制】
    if (runStateLessThan(c, STOP)) {
        // 正常退出的逻辑，是对空闲线程回收，不是执行出错
        if (!completedAbruptly) {
            // 根据是否回收核心线程确定【线程池中的线程数量最小值】
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 最小值为 0，但是线程队列不为空，需要一个线程来完成任务担保机制
            if (min == 0 &amp;amp;&amp;amp; !workQueue.isEmpty())
                min = 1;
            // 线程池中的线程数量大于最小值可以直接返回
            if (workerCountOf(c) &amp;gt;= min)
                return;
        }
        // 执行 task 时发生异常，有个线程因为异常终止了，需要添加
        // 或者线程池中的数量小于最小值，这里要创建一个新 worker 加进线程池
        addWorker(null, false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;停止方法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;shutdown()：停止线程池&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池全局锁
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 设置线程池状态为 SHUTDOWN，如果线程池状态大于 SHUTDOWN，就不会设置直接返回
        advanceRunState(SHUTDOWN);
        // 中断空闲线程
        interruptIdleWorkers();
        // 空方法，子类可以扩展
        onShutdown(); 
    } finally {
        // 释放线程池全局锁
        mainLock.unlock();
    }
    tryTerminate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;interruptIdleWorkers()：shutdown 方法会&lt;strong&gt;中断所有空闲线程&lt;/strong&gt;，根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务，不会中断正在运行的线程，所以 shutdown 方法会让所有的任务执行完毕&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// onlyOne == true 说明只中断一个线程 ，false 则中断所有线程
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    / /持有全局锁
    mainLock.lock();
    try {
        // 遍历所有 worker
        for (Worker w : workers) {
            // 获取当前 worker 的线程
            Thread t = w.thread;
            // 条件一成立：说明当前迭代的这个线程尚未中断
            // 条件二成立：说明【当前worker处于空闲状态】，阻塞在poll或者take，因为worker执行task时是要加锁的
            //           每个worker有一个独占锁，w.tryLock()尝试加锁，加锁成功返回 true
            if (!t.isInterrupted() &amp;amp;&amp;amp; w.tryLock()) {
                try {
                    // 中断线程，处于 queue 阻塞的线程会被唤醒，进入下一次自旋，返回 null，执行退出相逻辑
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    // 释放worker的独占锁
                    w.unlock();
                }
            }
            // false，代表中断所有的线程
            if (onlyOne)
                break;
        }

    } finally {
        // 释放全局锁
        mainLock.unlock();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdownNow()：直接关闭线程池，不会等待任务执行完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public List&amp;lt;Runnable&amp;gt; shutdownNow() {
    // 返回值引用
    List&amp;lt;Runnable&amp;gt; tasks;
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池全局锁
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 设置线程池状态为STOP
        advanceRunState(STOP);
        // 中断线程池中【所有线程】
        interruptWorkers();
        // 从阻塞队列中导出未处理的task
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }

    tryTerminate();
    // 返回当前任务队列中 未处理的任务。
    return tasks;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tryTerminate()：设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final void tryTerminate() {
    for (;;) {
        // 获取 ctl 的值
        int c = ctl.get();
        // 线程池正常，或者有其他线程执行了状态转换的方法，当前线程直接返回
        if (isRunning(c) || runStateAtLeast(c, TIDYING) ||
            // 线程池是 SHUTDOWN 并且任务队列不是空，需要去处理队列中的任务
            (runStateOf(c) == SHUTDOWN &amp;amp;&amp;amp; ! workQueue.isEmpty()))
            return;
        
        // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空
        // 判断线程池中线程的数量
        if (workerCountOf(c) != 0) {
            // 【中断一个空闲线程】，在 queue.take() | queue.poll() 阻塞空闲
            // 唤醒后的线程会在getTask()方法返回null，
            // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
  // 池中的线程数量为 0 来到这里
        final ReentrantLock mainLock = this.mainLock;
        // 加全局锁
        mainLock.lock();
        try {
            // 设置线程池状态为 TIDYING 状态，线程数量为 0
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 结束线程池
                    terminated();
                } finally {
                    // 设置线程池状态为TERMINATED状态。
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 【唤醒所有调用 awaitTermination() 方法的线程】
                    termination.signalAll();
                }
                return;
            }
        } finally {
   // 释放线程池全局锁
            mainLock.unlock();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;Future&lt;/h4&gt;
&lt;h5&gt;线程使用&lt;/h5&gt;
&lt;p&gt;FutureTask 未来任务对象，继承 Runnable、Future 接口，用于包装 Callable 对象，实现任务的提交&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask&amp;lt;String&amp;gt; task = new FutureTask&amp;lt;&amp;gt;(new Callable&amp;lt;String&amp;gt;() {
        @Override
        public String call() throws Exception {
            return &quot;Hello World&quot;;
        }
    });
    new Thread(task).start(); //启动线程
    String msg = task.get(); //获取返回任务数据
    System.out.println(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public FutureTask(Callable&amp;lt;V&amp;gt; callable){
 this.callable = callable; // 属性注入
    this.state = NEW;    // 任务状态设置为 new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public FutureTask(Runnable runnable, V result) {
    // 适配器模式
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       
}
public static &amp;lt;T&amp;gt; Callable&amp;lt;T&amp;gt; callable(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    // 使用装饰者模式将 runnable 转换成 callable 接口，外部线程通过 get 获取
    // 当前任务执行结果时，结果可能为 null 也可能为传进来的值，【传进来什么返回什么】
    return new RunnableAdapter&amp;lt;T&amp;gt;(task, result);
}
static final class RunnableAdapter&amp;lt;T&amp;gt; implements Callable&amp;lt;T&amp;gt; {
    final Runnable task;
    final T result;
    // 构造方法
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        // 实则调用 Runnable#run 方法
        task.run();
        // 返回值为构造 FutureTask 对象时传入的返回值或者是 null
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h5&gt;成员属性&lt;/h5&gt;
&lt;p&gt;FutureTask 类的成员属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;任务状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示当前task状态
private volatile int state;
// 当前任务尚未执行
private static final int NEW          = 0;
// 当前任务正在结束，尚未完全结束，一种临界状态
private static final int COMPLETING   = 1;
// 当前任务正常结束
private static final int NORMAL       = 2;
// 当前任务执行过程中发生了异常，内部封装的 callable.run() 向上抛出异常了
private static final int EXCEPTIONAL  = 3;
// 当前任务被取消
private static final int CANCELLED    = 4;
// 当前任务中断中
private static final int INTERRUPTING = 5;
// 当前任务已中断
private static final int INTERRUPTED  = 6;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Callable&amp;lt;V&amp;gt; callable; // Runnable 使用装饰者模式伪装成 Callable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存储任务执行的结果&lt;/strong&gt;，这是 run 方法返回值是 void 也可以获取到执行结果的原因：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常情况下：任务正常执行结束，outcome 保存执行结果，callable 返回值
// 非正常情况：callable 向上抛出异常，outcome 保存异常
private Object outcome; 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行当前任务的线程对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile Thread runner; // 当前任务被线程执行期间，保存当前执行任务的线程对象引用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程阻塞队列的头节点&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 会有很多线程去 get 当前任务的结果，这里使用了一种数据结构头插头取（类似栈）的一个队列来保存所有的 get 线程
private volatile WaitNode waiters;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class WaitNode {
    // 单向链表
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;成员方法&lt;/h5&gt;
&lt;p&gt;FutureTask 类的成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FutureTask#run&lt;/strong&gt;：任务执行入口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    //条件一：成立说明当前 task 已经被执行过了或者被 cancel 了，非 NEW 状态的任务，线程就不需要处理了
    //条件二：线程是 NEW 状态，尝试设置当前任务对象的线程是当前线程，设置失败说明其他线程抢占了该任务，直接返回
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        // 执行到这里，当前 task 一定是 NEW 状态，而且【当前线程也抢占 task 成功】
        Callable&amp;lt;V&amp;gt; c = callable;
        // 判断任务是否为空，防止空指针异常；判断 state 状态，防止外部线程在此期间 cancel 掉当前任务
        // 【因为 task 的执行者已经设置为当前线程，所以这里是线程安全的】
        if (c != null &amp;amp;&amp;amp; state == NEW) {
            V result;
            // true 表示 callable.run 代码块执行成功 未抛出异常
            // false 表示 callable.run 代码块执行失败 抛出异常
            boolean ran;
            try {
    // 【调用自定义的方法，执行结果赋值给 result】
                result = c.call();
                // 没有出现异常
                ran = true;
            } catch (Throwable ex) {
                // 出现异常，返回值置空，ran 置为 false
                result = null;
                ran = false;
                // 设置返回的异常
                setException(ex);
            }
            // 代码块执行正常
            if (ran)
                // 设置返回的结果
                set(result);
        }
    } finally {
        // 任务执行完成，取消线程的引用，help GC
        runner = null;
        int s = state;
        // 判断任务是不是被中断
        if (s &amp;gt;= INTERRUPTING)
            // 执行中断处理方法
            handlePossibleCancellationInterrupt(s);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#set：设置正常返回值，首先将任务状态设置为 COMPLETING 状态代表完成中，逻辑执行完设置为 NORMAL 状态代表任务正常执行完成，最后唤醒 get() 阻塞线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void set(V v) {
    // CAS 方式设置当前任务状态为完成中，设置失败说明其他线程取消了该任务
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【将结果赋值给 outcome】
        outcome = v;
        // 将当前任务状态修改为 NORMAL 正常结束状态。
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#setException：设置异常返回值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 赋值给返回结果，用来向上层抛出来的异常
        outcome = t;
        // 将当前任务的状态 修改为 EXCEPTIONAL
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#finishCompletion：&lt;strong&gt;唤醒 get() 阻塞线程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void finishCompletion() {
    // 遍历所有的等待的节点，q 指向头节点
    for (WaitNode q; (q = waiters) != null;) {
        // 使用cas设置 waiters 为 null，防止外部线程使用cancel取消当前任务，触发finishCompletion方法重复执行
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            // 自旋
            for (;;) {
                // 获取当前 WaitNode 节点封装的 thread
                Thread t = q.thread;
                // 当前线程不为 null，唤醒当前 get() 等待获取数据的线程
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                // 获取当前节点的下一个节点
                WaitNode next = q.next;
                // 当前节点是最后一个节点了
                if (next == null)
                    break;
                // 断开链表
                q.next = null; // help gc
                q = next;
            }
            break;
        }
    }
    done();
    callable = null; // help GC
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#handlePossibleCancellationInterrupt：任务中断处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void handlePossibleCancellationInterrupt(int s) {
    if (s == INTERRUPTING)
        // 中断状态中
        while (state == INTERRUPTING)
            // 等待中断完成
            Thread.yield();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FutureTask#get&lt;/strong&gt;：获取任务执行的返回值，执行 run 和 get 的不是同一个线程，一般有多个线程 get，只有一个线程 run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public V get() throws InterruptedException, ExecutionException {
    // 获取当前任务状态
    int s = state;
    // 条件成立说明任务还没执行完成
    if (s &amp;lt;= COMPLETING)
        // 返回 task 当前状态，可能当前线程在里面已经睡了一会
        s = awaitDone(false, 0L);
    return report(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#awaitDone：&lt;strong&gt;get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    // 0 不带超时
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 引用当前线程，封装成 WaitNode 对象
    WaitNode q = null;
    // 表示当前线程 waitNode 对象，是否进入阻塞队列
    boolean queued = false;
    // 【三次自旋开始休眠】
    for (;;) {
        // 判断当前 get() 线程是否被打断，打断返回 true，清除打断标记
        if (Thread.interrupted()) {
            // 当前线程对应的等待 node 出队，
            removeWaiter(q);
            throw new InterruptedException();
        }
  // 获取任务状态
        int s = state;
        // 条件成立说明当前任务执行完成已经有结果了
        if (s &amp;gt; COMPLETING) {
            // 条件成立说明已经为当前线程创建了 WaitNode，置空 help GC
            if (q != null)
                q.thread = null;
            // 返回当前的状态
            return s;
        }
        // 条件成立说明当前任务接近完成状态，这里让当前线程释放一下 cpu ，等待进行下一次抢占 cpu
        else if (s == COMPLETING) 
            Thread.yield();
        // 【第一次自旋】，当前线程还未创建 WaitNode 对象，此时为当前线程创建 WaitNode对象
        else if (q == null)
            q = new WaitNode();
        // 【第二次自旋】，当前线程已经创建 WaitNode 对象了，但是node对象还未入队
        else if (!queued)
            // waiters 指向队首，让当前 WaitNode 成为新的队首，【头插法】，失败说明其他线程修改了新的队首
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
        // 【第三次自旋】，会到这里，或者 else 内
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos &amp;lt;= 0L) {
                removeWaiter(q);
                return state;
            }
            // 阻塞指定的时间
            LockSupport.parkNanos(this, nanos);
        }
        // 条件成立：说明需要阻塞
        else
            // 【当前 get 操作的线程被 park 阻塞】，除非有其它线程将唤醒或者将当前线程中断
            LockSupport.park(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FutureTask#report：封装运行结果，可以获取 run() 方法中设置的成员变量 outcome，&lt;strong&gt;这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private V report(int s) throws ExecutionException {
    // 获取执行结果，是在一个 futuretask 对象中的属性，可以直接获取
    Object x = outcome;
    // 当前任务状态正常结束
    if (s == NORMAL)
        return (V)x; // 直接返回 callable 的逻辑结果
    // 当前任务被取消或者中断
    if (s &amp;gt;= CANCELLED)
        throw new CancellationException();  // 抛出异常
    // 执行到这里说明自定义的 callable 中的方法有异常，使用 outcome 上层抛出异常
    throw new ExecutionException((Throwable)x); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FutureTask#cancel：任务取消，打断正在执行该任务的线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean cancel(boolean mayInterruptIfRunning) {
    // 条件一：表示当前任务处于运行中或者处于线程池任务队列中
    // 条件二：表示修改状态，成功可以去执行下面逻辑，否则返回 false 表示 cancel 失败
    if (!(state == NEW &amp;amp;&amp;amp;
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                                   mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {
        // 如果任务已经被执行，是否允许打断
        if (mayInterruptIfRunning) {
            try {
                // 获取执行当前 FutureTask 的线程
                Thread t = runner;
                if (t != null)
                    // 打断执行的线程
                    t.interrupt();
            } finally {
                // 设置任务状态为【中断完成】
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        // 唤醒所有 get() 阻塞的线程
        finishCompletion();
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;任务调度&lt;/h3&gt;
&lt;h4&gt;Timer&lt;/h4&gt;
&lt;p&gt;Timer 实现定时功能，Timer 的优点在于简单易用，但由于所有任务都是由同一个线程来调度，因此所有任务都是串行执行的，同一时间只能有一个任务在执行，前一个任务的延迟或异常都将会影响到之后的任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void method1() {
    Timer timer = new Timer();
    TimerTask task1 = new TimerTask() {
        @Override
        public void run() {
            System.out.println(&quot;task 1&quot;);
            //int i = 1 / 0;//任务一的出错会导致任务二无法执行
            Thread.sleep(2000);
        }
    };
    TimerTask task2 = new TimerTask() {
        @Override
        public void run() {
            System.out.println(&quot;task 2&quot;);
        }
    };
    // 使用 timer 添加两个任务，希望它们都在 1s 后执行
 // 但由于 timer 内只有一个线程来顺序执行队列中的任务，因此任务1的延时，影响了任务2的执行
    timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1
    timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;Scheduled&lt;/h4&gt;
&lt;p&gt;任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用内部类 ScheduledFutureTask 封装任务&lt;/li&gt;
&lt;li&gt;使用内部类 DelayedWorkQueue 作为线程池队列&lt;/li&gt;
&lt;li&gt;重写 onShutdown 方法去处理 shutdown 后的任务&lt;/li&gt;
&lt;li&gt;提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法，以便开发者进行扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造方法：&lt;code&gt;Executors.newScheduledThreadPool(int corePoolSize)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 最大线程数固定为 Integer.MAX_VALUE，保活时间 keepAliveTime 固定为 0
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          // 阻塞队列是 DelayedWorkQueue
          new DelayedWorkQueue());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用 API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; schedule(Runnable/Callable&amp;lt;V&amp;gt;, long delay, TimeUnit u)&lt;/code&gt;：延迟执行任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; scheduleAtFixedRate(Runnable/Callable&amp;lt;V&amp;gt;, long initialDelay, long period, TimeUnit unit)&lt;/code&gt;：定时执行周期任务，不考虑执行的耗时，参数为初始延迟时间、间隔时间、单位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScheduledFuture&amp;lt;?&amp;gt; scheduleWithFixedDelay(Runnable/Callable&amp;lt;V&amp;gt;, long initialDelay, long delay, TimeUnit unit)&lt;/code&gt;：定时执行周期任务，考虑执行的耗时，参数为初始延迟时间、间隔时间、单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;延迟任务，但是出现异常并不会在控制台打印，也不会影响其他线程的执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    // 线程池大小为1时也是串行执行
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    // 添加两个任务，都在 1s 后同时执行
    executor.schedule(() -&amp;gt; {
     System.out.println(&quot;任务1，执行时间：&quot; + new Date());
        //int i = 1 / 0;
     try { Thread.sleep(2000); } catch (InterruptedException e) { }
    }, 1000, TimeUnit.MILLISECONDS);
    
    executor.schedule(() -&amp;gt; {
     System.out.println(&quot;任务2，执行时间：&quot; + new Date());
    }, 1000, TimeUnit.MILLISECONDS);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 scheduleAtFixedRate：&lt;strong&gt;一次任务的启动到下一次任务的启动&lt;/strong&gt;之间只要大于等于间隔时间，抢占到 CPU 就会立即执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    System.out.println(&quot;start...&quot; + new Date());
    
    pool.scheduleAtFixedRate(() -&amp;gt; {
        System.out.println(&quot;running...&quot; + new Date());
        Thread.sleep(2000);
    }, 1, 1, TimeUnit.SECONDS);
}

/*start...Sat Apr 24 18:08:12 CST 2021
running...Sat Apr 24 18:08:13 CST 2021
running...Sat Apr 24 18:08:15 CST 2021
running...Sat Apr 24 18:08:17 CST 2021
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时任务 scheduleWithFixedDelay：&lt;strong&gt;一次任务的结束到下一次任务的启动之间&lt;/strong&gt;等于间隔时间，抢占到 CPU 就会立即执行，这个方法才是真正的设置两个任务之间的间隔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
    System.out.println(&quot;start...&quot; + new Date());
    
    pool.scheduleWithFixedDelay(() -&amp;gt; {
        System.out.println(&quot;running...&quot; + new Date());
        Thread.sleep(2000);
    }, 1, 1, TimeUnit.SECONDS);
}
/*start...Sat Apr 24 18:11:41 CST 2021
running...Sat Apr 24 18:11:42 CST 2021
running...Sat Apr 24 18:11:45 CST 2021
running...Sat Apr 24 18:11:48 CST 2021
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;成员属性&lt;/h4&gt;
&lt;h5&gt;成员变量&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;shutdown 后是否继续执行周期任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile boolean continueExistingPeriodicTasksAfterShutdown;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shutdown 后是否继续执行延迟任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消方法是否将该任务从队列中移除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 默认 false，不移除，等到线程拿到任务之后抛弃
private volatile boolean removeOnCancel = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务的序列号，可以用来比较优先级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final AtomicLong sequencer = new AtomicLong();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;延迟任务&lt;/h5&gt;
&lt;p&gt;ScheduledFutureTask 继承 FutureTask，实现 RunnableScheduledFuture 接口，具有延迟执行的特点，覆盖 FutureTask 的 run 方法来实现对&lt;strong&gt;延时执行、周期执行&lt;/strong&gt;的支持。对于延时任务调用 FutureTask#run，而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列&lt;/p&gt;
&lt;p&gt;在调度线程池中无论是 runnable 还是 callable，无论是否需要延迟和定时，所有的任务都会被封装成 ScheduledFutureTask&lt;/p&gt;
&lt;p&gt;成员变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;任务序列号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final long sequenceNumber;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private long time;   // 任务可以被执行的时间，交付时间，以纳秒表示
private final long period; // 0 表示非周期任务，正数表示 fixed-rate 模式的周期，负数表示 fixed-delay 模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fixed-rate：两次开始启动的间隔，fixed-delay：一次执行结束到下一次开始启动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实际的任务对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RunnableScheduledFuture&amp;lt;V&amp;gt; outerTask = this;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务在队列数组中的索引下标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DelayedWorkQueue 底层使用的数据结构是最小堆，记录当前任务在堆中的索引，-1 代表删除
int heapIndex;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成员方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    // 任务的触发时间
    this.time = ns;
    // 任务的周期，多长时间执行一次
    this.period = period;
    // 任务的序号
    this.sequenceNumber = sequencer.getAndIncrement();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;compareTo()：ScheduledFutureTask 根据执行时间 time 正序排列，如果执行时间相同，在按照序列号 sequenceNumber 正序排列，任务需要放入 DelayedWorkQueue，延迟队列中使用该方法按照从小到大进行排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        // 类型强转
        ScheduledFutureTask&amp;lt;?&amp;gt; x = (ScheduledFutureTask&amp;lt;?&amp;gt;)other;
        // 比较者 - 被比较者的执行时间
        long diff = time - x.time;
        // 比较者先执行
        if (diff &amp;lt; 0)
            return -1;
        // 被比较者先执行
        else if (diff &amp;gt; 0)
            return 1;
        // 比较者的序列号小
        else if (sequenceNumber &amp;lt; x.sequenceNumber)
            return -1;
        else
            return 1;
    }
    // 不是 ScheduledFutureTask 类型时，根据延迟时间排序
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff &amp;lt; 0) ? -1 : (diff &amp;gt; 0) ? 1 : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;run()：执行任务，非周期任务直接完成直接结束，&lt;strong&gt;周期任务执行完后会设置下一次的执行时间，重新放入线程池的阻塞队列&lt;/strong&gt;，如果线程池中的线程数量少于核心线程，就会添加 Worker 开启新线程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void run() {
    // 是否周期性，就是判断 period 是否为 0
    boolean periodic = isPeriodic();
    // 根据是否是周期任务检查当前状态能否执行任务，不能执行就取消任务
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 非周期任务，直接调用 FutureTask#run 执行
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 周期任务的执行，返回 true 表示执行成功
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 设置周期任务的下一次执行时间
        setNextRunTime();
        // 任务的下一次执行安排，如果当前线程池状态可以执行周期任务，加入队列，并开启新线程
        reExecutePeriodic(outerTask);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;周期任务正常完成后&lt;strong&gt;任务的状态不会变化&lt;/strong&gt;，依旧是 NEW，不会设置 outcome 属性。但是如果本次任务执行出现异常，会进入 setException 方法将任务状态置为异常，把异常保存在 outcome 中，方法返回 false，后续的该任务将不会再周期的执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected boolean runAndReset() {
    // 任务不是新建的状态了，或者被别的线程执行了，直接返回 false
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        Callable&amp;lt;V&amp;gt; c = callable;
        if (c != null &amp;amp;&amp;amp; s == NEW) {
            try {
                // 执行方法，没有返回值
                c.call();
                ran = true;
            } catch (Throwable ex) {
                // 出现异常，把任务设置为异常状态，唤醒所有的 get 阻塞线程
                setException(ex);
            }
        }
    } finally {
  // 执行完成把执行线程引用置为 null
        runner = null;
        s = state;
        // 如果线程被中断